import { describe, expect, it, vi } from "vitest"; import { ActionSkippedError, LastQueue } from "./lastQueue"; async function testPromise(arg, _signal) { return Promise.resolve(arg); } const wait = (ms = 5) => new Promise((r) => setTimeout(r, ms)); describe("test last-queue", () => { it("should respect throttle period", async () => { vi.useFakeTimers(); const throttle = 50; const queue = new LastQueue(throttle); const timestamps = []; async function timedAction(arg) { timestamps.push(Date.now()); return arg; } queue.enqueue(timedAction, 1); queue.enqueue(timedAction, 2); queue.enqueue(timedAction, 3); vi.advanceTimersByTime(throttle * 3); await Promise.resolve(); expect(timestamps.length).toBeGreaterThanOrEqual(1); const deltas = timestamps.slice(1).map((t, i) => t - timestamps[i]); expect(deltas.every((d) => d === 0 || d >= throttle)).toBe(true); vi.useRealTimers(); }); it("should reject skipped promises when rejectSkipped is true", async () => { const queue = new LastQueue(0, true); let rejected = 0; const rejectAction = vi.fn((arg) => Promise.resolve(arg)); queue.enqueue(rejectAction, 1).catch((e) => { if (e instanceof ActionSkippedError) { rejected++; } }); queue.enqueue(rejectAction, 2).catch((e) => { if (e instanceof ActionSkippedError) { rejected++; } }); await queue.enqueue(rejectAction, 3); await wait(10); expect(rejected).toEqual(1); }); it("should cancel earlier queued actions per key", async () => { const queue = new LastQueue(0); const calls = []; async function recordAction(arg) { calls.push(arg); return arg; } queue.enqueue(recordAction, 1, "keyA"); queue.enqueue(recordAction, 2, "keyA"); queue.enqueue(recordAction, 3, "keyA"); await wait(10); expect(calls).toEqual([1, 3]); }); it("should isolate independent keys", async () => { const queue = new LastQueue(0); const results = []; queue.enqueue(testPromise, "a", "A").then((r) => results.push(r)); queue.enqueue(testPromise, "b", "B").then((r) => results.push(r)); queue.enqueue(testPromise, "c", "C").then((r) => results.push(r)); await wait(10); expect(results.sort()).toEqual(["a", "b", "c"]); }); it("should handle thrown errors without breaking queue", async () => { const queue = new LastQueue(0); const results = []; async function faultyAction(arg) { if (arg === 1) { throw new Error("TestError"); } return arg; } queue.enqueue(faultyAction, 1).catch((e) => results.push(e.message)); await queue.enqueue(faultyAction, 2).then((r) => results.push(r)); expect(results).toContain("TestError"); expect(results).toContain(2); }); it("should execute replacement if enqueued during completion", async () => { const queue = new LastQueue(0); const results = []; async function delayedAction(arg) { if (arg === 1) { setTimeout(() => queue.enqueue(delayedAction, 2), 0); } return arg; } await queue.enqueue(delayedAction, 1).then((r) => results.push(r)); await wait(10); expect(results).toContain(1); }); it("should not hang when throttle is zero or negative", async () => { const queue = new LastQueue(-1); const results = []; for (let i = 0; i < 5; i++) { await queue.enqueue(testPromise, i).then((r) => results.push(r)); } expect(results).toEqual([0, 1, 2, 3, 4]); }); it("should clean up idle keys after completion", async () => { const queue = new LastQueue(0); await queue.enqueue(testPromise, 1, "cleanupKey"); await wait(5); const second = await queue.enqueue(testPromise, 2, "cleanupKey"); expect(second).toBe(2); }); it("should resolve skipped promises to undefined when rejectSkipped is false", async () => { const queue = new LastQueue(0, false); const results = []; queue.enqueue(testPromise, 1).then((r) => results.push(r)); await queue.enqueue(testPromise, 2); await wait(5); const defined = results.filter((r) => r !== undefined); const skipped = results.filter((r) => r === undefined); // Either the first completed normally (defined=[1]) // or it was skipped and undefined (defined=[2]) expect(defined.length).toBe(1); expect([1, 2]).toContain(defined[0]); expect(skipped.length).toBeLessThanOrEqual(1); }); it("should not fail if a running task is replaced during execution", async () => { const queue = new LastQueue(0); let finished = false; async function longAction(arg, signal) { await new Promise((r) => setTimeout(r, 30)); if (!signal.aborted) { finished = true; } return arg; } queue.enqueue(longAction, 1, "key"); await wait(5); await queue.enqueue(longAction, 2, "key"); await wait(40); expect(finished).toBe(true); }); it("detects blocked throttle under fake timers", async () => { vi.useFakeTimers(); const q = new LastQueue(300); const calls = []; async function fn(n) { calls.push(n); return n; } q.enqueue(fn, 1); q.enqueue(fn, 2); q.enqueue(fn, 3); vi.advanceTimersByTime(1000); expect(calls.length).toBeGreaterThan(0); }); it("should not leak memory after many keys", async () => { const queue = new LastQueue(0); for (let i = 0; i < 1000; i++) { await queue.enqueue(testPromise, i, `key${i}`); } expect(queue["queues"].size).toBe(0); expect(queue["pending"].size).toBe(0); expect(queue["timeoutIds"].size).toBe(0); }); it("should throttle per key independently", async () => { vi.useFakeTimers(); const throttle = 50; const queue = new LastQueue(throttle); const stamps = { A: [], B: [] }; const action = vi.fn(async (key) => { stamps[key].push(Date.now()); return key; }); queue.enqueue(action, "A", "A"); queue.enqueue(action, "A", "A"); queue.enqueue(action, "B", "B"); queue.enqueue(action, "B", "B"); vi.advanceTimersByTime(throttle * 3); await Promise.resolve(); expect(stamps.A.length >= 2).toBe(true); expect(stamps.B.length >= 2).toBe(true); const deltaA = stamps.A[1] - stamps.A[0]; const deltaB = stamps.B[1] - stamps.B[0]; expect(deltaA >= throttle || deltaA === 0).toBe(true); expect(deltaB >= throttle || deltaB === 0).toBe(true); vi.useRealTimers(); }); it("should propagate AbortSignal and reject on abort", async () => { vi.useFakeTimers(); const queue = new LastQueue(0); const controller = new AbortController(); let aborted = false; const action = vi.fn(async (_arg, signal) => { signal?.addEventListener("abort", () => { aborted = true; }); if (signal?.aborted) { throw new Error("Aborted early"); } return "done"; }); const first = queue.enqueue(action, null, "key", { signal: controller.signal }); controller.abort(); vi.runAllTimers(); const result = await first; expect(result).toBeUndefined(); expect(aborted).toBe(true); expect(action).toHaveBeenCalledTimes(1); vi.useRealTimers(); }); it("should skip all but first and last in rapid zero-throttle burst", async () => { const queue = new LastQueue(0); const results = []; for (let i = 0; i < 1000; i++) { queue.enqueue(testPromise, i).then((r) => r !== undefined && results.push(r)); } await queue.enqueue(testPromise, 999).then((r) => r !== undefined && results.push(r)); await wait(10); expect(results).toEqual([0, 999]); }); it("should clear timeout on skip", async () => { vi.useFakeTimers(); const queue = new LastQueue(100); queue.enqueue(testPromise, 1); // starts, will wait 100ms before next await vi.runAllTimersAsync(); // finish first task queue.enqueue(testPromise, 2); // queues second, sets timeout queue.enqueue(testPromise, 3); // skips second → should clear timeout vi.runAllTimers(); // any leftover timeout would error expect(queue["timeoutIds"].size).toBe(0); vi.useRealTimers(); }); it("should handle mixed string/number keys without collision", async () => { const queue = new LastQueue(0); const results = new Set(); await queue.enqueue(testPromise, "str", "key"); await queue.enqueue(testPromise, 123, 123); queue.enqueue(testPromise, "num", "123").then((r) => results.add(r)); queue.enqueue(testPromise, 456, "key").then((r) => results.add(r)); await wait(10); expect(results.has("num")).toBe(true); expect(results.has(456)).toBe(true); }); });