Push notification dedupe: one message = one notification — an offline message was notified twice (FCM push, then again when the app synced the outbox and mirrored the replayed frames). Fix: the gateway records the highest outbox cursor delivered per device via push (devices.last_pushed_cursor, advanced only on successful send) and returns it in hello.ack; sync-replayed frames carry their outbox cursor in the envelope; the app skips system notifications for replayed frames at/below the watermark (live frames never suppressed — that is the case where no push fired). Also: 5s per-chat push coalescing so a cron delivery (notification frame + message frame) pushes once, and the FCM handler no longer posts a redundant notification (skips when WS is connected or FCM already displayed the notification payload; data-only messages are the exception). Docs: frames.schema.json, 04-wire-protocol.md, 08-push.md §8.8
This commit is contained in:
1 parent
acd5fb4ad0
commit
9f3f9842c8
11 files changed
+301
-98
No files matched your search
+31
-1
@@ -102,4 +102,34 @@ persist until acted on.
|
||||
short preview (privacy on lock screen). Full content is fetched via `sync`
|
||||
over the authenticated WS.
|
||||
- ntfy: use a **private topic + auth token** for any real trust boundary (hermes
|
||||
ntfy adapter guidance).
|
||||
ntfy adapter guidance).
|
||||
|
||||
## 8.8 Notification dedupe (push vs. sync)
|
||||
|
||||
A message sent while the device is offline is notified **twice** by naive
|
||||
design: once by the push (FCM displays the `notification` payload), and again
|
||||
when the app reconnects, syncs the outbox, and mirrors the replayed frames to
|
||||
system notifications (the background-mirror path, §8.5). The fix is a per-
|
||||
device push watermark:
|
||||
|
||||
- **Gateway** records the highest outbox cursor delivered to each device via
|
||||
the push backend (`devices.last_pushed_cursor`, advanced only on a
|
||||
*successful* send) and returns it in `hello.ack` as `last_pushed_cursor`.
|
||||
- **Gateway** coalesces back-to-back pushes per chat (5 s window): a cron
|
||||
delivery parks a notification frame AND a message frame, and only the first
|
||||
pushes — the second reaches the app via sync (tap the first notification).
|
||||
- **Gateway** tags every `sync`-replayed frame with its outbox cursor in the
|
||||
frame envelope (`cursor`; live frames carry none).
|
||||
- **App** skips system notifications for replayed frames with
|
||||
`cursor <= lastPushedCursor` (they already woke the device). Live frames are
|
||||
never suppressed — that is exactly the case where no push fired and the app
|
||||
must notify itself.
|
||||
- **App** FCM handler (`onMessageReceived`) posts nothing when the WS is
|
||||
connected (the background-mirror path handles it) and nothing when the
|
||||
message carried a `notification` payload (FCM already displayed it);
|
||||
data-only messages are the exception (the app must display them itself).
|
||||
|
||||
Residual edge: FCM is at-least-once, so a lost device ack can still produce a
|
||||
duplicate *system-displayed* notification (two `FCM-Notification:*` ids). The
|
||||
designed evolution is the data-only push option (§8.2.1), which moves display
|
||||
into the app and lets it use a stable per-message notification id.
|
||||
Reference in new issue
Block a user