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