diff --git a/doc/api/timers.md b/doc/api/timers.md index cb26e4b16a3bae..de983b83ba9568 100644 --- a/doc/api/timers.md +++ b/doc/api/timers.md @@ -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 diff --git a/lib/internal/timers.js b/lib/internal/timers.js index ead8bec819fbaa..fc1a23839629ac 100644 --- a/lib/internal/timers.js +++ b/lib/internal/timers.js @@ -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, diff --git a/lib/timers.js b/lib/timers.js index 53e934f3c9ac7c..878f360ab0c04e 100644 --- a/lib/timers.js +++ b/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) diff --git a/lib/timers/promises.js b/lib/timers/promises.js new file mode 100644 index 00000000000000..78197fe86f6e22 --- /dev/null +++ b/lib/timers/promises.js @@ -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, +}; diff --git a/node.gyp b/node.gyp index 0af72d48c25150..6d9e9cb3fc009e 100644 --- a/node.gyp +++ b/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', diff --git a/test/parallel/test-timers-promisified.js b/test/parallel/test-timers-promisified.js index f46dc3f24d20eb..6cd932a2d169ff 100644 --- a/test/parallel/test-timers-promisified.js +++ b/test/parallel/test-timers-promisified.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, ''); + })); }