Uh oh!
There was an error while loading. Please reload this page.
lib: add Timer/Immediate to promisifyed setTimer/setImmediate - #29521
lib: add Timer/Immediate to promisifyed setTimer/setImmediate#29521everett1992 wants to merge 1 commit into
Conversation
The current implementation of the promisified setTimeout and setImmediate does not make the Timeout or Immediate available, so they cannot be canceled or unreffed. The docs and code follow the pattern established by child_process - lib/child_process.js#L150-L171 - doc/api/child_process.md#L217-L222
Frankly, if you think you might ever need to do a cancel, then Promises are just the wrong abstraction. If you await such a thing, what is the expected result? An error? Cancelable promises don't exist so the base expectations are not even clear. |
everett1992
commented
Sep 11, 2019
I wrote this PR because I wrote some code like this If you can't cancel the timeout then deadline will always resolve and throw an error, even when doWork completes first. With this proposal you can write |
devsnek
commented
Sep 11, 2019
to be fair it doesn't really matter if it rejects, race will already have resolved. |
joyeecheung
commented
Sep 11, 2019
An alternative for implementing cancellation might be implementing the AbortController API by accepting an AbortSignal in |
jasnell
commented
Sep 11, 2019
Going the An alternative approach could be to attach an // In this option, the AbortController would abort all Promises// created by the sleep function.constsleep=util.promisify(setTimeout)sleep(10)sleep(20)sleep.abort()// cancel both sleep promises// orsleep.abortController.abort()// cancel both sleep promisesOr... constsleep=util.promisify(setTimeout)constabortable1=sleep(10)constabortable2=sleep(20)abortable1.abort()abortable2.abort()In the second example, the Promise returned by sleep would essentially implement the Obviously the second option has the downside of extending the Promise API which would cause problems down the road if TC39 decides to add it's own abort or cancel method. Bottom line, however, is that the promisified function should provide a way of allowing the user to discover if the Promise is abortable as opposed to the user just passing in an |
joyeecheung
commented
Sep 11, 2019
Since we implement |
jasnell
commented
Sep 11, 2019
that could work also but ends up with the requirement that letfn;try{fn=util.promisify(otherFn,abortController)// cancelable}catch{// not cancelable}vs. constfn=util.promisify(otherFn)if(typeoffn.abort==='function'){// cancelable}else{// cancelable}Stylistically I prefer the latter over the former. |
everett1992
commented
Sep 11, 2019
It matters if the |
This discussion makes me question the point of having custom util.promisify in the core library, instead of adding promise native functions to the libraries, like fs/promises. Then users don't have to question if a util.promisify'ed function supports abort controllers or not. That said, users know if util.promisify supports an abort controller by reading the documentation, or thru static types. child_process has a better use case for exposing the |
We could make Timeout objects "thenable", where an error in the callback bubbles to a constdeadline=util.promisify(setTimeout)(TIMEOUT);deadline.then(()=>{throwError('Task took too long');});consttask=doWork();promise.race([task,deadline]);Then likely becomes constdeadline=setTimeout(TIMEOUT,()=>{throwError('Task took too long');});consttask=doWork();promise.race([task,deadline]).finally(()=>{clearTimeout(deadline);}) |
This comment has been minimized.
This comment has been minimized.
jasnell
commented
Sep 16, 2019
+1 to that @Fishrock123 ... Making the timeout object a thenable makes a lot of sense. The on clearTimeout(), the thenable can reject if it hasn't already resolved. Makes for a nice elegant solution. Just thinking about asyncfoo(){constinterval=setInterval(fn,1000)forawait(constmofinterval){/* ... */clearInterval(interval)// would end the iterator...}) |
Fishrock123
commented
Sep 16, 2019
@everett1992 yes, I will correct that. Doing that also makes the async iterator story more streamlined, since both could just be directly on the timeout object. |
devsnek
commented
Sep 16, 2019
@nodejs/open-standards btw |
Uh oh!
There was an error while loading. Please reload this page.
devsnek
commented
Sep 16, 2019
would be nice to see if its possible to say in sync with the dom too: whatwg/html#617 |
Fishrock123
commented
Sep 17, 2019
The dom has a bunch of problem which we frankly don't have (because someone had a good idea and returned a timer object rather than a number). I don't think we should follow them, it's very hard to trust a good api design to come out of that, considering what the dom wants compared to us. |
@Fishrock123 i'm not saying we have to, but i think we should at least explore it. I'm planning to work on the dom api some time in the nearish future, so it shouldn't be too hard to keep node in the loop. |
everett1992
commented
Sep 17, 2019
for what it's worth the DOM issue discusses adding a new function, not extending setTimeout. This feels like an opportunity to design a good api for dom and node. |
Fishrock123
commented
Sep 17, 2019
I have written my thoughts into a somewhat long post over on the whatwg thread: whatwg/html#617 (comment) |
mcollina
commented
Sep 17, 2019
I'm in favor of using thenables and an async iterators. |
jasnell
commented
Sep 17, 2019
Looks like the response on the other thread @Fishrock123 points to is that the whatwg approach is going to use AbortController no matter what. We can go with that approach also as an option, but it will be difficult to implement consistently, so I'm definitely still in favor of making the object returned by |
benjamingr
left a comment
There was a problem hiding this comment.
Thanks a lot for doing this and for taking a stab at it. I agree with the other comments that this is rather footfun-y :/
devsnek
commented
Sep 17, 2019
@jasnell there is also work at tc39 to standardize a cancellation api that would drop in with AbortController and could therefore interoperate with node. All I'm asking is that we slow down a little bit here :) I'm going to be working on async dom timers at some point in the coming months and as someone who also maintains node I'm inclined to at least try to match up the apis. |
jasnell
commented
Sep 17, 2019
@devnek ... no worries on being patient on it. I'm hesitant to do anything on cancelable promises until there's an official direction on it anyway. That said, would there be harm in experimenting with a couple of different approaches if those are clearly marked experimental? For instance, code such as the following could easily emit a process warning... constsleep=setTimeout(fn,100);sleep.catch(/* ... */)clearTimeout(sleep) |
Fishrock123
commented
Sep 17, 2019
@devsnek Fwiw, as noted in my post to whatwg, cancellation does not cover all use cases for persistents. |
benjamingr
commented
Sep 18, 2019
I would be fine however with supporting |
@Fishrock123 just to be clear, we cannot:
That would break with async functions which always return a native promise, the proposal to let us do that was rejected 3 years ago in tc39 (compositional functions). |
ljharb
commented
Sep 18, 2019
I would strongly suggest not supporting AbortController outside of fetch; it's not a great name, and it doesn't imo fit with Promises in a general sense. |
Fishrock123
commented
Sep 18, 2019
@benjamingr I do not understand what you are referring to. I have a patch in progress which appears to work fine -- do you mean having the timeout callback be an async function? (That would be a separate issue...?) |
Fishrock123
commented
Sep 18, 2019
@ljharb You may want to leave that feedback on whatwg/html#617 or else whatwg will likely implement it for other apis too. |
ljharb
commented
Sep 18, 2019
They’ll likely do it regardless of the feedback they get. |
devsnek
commented
Sep 18, 2019
@Fishrock123 i think Benjamin is referring to something like this: asyncfunctionx(){returnsetTimeout(()=>{},10);}x().then((timer)=>{clearTimeout(timer);}); |
benjamingr
commented
Sep 19, 2019
@ljharb we should strive to do the same thing as the web platform and standard suggest rather than roll our own abstractions IMO. If the web platform has settled on AbortController that is what we should do in my opinion. Otherwise we're going against the web platform.
There was a proposal ( https://github.com/jhusain/compositional-functions ) to make it possible to hook into await but it did not pass. We can expose an async iterator for setInterval just fine :] @jasnell - that would not work - util.promisify returns a function which means that the AbortController would have to be passed on the returned function rather than on the util.promisify call no? |
benjamingr
commented
Sep 19, 2019
Also - I am -1 on using That said, deep down I still believe that cancellable promises like other languages and ecosystems do are the correct solution to this problem. I do not want to pursue that because that is not the direction TC39 is giving us so I recommend we stick to the standards. |
benjamingr
commented
Sep 19, 2019
And just to clarify - I am +1 on an async iterator for |
ljharb
commented
Sep 19, 2019
@benjamingr the language still has an active proposal for general promise cancellation; I’m suggesting waiting for that, even if it takes awhile. |
Fishrock123
commented
Sep 19, 2019
As previously noted, making the promise cancellable only solves one of the issues. |
benjamingr
commented
Sep 20, 2019
Link? |
ljharb
commented
Sep 20, 2019
Oh, that one - if I understand correctly that proposal is both inactive (stage 1 for years) and intends to be compatible (interop wise not api wise) with AbortController. The bigger issue with AbortController is that it depends on Pinging @rbuckton - are you still pursuing that proposal? |
rbuckton
commented
Sep 21, 2019
Yes, I just haven't had much time to spend on it lately. |
@rbuckton do you know when you will be presenting it next? |
rbuckton
commented
Sep 21, 2019
@benjamingr I'm focusing on advancing two of my other proposals at the moment. It likely won't be until next year that I can focus on the proposal again. Currently the proposal is focused on a "protocol" based approach using a Symbol-based API (similar to iteration), rather than an actual cancellation token implementation. The goal is that the ECMAScript spec would specify the protocol, which could be added to the existing AbortController/AbortSignal implementation, and that library authors and other runtimes (such as NodeJS) could also implement the protocol without needing to depend on AbortController (and thus EventTarget). However, since there would be no actual implementation in the ECMAScript spec, there need to be compelling use cases in ECMAScript spec for such a feature. The upside of a protocol-based approach is that a library author could accept cancellation signals by depending on the protocol rather than needing to feature test for You can see an example of this protocol implemented in userland in |
benjamingr
commented
Sep 22, 2019
@rbuckton what do you think Node.js should do with its cancellation? We have the following options:
Given that the current proposal is stage 1 and work likely won't happen on it for another year at which point it has to go through a risky process - what should Node.js do? If we decide this is a priority for Node.js is there any way for our own TC39 representation (assuming they are interested) to help? (and to be clear that is fair, you owe us nothing and I am not complaining and am thankful for your work) |
Fishrock123
commented
Sep 22, 2019
Note: the choice we make here will likely have implications on other non-web apis that node ships too. |
BridgeAR
commented
Jan 12, 2020
Is there any progress? |
Fishrock123
commented
May 19, 2020
So, Rust's The answer is: have the timeout Promise wrap (be chained from) the Promise which could "cancel" it. If the upstream needs to cancel, it can just resolve the promise, which should be able to cancel the timer promise's internal timer. |
8ae28ff to
2935f72Compare
The current implementation of the promisified setTimeout and
setImmediate does not make the Timeout or Immediate available, so they
cannot be canceled or unreffed.
The docs and code follow the pattern established by child_process
There are two issues with this implementation
timeoutorimmediateproperties that exist or may exist on other promise implementations or future promise implementations. Looks like bluebird promises include a.timeoutmethod. http://bluebirdjs.com/docs/api/timeout.htmlclearTimeout()orclearImmediate()properties that clears the timer and rejects the promise. I can't currently think of a use case for calling unref, unref, or refresh on a promised timer so it may not be necessary to expose the whole timer instance.Checklist
make -j4 test(UNIX), orvcbuild test(Windows) passes