// Service Worker SAR // C4/NFR-2: cache de API para uso offline (network-first, fallback to cache) // C6: Web Push // App shell: stale-while-revalidate para assets estáticos const API_CACHE = 'sar-api-v2'; const SHELL_CACHE = 'sar-shell-v2'; // Paths de API que valem ser cacheados para offline // Auth e mutations (POST/PATCH) nunca são interceptados const CACHEABLE_API = [ '/api/v1/clients', '/api/v1/catalog', '/api/v1/orders', '/api/v1/dashboard', '/api/v1/auth/me', ]; // ── Fetch ────────────────────────────────────────────────────────────────────── self.addEventListener('fetch', (event) => { const { request } = event; if (request.method !== 'GET') return; const url = new URL(request.url); if (url.pathname.startsWith('/api/v1/')) { const cacheable = CACHEABLE_API.some((p) => url.pathname.startsWith(p)); if (cacheable) { event.respondWith(networkFirst(request, API_CACHE)); } return; } if (request.mode === 'navigate') { // App shell HTML — network first, cache fallback event.respondWith(shellNavigate(request)); return; } // Assets estáticos (JS/CSS/fontes/imagens) — stale-while-revalidate if (/\.(js|css|woff2?|png|svg|ico)$/.test(url.pathname)) { event.respondWith(staleWhileRevalidate(request, SHELL_CACHE)); } }); // Network-first: tenta rede, cai no cache se offline async function networkFirst(request, cacheName) { const cache = await caches.open(cacheName); try { const response = await fetch(request); if (response.ok) { cache.put(request, response.clone()); } return response; } catch { const cached = await cache.match(request); if (cached) return cached; return offlineResponse(); } } // Navigate: network first; fallback para a raiz cacheada async function shellNavigate(request) { const cache = await caches.open(SHELL_CACHE); try { const response = await fetch(request); if (response.ok) { cache.put(request, response.clone()); // Sempre armazena a raiz como fallback universal cache.put(new Request('/'), response.clone()); } return response; } catch { const cached = (await cache.match(request)) ?? (await cache.match('/')); if (cached) return cached; return offlineResponse(); } } // Stale-while-revalidate: responde do cache, atualiza em background async function staleWhileRevalidate(request, cacheName) { const cache = await caches.open(cacheName); const cached = await cache.match(request); const networkFetch = fetch(request).then((response) => { if (response.ok) cache.put(request, response.clone()); return response; }); return cached ?? networkFetch; } function offlineResponse() { return new Response( JSON.stringify({ type: 'sar:offline', title: 'Sem conexão', status: 503, }), { status: 503, headers: { 'Content-Type': 'application/json' } }, ); } // ── Push (C6) ───────────────────────────────────────────────────────────────── self.addEventListener('push', (event) => { const data = event.data?.json() ?? {}; event.waitUntil( self.registration.showNotification(data.title ?? 'SAR', { body: data.body ?? '', icon: '/sar-icon.png', badge: '/sar-icon.png', data: data.url ? { url: data.url } : undefined, }), ); }); self.addEventListener('notificationclick', (event) => { event.notification.close(); const url = event.notification.data?.url; if (url) { event.waitUntil(clients.openWindow(url)); } });