Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

timers: introduce timers/promises #33950

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
34 changes: 34 additions & 0 deletions doc/api/timers.md
Expand Up @@ -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
Expand Down
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 @@ -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,
Expand Down
145 changes: 19 additions & 126 deletions lib/timers.js
Expand Up @@ -23,15 +23,9 @@

const {
MathTrunc,
Promise,
Object,
} = primordials;

const {
codes: { ERR_INVALID_ARG_TYPE }
} = require('internal/errors');

let DOMException;

const {
immediateInfo,
toggleImmediateRef
Expand All @@ -40,13 +34,13 @@ const L = require('internal/linkedlist');
const {
async_id_symbol,
Timeout,
Immediate,
decRefCount,
immediateInfoFields: {
kCount,
kRefCount
},
kRefed,
initAsyncResource,
getTimerDuration,
timerListMap,
timerListQueue,
Expand All @@ -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.
Expand Down Expand Up @@ -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);

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

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