timers.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425
  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 and the Timer has the refresh()-
  170. // method available, call it to refresh the timer.
  171. // Some timer objects returned by setTimeout may not have a .refresh()
  172. // method (e.g. mocked timers in tests).
  173. if (fastNowTimeout?.refresh) {
  174. fastNowTimeout.refresh()
  175. // fastNowTimeout is not instantiated yet or refresh is not availabe,
  176. // create a new Timer.
  177. } else {
  178. clearTimeout(fastNowTimeout)
  179. fastNowTimeout = setTimeout(onTick, TICK_MS)
  180. // If the Timer has an unref method, call it to allow the process to exit,
  181. // if there are no other active handles. When using fake timers or mocked
  182. // environments (like Jest), .unref() may not be defined,
  183. fastNowTimeout?.unref()
  184. }
  185. }
  186. /**
  187. * The `FastTimer` class is a data structure designed to store and manage
  188. * timer information.
  189. */
  190. class FastTimer {
  191. [kFastTimer] = true
  192. /**
  193. * The state of the timer, which can be one of the following:
  194. * - NOT_IN_LIST (-2)
  195. * - TO_BE_CLEARED (-1)
  196. * - PENDING (0)
  197. * - ACTIVE (1)
  198. *
  199. * @type {-2|-1|0|1}
  200. * @private
  201. */
  202. _state = NOT_IN_LIST
  203. /**
  204. * The number of milliseconds to wait before calling the callback.
  205. *
  206. * @type {number}
  207. * @private
  208. */
  209. _idleTimeout = -1
  210. /**
  211. * The time in milliseconds when the timer was started. This value is used to
  212. * calculate when the timer should expire.
  213. *
  214. * @type {number}
  215. * @default -1
  216. * @private
  217. */
  218. _idleStart = -1
  219. /**
  220. * The function to be executed when the timer expires.
  221. * @type {Function}
  222. * @private
  223. */
  224. _onTimeout
  225. /**
  226. * The argument to be passed to the callback when the timer expires.
  227. *
  228. * @type {*}
  229. * @private
  230. */
  231. _timerArg
  232. /**
  233. * @constructor
  234. * @param {Function} callback A function to be executed after the timer
  235. * expires.
  236. * @param {number} delay The time, in milliseconds that the timer should wait
  237. * before the specified function or code is executed.
  238. * @param {*} arg
  239. */
  240. constructor (callback, delay, arg) {
  241. this._onTimeout = callback
  242. this._idleTimeout = delay
  243. this._timerArg = arg
  244. this.refresh()
  245. }
  246. /**
  247. * Sets the timer's start time to the current time, and reschedules the timer
  248. * to call its callback at the previously specified duration adjusted to the
  249. * current time.
  250. * Using this on a timer that has already called its callback will reactivate
  251. * the timer.
  252. *
  253. * @returns {void}
  254. */
  255. refresh () {
  256. // In the special case that the timer is not in the list of active timers,
  257. // add it back to the array to be processed in the next tick by the onTick
  258. // function.
  259. if (this._state === NOT_IN_LIST) {
  260. fastTimers.push(this)
  261. }
  262. // If the timer is the only active timer, refresh the fastNowTimeout for
  263. // better resolution.
  264. if (!fastNowTimeout || fastTimers.length === 1) {
  265. refreshTimeout()
  266. }
  267. // Setting the state to PENDING will cause the timer to be reset in the
  268. // next tick by the onTick function.
  269. this._state = PENDING
  270. }
  271. /**
  272. * The `clear` method cancels the timer, preventing it from executing.
  273. *
  274. * @returns {void}
  275. * @private
  276. */
  277. clear () {
  278. // Set the state to TO_BE_CLEARED to mark the timer for removal in the next
  279. // tick by the onTick function.
  280. this._state = TO_BE_CLEARED
  281. // Reset the _idleStart value to -1 to indicate that the timer is no longer
  282. // active.
  283. this._idleStart = -1
  284. }
  285. }
  286. /**
  287. * This module exports a setTimeout and clearTimeout function that can be
  288. * used as a drop-in replacement for the native functions.
  289. */
  290. module.exports = {
  291. /**
  292. * The setTimeout() method sets a timer which executes a function once the
  293. * timer expires.
  294. * @param {Function} callback A function to be executed after the timer
  295. * expires.
  296. * @param {number} delay The time, in milliseconds that the timer should
  297. * wait before the specified function or code is executed.
  298. * @param {*} [arg] An optional argument to be passed to the callback function
  299. * when the timer expires.
  300. * @returns {NodeJS.Timeout|FastTimer}
  301. */
  302. setTimeout (callback, delay, arg) {
  303. // If the delay is less than or equal to the RESOLUTION_MS value return a
  304. // native Node.js Timer instance.
  305. return delay <= RESOLUTION_MS
  306. ? setTimeout(callback, delay, arg)
  307. : new FastTimer(callback, delay, arg)
  308. },
  309. /**
  310. * The clearTimeout method cancels an instantiated Timer previously created
  311. * by calling setTimeout.
  312. *
  313. * @param {NodeJS.Timeout|FastTimer} timeout
  314. */
  315. clearTimeout (timeout) {
  316. // If the timeout is a FastTimer, call its own clear method.
  317. if (timeout[kFastTimer]) {
  318. /**
  319. * @type {FastTimer}
  320. */
  321. timeout.clear()
  322. // Otherwise it is an instance of a native NodeJS.Timeout, so call the
  323. // Node.js native clearTimeout function.
  324. } else {
  325. clearTimeout(timeout)
  326. }
  327. },
  328. /**
  329. * The setFastTimeout() method sets a fastTimer which executes a function once
  330. * the timer expires.
  331. * @param {Function} callback A function to be executed after the timer
  332. * expires.
  333. * @param {number} delay The time, in milliseconds that the timer should
  334. * wait before the specified function or code is executed.
  335. * @param {*} [arg] An optional argument to be passed to the callback function
  336. * when the timer expires.
  337. * @returns {FastTimer}
  338. */
  339. setFastTimeout (callback, delay, arg) {
  340. return new FastTimer(callback, delay, arg)
  341. },
  342. /**
  343. * The clearTimeout method cancels an instantiated FastTimer previously
  344. * created by calling setFastTimeout.
  345. *
  346. * @param {FastTimer} timeout
  347. */
  348. clearFastTimeout (timeout) {
  349. timeout.clear()
  350. },
  351. /**
  352. * The now method returns the value of the internal fast timer clock.
  353. *
  354. * @returns {number}
  355. */
  356. now () {
  357. return fastNow
  358. },
  359. /**
  360. * Trigger the onTick function to process the fastTimers array.
  361. * Exported for testing purposes only.
  362. * Marking as deprecated to discourage any use outside of testing.
  363. * @deprecated
  364. * @param {number} [delay=0] The delay in milliseconds to add to the now value.
  365. */
  366. tick (delay = 0) {
  367. fastNow += delay - RESOLUTION_MS + 1
  368. onTick()
  369. onTick()
  370. },
  371. /**
  372. * Reset FastTimers.
  373. * Exported for testing purposes only.
  374. * Marking as deprecated to discourage any use outside of testing.
  375. * @deprecated
  376. */
  377. reset () {
  378. fastNow = 0
  379. fastTimers.length = 0
  380. clearTimeout(fastNowTimeout)
  381. fastNowTimeout = null
  382. },
  383. /**
  384. * Exporting for testing purposes only.
  385. * Marking as deprecated to discourage any use outside of testing.
  386. * @deprecated
  387. */
  388. kFastTimer
  389. }