Repository navigation
feat(notifications): let realtime broadcasts wake a device and supersede themselves - #9
Merged
3m1n3nc3 merged 4 commits intoSep 16, 2026
Conversation
…ede themselves
The Firebase driver sent `data: { event, payload }` and nothing else. With no
`android.priority`, a data message goes out at normal priority, which Doze and
App Standby are free to hold until the next maintenance window -- exactly the
state a device is in when something time-critical needs to reach it. The typed
messaging slice also made this unreachable from outside: there was no parameter
to pass a priority through, so it could not be worked around downstream.
Delivery options
`.priority()`, `.ttl()` and `.collapseKey()` on the realtime builder, carried
to the driver through a new optional `delivery` argument on `RealtimeDriver`.
Each hint converts to the unit its platform expects -- Android TTL in
milliseconds, `apns-expiration` as an absolute epoch second, Web Push in
seconds. Defaults can be set under `drivers.realtime.delivery` or per
transport, and raw `android` / `apns` / `webpush` blocks pass through for
anything unmapped. Pusher accepts and ignores them; it holds an open
connection, so there is no dozing radio to wake.
Priority reaches APNs only once the message is a visible push, which the
caller declares through the `apns` escape hatch. Apple requires a background
push -- what a data-only message becomes on iOS -- to be priority 5 or 1, so
deriving 10 there would send APNs something it is documented to reject.
Supersession
`.tag()` gives a notification a stable identity so a later one replaces it
rather than stacking beside it, and `.retract()` withdraws one outright.
Retractions are never persisted. `tag` is deliberately not wired to
`collapseKey`: a tag is an unbounded client-side identity, while FCM keeps
only four collapse keys per device and silently evicts the rest.
`@arkstack/realtime` gains `supersede()`, applied by the React and Vue
bindings and exported for hand-rolled lists.
Custom transports
`driverFactory` supplies a `RealtimeDriver` of your own, mirroring the
client's existing `transportFactory`; `broadcast()` now accepts payloads that
are not notifications. Together these let an application reach a transport
this package does not ship -- APNs or PushKit, say -- without forking it.
Additive throughout: an unhinted broadcast produces a byte-identical message,
and a driver written against the old three-parameter contract still compiles.
…uctor - notifications: add `CUserNotification`, a constructor type for the abstract `UserNotification` model, and resolve `getModel` against it. - jobs, h3 template: import ordering, method spacing, and a narrower `H3Middleware` import path.
3m1n3nc3
force-pushed
the
feat/notifications/realtime-delivery-and-supersession
branch
from
September 16, 2026 12:59
eb36d78 to
6c467de
Compare
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
3m1n3nc3
deleted the
feat/notifications/realtime-delivery-and-supersession
branch
September 16, 2026 13:19
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
The Firebase driver sent
data: { event, payload }and nothing else — noandroid.priority, no TTL, no collapse key, no APNs headers. An FCM data message defaults to normal priority, and Doze and App Standby may hold a normal-priority message until the next maintenance window, which is exactly the state a device is in when something time-critical needs to reach it.It also could not be worked around downstream. The hand-rolled
FirebaseMessagingslice declaredsend({ topic, data })with no index signature, andRealtimeDriver.broadcastwas fixed at three parameters, so there was nowhere for a caller to pass a priority.What
Delivery options.
.priority(),.ttl()and.collapseKey()on the realtime builder, reaching the driver through a new optionaldeliveryargument. Each hint converts to the unit its platform expects — Android TTL in milliseconds,apns-expirationas an absolute epoch second, Web Push in seconds. Defaults live underdrivers.realtime.deliveryor per transport; rawandroid/apns/webpushblocks pass through for anything unmapped. Pusher accepts and ignores them.Supersession.
.tag()gives a notification a stable identity so a later one replaces it in place;.retract()withdraws one outright.@arkstack/realtimegainssupersede(), applied by the React and Vue bindings and exported for hand-rolled lists.Custom transports.
driverFactorysupplies your ownRealtimeDriver, mirroring the client's existingtransportFactory, andbroadcast()now accepts non-notification payloads.Two deliberate calls
Priority does not reach APNs by default. A data-only message becomes a background push on iOS, and Apple requires those to be priority 5 or 1 — mapping
highto 10 would send APNs something documented to be rejected, turning throttled-but-working delivery into an error. It is derived only once the caller declares a visible push through theapnsescape hatch. Ringing on iOS needs PushKit, which FCM cannot address at all;driverFactoryis the supported way to bring that yourself.tagis not wired tocollapseKey. They look alike but have different budgets: a tag is an unbounded client-side identity, while FCM keeps only four collapse keys per device and silently evicts the rest. Deriving one per tag would starve that budget. Set both when you want to supersede both what is queued and what is displayed.Breaking?
No — minor. Every addition is optional, and an unhinted broadcast produces a byte-identical FCM message. I probed a driver written against the old three-parameter contract with the narrow payload type; it still compiles, since TypeScript compares method parameters bivariantly.
Two notes for the release:
drivers.realtime.transportwent required → optional, since naming a built-in is meaningless alongsidedriverFactory. Writing a config is unaffected; reading that property now yields| undefined.@arkstack/realtimebefore sending a first retraction. A client that predates this ignoresretractedand would render a retraction as an empty notification. This is also the argument for superseding with replacement content rather than a bare retraction — a replacement degrades correctly on an old client.Verification
76 tests pass across both packages (40 new),
pnpm lintclean, both packages and the docs site build.