Skip to content

Virtual Clock & Timer Interception (setTimeout / setInterval / performance.now / requestAnimationFrame) + Time-Travel Controls #2

@hoangsonww

Description

@hoangsonww

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.

Goals

  1. Optional interception (monkey-patch) for:

    • setTimeout, clearTimeout
    • setInterval, clearInterval
    • performance.now
    • requestAnimationFrame, cancelAnimationFrame (browser only)
    • (Node) setImmediate, clearImmediate
  2. Virtual Clock API:

    • advanceBy(ms), advanceTo(timestampMs), tick() (run next scheduled task), flush() (run all due tasks)
    • now() returns the virtual, monotonic clock time
  3. Determinism & order guarantees for same-time tasks (stable FIFO by scheduling order).

  4. Zero-wait testing: advancing time triggers due callbacks synchronously without real delays.

  5. 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

type TimeWarpOptions = {
  freezeAt?: number | null;      // existing
  speed?: number;                // existing
  monkeyPatch?: boolean;         // existing Date patch

  // NEW: timer interception & virtual clock
  timers?: {
    intercept?: boolean;         // default: false
    rAF?: boolean;               // default: true when intercept=true and window exists
    performanceNow?: boolean;    // default: true when intercept=true
    immediate?: boolean;         // Node setImmediate, default: true when intercept=true in Node
  };
};

// New Virtual Clock interface
interface VirtualClock {
  // time
  now(): number;                 // virtual monotonic time (maps to performance.now if patched)
  dateNow(): number;             // virtual epoch ms (maps to Date.now if patched)

  // time travel
  advanceBy(ms: number): void;
  advanceTo(targetEpochMs: number): void; // jumps Date epoch and perf base accordingly
  tick(): boolean;               // run next due task; returns whether something ran
  flush(limit?: number): number; // run all due tasks (or up to 'limit'); returns count

  // inspection
  getPendingTimers(): Array<{ id: number; type: 'timeout'|'interval'|'raf'|'immediate'; at: number }>;
}

// New exports
export function getVirtualClock(): VirtualClock;
export function isTimerInterceptionEnabled(): 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 }
});

const clock = getVirtualClock();
let fired = 0;

setTimeout(() => fired++, 1000);
setInterval(() => fired += 10, 500);

clock.advanceBy(500);   // runs interval once -> fired = 10
clock.advanceBy(500);   // runs timeout -> fired = 11; interval again -> fired = 21
clock.flush();          // nothing else due -> returns 0

disableTimeWarp();

Browser RAF

enableTimeWarp({ timers: { intercept: true, rAF: true } });
const clock = getVirtualClock();

let frames = 0;
function loop(ts: number) {
  frames++;
  if (frames < 3) requestAnimationFrame(loop);
}
requestAnimationFrame(loop);

clock.advanceBy(50); // ~3 frames worth -> frames === 3

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:

    • Basic scheduling, canceling, and re-scheduling
    • Intervals under long advanceBy jumps
    • performance.now and RAF timestamps
    • Node setImmediate ordering (if enabled)
    • Concurrency/reentrancy within callbacks
  • README updated with usage, examples, and caveats.

Implementation Notes

  • Maintain a min-heap priority queue keyed by dueTime and sequence for stable ordering.
  • Represent virtual time as { epochBaseMs, perfBaseMs } so Date.now() and performance.now() remain coherent.
  • For RAF, simulate frame cadence using a configurable frameDurationMs (default 16.67ms) and batch callbacks per “frame”.
  • Provide a no-op shim when APIs don’t exist (e.g., RAF in Node); only patch what’s present.

Potential Follow-Ups (separate issues)

  • Jest integration helper: tiny adapter to sync with jest.useFakeTimers('modern') semantics.
  • Temporal API support (TC39): Temporal.Now hooks.
  • Clock domains: multiple independent virtual clocks with scoped patches.
  • Devtools overlay to visualize scheduled tasks and time jumps.

Metadata

Metadata

Assignees

Labels

bugSomething isn't workingdocumentationImprovements or additions to documentationduplicateThis issue or pull request already existsenhancementNew feature or requestgood first issueGood for newcomershelp wantedExtra attention is neededquestionFurther information is requested

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions