From 7702f7e19a4898df6074d906583e600869c3f10b Mon Sep 17 00:00:00 2001 From: Lucas Berger Date: Tue, 9 Jun 2026 22:22:04 -0400 Subject: [PATCH] fix(05-review): CR-01+WR-01 prune sentReminders after each scan and mark-sent after dispatch --- apps/api/src/broker/reminderScheduler.ts | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/apps/api/src/broker/reminderScheduler.ts b/apps/api/src/broker/reminderScheduler.ts index 9eb531d..b8ace68 100644 --- a/apps/api/src/broker/reminderScheduler.ts +++ b/apps/api/src/broker/reminderScheduler.ts @@ -130,8 +130,6 @@ export async function runReminderCheck(now = new Date()): Promise { try { const key = `${uid}:${minuteBucket}` if (sentReminders.has(key)) continue - // Mark sent BEFORE dispatching to prevent re-entry on concurrent ticks - sentReminders.add(key) const dateStr = yyyyMmDd(event.dtstartUtc) const notification: NotificationPayload = { @@ -157,6 +155,11 @@ export async function runReminderCheck(now = new Date()): Promise { ) } } + + // WR-01: mark sent AFTER all dispatches have been attempted. Pre-marking before + // dispatch prevents retry in the same bucket when dispatchPush throws — at-least-once + // delivery requires not pre-marking the key. + sentReminders.add(key) } catch (err) { // Per-event error isolation (T-05-18): one bad event never aborts remaining events. console.error( @@ -165,6 +168,18 @@ export async function runReminderCheck(now = new Date()): Promise { ) } } + + // CR-01: Prune stale entries from sentReminders to prevent unbounded growth. + // Entries for the previous minute bucket and older are no longer needed — the + // dedup window is (uid, minuteBucket), and the current minute has now been + // processed. Keep only the current bucket; discard everything older. + const staleBucket = minuteBucket - 1 + for (const key of sentReminders) { + const colonIdx = key.lastIndexOf(':') + if (colonIdx !== -1 && Number(key.slice(colonIdx + 1)) < staleBucket) { + sentReminders.delete(key) + } + } } // ── Scheduler ─────────────────────────────────────────────────────────────────