Skip to content

Commit

Permalink
timers: move promisified timers implementations
Browse files Browse the repository at this point in the history
Move the promisified timers implementations into a new internal.
submodule.

Also adds `ref` option to the promisified versions.

Signed-off-by: James M Snell <jasnell@gmail.com>

PR-URL: nodejs#33950
Reviewed-By: Denys Otrishko <shishugi@gmail.com>
Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com>
  • Loading branch information
jasnell authored and targos committed Apr 26, 2021
1 parent 0ce52b7 commit e2f13e7
Show file tree
Hide file tree
Showing 5 changed files with 194 additions and 128 deletions.
44 changes: 43 additions & 1 deletion lib/internal/timers.js
Expand Up @@ -84,7 +84,8 @@ const {
scheduleTimer,
toggleTimerRef,
getLibuvNow,
immediateInfo
immediateInfo,
toggleImmediateRef
} = internalBinding('timers');

const {
Expand Down Expand Up @@ -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,
Expand Down
124 changes: 124 additions & 0 deletions 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,
};
147 changes: 20 additions & 127 deletions lib/timers.js
Expand Up @@ -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
Expand All @@ -42,14 +36,14 @@ const L = require('internal/linkedlist');
const {
async_id_symbol,
Timeout,
Immediate,
decRefCount,
immediateInfoFields: {
kCount,
kRefCount
},
kRefed,
kHasPrimitive,
initAsyncResource,
getTimerDuration,
timerListMap,
timerListQueue,
Expand All @@ -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.
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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);

Expand All @@ -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)
Expand Down

0 comments on commit e2f13e7

Please sign in to comment.