The server side has been finished for a while — VAPID keys, RFC 8291 encryption, delivery on reminders and assignments — and nothing in the browser ever asked for a subscription. web/static/ had a manifest and an icon and no sw.js, so a subscription could not be created at all. The worker handles push and notificationclick and nothing else. There is deliberately no offline cache: the app is a live view of a list other people are editing, and a cached shell serving yesterday's data is worse than the browser saying it cannot connect. Adding one later means versioning and invalidating it, which is a feature rather than a few lines here. Unsubscribing tells the server first. If the browser unsubscribed and then the request failed, the server would keep pushing to an endpoint nothing is listening on, and only a 410 from the push service would ever clean it up. Still not verified against a live push service — this is the client half of a chain whose other half has only ever been tested against the RFC. Co-Authored-By: Claude Opus 5 <[email protected]>
65 lines
2.2 KiB
JavaScript
65 lines
2.2 KiB
JavaScript
/**
|
|
* verdande's service worker.
|
|
*
|
|
* It exists for Web Push and nothing else. There is deliberately no offline cache:
|
|
* the app is a live view of a task list that other people are editing, and a cached
|
|
* shell serving yesterday's data is worse than a browser saying it cannot connect.
|
|
* Adding one later means versioning and invalidating it, which is a real feature
|
|
* rather than a few lines here.
|
|
*/
|
|
|
|
// Take over as soon as the browser is willing. Without these two, the first push
|
|
// after granting permission arrives with no worker in control to show it.
|
|
self.addEventListener('install', () => self.skipWaiting());
|
|
self.addEventListener('activate', (event) => event.waitUntil(self.clients.claim()));
|
|
|
|
self.addEventListener('push', (event) => {
|
|
let payload = {};
|
|
try {
|
|
payload = event.data ? event.data.json() : {};
|
|
} catch {
|
|
// A push whose body is not our JSON still deserves to be shown: browsers
|
|
// display their own "site updated in the background" notice otherwise, which
|
|
// looks like a bug in the app.
|
|
payload = { title: 'verdande', body: event.data ? event.data.text() : '' };
|
|
}
|
|
|
|
const title = payload.title || 'verdande';
|
|
event.waitUntil(
|
|
self.registration.showNotification(title, {
|
|
body: payload.body || '',
|
|
icon: '/icon.svg',
|
|
badge: '/icon.svg',
|
|
// The tag collapses repeats: five comments on one task should replace each
|
|
// other rather than stack into five separate notifications.
|
|
tag: payload.tag || payload.url || 'verdande',
|
|
data: { url: payload.url || '/' }
|
|
})
|
|
);
|
|
});
|
|
|
|
self.addEventListener('notificationclick', (event) => {
|
|
event.notification.close();
|
|
const target = event.notification.data?.url || '/';
|
|
|
|
event.waitUntil(
|
|
(async () => {
|
|
const clientList = await self.clients.matchAll({
|
|
type: 'window',
|
|
includeUncontrolled: true
|
|
});
|
|
|
|
// Focus a tab that already has the app open rather than opening a second
|
|
// one. Somebody with the app pinned does not want a new tab per reminder.
|
|
for (const client of clientList) {
|
|
if (new URL(client.url).origin === self.location.origin) {
|
|
await client.focus();
|
|
if ('navigate' in client) await client.navigate(target);
|
|
return;
|
|
}
|
|
}
|
|
await self.clients.openWindow(target);
|
|
})()
|
|
);
|
|
});
|