129 lines
3.9 KiB
TypeScript
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);
|
|
});
|
|
});
|