docs(09-01): complete outbox drain trigger wiring plan
This commit is contained in:
@@ -0,0 +1,125 @@
|
||||
---
|
||||
phase: 09-faster-write-back
|
||||
plan: "01"
|
||||
subsystem: api-broker
|
||||
tags: [outbox, event-driven, tdd, drain-trigger, caldav]
|
||||
dependency_graph:
|
||||
requires: []
|
||||
provides:
|
||||
- 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)
|
||||
affects:
|
||||
- apps/api/src/broker/outboxWorker.ts
|
||||
- apps/api/tests/broker/outboxWorker.test.ts
|
||||
tech_stack:
|
||||
added:
|
||||
- node:events EventEmitter (outboxTrigger.ts — zero external dependency)
|
||||
patterns:
|
||||
- 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
|
||||
key_files:
|
||||
created:
|
||||
- apps/api/src/lib/outboxTrigger.ts
|
||||
modified:
|
||||
- apps/api/src/broker/outboxWorker.ts
|
||||
- apps/api/tests/broker/outboxWorker.test.ts
|
||||
decisions:
|
||||
- "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"
|
||||
metrics:
|
||||
duration_seconds: 341
|
||||
completed_date: "2026-06-12"
|
||||
tasks_completed: 3
|
||||
files_changed: 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(): 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 `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: `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 --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
|
||||
Reference in New Issue
Block a user