mirror of
https://github.com/nodejs/node.git
synced 2024-11-21 10:59:27 +00:00
timers: introduce timers/promises
Move the promisified timers implementations into a new sub-module to avoid the need to promisify. The promisified versions now return the timers/promises versions. Also adds `ref` option to the promisified versions ```js const { setTimeout, setImmediate } = require('timers/promises'); setTimeout(10, null, { ref: false }) .then(console.log); setImmediate(null, { ref: false }) .then(console.log); ``` Signed-off-by: James M Snell <jasnell@gmail.com> PR-URL: https://github.com/nodejs/node/pull/33950 Reviewed-By: Denys Otrishko <shishugi@gmail.com> Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com>
This commit is contained in:
parent
bfc0e3f0b0
commit
a8904e8eee
@ -302,6 +302,40 @@ added: v0.0.1
|
||||
|
||||
Cancels a `Timeout` object created by [`setTimeout()`][].
|
||||
|
||||
## Timers Promises API
|
||||
|
||||
> Stability: 1 - Experimental
|
||||
|
||||
The `timers/promises` API provides an alternative set of timer functions
|
||||
that return `Promise` objects. The API is accessible via
|
||||
`require('timers/promises')`.
|
||||
|
||||
```js
|
||||
const timersPromises = require('timers/promises');
|
||||
```
|
||||
|
||||
### `timersPromises.setTimeout(delay\[, value\[, options\]\])
|
||||
|
||||
* `delay` {number} The number of milliseconds to wait before resolving the
|
||||
`Promise`.
|
||||
* `value` {any} A value with which the `Promise` is resolved.
|
||||
* `options` {Object}
|
||||
* `ref` {boolean} Set to `false` to indicate that the scheduled `Timeout`
|
||||
should not require the Node.js event loop to remain active.
|
||||
**Default**: `true`.
|
||||
* `signal` {AbortSignal} An optional `AbortSignal` that can be used to
|
||||
cancel the scheduled `Timeout`.
|
||||
|
||||
### `timersPromises.setImmediate(\[value\[, options\]\])
|
||||
|
||||
* `value` {any} A value with which the `Promise` is resolved.
|
||||
* `options` {Object}
|
||||
* `ref` {boolean} Set to `false` to indicate that the scheduled `Immediate`
|
||||
should not require the Node.js event loop to remain active.
|
||||
**Default**: `true`.
|
||||
* `signal` {AbortSignal} An optional `AbortSignal` that can be used to
|
||||
cancel the scheduled `Immediate`.
|
||||
|
||||
[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
|
||||
|
@ -84,7 +84,8 @@ const {
|
||||
scheduleTimer,
|
||||
toggleTimerRef,
|
||||
getLibuvNow,
|
||||
immediateInfo
|
||||
immediateInfo,
|
||||
toggleImmediateRef
|
||||
} = internalBinding('timers');
|
||||
|
||||
const {
|
||||
@ -590,12 +591,53 @@ function getTimerCallbacks(runNextTicks) {
|
||||
};
|
||||
}
|
||||
|
||||
class Immediate {
|
||||
constructor(callback, args) {
|
||||
this._idleNext = null;
|
||||
this._idlePrev = null;
|
||||
this._onImmediate = callback;
|
||||
this._argv = args;
|
||||
this._destroyed = false;
|
||||
this[kRefed] = false;
|
||||
|
||||
initAsyncResource(this, 'Immediate');
|
||||
|
||||
this.ref();
|
||||
immediateInfo[kCount]++;
|
||||
|
||||
immediateQueue.append(this);
|
||||
}
|
||||
|
||||
ref() {
|
||||
if (this[kRefed] === false) {
|
||||
this[kRefed] = true;
|
||||
if (immediateInfo[kRefCount]++ === 0)
|
||||
toggleImmediateRef(true);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
unref() {
|
||||
if (this[kRefed] === true) {
|
||||
this[kRefed] = false;
|
||||
if (--immediateInfo[kRefCount] === 0)
|
||||
toggleImmediateRef(false);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
hasRef() {
|
||||
return !!this[kRefed];
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
TIMEOUT_MAX,
|
||||
kTimeout: Symbol('timeout'), // For hiding Timeouts on other internals.
|
||||
async_id_symbol,
|
||||
trigger_async_id_symbol,
|
||||
Timeout,
|
||||
Immediate,
|
||||
kRefed,
|
||||
initAsyncResource,
|
||||
setUnrefTimeout,
|
||||
|
145
lib/timers.js
145
lib/timers.js
@ -23,15 +23,9 @@
|
||||
|
||||
const {
|
||||
MathTrunc,
|
||||
Promise,
|
||||
Object,
|
||||
} = primordials;
|
||||
|
||||
const {
|
||||
codes: { ERR_INVALID_ARG_TYPE }
|
||||
} = require('internal/errors');
|
||||
|
||||
let DOMException;
|
||||
|
||||
const {
|
||||
immediateInfo,
|
||||
toggleImmediateRef
|
||||
@ -40,13 +34,13 @@ const L = require('internal/linkedlist');
|
||||
const {
|
||||
async_id_symbol,
|
||||
Timeout,
|
||||
Immediate,
|
||||
decRefCount,
|
||||
immediateInfoFields: {
|
||||
kCount,
|
||||
kRefCount
|
||||
},
|
||||
kRefed,
|
||||
initAsyncResource,
|
||||
getTimerDuration,
|
||||
timerListMap,
|
||||
timerListQueue,
|
||||
@ -64,6 +58,8 @@ let debug = require('internal/util/debuglog').debuglog('timer', (fn) => {
|
||||
});
|
||||
const { validateCallback } = require('internal/validators');
|
||||
|
||||
let timersPromises;
|
||||
|
||||
const {
|
||||
destroyHooksExist,
|
||||
// The needed emit*() functions.
|
||||
@ -124,12 +120,6 @@ 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);
|
||||
|
||||
@ -160,44 +150,14 @@ function setTimeout(callback, after, arg1, arg2, arg3) {
|
||||
return timeout;
|
||||
}
|
||||
|
||||
setTimeout[customPromisify] = function(after, value, options = {}) {
|
||||
const args = value !== undefined ? [value] : value;
|
||||
if (options == null || typeof options !== 'object') {
|
||||
return Promise.reject(
|
||||
new ERR_INVALID_ARG_TYPE(
|
||||
'options',
|
||||
'Object',
|
||||
options));
|
||||
Object.defineProperty(setTimeout, customPromisify, {
|
||||
enumerable: true,
|
||||
get() {
|
||||
if (!timersPromises)
|
||||
timersPromises = require('timers/promises');
|
||||
return timersPromises.setTimeout;
|
||||
}
|
||||
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', () => {
|
||||
if (!timeout._destroyed) {
|
||||
clearTimeout(timeout);
|
||||
reject(lazyDOMException('AbortError'));
|
||||
}
|
||||
}, { once: true });
|
||||
}
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
function clearTimeout(timer) {
|
||||
if (timer && timer._onTimeout) {
|
||||
@ -248,46 +208,6 @@ Timeout.prototype.close = function() {
|
||||
return this;
|
||||
};
|
||||
|
||||
const Immediate = class Immediate {
|
||||
constructor(callback, args) {
|
||||
this._idleNext = null;
|
||||
this._idlePrev = null;
|
||||
this._onImmediate = callback;
|
||||
this._argv = args;
|
||||
this._destroyed = false;
|
||||
this[kRefed] = false;
|
||||
|
||||
initAsyncResource(this, 'Immediate');
|
||||
|
||||
this.ref();
|
||||
immediateInfo[kCount]++;
|
||||
|
||||
immediateQueue.append(this);
|
||||
}
|
||||
|
||||
ref() {
|
||||
if (this[kRefed] === false) {
|
||||
this[kRefed] = true;
|
||||
if (immediateInfo[kRefCount]++ === 0)
|
||||
toggleImmediateRef(true);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
unref() {
|
||||
if (this[kRefed] === true) {
|
||||
this[kRefed] = false;
|
||||
if (--immediateInfo[kRefCount] === 0)
|
||||
toggleImmediateRef(false);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
hasRef() {
|
||||
return !!this[kRefed];
|
||||
}
|
||||
};
|
||||
|
||||
function setImmediate(callback, arg1, arg2, arg3) {
|
||||
validateCallback(callback);
|
||||
|
||||
@ -314,42 +234,15 @@ function setImmediate(callback, arg1, arg2, arg3) {
|
||||
return new Immediate(callback, args);
|
||||
}
|
||||
|
||||
setImmediate[customPromisify] = function(value, options = {}) {
|
||||
if (options == null || typeof options !== 'object') {
|
||||
return Promise.reject(
|
||||
new ERR_INVALID_ARG_TYPE(
|
||||
'options',
|
||||
'Object',
|
||||
options));
|
||||
Object.defineProperty(setImmediate, customPromisify, {
|
||||
enumerable: true,
|
||||
get() {
|
||||
if (!timersPromises)
|
||||
timersPromises = require('timers/promises');
|
||||
return timersPromises.setImmediate;
|
||||
}
|
||||
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', () => {
|
||||
if (!immediate._destroyed) {
|
||||
clearImmediate(immediate);
|
||||
reject(lazyDOMException('AbortError'));
|
||||
}
|
||||
}, { once: true });
|
||||
}
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
function clearImmediate(immediate) {
|
||||
if (!immediate || immediate._destroyed)
|
||||
|
124
lib/timers/promises.js
Normal file
124
lib/timers/promises.js
Normal file
@ -0,0 +1,124 @@
|
||||
'use strict';
|
||||
|
||||
const {
|
||||
Promise,
|
||||
PromiseReject,
|
||||
} = primordials;
|
||||
|
||||
const {
|
||||
Timeout,
|
||||
Immediate,
|
||||
insert
|
||||
} = require('internal/timers');
|
||||
|
||||
const {
|
||||
hideStackFrames,
|
||||
codes: { ERR_INVALID_ARG_TYPE }
|
||||
} = require('internal/errors');
|
||||
|
||||
let DOMException;
|
||||
|
||||
const lazyDOMException = hideStackFrames((message) => {
|
||||
if (DOMException === undefined)
|
||||
DOMException = internalBinding('messaging').DOMException;
|
||||
return new DOMException(message);
|
||||
});
|
||||
|
||||
function setTimeout(after, value, options = {}) {
|
||||
const args = value !== undefined ? [value] : value;
|
||||
if (options == null || typeof options !== 'object') {
|
||||
return PromiseReject(
|
||||
new ERR_INVALID_ARG_TYPE(
|
||||
'options',
|
||||
'Object',
|
||||
options));
|
||||
}
|
||||
const { signal, ref = true } = options;
|
||||
if (signal !== undefined &&
|
||||
(signal === null ||
|
||||
typeof signal !== 'object' ||
|
||||
!('aborted' in signal))) {
|
||||
return PromiseReject(
|
||||
new ERR_INVALID_ARG_TYPE(
|
||||
'options.signal',
|
||||
'AbortSignal',
|
||||
signal));
|
||||
}
|
||||
if (typeof ref !== 'boolean') {
|
||||
return PromiseReject(
|
||||
new ERR_INVALID_ARG_TYPE(
|
||||
'options.ref',
|
||||
'boolean',
|
||||
ref));
|
||||
}
|
||||
// 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 PromiseReject(lazyDOMException('AbortError'));
|
||||
return new Promise((resolve, reject) => {
|
||||
const timeout = new Timeout(resolve, after, args, false, true);
|
||||
if (!ref) timeout.unref();
|
||||
insert(timeout, timeout._idleTimeout);
|
||||
if (signal) {
|
||||
signal.addEventListener('abort', () => {
|
||||
if (!timeout._destroyed) {
|
||||
// eslint-disable-next-line no-undef
|
||||
clearTimeout(timeout);
|
||||
reject(lazyDOMException('AbortError'));
|
||||
}
|
||||
}, { once: true });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function setImmediate(value, options = {}) {
|
||||
if (options == null || typeof options !== 'object') {
|
||||
return PromiseReject(
|
||||
new ERR_INVALID_ARG_TYPE(
|
||||
'options',
|
||||
'Object',
|
||||
options));
|
||||
}
|
||||
const { signal, ref = true } = options;
|
||||
if (signal !== undefined &&
|
||||
(signal === null ||
|
||||
typeof signal !== 'object' ||
|
||||
!('aborted' in signal))) {
|
||||
return PromiseReject(
|
||||
new ERR_INVALID_ARG_TYPE(
|
||||
'options.signal',
|
||||
'AbortSignal',
|
||||
signal));
|
||||
}
|
||||
if (typeof ref !== 'boolean') {
|
||||
return PromiseReject(
|
||||
new ERR_INVALID_ARG_TYPE(
|
||||
'options.ref',
|
||||
'boolean',
|
||||
ref));
|
||||
}
|
||||
// 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 PromiseReject(lazyDOMException('AbortError'));
|
||||
return new Promise((resolve, reject) => {
|
||||
const immediate = new Immediate(resolve, [value]);
|
||||
if (!ref) immediate.unref();
|
||||
if (signal) {
|
||||
signal.addEventListener('abort', () => {
|
||||
if (!immediate._destroyed) {
|
||||
// eslint-disable-next-line no-undef
|
||||
clearImmediate(immediate);
|
||||
reject(lazyDOMException('AbortError'));
|
||||
}
|
||||
}, { once: true });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
setTimeout,
|
||||
setImmediate,
|
||||
};
|
1
node.gyp
1
node.gyp
@ -85,6 +85,7 @@
|
||||
'lib/_stream_wrap.js',
|
||||
'lib/string_decoder.js',
|
||||
'lib/sys.js',
|
||||
'lib/timers/promises.js',
|
||||
'lib/timers.js',
|
||||
'lib/tls.js',
|
||||
'lib/_tls_common.js',
|
||||
|
@ -4,11 +4,18 @@ const common = require('../common');
|
||||
const assert = require('assert');
|
||||
const timers = require('timers');
|
||||
const { promisify } = require('util');
|
||||
const child_process = require('child_process');
|
||||
|
||||
const timerPromises = require('timers/promises');
|
||||
|
||||
/* eslint-disable no-restricted-syntax */
|
||||
|
||||
const setTimeout = promisify(timers.setTimeout);
|
||||
const setImmediate = promisify(timers.setImmediate);
|
||||
const exec = promisify(child_process.exec);
|
||||
|
||||
assert.strictEqual(setTimeout, timerPromises.setTimeout);
|
||||
assert.strictEqual(setImmediate, timerPromises.setImmediate);
|
||||
|
||||
process.on('multipleResolves', common.mustNotCall());
|
||||
|
||||
@ -108,4 +115,26 @@ process.on('multipleResolves', common.mustNotCall());
|
||||
(signal) => assert.rejects(setTimeout(10, null, { signal })), {
|
||||
code: 'ERR_INVALID_ARG_TYPE'
|
||||
})).then(common.mustCall());
|
||||
|
||||
Promise.all(
|
||||
[1, '', Infinity, null, {}].map(
|
||||
(ref) => assert.rejects(setTimeout(10, null, { ref })), {
|
||||
code: 'ERR_INVALID_ARG_TYPE'
|
||||
})).then(common.mustCall());
|
||||
}
|
||||
|
||||
{
|
||||
exec(`${process.execPath} -pe "const assert = require('assert');` +
|
||||
'require(\'timers/promises\').setTimeout(1000, null, { ref: false }).' +
|
||||
'then(assert.fail)"').then(common.mustCall(({ stderr }) => {
|
||||
assert.strictEqual(stderr, '');
|
||||
}));
|
||||
}
|
||||
|
||||
{
|
||||
exec(`${process.execPath} -pe "const assert = require('assert');` +
|
||||
'require(\'timers/promises\').setImmediate(null, { ref: false }).' +
|
||||
'then(assert.fail)"').then(common.mustCall(({ stderr }) => {
|
||||
assert.strictEqual(stderr, '');
|
||||
}));
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user