Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add parseAsync #1118

Merged
merged 4 commits into from Jan 5, 2020
Merged

Add parseAsync #1118

merged 4 commits into from Jan 5, 2020

Conversation

shadowspawn
Copy link
Collaborator

Pull Request

Problem

People are using async routines from TypeScript and Javascript, and want to declare async action handlers. For the caller to properly wind up the async calls, Commander needs to expose something to allow handling the asynchronous calls.

See #806

Solution

Add a parseAsync() call. It calls the normal parse, but then returns a Promise which includes the possibly async action handler. The client can then deal with the Promise in the normal way.

It is ok to have a mixture of async and non-async action handlers. Calling parseAsync works with either. Basically, if you have any async action handlers then you should call parseAsync instead of parse.

With this TypeScript code:

import * as commander from 'commander';

function sleep(ms: number) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

async function run() {
  console.log('run+');
  await sleep(2000);
  console.log('run-');
}

const program = new commander.Command();
program
  .command("wait")
  .action(run);


program.parseAsync(['node', 'foo', 'wait']).then(() => {
  console.log('parseAsync success');
}).catch(err => {
  console.log('parseAsync failed');
});

console.log('Reached end of program');

See this:

$ npx tsc async.ts && node async.js
run+
Reached end of program
run-
parseAsync success

@shadowspawn
Copy link
Collaborator Author

shadowspawn commented Dec 11, 2019

To do:

  • add tests
  • investigate compatibility with node 6
  • do some testing from JavaScript (initial development used TypeScript)
  • README

Note: I am not a Promise expert. If there are bad patterns or assumptions in this code, then please let me know!

@jdmarshall
Copy link

jdmarshall commented Dec 11, 2019

I think I agree that the problem traces to the end of the event listener in action(), but if I back up for a second I have two other concerns.

The simplest one is, wouldn't it be simpler to register a new event that is emitted after fn.apply() resolves/returns.
The harder question is why action.apply() in an event handler in the first place? That makes propagating errors (and actions will have errors, especially when you are writing them!) much more problematic.

I know I wrote an async action. If you give me an event to wait for, I can promisify it in a couple lines of code and only call process.exit() after it resolves, and I think I'd be perfectly okay with that. In fact if I have multiple commands, which I do, that will still result in simpler code than my weird setInterval workaround.

@shadowspawn
Copy link
Collaborator Author

shadowspawn commented Dec 12, 2019

I think we have two different problems @jdmarshall.

I was focused on caller being able to manage async action handlers, and (say) await actions being completing before proceeding with other code. My intention was to make it possible and the rest was up to the caller.

Your problem (with setInterval workaround) is presumably that a pending promise alone does not keep node running, which is potentially unexpected:

I didn't encounter the early-exit problem because I was using setTimeout as my async example. I am not sure if it is up to Commander to keep node running forever, but it might be reasonable to keep it running for some default period (like say 10s, which is the default timeout on docker stop).

@shadowspawn
Copy link
Collaborator Author

shadowspawn commented Dec 12, 2019

@jdmarshall Do you have a small example of a problem async action that would not run correctly for you if you could use await, without additional timer? (i.e. what is the code doing asynchronously that node does not recognise will resolve later?)

@jdmarshall
Copy link

Not at present. What drew me here was that I had an async action that seemed to be working fine, performing a lot of async calls (throttled uploads), and then I added a second async task to the end of that action and it never ran.

I haven't been back to it yet to come up with a repro case.

@shadowspawn
Copy link
Collaborator Author

Here is a javascript example for node 8 or up with await. Lots of logging!

const program = require('commander');

function sleep(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

async function doSomethingAsync() {
  console.log('doSomethingAsync+');
  await sleep(1000);
  console.log('doSomethingAsync-');
}

async function main() {
  console.log('main+');
  program
    .command('example')
    .action(doSomethingAsync);
  console.log('Calling parseAsync');
  const promise = program.parseAsync(['node', 'script', 'example']);
  console.log('Working before waiting');
  await promise;
  console.log('main-');
}

console.log('start program');
main().then(() => {
  console.log('main succeeded');
}).catch(() => {
  console.log('main failed');
})
console.log('finish program');
$ node example.js
start program
main+
Calling parseAsync
doSomethingAsync+
Working before waiting
finish program
doSomethingAsync-
main-
main succeeded

Copy link

@blendsdk blendsdk left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have tested this PR and it works for me. I am on node v.10.14.2.
I am running async actions against a lengthy database update script.

@shadowspawn
Copy link
Collaborator Author

Thanks @blendsdk

@blendsdk blendsdk mentioned this pull request Dec 29, 2019
Some noise in TOC due to changes in plugin which maintains TOC.
@shadowspawn shadowspawn changed the title [WIP] Add parseAsync Add parseAsync Dec 30, 2019
@shadowspawn shadowspawn marked this pull request as ready for review December 30, 2019 23:05
@shadowspawn
Copy link
Collaborator Author

Added README. Have a test and typings.
Moving out of draft.

This is a non-breaking addition so can go in v4, but I am happy to leave it for v5 if preferred.

@shadowspawn
Copy link
Collaborator Author

This is the only PR I am interested in including before v4.1, but happy to leave it for v5 whether for safety or to allow more time to review @abetomo . No worries.

Copy link
Collaborator

@abetomo abetomo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

@shadowspawn shadowspawn merged commit 7bcf117 into tj:develop Jan 5, 2020
@shadowspawn
Copy link
Collaborator Author

Thanks @abetomo. I'll start preparing 4.1 soon.

@shadowspawn shadowspawn added the pending release Merged into a branch for a future release, but not released yet label Jan 5, 2020
@shadowspawn shadowspawn deleted the feature/async branch January 5, 2020 07:16
@shadowspawn
Copy link
Collaborator Author

CHANGELOG updated for 4.1 on develop branch. Ready to release to master, version number and tag still to be updated.

npm version minor

I'll add an issue for last change to Chinese README but we can add that after release.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants