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: add experimental scheduler api #40909

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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
45 changes: 45 additions & 0 deletions doc/api/timers.md
Expand Up @@ -472,7 +472,52 @@ const interval = 100;
})();
```

### `timersPromises.scheduler.wait(delay[, options])`

jasnell marked this conversation as resolved.
Show resolved Hide resolved
<!-- 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()`

jasnell marked this conversation as resolved.
Show resolved Hide resolved
<!-- 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());