The syncNotify coalescer buffers for 5s and collapses a burst into one
"N changes" summary, which is the documented defence against the overnight
background-sync flood. But the guard above it short-circuited to an
immediate post whenever AppState was not 'active', so the protection only
ever ran in the FOREGROUND: the one state where the user is already looking
at the calendar and least needs a notification.
The bypass was not gratuitous. RN freezes JS timers while backgrounded, so
a deferred flush never runs until the app is foregrounded again (#100), and
posting synchronously is what made background notifications work at all.
Confirmed on a Pixel 9 Pro while diagnosing #126: a 3s timer scheduled at
background time had still not fired after 12s backgrounded, and only ran on
resume. So the fix has to be timer-free.
Two timer-free levers, both compared against Date.now() at post time:
1. A content-derived notification id. nm.notify(id, ...) REPLACES an
existing notification with the same id, so identical repeats collapse
at the OS level. The old code drew a fresh random id per post, which
guaranteed the opposite: three identical posts became three rows.
2. A rolling burst counter. The first change of a burst posts verbatim so
the user sees what actually changed; later ones go out under one
stable summary id, so a flood updates a single "N changes" line in
place instead of adding a row each time.
Exact (title, body) repeats inside a 10s window are dropped outright, and
the dedup map prunes aged-out entries so it cannot grow without bound.
Immediate alerts (rejoin requests) keep their own text and are never folded
into a rollup, but are still deduped against an exact repeat.
Foreground behaviour is unchanged. The background path no longer strands
anything the foreground buffer had pending: it flushes that first.
Decision logic extracted to src/lib/syncNotifyPolicy.js so it is unit
testable, matching the eventRegistry/groupRecord/backendBootstrap split.
12 new tests including the regression: 12 distinct changes in a burst now
yield 2 distinct notification ids instead of 12. 196/196 pass, no new type
errors, and the build boots clean on the TCL.
Fixes#128
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EPBW7sZJPAjkVRiqnB722W