timers.js 11 KB

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