mirror of
https://github.com/HeyPuter/puter.git
synced 2026-10-12 14:50:39 +00:00
docs: catch-up-on-notifications events recipe (PUT-1961)
This commit is contained in:
1 parent
830d7cb745
commit
e2f608aa5f
2 files changed
+178
-1
No files matched your search
@@ -0,0 +1,176 @@
|
||||
---
|
||||
title: Catch Up on Notifications
|
||||
description: "Learn how to show Puter notifications in your app with Puter.js events, including the ones that arrived while your app was closed."
|
||||
tags: [events]
|
||||
order: 66
|
||||
---
|
||||
|
||||
<div class="info">The Events API is in beta. Event shapes, limits, and behavior may change between releases.</div>
|
||||
|
||||
Puter posts notifications to a user's account when something needs their
|
||||
attention, for example when a persistent subscription your app made for them
|
||||
ends, or, for the app's owner, when its worker finishes deploying. With the
|
||||
[Events API](/Events/) your app can show these as they arrive, and catch up on
|
||||
the ones that came in while it was closed. Puter creates notifications itself.
|
||||
An app can't post its own.
|
||||
|
||||
This recipe builds on [Watch for Changes](/recipes/events-watch-for-changes/),
|
||||
which covers what every subscription shares: gaps, stopping, reconnects and
|
||||
cost. The examples call `showNotification(event)`, your own function that shows
|
||||
one notification, such as a toast or a row in a panel.
|
||||
|
||||
## Show New Notifications
|
||||
|
||||
To show notifications as they arrive, subscribe to `notif:app-user` with
|
||||
[`puter.events.onLocal()`](/Events/onLocal/):
|
||||
|
||||
```js
|
||||
const sub = await puter.events.onLocal('notif:app-user', ({ event }) => {
|
||||
if (event.op === 'gap') return;
|
||||
showNotification(event);
|
||||
});
|
||||
```
|
||||
|
||||
A gap carries no notification, so this handler skips it.
|
||||
[Catch Up, Then Keep Listening](#catch-up-then-keep-listening) handles gaps
|
||||
properly. The fields to use are:
|
||||
|
||||
- `id`: the notification's id. A notification has the same `id` whether it
|
||||
arrives live or from `fetch()`, so use it to tell notifications apart.
|
||||
- `type`: what kind of notification it is, such as `app.events.ended`.
|
||||
- `notification.title`: the text to show. The other fields in `notification`
|
||||
depend on the type.
|
||||
- `ts`: when it was created, in milliseconds since the epoch.
|
||||
|
||||
Don't use `seq` or `ts` to tell notifications apart. `seq` is only a position
|
||||
within one page or one delivery, and two notifications can share a `ts`.
|
||||
|
||||
## Which Notifications Your App Sees
|
||||
|
||||
A `notif:` subject names one part of the user's notifications:
|
||||
|
||||
- `notif:app-user` is about the user's use of your app. Today that is Puter
|
||||
telling them that a [persistent subscription](/Events/onPersistent/) your app
|
||||
made for them has ended or stopped delivering (`app.events.ended`).
|
||||
- `notif:developer` is about your app itself, for the account that owns it:
|
||||
your app's worker deploys (`app.worker.deployed`, `app.worker.deployFailed`)
|
||||
and suspended event handlers (`app.events.suspended`). For any other user,
|
||||
`onLocal()` rejects with `subject_does_not_exist` and `fetch()` returns an
|
||||
empty page.
|
||||
- `notif:account` holds account notifications, such as a file shared with the
|
||||
user. Apps and websites never see these, and no permission changes that.
|
||||
`onLocal()` rejects with `subject_does_not_exist` and `fetch()` returns an
|
||||
empty page.
|
||||
|
||||
Reading your app's own notifications needs no permission. Puter fills in your
|
||||
[app ID](/Utils/appID/), so the subject never needs one, and another app's
|
||||
notifications are never visible to yours.
|
||||
|
||||
## Read What You Missed
|
||||
|
||||
To read the notifications that arrived while your app was closed, call
|
||||
[`puter.events.fetch()`](/Events/fetch/). It returns a page of notifications,
|
||||
oldest first, in the same shape as live ones. Pass each page's `cursor` back as
|
||||
`after` until a page comes back without one:
|
||||
|
||||
```js
|
||||
let after;
|
||||
do {
|
||||
const page = await puter.events.fetch({ subject: 'notif:app-user', after });
|
||||
page.items.forEach(showNotification);
|
||||
after = page.cursor;
|
||||
} while (after);
|
||||
```
|
||||
|
||||
A page holds 50 notifications by default. Pass `limit` for up to 200, and a
|
||||
larger `limit` is treated as 200. Without `after`, `fetch()` starts from the
|
||||
oldest notification Puter still keeps. Puter deletes old notifications after a
|
||||
while, so a user who was away longer than that starts from what is left.
|
||||
|
||||
`fetch()` only reads. It doesn't mark anything as read, and it also returns
|
||||
notifications the user already dismissed, so your app has to remember what it
|
||||
has shown. The next section does that.
|
||||
|
||||
Only `notif:` subjects can be fetched. A `kv:` or `fs:` subject rejects with
|
||||
`fetch_unsupported_subject`, because nothing stores those changes. Read the
|
||||
data itself again instead, as in
|
||||
[Watch for Changes](/recipes/events-watch-for-changes/).
|
||||
|
||||
## Catch Up, Then Keep Listening
|
||||
|
||||
To show each notification once, across visits, combine the two. Subscribe, then
|
||||
fetch from where the last visit stopped, and save the cursor with
|
||||
[`puter.kv`](/KV/) so the next visit starts there:
|
||||
|
||||
```js
|
||||
const subject = 'notif:app-user';
|
||||
const saved = await puter.kv.get('notifications') ?? {};
|
||||
let after = saved.after;
|
||||
const seen = new Set(saved.seen);
|
||||
|
||||
function showOnce (event) {
|
||||
if (seen.has(event.id)) return;
|
||||
seen.add(event.id);
|
||||
showNotification(event);
|
||||
}
|
||||
|
||||
async function save () {
|
||||
await puter.kv.set('notifications', { after, seen: [...seen].slice(-200) });
|
||||
}
|
||||
|
||||
async function catchUp () {
|
||||
let page;
|
||||
do {
|
||||
page = await puter.events.fetch({ subject, after });
|
||||
page.items.forEach(showOnce);
|
||||
after = page.cursor ?? after;
|
||||
} while (page.cursor);
|
||||
await save();
|
||||
}
|
||||
|
||||
await puter.events.onLocal(subject, async ({ event }) => {
|
||||
if (event.op === 'gap') return catchUp();
|
||||
showOnce(event);
|
||||
await save();
|
||||
});
|
||||
await catchUp();
|
||||
```
|
||||
|
||||
Subscribe first, then fetch. A notification created while `catchUp()` runs then
|
||||
reaches the handler. If you fetch first, one created between the fetch and the
|
||||
subscription stays hidden until the next visit. Subscribing first means a
|
||||
notification can arrive both live and from `fetch()`, and `showOnce()` drops the
|
||||
second copy by `id`.
|
||||
|
||||
The last page has no cursor, so `after` stays on the cursor before it, and the
|
||||
next visit reads that page again. That is why the ids are saved along with the
|
||||
cursor. Only the newest ones can come back, so the last 200 are enough.
|
||||
|
||||
A [gap](/recipes/events-watch-for-changes/#handle-gaps) means notifications
|
||||
were not delivered, and one also arrives after a reconnect. Calling
|
||||
`catchUp()` on a gap reads what was missed, and `showOnce()` skips the rest.
|
||||
|
||||
On a first visit there is no saved cursor, so `catchUp()` shows every
|
||||
notification Puter still keeps.
|
||||
|
||||
## Notes
|
||||
|
||||
- `fetch()` doesn't open the sign-in. On a website with nobody signed in, it
|
||||
rejects with `token_missing`. In the example above, `puter.kv.get()` signs
|
||||
the user in first. [Watch for Changes](/recipes/events-watch-for-changes/#watch-a-key)
|
||||
shows how to start from a sign-in button.
|
||||
- Leave `after` undefined when there is no cursor. `fetch()` rejects `null`
|
||||
with `invalid_request`.
|
||||
- `fetch()` allows 120 calls a minute per user and app, and rejects with
|
||||
`too_many_requests` past that. Call it on start and on a gap, never on a
|
||||
timer. The subscription already tells you when something new arrives. See
|
||||
[Rate Limits and Quotas](/rate-limits-and-quotas/#events).
|
||||
- `fetch()` rejects with `invalid_subject_audience` for an audience other than
|
||||
`app-user`, `developer` or `account`, and with `events_disabled` on a server
|
||||
where events are off. A server with notification events turned off rejects
|
||||
the `onLocal()` subscription with `invalid_subject`, while `fetch()` still
|
||||
works.
|
||||
- A notification subscription counts toward the same subscribe limit and costs
|
||||
the same per delivery as any other. [Watch for Changes](/recipes/events-watch-for-changes/#stop-watching)
|
||||
covers `off()`, and its [Notes](/recipes/events-watch-for-changes/#notes)
|
||||
cover cost.
|
||||
@@ -256,7 +256,8 @@ updating without any sign.
|
||||
- [`puter.kv.flush()`](/KV/flush/) sends no event, and neither does a key that
|
||||
expires on its own.
|
||||
- [`puter.events.fetch()`](/Events/fetch/) cannot catch up on key-value or file
|
||||
changes. It only reads notifications.
|
||||
changes. It only reads notifications, as in [Catch Up on
|
||||
Notifications](/recipes/events-catch-up-on-notifications/).
|
||||
- Watching a folder outside your app's own, such as the user's Documents, needs
|
||||
the user's permission first. [Ask for Access](/recipes/perms-ask-for-access/)
|
||||
covers it.
|
||||
|
||||
Reference in new issue
Block a user