import { describe, it, expect } from "vitest"; import { PerKeyMutex } from "./per-key-mutex.js"; /** Tiny clock-free helper: returns a Promise that resolves after * `n` microtasks. Lets us check ordering without real timers. */ function tickN(n: number): Promise { let p: Promise = Promise.resolve(); for (let i = 0; i < n; i++) p = p.then(); return p; } describe("PerKeyMutex", () => { it("allows a single call against one key to run immediately", async () => { const m = new PerKeyMutex(); const result = await m.run("k1", async () => 42); expect(result).toBe(42); }); it("serialises two calls against the same key", async () => { const m = new PerKeyMutex(); const order: string[] = []; const a = m.run("k1", async () => { order.push("a-start"); await tickN(5); order.push("a-end"); }); const b = m.run("k1", async () => { order.push("b-start"); order.push("b-end"); }); await Promise.all([a, b]); expect(order).toEqual(["a-start", "a-end", "b-start", "b-end"]); }); it("runs different keys in parallel", async () => { const m = new PerKeyMutex(); const order: string[] = []; const a = m.run("k1", async () => { order.push("a-start"); await tickN(5); order.push("a-end"); }); const b = m.run("k2", async () => { order.push("b-start"); order.push("b-end"); }); await Promise.all([a, b]); expect(order[0]).toBe("a-start"); expect(order).toContain("b-start"); expect(order).toContain("b-end"); // b's pair lands before a's end (they run in parallel). expect(order.indexOf("b-end")).toBeLessThan(order.indexOf("a-end")); }); it("releases the lock when the handler throws", async () => { const m = new PerKeyMutex(); await expect( m.run("k1", async () => { throw new Error("boom"); }), ).rejects.toThrow("boom"); const result = await m.run("k1", async () => "after"); expect(result).toBe("after"); }); it("forwards the resolved value of the handler", async () => { const m = new PerKeyMutex(); const out = await m.run("k1", async () => ({ ok: true, n: 7 })); expect(out).toEqual({ ok: true, n: 7 }); }); it("cleans up internal state for keys with no waiters", async () => { const m = new PerKeyMutex(); await m.run("k1", async () => {}); expect(m.activeKeyCount()).toBe(0); }); it("retains a key while a chain is in flight, then drops it", async () => { const m = new PerKeyMutex(); let release!: () => void; const gate = new Promise((r) => (release = r)); const inFlight = m.run("k1", () => gate); expect(m.activeKeyCount()).toBe(1); release(); await inFlight; expect(m.activeKeyCount()).toBe(0); }); });