Skip to content

Commit 2cb80b2

Browse files
Linkgoronfabiancook
andcommitted
timers: introduce setInterval async iterator
Added setInterval async generator to timers\promises. Utilises async generators to provide an iterator compatible with `for await`. Co-Authored-By: Fabian Cook <hello@fabiancook.dev>
1 parent d0a92e2 commit 2cb80b2

File tree

4 files changed

+301
-8
lines changed

4 files changed

+301
-8
lines changed

doc/api/timers.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -363,6 +363,26 @@ added: v15.0.0
363363
* `signal` {AbortSignal} An optional `AbortSignal` that can be used to
364364
cancel the scheduled `Immediate`.
365365

366+
### `timersPromises.setInterval([delay[, value[, options]]])`
367+
<!-- YAML
368+
added: REPLACEME
369+
-->
370+
371+
* `delay` {number} The number of milliseconds to wait between iterations.
372+
**Default**: `1`.
373+
* `value` {any} A value with which the iterator returns.
374+
* `options` {Object}
375+
* `ref` {boolean} Set to `false` to indicate that the scheduled `Timeout`
376+
between iterations should not require the Node.js event loop to
377+
remain active.
378+
**Default**: `true`.
379+
* `signal` {AbortSignal} An optional `AbortSignal` that can be used to
380+
cancel the scheduled `Timeout` between operations.
381+
* `throwOnAbort` {boolean} Set to `true` to indicate that the iterator
382+
should finish regularly when the signal is aborted. When set to `false`
383+
the iterator throws after it yields all values.
384+
**Default**: `false`
385+
366386
[Event Loop]: https://nodejs.org/en/docs/guides/event-loop-timers-and-nexttick/#setimmediate-vs-settimeout
367387
[`AbortController`]: globals.md#globals_class_abortcontroller
368388
[`TypeError`]: errors.md#errors_class_typeerror

lib/timers.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,16 @@ function setInterval(callback, repeat, arg1, arg2, arg3) {
215215
return timeout;
216216
}
217217

218+
219+
ObjectDefineProperty(setInterval, customPromisify, {
220+
enumerable: true,
221+
get() {
222+
if (!timersPromises)
223+
timersPromises = require('timers/promises');
224+
return timersPromises.setInterval;
225+
}
226+
});
227+
218228
function clearInterval(timer) {
219229
// clearTimeout and clearInterval can be used to clear timers created from
220230
// both setTimeout and setInterval, as specified by HTML Living Standard:

lib/timers/promises.js

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,11 @@ const {
1818
codes: { ERR_INVALID_ARG_TYPE }
1919
} = require('internal/errors');
2020

21-
const { validateAbortSignal } = require('internal/validators');
21+
const {
22+
validateAbortSignal,
23+
validateBoolean,
24+
validateObject,
25+
} = require('internal/validators');
2226

2327
function cancelListenerHandler(clear, reject) {
2428
if (!this._destroyed) {
@@ -111,7 +115,70 @@ function setImmediate(value, options = {}) {
111115
() => signal.removeEventListener('abort', oncancel)) : ret;
112116
}
113117

118+
async function* setInterval(after, value, options = {}) {
119+
validateObject(options, 'options');
120+
const { signal, ref = true, throwOnAbort = true } = options;
121+
validateAbortSignal(signal, 'options.signal');
122+
validateBoolean(ref, 'options.ref');
123+
validateBoolean(throwOnAbort, 'options.throwOnAbort');
124+
125+
if (signal?.aborted) {
126+
if (throwOnAbort) throw new AbortError();
127+
return;
128+
}
129+
130+
let onCancel;
131+
let notYielded = 0;
132+
let passCallback;
133+
let abortCallback;
134+
const interval = new Timeout(() => {
135+
notYielded++;
136+
if (passCallback) {
137+
passCallback();
138+
passCallback = undefined;
139+
abortCallback = undefined;
140+
}
141+
}, after, undefined, true, true);
142+
if (!ref) interval.unref();
143+
insert(interval, interval._idleTimeout);
144+
if (signal) {
145+
onCancel = () => {
146+
// eslint-disable-next-line no-undef
147+
clearInterval(interval);
148+
if (abortCallback) {
149+
abortCallback(new AbortError());
150+
passCallback = undefined;
151+
abortCallback = undefined;
152+
}
153+
};
154+
signal.addEventListener('abort', onCancel, { once: true });
155+
}
156+
157+
while (!signal?.aborted) {
158+
if (notYielded === 0) {
159+
try {
160+
await new Promise((resolve, reject) => {
161+
passCallback = resolve;
162+
abortCallback = reject;
163+
});
164+
} catch (err) {
165+
if (throwOnAbort) {
166+
throw err;
167+
}
168+
return;
169+
}
170+
}
171+
for (; notYielded > 0; notYielded--) {
172+
yield value;
173+
}
174+
}
175+
if (throwOnAbort) {
176+
throw new AbortError();
177+
}
178+
}
179+
114180
module.exports = {
115181
setTimeout,
116182
setImmediate,
183+
setInterval,
117184
};

test/parallel/test-timers-promisified.js

Lines changed: 203 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,12 @@ const timerPromises = require('timers/promises');
1515

1616
const setTimeout = promisify(timers.setTimeout);
1717
const setImmediate = promisify(timers.setImmediate);
18+
const setInterval = promisify(timers.setInterval);
1819
const exec = promisify(child_process.exec);
1920

2021
assert.strictEqual(setTimeout, timerPromises.setTimeout);
2122
assert.strictEqual(setImmediate, timerPromises.setImmediate);
23+
assert.strictEqual(setInterval, timerPromises.setInterval);
2224

2325
process.on('multipleResolves', common.mustNotCall());
2426

@@ -50,48 +52,158 @@ process.on('multipleResolves', common.mustNotCall());
5052
}));
5153
}
5254

55+
{
56+
const controller = new AbortController();
57+
const { signal } = controller;
58+
const iterable = setInterval(1, undefined, { signal });
59+
const iterator = iterable[Symbol.asyncIterator]();
60+
const promise = iterator.next();
61+
promise.then(common.mustCall((result) => {
62+
assert.ok(!result.done);
63+
assert.strictEqual(result.value, undefined);
64+
controller.abort();
65+
return assert.rejects(iterator.next(), /AbortError/);
66+
})).then(common.mustCall());
67+
}
68+
69+
{
70+
const controller = new AbortController();
71+
const { signal } = controller;
72+
const iterable = setInterval(1, undefined, { signal, throwOnAbort: false });
73+
const iterator = iterable[Symbol.asyncIterator]();
74+
const promise = iterator.next();
75+
promise.then(common.mustCall((result) => {
76+
assert.ok(!result.done);
77+
assert.strictEqual(result.value, undefined);
78+
controller.abort();
79+
return iterator.next();
80+
})).then(common.mustCall((result) => {
81+
assert.ok(result.done);
82+
}));
83+
}
84+
85+
{
86+
const controller = new AbortController();
87+
const { signal } = controller;
88+
const iterable = setInterval(1, 'foobar', { signal });
89+
const iterator = iterable[Symbol.asyncIterator]();
90+
const promise = iterator.next();
91+
promise.then(common.mustCall((result) => {
92+
assert.ok(!result.done);
93+
assert.strictEqual(result.value, 'foobar');
94+
controller.abort();
95+
return assert.rejects(iterator.next(), /AbortError/);
96+
})).then(common.mustCall());
97+
}
98+
99+
{
100+
const controller = new AbortController();
101+
const { signal } = controller;
102+
const iterable = setInterval(1, 'foobar', { signal, throwOnAbort: false });
103+
const iterator = iterable[Symbol.asyncIterator]();
104+
const promise = iterator.next();
105+
promise.then(common.mustCall((result) => {
106+
assert.ok(!result.done);
107+
assert.strictEqual(result.value, 'foobar');
108+
controller.abort();
109+
return iterator.next();
110+
})).then(common.mustCall((result) => {
111+
assert.ok(result.done);
112+
}));
113+
}
114+
53115
{
54116
const ac = new AbortController();
55117
const signal = ac.signal;
56-
assert.rejects(setTimeout(10, undefined, { signal }), /AbortError/);
118+
assert.rejects(setTimeout(10, undefined, { signal }), /AbortError/)
119+
.then(common.mustCall());
57120
ac.abort();
58121
}
59122

60123
{
61124
const ac = new AbortController();
62125
const signal = ac.signal;
63126
ac.abort(); // Abort in advance
64-
assert.rejects(setTimeout(10, undefined, { signal }), /AbortError/);
127+
assert.rejects(setTimeout(10, undefined, { signal }), /AbortError/)
128+
.then(common.mustCall());
65129
}
66130

67131
{
68132
const ac = new AbortController();
69133
const signal = ac.signal;
70-
assert.rejects(setImmediate(10, { signal }), /AbortError/);
134+
assert.rejects(setImmediate(10, { signal }), /AbortError/)
135+
.then(common.mustCall());
71136
ac.abort();
72137
}
73138

74139
{
75140
const ac = new AbortController();
76141
const signal = ac.signal;
77142
ac.abort(); // Abort in advance
78-
assert.rejects(setImmediate(10, { signal }), /AbortError/);
143+
assert.rejects(setImmediate(10, { signal }), /AbortError/)
144+
.then(common.mustCall());
145+
}
146+
147+
{
148+
const ac = new AbortController();
149+
const { signal } = ac;
150+
ac.abort(); // Abort in advance
151+
152+
const iterable = setInterval(1, undefined, { signal });
153+
const iterator = iterable[Symbol.asyncIterator]();
154+
assert.rejects(iterator.next(), /AbortError/).then(common.mustCall());
155+
}
156+
157+
{
158+
const ac = new AbortController();
159+
const { signal } = ac;
160+
161+
const iterable = setInterval(100, undefined, { signal });
162+
const iterator = iterable[Symbol.asyncIterator]();
163+
164+
// This promise should take 100 seconds to resolve, so now aborting it should
165+
// mean we abort early
166+
const promise = iterator.next();
167+
168+
ac.abort(); // Abort in after we have a next promise
169+
170+
assert.rejects(promise, /AbortError/).then(common.mustCall());
79171
}
80172

81173
{
82174
// Check that aborting after resolve will not reject.
83175
const ac = new AbortController();
84176
const signal = ac.signal;
85-
setTimeout(10, undefined, { signal }).then(() => {
177+
assert.doesNotReject(setTimeout(10, undefined, { signal }).then(common.mustCall(() => {
86178
ac.abort();
87-
});
179+
}))).then(common.mustCall());
88180
}
89181
{
90182
// Check that aborting after resolve will not reject.
91183
const ac = new AbortController();
92184
const signal = ac.signal;
93-
setImmediate(10, { signal }).then(() => {
185+
assert.doesNotReject(setImmediate(10, { signal }).then(common.mustCall(() => {
94186
ac.abort();
187+
}))).then(common.mustCall());
188+
}
189+
190+
{
191+
[1, '', Infinity, null, {}].forEach((ref) => {
192+
const iterable = setInterval(10, undefined, { ref });
193+
assert.rejects(() => iterable[Symbol.asyncIterator]().next(), /ERR_INVALID_ARG_TYPE/)
194+
.then(common.mustCall());
195+
});
196+
197+
[1, '', Infinity, null, {}].forEach((signal) => {
198+
const iterable = setInterval(10, undefined, { signal });
199+
assert.rejects(() => iterable[Symbol.asyncIterator]().next(), /ERR_INVALID_ARG_TYPE/)
200+
.then(common.mustCall());
201+
});
202+
203+
[1, '', Infinity, null, true, false].forEach((options) => {
204+
const iterable = setInterval(10, undefined, options);
205+
assert.rejects(() => iterable[Symbol.asyncIterator]().next(), /ERR_INVALID_ARG_TYPE/)
206+
.then(common.mustCall());
95207
});
96208
}
97209

@@ -165,3 +277,87 @@ process.on('multipleResolves', common.mustNotCall());
165277
assert.strictEqual(stderr, '');
166278
}));
167279
}
280+
281+
{
282+
exec(`${process.execPath} -pe "const assert = require('assert');` +
283+
'const interval = require(\'timers/promises\')' +
284+
'.setInterval(1000, null, { ref: false });' +
285+
'interval[Symbol.asyncIterator]().next()' +
286+
'.then(assert.fail)"').then(common.mustCall(({ stderr }) => {
287+
assert.strictEqual(stderr, '');
288+
}));
289+
}
290+
291+
{
292+
async function runInterval(fn, intervalTime, signal) {
293+
const input = 'foobar';
294+
const interval = setInterval(intervalTime, input, { signal });
295+
let iteration = 0;
296+
for await (const value of interval) {
297+
const time = Date.now();
298+
assert.strictEqual(value, input);
299+
await fn(time, iteration);
300+
iteration++;
301+
}
302+
}
303+
304+
{
305+
// check that we call the correct amount of times.
306+
const controller = new AbortController();
307+
const { signal } = controller;
308+
309+
let loopCount = 0;
310+
const delay = 20;
311+
const timeoutLoop = runInterval(() => {
312+
loopCount++;
313+
if (loopCount === 5) controller.abort();
314+
if (loopCount > 5) throw new Error('ran too many times');
315+
}, delay, signal);
316+
317+
assert.rejects(timeoutLoop,/AbortError/).then(common.mustCall(() => {
318+
assert.strictEqual(loopCount, 5);
319+
}));
320+
}
321+
322+
{
323+
// Check that if we abort when we delay long enough
324+
const controller = new AbortController();
325+
const { signal } = controller;
326+
327+
let prevTime;
328+
const delay = 25;
329+
const timeoutLoop = runInterval((time, iteration) => {
330+
if (iteration === 5) controller.abort();
331+
// give some slack because of timers
332+
if (prevTime && (time - prevTime < (delay-5))) {
333+
const diff = time - prevTime;
334+
throw new Error(`${diff} between iterations, lower than ${delay}`);
335+
}
336+
prevTime = time;
337+
}, delay, signal);
338+
339+
assert.rejects(timeoutLoop, /AbortError/).then(common.mustCall());
340+
}
341+
342+
{
343+
// Check that if we abort when we have some callbacks left,
344+
// we actually call them.
345+
const controller = new AbortController();
346+
const { signal } = controller;
347+
const delay = 10;
348+
let totalIterations = 0;
349+
const timeoutLoop = runInterval(async (time, iterationNumber) => {
350+
if (iterationNumber === 1) {
351+
await setTimeout(delay * 3);
352+
controller.abort();
353+
}
354+
if (iterationNumber > totalIterations) {
355+
totalIterations = iterationNumber;
356+
}
357+
}, delay, signal);
358+
359+
timeoutLoop.catch(common.mustCall(() => {
360+
assert.ok(totalIterations >= 3);
361+
}));
362+
}
363+
}

0 commit comments

Comments
 (0)