From d72b7ba0f1e44a3c6cd5a505b92b9397663a8986 Mon Sep 17 00:00:00 2001 From: Anatoli Papirovski Date: Tue, 5 Jun 2018 11:40:48 -0400 Subject: [PATCH] timers: allow timers to be used as primitives For web compatibility this allows timers to be stored as the key of an Object property and be passed back to corresponding method to clear the timer. Co-authored-by: Bradley Farias --- doc/api/timers.md | 8 ++++++ lib/internal/timers.js | 3 +++ lib/timers.js | 31 +++++++++++++++++++++++ test/parallel/test-timers-to-primitive.js | 25 ++++++++++++++++++ 4 files changed, 67 insertions(+) create mode 100644 test/parallel/test-timers-to-primitive.js 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.