Skip to content

Commit cca0372

Browse files
lundibundiMylesBorins
authored andcommitted
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 <bradley.meck@gmail.com> Co-authored-by: Anatoli Papirovski <apapirovski@mac.com> Refs: #21152 Backport-PR-URL: #34482 PR-URL: #34017 Backport-PR-URL: #34482 Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Bradley Farias <bradley.meck@gmail.com> Reviewed-By: Jeremiah Senkpiel <fishrock123@rocketmail.com> Reviewed-By: Anna Henningsen <anna@addaleax.net> Reviewed-By: Trivikram Kamat <trivikr.dev@gmail.com> Reviewed-By: Yongsheng Zhang <zyszys98@gmail.com> Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com> Signed-off-by: Denys Otrishko <shishugi@gmail.com>
1 parent b30b518 commit cca0372

File tree

4 files changed

+77
-0
lines changed

4 files changed

+77
-0
lines changed

doc/api/timers.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,21 @@ Calling `timeout.unref()` creates an internal timer that will wake the Node.js
125125
event loop. Creating too many of these can adversely impact performance
126126
of the Node.js application.
127127

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

130145
A timer in Node.js is an internal construct that calls a given function after
@@ -274,3 +289,4 @@ Cancels a `Timeout` object created by [`setTimeout()`][].
274289
[`setInterval()`]: timers.html#timers_setinterval_callback_delay_args
275290
[`setTimeout()`]: timers.html#timers_settimeout_callback_delay_args
276291
[`util.promisify()`]: util.html#util_util_promisify_original
292+
[`worker_threads`]: worker_threads.html

lib/internal/timers.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,8 @@ const {
103103
const async_id_symbol = Symbol('asyncId');
104104
const trigger_async_id_symbol = Symbol('triggerId');
105105

106+
const kHasPrimitive = Symbol('kHasPrimitive');
107+
106108
const {
107109
ERR_INVALID_CALLBACK,
108110
ERR_OUT_OF_RANGE
@@ -184,6 +186,7 @@ function Timeout(callback, after, args, isRepeat, isRefed) {
184186
if (isRefed)
185187
incRefCount();
186188
this[kRefed] = isRefed;
189+
this[kHasPrimitive] = false;
187190

188191
initAsyncResource(this, 'Timeout');
189192
}
@@ -597,6 +600,7 @@ module.exports = {
597600
trigger_async_id_symbol,
598601
Timeout,
599602
kRefed,
603+
kHasPrimitive,
600604
initAsyncResource,
601605
setUnrefTimeout,
602606
getTimerDuration,

lib/timers.js

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,10 @@
2222
'use strict';
2323

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

2931
const {
@@ -40,6 +42,7 @@ const {
4042
kRefCount
4143
},
4244
kRefed,
45+
kHasPrimitive,
4346
initAsyncResource,
4447
getTimerDuration,
4548
timerListMap,
@@ -64,13 +67,21 @@ const {
6467
emitDestroy
6568
} = require('internal/async_hooks');
6669

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

7280
item._destroyed = true;
7381

82+
if (item[kHasPrimitive])
83+
delete knownTimersById[item[async_id_symbol]];
84+
7485
// Fewer checks may be possible, but these cover everything.
7586
if (destroyHooksExist() && item[async_id_symbol] !== undefined)
7687
emitDestroy(item[async_id_symbol]);
@@ -161,6 +172,14 @@ function clearTimeout(timer) {
161172
if (timer && timer._onTimeout) {
162173
timer._onTimeout = null;
163174
unenroll(timer);
175+
return;
176+
}
177+
if (typeof timer === 'number' || typeof timer === 'string') {
178+
const timerInstance = knownTimersById[timer];
179+
if (timerInstance !== undefined) {
180+
timerInstance._onTimeout = null;
181+
unenroll(timerInstance);
182+
}
164183
}
165184
}
166185

@@ -206,6 +225,15 @@ Timeout.prototype.close = function() {
206225
return this;
207226
};
208227

228+
Timeout.prototype[SymbolToPrimitive] = function() {
229+
const id = this[async_id_symbol];
230+
if (!this[kHasPrimitive]) {
231+
this[kHasPrimitive] = true;
232+
knownTimersById[id] = this;
233+
}
234+
return id;
235+
};
236+
209237
const Immediate = class Immediate {
210238
constructor(callback, args) {
211239
this._idleNext = null;
Lines changed: 29 additions & 0 deletions
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)