Initial Commit
This commit is contained in:
128
packages/foundation/timing/cron/tests/integration.test.ts
Normal file
128
packages/foundation/timing/cron/tests/integration.test.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user