91 lines
4.2 KiB
JavaScript
91 lines
4.2 KiB
JavaScript
// The app shell as a service worker, so an installed RecipesCam opens with no
|
|
// network at all. Not bundled on purpose: through vite/rollup it would stop being
|
|
// readable in DevTools, the one place a worker gets debugged. VERSION is the cache
|
|
// name and the whole update story — bump it when the shell changes, and `activate`
|
|
// drops every older cache.
|
|
//
|
|
// v2 is the bump that flushed a shell this worker had already pinned: before the
|
|
// header went on in nginx, a browser was free to guess a freshness window for
|
|
// index.html and answer a navigation with the previous build's shell — and its
|
|
// hashed bundle with it, which the STATIC rule below then served cache-first
|
|
// forever. Every navigation here now goes past the browser's own cache, so the
|
|
// build a visitor gets is the one the server has.
|
|
const VERSION = 'recipescam-v10';
|
|
|
|
// nginx answers all three with the same index.html (SPA fallback), so they are one
|
|
// document under three keys: an offline navigation finds it whichever key it asks.
|
|
const SHELL = ['/', '/app', '/library'];
|
|
|
|
// Cache-first paths: vite hashes every filename in its own bundle, and the wasm,
|
|
// the models and the icons never change under a given build.
|
|
const STATIC = /^\/(assets|wasm|models|icons)\//;
|
|
|
|
// The shell, read past the browser's cache on purpose: a `cache.add` of '/'
|
|
// consults that cache like any other fetch, so a shell stored during a stale
|
|
// window would be precached as the offline shell of the build that replaced it.
|
|
const shellRequest = (url) => new Request(url, { cache: 'reload' });
|
|
|
|
// Only a complete same-origin 200 is worth keeping — an opaque or error body
|
|
// stored here comes back as a failure on the next visit, and nothing in the worker
|
|
// can tell the two apart by then.
|
|
const cachedFetch = (request) =>
|
|
fetch(request).then((response) => {
|
|
if (response.ok) {
|
|
const copy = response.clone();
|
|
caches.open(VERSION).then((cache) => cache.put(request, copy));
|
|
}
|
|
return response;
|
|
});
|
|
|
|
// Offline, the studio the app is for; the landing page for a build without /app.
|
|
const offlineShell = async () =>
|
|
(await caches.match('/app')) ?? (await caches.match('/')) ?? Response.error();
|
|
|
|
self.addEventListener('install', (event) => {
|
|
// Best effort, one route at a time: a deploy caught mid-flight must not leave the
|
|
// worker uninstalled, so each route fails on its own.
|
|
event.waitUntil(
|
|
caches.open(VERSION)
|
|
.then((cache) => Promise.all(SHELL.map((url) => cache.add(shellRequest(url)).catch(() => {}))))
|
|
.then(() => self.skipWaiting()),
|
|
);
|
|
});
|
|
|
|
self.addEventListener('activate', (event) => {
|
|
event.waitUntil(
|
|
caches.keys()
|
|
.then((names) => Promise.all(names.filter((n) => n !== VERSION).map((n) => caches.delete(n))))
|
|
.then(() => self.clients.claim()),
|
|
);
|
|
});
|
|
|
|
self.addEventListener('fetch', (event) => {
|
|
const { request } = event;
|
|
// A worker is a cache, not a proxy: a POST, the API, another origin's font — a
|
|
// request that is not a plain same-origin read — goes straight out.
|
|
if (request.method !== 'GET') return;
|
|
const url = new URL(request.url);
|
|
if (url.origin !== self.location.origin || url.pathname.startsWith('/api/')) return;
|
|
|
|
// A navigation is network-first even when it is cached: a shell answered from the
|
|
// cache while the network holds a newer build pins the visitor to the old one. It
|
|
// is also the path an installed app opens through with no network.
|
|
// `no-store` because a plain fetch is not the network: it may be answered by the
|
|
// browser's own cache, which is the other place a stale shell hides.
|
|
if (request.mode === 'navigate') {
|
|
event.respondWith(fetch(request, { cache: 'no-store' }).catch(offlineShell));
|
|
return;
|
|
}
|
|
|
|
// ponytail: a navigation is never written back, the precached entry is what
|
|
// answers offline. Add a put() here once a route must survive on its own.
|
|
if (STATIC.test(url.pathname)) {
|
|
event.respondWith(caches.match(request).then((hit) => hit ?? cachedFetch(request)));
|
|
return;
|
|
}
|
|
|
|
// Everything else — a font, the manifest, an uploaded photo — is network-first,
|
|
// and answered from the cache only when that fails.
|
|
event.respondWith(fetch(request).catch(async () => (await caches.match(request)) ?? Response.error()));
|
|
});
|