Files
2026-06-18 22:21:38 -04:00

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
outbox
event-driven
tdd
drain-trigger
caldav
requires provides affects
signalOutboxDrain (apps/api/src/lib/outboxTrigger.ts)
onOutboxDrain (apps/api/src/lib/outboxTrigger.ts)
scheduleOutboxDrain (apps/api/src/broker/outboxWorker.ts)
drainRequested (apps/api/src/broker/outboxWorker.ts)
initOutboxTrigger (apps/api/src/broker/outboxWorker.ts)
apps/api/src/broker/outboxWorker.ts
apps/api/tests/broker/outboxWorker.test.ts
added patterns
node:events EventEmitter (outboxTrigger.ts — zero external dependency)
Module-level EventEmitter singleton (same as listEmitter.ts)
scheduleOutboxDrain isDraining guard + drainRequested trailing-re-drain loop (D-05)
TDD RED/GREEN via test(09-01)/feat(09-01) commits
created modified
apps/api/src/lib/outboxTrigger.ts
apps/api/src/broker/outboxWorker.ts
apps/api/tests/broker/outboxWorker.test.ts
D-TDD-INIT: initOutboxTrigger() called in beforeAll (not beforeEach) for the trigger-wiring describe block — avoids listener accumulation while still wiring the EventEmitter → scheduleOutboxDrain path for signal-driven tests (SC-1, D-05)
D-TEST-C-MOCK: Test C (D-07) uses mockImplementationOnce to return the row on the first pending-rows query and empty on subsequent queries — correctly simulates drain 1 processing the row so the trailing re-drain (triggered by drainRequested) finds 0 rows and calls createCalendarEvent exactly once
duration_seconds completed_date tasks_completed files_changed
341 2026-06-12 3 3

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(): voidemitter.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 setMaxListeners call — 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 invoke createCalendarEvent exactly 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:

  1. import { onOutboxDrain } from '../lib/outboxTrigger.js'
  2. let drainRequested = false — trailing-re-drain flag (D-05), immediately after isDraining
  3. export function scheduleOutboxDrain(): void — checks isDraining; if true sets drainRequested = true and returns; otherwise calls runOutboxDrain() with .catch (D-02) and .finally that resets drainRequested = false BEFORE any recursive scheduleOutboxDrain() call (Pitfall 3 / T-09-01)
  4. export function initOutboxTrigger(): void — calls onOutboxDrain(() => scheduleOutboxDrain())
  5. startOutboxWorker setInterval body changed from runOutboxDrain().catch(...) to bare scheduleOutboxDrain() — 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 createCalendarEvent called exactly once, but the trailing re-drain triggered by drainRequested also executed against the same always-returning mock, calling createCalendarEvent twice.
  • 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 without initOutboxTrigger() registering the listener, the signal went nowhere.
  • Fix: Added beforeAll(() => { initOutboxTrigger(); }) to the new describe block; also imported initOutboxTrigger and changed import { beforeEach } to include beforeAll.
  • Files modified: apps/api/tests/broker/outboxWorker.test.ts
  • Commit: 2b11304

TDD Gate Compliance

  • RED commit: bcde073test(09-01): add failing trigger-wiring tests for SC-1, D-05, D-07
  • GREEN commit: 2b11304feat(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 --noEmit clean

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 — FOUND
  • apps/api/src/broker/outboxWorker.ts — verified: scheduleOutboxDrain, initOutboxTrigger, drainRequested present
  • Task 1 commit 1e12d70 — FOUND
  • Task 2 commit bcde073 — FOUND
  • Task 3 commit 2b11304 — FOUND