You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Summary
Introduce a deterministic Virtual Clock that intercepts and controls common time sources and schedulers—setTimeout, setInterval, performance.now, and requestAnimationFrame—in addition to the existing Date manipulation. Provide an explicit time-travel API (advanceBy, advanceTo, tick, flush) for precise, fast-forwardable simulations and tests in both Node and browsers.
Motivation
Deterministic tests without real waiting: Today, changing Date.now() or speed helps, but timeouts/intervals/RAF still run on real wall clock. A Virtual Clock lets tests jump forward instantly and flush pending timers.
Parity with ecosystem expectations: Tools like Jest’s modern fake timers and sinon’s fake timers are popular. Having first-class support in this library creates a one-stop solution for both logical time (Date) and scheduled tasks.
Browser + Node coverage: Frontend apps rely on requestAnimationFrame and performance.now; Node apps rely on precise timer control. A unified abstraction simplifies usage across environments.
Faster feedback loops: Simulations (e.g., “simulate 24 hours of app activity”) become milliseconds.
advanceBy(ms), advanceTo(timestampMs), tick() (run next scheduled task), flush() (run all due tasks)
now() returns the virtual, monotonic clock time
Determinism & order guarantees for same-time tasks (stable FIFO by scheduling order).
Zero-wait testing: advancing time triggers due callbacks synchronously without real delays.
Backward compatible: current enableTimeWarp usage continues to work; timer interception is opt-in.
Non-Goals
Full event-loop phase emulation (we’ll keep a pragmatic, deterministic queue model).
Complex cron parsing (could be a follow-up).
Proposed API
typeTimeWarpOptions={freezeAt?: number|null;// existingspeed?: number;// existingmonkeyPatch?: boolean;// existing Date patch// NEW: timer interception & virtual clocktimers?: {intercept?: boolean;// default: falserAF?: boolean;// default: true when intercept=true and window existsperformanceNow?: boolean;// default: true when intercept=trueimmediate?: boolean;// Node setImmediate, default: true when intercept=true in Node};};// New Virtual Clock interfaceinterfaceVirtualClock{// timenow(): number;// virtual monotonic time (maps to performance.now if patched)dateNow(): number;// virtual epoch ms (maps to Date.now if patched)// time traveladvanceBy(ms: number): void;advanceTo(targetEpochMs: number): void;// jumps Date epoch and perf base accordinglytick(): boolean;// run next due task; returns whether something ranflush(limit?: number): number;// run all due tasks (or up to 'limit'); returns count// inspectiongetPendingTimers(): Array<{id: number;type: 'timeout'|'interval'|'raf'|'immediate';at: number}>;}// New exportsexportfunctiongetVirtualClock(): VirtualClock;exportfunctionisTimerInterceptionEnabled(): boolean;
Request/Runtime Behavior
When timers.intercept is enabled:
setTimeout/setInterval schedule into the Virtual Clock queue at virtualEpoch + delay.
performance.now() derives from the Virtual Clock’s monotonic origin.
In browsers, requestAnimationFrame schedules at the next frame boundary; advancing time beyond ~16.67ms triggers RAF callbacks in order, with a synthetic high-res timestamp (from performance.now()).
In Node, setImmediate queues into a micro-slot that runs before timers scheduled at the same virtual timestamp.
advanceBy / advanceTo move virtual time and synchronously run all now-due callbacks.
Intervals re-schedule based on their registered period using the virtual time, not wall clock.
If both speed (real-time acceleration) and interception are enabled, time still flows automatically; advanceBy/advanceTo are additive manual controls. (Docs will clarify tradeoffs; most tests will prefer freezeAt or speed=0 + manual advanceBy.)
Error Handling & Edges
Reentrancy: If a timer schedules another timer at the same timestamp, execution is FIFO by enqueue order.
Long jumps: advanceBy(1e9) executes due tasks in timestamp order; intervals iterate predictably without drift.
Cancel semantics: clear* removes pending entries before execution.
Examples
import{enableTimeWarp,getVirtualClock,disableTimeWarp}from'time-warp-manipulation';enableTimeWarp({freezeAt: Date.UTC(2030,0,1),monkeyPatch: true,timers: {intercept: true}});constclock=getVirtualClock();letfired=0;setTimeout(()=>fired++,1000);setInterval(()=>fired+=10,500);clock.advanceBy(500);// runs interval once -> fired = 10clock.advanceBy(500);// runs timeout -> fired = 11; interval again -> fired = 21clock.flush();// nothing else due -> returns 0disableTimeWarp();
Summary
Introduce a deterministic Virtual Clock that intercepts and controls common time sources and schedulers—
setTimeout,setInterval,performance.now, andrequestAnimationFrame—in addition to the existingDatemanipulation. Provide an explicit time-travel API (advanceBy,advanceTo,tick,flush) for precise, fast-forwardable simulations and tests in both Node and browsers.Motivation
Date.now()or speed helps, but timeouts/intervals/RAF still run on real wall clock. A Virtual Clock lets tests jump forward instantly and flush pending timers.Date) and scheduled tasks.requestAnimationFrameandperformance.now; Node apps rely on precise timer control. A unified abstraction simplifies usage across environments.Goals
Optional interception (monkey-patch) for:
setTimeout,clearTimeoutsetInterval,clearIntervalperformance.nowrequestAnimationFrame,cancelAnimationFrame(browser only)setImmediate,clearImmediateVirtual Clock API:
advanceBy(ms),advanceTo(timestampMs),tick()(run next scheduled task),flush()(run all due tasks)now()returns the virtual, monotonic clock timeDeterminism & order guarantees for same-time tasks (stable FIFO by scheduling order).
Zero-wait testing: advancing time triggers due callbacks synchronously without real delays.
Backward compatible: current
enableTimeWarpusage continues to work; timer interception is opt-in.Non-Goals
Proposed API
Request/Runtime Behavior
When
timers.interceptis enabled:setTimeout/setIntervalschedule into the Virtual Clock queue atvirtualEpoch + delay.performance.now()derives from the Virtual Clock’s monotonic origin.requestAnimationFrameschedules at the next frame boundary; advancing time beyond ~16.67ms triggers RAF callbacks in order, with a synthetic high-res timestamp (fromperformance.now()).setImmediatequeues into a micro-slot that runs before timers scheduled at the same virtual timestamp.advanceBy/advanceTomove virtual time and synchronously run all now-due callbacks.Intervals re-schedule based on their registered period using the virtual time, not wall clock.
If both
speed(real-time acceleration) and interception are enabled, time still flows automatically;advanceBy/advanceToare additive manual controls. (Docs will clarify tradeoffs; most tests will preferfreezeAtorspeed=0+ manualadvanceBy.)Error Handling & Edges
advanceBy(1e9)executes due tasks in timestamp order; intervals iterate predictably without drift.clear*removes pending entries before execution.Examples
Browser RAF
Acceptance Criteria
Timer interception works for
setTimeout,setInterval,clear*across Node & browser.performance.now and Date.now reflect virtual time when enabled.
requestAnimationFrame and cancelAnimationFrame supported in browser envs.
Virtual Clock API (
advanceBy,advanceTo,tick,flush,now) exposed and documented.Deterministic execution order for timers scheduled at the same timestamp (stable FIFO).
Intervals reschedule based on original cadence without cumulative drift.
Works with existing options (
freezeAt,speed,monkeyPatch) and remains backward-compatible when interception is off.Comprehensive tests:
advanceByjumpsperformance.nowand RAF timestampssetImmediateordering (if enabled)README updated with usage, examples, and caveats.
Implementation Notes
{ epochBaseMs, perfBaseMs }soDate.now()andperformance.now()remain coherent.frameDurationMs(default 16.67ms) and batch callbacks per “frame”.Potential Follow-Ups (separate issues)
jest.useFakeTimers('modern')semantics.Temporal.Nowhooks.