warpclock
v0.2.1
Published
Deterministic time engine for JavaScript async code
Maintainers
Readme
warpclock 🕐
Deterministic time engine for JavaScript async
Execute async code under a controlled virtual clock with deterministic execution order and record/replay capability.
Problem it solves
JavaScript async depends on:
- System clock (not controllable)
- Non-deterministic event loop
- Non-reproducible timers
- Global RNG without seed
This prevents:
- Reproducing bugs
- Flaky tests (random values)
- Simulating long time intervals without waiting
warpclock defines a deterministic micro-runtime where time is a data value, not an external variable.
Implemented features
Core components (deterministic runtime)
1. VirtualClock
Virtual monotonic clock that replaces Date.now() and performance.now().
const clock = new VirtualClock(0);
console.log(clock.now()); // 0
clock.advanceBy(1000);
console.log(clock.now()); // 1000
clock.advanceTo(5000);
console.log(clock.now()); // 5000API:
now(): number- Get current timeadvanceBy(ms: number): void- Advance timeadvanceTo(ts: number): void- Advance to specific timestampreset(time?: number): void- Reset the clock
2. DeterministicScheduler
Executes tasks in total deterministic order.
Model:
interface Task {
id: number // incremental
time: number // virtual timestamp
type: "micro" | "macro"
callback: () => any
}Execution order:
- Lower timestamp
- Microtasks before macrotasks
- Lower ID
API:
scheduleTask(time, callback, type): number- Schedule taskexecuteAll(): Promise<void>- Execute all tasksexecuteUntilTime(time): Promise<void>- Execute until specific time
3. TimerManager
Implements setTimeout, setInterval, clearTimeout, clearInterval without using system clock.
const timers = new TimerManager(scheduler, clock);
const id1 = timers.setTimeout(() => {
console.log("A");
}, 1000);
const id2 = timers.setInterval(() => {
console.log("B");
}, 500);
timers.clearTimeout(id1);
timers.clearInterval(id2);4. ExecutionContext
Container with clock, scheduler, timers, rng and trace. Provides isolated API to the user.
const ctx = new ExecutionContext({
initialTime: 0,
seed: 42,
recordTrace: false
});
ctx.clock.now() // 0
ctx.random() // [0-1) deterministic
await ctx.sleep(500) // Advances clock virtually
await ctx.advanceBy(1000) // Advances clock + executes tasks5. Sleep primitive
Promise that resolves after ms of virtual time.
await ctx.sleep(1000); // Advances 1000ms virtually
console.log(ctx.clock.now()); // 1000Public API
warp() - Main API
Execute async code inside the deterministic engine.
import { warp } from "warpclock";
await warp(async (ctx) => {
// Everything here uses the virtual clock
// Virtual timers
ctx.timers.setTimeout(() => {
console.log("1000ms virtual time passed");
}, 1000);
// Virtual sleep
await ctx.sleep(500);
console.log("500ms virtual time passed");
// Deterministic RNG
const random = ctx.random(); // 0-1
// Advance time manually
await ctx.advanceBy(2000);
// Execute all pending tasks
await ctx.executeAll();
}, {
initialTime: 0,
seed: 42
});Configuration:
interface ExecutionContextConfig {
initialTime?: number; // Initial clock time
seed?: number; // Seed for RNG
recordTrace?: boolean; // Record events
}6. SeededRNG
Deterministic PRNG (xorshift32) that replaces Math.random().
const rng = new SeededRNG(42);
rng.random(); // Same value with seed=42
rng.setSeed(42);
rng.random(); // Same value again7. TraceRecorder (v1 simple)
Records execution events to validate determinism.
ctx.trace.exportTrace(); // Array of TraceEvent[]Format:
interface TraceEvent {
type: "schedule" | "execute"
taskId: number
time: number
taskType?: "micro" | "macro"
meta?: Record<string, any>
}Usage examples
Determinism: same seed = same result
const result1: string[] = [];
await warp(async (ctx) => {
for (let i = 0; i < 5; i++) {
result1.push(ctx.random().toFixed(2));
}
}, { seed: 42 });
const result2: string[] = [];
await warp(async (ctx) => {
for (let i = 0; i < 5; i++) {
result2.push(ctx.random().toFixed(2));
}
}, { seed: 42 });
console.log(result1 === result2); // trueDebugging & validation (implemented)
Tools for tracing, determinism checks, debugging, and profiling:
- TraceReplayer: validate/format/compare traces.
- DeterminismValidator: assert deterministic or non-deterministic behavior across seeds/configs.
- Debugger: snapshots, timeline visualization, pending tasks, summaries, pattern analysis.
- Profiler: performance stats, reports, bottleneck hints.
Example (timeline + replay + determinism):
import { warp, TraceReplayer, DeterminismValidator, Debugger } from "warpclock";
const ctx = await warp(async (c) => {
c.timers.setTimeout(() => {}, 10);
c.timers.setTimeout(() => {}, 50);
await c.executeAll();
return c;
}, { seed: 123, recordTrace: true });
const dbg = new Debugger(ctx);
console.log(dbg.visualizeTimeline());
const replay = await TraceReplayer.validateReplay(async (c) => {
c.timers.setTimeout(() => {}, 10);
c.timers.setTimeout(() => {}, 50);
await c.executeAll();
}, 123);
console.log(replay.valid); // true
const det = await DeterminismValidator.assertDeterministic(async (c) => {
const out: number[] = [];
out.push(c.random());
out.push(c.random());
return out;
}, 5, 123);
console.log(det.isDeterministic); // trueFlake-free timers
await warp(async (ctx) => {
const order: string[] = [];
ctx.timers.setTimeout(() => order.push("A"), 100);
ctx.timers.setTimeout(() => order.push("B"), 50);
ctx.timers.setTimeout(() => order.push("C"), 150);
await ctx.executeAll();
console.log(order); // ["B", "A", "C"] - ALWAYS
});Long time simulation
await warp(async (ctx) => {
// Simulate 1 year (365 days) in real milliseconds
const oneYear = 365 * 24 * 60 * 60 * 1000;
const startTime = ctx.clock.now();
await ctx.advanceTo(startTime + oneYear);
// All time operations completed in ms real time
console.log("1 year simulated in milliseconds");
});Testing async without flakes
test("should process events in order", async () => {
const processed: number[] = [];
await warp(async (ctx) => {
ctx.timers.setTimeout(() => processed.push(1), 100);
ctx.timers.setTimeout(() => processed.push(2), 50);
await ctx.executeAll();
}, { seed: 123 });
expect(processed).toEqual([2, 1]); // Never fails
});Architecture
warpclock/
├── src/
│ ├── VirtualClock.ts # Virtual monotonic clock
│ ├── DeterministicScheduler.ts # Ordered task scheduler
│ ├── TimerManager.ts # setTimeout/setInterval
│ ├── SeededRNG.ts # Deterministic PRNG
│ ├── TraceRecorder.ts # Event recording
│ ├── ExecutionContext.ts # Main container
│ ├── index.ts # Public API
│ └── __tests__/
│ ├── VirtualClock.test.ts
│ ├── DeterministicScheduler.test.ts
│ ├── warp.test.ts
│ └── milestone3.test.ts
├── package.json
├── tsconfig.json
├── jest.config.js
└── README.mdDesign principles
✅ Total determinism: same seed + same code → same order and result
✅ No global monkey-patching: everything is isolated by default
✅ Explicit scope: user runs code inside warp()
✅ Small core: timers, scheduler, sleep, RNG
✅ Clear boundaries: real IO (fetch, fs) stays out
What it does NOT do
- ❌ Simulate Node.js or browser
- ❌ Automatic global monkey-patching
- ❌ fetch, fs, network IO
- ❌ Cryptographic determinism
- ❌ UI/DOM
- ❌ Plugins
Testing
npm test # Run tests
npm test -- --watch # Watch mode
npm run build # Compile TypeScript
npm run dev # Watch compilationCurrent coverage:
Future work
- Advanced trace replay with time warping
- Capture/replay of promises
- Statistical determinism testing
- Integration with testing frameworks
Support
If warpclock helps you, you can support its development:
- GitHub Sponsors: https://github.com/sponsors/ne07hekin6
- Ko-fi: https://ko-fi.com/ne07hekin6
License
MIT
Author: Leandro Gaurisse
Philosophy
"This project does not simulate Node or browser. It defines a deterministic micro-runtime where time is a data value, not an external variable."
The goal is to provide a simple and powerful way to:
- Reproduce bugs exactly
- Test async without flakiness
- Simulate time intervals without waiting
- Guarantee deterministic execution order
All within an explicit and controlled scope.
