Files
suaveplan/packages/foundation/timing/cron/tests/integration.test.ts
2026-05-27 23:47:53 +01:00

129 lines
3.9 KiB
TypeScript

import { describe, expect, it } from "@suaveplan/testing/runner";
import { CronScheduler } from "../src/index.js";
import type { SchedulerTimers } from "../src/scheduler/cron-scheduler.types.js";
interface PendingTimer {
id: number;
cb: () => void;
fireAt: number;
}
function createHarness(start: number) {
const state = { now: start, nextId: 1 };
const pending = new Map<number, PendingTimer>();
const timers: SchedulerTimers = {
setTimer: (cb, ms) => {
const id = state.nextId++;
pending.set(id, { id, cb, fireAt: state.now + ms });
return id;
},
clearTimer: (id) => pending.delete(id),
};
async function advance(delta: number): Promise<void> {
const target = state.now + delta;
while (true) {
let earliest: PendingTimer | null = null;
for (const t of pending.values()) {
if (t.fireAt > target) continue;
if (earliest === null || t.fireAt < earliest.fireAt)
earliest = t;
}
if (earliest === null) break;
state.now = earliest.fireAt;
pending.delete(earliest.id);
earliest.cb();
// Let micro-task queue drain so the handler's finally re-schedules
// before we look for the next due timer.
await Promise.resolve();
await Promise.resolve();
await Promise.resolve();
}
state.now = target;
}
return {
clock: () => state.now,
timers,
advance,
get pending() {
return pending;
},
};
}
describe("integration — CronScheduler with multiple jobs", () => {
it("fires three jobs at their independent intervals", {
timeout: 30000,
}, async () => {
const harness = createHarness(Date.UTC(2026, 4, 11, 9, 59, 0));
const fires = { hourly: 0, fiveMin: 0, daily: 0 };
const scheduler = new CronScheduler({
clock: harness.clock,
timers: harness.timers,
});
scheduler.add(
"0 * * * *",
() => {
fires.hourly += 1;
},
{ id: "hourly" }
);
scheduler.add(
"*/5 * * * *",
() => {
fires.fiveMin += 1;
},
{ id: "five" }
);
scheduler.add(
"0 0 * * *",
() => {
fires.daily += 1;
},
{ id: "daily" }
);
scheduler.start();
// Advance 2 hours and 30 minutes.
await harness.advance(2 * 60 * 60 * 1000 + 30 * 60 * 1000);
expect(fires.hourly).toBeGreaterThanOrEqual(2);
expect(fires.fiveMin).toBeGreaterThanOrEqual(20);
await scheduler.stop();
});
it("stop() drains in-flight handler", { timeout: 30000 }, async () => {
const harness = createHarness(Date.UTC(2026, 4, 11, 9, 59, 0));
let resolveHandler!: () => void;
const holding = new Promise<void>((r) => {
resolveHandler = r;
});
let completed = false;
const scheduler = new CronScheduler({
clock: harness.clock,
timers: harness.timers,
});
scheduler.add("0 * * * *", async () => {
await holding;
completed = true;
});
scheduler.start();
await harness.advance(2 * 60 * 1000);
const stopPromise = scheduler.stop();
let stopResolved = false;
const watcher = stopPromise.then(() => {
stopResolved = true;
});
// Give the runtime a few microtasks to let the watcher attach.
await Promise.resolve();
await Promise.resolve();
expect(stopResolved).toBe(false);
resolveHandler();
await watcher;
expect(completed).toBe(true);
expect(stopResolved).toBe(true);
});
});