unroll / sw.js
Chase Hendrick
Claude Opus 5.5 (1M context)
Unroll 1.7.0: Live sheet, stream any public segment and volume and see the volume on the sheet
7391956 unverified
Raw History Blame Contribute Delete
5.11 kB
// Unroll service worker: caches the app shell and the default scroll so an installed copy works offline.
// The page registers it as sw.js?v=<version>-<sha> from version.json, so every stamped build gets fresh cache names
// and the caches of older versions are deleted when the new worker activates.
const VERSION = new URL(self.location.href).searchParams.get('v') || 'dev';
const SHELL = `unroll-shell-${VERSION}`;
const DATA = `unroll-data-${VERSION}`;
const RUNTIME = `unroll-runtime-${VERSION}`;
const OURS = [SHELL, DATA, RUNTIME];
const SHELL_FILES = [
'./', 'index.html', 'manifest.webmanifest', 'version.json', 'glossary.json',
'style.css', 'bench.css', 'ui.css',
'app.js', 'polish.js', 'ui.js', 'tear.js', 'damage.js',
'live.html', 'lib/livesheet.js',
'lib/scrolls.js', 'lib/manifest.js', 'lib/openown.js', 'lib/formats.js', 'lib/example.js',
'lib/bench-core.js', 'lib/bench.js', 'lib/bench-worker.js',
'lib/dragres.js', 'lib/ctwl.js', 'lib/ctsource.js', 'lib/ctvolume.js', 'lib/ctlabels.js', 'lib/ctapi.js', 'lib/api.js', 'lib/apispec.js', 'lib/embed.js', 'lib/version.js', 'api.json',
'icons/icon.svg', 'icons/icon-192.png', 'icons/icon-512.png', 'icons/maskable-512.png', 'icons/apple-touch-icon.png',
];
const THREE = 'https://cdn.jsdelivr.net/npm/three@0.160.0/';
const CDN_FILES = [
'build/three.module.js', 'examples/jsm/controls/OrbitControls.js',
'examples/jsm/postprocessing/EffectComposer.js', 'examples/jsm/postprocessing/RenderPass.js',
'examples/jsm/postprocessing/UnrealBloomPass.js', 'examples/jsm/postprocessing/OutputPass.js',
].map((p) => THREE + p);
// the default scroll: the same files index.html preloads
const DATA_FILES = [
'scrolls/index.json', 'scrolls/PHerc0139/manifest.json',
'data/scroll.json', 'data/scroll.i16p.gz', 'data/atlas_ink.webp', 'data/atlas_ct.webp',
];
async function addAll(name, urls, opts) {
const c = await caches.open(name);
await Promise.all(urls.map(async (u) => {
try { const r = await fetch(new Request(u, opts)); if (r.ok || r.type === 'opaque') await c.put(u, r); }
catch (e) { /* one missing file must not block the install; it is fetched on first use instead */ }
}));
}
self.addEventListener('install', (e) => {
e.waitUntil((async () => {
const shell = await caches.open(SHELL);
await shell.addAll(SHELL_FILES.map((u) => new Request(u, { cache: 'reload' })));
await addAll(SHELL, CDN_FILES, { mode: 'cors' });
await addAll(DATA, DATA_FILES, { cache: 'reload' });
await self.skipWaiting();
})());
});
self.addEventListener('activate', (e) => {
e.waitUntil((async () => {
for (const k of await caches.keys()) if (k.startsWith('unroll-') && !OURS.includes(k)) await caches.delete(k);
await self.clients.claim();
})());
});
self.addEventListener('message', (e) => { if (e.data === 'version') e.source?.postMessage({ type: 'unroll-sw', version: VERSION }); });
const isData = (url) => /\/(data|scrolls)\//.test(url.pathname);
async function fromCache(req) {
// exact match only: a new release asks for file.js?v=<new>, and answering that with the old file.js?v=<old> mixes
// versions on the first load after an update (an old app.js against a new index.html)
return caches.match(req);
}
// offline fallback: any cached copy of the same path, whatever its ?v=
const anyCopy = (req) => caches.match(req, { ignoreSearch: true });
self.addEventListener('fetch', (e) => {
const req = e.request;
if (req.method !== 'GET' || req.headers.has('range')) return;
const url = new URL(req.url);
const same = url.origin === self.location.origin;
const cdn = url.href.startsWith(THREE) || url.hostname === 'fonts.googleapis.com' || url.hostname === 'fonts.gstatic.com';
if (!same && !cdn) return;
if (req.mode === 'navigate') {
// network first so a new release shows up at once; the cached page when offline
e.respondWith(fetch(req).then((r) => { if (r.ok) caches.open(SHELL).then((c) => c.put('index.html', r.clone())); return r; })
.catch(async () => (await caches.match('index.html')) || (await caches.match('./')) || Response.error()));
return;
}
if (same && isData(url)) {
// scroll data never changes under one version: cache first, keep what was viewed for offline use
e.respondWith(fromCache(req).then((hit) => hit || fetch(req).then((r) => {
if (r.ok && r.status === 200) { const copy = r.clone(); caches.open(url.pathname.includes('/data/hi/') || !DATA_FILES.some((f) => url.pathname.endsWith(f)) ? RUNTIME : DATA).then((c) => c.put(req, copy)); }
return r;
}).catch(async () => (await anyCopy(req)) || Response.error())));
return;
}
// shell, three.js and fonts: stale while revalidate
e.respondWith(fromCache(req).then((hit) => {
const net = fetch(req).then((r) => {
if (r.ok || r.type === 'opaque') { const copy = r.clone(); caches.open(same ? SHELL : RUNTIME).then((c) => c.put(req, copy)); }
return r;
}).catch(async () => hit || (await anyCopy(req)) || Response.error());
if (hit) { e.waitUntil(net.catch(() => {})); return hit; }
return net;
}));
});