71 lines
3.4 KiB
JavaScript
71 lines
3.4 KiB
JavaScript
// Service worker.
|
|||
|
|
// Goal: keep the app usable on flaky/no network without ever serving stale DATA.
|
||
|
|
// • App SHELL (HTML/CSS/JS/manifest) — network-first, fall back to cache so the
|
||
|
|
// app still loads when offline. Successful fetches refresh the cache.
|
||
|
|
// • API (/api/*) — never cached. The client handles offline itself (it queues
|
||
|
|
// check-offs in localStorage and syncs on reconnect).
|
||
|
|
|
||
|
|
const SW_VERSION = 'v5';
|
||
|
|
const SHELL_CACHE = 'shell-' + SW_VERSION;
|
||
|
|
const SHELL = [
|
||
|
|
'/', '/index.html', '/app.js', '/styles.css', '/md.js', '/manifest.webmanifest',
|
||
|
|
'/vendor/motion.global.js'
|
||
|
|
];
|
||
|
|
|
||
|
|
self.addEventListener('install', (event) => {
|
||
|
|
event.waitUntil(
|
||
|
|
// Per-URL (not atomic addAll): one missing/404 entry must not reject the whole batch and
|
||
|
|
// leave the new shell cache empty — otherwise a hard-offline navigation right after a
|
||
|
|
// version bump would fall through to the offline page. allSettled caches what it can.
|
||
|
|
caches.open(SHELL_CACHE)
|
||
|
|
.then((c) => Promise.allSettled(SHELL.map((u) => c.add(u))))
|
||
|
|
.catch(() => {})
|
||
|
|
);
|
||
|
|
self.skipWaiting(); // activate the new worker on next load
|
||
|
|
});
|
||
|
|
|
||
|
|
self.addEventListener('activate', (event) => {
|
||
|
|
event.waitUntil((async () => {
|
||
|
|
const keys = await caches.keys();
|
||
|
|
await Promise.all(keys.filter((k) => k !== SHELL_CACHE).map((k) => caches.delete(k)));
|
||
|
|
await self.clients.claim();
|
||
|
|
})());
|
||
|
|
});
|
||
|
|
|
||
|
|
const OFFLINE_HTML =
|
||
|
|
'<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Offline</title><style>body{font-family:-apple-system,Segoe UI,sans-serif;background:#1a1a1a;color:#e0e0e0;display:flex;align-items:center;justify-content:center;height:100vh;margin:0;text-align:center;padding:20px;}h1{color:#3498db;margin-bottom:8px;}p{color:#b0b0b0;max-width:380px;}</style></head><body><div><h1>Offline</h1><p>The checklist needs an internet connection the first time. Try again when you\'re back online.</p></div></body></html>';
|
||
|
|
|
||
|
|
self.addEventListener('fetch', (event) => {
|
||
|
|
const req = event.request;
|
||
|
|
if (req.method !== 'GET') return;
|
||
|
|
const url = new URL(req.url);
|
||
|
|
if (url.origin !== self.location.origin) return; // third-party: passthrough
|
||
|
|
if (url.pathname.startsWith('/api/')) return; // never cache API; client handles offline
|
||
|
|
// Full-resolution equipment photos can be several MB each and pile up fast; caching them
|
||
|
|
// in the shell risks blowing the (small, all-or-nothing) iOS cache quota and evicting the
|
||
|
|
// app shell. They already carry an immutable 1-year HTTP cache header, so let the browser's
|
||
|
|
// HTTP cache handle them and keep them out of the shell cache entirely.
|
||
|
|
if (url.pathname.startsWith('/equip-images/')) return;
|
||
|
|
|
||
|
|
event.respondWith((async () => {
|
||
|
|
try {
|
||
|
|
const fresh = await fetch(req);
|
||
|
|
// Cache successful shell/static GETs for offline fallback.
|
||
|
|
if (fresh && fresh.ok && fresh.type === 'basic') {
|
||
|
|
const cache = await caches.open(SHELL_CACHE);
|
||
|
|
cache.put(req, fresh.clone());
|
||
|
|
}
|
||
|
|
return fresh;
|
||
|
|
} catch (_) {
|
||
|
|
const cached = await caches.match(req);
|
||
|
|
if (cached) return cached;
|
||
|
|
if (req.mode === 'navigate') {
|
||
|
|
const idx = (await caches.match('/index.html')) || (await caches.match('/'));
|
||
|
|
if (idx) return idx;
|
||
|
|
return new Response(OFFLINE_HTML, { headers: { 'Content-Type': 'text/html; charset=utf-8' } });
|
||
|
|
}
|
||
|
|
throw new Error('Offline');
|
||
|
|
}
|
||
|
|
})());
|
||
|
|
});
|