/** * Service Worker — Italiano Vivo * Estratégia: * - Shell (HTML/CSS/JS do app): cache-first * - /content/* e /audio/*: cache-first com network fallback + runtime caching * - /api/*: network-only (nunca cachear) * * Objetivo: modo resgate offline — frases + áudio já baixados funcionam no avião. */ const CACHE_NAME = "italiano-vivo-v2"; const SHELL_ASSETS = ["/", "/index.html"]; // ─── Install: pré-cache do shell ─── self.addEventListener("install", (event) => { event.waitUntil( caches.open(CACHE_NAME).then((cache) => cache.addAll(SHELL_ASSETS)), ); self.skipWaiting(); }); // ─── Activate: limpar caches antigos ─── self.addEventListener("activate", (event) => { event.waitUntil( caches.keys().then((keys) => Promise.all( keys .filter((k) => k !== CACHE_NAME) .map((k) => caches.delete(k)), ), ), ); self.clients.claim(); }); // ─── Fetch: roteamento por padrão de URL ─── self.addEventListener("fetch", (event) => { const url = new URL(event.request.url); const pathname = url.pathname; // API: sempre network-only if (pathname.startsWith("/api/")) { return; // deixa o browser fazer o fetch normal } // Content e Audio: cache-first com network fallback + runtime caching if (pathname.startsWith("/content/") || pathname.startsWith("/audio/")) { event.respondWith(cacheFirstWithRuntimeCache(event.request)); return; } // Shell e demais assets: cache-first event.respondWith(cacheFirst(event.request)); }); /** * Network-first para o shell (HTML): sempre busca a versao mais nova e * atualiza o cache em background. Evita o app ficar preso numa build antiga * apos deploy. Assets versionados (/assets/*-hash) podem ser cache-first * porque o nome muda a cada build. */ async function cacheFirst(request) { const url = new URL(request.url); const isShellHtml = url.pathname === "/" || url.pathname === "/index.html" || url.pathname === "/sw.js"; if (isShellHtml) { try { const fresh = await fetch(request); if (fresh.ok) { const cache = await caches.open(CACHE_NAME); cache.put(request, fresh.clone()); } return fresh; } catch (_err) { const cached = await caches.match(request); if (cached) return cached; throw _err; } } const cached = await caches.match(request); if (cached) return cached; return fetch(request); } /** * Cache-first com runtime caching: tenta cache, se não achar vai na rede * e salva a resposta no cache para uso offline futuro. * Usado para /content/* e /audio/* que são baixados sob demanda. */ async function cacheFirstWithRuntimeCache(request) { // Network-first para /content/* (JSON de missoes pode ser corrigido a qualquer // momento; cache antigo nao deve esconder o novo). Audio e imutavel -> cache-first. const url = new URL(request.url); if (url.pathname.startsWith("/content/")) { try { const response = await fetch(request); if (response.ok) { const cache = await caches.open(CACHE_NAME); cache.put(request, response.clone()); } return response; } catch (_err) { const cached = await caches.match(request); if (cached) return cached; return new Response("Offline — Italiano Vivo", { status: 503, statusText: "Service Unavailable", headers: { "Content-Type": "text/plain" }, }); } } const cached = await caches.match(request); if (cached) return cached; try { const response = await fetch(request); // Só cachear respostas OK (não cachear 404/500) if (response.ok) { const cache = await caches.open(CACHE_NAME); cache.put(request, response.clone()); } return response; } catch (_err) { // Offline e sem cache: retorna erro genérico return new Response("Offline — Italiano Vivo", { status: 503, statusText: "Service Unavailable", headers: { "Content-Type": "text/plain" }, }); } }