nstdlib-nightly
Version:
Node.js standard library converted to runtime-agnostic ES modules.
739 lines (651 loc) • 21.3 kB
JavaScript
// 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 };