Skip to content

Commit 36f0340

Browse files
committed
timers: allow timers to be used as primitives
This allows timers to be matched to numeric Ids and therefore used as keys of an Object, passed and stored without storing the Timer instance. clearTimeout/clearInterval is modified to support numeric/string Ids. Co-authored-by: Bradley Farias <[email protected]> Co-authored-by: Anatoli Papirovski <[email protected]> Refs: #21152
1 parent 86cbad8 commit 36f0340

File tree

4 files changed

+77
-0
lines changed

4 files changed

+77
-0
lines changed

doc/api/timers.md

+16
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,21 @@ Calling `timeout.unref()` creates an internal timer that will wake the Node.js
123123
event loop. Creating too many of these can adversely impact performance
124124
of the Node.js application.
125125

126+
### `timeout[Symbol.toPrimitive]()`
127+
<!-- YAML
128+
added: REPLACEME
129+
-->
130+
131+
* Returns: {integer} number that can be used to reference this `timeout`
132+
133+
Coerce a `Timeout` to a primitive, a primitive will be generated that
134+
can be used to clear the `Timeout`.
135+
The generated number can only be used in the same thread where timeout
136+
was created. Therefore to use it cross [`worker_threads`][] it has
137+
to first be passed to a correct thread.
138+
This allows enhanced compatibility with browser's `setTimeout()`, and
139+
`setInterval()` implementations.
140+
126141
## Scheduling timers
127142

128143
A timer in Node.js is an internal construct that calls a given function after
@@ -346,3 +361,4 @@ const timersPromises = require('timers/promises');
346361
[`setInterval()`]: timers.html#timers_setinterval_callback_delay_args
347362
[`setTimeout()`]: timers.html#timers_settimeout_callback_delay_args
348363
[`util.promisify()`]: util.html#util_util_promisify_original
364+
[`worker_threads`]: worker_threads.html

lib/internal/timers.js

+4
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,8 @@ const {
104104
const async_id_symbol = Symbol('asyncId');
105105
const trigger_async_id_symbol = Symbol('triggerId');
106106

107+
const kHasPrimitive = Symbol('kHasPrimitive');
108+
107109
const {
108110
ERR_INVALID_CALLBACK,
109111
ERR_OUT_OF_RANGE
@@ -185,6 +187,7 @@ function Timeout(callback, after, args, isRepeat, isRefed) {
185187
if (isRefed)
186188
incRefCount();
187189
this[kRefed] = isRefed;
190+
this[kHasPrimitive] = false;
188191

189192
initAsyncResource(this, 'Timeout');
190193
}
@@ -639,6 +642,7 @@ module.exports = {
639642
Timeout,
640643
Immediate,
641644
kRefed,
645+
kHasPrimitive,
642646
initAsyncResource,
643647
setUnrefTimeout,
644648
getTimerDuration,

lib/timers.js

+28
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,10 @@
2222
'use strict';
2323

2424
const {
25+
ObjectCreate,
2526
MathTrunc,
2627
Object,
28+
SymbolToPrimitive
2729
} = primordials;
2830

2931
const {
@@ -41,6 +43,7 @@ const {
4143
kRefCount
4244
},
4345
kRefed,
46+
kHasPrimitive,
4447
getTimerDuration,
4548
timerListMap,
4649
timerListQueue,
@@ -66,13 +69,21 @@ const {
6669
emitDestroy
6770
} = require('internal/async_hooks');
6871

72+
// This stores all the known timer async ids to allow users to clearTimeout and
73+
// clearInterval using those ids, to match the spec and the rest of the web
74+
// platform.
75+
const knownTimersById = ObjectCreate(null);
76+
6977
// Remove a timer. Cancels the timeout and resets the relevant timer properties.
7078
function unenroll(item) {
7179
if (item._destroyed)
7280
return;
7381

7482
item._destroyed = true;
7583

84+
if (item[kHasPrimitive])
85+
delete knownTimersById[item[async_id_symbol]];
86+
7687
// Fewer checks may be possible, but these cover everything.
7788
if (destroyHooksExist() && item[async_id_symbol] !== undefined)
7889
emitDestroy(item[async_id_symbol]);
@@ -163,6 +174,14 @@ function clearTimeout(timer) {
163174
if (timer && timer._onTimeout) {
164175
timer._onTimeout = null;
165176
unenroll(timer);
177+
return;
178+
}
179+
if (typeof timer === 'number' || typeof timer === 'string') {
180+
const timerInstance = knownTimersById[timer];
181+
if (timerInstance !== undefined) {
182+
timerInstance._onTimeout = null;
183+
unenroll(timerInstance);
184+
}
166185
}
167186
}
168187

@@ -208,6 +227,15 @@ Timeout.prototype.close = function() {
208227
return this;
209228
};
210229

230+
Timeout.prototype[SymbolToPrimitive] = function() {
231+
const id = this[async_id_symbol];
232+
if (!this[kHasPrimitive]) {
233+
this[kHasPrimitive] = true;
234+
knownTimersById[id] = this;
235+
}
236+
return id;
237+
};
238+
211239
function setImmediate(callback, arg1, arg2, arg3) {
212240
validateCallback(callback);
213241

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
'use strict';
2+
3+
const common = require('../common');
4+
const assert = require('assert');
5+
6+
[
7+
setTimeout(common.mustNotCall(), 1),
8+
setInterval(common.mustNotCall(), 1),
9+
].forEach((timeout) => {
10+
assert.strictEqual(Number.isNaN(+timeout), false);
11+
assert.strictEqual(+timeout, timeout[Symbol.toPrimitive]());
12+
assert.strictEqual(`${timeout}`, timeout[Symbol.toPrimitive]().toString());
13+
assert.deepStrictEqual(Object.keys({ [timeout]: timeout }), [`${timeout}`]);
14+
clearTimeout(+timeout);
15+
});
16+
17+
{
18+
// Check that clearTimeout works with number id.
19+
const timeout = setTimeout(common.mustNotCall(), 1);
20+
const id = +timeout;
21+
clearTimeout(id);
22+
}
23+
24+
{
25+
// Check that clearTimeout works with string id.
26+
const timeout = setTimeout(common.mustNotCall(), 1);
27+
const id = `${timeout}`;
28+
clearTimeout(id);
29+
}

0 commit comments

Comments
 (0)