Spaces:
Running
Running
| // Service worker TEMPLATE — deploy_pages.mjs stamps e2d0257 and | |
| // ["/","/about.html","/board.html","/manifest.webmanifest","/icon-192.png","/assets/main-sLgfbjfE.js","/assets/modulepreload-polyfill-B5Qt9EMX.js","/assets/preload-helper-BSrbm-kb.js","/assets/models-DPWHLyww.js","/assets/main-CNSay7C8.css","/assets/about-BRw9WSHl.css","/assets/board-sIXKcNqU.js","/assets/board-DFM1bO4t.css","/assets/playfair-vietnamese-Cabi7G8-.woff2","/assets/playfair-latin-BOwq7MWX.woff2"] and writes the result to dist/sw.js. Never served in dev. | |
| // | |
| // Cache layers (all same-origin GET only): | |
| // moxhi-shell-<build> app shell: precached index + hashed /assets/* | |
| // (cache-first — names are content-hashed), replaced | |
| // wholesale on each deploy. | |
| // moxhi-data-v1 small mutable files (/model/*.json, weights | |
| // manifest.json, manifest.webmanifest, icons): | |
| // network-first, cache is the offline fallback. | |
| // weights shards (.bin) are NOT touched here — src/engine/weights.js owns | |
| // them in its own Cache Storage bucket (content-addressed names). | |
| /* global self, caches, fetch, location */ | |
| // 20260830092302 (yyyymmddHHMMSS, stamped by deploy_pages.mjs) makes shell | |
| // generations SORTABLE by name — activate keeps the newest previous | |
| // generation instead of guessing which cache an old tab still needs. | |
| const SHELL = 'moxhi-shell-20260830092302-e2d0257'; | |
| const DATA = 'moxhi-data-v1'; | |
| const PRECACHE = ["/","/about.html","/board.html","/manifest.webmanifest","/icon-192.png","/assets/main-sLgfbjfE.js","/assets/modulepreload-polyfill-B5Qt9EMX.js","/assets/preload-helper-BSrbm-kb.js","/assets/models-DPWHLyww.js","/assets/main-CNSay7C8.css","/assets/about-BRw9WSHl.css","/assets/board-sIXKcNqU.js","/assets/board-DFM1bO4t.css","/assets/playfair-vietnamese-Cabi7G8-.woff2","/assets/playfair-latin-BOwq7MWX.woff2"]; | |
| // Model/data files the app fetches during startup — often BEFORE this SW has | |
| // claimed the page on its very first visit, so runtime caching would miss | |
| // them and the first offline reload would fail engine init. Precache them | |
| // here (best-effort; /model/* is immutable-cached so this is usually free). | |
| const DATA_PRECACHE = ["/weights/manifest.json","/model/moxhi/tokenizer.json?v=v4.0.3","/model/moxhi/tokenizer_config.json?v=v4.0.3","/weights/hachimi60/manifest.json","/model/hachimi60/tokenizer.json?v=v4.0.3","/model/hachimi60/tokenizer_config.json?v=v4.0.3"]; | |
| // cache:'no-cache' on install + network-first fetches: go to the server | |
| // (ETag revalidation) instead of reading through the browser HTTP cache. | |
| // Without it, /model/*.json — immutable-cached for a year under a FIXED | |
| // name — stayed "fresh" locally, so install and networkFirst both kept | |
| // re-serving the pre-audit tokenizer through every deploy. | |
| const REVALIDATE = { cache: 'no-cache' }; | |
| self.addEventListener('install', (event) => { | |
| event.waitUntil((async () => { | |
| const shell = await caches.open(SHELL); | |
| // Not addAll: Pages 308-redirects pretty URLs (/about.html -> /about), | |
| // and a cached redirected:true response is rejected when served to a | |
| // navigation. Re-wrap each response clean; Promise.all keeps install | |
| // all-or-nothing like addAll did. | |
| await Promise.all(PRECACHE.map(async (url) => { | |
| const res = await fetch(url, REVALIDATE); | |
| if (!res.ok) throw new Error(`precache ${url}: ${res.status}`); | |
| const body = await res.blob(); | |
| await shell.put(url, new Response(body, { status: 200, headers: res.headers })); | |
| })); | |
| const data = await caches.open(DATA); | |
| await Promise.allSettled(DATA_PRECACHE.map(async (url) => { | |
| const res = await fetch(url, REVALIDATE); | |
| if (res.ok) await data.put(url, res); | |
| })); | |
| // Activate only after the all-or-nothing shell precache succeeds. This | |
| // also lets clients escape an older worker whose HF root navigation was | |
| // stranded by the host's / -> /index.html redirect. | |
| await self.skipWaiting(); | |
| })()); | |
| }); | |
| self.addEventListener('activate', (event) => { | |
| event.waitUntil((async () => { | |
| // Keep the newest PREVIOUS shell generation alongside the current one. | |
| // skipWaiting + claim means tabs running the old build are now controlled | |
| // by THIS worker — their lazy chunks (entity panel, hanviet/names data) | |
| // carry old hashes the server no longer serves after a deploy, so wiping | |
| // every old cache turned the first panel-open after a deploy into a | |
| // ChunkLoadError. Generations older than one are deleted; legacy | |
| // un-timestamped caches sort oldest. | |
| const gen = (k) => k.match(/^moxhi-shell-(\d{14})-/)?.[1] ?? ''; | |
| const shells = (await caches.keys()) | |
| .filter((k) => k.startsWith('moxhi-shell-') && k !== SHELL) | |
| .sort((a, b) => gen(b).localeCompare(gen(a))); | |
| for (const key of shells.slice(1)) await caches.delete(key); | |
| await self.clients.claim(); | |
| })()); | |
| }); | |
| // ignoreVary everywhere: entries are keyed by content-hashed (or | |
| // version-checked) same-origin URLs, but servers may stamp `Vary: Origin` | |
| // (vite preview does) — and module/CSS requests carry an Origin header while | |
| // install-time fetches don't, so honoring Vary would miss every hit offline. | |
| const MATCH_OPTS = { ignoreVary: true }; | |
| // An old tab's lazy import after a deploy: its hashed chunk is not in the | |
| // CURRENT shell and 404s on the server — but the previous generation kept by | |
| // activate may still hold it. | |
| async function matchOlderShell(req) { | |
| for (const key of await caches.keys()) { | |
| if (!key.startsWith('moxhi-shell-') || key === SHELL) continue; | |
| const hit = await (await caches.open(key)).match(req, MATCH_OPTS); | |
| if (hit) return hit; | |
| } | |
| return null; | |
| } | |
| async function cacheFirst(req) { | |
| const cache = await caches.open(SHELL); | |
| const hit = await cache.match(req, MATCH_OPTS); | |
| if (hit) return hit; | |
| try { | |
| const res = await fetch(req); | |
| if (res.ok) { | |
| await cache.put(req, res.clone()); | |
| return res; | |
| } | |
| return (await matchOlderShell(req)) ?? res; | |
| } catch (err) { | |
| const stale = await matchOlderShell(req); | |
| if (stale) return stale; | |
| throw err; | |
| } | |
| } | |
| async function networkFirst(req, cacheName, fallbackUrl) { | |
| const cache = await caches.open(cacheName); | |
| // Pages serves pretty URLs for their .html files (308) — an offline | |
| // navigation to the pretty URL must still find the precached .html key. | |
| const cachedFallback = async () => { | |
| const htmlAlias = `${new URL(req.url).pathname.replace(/\/$/, '')}.html`; | |
| return (await cache.match(req, MATCH_OPTS)) | |
| ?? (await cache.match(htmlAlias, MATCH_OPTS)) | |
| ?? (fallbackUrl ? await cache.match(fallbackUrl, MATCH_OPTS) : null) | |
| ?? null; | |
| }; | |
| try { | |
| // fetch(req, init) throws on navigate-mode Requests — pass the URL then. | |
| let res = await fetch(req.mode === 'navigate' ? req.url : req, REVALIDATE); | |
| // HF Static Spaces redirects / to /index.html. A service worker cannot | |
| // return that redirected Response to a controlled navigation: Chrome | |
| // rejects it with net::ERR_FAILED. Re-wrap the final 200 body so the | |
| // response is no longer marked redirected before caching/returning it. | |
| if (req.mode === 'navigate' && res.redirected) { | |
| const body = await res.blob(); | |
| res = new Response(body, { | |
| status: res.status, | |
| statusText: res.statusText, | |
| headers: res.headers, | |
| }); | |
| } | |
| if (res.ok) { | |
| await cache.put(req, res.clone()); | |
| return res; | |
| } | |
| // 5xx (origin hiccup): a stale copy beats an error page. 404 falls | |
| // through the same way — a miss in cache just returns the response. | |
| return (await cachedFallback()) ?? res; | |
| } catch (err) { | |
| const hit = await cachedFallback(); | |
| if (hit) return hit; | |
| throw err; | |
| } | |
| } | |
| self.addEventListener('fetch', (event) => { | |
| const req = event.request; | |
| if (req.method !== 'GET') return; | |
| const url = new URL(req.url); | |
| if (url.origin !== location.origin) return; | |
| // API responses must never come from cache: a cached /api/board silently | |
| // serves a stale leaderboard offline, and POSTs are non-GET anyway. | |
| if (url.pathname.startsWith('/api/')) return; | |
| if (url.pathname.endsWith('.bin')) return; // weights loader's own cache | |
| if (req.mode === 'navigate') { | |
| event.respondWith(networkFirst(req, SHELL, '/')); | |
| } else if (url.pathname.startsWith('/assets/') || PRECACHE.includes(url.pathname)) { | |
| // PRECACHE membership routes to the SHELL cache the install actually | |
| // filled — manifest.webmanifest/icon were precached there but previously | |
| // served through the data cache, so the first offline load missed them. | |
| // (Navigations match the branch above; this only carries subresources.) | |
| event.respondWith(cacheFirst(req)); | |
| } else { | |
| event.respondWith(networkFirst(req, DATA)); | |
| } | |
| }); | |