Files
familysync/.planning/milestones/v1.0-phases/04-shared-lists-live-sync/04-02-SUMMARY.md
T
Lucas Berger 982438dc10 style(13-03): apply Prettier formatting across repo
Mechanical reformat — no logic changes. 398 files changed, 19125
insertions(+), 16457 deletions(-). Prettier 3.8.4 with .prettierrc
(singleQuote:true, semi:true, tabWidth:2, trailingComma:all,
printWidth:100). Isolated per D-13-08 for reviewability.
2026-06-11 20:35:18 -04:00

131 lines
6.9 KiB
Markdown

---
phase: 04-shared-lists-live-sync
plan: '02'
subsystem: api-lib, test-harness
tags: [listEmitter, listAccess, scoped-fanout, D-04, tdd, eventEmitter, sse-primitive]
dependency_graph:
requires:
- 04-01 (lists/list_shares schema, test harness, vitest.config.ts)
provides:
- 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)
affects:
- apps/api/tests/lib/listEmitter.test.ts (stubs replaced with real assertions)
- apps/api/vitest.config.ts (fileParallelism:false added)
tech_stack:
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
key_files:
created:
- apps/api/src/lib/listEmitter.ts
- apps/api/src/lib/listAccess.ts
- apps/api/tests/lib/listAccess.test.ts
modified:
- apps/api/tests/lib/listEmitter.test.ts (it.todo stubs replaced with real assertions)
- apps/api/vitest.config.ts (fileParallelism:false; sequence.concurrent:false)
decisions:
- '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'
metrics:
duration: '~15 minutes'
completed: '2026-06-09'
task_count: 3
file_count: 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