Bloque H3 — official createFakeTimerClock() exported from \$timer

The `engine-timers.test.ts` file already had a `createFakeClock` for
internal use, but every artifact under test that accepts a `clock`
injection (storage, format, logger, http, session auto-refresh, …)
was rolling its own. Promoting the helper to the public surface gives
the ecosystem a single source of deterministic time for tests.

`createFakeTimerClock(start = 0): FakeTimerClock` exposes
`advanceBy(ms)`, `advanceTo(ms)` and `pendingCount()` on top of the
shared `TimerClock` shape. Microtask flushes between fired entries
keep awaited promises inside scheduled callbacks resolved before time
moves on.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
master
dev 5 months ago
parent 5152b23c6b
commit 0c15e0b94c

@ -0,0 +1,100 @@
import type { TimerClock, TimerNativeHandle } from '$libs/timer';
interface FakeClockEntry {
id: number;
dueAt: number;
fn: () => void;
cancelled: boolean;
}
/**
* Test-only TimerClock with manual time advancement. Same shape that
* `createEngineTimers({ clock })` and the format/storage `clock`
* options accept, so any artifact under test can be wired against this
* deterministic source without touching `vi.useFakeTimers()`.
*
* Methods:
* - `advanceBy(ms)` — fires every entry whose `dueAt` is reached during
* the advancement, in due-order, with microtask flushes between each
* so awaited promises inside callbacks resolve before time moves on.
* - `advanceTo(ms)` — convenience over `advanceBy`.
* - `pendingCount()` — for tests that want to assert teardown is clean.
*/
export interface FakeTimerClock extends TimerClock {
advanceBy(ms: number): Promise<void>;
advanceTo(ms: number): Promise<void>;
pendingCount(): number;
}
/**
* Build a fake `TimerClock` with manual time advancement. Use in tests
* for any artifact that accepts a `clock` injection (storage, format,
* logger, http, session auto-refresh, …).
*
* @example
* const clock = createFakeTimerClock(0);
* const timers = createEngineTimers({ clock });
* timers.schedule('demo', 100, () => fired = true);
* await clock.advanceBy(150);
* expect(fired).toBe(true);
*/
export function createFakeTimerClock(start = 0): FakeTimerClock {
let now = start;
const queue = new Map<number, FakeClockEntry>();
let nextId = 1;
function fire(entry: FakeClockEntry): void {
if (entry.cancelled) return;
queue.delete(entry.id);
entry.fn();
}
async function flushMicrotasks(): Promise<void> {
// Yield to the microtask queue so any awaited promises inside
// scheduled callbacks resolve before we advance further.
for (let i = 0; i < 10; i++) await Promise.resolve();
}
const clock: FakeTimerClock = {
now: () => now,
setTimeout: (fn: () => void, delayMs: number): TimerNativeHandle => {
const entry: FakeClockEntry = {
id: nextId++,
dueAt: now + delayMs,
fn,
cancelled: false
};
queue.set(entry.id, entry);
return entry.id;
},
clearTimeout: (handle: TimerNativeHandle): void => {
const entry = queue.get(handle as number);
if (entry !== undefined) {
entry.cancelled = true;
queue.delete(entry.id);
}
},
async advanceBy(ms: number): Promise<void> {
const target = now + ms;
while (true) {
let next: FakeClockEntry | undefined;
for (const e of queue.values()) {
if (e.dueAt > target) continue;
if (next === undefined || e.dueAt < next.dueAt) next = e;
}
if (next === undefined) break;
now = next.dueAt;
fire(next);
await flushMicrotasks();
}
now = target;
await flushMicrotasks();
},
async advanceTo(target: number): Promise<void> {
if (target <= now) return;
await clock.advanceBy(target - now);
},
pendingCount: () => queue.size
};
return clock;
}

@ -5,6 +5,7 @@
export { createEngineTimers } from './engine-timers.ts';
export { createActiveTimers, type ActiveTimers } from './active-timers.svelte.ts';
export { createSystemTimerClock } from './clock.ts';
export { createFakeTimerClock, type FakeTimerClock } from './fake-clock.ts';
export { computeBackoffDelay } from './backoff.ts';
export { createTimerDiagnostics, emitTimerDiagnostic } from './diagnostics.ts';

@ -0,0 +1,45 @@
import { describe, expect, it } from 'vitest';
import { createFakeTimerClock } from '../fake-clock.ts';
describe('createFakeTimerClock', () => {
it('starts at the configured time and advances on demand', async () => {
const clock = createFakeTimerClock(1000);
expect(clock.now()).toBe(1000);
await clock.advanceBy(500);
expect(clock.now()).toBe(1500);
await clock.advanceTo(2000);
expect(clock.now()).toBe(2000);
});
it('fires scheduled timeouts in due-order on advanceBy', async () => {
const clock = createFakeTimerClock(0);
const events: string[] = [];
clock.setTimeout(() => events.push('B-200'), 200);
clock.setTimeout(() => events.push('A-100'), 100);
clock.setTimeout(() => events.push('C-300'), 300);
await clock.advanceBy(250);
expect(events).toEqual(['A-100', 'B-200']);
await clock.advanceBy(100);
expect(events).toEqual(['A-100', 'B-200', 'C-300']);
expect(clock.pendingCount()).toBe(0);
});
it('cancels timeouts via clearTimeout', async () => {
const clock = createFakeTimerClock(0);
let fired = false;
const handle = clock.setTimeout(() => {
fired = true;
}, 100);
clock.clearTimeout(handle);
await clock.advanceBy(200);
expect(fired).toBe(false);
expect(clock.pendingCount()).toBe(0);
});
});
Loading…
Cancel
Save

Powered by TurnKey Linux.