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

timers: introduce setInterval async iterator #37153

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions doc/api/timers.md
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,38 @@ added: v15.0.0
* `signal` {AbortSignal} An optional `AbortSignal` that can be used to
cancel the scheduled `Immediate`.

### `timersPromises.setInterval([delay[, value[, options]]])`
<!-- YAML
added: REPLACEME
-->

Returns an async iterator that generates values in an interval of `delay` ms.

* `delay` {number} The number of milliseconds to wait between iterations.
**Default**: `1`.
* `value` {any} A value with which the iterator returns.
* `options` {Object}
* `ref` {boolean} Set to `false` to indicate that the scheduled `Timeout`
between iterations should not require the Node.js event loop to
remain active.
**Default**: `true`.
* `signal` {AbortSignal} An optional `AbortSignal` that can be used to
cancel the scheduled `Timeout` between operations.
benjamingr marked this conversation as resolved.
Show resolved Hide resolved

```js
(async function() {
const { setInterval } = require('timers/promises');
const interval = 100;
for await (const startTime of setInterval(interval, Date.now())) {
const now = Date.now();
console.log(now);
if ((now - startTime) > 1000)
break;
}
Copy link
Member

Choose a reason for hiding this comment

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

I would make the example use the value, or add a second example that uses the value.

Copy link
Member Author

Choose a reason for hiding this comment

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

done

console.log(Date.now());
})();
```

[Event Loop]: https://nodejs.org/en/docs/guides/event-loop-timers-and-nexttick/#setimmediate-vs-settimeout
[`AbortController`]: globals.md#globals_class_abortcontroller
[`TypeError`]: errors.md#errors_class_typeerror
Expand Down
58 changes: 57 additions & 1 deletion lib/timers/promises.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,11 @@ const {
codes: { ERR_INVALID_ARG_TYPE }
} = require('internal/errors');

const { validateAbortSignal } = require('internal/validators');
const {
validateAbortSignal,
validateBoolean,
validateObject,
} = require('internal/validators');

function cancelListenerHandler(clear, reject) {
if (!this._destroyed) {
Expand Down Expand Up @@ -111,7 +115,59 @@ function setImmediate(value, options = {}) {
() => signal.removeEventListener('abort', oncancel)) : ret;
}

async function* setInterval(after, value, options = {}) {
validateObject(options, 'options');
const { signal, ref = true } = options;
validateAbortSignal(signal, 'options.signal');
validateBoolean(ref, 'options.ref');

if (signal?.aborted)
throw new AbortError();

let onCancel;
let interval;
try {
let notYielded = 0;
let callback;
interval = new Timeout(() => {
notYielded++;
if (callback) {
callback();
callback = undefined;
}
}, after, undefined, true, true);
if (!ref) interval.unref();
insert(interval, interval._idleTimeout);
if (signal) {
onCancel = () => {
// eslint-disable-next-line no-undef
clearInterval(interval);
if (callback) {
callback(PromiseReject(new AbortError()));
callback = undefined;
}
};
signal.addEventListener('abort', onCancel, { once: true });
}

while (!signal?.aborted) {
if (notYielded === 0) {
await new Promise((resolve) => callback = resolve);
}
for (; notYielded > 0; notYielded--) {
yield value;
Copy link
Member

Choose a reason for hiding this comment

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

If typeof value === 'function', then it would be worthwhile calling the function here. Also, it would be worthwhile making this an await so that if value is a promise the yield happens after the Promise is resolved...

So, something like...

Suggested change
yield value;
yield await (typeof value === 'function' ? value() : value);

This would allow patterns such as:

async function getValue() { /** ... **/ }

for await (const a of setInterval(100, getValue)) {
  console.log(a);
}

// and

for await (const a of setInterval(100, getValue())) {
  console.log(a);
}

Copy link
Member

Choose a reason for hiding this comment

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

@jasnell
Ideally when this lands (stage 2, so good chance) you could just do setInterval(100).map(getValue) which is kind of neater :]

}
}
throw new AbortError();
} finally {
// eslint-disable-next-line no-undef
clearInterval(interval);
signal?.removeEventListener('abort', onCancel);
}
}

module.exports = {
setTimeout,
setImmediate,
setInterval,
};
Loading