Skip to content

Commit

Permalink
timers: introduce timers/promises
Browse files Browse the repository at this point in the history
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>
  • Loading branch information
jasnell committed Jun 22, 2020
1 parent 8ef86a9 commit 78edbf6
Show file tree
Hide file tree
Showing 6 changed files with 235 additions and 129 deletions.
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
132 changes: 4 additions & 128 deletions lib/timers.js
Expand Up @@ -23,15 +23,8 @@

const {
MathTrunc,
Promise,
} = primordials;

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

let DOMException;

const {
immediateInfo,
toggleImmediateRef
Expand All @@ -40,13 +33,13 @@ const L = require('internal/linkedlist');
const {
async_id_symbol,
Timeout,
Immediate,
decRefCount,
immediateInfoFields: {
kCount,
kRefCount
},
kRefed,
initAsyncResource,
getTimerDuration,
timerListMap,
timerListQueue,
Expand All @@ -59,6 +52,7 @@ const {
promisify: { custom: customPromisify },
deprecate
} = require('internal/util');
const timersPromises = require('timers/promises');
let debug = require('internal/util/debuglog').debuglog('timer', (fn) => {
debug = fn;
});
Expand Down Expand Up @@ -124,12 +118,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 +148,7 @@ 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));
}
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 });
}
});
};
setTimeout[customPromisify] = timersPromises.setTimeout;

function clearTimeout(timer) {
if (timer && timer._onTimeout) {
Expand Down Expand Up @@ -248,46 +199,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 +225,7 @@ 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));
}
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 });
}
});
};
setImmediate[customPromisify] = timersPromises.setImmediate;

function clearImmediate(immediate) {
if (!immediate || immediate._destroyed)
Expand Down

0 comments on commit 78edbf6

Please sign in to comment.