// Service worker: la app abre sin conexión (shell en caché) y los capítulos guardados "en este // dispositivo" (caché OFFLINE, la llena app.js) se leen sin red. const SHELL = 'shell-v1' const OFFLINE = 'offline-v1' const SHELL_FILES = ['/', '/app.js', '/style.css', '/icon.svg', '/manifest.webmanifest', '/icon-192.png', '/icon-512.png'] self.addEventListener('install', e => { e.waitUntil(caches.open(SHELL).then(c => c.addAll(SHELL_FILES)).then(() => self.skipWaiting())) }) self.addEventListener('activate', e => { e.waitUntil(caches.keys() .then(keys => Promise.all(keys.filter(k => k !== SHELL && k !== OFFLINE).map(k => caches.delete(k)))) .then(() => self.clients.claim())) }) // Red primero (así siempre se ve la última versión) y la caché si no hay conexión. Si hay copia guardada // y el servidor no contesta en NET_WAIT ms (red que no llega, servidor caído o colgado), se usa la copia // sin esperar a que el navegador dé la conexión por perdida; la petición sigue y actualiza la caché. const NET_WAIT = 3000 const DOWN_FOR = 20e3 let downUntil = 0 // La app distingue así una copia guardada de una respuesta real del servidor. async function fromCache(res) { const headers = new Headers(res.headers) headers.set('x-from-cache', '1') return new Response(await res.blob(), { status: res.status, statusText: res.statusText, headers }) } async function networkFirst(req, cacheName, { store = true, ignoreSearch = false, fallback } = {}) { const cached = () => caches.match(req, { ignoreSearch, cacheName }).then(hit => hit || (fallback && caches.match(fallback))) const net = fetch(req).then(async res => { if (store && res.ok) { const c = await caches.open(cacheName) if (store === true || (await c.match(req, { ignoreSearch }))) await c.put(req, res.clone()) } return res }) net.then(() => { downUntil = 0 }, () => {}) const hit = await cached() if (!hit) return net // sin copia: solo queda esperar a la red // Si hace poco que el servidor no contestaba, la copia directamente (la red sigue intentándolo). if (Date.now() < downUntil) return fromCache(hit) let timer const timeout = new Promise(r => { timer = setTimeout(() => r(null), NET_WAIT) }) const res = await Promise.race([net.catch(() => null), timeout]) clearTimeout(timer) if (!res) downUntil = Date.now() + DOWN_FOR return res || fromCache(hit) } self.addEventListener('fetch', e => { const req = e.request if (req.method !== 'GET') return const url = new URL(req.url) if (url.origin !== location.origin) return const p = url.pathname if (req.mode === 'navigate') return e.respondWith(networkFirst(req, SHELL, { fallback: '/' })) if (SHELL_FILES.includes(p) || p === '/sw.js') return e.respondWith(networkFirst(req, SHELL)) if (p === '/api/me') return e.respondWith(networkFirst(req, SHELL)) // Ficha y capítulo: solo se guardan si el usuario los guardó (store: 'existing' = refrescar si ya estaban). if (p.startsWith('/api/manga/')) return e.respondWith(networkFirst(req, OFFLINE, { store: 'existing', ignoreSearch: true })) if (p.startsWith('/api/chapter/')) return e.respondWith(networkFirst(req, OFFLINE, { store: false, ignoreSearch: true })) // Imágenes: si están guardadas, sin red (más rápido y sin gastar datos). if (p.startsWith('/api/local/') || p.startsWith('/api/img') || p.startsWith('/api/local-cover/')) { e.respondWith(caches.open(OFFLINE).then(c => c.match(req)).then(hit => hit || fetch(req))) } })