Skip to content

Commit 987e0cb

Browse files
lundibundiaddaleax
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 <[email protected]> Co-authored-by: Anatoli Papirovski <[email protected]> Refs: #21152 Backport-PR-URL: #34482 PR-URL: #34017 Backport-PR-URL: #34482 Reviewed-By: James M Snell <[email protected]> Reviewed-By: Bradley Farias <[email protected]> Reviewed-By: Jeremiah Senkpiel <[email protected]> Reviewed-By: Anna Henningsen <[email protected]> Reviewed-By: Trivikram Kamat <[email protected]> Reviewed-By: Yongsheng Zhang <[email protected]> Reviewed-By: Benjamin Gruenbaum <[email protected]> Signed-off-by: Denys Otrishko <[email protected]>
1 parent 14d4bfa commit 987e0cb

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
@@ -182,6 +184,7 @@ function Timeout(callback, after, args, isRepeat, isRefed) {
182184
if (isRefed)
183185
incRefCount();
184186
this[kRefed] = isRefed;
187+
this[kHasPrimitive] = false;
185188

186189
initAsyncResource(this, 'Timeout');
187190
}
@@ -595,6 +598,7 @@ module.exports = {
595598
trigger_async_id_symbol,
596599
Timeout,
597600
kRefed,
601+
kHasPrimitive,
598602
initAsyncResource,
599603
setUnrefTimeout,
600604
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,
@@ -62,13 +65,21 @@ const {
6265
emitDestroy
6366
} = require('internal/async_hooks');
6467

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

7078
item._destroyed = true;
7179

80+
if (item[kHasPrimitive])
81+
delete knownTimersById[item[async_id_symbol]];
82+
7283
// Fewer checks may be possible, but these cover everything.
7384
if (destroyHooksExist() && item[async_id_symbol] !== undefined)
7485
emitDestroy(item[async_id_symbol]);
@@ -159,6 +170,14 @@ function clearTimeout(timer) {
159170
if (timer && timer._onTimeout) {
160171
timer._onTimeout = null;
161172
unenroll(timer);
173+
return;
174+
}
175+
if (typeof timer === 'number' || typeof timer === 'string') {
176+
const timerInstance = knownTimersById[timer];
177+
if (timerInstance !== undefined) {
178+
timerInstance._onTimeout = null;
179+
unenroll(timerInstance);
180+
}
162181
}
163182
}
164183

@@ -204,6 +223,15 @@ Timeout.prototype.close = function() {
204223
return this;
205224
};
206225

226+
Timeout.prototype[SymbolToPrimitive] = function() {
227+
const id = this[async_id_symbol];
228+
if (!this[kHasPrimitive]) {
229+
this[kHasPrimitive] = true;
230+
knownTimersById[id] = this;
231+
}
232+
return id;
233+
};
234+
207235
const Immediate = class Immediate {
208236
constructor(callback, args) {
209237
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)