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:
James M Snell 2020-06-18 13:22:17 -07:00
parent bfc0e3f0b0
commit a8904e8eee
No known key found for this signature in database
GPG Key ID: 7341B15C070877AC
6 changed files with 250 additions and 127 deletions

View File

@ -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

View File

@ -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,

View File

@ -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
View 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,
};

View File

@ -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',

View File

@ -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, '');
}));
}