// Service Worker para PWA // Cache First Strategy para funcionamiento offline // Versión del cache - Incrementar cuando hay cambios importantes const CACHE_VERSION = 'v1.0.1'; const CACHE_NAME = `emerges-tes-${CACHE_VERSION}`; const RUNTIME_CACHE = `emerges-tes-runtime-${CACHE_VERSION}`; // Detectar base path dinámicamente (para GitHub Pages) const BASE_PATH = self.location.pathname.split('/').slice(0, -1).join('/') || '/'; const normalizePath = (path) => { if (path.startsWith('/')) { return BASE_PATH === '/' ? path : `${BASE_PATH}${path}`; } return `${BASE_PATH}/${path}`; }; // Archivos estáticos a cachear en la instalación const STATIC_ASSETS = [ normalizePath('/'), normalizePath('/index.html'), normalizePath('/manifest.json'), normalizePath('/favicon.ico'), // Nota: Las imágenes en /assets/infografias/ se cachearán automáticamente // cuando se soliciten (cache-first strategy para imágenes) ]; // Instalación del Service Worker self.addEventListener('install', (event) => { console.log('[SW] Installing service worker...'); event.waitUntil( caches.open(CACHE_NAME) .then((cache) => { console.log('[SW] Caching static assets'); return cache.addAll(STATIC_ASSETS); }) .then(() => self.skipWaiting()) // Activar inmediatamente ); }); // Activación del Service Worker self.addEventListener('activate', (event) => { console.log('[SW] Activating service worker...'); event.waitUntil( caches.keys().then((cacheNames) => { return Promise.all( cacheNames .filter((cacheName) => { // Eliminar caches antiguos return cacheName !== CACHE_NAME && cacheName !== RUNTIME_CACHE; }) .map((cacheName) => { console.log('[SW] Deleting old cache:', cacheName); return caches.delete(cacheName); }) ); }) .then(() => self.clients.claim()) // Tomar control de todas las páginas ); }); // Interceptar peticiones self.addEventListener('fetch', (event) => { const { request } = event; const url = new URL(request.url); // Ignorar peticiones no GET if (request.method !== 'GET') { return; } // Ignorar peticiones a APIs externas (si las hay) if (url.origin !== location.origin) { return; } // Estrategia: Cache First para assets estáticos if ( request.destination === 'script' || request.destination === 'style' || request.destination === 'image' || request.destination === 'font' || url.pathname.endsWith('.md') || url.pathname.includes('/assets/infografias/') || url.pathname.includes('/assets/') ) { event.respondWith(cacheFirst(request)); } else { // Network First para HTML y otros event.respondWith(networkFirst(request)); } }); // Cache First Strategy async function cacheFirst(request) { const cache = await caches.open(CACHE_NAME); const cached = await cache.match(request); if (cached) { return cached; } try { const response = await fetch(request); if (response.ok) { cache.put(request, response.clone()); } return response; } catch (error) { console.error('[SW] Fetch failed:', error); // Si es una imagen, retornar una imagen placeholder if (request.destination === 'image') { return new Response('', { status: 404 }); } throw error; } } // Network First Strategy async function networkFirst(request) { const cache = await caches.open(RUNTIME_CACHE); try { const response = await fetch(request); if (response.ok) { cache.put(request, response.clone()); } return response; } catch (error) { console.log('[SW] Network failed, trying cache:', error); const cached = await cache.match(request); if (cached) { return cached; } // Si no hay cache y estamos offline, retornar index.html para SPA if (request.mode === 'navigate') { const indexCache = await caches.open(CACHE_NAME); const indexHtml = await indexCache.match(normalizePath('/index.html')); if (indexHtml) { return indexHtml; } } throw error; } } // Manejar mensajes del cliente self.addEventListener('message', (event) => { if (event.data && event.data.type === 'SKIP_WAITING') { self.skipWaiting(); } if (event.data && event.data.type === 'CACHE_URLS') { event.waitUntil( caches.open(CACHE_NAME).then((cache) => { return cache.addAll(event.data.urls); }) ); } });