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>
  • Loading branch information
jasnell committed Nov 21, 2021
1 parent 129c12e commit 55ebbfe
Show file tree
Hide file tree
Showing 3 changed files with 113 additions and 0 deletions.
37 changes: 37 additions & 0 deletions doc/api/timers.md
Original file line number Diff line number Diff line change
Expand Up @@ -472,7 +472,44 @@ const interval = 100;
})();
```

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

> 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()`

> 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
26 changes: 26 additions & 0 deletions lib/timers/promises.js
Original file line number Diff line number Diff line change
Expand Up @@ -170,8 +170,34 @@ 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 {
/**
* @returns {Promise<void>}
*/
yield() { return setImmediate(); }

/**
* @typedef {import('../internal/abort_controller').AbortSignal} AbortSignal
* @param {number} delay
* @param {{ signal? : AbortSignal }} [options]
* @returns {Promise<void>}
*/
wait(delay, options) {

return setTimeout(delay, undefined, { signal: options?.signal });
}
}

module.exports = {
setTimeout,
setImmediate,
setInterval,
scheduler: new Scheduler(),
};
50 changes: 50 additions & 0 deletions test/parallel/test-timers-promises-scheduler.js
Original file line number Diff line number Diff line change
@@ -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 55ebbfe

Please sign in to comment.