Skip to content

Commit

Permalink
timers: add experimental scheduler api
Browse files Browse the repository at this point in the history
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>
  • Loading branch information
jasnell authored and danielleadams committed Dec 13, 2021
1 parent 2125449 commit 0caa348
Show file tree
Hide file tree
Showing 3 changed files with 141 additions and 1 deletion.
45 changes: 45 additions & 0 deletions doc/api/timers.md
Expand Up @@ -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
Expand Down
47 changes: 46 additions & 1 deletion lib/timers/promises.js
Expand Up @@ -4,7 +4,9 @@ const {
FunctionPrototypeBind,
Promise,
PromiseReject,
ReflectConstruct,
SafePromisePrototypeFinally,
Symbol,
} = primordials;

const {
Expand All @@ -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 {
Expand All @@ -24,6 +30,8 @@ const {
validateObject,
} = require('internal/validators');

const kScheduler = Symbol('kScheduler');

function cancelListenerHandler(clear, reject, signal) {
if (!this._destroyed) {
clear(this);
Expand Down Expand Up @@ -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),
};
50 changes: 50 additions & 0 deletions test/parallel/test-timers-promises-scheduler.js
@@ -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());

0 comments on commit 0caa348

Please sign in to comment.