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
Accepting sync and async callbacks while keeping yargs functions sync-only is incoherent #1420
Comments
Async middlewares would have to be taken into account as well in the conception. |
Where are promises currently used in Yargs?
|
@bcoe @gajus A beginning of proposal to discuss: General approachWrapping maybe promise handling codeSeveral of our functions would need to work with "maybe promise" (name already used by our current "is-promise" module), ie inputs which are either promises, or not. To avoid adding tests everywhere, and/or falling into a callback hell (we have no async in node 6, remember #1422), and to change as less code as possible, I suggest wrapping our code dealing with maybe promises with something like this: function wrapMaybePromise(maybePromise, resolveHandler, rejectHandler) {
return isPromise(maybePromise)
? maybePromise.then(resolveHandler, rejectHandler)
: resolveHandler(maybePromise)
} This would help us turn easily sync-only parts of our code, for example: innerArgv = innerYargs._parseArgs(null, null, true, commandIndex)
aliases = innerYargs.parsed.aliases
// [...]
if (!yargs._hasOutput()) {
positionalMap = populatePositionals(commandHandler, innerArgv, currentContext, yargs)
}
const middlewares = globalMiddleware.slice(0).concat(commandHandler.middlewares || [])
applyMiddleware(innerArgv, yargs, middlewares, true)
// [...]
return innerArgv into maybe-promise-compatible code: innerArgv = innerYargs._parseArgs(null, null, true, commandIndex)
return wrapMaybePromise(innerArgv, (innerArgv) => {
aliases = innerYargs.parsed.aliases
// [...]
if (!yargs._hasOutput()) {
positionalMap = populatePositionals(commandHandler, innerArgv, currentContext, yargs)
}
const middlewares = globalMiddleware.slice(0).concat(commandHandler.middlewares || [])
applyMiddleware(innerArgv, yargs, middlewares, true)
// [...]
return innerArgv
}) This would also help us simplify some of our already boilerplate-code, such as: innerArgv = applyMiddleware(innerArgv, yargs, middlewares, false)
let handlerResult
if (isPromise(innerArgv)) {
handlerResult = innerArgv.then(argv => commandHandler.handler(argv))
} else {
handlerResult = commandHandler.handler(innerArgv)
}
if (isPromise(handlerResult)) {
// [...]
} into: innerArgv = applyMiddleware(innerArgv, yargs, middlewares, false)
handlerResult = wrapMaybePromise(innerArgv, argv => commandHandler.handler(argv))
if (isPromise(handlerResult)) {
// [...]
} Propagating maybe promisesAny function having maybe promises as input should output a maybe promise. This would ensure:
while keeping an unchanged API for sync-only applications (except for showHelp(), which would non longer be chainable, but had no reason to IMHO). To implement this, we should start from the identified maybe promise inputs (see "Where are promises currently used in Yargs?" above), and identify their propagation tree through functions, down to the API. This will give us the list of all impacted functions to modify (including tests and documentation). This is going to be my next work ;-) |
Here is the maybe promises propagation tree:
Synthesis:
Waiting for your feedback now before going ahead ;-) |
@mleguen I will make an effort to give feedback this weekend, appreciate the deep dive. |
at a glance, I think this is sounding like a well thought out proposal, could you provide some pseudo-code of what the API would look like, as an example I'm wondering if:
Would mean that I like this idea, it would be a breaking change worth making. |
@bcoe you are right, I forgot to add an example of how to used the proposed API. I like your idea of "as soon as a promise is introduced anywhere, the resolution becomes async in general", but however I see this is not what I proposed leads us to. Take a little bit more complex example:
In your first example, depending on wether the command handler is called or not, |
Please consider adding These will better support static code analysis and editor tooling. They will be strictly opt-in; developers are free to use the existing methods and fields synchronously as outlined in this proposal. No one will be forced to use the
|
@cspotcode Instead of sync variants of affected functions, would an option to prevent returning a promise do the job for you? |
This should answer @bcoe 's question about examples of the new API in use. @cspotcode , I still have to add an example with the requested behavior to prevent returning a promise. Future API usage examplesSync-onlySync-only applications would not be impacted: try {
const argv = yargs
.command('syncCmdSuccess', 'description', () => {}, () => {})
.command('syncCmdError', 'description', () => {}, () => {
throw new Error()
})
.parse()
// The syntax was incorrect or the selected command handler returned a value
doWatheverYouWantWithParsingResults(argv)
} catch(err) {
// The selected command handler threw
dealWithHandlerErrors(err)
} Async-onlyAsync-only applications would now be possible: With async/awaitStraightforward with async/await: try {
const argv = await yargs
.command('asyncCmdSuccess', 'description', () => {}, () => Promise(
(resolve) => setTimeout(() => resolve(), 500)
))
.command('asyncCmdError', 'description', () => {}, () => Promise(
(_, reject) => setTimeout(() => reject(), 500)
))
.parse()
// The syntax was incorrect or the selected command handler promise was resolved
doWatheverYouWantWithParsingResults(argv)
} catch(err) {
// The selected command handler promise was rejected
dealWithHandlerErrors(err)
} Without async/awaitNot that difficult without async/await: const argv = yargs
.command('asyncCmdSuccess', 'description', () => {}, () => Promise(
(resolve) => setTimeout(() => resolve(), 500)
))
.command('asyncCmdError', 'description', () => {}, () => Promise(
(_, reject) => setTimeout(() => reject(), 500)
))
.parse()
.then(
// The syntax was incorrect or the selected command handler promise was resolved
doWatheverYouWantWithParsingResults,
// The selected command handler promise was rejected
dealWithHandlerErrors
) Sync-async mixWith async/awaitStraightforward as usual with async/await: try {
const argv = await yargs
.command('asyncCmdSuccess', 'description', () => {}, () => Promise(
(resolve) => setTimeout(() => resolve(), 500)
))
.command('asyncCmdError', 'description', () => {}, () => Promise(
(_, reject) => setTimeout(() => reject(), 500)
))
.command('syncCmdSuccess', 'description', () => {}, () => {})
.command('syncCmdError', 'description', () => {}, () => {
throw new Error()
})
.parse()
// The syntax was incorrect or the selected command handler returned a value or a promise which is now resolved
doWatheverYouWantWithParsingResults(argv)
} catch(err) {
// The selected command handler threw or returned a promise which is now rejected
dealWithHandlerErrors(err)
} Without async/awaitA bit more complex without async/await, but still clean: const argv = new Promise(resolve => resolve(
yargs
.command('asyncCmdSuccess', 'description', () => {}, () => Promise(
(resolve) => setTimeout(() => resolve(), 500)
))
.command('asyncCmdError', 'description', () => {}, () => Promise(
(_, reject) => setTimeout(() => reject(), 500)
))
.command('syncCmdSuccess', 'description', () => {}, () => {})
.command('syncCmdError', 'description', () => {}, () => {
throw new Error()
})
.parse()
))
.then(
// The syntax was incorrect or the selected command handler returned a value or a promise which is now resolved
doWatheverYouWantWithParsingResults,
// The selected command handler threw or returned a promise which is now rejected
dealWithHandlerErrors
) The additional Promise layer here is used to turn the argv array, the promise of argv array or the exception resulting from parse() call into a promise of argv array, to handle its resolution or rejection in a single point. |
@cspotcode Here is what I propose for sync applications not wanting callbacks to return promises: Future API usage examples (follow-up)Sync-async mix with sync-only optiontry {
const argv = yargs
.command('asyncCmdSuccess', 'description', () => {}, () => Promise(
(resolve) => setTimeout(() => resolve(), 500)
))
.command('asyncCmdError', 'description', () => {}, () => Promise(
(_, reject) => setTimeout(() => reject(), 500)
))
.command('syncCmdSuccess', 'description', () => {}, () => {})
.command('syncCmdError', 'description', () => {}, () => {
throw new Error()
})
.parseSync()
// The syntax was incorrect or the selected command handler returned a value
doWatheverYouWantWithParsingResults(argv)
} catch(err) {
if (err instanceof YAsyncError) {
// The selected command handler returned a promise
dealWithHandlerReturningAPromiseWhenNotAllowed(err)
} else {
// The selected command handler threw
dealWithHandlerErrors(err)
}
} EDIT: V2 taking @cspotcode 's concerns about IDE support into account. |
The issue is supporting editor tooling and code completion. Using dynamic
flags and toggles to change function signatures generally doesn't work,
because the editor can't understand it without super-complicated type
declarations. Using different function names is a simpler approach,
because each function maintains a consistent signature.
…On Wed, Sep 25, 2019, 5:49 AM Mael Le Guen ***@***.***> wrote:
@cspotcode <https://github.com/cspotcode> Here is what I propose for sync
applications not wanting callbacks to return promises:
Future API usage examples (follow-up) Sync-async mix with sync-only option
try {
const argv = yargs
.async(false)
.command('asyncCmdSuccess', 'description', () => {}, () => Promise(
(resolve) => setTimeout(() => resolve(), 500)
))
.command('asyncCmdError', 'description', () => {}, () => Promise(
(_, reject) => setTimeout(() => reject(), 500)
))
.command('syncCmdSuccess', 'description', () => {}, () => {})
.command('syncCmdError', 'description', () => {}, () => {
throw new Error()
})
.parse()
// The syntax was incorrect or the selected command handler returned a value
doWatheverYouWantWithParsingResults(argv)
} catch(err) {
if (err instanceof YAsyncError) {
// The selected command handler returned a promise
dealWithHandlerReturningAPromiseWhenNotAllowed(err)
} else {
// The selected command handler threw
dealWithHandlerErrors(err)
}
}
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
<#1420?email_source=notifications&email_token=AAC35OCD2L6UXG2EQGSWSCDQLMXZVA5CNFSM4ITPDSE2YY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGOD7RJUOY#issuecomment-534944315>,
or mute the thread
<https://github.com/notifications/unsubscribe-auth/AAC35OFYPXJSVQTPLXZYJ63QLMXZVANCNFSM4ITPDSEQ>
.
|
@cspotcode I edited my example above to add a If there are no other suggestions/objections, I will start working on this next week. |
@mleguen looks good to me, thanks! |
To stay coherent with @cspotcode 's request to add a parseSync function, I am also adding a parseAsync function, which will always return a promise (easier to handle for users not yet using async/await). The general recommendations would become: use parseAsync in most cases, use parseSync if you absolutely need to stay sync. And we could imagine deprecating parse in a future version. |
As node 6 support is going to be dropped in the next major version (#1438 ), we no longer need all the stuff about "maybePromises" described above. I will use async/await instead. |
The work in #1876 should address the issues raised in this thread. It can currently be installed via |
This issue is a follow-up of the discusion with @bcoe and @gajus in #1412
Example with command handlers:
parse()
andshowHelp()
(and maybe other functions as well?) when an async handler rejectparse()
andshowHelp()
are sync, the calling program has no way to know if and when such an error will occurparse()
,showHelp()
, etc.) should return a promise, to be resolved/rejected after the handler completes, for the calling program to know what is happeningBefore opening any PR, please detail in this issue the intended conception for review, and do not limit it to the command handler example detailed here (there may be other cases of async input).
The text was updated successfully, but these errors were encountered: