ngocdang83's picture
moxhi e2d0257
de6d73b verified
Raw
History Blame Contribute Delete
8.88 kB
// 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));
}
});