Files
familysync/.planning/phases/09-faster-write-back/09-02-SUMMARY.md
T
Lucas Berger fff5789a5f docs(09-02): complete enqueue-site signal wiring plan
- 09-02-SUMMARY.md: 4 signalOutboxDrain() sites + initOutboxTrigger() startup wiring
- STATE.md: plan position advanced, metrics recorded
- ROADMAP.md: plan progress updated
- REQUIREMENTS.md: CAL-15 marked complete
2026-06-12 16:59:27 -04:00

4.9 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 02 api-routes-startup
outbox
event-driven
drain-signal
caldav
startup-wiring
requires provides affects
signalOutboxDrain (apps/api/src/lib/outboxTrigger.ts — Plan 01)
initOutboxTrigger (apps/api/src/broker/outboxWorker.ts — Plan 01)
Four post-commit signalOutboxDrain() publish sites in events.ts (create, edit-as-move, same-cal update, delete)
initOutboxTrigger() wired at startup in index.ts under isMainModule()
apps/api/src/routes/events.ts
apps/api/src/index.ts
added patterns
Fire-and-forget in-process EventEmitter signal after each outbox enqueue commit (D-04)
isMainModule() guard for startup-only listener registration (Pitfall 4)
edit-as-move signal fires after await db.transaction() resolves, not inside callback (D-03)
created modified
apps/api/src/routes/events.ts
apps/api/src/index.ts
D-03-signal-placement: signalOutboxDrain() for edit-as-move placed AFTER await db.transaction() resolves (line after the statement), never inside the async callback — guarantees DELETE+CREATE rows are durably committed before any drain observes them (SC-3 / Pitfall 1)
D-04-fire-and-forget: signalOutboxDrain() is never awaited — synchronous emit to in-process EventEmitter, cannot block the 202 response (SC-2 / T-09-07)
Pitfall4-guard: initOutboxTrigger() called only inside isMainModule() block — tests importing app never register the drain listener, no open handles in test process (T-09-06)
duration_seconds completed_date tasks_completed files_changed
240 2026-06-12 2 2

Phase 09 Plan 02: Enqueue-Site Signal Wiring Summary

One-liner: Four fire-and-forget signalOutboxDrain() publish sites added to events.ts (create, edit-as-move-after-transaction, same-cal update, delete) and initOutboxTrigger() wired at startup in index.ts under isMainModule(), connecting the Plan 01 EventEmitter mechanism to live writes and eliminating the up-to-15s polling delay.

What Was Built

Task 1: apps/api/src/routes/events.ts — Four publish sites

Import added: import { signalOutboxDrain } from '../lib/outboxTrigger.js';

Four signalOutboxDrain() calls inserted, each fire-and-forget (never awaited):

Site Route Placement
1 POST /create After await db.insert(calendarOutbox).values({...}), before return c.json({ uid }, 202)
2 PATCH /:uid/edit (edit-as-move) After await db.transaction(async (tx) => {...}) RESOLVES, before return c.json({ uid: newUid }, 202) — critical: NOT inside the callback
3 PATCH /:uid/edit (same-cal update) After await db.insert(calendarOutbox).values({ operation: 'update', ... }), before return c.json({ uid }, 202)
4 DELETE /:uid After await db.insert(calendarOutbox).values({ operation: 'delete', ... }), before return c.json({ uid }, 202)

No route status codes, response bodies, validation, or ownership checks were modified. No inline CalDAV calls added. No signal inside the db.transaction callback. No signal inside any catch block.

Task 2: apps/api/src/index.ts — Startup wiring

  • Import extended: import { startOutboxWorker, initOutboxTrigger } from './broker/outboxWorker.js';
  • initOutboxTrigger(); added immediately after startOutboxWorker(); inside the if (isMainModule()) block (line 139), with comment // subscribe drain signal listener (D-01)
  • isMainModule(), startBrokerPoller, startReminderScheduler, and serve(...) unchanged

Deviations from Plan

None — plan executed exactly as written.

Verification Evidence

grep -c "signalOutboxDrain()" apps/api/src/routes/events.ts → 4
grep -n "await signalOutboxDrain" apps/api/src/routes/events.ts → (none)
initOutboxTrigger() at line 139 — inside isMainModule() guard

npx tsc --noEmit → (no output, clean)

npx vitest run tests/routes/events.test.ts tests/broker/outboxWorker.test.ts
  Test Files  2 passed (2)
       Tests  53 passed (53)

Full suite: 240 passed, 1 failed (known pre-existing flaky timeout in lists.test.ts:
  "toggling isShared false→true re-populates list_shares for other members" — 5028ms,
  exceeds 5000ms global testTimeout; passes with --testTimeout=30000; unrelated to this plan)

Known Stubs

None.

Threat Flags

None — this plan introduces no new network endpoints, auth paths, file access patterns, or schema changes. T-09-05 (edit-as-move signal placement) and T-09-06 (listener registration gating) are both verified: signal is after the transaction resolves, and initOutboxTrigger is inside isMainModule().

Self-Check: PASSED

  • apps/api/src/routes/events.ts — FOUND; 4 signalOutboxDrain() calls verified
  • apps/api/src/index.ts — FOUND; initOutboxTrigger() inside isMainModule() at line 139 verified
  • Task 1 commit 30eff3d — FOUND
  • Task 2 commit 0ebdf48 — FOUND