Files
familysync/.planning/phases/04-shared-lists-live-sync/04-02-SUMMARY.md
T
Lucas Berger 9fb1e0da84 docs(04-02): complete scoped fan-out primitives plan
SUMMARY.md for 04-02: listEmitter + listAccess TDD plan.
RED/GREEN gate confirmed. D-04 isolation proven. ioredis not introduced.
2026-06-09 12:24:50 -04:00

6.1 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
04-shared-lists-live-sync 02 api-lib, test-harness
listEmitter
listAccess
scoped-fanout
D-04
tdd
eventEmitter
sse-primitive
requires provides affects
04-01 (lists/list_shares schema, test harness, vitest.config.ts)
publishListEvent(listId, event)
scoped in-process fan-out
subscribeListEvents(listId, handler)
per-list subscription returning unsub closure
ListEvent type union
getAccessibleListIds(userId)
owner OR list_shares access-scope query
fileParallelism:false vitest config (prevents DB test race conditions)
apps/api/tests/lib/listEmitter.test.ts (stubs replaced with real assertions)
apps/api/vitest.config.ts (fileParallelism:false added)
added patterns
Module-level EventEmitter singleton; per-list channel key list:${listId}
subscribeListEvents returns unsub closure (emitter.off)
Two-query union (owned + shared) with Set dedup for getAccessibleListIds
randomUUID() suffix in test seed helpers to avoid unique-key collisions
fileParallelism:false to serialize DB test file execution
created modified
apps/api/src/lib/listEmitter.ts
apps/api/src/lib/listAccess.ts
apps/api/tests/lib/listAccess.test.ts
apps/api/tests/lib/listEmitter.test.ts (it.todo stubs replaced with real assertions)
apps/api/vitest.config.ts (fileParallelism:false; sequence.concurrent:false)
D-04: In-memory EventEmitter per-list channel isolation confirmed by Test 2 (cross-list negative)
D-18: ioredis NOT introduced; abstraction boundary in listEmitter.ts makes future Redis swap mechanical
vitest fileParallelism:false: global afterEach in test/setup.ts truncates shared MariaDB state; parallel files caused FK violations mid-test
getAccessibleListIds: two-select + Set approach per RESEARCH Finding 3 (not single OR-join) — simpler, equally correct
listAccess.test.ts in tests/lib/ (not src/lib/) per tdd_note convention matching listEmitter placement
duration completed task_count file_count
~15 minutes 2026-06-09 3 5

Phase 4 Plan 2: Scoped Fan-out Primitives Summary

One-liner: In-memory per-list EventEmitter singleton (listEmitter.ts) + owner/shares access-scope query (listAccess.ts) with D-04 isolation proven by RED/GREEN TDD gate.

TDD Gate Compliance

Gate Commit Status
RED — failing tests 2d250af PASS — module-not-found; 6 tests failed as expected
GREEN — implementation 792efeb PASS — all 9 tests pass
REFACTOR (skipped) No refactoring needed — implementation was clean on first pass

Tasks Completed

Task Name Commit Files
RED Write failing listEmitter + listAccess tests 2d250af listEmitter.test.ts (stubs → assertions), listAccess.test.ts (new)
GREEN Implement listEmitter.ts + listAccess.ts 792efeb listEmitter.ts, listAccess.ts, listAccess.test.ts (UUID fix), vitest.config.ts
FIX fileParallelism:false to eliminate DB race condition 9e17853 vitest.config.ts

Deviations from Plan

Auto-fixed Issues

1. [Rule 1 - Bug] Test seed helper oidc_sub collisions across runs

  • Found during: GREEN phase — running both test files together
  • Issue: seedUser('owner-5') inserted sub-owner-5 on first run; on the second run (or when running tests without cleanup of the users table), the uniq_oidc_identity key fired ER_DUP_ENTRY.
  • Fix: Added randomUUID() suffix: oidcSub: sub-${label}-${randomUUID()} — unique per invocation regardless of table state.
  • Files modified: apps/api/tests/lib/listAccess.test.ts
  • Commit: 792efeb

2. [Rule 1 - Bug] Concurrent test files race against shared-MariaDB global afterEach

  • Found during: GREEN phase — running both test files together (and during full suite run)
  • Issue: vitest defaults to fileParallelism: true. The global afterEach in test/setup.ts runs in every worker and truncates lists/listShares. When two DB-backed test files ran concurrently, file A's afterEach deleted rows that file B's test was still reading — producing FK violations (ER_NO_REFERENCED_ROW_2) and incorrect empty results.
  • Fix: Added fileParallelism: false to vitest.config.ts, serializing test file execution.
  • Files modified: apps/api/vitest.config.ts
  • Commit: 9e17853

Verification Results

TDD Tests

  • listEmitter suite: 5 passed (Tests 1-4 + D-18 scale check)
  • listAccess suite: 4 passed (Tests 5-8)
  • Test 2 (D-04 cross-list negative): GREEN — handler subscribed to list 1 received 0 events when list 2 published
  • Test 7 (D-04 private-list negative): GREEN — getAccessibleListIds(otherUser) did not return a list owned exclusively by another user

Full API Suite

  • 15 test files passed | 3 skipped (Wave-0 stubs, expected) | 117 passed | 38 todo
  • No regressions from prior plans

TypeScript

  • pnpm --filter @familysync/api exec tsc --noEmit — PASS

ioredis Check

  • grep -r "ioredis" apps/api/ — not present (D-18 confirmed)

Known Stubs

None. Both modules are fully implemented and tested.

Threat Surface Scan

Flag File Description
T-04-02 (mitigated) apps/api/src/lib/listEmitter.ts Fan-out channel keyed by listId; cross-list isolation proven by Test 2
T-04-03 (mitigated) apps/api/src/lib/listAccess.ts Access-scope query restricted to owner_id OR list_shares; over-return proven impossible by Test 7
T-04-04 (accepted) apps/api/src/lib/listEmitter.ts setMaxListeners(200) headroom applied; DoS risk accepted for household scale

Self-Check: PASSED

  • apps/api/src/lib/listEmitter.ts — FOUND
  • apps/api/src/lib/listAccess.ts — FOUND
  • apps/api/tests/lib/listEmitter.test.ts — FOUND (stubs replaced)
  • apps/api/tests/lib/listAccess.test.ts — FOUND
  • apps/api/vitest.config.ts — FOUND (fileParallelism:false)
  • Commit 2d250af (RED) — FOUND
  • Commit 792efeb (GREEN) — FOUND
  • Commit 9e17853 (fix) — FOUND