fix(queue): let a shut-down queue be started again

`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>
This commit is contained in:
2026-08-23 11:36:44 +05:30
co-authored by Claude Opus 5
parent 64ab20cc95
commit d87b197224
3 changed files with 60 additions and 5 deletions
+29
View File
@@ -63,3 +63,32 @@ test("defineQueue validates payloads before persistence", async () => {
});
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();
});