Files
verdande/web/src/lib/connectivity.svelte.js
T
kristianwindandClaude Opus 4.8 57689ddfbb 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>
2026-09-03 22:46:35 +02:00

71 lines
2.7 KiB
JavaScript

/**
* Connectivity, as the app experiences it — not as the browser reports it.
*
* 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
* "reconnecting" rather than reverting the change under the user's hands. If the
* window runs out it is a real outage, and then the normal error path takes over.
*
* `navigator.onLine` is only a hint — it goes false when the machine has no route,
* but it stays true for a dead gateway or a server that is down, so it cannot be
* the source of truth. It is used here only to wake a waiting retry the moment the
* OS sees a link again, which is the one thing it is reliable for.
*/
import { browser } from '$app/environment';
class Connectivity {
/** 'online' — every recent request reached the server.
* 'retrying' — a request failed on the network and is being retried.
* 'lost' — a request gave up after the retry window; a real outage. */
status = $state('online');
/** A request reached the server. The network is up, whatever it was doing. */
reachable() {
this.status = 'online';
}
/** A request failed on the network and is about to be tried again. */
retrying() {
if (this.status !== 'lost') this.status = 'retrying';
}
/** A request used up its retry window without reaching the server. */
lost() {
this.status = 'lost';
}
}
export const connectivity = new Connectivity();
if (browser) {
// The OS lost its link: say so at once rather than waiting for the next request
// to fail. It does not, on its own, mean the server is unreachable — but with no
// route there is nothing a save can do but wait, and the honest thing is to show
// it waiting.
window.addEventListener('offline', () => connectivity.retrying());
}
/**
* Sleeps for `ms`, but wakes early the moment the OS reports a link again — so a
* retry that is biding its time does not sit out the last two seconds of a blip
* that has already ended.
*/
export function sleepOrOnline(ms) {
return new Promise((resolve) => {
let timer;
const done = () => {
clearTimeout(timer);
if (browser) window.removeEventListener('online', done);
resolve();
};
timer = setTimeout(done, ms);
if (browser) window.addEventListener('online', done, { once: true });
});
}