const CONTENT_CACHE_NAME = "healer-man-content-v1"; const BOOTSTRAP_CACHE_NAME = "healer-man-bootstrap-v1"; const OBJECT_HEADER = "x-healer-man-content-object"; const ENCODING_HEADER = "x-healer-man-content-encoding"; let bundledManifest; function mimeType(path) { const extension = path.split(".").pop(); return ({ glb: "model/gltf-binary", json: "application/json", js: "text/javascript", wasm: "application/wasm", ktx2: "image/ktx2", png: "image/png", webp: "image/webp", avif: "image/avif", svg: "image/svg+xml", mp3: "audio/mpeg", ogg: "audio/ogg", wav: "audio/wav", webm: "video/webm" })[extension] ?? "application/octet-stream"; } async function decodedResponse(response, path, encoding) { if (encoding !== "gzip") return response; const headers = new Headers(response.headers); headers.set("content-type", mimeType(path)); headers.delete("content-length"); headers.delete("content-encoding"); headers.delete(ENCODING_HEADER); return new Response(response.body.pipeThrough(new DecompressionStream("gzip")), { status: 200, headers }); } async function bundledAsset(path) { if (!bundledManifest) { bundledManifest = fetch("/content/bootstrap-manifest.json", { cache: "no-cache" }) .then((response) => response.ok ? response.json() : null) .catch(() => null); } const manifest = await bundledManifest; if (!manifest) { bundledManifest = null; return null; } const entry = manifest.files?.[path]; if (!entry) return null; const object = entry.download ?? entry; // Older bootstrap catalogs served their original logical paths directly. if (!object.url.startsWith("/content/bundled/")) return fetch(path); const bootstrapCache = await caches.open(BOOTSTRAP_CACHE_NAME); const objectKey = new URL(object.url, self.location.origin).toString(); let response = await bootstrapCache.match(objectKey); if (!response) { response = await fetch(object.url); if (response.ok) await bootstrapCache.put(objectKey, response.clone()); } if (!response.ok) return response; if (entry.download) return decodedResponse(response, path, entry.download.encoding); return new Response(response.body, { headers: { "content-type": mimeType(path) } }); } const MANAGED_ASSET_PREFIXES = [ "/assets/characters/", "/assets/creatures/", "/assets/equipment/", "/assets/game/", "/assets/ui/", "/basis/", "/data/", ]; const BUNDLED_ASSET_PREFIXES = [ "/assets/ui/", "/assets/equipment/", "/assets/characters/wow335a-v1/manifest.json", "/assets/characters/wow335a-v1/human/", "/assets/creatures/wailing-caverns/", "/assets/game/dungeons/wailing-caverns/", ]; const BUNDLED_ASSET_PATHS = new Set([ "/assets/characters/wow335a-v1/textures/character/human/faciallowerhair00_00.webp", "/assets/characters/wow335a-v1/textures/character/human/facialupperhair00_00.webp", "/assets/characters/wow335a-v1/textures/character/human/female/humanfemalefacelower00_00.webp", "/assets/characters/wow335a-v1/textures/character/human/female/humanfemalefaceupper00_00.webp", "/assets/characters/wow335a-v1/textures/character/human/female/humanfemalenakedpelvisskin00_00.webp", "/assets/characters/wow335a-v1/textures/character/human/female/humanfemalenakedtorsoskin00_00.webp", "/assets/characters/wow335a-v1/textures/character/human/female/humanfemaleskin00_00.webp", "/assets/characters/wow335a-v1/textures/character/human/hair00_00.webp", "/assets/characters/wow335a-v1/textures/character/human/male/humanmalefacelower00_00.webp", "/assets/characters/wow335a-v1/textures/character/human/male/humanmalefaceupper00_00.webp", "/assets/characters/wow335a-v1/textures/character/human/male/humanmalenakedpelvisskin00_00.webp", "/assets/characters/wow335a-v1/textures/character/human/male/humanmaleskin00_00.webp", "/assets/characters/wow335a-v1/textures/character/human/scalplowerhair00_00.webp", "/assets/characters/wow335a-v1/textures/character/human/scalpupperhair00_00.webp", ]); self.addEventListener("install", (event) => { event.waitUntil(self.skipWaiting()); }); self.addEventListener("message", (event) => { if (event.data?.type === "refresh-content-bootstrap") bundledManifest = null; }); self.addEventListener("activate", (event) => { event.waitUntil((async () => { const names = await caches.keys(); await Promise.all(names .filter((name) => name.startsWith(`${CONTENT_CACHE_NAME}-staging-`) || (name.startsWith("healer-man-bootstrap-") && name !== BOOTSTRAP_CACHE_NAME)) .map((name) => caches.delete(name))); await self.clients.claim(); })()); }); self.addEventListener("fetch", (event) => { const { request } = event; if (request.method !== "GET") return; const url = new URL(request.url); if (url.origin !== self.location.origin || !MANAGED_ASSET_PREFIXES.some((prefix) => url.pathname.startsWith(prefix))) return; event.respondWith((async () => { const cache = await caches.open(CONTENT_CACHE_NAME); const cached = await cache.match(request, { ignoreSearch: true }); if (cached) { const objectPath = cached.headers.get(OBJECT_HEADER); if (!objectPath) return cached; if (objectPath.startsWith("/content/cache-objects/")) { const object = await cache.match(new URL(objectPath, self.location.origin).toString()); if (object) return decodedResponse(object, url.pathname, object.headers.get(ENCODING_HEADER)); } } const bundled = await bundledAsset(url.pathname); if (bundled) return bundled; if (BUNDLED_ASSET_PATHS.has(url.pathname) || BUNDLED_ASSET_PREFIXES.some((prefix) => url.pathname.startsWith(prefix))) { return fetch(request); } return new Response("Optional content is not installed.", { status: 404, headers: { "content-type": "text/plain; charset=utf-8" }, }); })()); });