diff --git a/lib/internal/timers.js b/lib/internal/timers.js index 045994a23d0210..d39bab9d074895 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 { @@ -593,12 +594,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, kHasPrimitive, initAsyncResource, diff --git a/lib/internal/timers/promises.js b/lib/internal/timers/promises.js new file mode 100644 index 00000000000000..78197fe86f6e22 --- /dev/null +++ b/lib/internal/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/lib/timers.js b/lib/timers.js index 60e8bc5c0d073d..27f0f11f095f62 100644 --- a/lib/timers.js +++ b/lib/timers.js @@ -24,16 +24,10 @@ const { ObjectCreate, MathTrunc, - Promise, - SymbolToPrimitive + Object, + SymbolToPrimitive, } = primordials; -const { - codes: { ERR_INVALID_ARG_TYPE } -} = require('internal/errors'); - -let DOMException; - const { immediateInfo, toggleImmediateRef @@ -42,6 +36,7 @@ const L = require('internal/linkedlist'); const { async_id_symbol, Timeout, + Immediate, decRefCount, immediateInfoFields: { kCount, @@ -49,7 +44,6 @@ const { }, kRefed, kHasPrimitive, - initAsyncResource, getTimerDuration, timerListMap, timerListQueue, @@ -67,6 +61,8 @@ let debug = require('internal/util/debuglog').debuglog('timer', (fn) => { }); const { validateCallback } = require('internal/validators'); +let timersPromises; + const { destroyHooksExist, // The needed emit*() functions. @@ -135,12 +131,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); @@ -171,44 +161,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('internal/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) { @@ -276,46 +236,6 @@ Timeout.prototype[SymbolToPrimitive] = function() { return id; }; -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); @@ -342,42 +262,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('internal/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/node.gyp b/node.gyp index fbc26fed51cf5d..ab2480c4657540 100644 --- a/node.gyp +++ b/node.gyp @@ -199,6 +199,7 @@ 'lib/internal/source_map/source_map.js', 'lib/internal/source_map/source_map_cache.js', 'lib/internal/test/binding.js', + 'lib/internal/timers/promises.js', 'lib/internal/timers.js', 'lib/internal/tls.js', 'lib/internal/trace_events_async_hooks.js', diff --git a/test/parallel/test-timers-promisified.js b/test/parallel/test-timers-promisified.js index 286a7fd8965d36..1b1e98d628543a 100644 --- a/test/parallel/test-timers-promisified.js +++ b/test/parallel/test-timers-promisified.js @@ -108,4 +108,10 @@ 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()); }