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:
parent
0ef6956225
commit
bfbdc84738
@ -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
|
||||
|
@ -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) {
|
||||
|
@ -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());
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user