diff --git a/doc/api/timers.md b/doc/api/timers.md index b86a49a932ec1c..db8e8dfce43390 100644 --- a/doc/api/timers.md +++ b/doc/api/timers.md @@ -123,6 +123,14 @@ Calling `timeout.unref()` creates an internal timer that will wake the Node.js event loop. Creating too many of these can adversely impact performance of the Node.js application. +### timeout[Symbol.toPrimitive]() + +* Returns: {integer} + +When coercing a `Timeout` to a primitive, a primitive will be generated that +can be used to clear the `Timeout`. This allows enhanced compatibility with +browser `setTimeout`, and `setInterval` implementations. + ## Scheduling Timers A timer in Node.js is an internal construct that calls a given function after diff --git a/lib/internal/timers.js b/lib/internal/timers.js index 44ebc65dc1d4aa..c7a93695d931bc 100644 --- a/lib/internal/timers.js +++ b/lib/internal/timers.js @@ -19,6 +19,7 @@ const { // Timeout values > TIMEOUT_MAX are set to 1. const TIMEOUT_MAX = 2 ** 31 - 1; +const kHasPrimitive = Symbol('hasPrimitive'); const kRefed = Symbol('refed'); module.exports = { @@ -27,6 +28,7 @@ module.exports = { async_id_symbol, trigger_async_id_symbol, Timeout, + kHasPrimitive, kRefed, initAsyncResource, setUnrefTimeout, @@ -75,6 +77,7 @@ function Timeout(callback, after, args, isRepeat) { this._repeat = isRepeat ? after : null; this._destroyed = false; + this[kHasPrimitive] = false; this[kRefed] = null; initAsyncResource(this, 'Timeout'); diff --git a/lib/timers.js b/lib/timers.js index 5ae0e6a5ad4fd4..002b5fc08ae845 100644 --- a/lib/timers.js +++ b/lib/timers.js @@ -31,6 +31,7 @@ const { async_id_symbol, trigger_async_id_symbol, Timeout, + kHasPrimitive, kRefed, initAsyncResource, validateTimerDuration @@ -136,6 +137,11 @@ const [immediateInfo, toggleImmediateRef] = // - value = linked list const lists = Object.create(null); +// This stores all the known timer async ids to allow users to clearTimeout and +// clearInterval using those ids, to match the spec and the rest of the web +// platform. +const knownTimersById = Object.create(null); + // This is a priority queue with a custom sorting function that first compares // the expiry times of two lists and if they're the same then compares their // individual IDs to determine which list was created first. @@ -347,6 +353,9 @@ function tryOnTimeout(timer, start) { refCount--; timer[kRefed] = null; + if (timer[kHasPrimitive]) + delete knownTimersById[timer[async_id_symbol]]; + if (destroyHooksExist() && !timer._destroyed) { emitDestroy(timer[async_id_symbol]); timer._destroyed = true; @@ -385,6 +394,9 @@ function unenroll(item) { } item[kRefed] = null; + if (item[kHasPrimitive]) + delete knownTimersById[item[async_id_symbol]]; + // if active is called later, then we want to make sure not to insert again item._idleTimeout = -1; } @@ -487,6 +499,16 @@ const clearTimeout = exports.clearTimeout = function clearTimeout(timer) { if (timer && timer._onTimeout) { timer._onTimeout = null; unenroll(timer); + return; + } + + const timerType = typeof timer; + if (timerType === 'string' || timerType === 'number') { + const timerInstance = knownTimersById[timer]; + if (timerInstance !== undefined) { + timerInstance._onTimeout = null; + unenroll(timerInstance); + } } }; @@ -531,6 +553,15 @@ exports.clearInterval = function clearInterval(timer) { }; +Timeout.prototype[Symbol.toPrimitive] = function() { + const id = this[async_id_symbol]; + if (!this[kHasPrimitive]) { + this[kHasPrimitive] = true; + knownTimersById[id] = this; + } + return id; +}; + Timeout.prototype.unref = function() { if (this[kRefed]) { this[kRefed] = false; diff --git a/test/parallel/test-timers-to-primitive.js b/test/parallel/test-timers-to-primitive.js new file mode 100644 index 00000000000000..e0a9f8cd0f9ff0 --- /dev/null +++ b/test/parallel/test-timers-to-primitive.js @@ -0,0 +1,25 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); + +const timeout1 = setTimeout(common.mustNotCall(), 1); +const timeout2 = setInterval(common.mustNotCall(), 1); + +assert.strictEqual(Number.isNaN(+timeout1), false); +assert.strictEqual(Number.isNaN(+timeout2), false); + +assert.strictEqual(+timeout1, timeout1[Symbol.toPrimitive]()); + +assert.notStrictEqual(`${timeout1}`, Object.prototype.toString.call(timeout1)); +assert.notStrictEqual(`${timeout2}`, Object.prototype.toString.call(timeout2)); + +assert.notStrictEqual(+timeout1, +timeout2); + +const o = {}; +o[timeout1] = timeout1; +o[timeout2] = timeout2; +const keys = Object.keys(o); +assert.deepStrictEqual(keys, [`${timeout1}`, `${timeout2}`]); + +clearTimeout(keys[0]); // Works for string. +clearInterval(+timeout2); // Works for integer.