Skip to content

Commit 8bce46a

Browse files
committed
timers: add experimental scheduler api
Adds experimental implementations of the yield and wait APIs being explored at https://github.com/WICG/scheduling-apis. When I asked the WHATWG folks about the possibility of standardizing the [awaitable versions of setTimeout/setImmediate](whatwg/html#7340) that we have implemented in `timers/promises`, they pointed at the work in progress scheduling APIs draft as they direction they'll be going. While there is definitely a few thing in that draft that have questionable utility to Node.js, the yield and wait APIs map cleanly to the setImmediate and setTimeout we already have. Signed-off-by: James M Snell <jasnell@gmail.com> PR-URL: #40909 Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com> Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com> Reviewed-By: Darshan Sen <raisinten@gmail.com>
1 parent 6ef6bdf commit 8bce46a

File tree

3 files changed

+141
-1
lines changed

3 files changed

+141
-1
lines changed

doc/api/timers.md

+45
Original file line numberDiff line numberDiff line change
@@ -472,7 +472,52 @@ const interval = 100;
472472
})();
473473
```
474474

475+
### `timersPromises.scheduler.wait(delay[, options])`
476+
477+
<!-- YAML
478+
added: REPLACEME
479+
-->
480+
481+
> Stability: 1 - Experimental
482+
483+
* `delay` {number} The number of milliseconds to wait before resolving the
484+
promise.
485+
* `options` {Object}
486+
* `signal` {AbortSignal} An optional `AbortSignal` that can be used to
487+
cancel waiting.
488+
* Returns: {Promise}
489+
490+
An experimental API defined by the [Scheduling APIs][] draft specification
491+
being developed as a standard Web Platform API.
492+
493+
Calling `timersPromises.scheduler.wait(delay, options)` is roughly equivalent
494+
to calling `timersPromises.setTimeout(delay, undefined, options)` except that
495+
the `ref` option is not supported.
496+
497+
```mjs
498+
import { scheduler } from 'timers/promises';
499+
500+
await scheduler.wait(1000); // Wait one second before continuing
501+
```
502+
503+
### `timersPromises.scheduler.yield()`
504+
505+
<!-- YAML
506+
added: REPLACEME
507+
-->
508+
509+
> Stability: 1 - Experimental
510+
511+
* Returns: {Promise}
512+
513+
An experimental API defined by the [Scheduling APIs][] draft specification
514+
being developed as a standard Web Platform API.
515+
516+
Calling `timersPromises.scheduler.yield()` is equivalent to calling
517+
`timersPromises.setImmediate()` with no arguments.
518+
475519
[Event Loop]: https://nodejs.org/en/docs/guides/event-loop-timers-and-nexttick/#setimmediate-vs-settimeout
520+
[Scheduling APIs]: https://github.com/WICG/scheduling-apis
476521
[`AbortController`]: globals.md#class-abortcontroller
477522
[`TypeError`]: errors.md#class-typeerror
478523
[`clearImmediate()`]: #clearimmediateimmediate

lib/timers/promises.js

+46-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ const {
44
FunctionPrototypeBind,
55
Promise,
66
PromiseReject,
7+
ReflectConstruct,
78
SafePromisePrototypeFinally,
9+
Symbol,
810
} = primordials;
911

1012
const {
@@ -15,7 +17,11 @@ const {
1517

1618
const {
1719
AbortError,
18-
codes: { ERR_INVALID_ARG_TYPE }
20+
codes: {
21+
ERR_ILLEGAL_CONSTRUCTOR,
22+
ERR_INVALID_ARG_TYPE,
23+
ERR_INVALID_THIS,
24+
}
1925
} = require('internal/errors');
2026

2127
const {
@@ -24,6 +30,8 @@ const {
2430
validateObject,
2531
} = require('internal/validators');
2632

33+
const kScheduler = Symbol('kScheduler');
34+
2735
function cancelListenerHandler(clear, reject, signal) {
2836
if (!this._destroyed) {
2937
clear(this);
@@ -173,8 +181,45 @@ async function* setInterval(after, value, options = {}) {
173181
}
174182
}
175183

184+
// TODO(@jasnell): Scheduler is an API currently being discussed by WICG
185+
// for Web Platform standardization: https://github.com/WICG/scheduling-apis
186+
// The scheduler.yield() and scheduler.wait() methods correspond roughly to
187+
// the awaitable setTimeout and setImmediate implementations here. This api
188+
// should be considered to be experimental until the spec for these are
189+
// finalized. Note, also, that Scheduler is expected to be defined as a global,
190+
// but while the API is experimental we shouldn't expose it as such.
191+
class Scheduler {
192+
constructor() {
193+
throw new ERR_ILLEGAL_CONSTRUCTOR();
194+
}
195+
196+
/**
197+
* @returns {Promise<void>}
198+
*/
199+
yield() {
200+
if (!this[kScheduler])
201+
throw new ERR_INVALID_THIS('Scheduler');
202+
return setImmediate();
203+
}
204+
205+
/**
206+
* @typedef {import('../internal/abort_controller').AbortSignal} AbortSignal
207+
* @param {number} delay
208+
* @param {{ signal?: AbortSignal }} [options]
209+
* @returns {Promise<void>}
210+
*/
211+
wait(delay, options) {
212+
if (!this[kScheduler])
213+
throw new ERR_INVALID_THIS('Scheduler');
214+
return setTimeout(delay, undefined, { signal: options?.signal });
215+
}
216+
}
217+
176218
module.exports = {
177219
setTimeout,
178220
setImmediate,
179221
setInterval,
222+
scheduler: ReflectConstruct(function() {
223+
this[kScheduler] = true;
224+
}, [], Scheduler),
180225
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
'use strict';
2+
3+
const common = require('../common');
4+
5+
const { scheduler } = require('timers/promises');
6+
const { setTimeout } = require('timers');
7+
const {
8+
strictEqual,
9+
rejects,
10+
} = require('assert');
11+
12+
async function testYield() {
13+
await scheduler.yield();
14+
process.emit('foo');
15+
}
16+
testYield().then(common.mustCall());
17+
queueMicrotask(common.mustCall(() => {
18+
process.addListener('foo', common.mustCall());
19+
}));
20+
21+
async function testWait() {
22+
let value = 0;
23+
setTimeout(() => value++, 10);
24+
await scheduler.wait(15);
25+
strictEqual(value, 1);
26+
}
27+
28+
testWait().then(common.mustCall());
29+
30+
async function testCancelableWait1() {
31+
const ac = new AbortController();
32+
const wait = scheduler.wait(1e6, { signal: ac.signal });
33+
ac.abort();
34+
await rejects(wait, {
35+
code: 'ABORT_ERR',
36+
message: 'The operation was aborted',
37+
});
38+
}
39+
40+
testCancelableWait1().then(common.mustCall());
41+
42+
async function testCancelableWait2() {
43+
const wait = scheduler.wait(10000, { signal: AbortSignal.abort() });
44+
await rejects(wait, {
45+
code: 'ABORT_ERR',
46+
message: 'The operation was aborted',
47+
});
48+
}
49+
50+
testCancelableWait2().then(common.mustCall());

0 commit comments

Comments
 (0)