Event-driven outbox drain so created/edited/deleted events reach Fastmail in ~1–2s instead of waiting up to ~15s for the next interval tick — every existing durability guarantee preserved.
apps/api/src/broker/outboxWorker.ts — scheduleOutboxDrain() wrapper with the existing isDraining guard + a drainRequested trailing-re-drain loop (reset before the recursive call); initOutboxTrigger() subscriber; the 15s setInterval body now routes through the wrapper. runOutboxDrain() internals byte-for-byte unchanged.
apps/api/src/routes/events.ts — four fire-and-forget signalOutboxDrain() publish sites, one after each enqueue commit (create, edit-as-move after the transaction resolves, same-calendar update, delete).
apps/api/src/index.ts — initOutboxTrigger() wired under isMainModule() after startOutboxWorker() (never registered in the test process).
Verification
Phase verification: 5/5 success criteria passed (drain-on-enqueue, optimistic-202 / no inline CalDAV, create-before-delete on moves, exactly-once per uid, 15s fallback intact).
TDD: RED → GREEN for the trigger-wiring suite (SC-1 / D-05 / D-07); 30/30 broker tests, 23/23 events-route tests; tsc --noEmit clean; full api suite green (modulo one pre-existing, unrelated flaky lists-share timeout).
Code review: 0 blockers, 4 advisory warnings (robustness/teardown), 3 info — see 09-REVIEW.md.
## Phase 9: Faster Write-Back (CAL-15)
Event-driven outbox drain so created/edited/deleted events reach Fastmail in ~1–2s instead of waiting up to ~15s for the next interval tick — every existing durability guarantee preserved.
### What changed
- **`apps/api/src/lib/outboxTrigger.ts`** (new) — zero-dependency in-process `node:events` EventEmitter: `signalOutboxDrain()` / `onOutboxDrain()`. Single subscriber, fire-and-forget.
- **`apps/api/src/broker/outboxWorker.ts`** — `scheduleOutboxDrain()` wrapper with the existing `isDraining` guard + a `drainRequested` trailing-re-drain loop (reset before the recursive call); `initOutboxTrigger()` subscriber; the 15s `setInterval` body now routes through the wrapper. `runOutboxDrain()` internals byte-for-byte unchanged.
- **`apps/api/src/routes/events.ts`** — four fire-and-forget `signalOutboxDrain()` publish sites, one after each enqueue commit (create, edit-as-move *after* the transaction resolves, same-calendar update, delete).
- **`apps/api/src/index.ts`** — `initOutboxTrigger()` wired under `isMainModule()` after `startOutboxWorker()` (never registered in the test process).
### Verification
- Phase verification: **5/5 success criteria passed** (drain-on-enqueue, optimistic-202 / no inline CalDAV, create-before-delete on moves, exactly-once per uid, 15s fallback intact).
- TDD: RED → GREEN for the trigger-wiring suite (SC-1 / D-05 / D-07); 30/30 broker tests, 23/23 events-route tests; `tsc --noEmit` clean; full api suite green (modulo one pre-existing, unrelated flaky lists-share timeout).
- Code review: 0 blockers, 4 advisory warnings (robustness/teardown), 3 info — see `09-REVIEW.md`.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
- Import scheduleOutboxDrain (not yet exported — causes RED)
- Import signalOutboxDrain from outboxTrigger.ts
- Add describe block 'scheduleOutboxDrain — trigger wiring (D-09)' with 3 tests:
Test A SC-1: signalOutboxDrain() fires drain promptly without timer advance
Test B D-05: two mid-drain signals collapse to exactly one trailing re-drain
Test C D-07: concurrent scheduleOutboxDrain() calls dispatch exactly once via isDraining guard
- 27 pre-existing tests unmodified and passing; 3 new tests failing (RED)
- import signalOutboxDrain from '../lib/outboxTrigger.js'
- Site 1 (POST /create): fire-and-forget signal after outbox insert, before 202
- Site 2 (PATCH edit-as-move): signal after await db.transaction() resolves, not inside callback (D-03)
- Site 3 (PATCH same-calendar update): signal after outbox insert, before 202
- Site 4 (DELETE): signal after outbox insert, before 202
- extend outboxWorker import to include initOutboxTrigger alongside startOutboxWorker
- call initOutboxTrigger() immediately after startOutboxWorker() inside isMainModule() guard
- gated by isMainModule() so tests importing app never register the drain listener (Pitfall 4)
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Phase 9: Faster Write-Back (CAL-15)
Event-driven outbox drain so created/edited/deleted events reach Fastmail in ~1–2s instead of waiting up to ~15s for the next interval tick — every existing durability guarantee preserved.
What changed
apps/api/src/lib/outboxTrigger.ts(new) — zero-dependency in-processnode:eventsEventEmitter:signalOutboxDrain()/onOutboxDrain(). Single subscriber, fire-and-forget.apps/api/src/broker/outboxWorker.ts—scheduleOutboxDrain()wrapper with the existingisDrainingguard + adrainRequestedtrailing-re-drain loop (reset before the recursive call);initOutboxTrigger()subscriber; the 15ssetIntervalbody now routes through the wrapper.runOutboxDrain()internals byte-for-byte unchanged.apps/api/src/routes/events.ts— four fire-and-forgetsignalOutboxDrain()publish sites, one after each enqueue commit (create, edit-as-move after the transaction resolves, same-calendar update, delete).apps/api/src/index.ts—initOutboxTrigger()wired underisMainModule()afterstartOutboxWorker()(never registered in the test process).Verification
tsc --noEmitclean; full api suite green (modulo one pre-existing, unrelated flaky lists-share timeout).09-REVIEW.md.🤖 Generated with Claude Code
- Add import { onOutboxDrain } from outboxTrigger.js - Add let drainRequested = false (D-05 trailing-re-drain flag) - Export scheduleOutboxDrain(): void — isDraining guard + drainRequested loop (D-05/T-09-01) drainRequested=false reset precedes recursive call (Pitfall 3) errors caught via .catch to prevent crash (D-02/T-09-03) - Export initOutboxTrigger(): void — registers onOutboxDrain(() => scheduleOutboxDrain()) - startOutboxWorker setInterval body: scheduleOutboxDrain() replaces runOutboxDrain().catch() 15 * 1000 interval unchanged (D-08) - runOutboxDrain body/isDraining guard/finally unchanged (D-02/D-07) - Fix trigger-wiring tests: add beforeAll(initOutboxTrigger) to wire EventEmitter listener; fix Test C mock to return empty rows on trailing drain (correct D-07 behaviour) - 30/30 outboxWorker tests GREEN; tsc --noEmit clean