UNPKG

nstdlib-nightly

Version:

Node.js standard library converted to runtime-agnostic ES modules.

739 lines (651 loc) 21.3 kB
// Source: https://github.com/nodejs/node/blob/65eff1eb/lib/internal/test_runner/mock/mock_timers.js import { validateAbortSignal, validateNumber, validateStringArray, } from "nstdlib/lib/internal/validators"; import { emitExperimentalWarning } from "nstdlib/lib/internal/util"; import { AbortError, codes as __codes__ } from "nstdlib/lib/internal/errors"; import * as PriorityQueue from "nstdlib/lib/internal/priority_queue"; import * as nodeTimers from "nstdlib/lib/timers"; import * as nodeTimersPromises from "nstdlib/lib/timers/promises"; import * as EventEmitter from "nstdlib/lib/events"; import * as __hoisted_internal_event_target__ from "nstdlib/lib/internal/event_target"; const { ERR_INVALID_ARG_VALUE, ERR_INVALID_STATE } = __codes__; let kResistStopPropagation; // Internal reference to the MockTimers class inside MockDate let kMock; // Initial epoch to which #now should be set to const kInitialEpoch = 0; function compareTimersLists(a, b) { return a.runAt - b.runAt || a.id - b.id; } function setPosition(node, pos) { node.priorityQueuePosition = pos; } function abortIt(signal) { return new AbortError(undefined, { __proto__: null, cause: signal.reason }); } /** * @enum {('setTimeout'|'setInterval'|'setImmediate'|'Date')[]} Supported timers */ const SUPPORTED_APIS = ["setTimeout", "setInterval", "setImmediate", "Date"]; const TIMERS_DEFAULT_INTERVAL = { __proto__: null, setImmediate: -1, }; class Timeout { constructor(opts) { this.id = opts.id; this.callback = opts.callback; this.runAt = opts.runAt; this.interval = opts.interval; this.args = opts.args; } hasRef() { return true; } ref() { return this; } unref() { return this; } refresh() { return this; } } class MockTimers { #realSetTimeout; #realClearTimeout; #realSetInterval; #realClearInterval; #realSetImmediate; #realClearImmediate; #realPromisifiedSetTimeout; #realPromisifiedSetInterval; #realTimersSetTimeout; #realTimersClearTimeout; #realTimersSetInterval; #realTimersClearInterval; #realTimersSetImmediate; #realTimersClearImmediate; #realPromisifiedSetImmediate; #nativeDateDescriptor; #timersInContext = []; #isEnabled = false; #currentTimer = 1; #now = kInitialEpoch; #executionQueue = new PriorityQueue(compareTimersLists, setPosition); #setTimeout = Function.prototype.bind.call(this.#createTimer, this, false); #clearTimeout = Function.prototype.bind.call(this.#clearTimer, this); #setInterval = Function.prototype.bind.call(this.#createTimer, this, true); #clearInterval = Function.prototype.bind.call(this.#clearTimer, this); #clearImmediate = Function.prototype.bind.call(this.#clearTimer, this); constructor() { emitExperimentalWarning("The MockTimers API"); } #restoreSetImmediate() { Object.defineProperty(globalThis, "setImmediate", this.#realSetImmediate); Object.defineProperty( globalThis, "clearImmediate", this.#realClearImmediate, ); Object.defineProperty( nodeTimers, "setImmediate", this.#realTimersSetImmediate, ); Object.defineProperty( nodeTimers, "clearImmediate", this.#realTimersClearImmediate, ); Object.defineProperty( nodeTimersPromises, "setImmediate", this.#realPromisifiedSetImmediate, ); } #restoreOriginalSetInterval() { Object.defineProperty(globalThis, "setInterval", this.#realSetInterval); Object.defineProperty(globalThis, "clearInterval", this.#realClearInterval); Object.defineProperty( nodeTimers, "setInterval", this.#realTimersSetInterval, ); Object.defineProperty( nodeTimers, "clearInterval", this.#realTimersClearInterval, ); Object.defineProperty( nodeTimersPromises, "setInterval", this.#realPromisifiedSetInterval, ); } #restoreOriginalSetTimeout() { Object.defineProperty(globalThis, "setTimeout", this.#realSetTimeout); Object.defineProperty(globalThis, "clearTimeout", this.#realClearTimeout); Object.defineProperty(nodeTimers, "setTimeout", this.#realTimersSetTimeout); Object.defineProperty( nodeTimers, "clearTimeout", this.#realTimersClearTimeout, ); Object.defineProperty( nodeTimersPromises, "setTimeout", this.#realPromisifiedSetTimeout, ); } #storeOriginalSetImmediate() { this.#realSetImmediate = Object.getOwnPropertyDescriptor( globalThis, "setImmediate", ); this.#realClearImmediate = Object.getOwnPropertyDescriptor( globalThis, "clearImmediate", ); this.#realTimersSetImmediate = Object.getOwnPropertyDescriptor( nodeTimers, "setImmediate", ); this.#realTimersClearImmediate = Object.getOwnPropertyDescriptor( nodeTimers, "clearImmediate", ); this.#realPromisifiedSetImmediate = Object.getOwnPropertyDescriptor( nodeTimersPromises, "setImmediate", ); } #storeOriginalSetInterval() { this.#realSetInterval = Object.getOwnPropertyDescriptor( globalThis, "setInterval", ); this.#realClearInterval = Object.getOwnPropertyDescriptor( globalThis, "clearInterval", ); this.#realTimersSetInterval = Object.getOwnPropertyDescriptor( nodeTimers, "setInterval", ); this.#realTimersClearInterval = Object.getOwnPropertyDescriptor( nodeTimers, "clearInterval", ); this.#realPromisifiedSetInterval = Object.getOwnPropertyDescriptor( nodeTimersPromises, "setInterval", ); } #storeOriginalSetTimeout() { this.#realSetTimeout = Object.getOwnPropertyDescriptor( globalThis, "setTimeout", ); this.#realClearTimeout = Object.getOwnPropertyDescriptor( globalThis, "clearTimeout", ); this.#realTimersSetTimeout = Object.getOwnPropertyDescriptor( nodeTimers, "setTimeout", ); this.#realTimersClearTimeout = Object.getOwnPropertyDescriptor( nodeTimers, "clearTimeout", ); this.#realPromisifiedSetTimeout = Object.getOwnPropertyDescriptor( nodeTimersPromises, "setTimeout", ); } #createTimer(isInterval, callback, delay, ...args) { const timerId = this.#currentTimer++; const opts = { __proto__: null, id: timerId, callback, runAt: this.#now + delay, interval: isInterval ? delay : undefined, args, }; const timer = new Timeout(opts); this.#executionQueue.insert(timer); return timer; } #clearTimer(timer) { if (timer?.priorityQueuePosition !== undefined) { this.#executionQueue.removeAt(timer.priorityQueuePosition); timer.priorityQueuePosition = undefined; } } #createDate() { kMock ??= Symbol("MockTimers"); const NativeDateConstructor = this.#nativeDateDescriptor.value; /** * Function to mock the Date constructor, treats cases as per ECMA-262 * and returns a Date object with a mocked implementation * @typedef {Date} MockDate * @returns {MockDate} a mocked Date object */ function MockDate(year, month, date, hours, minutes, seconds, ms) { const mockTimersSource = MockDate[kMock]; const nativeDate = mockTimersSource.#nativeDateDescriptor.value; // As of the fake-timers implementation for Sinon // ref https://github.com/sinonjs/fake-timers/blob/a4c757f80840829e45e0852ea1b17d87a998388e/src/fake-timers-src.js#L456 // This covers the Date constructor called as a function ref. // ECMA-262 Edition 5.1 section 15.9.2. // and ECMA-262 Edition 14 Section 21.4.2.1 // replaces 'this instanceof MockDate' with a more reliable check // from ECMA-262 Edition 14 Section 13.3.12.1 NewTarget if (!new.target) { return Date.prototype.toString.call( new nativeDate(mockTimersSource.#now), ); } // Cases where Date is called as a constructor // This is intended as a defensive implementation to avoid // having unexpected returns switch (arguments.length) { case 0: return new nativeDate(MockDate[kMock].#now); case 1: return new nativeDate(year); case 2: return new nativeDate(year, month); case 3: return new nativeDate(year, month, date); case 4: return new nativeDate(year, month, date, hours); case 5: return new nativeDate(year, month, date, hours, minutes); case 6: return new nativeDate(year, month, date, hours, minutes, seconds); default: return new nativeDate(year, month, date, hours, minutes, seconds, ms); } } // Prototype is read-only, and non assignable through Object.defineProperties // eslint-disable-next-line no-unused-vars -- used to get the prototype out of the object const { prototype, ...dateProps } = Object.getOwnPropertyDescriptors( NativeDateConstructor, ); // Binds all the properties of Date to the MockDate function Object.defineProperties(MockDate, dateProps); MockDate.now = function now() { return MockDate[kMock].#now; }; // This is just to print the function { native code } in the console // when the user prints the function and not the internal code MockDate.toString = function toString() { return Function.prototype.toString.call( MockDate[kMock].#nativeDateDescriptor.value, ); }; // We need to polute the prototype of this Object.defineProperties(MockDate, { __proto__: null, [kMock]: { __proto__: null, enumerable: false, configurable: false, writable: false, value: this, }, isMock: { __proto__: null, enumerable: true, configurable: false, writable: false, value: true, }, }); MockDate.prototype = NativeDateConstructor.prototype; MockDate.parse = NativeDateConstructor.parse; MockDate.UTC = NativeDateConstructor.UTC; MockDate.prototype.toUTCString = NativeDateConstructor.prototype.toUTCString; return MockDate; } async *#setIntervalPromisified(interval, result, options) { const context = this; const emitter = new EventEmitter(); if (options?.signal) { validateAbortSignal(options.signal, "options.signal"); if (options.signal.aborted) { throw abortIt(options.signal); } const onAbort = (reason) => { emitter.emit("data", { __proto__: null, aborted: true, reason }); }; kResistStopPropagation ??= __hoisted_internal_event_target__.kResistStopPropagation; options.signal.addEventListener("abort", onAbort, { __proto__: null, once: true, [kResistStopPropagation]: true, }); } const eventIt = EventEmitter.on(emitter, "data"); const callback = () => { emitter.emit("data", result); }; const timer = this.#createTimer(true, callback, interval, options); const clearListeners = () => { emitter.removeAllListeners(); context.#clearTimer(timer); }; const iterator = { __proto__: null, [SymbolAsyncIterator]() { return this; }, async next() { const result = await eventIt.next(); const value = Array.prototype.at.call(result.value, 0); if (value?.aborted) { iterator.return(); throw abortIt(options.signal); } return { __proto__: null, done: result.done, value, }; }, async return() { clearListeners(); return eventIt.return(); }, }; yield* iterator; } #setImmediate(callback, ...args) { return this.#createTimer( false, callback, TIMERS_DEFAULT_INTERVAL.setImmediate, ...args, ); } #promisifyTimer({ timerFn, clearFn, ms, result, options }) { return new Promise((resolve, reject) => { if (options?.signal) { try { validateAbortSignal(options.signal, "options.signal"); } catch (err) { return reject(err); } if (options.signal.aborted) { return reject(abortIt(options.signal)); } } const onabort = () => { clearFn(timer); return reject(abortIt(options.signal)); }; const timer = timerFn(() => { return resolve(result); }, ms); if (options?.signal) { kResistStopPropagation ??= __hoisted_internal_event_target__.kResistStopPropagation; options.signal.addEventListener("abort", onabort, { __proto__: null, once: true, [kResistStopPropagation]: true, }); } }); } #setImmediatePromisified(result, options) { return this.#promisifyTimer({ __proto__: null, timerFn: Function.prototype.bind.call(this.#setImmediate, this), clearFn: Function.prototype.bind.call(this.#clearImmediate, this), ms: TIMERS_DEFAULT_INTERVAL.setImmediate, result, options, }); } #setTimeoutPromisified(ms, result, options) { return this.#promisifyTimer({ __proto__: null, timerFn: Function.prototype.bind.call(this.#setTimeout, this), clearFn: Function.prototype.bind.call(this.#clearTimeout, this), ms, result, options, }); } #assertTimersAreEnabled() { if (!this.#isEnabled) { throw new ERR_INVALID_STATE( "You should enable MockTimers first by calling the .enable function", ); } } #assertTimeArg(time) { if (time < 0) { throw new ERR_INVALID_ARG_VALUE("time", "positive integer", time); } } #isValidDateWithGetTime(maybeDate) { // Validation inspired on https://github.com/inspect-js/is-date-object/blob/main/index.js#L3-L11 try { Date.prototype.getTime.call(maybeDate); return true; } catch { return false; } } #toggleEnableTimers(activate) { const options = { __proto__: null, toFake: { __proto__: null, setTimeout: () => { this.#storeOriginalSetTimeout(); globalThis.setTimeout = this.#setTimeout; globalThis.clearTimeout = this.#clearTimeout; nodeTimers.setTimeout = this.#setTimeout; nodeTimers.clearTimeout = this.#clearTimeout; nodeTimersPromises.setTimeout = Function.prototype.bind.call( this.#setTimeoutPromisified, this, ); }, setInterval: () => { this.#storeOriginalSetInterval(); globalThis.setInterval = this.#setInterval; globalThis.clearInterval = this.#clearInterval; nodeTimers.setInterval = this.#setInterval; nodeTimers.clearInterval = this.#clearInterval; nodeTimersPromises.setInterval = Function.prototype.bind.call( this.#setIntervalPromisified, this, ); }, setImmediate: () => { this.#storeOriginalSetImmediate(); // setImmediate functions needs to bind MockTimers // otherwise it will throw an error when called // "Receiver must be an instance of MockTimers" // because #setImmediate is the only function here // that calls #createTimer and it's not bound to MockTimers globalThis.setImmediate = Function.prototype.bind.call( this.#setImmediate, this, ); globalThis.clearImmediate = this.#clearImmediate; nodeTimers.setImmediate = Function.prototype.bind.call( this.#setImmediate, this, ); nodeTimers.clearImmediate = this.#clearImmediate; nodeTimersPromises.setImmediate = Function.prototype.bind.call( this.#setImmediatePromisified, this, ); }, Date: () => { this.#nativeDateDescriptor = Object.getOwnPropertyDescriptor( globalThis, "Date", ); globalThis.Date = this.#createDate(); }, }, toReal: { __proto__: null, setTimeout: () => { this.#restoreOriginalSetTimeout(); }, setInterval: () => { this.#restoreOriginalSetInterval(); }, setImmediate: () => { this.#restoreSetImmediate(); }, Date: () => { Object.defineProperty(globalThis, "Date", this.#nativeDateDescriptor); }, }, }; const target = activate ? options.toFake : options.toReal; Array.prototype.forEach.call(this.#timersInContext, (timer) => target[timer](), ); this.#isEnabled = activate; } /** * Advances the virtual time of MockTimers by the specified duration (in milliseconds). * This method simulates the passage of time and triggers any scheduled timers that are due. * @param {number} [time=1] - The amount of time (in milliseconds) to advance the virtual time. * @throws {ERR_INVALID_STATE} If MockTimers are not enabled. * @throws {ERR_INVALID_ARG_VALUE} If a negative time value is provided. */ tick(time = 1) { this.#assertTimersAreEnabled(); this.#assertTimeArg(time); this.#now += time; let timer = this.#executionQueue.peek(); while (timer) { if (timer.runAt > this.#now) break; Function.prototype.apply.call(timer.callback, undefined, timer.args); // Check if the timeout was cleared by calling clearTimeout inside its own callback const afterCallback = this.#executionQueue.peek(); if (afterCallback?.id === timer.id) { this.#executionQueue.shift(); timer.priorityQueuePosition = undefined; } if (timer.interval !== undefined) { timer.runAt += timer.interval; this.#executionQueue.insert(timer); } timer = this.#executionQueue.peek(); } } /** * @typedef {{apis: SUPPORTED_APIS;now: number | Date;}} EnableOptions Options to enable the timers * @property {SUPPORTED_APIS} apis List of timers to enable, defaults to all * @property {number | Date} now The epoch to which the timers should be set to, defaults to 0 */ /** * Enables the MockTimers replacing the native timers with the fake ones. * @param {EnableOptions} [options] */ enable(options = { __proto__: null, apis: SUPPORTED_APIS, now: 0 }) { const internalOptions = { __proto__: null, ...options }; if (this.#isEnabled) { throw new ERR_INVALID_STATE("MockTimers is already enabled!"); } if (Number.isNaN(internalOptions.now)) { throw new ERR_INVALID_ARG_VALUE( "now", internalOptions.now, `epoch must be a positive integer received ${internalOptions.now}`, ); } if (!internalOptions.now) { internalOptions.now = 0; } if (!internalOptions.apis) { internalOptions.apis = SUPPORTED_APIS; } validateStringArray(internalOptions.apis, "options.apis"); // Check that the timers passed are supported Array.prototype.forEach.call(internalOptions.apis, (timer) => { if (!Array.prototype.includes.call(SUPPORTED_APIS, timer)) { throw new ERR_INVALID_ARG_VALUE( "options.apis", timer, `option ${timer} is not supported`, ); } }); this.#timersInContext = internalOptions.apis; // Checks if the second argument is the initial time if (this.#isValidDateWithGetTime(internalOptions.now)) { this.#now = Date.prototype.getTime.call(internalOptions.now); } else if ( validateNumber(internalOptions.now, "initialTime") === undefined ) { this.#assertTimeArg(internalOptions.now); this.#now = internalOptions.now; } this.#toggleEnableTimers(true); } /** * Sets the current time to the given epoch. * @param {number} time The epoch to set the current time to. */ setTime(time = kInitialEpoch) { validateNumber(time, "time"); this.#assertTimeArg(time); this.#assertTimersAreEnabled(); this.#now = time; } /** * An alias for `this.reset()`, allowing the disposal of the `MockTimers` instance. */ [Symbol.for("nodejs.dispose")]() { this.reset(); } /** * Resets MockTimers, disabling any enabled timers and clearing the execution queue. * Does nothing if MockTimers are not enabled. */ reset() { // Ignore if not enabled if (!this.#isEnabled) return; this.#toggleEnableTimers(false); this.#timersInContext = []; this.#now = kInitialEpoch; let timer = this.#executionQueue.peek(); while (timer) { this.#executionQueue.shift(); timer = this.#executionQueue.peek(); } } /** * Runs all scheduled timers until there are no more pending timers. * @throws {ERR_INVALID_STATE} If MockTimers are not enabled. */ runAll() { this.#assertTimersAreEnabled(); const longestTimer = this.#executionQueue.peekBottom(); if (!longestTimer) return; this.tick(longestTimer.runAt - this.#now); } } export { MockTimers };