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](https://github.com/whatwg/html/issues/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: https://github.com/nodejs/node/pull/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>
This commit is contained in:
James M Snell 2021-11-21 08:04:21 -08:00
parent 6ef6bdf1fd
commit 8bce46aff1
No known key found for this signature in database
GPG Key ID: 7341B15C070877AC
3 changed files with 141 additions and 1 deletions

View File

@ -472,7 +472,52 @@ const interval = 100;
})();
```
### `timersPromises.scheduler.wait(delay[, options])`
<!-- YAML
added: REPLACEME
-->
> Stability: 1 - Experimental
* `delay` {number} The number of milliseconds to wait before resolving the
promise.
* `options` {Object}
* `signal` {AbortSignal} An optional `AbortSignal` that can be used to
cancel waiting.
* Returns: {Promise}
An experimental API defined by the [Scheduling APIs][] draft specification
being developed as a standard Web Platform API.
Calling `timersPromises.scheduler.wait(delay, options)` is roughly equivalent
to calling `timersPromises.setTimeout(delay, undefined, options)` except that
the `ref` option is not supported.
```mjs
import { scheduler } from 'timers/promises';
await scheduler.wait(1000); // Wait one second before continuing
```
### `timersPromises.scheduler.yield()`
<!-- YAML
added: REPLACEME
-->
> Stability: 1 - Experimental
* Returns: {Promise}
An experimental API defined by the [Scheduling APIs][] draft specification
being developed as a standard Web Platform API.
Calling `timersPromises.scheduler.yield()` is equivalent to calling
`timersPromises.setImmediate()` with no arguments.
[Event Loop]: https://nodejs.org/en/docs/guides/event-loop-timers-and-nexttick/#setimmediate-vs-settimeout
[Scheduling APIs]: https://github.com/WICG/scheduling-apis
[`AbortController`]: globals.md#class-abortcontroller
[`TypeError`]: errors.md#class-typeerror
[`clearImmediate()`]: #clearimmediateimmediate

View File

@ -4,7 +4,9 @@ const {
FunctionPrototypeBind,
Promise,
PromiseReject,
ReflectConstruct,
SafePromisePrototypeFinally,
Symbol,
} = primordials;
const {
@ -15,7 +17,11 @@ const {
const {
AbortError,
codes: { ERR_INVALID_ARG_TYPE }
codes: {
ERR_ILLEGAL_CONSTRUCTOR,
ERR_INVALID_ARG_TYPE,
ERR_INVALID_THIS,
}
} = require('internal/errors');
const {
@ -24,6 +30,8 @@ const {
validateObject,
} = require('internal/validators');
const kScheduler = Symbol('kScheduler');
function cancelListenerHandler(clear, reject, signal) {
if (!this._destroyed) {
clear(this);
@ -173,8 +181,45 @@ async function* setInterval(after, value, options = {}) {
}
}
// TODO(@jasnell): Scheduler is an API currently being discussed by WICG
// for Web Platform standardization: https://github.com/WICG/scheduling-apis
// The scheduler.yield() and scheduler.wait() methods correspond roughly to
// the awaitable setTimeout and setImmediate implementations here. This api
// should be considered to be experimental until the spec for these are
// finalized. Note, also, that Scheduler is expected to be defined as a global,
// but while the API is experimental we shouldn't expose it as such.
class Scheduler {
constructor() {
throw new ERR_ILLEGAL_CONSTRUCTOR();
}
/**
* @returns {Promise<void>}
*/
yield() {
if (!this[kScheduler])
throw new ERR_INVALID_THIS('Scheduler');
return setImmediate();
}
/**
* @typedef {import('../internal/abort_controller').AbortSignal} AbortSignal
* @param {number} delay
* @param {{ signal?: AbortSignal }} [options]
* @returns {Promise<void>}
*/
wait(delay, options) {
if (!this[kScheduler])
throw new ERR_INVALID_THIS('Scheduler');
return setTimeout(delay, undefined, { signal: options?.signal });
}
}
module.exports = {
setTimeout,
setImmediate,
setInterval,
scheduler: ReflectConstruct(function() {
this[kScheduler] = true;
}, [], Scheduler),
};

View File

@ -0,0 +1,50 @@
'use strict';
const common = require('../common');
const { scheduler } = require('timers/promises');
const { setTimeout } = require('timers');
const {
strictEqual,
rejects,
} = require('assert');
async function testYield() {
await scheduler.yield();
process.emit('foo');
}
testYield().then(common.mustCall());
queueMicrotask(common.mustCall(() => {
process.addListener('foo', common.mustCall());
}));
async function testWait() {
let value = 0;
setTimeout(() => value++, 10);
await scheduler.wait(15);
strictEqual(value, 1);
}
testWait().then(common.mustCall());
async function testCancelableWait1() {
const ac = new AbortController();
const wait = scheduler.wait(1e6, { signal: ac.signal });
ac.abort();
await rejects(wait, {
code: 'ABORT_ERR',
message: 'The operation was aborted',
});
}
testCancelableWait1().then(common.mustCall());
async function testCancelableWait2() {
const wait = scheduler.wait(10000, { signal: AbortSignal.abort() });
await rejects(wait, {
code: 'ABORT_ERR',
message: 'The operation was aborted',
});
}
testCancelableWait2().then(common.mustCall());