@sinonjs/fake-timers
Version:
Fake JavaScript timers
1,486 lines (1,312 loc) • 109 kB
JavaScript
"use strict";
const globalObject = require("@sinonjs/commons").global;
let timersModule, timersPromisesModule;
if (typeof require === "function" && typeof module === "object") {
try {
timersModule = require("timers");
} catch {
// ignored
}
try {
timersPromisesModule = require("timers/promises");
} catch {
// ignored
}
}
/**
* @typedef {"nextAsync" | "manual" | "interval"} TickMode
*/
/**
* @typedef {object} NextAsyncTickMode
* @property {"nextAsync"} mode - runs timers one macrotask at a time
*/
/**
* @typedef {object} ManualTickMode
* @property {"manual"} mode - advances only when the caller explicitly ticks
*/
/**
* @typedef {object} IntervalTickMode
* @property {"interval"} mode - advances automatically on a native interval
* @property {number} [delta] - interval duration in milliseconds
*/
/**
* @typedef {IntervalTickMode | NextAsyncTickMode | ManualTickMode} TimerTickMode
*/
/**
* @callback FakeTimersFunction
* @param {...unknown[]} args
* @returns {unknown}
*/
/**
* @callback VoidVarArgsFunc
* @param {...unknown[]} args - optional arguments to call the callback with
* @returns {void}
*/
/**
* @callback NextTick
* @param {VoidVarArgsFunc} callback - the callback to run
* @param {...unknown[]} args - optional arguments to call the callback with
* @returns {void}
*/
/**
* @callback SetImmediate
* @param {VoidVarArgsFunc} callback - the callback to run
* @param {...unknown[]} args - optional arguments to call the callback with
* @returns {NodeImmediate}
*/
/**
* @callback SetTimeout
* @param {VoidVarArgsFunc} callback - the callback to run
* @param {number} [delay] - optional delay in milliseconds
* @param {...unknown[]} args - optional arguments to call the callback with
* @returns {TimerId} - the timeout identifier
*/
/**
* @callback ClearTimeout
* @param {TimerId} [id] - the timeout identifier to clear
* @returns {void}
*/
/**
* @callback SetInterval
* @param {VoidVarArgsFunc} callback - the callback to run
* @param {number} [delay] - optional delay in milliseconds
* @param {...unknown[]} args - optional arguments to call the callback with
* @returns {TimerId} - the interval identifier
*/
/**
* @callback ClearInterval
* @param {TimerId} [id] - the interval identifier to clear
* @returns {void}
*/
/**
* @callback QueueMicrotask
* @param {VoidVarArgsFunc} callback - the callback to run
* @returns {void}
*/
/**
* @callback TimeRemaining
* @returns {number}
*/
/**
* @typedef {object} IdleDeadline
* @property {boolean} didTimeout - whether or not the callback was called before reaching the optional timeout
* @property {TimeRemaining} timeRemaining - a floating-point value providing an estimate of the number of milliseconds remaining in the current idle period
*/
/**
* @callback RequestIdleCallbackCallback
* @param {IdleDeadline} deadline
*/
/**
* Queues a function to be called during a browser's idle periods
* @callback RequestIdleCallback
* @param {RequestIdleCallbackCallback} callback
* @param {{timeout?: number}} [options] - an options object
* @returns {number} the id
*/
/**
* @callback AnimationFrameCallback
* @param {number} timestamp
*/
/**
* @callback RequestAnimationFrame
* @param {AnimationFrameCallback} callback
* @returns {TimerId} - the request id
*/
/**
* @callback CancelAnimationFrame
* @param {TimerId} id - cancels a frame callback
* @returns {void}
*/
/**
* @callback CancelIdleCallback
* @param {TimerId} id - cancels a scheduled idle callback
* @returns {void}
*/
/**
* @callback ClearImmediate
* @param {NodeImmediate} id - faked `clearImmediate`
* @returns {void}
*/
/**
* @callback CountTimers
* @returns {number}
*/
/**
* @callback RunMicrotasks
* @returns {void}
*/
/**
* @typedef {object} TemporalDuration
* @property {number} years - years component
* @property {number} months - months component
* @property {number} weeks - weeks component
* @property {number} days - days component
* @property {number} hours - hours component
* @property {number} minutes - minutes component
* @property {number} seconds - seconds component
* @property {number} milliseconds - milliseconds component
* @property {number} microseconds - microseconds component
* @property {number} nanoseconds - nanoseconds component
* @property {(options: {unit: string, relativeTo?: unknown}) => number} total - converts to a single unit
*/
/**
* @typedef {object} TemporalTimelike
* @property {number} epochMilliseconds - milliseconds since the Unix epoch (present on Temporal.Instant and Temporal.ZonedDateTime)
*/
/**
* @callback Tick
* @param {number|string|TemporalDuration} tickValue milliseconds, a string parseable by parseTime, or a Temporal.Duration
* @returns {number} will return the new `now` value
*/
/**
* @callback TickAsync
* @param {number|string|TemporalDuration} tickValue milliseconds, a string parseable by parseTime, or a Temporal.Duration
* @returns {Promise<number>}
*/
/**
* @callback Next
* @returns {number}
*/
/**
* @callback NextAsync
* @returns {Promise<number>}
*/
/**
* @callback RunAll
* @returns {number}
*/
/**
* @callback RunToFrame
* @returns {number}
*/
/**
* @callback RunAllAsync
* @returns {Promise<number>}
*/
/**
* @callback RunToLast
* @returns {number}
*/
/**
* @callback RunToLastAsync
* @returns {Promise<number>}
*/
/**
* @callback Reset
* @returns {void}
*/
/**
* @callback SetSystemTime
* @param {number|Date|TemporalTimelike} [now] initial mocked time, as milliseconds since epoch, a Date, a Temporal.Instant, or a Temporal.ZonedDateTime
* @returns {void}
*/
/**
* @callback Jump
* @param {number|string|TemporalDuration} tickValue milliseconds, a human-readable value like "01:11:15", or a Temporal.Duration
* @returns {number}
*/
/**
* @callback Uninstall
* @returns {void}
*/
/**
* @callback SetTickMode
* @param {SetTickModeConfig} tickModeConfig - The new configuration for how the clock should tick.
* @returns {void}
*/
/**
* @callback Hrtime
* @param {Array<number>} [prev]
* @returns {Array<number>}
*/
/**
* @callback WithGlobal
* @param {object} _global Namespace to mock (e.g. `window`)
* @returns {FakeTimers}
*/
/**
* @typedef {"setTimeout" | "clearTimeout" | "setImmediate" | "clearImmediate" | "setInterval" | "clearInterval" | "Date" | "nextTick" | "hrtime" | "requestAnimationFrame" | "cancelAnimationFrame" | "requestIdleCallback" | "cancelIdleCallback" | "performance" | "queueMicrotask" | "Intl" | "Temporal"} FakeMethod
*/
/**
* @typedef {number | NodeImmediate | Timer} TimerId
*/
/* eslint-disable jsdoc/reject-any-type */
/**
* @typedef {Record<string, any> & {
* setTimeout?: SetTimeout,
* clearTimeout?: ClearTimeout,
* setInterval?: SetInterval,
* clearInterval?: ClearInterval,
* setImmediate?: SetImmediate,
* clearImmediate?: ClearImmediate,
* queueMicrotask?: QueueMicrotask,
* requestAnimationFrame?: RequestAnimationFrame,
* cancelAnimationFrame?: CancelAnimationFrame,
* requestIdleCallback?: RequestIdleCallback,
* cancelIdleCallback?: CancelIdleCallback,
* process?: any,
* performance?: any,
* Performance?: any,
* Intl?: any,
* Temporal?: any,
* Promise?: typeof Promise,
* Date: typeof Date & { isFake?: boolean, toSource?: () => string, clock?: any }
* }} GlobalObject
*/
/**
* @typedef {object} TimerHeap
* @property {Timer[]} timers - the heap-ordered timers
* @property {() => Timer | undefined} peek - returns the next timer without removing it
* @property {(timer: Timer) => void} push - adds a timer to the heap
* @property {() => Timer | undefined} pop - removes and returns the next timer
* @property {(timer: Timer) => void} remove - removes a specific timer
*/
/**
* @typedef {object} ClockTickMode
* @property {TickMode} mode - active tick mode
* @property {number} counter - increments whenever the mode changes
* @property {number} [delta] - interval length in milliseconds
*/
/**
* @typedef {object} SetTickModeConfig
* @property {TickMode} mode - desired tick mode
* @property {number} [delta] - interval length in milliseconds
*/
/**
* @typedef {Record<string, any> & { clock: Clock }} IntlWithClock
*/
/**
* @typedef {Record<string, any> & { now: () => number }} PerformanceLike
*/
/**
* @typedef {object} Timers
* @property {SetTimeout} setTimeout - native `setTimeout`
* @property {ClearTimeout} clearTimeout - native `clearTimeout`
* @property {SetInterval} setInterval - native `setInterval`
* @property {ClearInterval} clearInterval - native `clearInterval`
* @property {typeof Date} Date - native `Date`
* @property {typeof Intl} [Intl] - native `Intl`
* @property {any} [Temporal] - native `Temporal`
* @property {SetImmediate} [setImmediate] - native `setImmediate`, if available
* @property {ClearImmediate} [clearImmediate] - native `clearImmediate`, if available
* @property {Hrtime} [hrtime] - native `process.hrtime`, if available
* @property {NextTick} [nextTick] - native `process.nextTick`, if available
* @property {PerformanceLike} [performance] - native `performance`, if available
* @property {RequestAnimationFrame} [requestAnimationFrame] - native `requestAnimationFrame`, if available
* @property {QueueMicrotask} [queueMicrotask] - whether `queueMicrotask` exists
* @property {CancelAnimationFrame} [cancelAnimationFrame] - native `cancelAnimationFrame`, if available
* @property {RequestIdleCallback} [requestIdleCallback] - native `requestIdleCallback`, if available
* @property {CancelIdleCallback} [cancelIdleCallback] - native `cancelIdleCallback`, if available
*/
/**
* @typedef {object} ClockState
* @property {number} tickFrom - lower bound of the current tick range
* @property {number} tickTo - upper bound of the current tick range
* @property {number} [previous] - previous timer time used during ticking
* @property {number | null} [oldNow] - previous value of `now`
* @property {Timer} [timer] - timer currently being processed
* @property {unknown} [firstException] - first exception raised while processing timers
* @property {number} [nanosTotal] - accumulated nanoseconds from fractional ticks
* @property {number} [msFloat] - accumulated fractional milliseconds
* @property {number} [ms] - accumulated whole milliseconds
*/
/**
* @typedef {object} TimerInitialProps
* @property {VoidVarArgsFunc} func - callback or string to execute
* @property {unknown[]} [args] - arguments passed to the callback
* @property {'Timeout' | 'Interval' | 'Immediate' | 'AnimationFrame' | 'IdleCallback'} [type] - timer kind
* @property {number} [delay] - requested delay in milliseconds
* @property {number} [callAt] - scheduled execution time
* @property {number} [createdAt] - time at which the timer was created
* @property {boolean} [immediate] - whether this timer should run before non-immediate timers at the same time
* @property {number} [id] - unique timer identifier
* @property {Error} [error] - captured stack for loop diagnostics
* @property {number} [interval] - interval for repeated timers
* @property {boolean} [animation] - whether this is an animation frame timer
* @property {boolean} [requestIdleCallback] - whether this is an idle callback timer
* @property {number} [order] - execution order for timers at the same time
* @property {number} [heapIndex] - index in the timer heap
*/
/**
* @callback CreateClockCallback
* @param {number|Date|TemporalTimelike} [start] initial mocked time, as milliseconds since epoch, a Date, a Temporal.Instant, or a Temporal.ZonedDateTime
* @param {number} [loopLimit] maximum number of timers run before aborting with an infinite-loop error
* @returns {Clock}
*/
/**
* @callback InstallCallback
* @param {Config} [config] Optional config
* @returns {Clock}
*/
/**
* @typedef {object} FakeTimers
* @property {Timers} timers - the native timer APIs saved for later restoration
* @property {CreateClockCallback} createClock - creates a new fake clock
* @property {InstallCallback} install - installs the fake timers onto the default global object
* @property {WithGlobal} withGlobal - creates a fake-timers instance for a provided global object
*/
/**
* @typedef {object} Clock
* @property {number} now - current mocked time in milliseconds
* @property {typeof Date & {clock?: Clock, isFake?: boolean, toSource?: () => string}} Date - fake Date constructor bound to this clock
* @property {number} loopLimit - maximum number of timers before assuming an infinite loop
* @property {RequestIdleCallback} requestIdleCallback - schedules an idle callback
* @property {CancelIdleCallback} cancelIdleCallback - cancels a scheduled idle callback
* @property {SetTimeout} setTimeout - faked `setTimeout`
* @property {ClearTimeout} clearTimeout - faked `clearTimeout`
* @property {NextTick} nextTick - faked `process.nextTick`
* @property {QueueMicrotask} queueMicrotask - faked `queueMicrotask`
* @property {SetInterval} setInterval - faked `setInterval`
* @property {ClearInterval} clearInterval - faked `clearInterval`
* @property {SetImmediate} setImmediate - faked `setImmediate`
* @property {ClearImmediate} clearImmediate - faked `clearImmediate`
* @property {CountTimers} countTimers - counts scheduled timers
* @property {RequestAnimationFrame} requestAnimationFrame - schedules a frame callback
* @property {CancelAnimationFrame} cancelAnimationFrame - cancels a frame callback
* @property {RunMicrotasks} runMicrotasks - drains microtasks
* @property {Tick} tick - advances fake time synchronously
* @property {TickAsync} tickAsync - advances fake time asynchronously
* @property {Next} next - runs the next scheduled timer
* @property {NextAsync} nextAsync - runs the next scheduled timer asynchronously
* @property {RunAll} runAll - runs all scheduled timers
* @property {RunToFrame} runToFrame - runs timers up to the next animation frame
* @property {RunAllAsync} runAllAsync - runs all scheduled timers asynchronously
* @property {RunToLast} runToLast - runs timers up to the last scheduled timer
* @property {RunToLastAsync} runToLastAsync - runs timers up to the last scheduled timer asynchronously
* @property {Reset} reset - clears all timers and resets the clock
* @property {SetSystemTime} setSystemTime - sets the clock to a specific wall-clock time
* @property {Jump} jump - advances time and returns the new `now`
* @property {any} performance - fake performance object
* @property {Hrtime} hrtime - faked `process.hrtime`
* @property {Uninstall} uninstall - restores native timers
* @property {string[]} methods - names of faked methods
* @property {boolean} [shouldClearNativeTimers] - inherited from config
* @property {{methodName:string, original:unknown}[] | undefined} timersModuleMethods - saved Node timers module methods
* @property {{methodName:string, original:unknown}[] | undefined} timersPromisesModuleMethods - saved Node timers/promises methods
* @property {Map<VoidVarArgsFunc, AbortSignal>} abortListenerMap - active abort listeners
* @property {SetTickMode} setTickMode - switches the auto-tick mode
* @property {Map<number, Timer>} [timers] - internal timer storage
* @property {TimerHeap} [timerHeap] - internal timer heap
* @property {boolean} [duringTick] - internal flag
* @property {boolean} isNearInfiniteLimit - internal flag indicating the loop limit is nearly reached
* @property {TimerId} [attachedInterval] - internal flag
* @property {ClockTickMode} [tickMode] - internal flag
* @property {Timer[]} [jobs] - internal flag
* @property {IntlWithClock} [Intl] - fake Intl object
* @property {any} [Temporal] - fake Temporal object
*/
/* eslint-enable jsdoc/reject-any-type */
/**
* Configuration object for the `install` method.
* @typedef {object} Config
* @property {number|Date|TemporalTimelike} [now] initial mocked time, as milliseconds since epoch, a Date, a Temporal.Instant, or a Temporal.ZonedDateTime
* @property {FakeMethod[]} [toFake] method names that should be faked
* @property {FakeMethod[]} [toNotFake] method names that should remain native
* @property {number} [loopLimit] maximum number of timers run before aborting with an infinite-loop error
* @property {boolean} [shouldAdvanceTime] automatically increments mocked time while the clock is installed
* @property {number} [advanceTimeDelta] interval in milliseconds used when `shouldAdvanceTime` is enabled
* @property {boolean} [shouldClearNativeTimers] forwards clear calls to native methods when the timer is not fake
* @property {boolean} [ignoreMissingTimers] suppresses errors when a requested timer is missing from the global object
* @property {GlobalObject} [target] global object to install onto
*/
/**
* The internal structure to describe a scheduled fake timer
* @typedef {TimerInitialProps} Timer
* @property {unknown[]} args - arguments passed to the callback
* @property {number} callAt - scheduled execution time
* @property {number} createdAt - time at which the timer was created
* @property {number} id - unique timer identifier
* @property {'Timeout' | 'Interval' | 'Immediate' | 'AnimationFrame' | 'IdleCallback'} type - timer kind
*/
/**
* @callback NodeImmediateHasRef
* @returns {boolean}
*/
/**
* @callback NodeImmediateRef
* @returns {NodeImmediate}
*/
/**
* @callback NodeImmediateUnref
* @returns {NodeImmediate}
*/
/**
* A Node timer
* @typedef {object} NodeImmediate
* @property {NodeImmediateHasRef} hasRef - reports whether the timer keeps the event loop alive
* @property {NodeImmediateRef} ref - marks the timer as referenced
* @property {NodeImmediateUnref} unref - marks the timer as unreferenced
*/
/* eslint-disable complexity */
/**
* Mocks available features in the specified global namespace.
* @param {GlobalObject} _global Namespace to mock (e.g. `window`)
* @returns {FakeTimers}
*/
function withGlobal(_global) {
const maxTimeout = Math.pow(2, 31) - 1; //see https://heycam.github.io/webidl/#abstract-opdef-converttoint
const idCounterStart = 1e12; // arbitrarily large number to avoid collisions with native timer IDs
const NOOP = function () {
return undefined;
};
const NOOP_ARRAY = function () {
return [];
};
const isPresent = {};
let timeoutResult,
addTimerReturnsObject = false;
if (_global.setTimeout) {
isPresent.setTimeout = true;
timeoutResult = _global.setTimeout(NOOP, 0);
addTimerReturnsObject = typeof timeoutResult === "object";
}
isPresent.clearTimeout = Boolean(_global.clearTimeout);
isPresent.setInterval = Boolean(_global.setInterval);
isPresent.clearInterval = Boolean(_global.clearInterval);
isPresent.hrtime =
_global.process && typeof _global.process.hrtime === "function";
isPresent.hrtimeBigint =
isPresent.hrtime && typeof _global.process.hrtime.bigint === "function";
isPresent.nextTick =
_global.process && typeof _global.process.nextTick === "function";
const utilPromisify = _global.process && require("util").promisify;
isPresent.performance =
_global.performance && typeof _global.performance.now === "function";
const hasPerformancePrototype =
_global.Performance &&
(typeof _global.Performance).match(/^(function|object)$/);
const hasPerformanceConstructorPrototype =
_global.performance &&
_global.performance.constructor &&
_global.performance.constructor.prototype;
isPresent.queueMicrotask = Object.prototype.hasOwnProperty.call(
_global,
"queueMicrotask",
);
isPresent.requestAnimationFrame =
_global.requestAnimationFrame &&
typeof _global.requestAnimationFrame === "function";
isPresent.cancelAnimationFrame =
_global.cancelAnimationFrame &&
typeof _global.cancelAnimationFrame === "function";
isPresent.requestIdleCallback =
_global.requestIdleCallback &&
typeof _global.requestIdleCallback === "function";
isPresent.cancelIdleCallback =
_global.cancelIdleCallback &&
typeof _global.cancelIdleCallback === "function";
isPresent.setImmediate =
_global.setImmediate && typeof _global.setImmediate === "function";
isPresent.clearImmediate =
_global.clearImmediate && typeof _global.clearImmediate === "function";
isPresent.Intl = _global.Intl && typeof _global.Intl === "object";
isPresent.Temporal =
_global.Temporal !== null &&
typeof _global.Temporal === "object" &&
typeof _global.Temporal.Now !== "undefined" &&
typeof _global.Temporal.Instant !== "undefined";
if (_global.clearTimeout) {
_global.clearTimeout(timeoutResult);
}
const NativeDate = _global.Date;
const NativeIntl = isPresent.Intl
? Object.defineProperties(
Object.create(null),
Object.getOwnPropertyDescriptors(_global.Intl),
)
: undefined;
const NativeTemporal = isPresent.Temporal ? _global.Temporal : undefined;
let uniqueTimerId = idCounterStart;
/** @type {number} */
let uniqueTimerOrder = 0;
if (NativeDate === undefined) {
throw new Error(
"The global scope doesn't have a `Date` object" +
" (see https://github.com/sinonjs/sinon/issues/1852#issuecomment-419622780)",
);
}
isPresent.Date = true;
/**
* The PerformanceEntry object encapsulates a single performance metric
* that is part of the browser's performance timeline.
*
* This is an object returned by the `mark` and `measure` methods on the Performance prototype
*/
class FakePerformanceEntry {
constructor(name, entryType, startTime, duration) {
this.name = name;
this.entryType = entryType;
this.startTime = startTime;
this.duration = duration;
}
toJSON() {
return JSON.stringify({ ...this });
}
}
/**
* @param {number} num
* @returns {boolean}
*/
function isNumberFinite(num) {
if (Number.isFinite) {
return Number.isFinite(num);
}
return isFinite(num);
}
/**
* @param {Clock} clock
* @param {number} i
*/
function checkIsNearInfiniteLimit(clock, i) {
if (clock.loopLimit && i === clock.loopLimit - 1) {
clock.isNearInfiniteLimit = true;
}
}
/**
* @param {Clock} clock
*/
function resetIsNearInfiniteLimit(clock) {
if (clock) {
clock.isNearInfiniteLimit = false;
}
}
/**
* Parse strings like "01:10:00" (meaning 1 hour, 10 minutes, 0 seconds) into
* number of milliseconds. This is used to support human-readable strings passed
* to clock.tick()
* @param {string} str
* @returns {number}
*/
function parseTime(str) {
if (!str) {
return 0;
}
const strings = str.split(":");
const l = strings.length;
let i = l;
let ms = 0;
let parsed;
if (l > 3 || !/^(\d\d:){0,2}\d\d?$/.test(str)) {
throw new Error(
"tick only understands numbers, 'm:s' and 'h:m:s'. Each part must be two digits",
);
}
while (i--) {
parsed = parseInt(strings[i], 10);
if (parsed >= 60) {
throw new Error(`Invalid time ${str}`);
}
ms += parsed * Math.pow(60, l - i - 1);
}
return ms * 1000;
}
/**
* Get the decimal part of the millisecond value as nanoseconds
* @param {number} msFloat the number of milliseconds
* @returns {number} an integer number of nanoseconds in the range [0,1e6)
*
* Example: nanoRemainer(123.456789) -> 456789
*/
function nanoRemainder(msFloat) {
const modulo = 1e6;
const remainder = (msFloat * 1e6) % modulo;
const positiveRemainder =
remainder < 0 ? remainder + modulo : remainder;
return Math.floor(positiveRemainder);
}
/**
* Used to grok the `now` parameter to createClock.
* @param {Date|number|TemporalTimelike} epoch the system time
* @returns {number}
*/
function getEpoch(epoch) {
if (!epoch) {
return 0;
}
if (typeof epoch === "number") {
return epoch;
}
if (typeof (/** @type {Date} */ (epoch).getTime) === "function") {
return /** @type {Date} */ (epoch).getTime();
}
if (
typeof (
/** @type {TemporalTimelike} */ (epoch).epochMilliseconds
) === "number"
) {
// Temporal.Instant and Temporal.ZonedDateTime both have epochMilliseconds
return /** @type {TemporalTimelike} */ (epoch).epochMilliseconds;
}
throw new TypeError("now should be milliseconds since UNIX epoch");
}
/**
* @param {number} from
* @param {number} to
* @param {Timer} timer
* @returns {boolean}
*/
function inRange(from, to, timer) {
return timer && timer.callAt >= from && timer.callAt <= to;
}
/**
* @param {Clock} clock
* @param {Timer} job
* @returns {Error}
*/
function getInfiniteLoopError(clock, job) {
const infiniteLoopError = new Error(
`Aborting after running ${clock.loopLimit} timers, assuming an infinite loop!`,
);
if (!job.error) {
return infiniteLoopError;
}
// pattern never matched in Node
const computedTargetPattern = /target\.*[<|(|[].*?[>|\]|)]\s*/;
let clockMethodPattern = new RegExp(
String(Object.keys(clock).join("|")),
);
if (addTimerReturnsObject) {
// node.js environment
clockMethodPattern = new RegExp(
`\\s+at (Object\\.)?(?:${Object.keys(clock).join("|")})\\s+`,
);
}
let matchedLineIndex = -1;
job.error.stack.split("\n").some(function (line, i) {
// If we've matched a computed target line (e.g. setTimeout) then we
// don't need to look any further. Return true to stop iterating.
const matchedComputedTarget = line.match(computedTargetPattern);
/* istanbul ignore if */
if (matchedComputedTarget) {
matchedLineIndex = i;
return true;
}
// If we've matched a clock method line, then there may still be
// others further down the trace. Return false to keep iterating.
const matchedClockMethod = line.match(clockMethodPattern);
if (matchedClockMethod) {
matchedLineIndex = i;
return false;
}
// If we haven't matched anything on this line, but we matched
// previously and set the matched line index, then we can stop.
// If we haven't matched previously, then we should keep iterating.
return matchedLineIndex >= 0;
});
const stack = `${infiniteLoopError}\n${job.type || "Microtask"} - ${
job.func.name || "anonymous"
}\n${job.error.stack
.split("\n")
.slice(matchedLineIndex + 1)
.join("\n")}`;
try {
Object.defineProperty(infiniteLoopError, "stack", {
value: stack,
});
} catch {
// noop
}
return infiniteLoopError;
}
/**
* @returns {typeof Date & { clock: Clock }}
*/
function createDate() {
class ClockDate extends NativeDate {
/** @type {Clock} */
static clock;
constructor(...args) {
// Preserve fake time when Date is called without arguments.
if (args.length === 0) {
super(ClockDate.clock.now);
} else {
// The subclass is intentionally thin for explicit args.
// @ts-expect-error Date constructor overloads are intentionally dynamic.
super(...args);
}
// ensures identity checks using the constructor prop still works
// this should have no other functional effect
Object.defineProperty(this, "constructor", {
value: NativeDate,
enumerable: false,
});
}
static [Symbol.hasInstance](instance) {
return instance instanceof NativeDate;
}
}
ClockDate.isFake = true;
if (NativeDate.now) {
ClockDate.now = function now() {
return ClockDate.clock.now;
};
}
const NativeDateWithToSource =
/** @type {typeof Date & { toSource?: () => string }} */ (
NativeDate
);
if (NativeDateWithToSource.toSource) {
ClockDate.toSource = function toSource() {
return NativeDateWithToSource.toSource();
};
}
ClockDate.toString = function toString() {
return NativeDateWithToSource.toString();
};
// noinspection UnnecessaryLocalVariableJS
/**
* A normal Class constructor cannot be called without `new`, but Date can, so we need
* to wrap it in a Proxy in order to ensure this functionality of Date is kept intact
* @type {typeof ClockDate}
*/
const ClockDateProxy = new Proxy(ClockDate, {
// handler for [[Call]] invocations (i.e. not using `new`)
apply() {
// the Date constructor called as a function, ref Ecma-262 Edition 5.1, section 15.9.2.
// This remains so in the 10th edition of 2019 as well.
if (this instanceof ClockDate) {
throw new TypeError(
"A Proxy should only capture `new` calls with the `construct` handler. This is not supposed to be possible, so check the logic.",
);
}
return new NativeDate(ClockDate.clock.now).toString();
},
});
return /** @type {typeof Date & { clock: Clock }} */ (
/** @type {unknown} */ (ClockDateProxy)
);
}
/**
* Mirror Intl by default on our fake implementation
*
* Most of the properties are the original native ones,
* but we need to take control of those that have a
* dependency on the current clock.
* @param {Clock} clock
* @returns {IntlWithClock} the partly fake Intl implementation
*/
function createIntl(clock) {
/** @type {IntlWithClock} */
const IntlWithClock = { clock: clock };
/*
* All properties of Intl are non-enumerable, so we need
* to do a bit of work to get them out.
*/
Object.getOwnPropertyNames(NativeIntl).forEach(
(property) => (IntlWithClock[property] = NativeIntl[property]),
);
IntlWithClock.DateTimeFormat = function (...args) {
const realFormatter = new NativeIntl.DateTimeFormat(...args);
const formatter = {};
["formatRange", "formatRangeToParts", "resolvedOptions"].forEach(
(method) => {
formatter[method] =
realFormatter[method].bind(realFormatter);
},
);
["format", "formatToParts"].forEach((method) => {
formatter[method] = function (date) {
return realFormatter[method](
date || IntlWithClock.clock.now,
);
};
});
return formatter;
};
IntlWithClock.DateTimeFormat.prototype = Object.create(
NativeIntl.DateTimeFormat.prototype,
);
IntlWithClock.DateTimeFormat.supportedLocalesOf =
NativeIntl.DateTimeFormat.supportedLocalesOf;
return IntlWithClock;
}
//eslint-disable-next-line jsdoc/require-jsdoc
function createTemporal(clock, getNanos) {
const fakeNow = {
instant() {
return NativeTemporal.Instant.fromEpochNanoseconds(
BigInt(clock.now) * 1_000_000n + BigInt(getNanos()),
);
},
timeZoneId() {
return NativeTemporal.Now.timeZoneId();
},
zonedDateTimeISO(timeZone) {
const tz = timeZone ?? NativeTemporal.Now.timeZoneId();
return fakeNow.instant().toZonedDateTimeISO(tz);
},
plainDateTimeISO(timeZone) {
return fakeNow.zonedDateTimeISO(timeZone).toPlainDateTime();
},
plainDateISO(timeZone) {
return fakeNow.zonedDateTimeISO(timeZone).toPlainDate();
},
plainTimeISO(timeZone) {
return fakeNow.zonedDateTimeISO(timeZone).toPlainTime();
},
};
const TemporalWithClock = Object.create(
Object.getPrototypeOf(NativeTemporal),
);
[
...Object.getOwnPropertyNames(NativeTemporal),
...Object.getOwnPropertySymbols(NativeTemporal),
].forEach((prop) => {
Object.defineProperty(
TemporalWithClock,
prop,
Object.getOwnPropertyDescriptor(NativeTemporal, prop),
);
});
// Temporal.Now is writable:false in the spec so we must use defineProperty
Object.defineProperty(TemporalWithClock, "Now", {
value: fakeNow,
writable: true,
enumerable: false,
configurable: true,
});
return TemporalWithClock;
}
//eslint-disable-next-line jsdoc/require-jsdoc
function enqueueJob(clock, job) {
// enqueues a microtick-deferred task - ecma262/#sec-enqueuejob
if (!clock.jobs) {
clock.jobs = [];
}
clock.jobs.push(job);
}
//eslint-disable-next-line jsdoc/require-jsdoc
function runJobs(clock) {
// runs all microtick-deferred tasks - ecma262/#sec-runjobs
if (!clock.jobs) {
return;
}
const wasNearLimit = clock.isNearInfiniteLimit;
for (let i = 0; i < clock.jobs.length; i++) {
const job = clock.jobs[i];
job.func.apply(null, job.args);
checkIsNearInfiniteLimit(clock, i);
if (clock.loopLimit && i > clock.loopLimit) {
throw getInfiniteLoopError(clock, job);
}
}
if (!wasNearLimit) {
resetIsNearInfiniteLimit(clock);
}
clock.jobs = [];
}
/**
* A compact "soonest timer first" container.
*
* Think of this as a waiting room for scheduled callbacks where the next
* callback to run is always kept at the front of the list. The internal
* array is arranged so we can find, add, remove, and reorder timers
* efficiently without sorting the whole list every time something changes.
*
* The important idea is not the data structure name, but the behavior:
* the timer that should run next stays near the front, and when one timer
* moves, the rest are shifted just enough to keep that promise true.
*/
class TimerHeap {
constructor() {
this.timers = [];
}
/**
* Look at the next timer without removing it.
* This is the timer the clock would run first if time advanced now.
* @returns {Timer}
*/
peek() {
return this.timers[0];
}
/**
* Add a timer to the waiting room, then move it upward until it is in
* the right place relative to the timers it should run before and after.
* @param {Timer} timer
*/
push(timer) {
this.timers.push(timer);
this.bubbleUp(this.timers.length - 1);
}
/**
* Remove and return the next timer to run.
*
* We pull the front timer out, move the last timer into the empty spot,
* and then shift that replacement down until the ordering is correct
* again. That avoids rebuilding the whole list from scratch.
* @returns {Timer|undefined}
*/
pop() {
if (this.timers.length === 0) {
return undefined;
}
const first = this.timers[0];
const last = this.timers.pop();
if (this.timers.length > 0) {
this.timers[0] = last;
last.heapIndex = 0;
this.bubbleDown(0);
}
delete first.heapIndex;
return first;
}
/**
* Remove a specific timer from the waiting room.
*
* The heap stores timers in a shape that lets us jump directly to the
* timer's current position, replace it with the last timer, and then
* move that replacement up or down until the ordering is correct again.
* @param {Timer} timer
* @returns {boolean}
*/
remove(timer) {
const index = timer.heapIndex;
if (index === undefined || this.timers[index] !== timer) {
return false;
}
const last = this.timers.pop();
if (timer !== last) {
this.timers[index] = last;
last.heapIndex = index;
if (compareTimers(last, timer) < 0) {
this.bubbleUp(index);
} else {
this.bubbleDown(index);
}
}
delete timer.heapIndex;
return true;
}
/**
* Move a timer toward the front until it is no longer "earlier" than
* the timer above it.
*
* Conceptually, this is what happens when something newly scheduled
* turns out to belong ahead of its parent in the waiting room. We keep
* swapping it upward until it is no longer out of place.
* @param {number} index
*/
bubbleUp(index) {
const timer = this.timers[index];
let currentIndex = index;
while (currentIndex > 0) {
const parentIndex = Math.floor((currentIndex - 1) / 2);
const parent = this.timers[parentIndex];
if (compareTimers(timer, parent) < 0) {
this.timers[currentIndex] = parent;
parent.heapIndex = currentIndex;
currentIndex = parentIndex;
} else {
break;
}
}
this.timers[currentIndex] = timer;
timer.heapIndex = currentIndex;
}
/**
* Move a timer away from the front until the timer below it is no
* longer supposed to run after it.
*
* This is the opposite of `bubbleUp`: when a timer at the front is
* removed or moved, the replacement may be too far ahead, so we
* repeatedly swap it downward with the best child until the waiting
* room is ordered again.
* @param {number} index
*/
bubbleDown(index) {
const timer = this.timers[index];
let currentIndex = index;
const halfLength = Math.floor(this.timers.length / 2);
while (currentIndex < halfLength) {
const leftIndex = currentIndex * 2 + 1;
const rightIndex = leftIndex + 1;
let bestChildIndex = leftIndex;
let bestChild = this.timers[leftIndex];
if (
rightIndex < this.timers.length &&
compareTimers(this.timers[rightIndex], bestChild) < 0
) {
bestChildIndex = rightIndex;
bestChild = this.timers[rightIndex];
}
if (compareTimers(bestChild, timer) < 0) {
this.timers[currentIndex] = bestChild;
bestChild.heapIndex = currentIndex;
currentIndex = bestChildIndex;
} else {
break;
}
}
this.timers[currentIndex] = timer;
timer.heapIndex = currentIndex;
}
}
/**
* Ensure timer storage and heap stay in sync even if a clear path touches
* timer state before anything has been scheduled.
*
* Why do we need two data structures to keep tabs on timers?
* 1. Fast ID Lookup (clock.timers): This is a Map from timer IDs to their respective timer objects. It allows clearTimeout(id) and
* clearInterval(id) to be $O(1)$ operations. Without this map, finding a specific timer in the heap to remove it would require a linear
* $O(n)$ search, which would significantly degrade performance as the number of active timers grows.
* 2. Efficient Scheduling (clock.timerHeap): This is a priority queue (min-heap) that keeps timers ordered by their execution time (callAt). It
* allows the library to instantly find the next timer to run (peek() in $O(1)$) and efficiently update the schedule when timers are added or
* removed ($O(\log n)$).
*
* In short: clock.timers provides fast access by ID, while clock.timerHeap provides fast access by Time. Removing either one would make common
* operations (like clearing or finding the next timer) much slower.
* @param {Clock} clock
*/
function ensureTimerState(clock) {
if (!clock.timers) {
clock.timers = new Map();
clock.timerHeap = new TimerHeap();
}
}
/**
* @param {Clock} clock
* @param {number} id
* @returns {boolean}
*/
function hasTimer(clock, id) {
return clock.timers ? clock.timers.has(id) : false;
}
/**
* @param {Clock} clock
* @param {number} id
* @returns {Timer}
*/
function getTimer(clock, id) {
return clock.timers ? clock.timers.get(id) : undefined;
}
/**
* @param {Clock} clock
* @param {Timer} timer
*/
function setTimer(clock, timer) {
ensureTimerState(clock);
clock.timers.set(timer.id, timer);
}
/**
* @param {Clock} clock
* @param {number} id
* @returns {boolean}
*/
function deleteTimer(clock, id) {
return clock.timers ? clock.timers.delete(id) : false;
}
/**
* @param {Clock} clock
* @param {(timer: Timer) => void} callback
*/
function forEachActiveTimer(clock, callback) {
if (!clock.timers) {
return;
}
for (const timer of clock.timers.values()) {
callback(timer);
}
}
/**
* @param {Clock} clock
*/
function rebuildTimerHeap(clock) {
clock.timerHeap = new TimerHeap();
forEachActiveTimer(clock, (timer) => {
clock.timerHeap.push(timer);
});
}
/**
* @param {Clock} clock
* @param {TimerInitialProps} timer
* @returns {TimerId} id of the created timer
*/
function addTimer(clock, timer) {
if (timer.func === undefined) {
throw new Error("Callback must be provided to timer calls");
}
if (typeof timer.func !== "function") {
throw new TypeError(
`[ERR_INVALID_CALLBACK]: Callback must be a function. Received ${
timer.func
} of type ${typeof timer.func}`,
);
}
if (clock.isNearInfiniteLimit) {
timer.error = new Error();
}
timer.type = timer.immediate ? "Immediate" : "Timeout";
if (Object.prototype.hasOwnProperty.call(timer, "delay")) {
if (typeof timer.delay !== "number") {
timer.delay = parseInt(timer.delay, 10);
}
if (!isNumberFinite(timer.delay)) {
timer.delay = 0;
}
timer.delay = timer.delay > maxTimeout ? 1 : timer.delay;
timer.delay = Math.max(0, timer.delay);
}
if (Object.prototype.hasOwnProperty.call(timer, "interval")) {
timer.type = "Interval";
timer.interval = timer.interval > maxTimeout ? 1 : timer.interval;
}
if (Object.prototype.hasOwnProperty.call(timer, "animation")) {
timer.type = "AnimationFrame";
timer.animation = true;
}
if (
Object.prototype.hasOwnProperty.call(timer, "requestIdleCallback")
) {
// mark timer as IdleCallback type if it has no delay, otherwise it'd be of type timeout
// this way we are able to sort such that the timer only gets called when there's truly no pending task to run
if (!timer.delay) {
timer.type = "IdleCallback";
}
timer.requestIdleCallback = true;
}
ensureTimerState(clock);
while (hasTimer(clock, uniqueTimerId)) {
uniqueTimerId++;
if (uniqueTimerId >= Number.MAX_SAFE_INTEGER) {
uniqueTimerId = idCounterStart;
}
}
timer.id = uniqueTimerId++;
if (uniqueTimerId >= Number.MAX_SAFE_INTEGER) {
uniqueTimerId = idCounterStart;
}
timer.order = uniqueTimerOrder++;
timer.createdAt = clock.now;
timer.callAt =
clock.now +
(parseInt(String(timer.delay)) || (clock.duringTick ? 1 : 0));
setTimer(clock, timer);
clock.timerHeap.push(timer);
if (addTimerReturnsObject) {
const res = {
refed: true,
ref: function () {
this.refed = true;
return this;
},
unref: function () {
this.refed = false;
return this;
},
hasRef: function () {
return this.refed;
},
refresh: function () {
timer.callAt =
clock.now +
(parseInt(String(timer.delay)) ||
(clock.duringTick ? 1 : 0));
clock.timerHeap.remove(timer);
timer.order = uniqueTimerOrder++;
setTimer(clock, timer);
clock.timerHeap.push(timer);
return this;
},
[Symbol.toPrimitive]: function () {
return timer.id;
},
};
return res;
}
return timer.id;
}
/* eslint consistent-return: "off" */
/**
* Timer comparator
* @param {Timer} a
* @param {Timer} b
* @returns {number}
*/
function compareTimers(a, b) {
// Sort IdleCallback timers to the bottom when scheduled for the same time
if (a.type === "IdleCallback" && b.type !== "IdleCallback") {
return 1;
}
if (a.type !== "IdleCallback" && b.type === "IdleCallback") {
return -1;
}
// Sort first by absolute timing
if (a.callAt < b.callAt) {
return -1;
}
if (a.callAt > b.callAt) {
return 1;
}
// Sort next by immediate, immediate timers take precedence
if (a.immediate && !b.immediate) {
return -1;
}
if (!a.immediate && b.immediate) {
return 1;
}
if (a.order < b.order) {
return -1;
}
if (a.order > b.order) {
return 1;
}
// Sort next by creation time, earlier-created timers take precedence
if (a.createdAt < b.createdAt) {
return -1;
}
if (a.createdAt > b.createdAt) {
return 1;
}
// Sort next by id, lower-id timers take precedence
if (a.id < b.id) {
return -1;
}
if (a.id > b.id) {
return 1;
}
// As timer ids are unique, no fallback `0` is necessary
return 0;
}
/**
* @param {Clock} clock
* @param {number} from
* @param {number} to
* @returns {Timer}
*/
function firstTimerInRange(clock, from, to) {
if (!clock.timerHeap) {
return null;
}
const timers = clock.timerHeap.timers;
if (timers.length === 1 && timers[0].requestIdleCallback) {
return timers[0];
}
const first = clock.timerHeap.peek();
if (first && inRange(from, to, first)) {
return first;
}
/**
* @type {?Timer}
*/
let timer = null;
for (let i = 0; i < timers.length; i++) {
if (
inRange(from, to, timers[i]) &&
(!timer || compareTimers(timer, timers[i]) === 1)
) {
timer = timers[i];
}
}
return timer;
}
/**
* @param {Clock} clock
* @returns {Timer}
*/
function firstTimer(clock) {
if (!clock.timerHeap) {
return null;
}
return clock.timerHeap.peek() || null;
}
/