codekingpro/portable-devtools
114k
1'use strict'
2
3/**
4 * This module offers an optimized timer implementation designed for scenarios
5 * where high precision is not critical.
6 *
7 * The timer achieves faster performance by using a low-resolution approach,
8 * with an accuracy target of within 500ms. This makes it particularly useful
9 * for timers with delays of 1 second or more, where exact timing is less
10 * crucial.
11 *
12 * It's important to note that Node.js timers are inherently imprecise, as
13 * delays can occur due to the event loop being blocked by other operations.
14 * Consequently, timers may trigger later than their scheduled time.
15 */
16
17/**
18 * The fastNow variable contains the internal fast timer clock value.
19 *
20 * @type {number}
21 */
22let fastNow = 0
23
24/**
25 * RESOLUTION_MS represents the target resolution time in milliseconds.
26 *
27 * @type {number}
28 * @default 1000
29 */
30const RESOLUTION_MS = 1e3
31
32/**
33 * TICK_MS defines the desired interval in milliseconds between each tick.
34 * The target value is set to half the resolution time, minus 1 ms, to account
35 * for potential event loop overhead.
36 *
37 * @type {number}
38 * @default 499
39 */
40const TICK_MS = (RESOLUTION_MS >> 1) - 1
41
42/**
43 * fastNowTimeout is a Node.js timer used to manage and process
44 * the FastTimers stored in the `fastTimers` array.
45 *
46 * @type {NodeJS.Timeout}
47 */
48let fastNowTimeout
49
50/**
51 * The kFastTimer symbol is used to identify FastTimer instances.
52 *
53 * @type {Symbol}
54 */
55const kFastTimer = Symbol('kFastTimer')
56
57/**
58 * The fastTimers array contains all active FastTimers.
59 *
60 * @type {FastTimer[]}
61 */
62const fastTimers = []
63
64/**
65 * These constants represent the various states of a FastTimer.
66 */
67
68/**
69 * The `NOT_IN_LIST` constant indicates that the FastTimer is not included
70 * in the `fastTimers` array. Timers with this status will not be processed
71 * during the next tick by the `onTick` function.
72 *
73 * A FastTimer can be re-added to the `fastTimers` array by invoking the
74 * `refresh` method on the FastTimer instance.
75 *
76 * @type {-2}
77 */
78const NOT_IN_LIST = -2
79
80/**
81 * The `TO_BE_CLEARED` constant indicates that the FastTimer is scheduled
82 * for removal from the `fastTimers` array. A FastTimer in this state will
83 * be removed in the next tick by the `onTick` function and will no longer
84 * be processed.
85 *
86 * This status is also set when the `clear` method is called on the FastTimer instance.
87 *
88 * @type {-1}
89 */
90const TO_BE_CLEARED = -1
91
92/**
93 * The `PENDING` constant signifies that the FastTimer is awaiting processing
94 * in the next tick by the `onTick` function. Timers with this status will have
95 * their `_idleStart` value set and their status updated to `ACTIVE` in the next tick.
96 *
97 * @type {0}
98 */
99const PENDING = 0
100
101/**
102 * The `ACTIVE` constant indicates that the FastTimer is active and waiting
103 * for its timer to expire. During the next tick, the `onTick` function will
104 * check if the timer has expired, and if so, it will execute the associated callback.
105 *
106 * @type {1}
107 */
108const ACTIVE = 1
109
110/**
111 * The onTick function processes the fastTimers array.
112 *
113 * @returns {void}
114 */
115function onTick () {
116 /**
117 * Increment the fastNow value by the TICK_MS value, despite the actual time
118 * that has passed since the last tick. This approach ensures independence
119 * from the system clock and delays caused by a blocked event loop.
120 *
121 * @type {number}
122 */
123 fastNow += TICK_MS
124
125 /**
126 * The `idx` variable is used to iterate over the `fastTimers` array.
127 * Expired timers are removed by replacing them with the last element in the array.
128 * Consequently, `idx` is only incremented when the current element is not removed.
129 *
130 * @type {number}
131 */
132 let idx = 0
133
134 /**
135 * The len variable will contain the length of the fastTimers array
136 * and will be decremented when a FastTimer should be removed from the
137 * fastTimers array.
138 *
139 * @type {number}
140 */
141 let len = fastTimers.length
142
143 while (idx < len) {
144 /**
145 * @type {FastTimer}
146 */
147 const timer = fastTimers[idx]
148
149 // If the timer is in the ACTIVE state and the timer has expired, it will
150 // be processed in the next tick.
151 if (timer._state === PENDING) {
152 // Set the _idleStart value to the fastNow value minus the TICK_MS value
153 // to account for the time the timer was in the PENDING state.
154 timer._idleStart = fastNow - TICK_MS
155 timer._state = ACTIVE
156 } else if (
157 timer._state === ACTIVE &&
158 fastNow >= timer._idleStart + timer._idleTimeout
159 ) {
160 timer._state = TO_BE_CLEARED
161 timer._idleStart = -1
162 timer._onTimeout(timer._timerArg)
163 }
164
165 if (timer._state === TO_BE_CLEARED) {
166 timer._state = NOT_IN_LIST
167
168 // Move the last element to the current index and decrement len if it is
169 // not the only element in the array.
170 if (--len !== 0) {
171 fastTimers[idx] = fastTimers[len]
172 }
173 } else {
174 ++idx
175 }
176 }
177
178 // Set the length of the fastTimers array to the new length and thus
179 // removing the excess FastTimers elements from the array.
180 fastTimers.length = len
181
182 // If there are still active FastTimers in the array, refresh the Timer.
183 // If there are no active FastTimers, the timer will be refreshed again
184 // when a new FastTimer is instantiated.
185 if (fastTimers.length !== 0) {
186 refreshTimeout()
187 }
188}
189
190function refreshTimeout () {
191 // If the fastNowTimeout is already set, refresh it.
192 if (fastNowTimeout) {
193 fastNowTimeout.refresh()
194 // fastNowTimeout is not instantiated yet, create a new Timer.
195 } else {
196 clearTimeout(fastNowTimeout)
197 fastNowTimeout = setTimeout(onTick, TICK_MS)
198
199 // If the Timer has an unref method, call it to allow the process to exit if
200 // there are no other active handles.
201 if (fastNowTimeout.unref) {
202 fastNowTimeout.unref()
203 }
204 }
205}
206
207/**
208 * The `FastTimer` class is a data structure designed to store and manage
209 * timer information.
210 */
211class FastTimer {
212 [kFastTimer] = true
213
214 /**
215 * The state of the timer, which can be one of the following:
216 * - NOT_IN_LIST (-2)
217 * - TO_BE_CLEARED (-1)
218 * - PENDING (0)
219 * - ACTIVE (1)
220 *
221 * @type {-2|-1|0|1}
222 * @private
223 */
224 _state = NOT_IN_LIST
225
226 /**
227 * The number of milliseconds to wait before calling the callback.
228 *
229 * @type {number}
230 * @private
231 */
232 _idleTimeout = -1
233
234 /**
235 * The time in milliseconds when the timer was started. This value is used to
236 * calculate when the timer should expire.
237 *
238 * @type {number}
239 * @default -1
240 * @private
241 */
242 _idleStart = -1
243
244 /**
245 * The function to be executed when the timer expires.
246 * @type {Function}
247 * @private
248 */
249 _onTimeout
250
251 /**
252 * The argument to be passed to the callback when the timer expires.
253 *
254 * @type {*}
255 * @private
256 */
257 _timerArg
258
259 /**
260 * @constructor
261 * @param {Function} callback A function to be executed after the timer
262 * expires.
263 * @param {number} delay The time, in milliseconds that the timer should wait
264 * before the specified function or code is executed.
265 * @param {*} arg
266 */
267 constructor (callback, delay, arg) {
268 this._onTimeout = callback
269 this._idleTimeout = delay
270 this._timerArg = arg
271
272 this.refresh()
273 }
274
275 /**
276 * Sets the timer's start time to the current time, and reschedules the timer
277 * to call its callback at the previously specified duration adjusted to the
278 * current time.
279 * Using this on a timer that has already called its callback will reactivate
280 * the timer.
281 *
282 * @returns {void}
283 */
284 refresh () {
285 // In the special case that the timer is not in the list of active timers,
286 // add it back to the array to be processed in the next tick by the onTick
287 // function.
288 if (this._state === NOT_IN_LIST) {
289 fastTimers.push(this)
290 }
291
292 // If the timer is the only active timer, refresh the fastNowTimeout for
293 // better resolution.
294 if (!fastNowTimeout || fastTimers.length === 1) {
295 refreshTimeout()
296 }
297
298 // Setting the state to PENDING will cause the timer to be reset in the
299 // next tick by the onTick function.
300 this._state = PENDING
301 }
302
303 /**
304 * The `clear` method cancels the timer, preventing it from executing.
305 *
306 * @returns {void}
307 * @private
308 */
309 clear () {
310 // Set the state to TO_BE_CLEARED to mark the timer for removal in the next
311 // tick by the onTick function.
312 this._state = TO_BE_CLEARED
313
314 // Reset the _idleStart value to -1 to indicate that the timer is no longer
315 // active.
316 this._idleStart = -1
317 }
318}
319
320/**
321 * This module exports a setTimeout and clearTimeout function that can be
322 * used as a drop-in replacement for the native functions.
323 */
324module.exports = {
325 /**
326 * The setTimeout() method sets a timer which executes a function once the
327 * timer expires.
328 * @param {Function} callback A function to be executed after the timer
329 * expires.
330 * @param {number} delay The time, in milliseconds that the timer should
331 * wait before the specified function or code is executed.
332 * @param {*} [arg] An optional argument to be passed to the callback function
333 * when the timer expires.
334 * @returns {NodeJS.Timeout|FastTimer}
335 */
336 setTimeout (callback, delay, arg) {
337 // If the delay is less than or equal to the RESOLUTION_MS value return a
338 // native Node.js Timer instance.
339 return delay <= RESOLUTION_MS
340 ? setTimeout(callback, delay, arg)
341 : new FastTimer(callback, delay, arg)
342 },
343 /**
344 * The clearTimeout method cancels an instantiated Timer previously created
345 * by calling setTimeout.
346 *
347 * @param {NodeJS.Timeout|FastTimer} timeout
348 */
349 clearTimeout (timeout) {
350 // If the timeout is a FastTimer, call its own clear method.
351 if (timeout[kFastTimer]) {
352 /**
353 * @type {FastTimer}
354 */
355 timeout.clear()
356 // Otherwise it is an instance of a native NodeJS.Timeout, so call the
357 // Node.js native clearTimeout function.
358 } else {
359 clearTimeout(timeout)
360 }
361 },
362 /**
363 * The setFastTimeout() method sets a fastTimer which executes a function once
364 * the timer expires.
365 * @param {Function} callback A function to be executed after the timer
366 * expires.
367 * @param {number} delay The time, in milliseconds that the timer should
368 * wait before the specified function or code is executed.
369 * @param {*} [arg] An optional argument to be passed to the callback function
370 * when the timer expires.
371 * @returns {FastTimer}
372 */
373 setFastTimeout (callback, delay, arg) {
374 return new FastTimer(callback, delay, arg)
375 },
376 /**
377 * The clearTimeout method cancels an instantiated FastTimer previously
378 * created by calling setFastTimeout.
379 *
380 * @param {FastTimer} timeout
381 */
382 clearFastTimeout (timeout) {
383 timeout.clear()
384 },
385 /**
386 * The now method returns the value of the internal fast timer clock.
387 *
388 * @returns {number}
389 */
390 now () {
391 return fastNow
392 },
393 /**
394 * Trigger the onTick function to process the fastTimers array.
395 * Exported for testing purposes only.
396 * Marking as deprecated to discourage any use outside of testing.
397 * @deprecated
398 * @param {number} [delay=0] The delay in milliseconds to add to the now value.
399 */
400 tick (delay = 0) {
401 fastNow += delay - RESOLUTION_MS + 1
402 onTick()
403 onTick()
404 },
405 /**
406 * Reset FastTimers.
407 * Exported for testing purposes only.
408 * Marking as deprecated to discourage any use outside of testing.
409 * @deprecated
410 */
411 reset () {
412 fastNow = 0
413 fastTimers.length = 0
414 clearTimeout(fastNowTimeout)
415 fastNowTimeout = null
416 },
417 /**
418 * Exporting for testing purposes only.
419 * Marking as deprecated to discourage any use outside of testing.
420 * @deprecated
421 */
422 kFastTimer
423}
424 