Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
6.7 KiB
phase, plan, subsystem, tags, dependency_graph, tech_stack, key_files, decisions, metrics
| phase | plan | subsystem | tags | dependency_graph | tech_stack | key_files | decisions | metrics | |||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 09-faster-write-back | 01 | api-broker |
|
|
|
|
|
|
Phase 09 Plan 01: Outbox Drain Trigger Wiring Summary
One-liner: Zero-dependency in-process EventEmitter drain signal (outboxTrigger.ts) + scheduleOutboxDrain wrapper with drainRequested trailing-re-drain loop wired to outboxWorker.ts, eliminating the up-to-15s polling delay on enqueue.
What Was Built
Task 1: apps/api/src/lib/outboxTrigger.ts (new file)
Module-level EventEmitter singleton following the listEmitter.ts analog:
signalOutboxDrain(): void—emitter.emit('drain'), fire-and-forget (D-04)onOutboxDrain(handler: () => void): () => void— registers listener, returns unsubscribe- Only imports
node:events; no internal dependencies (zero circular-import risk) - No
setMaxListenerscall — single subscriber, default limit of 10 is correct
Task 2: RED test block in apps/api/tests/broker/outboxWorker.test.ts
Added describe('scheduleOutboxDrain — trigger wiring (D-09)', ...) with 3 failing tests:
- Test A (SC-1):
signalOutboxDrain()triggers drain promptly without timer advance - Test B (D-05): two mid-drain signals collapse to exactly one trailing re-drain
- Test C (D-07): two concurrent
scheduleOutboxDrain()calls invokecreateCalendarEventexactly once
Tests failed RED because scheduleOutboxDrain and initOutboxTrigger were not yet exported.
Task 3: GREEN implementation in apps/api/src/broker/outboxWorker.ts
Four additions to outboxWorker.ts:
import { onOutboxDrain } from '../lib/outboxTrigger.js'let drainRequested = false— trailing-re-drain flag (D-05), immediately afterisDrainingexport function scheduleOutboxDrain(): void— checksisDraining; if true setsdrainRequested = trueand returns; otherwise callsrunOutboxDrain()with.catch(D-02) and.finallythat resetsdrainRequested = falseBEFORE any recursivescheduleOutboxDrain()call (Pitfall 3 / T-09-01)export function initOutboxTrigger(): void— callsonOutboxDrain(() => scheduleOutboxDrain())startOutboxWorkersetInterval body changed fromrunOutboxDrain().catch(...)to barescheduleOutboxDrain()— 15s interval unchanged (D-08)
runOutboxDrain's body, if (isDraining) return;, isDraining = true;, and finally { isDraining = false; } are byte-for-byte unchanged.
Deviations from Plan
Auto-fixed Issues
1. [Rule 1 - Test Design] Test C mock required mockImplementationOnce to correctly simulate drain-1 row consumption
- Found during: Task 3 GREEN
- Issue: Test C expects
createCalendarEventcalled exactly once, but the trailing re-drain triggered bydrainRequestedalso executed against the same always-returning mock, callingcreateCalendarEventtwice. - Fix: Used
mockWherePending.mockImplementationOnce(() => Promise.resolve([row])).mockImplementation(() => Promise.resolve([]))so drain 1 gets the row and the trailing drain finds empty. - Files modified:
apps/api/tests/broker/outboxWorker.test.ts - Commit:
2b11304
2. [Rule 2 - Missing test wiring] beforeAll(initOutboxTrigger) required to wire EventEmitter listener for signal-driven tests
- Found during: Task 3 GREEN (Tests A and B failed because no listener was registered)
- Issue: Tests A and B call
signalOutboxDrain()but withoutinitOutboxTrigger()registering the listener, the signal went nowhere. - Fix: Added
beforeAll(() => { initOutboxTrigger(); })to the new describe block; also importedinitOutboxTriggerand changedimport { beforeEach }to includebeforeAll. - Files modified:
apps/api/tests/broker/outboxWorker.test.ts - Commit:
2b11304
TDD Gate Compliance
- RED commit:
bcde073—test(09-01): add failing trigger-wiring tests for SC-1, D-05, D-07 - GREEN commit:
2b11304—feat(09-01): add scheduleOutboxDrain, drainRequested, initOutboxTrigger; route setInterval through wrapper - RED gate: 3 new tests failing (27 pre-existing passing)
- GREEN gate: 30/30 tests passing,
tsc --noEmitclean
Verification Evidence
npx vitest run tests/broker/outboxWorker.test.ts
Test Files 1 passed (1)
Tests 30 passed (30)
npx tsc --noEmit → (no output, clean)
Known Stubs
None — all symbols produce correct runtime behavior. signalOutboxDrain is not yet wired to the enqueue path (Plan 02 adds it to events.ts); initOutboxTrigger is not yet called at startup (Plan 02 adds it to index.ts). These are intentional plan boundaries, not stubs.
Threat Flags
None — this plan introduces no new network endpoints, auth paths, file access patterns, or schema changes. The in-process EventEmitter boundary carries no payload and no user input crosses it. STRIDE mitigations T-09-01 through T-09-04 are implemented and verified by the trigger-wiring tests.
Self-Check: PASSED
apps/api/src/lib/outboxTrigger.ts— FOUNDapps/api/src/broker/outboxWorker.ts— verified:scheduleOutboxDrain,initOutboxTrigger,drainRequestedpresent- Task 1 commit
1e12d70— FOUND - Task 2 commit
bcde073— FOUND - Task 3 commit
2b11304— FOUND