feat(offline): hold through a short network drop with a caching worker

The service worker did Web Push and nothing else, with a note that a cache
serving yesterday's data was worse than an honest "cannot connect". This adds a
cache that does not do that: network-first for everything that can go stale, so a
reader online always gets the live answer and the cache is reached only when the
network cannot answer. The task and note lists stay live; they just no longer go
blank the moment the signal does — a reload through a tunnel, a lift or a
base-station handover comes back to the app instead of the browser's offline page.

Writes are untouched and still go straight to the network; api.js already retries
them across the same short drop. The shell cache is versioned so a deploy lands in
a fresh cache and the old one is dropped on activate; the data cache is cleared on
sign-out so the next person on the browser does not inherit it.

The old hand-registered /sw.js becomes SvelteKit's src/service-worker.js, which it
registers itself, and the push handlers move across unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
kristianwind
2026-09-03 22:46:35 +02:00
co-authored by Claude Opus 4.8
parent 7770ba9844
commit 57689ddfbb
6 changed files with 258 additions and 76 deletions
+10
View File
@@ -31,6 +31,16 @@
async function signOut() {
await api.logout();
// Empty the offline data cache before leaving: the service worker keeps the
// last-seen task and note lists so a network drop does not blank the app, and
// the next person to sign in on this browser must not find them.
try {
navigator.serviceWorker?.controller?.postMessage({ type: 'clear-api-cache' });
await caches?.delete('verdande-api');
} catch {
// No service worker / no Cache API (older browser, private mode): nothing
// was cached, so nothing to clear.
}
location.href = '/';
}
+5 -5
View File
@@ -1,11 +1,11 @@
/**
* Connectivity, as the app experiences it — not as the browser reports it.
*
* This is for short drops: a few seconds of lost Wi-Fi, a tunnel, a base station
* handing over. It is deliberately NOT offline support. The app is already loaded
* and its data is already in memory, so there is nothing to cache and nothing to
* open from cold; what a blip needs is for the save you just made to survive it,
* not for the whole app to run without a server.
* This is the WRITE side of surviving a short drop — a few seconds of lost Wi-Fi, a
* tunnel, a base station handing over. The READ side lives in the service worker,
* which keeps the last-seen task and note lists so a reload through a drop is not a
* blank screen. This store is the other half: it is what keeps the save you just made
* from being reverted under your hands while the network is briefly gone.
*
* The request layer drives this: it retries a failed request for a short window
* (see api.js) and tells this store while it is trying, so the interface can say
+7 -3
View File
@@ -26,9 +26,13 @@ export function permission() {
}
async function registration() {
// `ready` waits for an active worker; register() alone resolves while the
// worker is still installing, and subscribing against that fails.
await navigator.serviceWorker.register('/sw.js');
// SvelteKit registers src/service-worker.js itself on load, so normally there
// is already a registration here. Register it by hand only if there is not yet
// one — a push subscribe that races the app's own registration would otherwise
// find nothing. `ready` waits for an active worker; register() alone resolves
// while the worker is still installing, and subscribing against that fails.
const existing = await navigator.serviceWorker.getRegistration();
if (!existing) await navigator.serviceWorker.register('/service-worker.js');
return navigator.serviceWorker.ready;
}