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.
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 | |||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 05-web-push-notifications | 06 | api/reminder-scheduler |
|
|
|
|
|
|
Phase 05 Plan 06: reminderScheduler — shared timed 15-min reminder scan — Summary
TDD GREEN: reminderScheduler.ts implemented with D-05/D-07 SQL-enforced filtering, in-memory dedup, fan-out cross-join, and per-event error isolation — all 3 RED scaffold tests pass. Scheduler wired into index.ts isMainModule() guard.
Tasks Executed
Task 1: Implement reminderScheduler.ts (GREEN)
Status: Completed. Commit: b95f671
The RED scaffold (tests/broker/reminderScheduler.test.ts) was already committed in Plan 05-01 at ef558b6. This plan turns it GREEN.
Created apps/api/src/broker/reminderScheduler.ts with:
runReminderCheck(now = new Date()) — single reminder scan cycle:
- Drizzle query:
db.select().from(calendarEvents).innerJoin(calendars, ...).innerJoin(pushSubscriptions, sql\1=1`)` — 2 innerJoins; cross-join fans each event out to all subscribers - WHERE:
isShared=true AND allDay=false AND dtstartUtc >= now+14min AND dtstartUtc <= now+16min - D-05 enforced in QUERY (not copy) — personal events excluded at SQL level
- D-07 enforced in QUERY — all-day events excluded at SQL level
- Groups flat rows by uid, collects per-event subscription list
- Dedup:
sentReminders.add(\${uid}:${minuteBucket}`)` prevents window-boundary double-fire (T-05-18) - Title fallback:
event.title ?? uid— no "undefined" in reminder copy (NOTIF-01 / plan note) - Notification payload:
{ title, body: 'Starts in 15 min', tag: \reminder-${uid}`, navigate: `/calendar?date=${yyyyMmDd(dtstartUtc)}&event=${uid}` }` - Per-event and per-subscription try/catch for error isolation (T-05-18, T-05-19)
- Empty shared-calendar / empty push_subscriptions: cross-join returns 0 rows → zero sends, no crash (D-16)
startReminderScheduler() — node-cron * * * * * schedule (every minute):
- Same shape as
startBrokerPollerinpoller.ts—.catch()on the returned promise - Not called at import time (guards the test process per WR-04)
index.ts — added startReminderScheduler() call in the isMainModule() guard, after startOutboxWorker() and after webpush.setVapidDetails() (so VAPID is configured before the scheduler starts).
TDD Gate Compliance:
- RED:
test(05-01): add Wave-0 RED scaffolds + VAPID fixture + setup truncation—ef558b6(Plan 05-01) - GREEN:
feat(05-06): implement reminderScheduler — shared timed 15-min reminder scan—b95f671
Verification
pnpm --filter @familysync/api exec vitest run tests/broker/reminderScheduler.test.ts
Test Files 1 passed (1)
Tests 3 passed (3)
grep -q "startReminderScheduler" apps/api/src/index.ts → PASSED
pnpm --filter @familysync/api typecheck → passed (no errors)
Deviations from Plan
Auto-fixed issues
None. Plan executed exactly as written.
Architecture note (no deviation — design decision)
The cross-join approach (innerJoin(pushSubscriptions, sql\1=1`)) was chosen over two separate db.select()` calls because:
- The test scaffold's mock requires exactly 2
innerJoin()calls in a single chain (.from().innerJoin().innerJoin().where()) - A cross-join is semantically correct: shared reminder → all members
- Grouping by uid after the flat result correctly handles the fan-out while maintaining the
uid:minuteBucketdedup semantics
Known Stubs
None. The scheduler is fully implemented. The calendar_events.title column may be NULL for events synced before Plan 05-07 (which adds title extraction to the sync path), but the null fallback (event.title ?? uid) handles this gracefully without stubbing.
Threat Flags
No new threat surface. All three plan threats mitigated:
| Threat | Status |
|---|---|
| T-05-17: personal-calendar event in reminder | Mitigated — WHERE isShared=true enforced in SQL |
| T-05-18: duplicate reminder storm at window boundary | Mitigated — in-memory dedup Set; per-event try/catch |
| T-05-19: one bad subscription aborting cycle | Mitigated — per-subscription try/catch; dispatchPush swallows 410/404 |
Self-Check
Files created/verified:
- apps/api/src/broker/reminderScheduler.ts — exists
Commits verified:
ef558b6: test(05-01): add Wave-0 RED scaffolds + VAPID fixture + setup truncation (RED gate — Plan 05-01)b95f671: feat(05-06): implement reminderScheduler — shared timed 15-min reminder scan (GREEN gate)