const CACHE_NAME = 'whisky-vault-v10-bunker'; // Professional Bunker v10 // CONFIG: Core Pages & Static Assets const CORE_PAGES = [ '/', ]; const STATIC_ASSETS = [ '/manifest.webmanifest', '/icon-192.png', '/icon-512.png', '/favicon.ico', ]; // Helper: Broadcast to all clients (including those not yet controlled) async function broadcast(message) { try { const clients = await self.clients.matchAll({ includeUncontrolled: true, type: 'window' }); clients.forEach(client => client.postMessage(message)); } catch (e) { // Silently fail if no clients found } } // Helper: Fetch with Timeout & AbortController async function fetchWithTimeout(url, timeoutMs = 15000) { const controller = new AbortController(); const id = setTimeout(() => controller.abort(), timeoutMs); try { const response = await fetch(url, { signal: controller.signal, cache: 'no-store' // Ensure we get fresh bits for the bunker }); clearTimeout(id); return response; } catch (e) { clearTimeout(id); throw e; } } // ๐Ÿ—๏ธ INSTALL: Build the Bunker self.addEventListener('install', (event) => { // Immediate takeover self.skipWaiting(); event.waitUntil( caches.open(CACHE_NAME).then(async (cache) => { console.log('๐Ÿ—๏ธ PWA: Building Bunker v10...'); const items = [...CORE_PAGES, ...STATIC_ASSETS]; const total = items.length; let loaded = 0; // Sequential loading to avoid concurrent fetch limits on mobile for (const url of items) { try { const res = await fetchWithTimeout(url); if (res && res.ok) { await cache.put(url, res); } } catch (error) { console.error(`โš ๏ธ PWA: Pre-cache failed for ${url}:`, error); } finally { loaded++; broadcast({ type: 'PRECACHE_PROGRESS', progress: Math.round((loaded / total) * 100) }); } } console.log('โœ… PWA: Bunker build finished'); broadcast({ type: 'PRECACHE_COMPLETE', version: CACHE_NAME }); }) ); }); // ๐Ÿงน ACTIVATE: Cleanup old bunkers self.addEventListener('activate', (event) => { event.waitUntil( caches.keys().then((cacheNames) => { return Promise.all( cacheNames.map((cacheName) => { if (cacheName !== CACHE_NAME) { console.log('๐Ÿงน PWA: Clearing old bunker', cacheName); return caches.delete(cacheName); } }) ); }) ); self.clients.claim(); }); // ๐Ÿ’ฌ MESSAGE: Handle status checks self.addEventListener('message', (event) => { if (event.data?.type === 'CHECK_BUNKER_STATUS') { event.source.postMessage({ type: 'BUNKER_STATUS', isReady: true, version: CACHE_NAME }); } }); // ๐Ÿš€ FETCH: Bunker Strategy self.addEventListener('fetch', (event) => { if (event.request.method !== 'GET') return; const url = new URL(event.request.url); // 0. BYPASS for Auth/API/Supabase if (url.pathname.includes('/auth/') || url.pathname.includes('/api/') || url.hostname.includes('supabase.co')) { return; } // 1. RSC DATA (_next/data): Bunker with JSON Fallback if (url.pathname.startsWith('/_next/data/')) { event.respondWith( caches.match(event.request).then((cached) => { if (cached) return cached; return fetchWithTimeout(event.request, 3000) .catch(() => new Response(JSON.stringify({}), { headers: { 'Content-Type': 'application/json' } })); }) ); return; } // 2. NAVIGATION & ASSETS: Bunker + SWR const isNavigation = event.request.mode === 'navigate'; const isAsset = event.request.destination === 'style' || event.request.destination === 'script' || event.request.destination === 'worker' || event.request.destination === 'font' || event.request.destination === 'image' || url.pathname.startsWith('/_next/static'); if (isNavigation || isAsset) { event.respondWith( caches.match(event.request).then(async (cachedResponse) => { // Background update const fetchPromise = fetchWithTimeout(event.request, 8000) .then(async (networkResponse) => { if (networkResponse && networkResponse.status === 200) { const cache = await caches.open(CACHE_NAME); cache.put(event.request, networkResponse.clone()); } return networkResponse; }) .catch(() => { /* Background fail silent */ }); if (isNavigation) { if (cachedResponse) return cachedResponse; const shell = await caches.match('/'); if (shell) return shell; } return cachedResponse || fetchPromise || fetch(event.request); }) ); } });