From e1ffddf8dcf8ccc03682136567b20fd28a8144e6 Mon Sep 17 00:00:00 2001 From: Lucas Berger Date: Fri, 12 Jun 2026 20:51:32 -0400 Subject: [PATCH] fix(09): WR-02 make initOutboxTrigger idempotent and retain the unsubscribe handle via stopOutboxTrigger --- apps/api/src/broker/outboxWorker.ts | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/apps/api/src/broker/outboxWorker.ts b/apps/api/src/broker/outboxWorker.ts index 09540aa..d326c64 100644 --- a/apps/api/src/broker/outboxWorker.ts +++ b/apps/api/src/broker/outboxWorker.ts @@ -850,9 +850,27 @@ export async function runOutboxDrain(): Promise { * T-09-02: initOutboxTrigger is called only under isMainModule() in index.ts; * tests import runOutboxDrain/scheduleOutboxDrain directly and never register * a listener — no open-handle leak preventing process exit. + * + * WR-02: idempotent. The unsubscribe handle is retained so a duplicate call is a + * no-op (never double-registers the 'drain' listener) and stopOutboxTrigger() can + * remove it for a clean teardown (tests, graceful shutdown). */ +let unsubscribeDrain: (() => void) | null = null; + export function initOutboxTrigger(): void { - onOutboxDrain(() => scheduleOutboxDrain()); + if (unsubscribeDrain) return; // idempotent — never double-register + unsubscribeDrain = onOutboxDrain(() => scheduleOutboxDrain()); +} + +/** + * Remove the 'drain' listener registered by initOutboxTrigger() and reset the + * idempotency guard so a later initOutboxTrigger() can re-register cleanly. + * Used by the test suite's afterAll teardown (WR-03) and available for graceful + * shutdown. + */ +export function stopOutboxTrigger(): void { + unsubscribeDrain?.(); + unsubscribeDrain = null; } /**