feat(09-01): create outboxTrigger.ts zero-dependency EventEmitter signal module

- Module-level singleton EventEmitter, single subscriber, no setMaxListeners
- Export signalOutboxDrain(): void — fire-and-forget drain signal (D-04)
- Export onOutboxDrain(handler): () => void — register/unsubscribe listener
- Only imports node:events; zero internal dependencies (no circular import risk)
This commit is contained in:
Lucas Berger
2026-06-12 16:47:39 -04:00
parent a78c7241d6
commit 1e12d702a1
+39
View File
@@ -0,0 +1,39 @@
/**
* In-process outbox drain signal (D-01 / D-03 / D-04).
*
* Module-level singleton EventEmitter — one emitter shared across all callers
* in this Node.js process. Carries a zero-payload 'drain' event: signal is
* fire-and-forget; no data crosses this boundary (D-04).
*
* Single-subscriber design:
* - signalOutboxDrain() is called only after a successful enqueue DB commit (D-01/D-03).
* - The sole listener is registered by initOutboxTrigger() in outboxWorker.ts.
* - With exactly one subscriber the default EventEmitter limit of 10 is correct.
* - Do NOT call setMaxListeners — unlike listEmitter.ts (T-04-04, 200 SSE fan-out),
* this emitter never fans out to multiple listeners.
*
* Exports: signalOutboxDrain, onOutboxDrain
*/
import { EventEmitter } from 'node:events';
// Module-level singleton — one emitter shared across all route handlers
// in this Node.js process.
const emitter = new EventEmitter();
/**
* Fire a drain signal after a successful outbox enqueue commit.
* Synchronous and fire-and-forget — never awaited (D-04).
*/
export function signalOutboxDrain(): void {
emitter.emit('drain');
}
/**
* Register a handler to be invoked on each drain signal.
* Returns an unsubscribe function; call it to stop delivery to this handler.
*/
export function onOutboxDrain(handler: () => void): () => void {
emitter.on('drain', handler);
return () => emitter.off('drain', handler);
}