notification catalogue: GET /api/notify/mapping
the piece that makes a new notification kind a server change rather than an app store release. a push carries a code and integers; this is what the code means, synced to the device instead of compiled into it. deliberately the same shape as affordances rather than a second mechanism: a `notifications/` directory of yaml in the plugin repository, copied to capabilities/notifications/<publisher>/<appName>/ on install, removed on uninstall. publish.ts mirrors affordances/publish.ts closely enough to diff. the namespace is DERIVED from where the file lands, never from what it declares — same rule as mountPrefix. a plugin writing `officer/core:test` as a key gets `<publisher>/<plugin>:officer/core:test` and the real core entry is untouched. that is the security property of the whole scheme and it is pinned by test. version is a content hash, not a counter. a counter needs somewhere to live and is then wrong after a restore, a rollback, or two servers built from the same install; a hash needs no storage and is right in all three. so the client compares mv !== version, and both docs now say so — i had written "higher". core entries ship in code rather than on disk, because capabilities/ is installed rather than committed and `test` has to exist on a machine that has installed nothing — it is what first light is proved with. read from disk per request, not memoised: installing a plugin has to change the answer without a restart, or it presents as "the new plugin's notifications don't work", which is the hardest kind of bug to place. verified live: 401 through the platform, plugin catalogue appearing and disappearing with no restart, version returning to its prior value when the plugin is removed. 10 catalogue tests, 100% coverage of catalogue.ts.
This commit is contained in:
@@ -98,13 +98,19 @@ credential → APNs → device → banner.
|
||||
GET /api/notify/mapping → { version: 47, entries: { "<code>": { title, body?, target, route, thread } } }
|
||||
```
|
||||
|
||||
_(Not built yet — I'm writing it now. The shape is fixed; assume it and I'll match it.)_
|
||||
**Built and live** as of 2026-08-19. `Cache-Control: no-store` — the client decides by `version`, so a
|
||||
cached copy would defeat the sync it drives. The catalogue is re-read from disk per request, so installing
|
||||
a plugin changes the response with no restart.
|
||||
|
||||
Codes are namespaced `<publisher>/<appName>:<code>`, e.g. `officerdev/music:12.3`.
|
||||
|
||||
The app **fetches this and caches it into a shared App Group container** — shared because the notification
|
||||
extension in Phase 3 must read it without the app running. Sync on launch, and whenever a push arrives
|
||||
carrying an `mv` higher than the cached `version`.
|
||||
whose `mv` **differs from** the cached `version`.
|
||||
|
||||
**Compare with `!==`, not `>`.** `version` is a content hash, not a counter, so it does not increase
|
||||
monotonically — removing a plugin returns it to the value it had before that plugin was installed. That is
|
||||
deliberate: a counter would need persistent storage and would be wrong after a restore or a rollback.
|
||||
|
||||
Entries are templates: `title: "{n} new emails"` rendered against the push's `n`. `route` carries the deep
|
||||
link with `{id}` interpolated.
|
||||
|
||||
Reference in New Issue
Block a user