Files
familysync/.planning/phases/05-web-push-notifications/05-03-SUMMARY.md
T

5.3 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
05-web-push-notifications 03 api/push-coalescer
web-push
coalescer
debounce
tdd
red-green
D-01
D-03
requires provides affects
05-01
05-02
coalesceListPush — per-(list
actor) sliding debounce
apps/api/src/lib/pushCoalescer.ts
added patterns
module-level Map singleton (listEmitter.ts idiom)
sliding debounce setTimeout
injected dispatch for testability
created modified
apps/api/src/lib/pushCoalescer.ts
apps/api/tests/lib/pushCoalescer.test.ts
dispatch signature is (listId, actorId, count) — matches existing RED scaffold; richer payload shape deferred to caller (Plan 05-05)
key is ${listId}:${actorId} — per-(list,actor) matches D-01 intent; allows two members editing same list to coalesce independently
sliding debounce (each call resets timer) — per plan spec; leading debounce not used
dispatch return value is a Promise; errors caught and logged inside fire() so caller loop never breaks
duration completed_date tasks_completed files_changed
5 2026-06-10 2 2

Phase 05 Plan 03: pushCoalescer — per-(list,actor) debounce — Summary

TDD RED→GREEN: pushCoalescer.ts implemented; per-(list,actor) sliding debounce collapses list-change bursts into a single dispatch call carrying (listId, actorId, count).

Tasks Executed

Task 1: RED — fix lint warning, add actorId assertion

Status: Completed. Commit: 7af827a

The existing RED scaffold in apps/api/tests/lib/pushCoalescer.test.ts (from Plan 05-01) had a lint warning: calledActorId was destructured in test 1 but never asserted. Added expect(calledActorId).toBe(actorId) to make the self-suppression assertion explicit in the burst-coalescing test as well (not only in the dedicated D-03 test).

Tests still fail after this change (RED preserved): Cannot find module '.../pushCoalescer.js'.

Task 2: GREEN — implement pushCoalescer.ts

Status: Completed. Commit: c1758de

Created apps/api/src/lib/pushCoalescer.ts:

coalesceListPush(listId, actorId, dispatch, windowMs=45000)

  • Module-level Map<string, {count, timer}> keyed by ${listId}:${actorId}
  • First call in a burst: inserts entry with count=1, starts setTimeout(windowMs)
  • Subsequent calls within window: clearTimeout, increments count, resets timer (sliding debounce)
  • On timer fire: deletes map entry, calls dispatch(listId, actorId, count) — self-deleting entries keep the map bounded (T-05-08)
  • dispatch errors caught and logged with [pushCoalescer] prefix; never throws to caller

Verification

pnpm --filter @familysync/api exec vitest run tests/lib/pushCoalescer.test.ts

 Test Files  1 passed (1)
      Tests  3 passed (3)

pnpm --filter @familysync/api typecheck — passes.

TDD Gate Compliance

  • RED: test(05-03): add actorId assertion in burst test — fix unused var lint warning7af827a
  • GREEN: feat(05-03): implement pushCoalescer — per-(list,actor) sliding debounce (D-01/D-03)c1758de

Deviations from Plan

Auto-fixed Issues

1. [Rule 1 - Bug] Lint warning — unused calledActorId in burst test

  • Found during: Task 1 (RED)
  • Issue: calledActorId was destructured in test 1 but the assertion was missing, producing an unused-variable lint warning.
  • Fix: Added expect(calledActorId).toBe(actorId) — the burst test now also asserts self-suppression, not just the dedicated D-03 test.
  • Files modified: apps/api/tests/lib/pushCoalescer.test.ts
  • Commit: 7af827a

Dispatch signature simplification

The plan's <behavior> section describes dispatch(payload, actorId) where payload is a rich object {title, body, tag, navigate}. The existing RED scaffold (committed in Plan 05-01) uses dispatch(listId, actorId, count) — a simpler 3-argument form that defers notification copy construction to the caller.

The test is canonical; the implementation matches the test. The richer payload construction (D-02 generic copy: "${actorName} updated ${listName}", "${N} change(s)") is owned by the caller in Plan 05-05, which has the actorName/listName context from the DB row and passes a closure over dispatchPush.

Known Stubs

None. The coalescer is complete and testable. Plan 05-05 wires it into the list-change fan-out with actual notification copy.

Threat Flags

No new threat surface. pushCoalescer.ts is a pure in-memory utility module — no network endpoints, no auth paths, no file access.

T-05-06 (generic copy — no item text): mitigated by design — the coalescer passes only count, not item text; copy construction in Plan 05-05 will follow D-02. T-05-07 (self-notification): mitigated — actorId threaded to dispatch so caller can apply WHERE userId != actorId. T-05-08 (unbounded map): accepted — entries self-delete on timer fire; two-person household keeps keys bounded.

Self-Check

Files verified:

  • apps/api/src/lib/pushCoalescer.ts — exists
  • apps/api/tests/lib/pushCoalescer.test.ts — modified

Commits verified:

  • 7af827a: test(05-03): add actorId assertion in burst test — fix unused var lint warning (RED gate)
  • c1758de: feat(05-03): implement pushCoalescer — per-(list,actor) sliding debounce (D-01/D-03) (GREEN gate)

Self-Check: PASSED