`shutdown()` set `accepting = false` and `start()` refused for ever after, so a queue was single-use. Any process that boots more than one app broke: a test suite closing one harness and opening the next, a hot reload, a multi-tenant host. The failure landed far from its cause -- the SECOND app to boot threw WRN-QUEUE-CLOSED out of the dev server because an unrelated one had shut down earlier in the same process. That is what turned six example-app security tests red only when run alongside the rest of the suite. Starting is an explicit intent to run, so it reopens the queue. `add()` keeps its guard, so work offered to a queue that is shutting down is still refused. Also migrates the example app's welcome-email queue to `defineQueue`, which the new loader requires. It still used `defineJob`, so the loader refused it and took the whole example app down -- 14 failures from one unmigrated file. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
95 lines
3.6 KiB
TypeScript
95 lines
3.6 KiB
TypeScript
import { expect, test } from "bun:test";
|
|
import { createDurableQueue, defineQueue, memoryQueueStore } from "../src/index.ts";
|
|
|
|
test("defineQueue creates typed producers and registers workers", async () => {
|
|
const seen: number[] = [];
|
|
const completed: number[] = [];
|
|
const email = defineQueue({
|
|
name: "email",
|
|
options: { store: memoryQueueStore() },
|
|
jobs: {
|
|
send: {
|
|
maxAttempts: 4,
|
|
idempotency: (data: { messageId: number }) => `message:${data.messageId}`,
|
|
validate: (data: unknown): data is { messageId: number } =>
|
|
typeof data === "object" &&
|
|
data !== null &&
|
|
Number.isInteger((data as { messageId?: unknown }).messageId),
|
|
run: async ({ messageId }: { messageId: number }) => void seen.push(messageId),
|
|
success: async ({ messageId }: { messageId: number }) => void completed.push(messageId),
|
|
},
|
|
},
|
|
});
|
|
|
|
const first = await email.send.add({ messageId: 7 });
|
|
const duplicate = await email.jobs.send.add({ messageId: 7 });
|
|
expect(duplicate.id).toBe(first.id);
|
|
expect(await email.send.status(first.id)).toBe("queued");
|
|
expect(await email.drain()).toBe(1);
|
|
expect(seen).toEqual([7]);
|
|
expect(completed).toEqual([7]);
|
|
expect(await email.send.status(first.id)).toBe("completed");
|
|
});
|
|
|
|
test("durable queues expose lifecycle, health and events", async () => {
|
|
const events: string[] = [];
|
|
const queue = createDurableQueue({
|
|
pollMs: 10,
|
|
onEvent: (event) => void events.push(event.type),
|
|
});
|
|
queue.process("work", async () => {});
|
|
queue.start();
|
|
expect(queue.isRunning()).toBe(true);
|
|
await queue.add("work", {});
|
|
await queue.drain();
|
|
const health = await queue.health();
|
|
expect(health.pending).toBe(0);
|
|
expect(events).toContain("job.added");
|
|
expect(events).toContain("job.completed");
|
|
queue.stop();
|
|
expect(queue.isRunning()).toBe(false);
|
|
await queue.shutdown();
|
|
});
|
|
|
|
test("defineQueue validates payloads before persistence", async () => {
|
|
const queue = defineQueue({
|
|
name: "safe",
|
|
jobs: {
|
|
number: {
|
|
validate: (data: unknown): data is number => typeof data === "number",
|
|
run: async (_data: number) => {},
|
|
},
|
|
},
|
|
});
|
|
await expect(queue.number.add("no" as never)).rejects.toThrow("WRN-QUEUE-PAYLOAD");
|
|
});
|
|
|
|
// `shutdown()` used to close a queue permanently: it set `accepting = false`
|
|
// and `start()` refused for ever after. That made a queue single-use, which
|
|
// breaks any process that boots more than one app -- a test suite closing one
|
|
// harness and opening the next, a hot reload, a multi-tenant host. The failure
|
|
// was remote from its cause: the SECOND app to boot threw WRN-QUEUE-CLOSED out
|
|
// of the dev server, so a perfectly good app failed because an unrelated one
|
|
// had shut down earlier in the same process.
|
|
test("a queue that was shut down can be started again", async () => {
|
|
const { createDurableQueue, memoryQueueStore } = await import("../src/index.ts");
|
|
const queue = createDurableQueue({ pollMs: 5, store: memoryQueueStore() });
|
|
|
|
queue.start();
|
|
await queue.shutdown();
|
|
expect(queue.health ? (await queue.health()).accepting : false).toBe(false);
|
|
|
|
// The explicit intent to run again reopens it.
|
|
queue.start();
|
|
expect((await queue.health()).accepting).toBe(true);
|
|
|
|
// And it genuinely works, rather than merely reporting that it does.
|
|
const done: string[] = [];
|
|
queue.process("work", async (job: { data: { id: string } }) => void done.push(job.data.id));
|
|
await queue.add("work", { id: "after-restart" });
|
|
await queue.drain();
|
|
expect(done).toEqual(["after-restart"]);
|
|
|
|
await queue.shutdown();
|
|
});
|