Skip to content

Commit a8904e8

Browse files
committed
timers: introduce timers/promises
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 <[email protected]> PR-URL: #33950 Reviewed-By: Denys Otrishko <[email protected]> Reviewed-By: Benjamin Gruenbaum <[email protected]>
1 parent bfc0e3f commit a8904e8

File tree

6 files changed

+250
-127
lines changed

6 files changed

+250
-127
lines changed

doc/api/timers.md

+34
Original file line numberDiff line numberDiff line change
@@ -302,6 +302,40 @@ added: v0.0.1
302302

303303
Cancels a `Timeout` object created by [`setTimeout()`][].
304304

305+
## Timers Promises API
306+
307+
> Stability: 1 - Experimental
308+
309+
The `timers/promises` API provides an alternative set of timer functions
310+
that return `Promise` objects. The API is accessible via
311+
`require('timers/promises')`.
312+
313+
```js
314+
const timersPromises = require('timers/promises');
315+
```
316+
317+
### `timersPromises.setTimeout(delay\[, value\[, options\]\])
318+
319+
* `delay` {number} The number of milliseconds to wait before resolving the
320+
`Promise`.
321+
* `value` {any} A value with which the `Promise` is resolved.
322+
* `options` {Object}
323+
* `ref` {boolean} Set to `false` to indicate that the scheduled `Timeout`
324+
should not require the Node.js event loop to remain active.
325+
**Default**: `true`.
326+
* `signal` {AbortSignal} An optional `AbortSignal` that can be used to
327+
cancel the scheduled `Timeout`.
328+
329+
### `timersPromises.setImmediate(\[value\[, options\]\])
330+
331+
* `value` {any} A value with which the `Promise` is resolved.
332+
* `options` {Object}
333+
* `ref` {boolean} Set to `false` to indicate that the scheduled `Immediate`
334+
should not require the Node.js event loop to remain active.
335+
**Default**: `true`.
336+
* `signal` {AbortSignal} An optional `AbortSignal` that can be used to
337+
cancel the scheduled `Immediate`.
338+
305339
[Event Loop]: https://nodejs.org/en/docs/guides/event-loop-timers-and-nexttick/#setimmediate-vs-settimeout
306340
[`AbortController`]: globals.html#globals_class_abortcontroller
307341
[`TypeError`]: errors.html#errors_class_typeerror

lib/internal/timers.js

+43-1
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,8 @@ const {
8484
scheduleTimer,
8585
toggleTimerRef,
8686
getLibuvNow,
87-
immediateInfo
87+
immediateInfo,
88+
toggleImmediateRef
8889
} = internalBinding('timers');
8990

9091
const {
@@ -590,12 +591,53 @@ function getTimerCallbacks(runNextTicks) {
590591
};
591592
}
592593

594+
class Immediate {
595+
constructor(callback, args) {
596+
this._idleNext = null;
597+
this._idlePrev = null;
598+
this._onImmediate = callback;
599+
this._argv = args;
600+
this._destroyed = false;
601+
this[kRefed] = false;
602+
603+
initAsyncResource(this, 'Immediate');
604+
605+
this.ref();
606+
immediateInfo[kCount]++;
607+
608+
immediateQueue.append(this);
609+
}
610+
611+
ref() {
612+
if (this[kRefed] === false) {
613+
this[kRefed] = true;
614+
if (immediateInfo[kRefCount]++ === 0)
615+
toggleImmediateRef(true);
616+
}
617+
return this;
618+
}
619+
620+
unref() {
621+
if (this[kRefed] === true) {
622+
this[kRefed] = false;
623+
if (--immediateInfo[kRefCount] === 0)
624+
toggleImmediateRef(false);
625+
}
626+
return this;
627+
}
628+
629+
hasRef() {
630+
return !!this[kRefed];
631+
}
632+
}
633+
593634
module.exports = {
594635
TIMEOUT_MAX,
595636
kTimeout: Symbol('timeout'), // For hiding Timeouts on other internals.
596637
async_id_symbol,
597638
trigger_async_id_symbol,
598639
Timeout,
640+
Immediate,
599641
kRefed,
600642
initAsyncResource,
601643
setUnrefTimeout,

lib/timers.js

+19-126
Original file line numberDiff line numberDiff line change
@@ -23,15 +23,9 @@
2323

2424
const {
2525
MathTrunc,
26-
Promise,
26+
Object,
2727
} = primordials;
2828

29-
const {
30-
codes: { ERR_INVALID_ARG_TYPE }
31-
} = require('internal/errors');
32-
33-
let DOMException;
34-
3529
const {
3630
immediateInfo,
3731
toggleImmediateRef
@@ -40,13 +34,13 @@ const L = require('internal/linkedlist');
4034
const {
4135
async_id_symbol,
4236
Timeout,
37+
Immediate,
4338
decRefCount,
4439
immediateInfoFields: {
4540
kCount,
4641
kRefCount
4742
},
4843
kRefed,
49-
initAsyncResource,
5044
getTimerDuration,
5145
timerListMap,
5246
timerListQueue,
@@ -64,6 +58,8 @@ let debug = require('internal/util/debuglog').debuglog('timer', (fn) => {
6458
});
6559
const { validateCallback } = require('internal/validators');
6660

61+
let timersPromises;
62+
6763
const {
6864
destroyHooksExist,
6965
// The needed emit*() functions.
@@ -124,12 +120,6 @@ function enroll(item, msecs) {
124120
* DOM-style timers
125121
*/
126122

127-
function lazyDOMException(message) {
128-
if (DOMException === undefined)
129-
DOMException = internalBinding('messaging').DOMException;
130-
return new DOMException(message);
131-
}
132-
133123
function setTimeout(callback, after, arg1, arg2, arg3) {
134124
validateCallback(callback);
135125

@@ -160,44 +150,14 @@ function setTimeout(callback, after, arg1, arg2, arg3) {
160150
return timeout;
161151
}
162152

163-
setTimeout[customPromisify] = function(after, value, options = {}) {
164-
const args = value !== undefined ? [value] : value;
165-
if (options == null || typeof options !== 'object') {
166-
return Promise.reject(
167-
new ERR_INVALID_ARG_TYPE(
168-
'options',
169-
'Object',
170-
options));
153+
Object.defineProperty(setTimeout, customPromisify, {
154+
enumerable: true,
155+
get() {
156+
if (!timersPromises)
157+
timersPromises = require('timers/promises');
158+
return timersPromises.setTimeout;
171159
}
172-
const { signal } = options;
173-
if (signal !== undefined &&
174-
(signal === null ||
175-
typeof signal !== 'object' ||
176-
!('aborted' in signal))) {
177-
return Promise.reject(
178-
new ERR_INVALID_ARG_TYPE(
179-
'options.signal',
180-
'AbortSignal',
181-
signal));
182-
}
183-
// TODO(@jasnell): If a decision is made that this cannot be backported
184-
// to 12.x, then this can be converted to use optional chaining to
185-
// simplify the check.
186-
if (signal && signal.aborted)
187-
return Promise.reject(lazyDOMException('AbortError'));
188-
return new Promise((resolve, reject) => {
189-
const timeout = new Timeout(resolve, after, args, false, true);
190-
insert(timeout, timeout._idleTimeout);
191-
if (signal) {
192-
signal.addEventListener('abort', () => {
193-
if (!timeout._destroyed) {
194-
clearTimeout(timeout);
195-
reject(lazyDOMException('AbortError'));
196-
}
197-
}, { once: true });
198-
}
199-
});
200-
};
160+
});
201161

202162
function clearTimeout(timer) {
203163
if (timer && timer._onTimeout) {
@@ -248,46 +208,6 @@ Timeout.prototype.close = function() {
248208
return this;
249209
};
250210

251-
const Immediate = class Immediate {
252-
constructor(callback, args) {
253-
this._idleNext = null;
254-
this._idlePrev = null;
255-
this._onImmediate = callback;
256-
this._argv = args;
257-
this._destroyed = false;
258-
this[kRefed] = false;
259-
260-
initAsyncResource(this, 'Immediate');
261-
262-
this.ref();
263-
immediateInfo[kCount]++;
264-
265-
immediateQueue.append(this);
266-
}
267-
268-
ref() {
269-
if (this[kRefed] === false) {
270-
this[kRefed] = true;
271-
if (immediateInfo[kRefCount]++ === 0)
272-
toggleImmediateRef(true);
273-
}
274-
return this;
275-
}
276-
277-
unref() {
278-
if (this[kRefed] === true) {
279-
this[kRefed] = false;
280-
if (--immediateInfo[kRefCount] === 0)
281-
toggleImmediateRef(false);
282-
}
283-
return this;
284-
}
285-
286-
hasRef() {
287-
return !!this[kRefed];
288-
}
289-
};
290-
291211
function setImmediate(callback, arg1, arg2, arg3) {
292212
validateCallback(callback);
293213

@@ -314,42 +234,15 @@ function setImmediate(callback, arg1, arg2, arg3) {
314234
return new Immediate(callback, args);
315235
}
316236

317-
setImmediate[customPromisify] = function(value, options = {}) {
318-
if (options == null || typeof options !== 'object') {
319-
return Promise.reject(
320-
new ERR_INVALID_ARG_TYPE(
321-
'options',
322-
'Object',
323-
options));
237+
Object.defineProperty(setImmediate, customPromisify, {
238+
enumerable: true,
239+
get() {
240+
if (!timersPromises)
241+
timersPromises = require('timers/promises');
242+
return timersPromises.setImmediate;
324243
}
325-
const { signal } = options;
326-
if (signal !== undefined &&
327-
(signal === null ||
328-
typeof signal !== 'object' ||
329-
!('aborted' in signal))) {
330-
return Promise.reject(
331-
new ERR_INVALID_ARG_TYPE(
332-
'options.signal',
333-
'AbortSignal',
334-
signal));
335-
}
336-
// TODO(@jasnell): If a decision is made that this cannot be backported
337-
// to 12.x, then this can be converted to use optional chaining to
338-
// simplify the check.
339-
if (signal && signal.aborted)
340-
return Promise.reject(lazyDOMException('AbortError'));
341-
return new Promise((resolve, reject) => {
342-
const immediate = new Immediate(resolve, [value]);
343-
if (signal) {
344-
signal.addEventListener('abort', () => {
345-
if (!immediate._destroyed) {
346-
clearImmediate(immediate);
347-
reject(lazyDOMException('AbortError'));
348-
}
349-
}, { once: true });
350-
}
351-
});
352-
};
244+
});
245+
353246

354247
function clearImmediate(immediate) {
355248
if (!immediate || immediate._destroyed)

0 commit comments

Comments
 (0)