1'use strict'; 2 3const { 4 emitExperimentalWarning, 5} = require('internal/util'); 6 7const { 8 ArrayPrototypeForEach, 9 ArrayPrototypeIncludes, 10 DateNow, 11 FunctionPrototypeApply, 12 FunctionPrototypeBind, 13 ObjectDefineProperty, 14 ObjectGetOwnPropertyDescriptor, 15 Promise, 16 SymbolAsyncIterator, 17 SymbolDispose, 18 globalThis, 19} = primordials; 20const { 21 validateAbortSignal, 22 validateArray, 23} = require('internal/validators'); 24 25const { 26 AbortError, 27 codes: { 28 ERR_INVALID_STATE, 29 ERR_INVALID_ARG_VALUE, 30 }, 31} = require('internal/errors'); 32 33const PriorityQueue = require('internal/priority_queue'); 34const nodeTimers = require('timers'); 35const nodeTimersPromises = require('timers/promises'); 36const EventEmitter = require('events'); 37 38let kResistStopPropagation; 39 40function compareTimersLists(a, b) { 41 return (a.runAt - b.runAt) || (a.id - b.id); 42} 43 44function setPosition(node, pos) { 45 node.priorityQueuePosition = pos; 46} 47 48function abortIt(signal) { 49 return new AbortError(undefined, { __proto__: null, cause: signal.reason }); 50} 51 52const SUPPORTED_TIMERS = ['setTimeout', 'setInterval', 'setImmediate']; 53const TIMERS_DEFAULT_INTERVAL = { 54 __proto__: null, 55 setImmediate: -1, 56}; 57 58class MockTimers { 59 #realSetTimeout; 60 #realClearTimeout; 61 #realSetInterval; 62 #realClearInterval; 63 #realSetImmediate; 64 #realClearImmediate; 65 66 #realPromisifiedSetTimeout; 67 #realPromisifiedSetInterval; 68 69 #realTimersSetTimeout; 70 #realTimersClearTimeout; 71 #realTimersSetInterval; 72 #realTimersClearInterval; 73 #realTimersSetImmediate; 74 #realTimersClearImmediate; 75 #realPromisifiedSetImmediate; 76 77 #timersInContext = []; 78 #isEnabled = false; 79 #currentTimer = 1; 80 #now = DateNow(); 81 82 #executionQueue = new PriorityQueue(compareTimersLists, setPosition); 83 84 #setTimeout = FunctionPrototypeBind(this.#createTimer, this, false); 85 #clearTimeout = FunctionPrototypeBind(this.#clearTimer, this); 86 #setInterval = FunctionPrototypeBind(this.#createTimer, this, true); 87 #clearInterval = FunctionPrototypeBind(this.#clearTimer, this); 88 89 #setImmediate = (callback, ...args) => { 90 return this.#createTimer( 91 false, 92 callback, 93 TIMERS_DEFAULT_INTERVAL.setImmediate, 94 ...args, 95 ); 96 }; 97 98 #clearImmediate = FunctionPrototypeBind(this.#clearTimer, this); 99 constructor() { 100 emitExperimentalWarning('The MockTimers API'); 101 } 102 103 #createTimer(isInterval, callback, delay, ...args) { 104 const timerId = this.#currentTimer++; 105 this.#executionQueue.insert({ 106 __proto__: null, 107 id: timerId, 108 callback, 109 runAt: this.#now + delay, 110 interval: isInterval, 111 args, 112 }); 113 114 return timerId; 115 } 116 117 #clearTimer(position) { 118 this.#executionQueue.removeAt(position); 119 } 120 121 async * #setIntervalPromisified(interval, startTime, options) { 122 const context = this; 123 const emitter = new EventEmitter(); 124 if (options?.signal) { 125 validateAbortSignal(options.signal, 'options.signal'); 126 127 if (options.signal.aborted) { 128 throw abortIt(options.signal); 129 } 130 131 const onAbort = (reason) => { 132 emitter.emit('data', { __proto__: null, aborted: true, reason }); 133 }; 134 135 kResistStopPropagation ??= require('internal/event_target').kResistStopPropagation; 136 options.signal.addEventListener('abort', onAbort, { 137 __proto__: null, 138 once: true, 139 [kResistStopPropagation]: true, 140 }); 141 } 142 143 const eventIt = EventEmitter.on(emitter, 'data'); 144 const callback = () => { 145 startTime += interval; 146 emitter.emit('data', startTime); 147 }; 148 149 const timerId = this.#createTimer(true, callback, interval, options); 150 const clearListeners = () => { 151 emitter.removeAllListeners(); 152 context.#clearTimer(timerId); 153 }; 154 const iterator = { 155 __proto__: null, 156 [SymbolAsyncIterator]() { 157 return this; 158 }, 159 async next() { 160 const result = await eventIt.next(); 161 const value = result.value[0]; 162 if (value?.aborted) { 163 iterator.return(); 164 throw abortIt(options.signal); 165 } 166 167 return { 168 __proto__: null, 169 done: result.done, 170 value, 171 }; 172 }, 173 async return() { 174 clearListeners(); 175 return eventIt.return(); 176 }, 177 }; 178 yield* iterator; 179 } 180 181 #promisifyTimer({ timerFn, clearFn, ms, result, options }) { 182 return new Promise((resolve, reject) => { 183 if (options?.signal) { 184 try { 185 validateAbortSignal(options.signal, 'options.signal'); 186 } catch (err) { 187 return reject(err); 188 } 189 190 if (options.signal.aborted) { 191 return reject(abortIt(options.signal)); 192 } 193 } 194 195 const onabort = () => { 196 clearFn(id); 197 return reject(abortIt(options.signal)); 198 }; 199 200 const id = timerFn(() => { 201 return resolve(result); 202 }, ms); 203 204 if (options?.signal) { 205 kResistStopPropagation ??= require('internal/event_target').kResistStopPropagation; 206 options.signal.addEventListener('abort', onabort, { 207 __proto__: null, 208 once: true, 209 [kResistStopPropagation]: true, 210 }); 211 } 212 }); 213 } 214 215 #setImmediatePromisified(result, options) { 216 return this.#promisifyTimer({ 217 __proto__: null, 218 timerFn: FunctionPrototypeBind(this.#setImmediate, this), 219 clearFn: FunctionPrototypeBind(this.#clearImmediate, this), 220 ms: TIMERS_DEFAULT_INTERVAL.setImmediate, 221 result, 222 options, 223 }); 224 } 225 226 #setTimeoutPromisified(ms, result, options) { 227 return this.#promisifyTimer({ 228 __proto__: null, 229 timerFn: FunctionPrototypeBind(this.#setTimeout, this), 230 clearFn: FunctionPrototypeBind(this.#clearTimeout, this), 231 ms, 232 result, 233 options, 234 }); 235 } 236 237 #toggleEnableTimers(activate) { 238 const options = { 239 __proto__: null, 240 toFake: { 241 __proto__: null, 242 setTimeout: () => { 243 this.#storeOriginalSetTimeout(); 244 245 globalThis.setTimeout = this.#setTimeout; 246 globalThis.clearTimeout = this.#clearTimeout; 247 248 nodeTimers.setTimeout = this.#setTimeout; 249 nodeTimers.clearTimeout = this.#clearTimeout; 250 251 nodeTimersPromises.setTimeout = FunctionPrototypeBind( 252 this.#setTimeoutPromisified, 253 this, 254 ); 255 }, 256 setInterval: () => { 257 this.#storeOriginalSetInterval(); 258 259 globalThis.setInterval = this.#setInterval; 260 globalThis.clearInterval = this.#clearInterval; 261 262 nodeTimers.setInterval = this.#setInterval; 263 nodeTimers.clearInterval = this.#clearInterval; 264 265 nodeTimersPromises.setInterval = FunctionPrototypeBind( 266 this.#setIntervalPromisified, 267 this, 268 ); 269 }, 270 setImmediate: () => { 271 this.#storeOriginalSetImmediate(); 272 273 globalThis.setImmediate = this.#setImmediate; 274 globalThis.clearImmediate = this.#clearImmediate; 275 276 nodeTimers.setImmediate = this.#setImmediate; 277 nodeTimers.clearImmediate = this.#clearImmediate; 278 279 nodeTimersPromises.setImmediate = FunctionPrototypeBind( 280 this.#setImmediatePromisified, 281 this, 282 ); 283 }, 284 }, 285 toReal: { 286 __proto__: null, 287 setTimeout: () => { 288 this.#restoreOriginalSetTimeout(); 289 }, 290 setInterval: () => { 291 this.#restoreOriginalSetInterval(); 292 }, 293 setImmediate: () => { 294 this.#restoreSetImmediate(); 295 }, 296 }, 297 }; 298 299 const target = activate ? options.toFake : options.toReal; 300 ArrayPrototypeForEach(this.#timersInContext, (timer) => target[timer]()); 301 this.#isEnabled = activate; 302 } 303 304 #restoreSetImmediate() { 305 ObjectDefineProperty( 306 globalThis, 307 'setImmediate', 308 this.#realSetImmediate, 309 ); 310 ObjectDefineProperty( 311 globalThis, 312 'clearImmediate', 313 this.#realClearImmediate, 314 ); 315 ObjectDefineProperty( 316 nodeTimers, 317 'setImmediate', 318 this.#realTimersSetImmediate, 319 ); 320 ObjectDefineProperty( 321 nodeTimers, 322 'clearImmediate', 323 this.#realTimersClearImmediate, 324 ); 325 ObjectDefineProperty( 326 nodeTimersPromises, 327 'setImmediate', 328 this.#realPromisifiedSetImmediate, 329 ); 330 } 331 332 #restoreOriginalSetInterval() { 333 ObjectDefineProperty( 334 globalThis, 335 'setInterval', 336 this.#realSetInterval, 337 ); 338 ObjectDefineProperty( 339 globalThis, 340 'clearInterval', 341 this.#realClearInterval, 342 ); 343 ObjectDefineProperty( 344 nodeTimers, 345 'setInterval', 346 this.#realTimersSetInterval, 347 ); 348 ObjectDefineProperty( 349 nodeTimers, 350 'clearInterval', 351 this.#realTimersClearInterval, 352 ); 353 ObjectDefineProperty( 354 nodeTimersPromises, 355 'setInterval', 356 this.#realPromisifiedSetInterval, 357 ); 358 } 359 360 #restoreOriginalSetTimeout() { 361 ObjectDefineProperty( 362 globalThis, 363 'setTimeout', 364 this.#realSetTimeout, 365 ); 366 ObjectDefineProperty( 367 globalThis, 368 'clearTimeout', 369 this.#realClearTimeout, 370 ); 371 ObjectDefineProperty( 372 nodeTimers, 373 'setTimeout', 374 this.#realTimersSetTimeout, 375 ); 376 ObjectDefineProperty( 377 nodeTimers, 378 'clearTimeout', 379 this.#realTimersClearTimeout, 380 ); 381 ObjectDefineProperty( 382 nodeTimersPromises, 383 'setTimeout', 384 this.#realPromisifiedSetTimeout, 385 ); 386 } 387 388 #storeOriginalSetImmediate() { 389 this.#realSetImmediate = ObjectGetOwnPropertyDescriptor( 390 globalThis, 391 'setImmediate', 392 ); 393 this.#realClearImmediate = ObjectGetOwnPropertyDescriptor( 394 globalThis, 395 'clearImmediate', 396 ); 397 this.#realTimersSetImmediate = ObjectGetOwnPropertyDescriptor( 398 nodeTimers, 399 'setImmediate', 400 ); 401 this.#realTimersClearImmediate = ObjectGetOwnPropertyDescriptor( 402 nodeTimers, 403 'clearImmediate', 404 ); 405 this.#realPromisifiedSetImmediate = ObjectGetOwnPropertyDescriptor( 406 nodeTimersPromises, 407 'setImmediate', 408 ); 409 } 410 411 #storeOriginalSetInterval() { 412 this.#realSetInterval = ObjectGetOwnPropertyDescriptor( 413 globalThis, 414 'setInterval', 415 ); 416 this.#realClearInterval = ObjectGetOwnPropertyDescriptor( 417 globalThis, 418 'clearInterval', 419 ); 420 this.#realTimersSetInterval = ObjectGetOwnPropertyDescriptor( 421 nodeTimers, 422 'setInterval', 423 ); 424 this.#realTimersClearInterval = ObjectGetOwnPropertyDescriptor( 425 nodeTimers, 426 'clearInterval', 427 ); 428 this.#realPromisifiedSetInterval = ObjectGetOwnPropertyDescriptor( 429 nodeTimersPromises, 430 'setInterval', 431 ); 432 } 433 434 #storeOriginalSetTimeout() { 435 this.#realSetTimeout = ObjectGetOwnPropertyDescriptor( 436 globalThis, 437 'setTimeout', 438 ); 439 this.#realClearTimeout = ObjectGetOwnPropertyDescriptor( 440 globalThis, 441 'clearTimeout', 442 ); 443 this.#realTimersSetTimeout = ObjectGetOwnPropertyDescriptor( 444 nodeTimers, 445 'setTimeout', 446 ); 447 this.#realTimersClearTimeout = ObjectGetOwnPropertyDescriptor( 448 nodeTimers, 449 'clearTimeout', 450 ); 451 this.#realPromisifiedSetTimeout = ObjectGetOwnPropertyDescriptor( 452 nodeTimersPromises, 453 'setTimeout', 454 ); 455 } 456 457 /** 458 * Advances the virtual time of MockTimers by the specified duration (in milliseconds). 459 * This method simulates the passage of time and triggers any scheduled timers that are due. 460 * @param {number} [time=1] - The amount of time (in milliseconds) to advance the virtual time. 461 * @throws {ERR_INVALID_STATE} If MockTimers are not enabled. 462 * @throws {ERR_INVALID_ARG_VALUE} If a negative time value is provided. 463 */ 464 tick(time = 1) { 465 if (!this.#isEnabled) { 466 throw new ERR_INVALID_STATE( 467 'You should enable MockTimers first by calling the .enable function', 468 ); 469 } 470 471 if (time < 0) { 472 throw new ERR_INVALID_ARG_VALUE( 473 'time', 474 'positive integer', 475 time, 476 ); 477 } 478 479 this.#now += time; 480 let timer = this.#executionQueue.peek(); 481 while (timer) { 482 if (timer.runAt > this.#now) break; 483 FunctionPrototypeApply(timer.callback, undefined, timer.args); 484 485 this.#executionQueue.shift(); 486 487 if (timer.interval) { 488 timer.runAt += timer.interval; 489 this.#executionQueue.insert(timer); 490 return; 491 } 492 493 timer = this.#executionQueue.peek(); 494 } 495 } 496 497 /** 498 * Enables MockTimers for the specified timers. 499 * @param {string[]} timers - An array of timer types to enable, e.g., ['setTimeout', 'setInterval']. 500 * @throws {ERR_INVALID_STATE} If MockTimers are already enabled. 501 * @throws {ERR_INVALID_ARG_VALUE} If an unsupported timer type is specified. 502 */ 503 enable(timers = SUPPORTED_TIMERS) { 504 if (this.#isEnabled) { 505 throw new ERR_INVALID_STATE( 506 'MockTimers is already enabled!', 507 ); 508 } 509 510 validateArray(timers, 'timers'); 511 512 // Check that the timers passed are supported 513 ArrayPrototypeForEach(timers, (timer) => { 514 if (!ArrayPrototypeIncludes(SUPPORTED_TIMERS, timer)) { 515 throw new ERR_INVALID_ARG_VALUE( 516 'timers', 517 timer, 518 `option ${timer} is not supported`, 519 ); 520 } 521 }); 522 523 this.#timersInContext = timers; 524 this.#now = DateNow(); 525 this.#toggleEnableTimers(true); 526 } 527 528 /** 529 * An alias for `this.reset()`, allowing the disposal of the `MockTimers` instance. 530 */ 531 [SymbolDispose]() { 532 this.reset(); 533 } 534 535 /** 536 * Resets MockTimers, disabling any enabled timers and clearing the execution queue. 537 * Does nothing if MockTimers are not enabled. 538 */ 539 reset() { 540 // Ignore if not enabled 541 if (!this.#isEnabled) return; 542 543 this.#toggleEnableTimers(false); 544 this.#timersInContext = []; 545 546 let timer = this.#executionQueue.peek(); 547 while (timer) { 548 this.#executionQueue.shift(); 549 timer = this.#executionQueue.peek(); 550 } 551 } 552 553 /** 554 * Runs all scheduled timers until there are no more pending timers. 555 * @throws {ERR_INVALID_STATE} If MockTimers are not enabled. 556 */ 557 runAll() { 558 if (!this.#isEnabled) { 559 throw new ERR_INVALID_STATE( 560 'You should enable MockTimers first by calling the .enable function', 561 ); 562 } 563 564 this.tick(Infinity); 565 } 566} 567 568module.exports = { MockTimers }; 569