feat(05-06): implement reminderScheduler — shared timed 15-min reminder scan

- Create apps/api/src/broker/reminderScheduler.ts:
  - runReminderCheck(now): queries isShared=true + allDay=false events
    with dtstartUtc in [now+14min, now+16min] via calendarEvents→calendars→
    pushSubscriptions cross-join (2 innerJoins; fans out to all subscribers)
  - In-memory sentReminders Set keyed uid:minuteBucket prevents double-fire
    at window boundary (D-06, T-05-18)
  - Per-event and per-subscription try/catch for error isolation (T-05-19)
  - Null title fallback (event.title ?? uid) — handles rows before Plan 05-07
  - Empty shared-calendar set produces zero sends and no crash (D-16)
  - startReminderScheduler(): node-cron 1-min schedule wrapping runReminderCheck
- Wire startReminderScheduler() into index.ts isMainModule() guard after
  startOutboxWorker() and VAPID setVapidDetails (NOTIF-01)
- Tests: 3/3 GREEN (all-day excluded, non-shared excluded, dedup)
This commit is contained in:
Lucas Berger
2026-06-09 21:38:32 -04:00
parent 9b04528fd6
commit b95f671485
2 changed files with 187 additions and 0 deletions
+4
View File
@@ -13,6 +13,7 @@ import { oidcAuthMiddleware, processOAuthCallback } from './auth/middleware.js'
import { devAuthBypass } from './auth/devBypass.js'
import { startBrokerPoller } from './broker/poller.js'
import { startOutboxWorker } from './broker/outboxWorker.js'
import { startReminderScheduler } from './broker/reminderScheduler.js'
import webpush from 'web-push'
export const app = new Hono()
@@ -132,6 +133,9 @@ if (isMainModule()) {
startBrokerPoller()
// Drain the D-05 outbox every 15s: dispatches pending CalDAV writes to Fastmail.
startOutboxWorker()
// Start the 1-min reminder scan for shared timed events starting in ~15 min (NOTIF-01).
// VAPID must be configured (above) before this starts or push sends will fail.
startReminderScheduler()
serve({ fetch: app.fetch, port: 3000 }, (info) => {
console.log(`FamilySync API running on http://localhost:${info.port}`)