-
-
Notifications
You must be signed in to change notification settings - Fork 35.4k
timers: introduce setInterval async iterator #35841
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
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
Utilises Symbol.asyncIterator to provide an iterator compatible with `for await`
```
const {
setInterval
} = require('timers/promises');
const ac = new AbortController()
const signal = ac.signal
setTimeout(() => ac.abort(), 500)
for await (const value of setInterval(interval, null, { ref: false, signal })) {
}
```- Loading branch information
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,9 +1,11 @@ | ||||||||||||||||||||||||||||
| 'use strict'; | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| const { | ||||||||||||||||||||||||||||
| Symbol, | ||||||||||||||||||||||||||||
| FunctionPrototypeBind, | ||||||||||||||||||||||||||||
| Promise, | ||||||||||||||||||||||||||||
| PromisePrototypeFinally, | ||||||||||||||||||||||||||||
| PromiseResolve, | ||||||||||||||||||||||||||||
| PromiseReject, | ||||||||||||||||||||||||||||
| } = primordials; | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
|
|
@@ -125,7 +127,110 @@ function setImmediate(value, options = {}) { | |||||||||||||||||||||||||||
| () => signal.removeEventListener('abort', oncancel)) : ret; | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| function setInterval(after, value, options = {}) { | ||||||||||||||||||||||||||||
| const args = value !== undefined ? [value] : value; | ||||||||||||||||||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: for validation prefer using |
||||||||||||||||||||||||||||
| if (options == null || typeof options !== 'object') { | ||||||||||||||||||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. function f(o = {}) {
console.log({ o })
}
f(null)Outputs
This is a copy from
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah so it can be null but not undefined - you can check |
||||||||||||||||||||||||||||
| throw new ERR_INVALID_ARG_TYPE( | ||||||||||||||||||||||||||||
| 'options', | ||||||||||||||||||||||||||||
| 'Object', | ||||||||||||||||||||||||||||
| options); | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| const { signal, ref = true } = options; | ||||||||||||||||||||||||||||
| if (signal !== undefined && | ||||||||||||||||||||||||||||
| (signal === null || | ||||||||||||||||||||||||||||
| typeof signal !== 'object' || | ||||||||||||||||||||||||||||
| !('aborted' in signal))) { | ||||||||||||||||||||||||||||
| throw new ERR_INVALID_ARG_TYPE( | ||||||||||||||||||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ditto regarding |
||||||||||||||||||||||||||||
| 'options.signal', | ||||||||||||||||||||||||||||
| 'AbortSignal', | ||||||||||||||||||||||||||||
| signal); | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| if (typeof ref !== 'boolean') { | ||||||||||||||||||||||||||||
| throw new ERR_INVALID_ARG_TYPE( | ||||||||||||||||||||||||||||
| 'options.ref', | ||||||||||||||||||||||||||||
| 'boolean', | ||||||||||||||||||||||||||||
| ref); | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| return { | ||||||||||||||||||||||||||||
| [Symbol.asyncIterator]() { | ||||||||||||||||||||||||||||
| let timeout, | ||||||||||||||||||||||||||||
| deferred; | ||||||||||||||||||||||||||||
| let active = true; | ||||||||||||||||||||||||||||
| const iterator = { | ||||||||||||||||||||||||||||
| next() { | ||||||||||||||||||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this timing logic is incorrect. some examples: const it = setInterval(100);
it.next(); // should resolve after 100ms
it.next(); // should resolve after 200msconst it = setInterval(100);
await delay(50);
it.next(); // should resolve after 50ms
it.next(); // should resolve after 150msconst controller = new AbortController();
const it = setInterval(100, { signal: controller.signal });
it.next(); // below should reject this promise
controller.abort();
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This may have to be an option to enable one or the other timing.. Here is an example that brings the idea behind using timing for each const iterable = setInterval(100);
const iterator = iterable[Symbol.asyncIterator]();
await iterator.next()
await delay(150)
await iterator.next()
await delay(150)This would be the same as for await (const unused of setInterval(100)) {
await delay(150)
}There is also a difference between the "async iterable" instance (that can be passed directly to Timing given we're hooking onto the next occurring interval from Timing given we have one promise for each interval: The current timing would be const iterable = setInterval(100);
const iterator = iterable[Symbol.asyncIterator]();
const first = iterator.next()
const second = iterator.next()The above code seems a bit confusing... we would need to "queue" those resolves then, maybe thats fine? const defers = []
setInterval(() => {
const next = defers.shift()
next.resolve({ done: false, value })
}, after)
/* ... */
next() {
/* ... */
const deferred = /* ... */
defers.push(deferred)
return deferred.promise
},
return() {
/* ... */
const result = { done: true, value: undefined }
defers.forEach(deferred => deferred.resolve(result))
return result
}I think one, or the other, or both sets of logic will be good, just need to know what one
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Im in favour of having:
If we add the two additional options, this would definitely add to the overall complexity for sure, I would think we would need three completely seperate implementations for this. I believe
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Isn't that the mental model when someone does The disadvantage of my implementation in that comment is "what happens if the user 'misses' an interval?" The timing is right but the code should probably push the callbacks to an array (instead of replacing a reference) and fulfil them in the interval. Also cc @ronag wdyt about what we should do with timers -> async iterators conversion in terms of behavior?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Reading the HTML spec for https://html.spec.whatwg.org/multipage/timers-and-user-prompts.html#dom-setinterval
The repeating happens after the related tasks function has been invoked (which in our case is the external code executing, the stuff between
Then
The running of the task now is async, we tell the consumer to start the task by resolving the task, and the consumer signifies the task is complete by calling Aside from the above, we could warn if we skip intervals and leave it at that, but then this seems like inconsistent behaviour, if I had a 30 minute interval going, happened to take 31 minutes for my task, and now have to wait for 29 minutes till the next interval.... leading to the intervals actually being once an hour
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Conceptually I would imagine it to be equivalent to adding an Lines 550 to 562 in bec918f
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah! As I say that, I see
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Well, two things:
What I meant is just "the standard queueing trick" something like: async function* iteratorSetInterval(ms, options = {}) {
// validateOptions
// validateAbortSignal
let callbacks = [], pending = 0;
let aborted = options && options.signal && options.signal.aborted;
if (options && options.signal) {
options.signal.addEventListener('abort', () => {
aborted = true;
});
}
const timerRef = setInterval(() => {
if (callbacks.length > 0) callbacks.shift()();
else pending++;
}, ms);
if (options && options.ref === false) {
timerRef.unref();
}
try {
while (!aborted) {
yield await new Promise((resolve) => {
if (pending > 0) { resolve(); pending--; } ;
else callbacks.push(resolve);
});
}
} finally {
clearInterval(timerRef);
}
} |
||||||||||||||||||||||||||||
| if (!active) { | ||||||||||||||||||||||||||||
| return PromiseResolve({ | ||||||||||||||||||||||||||||
| done: true, | ||||||||||||||||||||||||||||
| value: undefined | ||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| // TODO(@jasnell): If a decision is made that this cannot | ||||||||||||||||||||||||||||
| // be backported to 12.x, then this can be converted to | ||||||||||||||||||||||||||||
| // use optional chaining to simplify the check. | ||||||||||||||||||||||||||||
| if (signal && signal.aborted) { | ||||||||||||||||||||||||||||
| return PromiseReject( | ||||||||||||||||||||||||||||
| lazyDOMException('The operation was aborted', 'AbortError') | ||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| const promise = new Promise( | ||||||||||||||||||||||||||||
| (resolve, reject) => { | ||||||||||||||||||||||||||||
| deferred = { resolve, reject }; | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||
| timeout = new Timeout((value) => { | ||||||||||||||||||||||||||||
| if (deferred) { | ||||||||||||||||||||||||||||
| deferred.resolve({ | ||||||||||||||||||||||||||||
| done: false, | ||||||||||||||||||||||||||||
| value | ||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||
| deferred = undefined; | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| }, after, args, false, true); | ||||||||||||||||||||||||||||
| if (!ref) timeout.unref(); | ||||||||||||||||||||||||||||
| insert(timeout, timeout._idleTimeout); | ||||||||||||||||||||||||||||
| return promise; | ||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||
| return() { | ||||||||||||||||||||||||||||
| active = false; | ||||||||||||||||||||||||||||
| if (timeout) { | ||||||||||||||||||||||||||||
| // eslint-disable-next-line no-undef | ||||||||||||||||||||||||||||
| clearTimeout(timeout); | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| if (deferred) { | ||||||||||||||||||||||||||||
| deferred.resolve({ | ||||||||||||||||||||||||||||
| done: true, | ||||||||||||||||||||||||||||
| value: undefined | ||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||
| deferred = undefined; | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| if (signal) { | ||||||||||||||||||||||||||||
| signal.removeEventListener('abort', onAbort); | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| return PromiseResolve({ | ||||||||||||||||||||||||||||
| done: true, | ||||||||||||||||||||||||||||
| value: undefined | ||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||
| if (signal) { | ||||||||||||||||||||||||||||
| signal.addEventListener('abort', onAbort, { once: true }); | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| return iterator; | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| function onAbort() { | ||||||||||||||||||||||||||||
| if (deferred) { | ||||||||||||||||||||||||||||
| deferred.reject( | ||||||||||||||||||||||||||||
| lazyDOMException('The operation was aborted', 'AbortError') | ||||||||||||||||||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Async iterators don't need to reject on cancellation or expose the abort error - it's enough to
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is definitely a question I hoped to come up in review. @/jasnell had suggested
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. cc @jasnell - i think the mental model is that streams and iterators have a concept of "abort" built in unlike promises - so it's better to use it probably :]
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I can see it both ways really. An option makes it more complicated, but gives users the flexibility. I'm honestly not sure which should be the default here.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I avoid doing cancellation "the regular JS way" with Note that if we implement this with an async generator this solution will work "automatically" by putting cleanup in a |
||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||
| deferred = undefined; | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| module.exports = { | ||||||||||||||||||||||||||||
| setTimeout, | ||||||||||||||||||||||||||||
| setImmediate, | ||||||||||||||||||||||||||||
| setInterval, | ||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||
Uh oh!
There was an error while loading. Please reload this page.