import { ascend, RBTree } from "../collections/rb_tree.ts";import { DelayOptions } from "../async/delay.ts";import { _internals } from "./_time.ts";
export class TimeError extends Error { constructor(message: string) { super(message); this.name = "TimeError"; }}
function isFakeDate(instance: unknown): instance is FakeDate { return instance instanceof FakeDate;}
interface FakeDate extends Date { date: Date;}
function FakeDate(this: void): string;function FakeDate(this: FakeDate): void;function FakeDate( this: FakeDate, value: string | number | Date,): void;function FakeDate( this: FakeDate, year: number, month: number, date?: number, hours?: number, minutes?: number, seconds?: number, ms?: number,): void;function FakeDate( this: FakeDate | void, ...args: any[]): string | void { if (args.length === 0) args.push(FakeDate.now()); if (isFakeDate(this)) { this.date = new _internals.Date(...(args as [])); } else { return new _internals.Date(args[0]).toString(); }}
FakeDate.parse = Date.parse;FakeDate.UTC = Date.UTC;FakeDate.now = () => time?.now ?? _internals.Date.now();Object.getOwnPropertyNames(Date.prototype).forEach((name: string) => { const propName: keyof Date = name as keyof Date; FakeDate.prototype[propName] = function ( this: FakeDate, ...args: any[] ): any { return (this.date[propName] as (...args: any[]) => any).apply( this.date, args, ); };});Object.getOwnPropertySymbols(Date.prototype).forEach((name: symbol) => { const propName: keyof Date = name as unknown as keyof Date; FakeDate.prototype[propName] = function ( this: FakeDate, ...args: any[] ): any { return (this.date[propName] as (...args: any[]) => any).apply( this.date, args, ); };});
interface Timer { id: number; callback: (...args: any[]) => void; delay: number; args: unknown[]; due: number; repeat: boolean;}
export interface FakeTimeOptions { advanceRate: number; advanceFrequency?: number;}
interface DueNode { due: number; timers: Timer[];}
let time: FakeTime | undefined = undefined;
function fakeSetTimeout( callback: (...args: any[]) => void, delay = 0, ...args: any[]): number { if (!time) throw new TimeError("no fake time"); return setTimer(callback, delay, args, false);}
function fakeClearTimeout(id?: number): void { if (!time) throw new TimeError("no fake time"); if (typeof id === "number" && dueNodes.has(id)) { dueNodes.delete(id); }}
function fakeSetInterval( callback: (...args: any[]) => unknown, delay = 0, ...args: any[]): number { if (!time) throw new TimeError("no fake time"); return setTimer(callback, delay, args, true);}
function fakeClearInterval(id?: number): void { if (!time) throw new TimeError("no fake time"); if (typeof id === "number" && dueNodes.has(id)) { dueNodes.delete(id); }}
function setTimer( callback: (...args: any[]) => void, delay = 0, args: unknown[], repeat = false,): number { const id: number = timerId.next().value; delay = Math.max(repeat ? 1 : 0, Math.floor(delay)); const due: number = now + delay; let dueNode: DueNode | null = dueTree.find({ due } as DueNode); if (dueNode === null) { dueNode = { due, timers: [] }; dueTree.insert(dueNode); } dueNode.timers.push({ id, callback, args, delay, due, repeat, }); dueNodes.set(id, dueNode); return id;}
function overrideGlobals(): void { globalThis.Date = FakeDate as DateConstructor; globalThis.setTimeout = fakeSetTimeout; globalThis.clearTimeout = fakeClearTimeout; globalThis.setInterval = fakeSetInterval; globalThis.clearInterval = fakeClearInterval;}
function restoreGlobals(): void { globalThis.Date = _internals.Date; globalThis.setTimeout = _internals.setTimeout; globalThis.clearTimeout = _internals.clearTimeout; globalThis.setInterval = _internals.setInterval; globalThis.clearInterval = _internals.clearInterval;}
function* timerIdGen() { let i = 1; while (true) yield i++;}
let startedAt: number;let now: number;let initializedAt: number;let advanceRate: number;let advanceFrequency: number;let advanceIntervalId: number | undefined;let timerId: Generator<number>;let dueNodes: Map<number, DueNode>;let dueTree: RBTree<DueNode>;
export class FakeTime { constructor( start?: number | string | Date | null, options?: FakeTimeOptions, ) { if (time) time.restore(); initializedAt = _internals.Date.now(); startedAt = start instanceof Date ? start.valueOf() : typeof start === "number" ? Math.floor(start) : typeof start === "string" ? (new Date(start)).valueOf() : initializedAt; if (Number.isNaN(startedAt)) throw new TimeError("invalid start"); now = startedAt;
timerId = timerIdGen(); dueNodes = new Map(); dueTree = new RBTree( (a: DueNode, b: DueNode) => ascend(a.due, b.due), );
overrideGlobals(); time = this;
advanceRate = Math.max( 0, options?.advanceRate ? options.advanceRate : 0, ); advanceFrequency = Math.max( 0, options?.advanceFrequency ? options.advanceFrequency : 10, ); advanceIntervalId = advanceRate > 0 ? _internals.setInterval.call(null, () => { this.tick(advanceRate * advanceFrequency); }, advanceFrequency) : undefined; }
static restore(): void { if (!time) throw new TimeError("time already restored"); time.restore(); }
static async restoreFor<T>( callback: (...args: any[]) => Promise<T> | T, ...args: any[] ): Promise<T> { if (!time) throw new TimeError("no fake time"); let result: T; restoreGlobals(); try { result = await callback.apply(null, args); } finally { overrideGlobals(); } return result; }
get now(): number { return now; } set now(value: number) { if (value < now) throw new Error("time cannot go backwards"); let dueNode: DueNode | null = dueTree.min(); while (dueNode && dueNode.due <= value) { const timer: Timer | undefined = dueNode.timers.shift(); if (timer && dueNodes.has(timer.id)) { now = timer.due; if (timer.repeat) { const due: number = timer.due + timer.delay; let dueNode: DueNode | null = dueTree.find({ due } as DueNode); if (dueNode === null) { dueNode = { due, timers: [] }; dueTree.insert(dueNode); } dueNode.timers.push({ ...timer, due }); dueNodes.set(timer.id, dueNode); } else { dueNodes.delete(timer.id); } timer.callback.apply(null, timer.args); } else if (!timer) { dueTree.remove(dueNode); dueNode = dueTree.min(); } } now = value; }
get start(): number { return startedAt; } set start(value: number) { throw new Error("cannot change start time after initialization"); }
async delay(ms: number, options: DelayOptions = {}): Promise<void> { const { signal } = options; if (signal?.aborted) { return Promise.reject( new DOMException("Delay was aborted.", "AbortError"), ); } return await new Promise((resolve, reject) => { let timer: number | null = null; const abort = () => FakeTime .restoreFor(() => { if (timer) clearTimeout(timer); }) .then(() => reject(new DOMException("Delay was aborted.", "AbortError")) ); const done = () => { signal?.removeEventListener("abort", abort); resolve(); }; FakeTime.restoreFor(() => setTimeout(done, ms)) .then((id) => timer = id); signal?.addEventListener("abort", abort, { once: true }); }); }
async runMicrotasks(): Promise<void> { await this.delay(0); }
tick(ms = 0): void { this.now += ms; }
async tickAsync(ms = 0): Promise<void> { await this.runMicrotasks(); this.now += ms; }
next(): boolean { const next = dueTree.min(); if (next) this.now = next.due; return !!next; }
async nextAsync(): Promise<boolean> { await this.runMicrotasks(); return this.next(); }
runAll(): void { while (!dueTree.isEmpty()) { this.next(); } }
async runAllAsync(): Promise<void> { while (!dueTree.isEmpty()) { await this.nextAsync(); } }
restore(): void { if (!time) throw new TimeError("time already restored"); time = undefined; restoreGlobals(); if (advanceIntervalId) clearInterval(advanceIntervalId); }}