0
0
mirror of https://github.com/nodejs/node.git synced 2024-11-29 07:00:59 +01:00

timers: allow promisified timeouts/immediates to be canceled

Using the new experimental AbortController...

Signed-off-by: James M Snell <jasnell@gmail.com>

PR-URL: https://github.com/nodejs/node/pull/33833
Reviewed-By: Anna Henningsen <anna@addaleax.net>
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Gus Caplan <me@gus.host>
This commit is contained in:
James M Snell 2020-06-10 14:14:23 -07:00
parent 0ef6956225
commit bfbdc84738
No known key found for this signature in database
GPG Key ID: 7341B15C070877AC
3 changed files with 171 additions and 6 deletions

View File

@ -232,8 +232,47 @@ The [`setImmediate()`][], [`setInterval()`][], and [`setTimeout()`][] methods
each return objects that represent the scheduled timers. These can be used to
cancel the timer and prevent it from triggering.
It is not possible to cancel timers that were created using the promisified
variants of [`setImmediate()`][], [`setTimeout()`][].
For the promisified variants of [`setImmediate()`][] and [`setTimeout()`][],
an [`AbortController`][] may be used to cancel the timer. When canceled, the
returned Promises will be rejected with an `'AbortError'`.
For `setImmediate()`:
```js
const util = require('util');
const setImmediatePromise = util.promisify(setImmediate);
const ac = new AbortController();
const signal = ac.signal;
setImmediatePromise('foobar', { signal })
.then(console.log)
.catch((err) => {
if (err.message === 'AbortError')
console.log('The immediate was aborted');
});
ac.abort();
```
For `setTimeout()`:
```js
const util = require('util');
const setTimeoutPromise = util.promisify(setTimeout);
const ac = new AbortController();
const signal = ac.signal;
setTimeoutPromise(1000, 'foobar', { signal })
.then(console.log)
.catch((err) => {
if (err.message === 'AbortError')
console.log('The timeout was aborted');
});
ac.abort();
```
### `clearImmediate(immediate)`
<!-- YAML
@ -264,6 +303,7 @@ added: v0.0.1
Cancels a `Timeout` object created by [`setTimeout()`][].
[Event Loop]: https://nodejs.org/en/docs/guides/event-loop-timers-and-nexttick/#setimmediate-vs-settimeout
[`AbortController`]: globals.html#globals_class_abortcontroller
[`TypeError`]: errors.html#errors_class_typeerror
[`clearImmediate()`]: timers.html#timers_clearimmediate_immediate
[`clearInterval()`]: timers.html#timers_clearinterval_timeout

View File

@ -26,6 +26,12 @@ const {
Promise,
} = primordials;
const {
codes: { ERR_INVALID_ARG_TYPE }
} = require('internal/errors');
let DOMException;
const {
immediateInfo,
toggleImmediateRef
@ -118,6 +124,11 @@ function enroll(item, msecs) {
* DOM-style timers
*/
function lazyDOMException(message) {
if (DOMException === undefined)
DOMException = internalBinding('messaging').DOMException;
return new DOMException(message);
}
function setTimeout(callback, after, arg1, arg2, arg3) {
validateCallback(callback);
@ -149,11 +160,40 @@ function setTimeout(callback, after, arg1, arg2, arg3) {
return timeout;
}
setTimeout[customPromisify] = function(after, value) {
setTimeout[customPromisify] = function(after, value, options = {}) {
const args = value !== undefined ? [value] : value;
return new Promise((resolve) => {
if (options == null || typeof options !== 'object') {
return Promise.reject(
new ERR_INVALID_ARG_TYPE(
'options',
'Object',
options));
}
const { signal } = options;
if (signal !== undefined &&
(signal === null ||
typeof signal !== 'object' ||
!('aborted' in signal))) {
return Promise.reject(
new ERR_INVALID_ARG_TYPE(
'options.signal',
'AbortSignal',
signal));
}
// TODO(@jasnell): If a decision is made that this cannot be backported
// to 12.x, then this can be converted to use optional chaining to
// simplify the check.
if (signal && signal.aborted)
return Promise.reject(lazyDOMException('AbortError'));
return new Promise((resolve, reject) => {
const timeout = new Timeout(resolve, after, args, false, true);
insert(timeout, timeout._idleTimeout);
if (signal) {
signal.addEventListener('abort', () => {
clearTimeout(timeout);
reject(lazyDOMException('AbortError'));
}, { once: true });
}
});
};
@ -272,8 +312,39 @@ function setImmediate(callback, arg1, arg2, arg3) {
return new Immediate(callback, args);
}
setImmediate[customPromisify] = function(value) {
return new Promise((resolve) => new Immediate(resolve, [value]));
setImmediate[customPromisify] = function(value, options = {}) {
if (options == null || typeof options !== 'object') {
return Promise.reject(
new ERR_INVALID_ARG_TYPE(
'options',
'Object',
options));
}
const { signal } = options;
if (signal !== undefined &&
(signal === null ||
typeof signal !== 'object' ||
!('aborted' in signal))) {
return Promise.reject(
new ERR_INVALID_ARG_TYPE(
'options.signal',
'AbortSignal',
signal));
}
// TODO(@jasnell): If a decision is made that this cannot be backported
// to 12.x, then this can be converted to use optional chaining to
// simplify the check.
if (signal && signal.aborted)
return Promise.reject(lazyDOMException('AbortError'));
return new Promise((resolve, reject) => {
const immediate = new Immediate(resolve, [value]);
if (signal) {
signal.addEventListener('abort', () => {
clearImmediate(immediate);
reject(lazyDOMException('AbortError'));
}, { once: true });
}
});
};
function clearImmediate(immediate) {

View File

@ -1,3 +1,4 @@
// Flags: --no-warnings
'use strict';
const common = require('../common');
const assert = require('assert');
@ -36,3 +37,56 @@ const setImmediate = promisify(timers.setImmediate);
assert.strictEqual(value, 'foobar');
}));
}
{
const ac = new AbortController();
const signal = ac.signal;
assert.rejects(setTimeout(10, undefined, { signal }), /AbortError/);
ac.abort();
}
{
const ac = new AbortController();
const signal = ac.signal;
ac.abort(); // Abort in advance
assert.rejects(setTimeout(10, undefined, { signal }), /AbortError/);
}
{
const ac = new AbortController();
const signal = ac.signal;
assert.rejects(setImmediate(10, { signal }), /AbortError/);
ac.abort();
}
{
const ac = new AbortController();
const signal = ac.signal;
ac.abort(); // Abort in advance
assert.rejects(setImmediate(10, { signal }), /AbortError/);
}
{
Promise.all(
[1, '', false, Infinity].map((i) => assert.rejects(setImmediate(10, i)), {
code: 'ERR_INVALID_ARG_TYPE'
})).then(common.mustCall());
Promise.all(
[1, '', false, Infinity, null, {}].map(
(signal) => assert.rejects(setImmediate(10, { signal })), {
code: 'ERR_INVALID_ARG_TYPE'
})).then(common.mustCall());
Promise.all(
[1, '', false, Infinity].map(
(i) => assert.rejects(setTimeout(10, null, i)), {
code: 'ERR_INVALID_ARG_TYPE'
})).then(common.mustCall());
Promise.all(
[1, '', false, Infinity, null, {}].map(
(signal) => assert.rejects(setTimeout(10, null, { signal })), {
code: 'ERR_INVALID_ARG_TYPE'
})).then(common.mustCall());
}