/// import { build, files, version } from '$service-worker'; // Create unique cache names for this deployment const STATIC_CACHE = `static-cache-${version}`; const SUPABASE_CACHE = `supabase-cache-${version}`; const DYNAMIC_CACHE = `dynamic-cache-${version}`; const ASSETS = [ ...build, // the app itself ...files // everything in `static` ]; // Configure which Supabase endpoints to cache and for how long const SUPABASE_CACHE_CONFIG = [ // Format: { urlPattern: RegExp, maxAgeSeconds: number, strategy: 'cache-first' | 'network-first' | 'stale-while-revalidate' } { urlPattern: /events/, maxAgeSeconds: 3600, strategy: 'stale-while-revalidate' }, { urlPattern: /participants/, maxAgeSeconds: 3600, strategy: 'network-first' }, { urlPattern: /sections/, maxAgeSeconds: 3600, strategy: 'cache-first' }, { urlPattern: /profiles/, maxAgeSeconds: 3600, strategy: 'cache-first' }, ]; // Helper to determine if a request is for Supabase function isSupabaseRequest(url) { return url.hostname.includes('supabase.co'); } // Helper to find matching cache config for a Supabase URL function getSupabaseCacheConfig(url) { const urlString = url.toString(); return SUPABASE_CACHE_CONFIG.find(config => config.urlPattern.test(urlString)); } // Helper to check if cached response is expired function isCacheExpired(cachedResponse, maxAgeSeconds) { if (!cachedResponse) return true; const cachedAt = new Date(cachedResponse.headers.get('sw-cache-timestamp') || 0); const now = new Date(); return (now.getTime() - cachedAt.getTime()) > (maxAgeSeconds * 1000); } // Helper to add timestamp to cached responses function addTimestampToResponse(response) { const clonedResponse = response.clone(); const headers = new Headers(clonedResponse.headers); headers.set('sw-cache-timestamp', new Date().toISOString()); return new Response(clonedResponse.body, { status: clonedResponse.status, statusText: clonedResponse.statusText, headers }); } self.addEventListener('install', (event) => { // Create a new cache and add all files to it async function addFilesToCache() { const cache = await caches.open(STATIC_CACHE); await cache.addAll(ASSETS); } event.waitUntil(addFilesToCache()); }); self.addEventListener('activate', (event) => { // Remove previous cached data from disk async function deleteOldCaches() { for (const key of await caches.keys()) { if (![STATIC_CACHE, SUPABASE_CACHE, DYNAMIC_CACHE].includes(key)) { await caches.delete(key); } } } event.waitUntil(deleteOldCaches()); }); self.addEventListener('fetch', (event) => { // ignore POST requests and other mutations if (event.request.method !== 'GET') return; async function respond() { const url = new URL(event.request.url); // Skip caching for auth routes and auth requests if (url.pathname.startsWith('/auth/') || url.pathname.includes('auth/v1')) { return fetch(event.request); } // Handle static assets if (ASSETS.includes(url.pathname)) { const staticCache = await caches.open(STATIC_CACHE); const cachedResponse = await staticCache.match(url.pathname); if (cachedResponse) { return cachedResponse; } } // Handle Supabase requests with specific caching strategies if (isSupabaseRequest(url)) { const cacheConfig = getSupabaseCacheConfig(url); if (!cacheConfig) { // No specific config, use network-only for unconfigured Supabase endpoints return fetch(event.request); } const supabaseCache = await caches.open(SUPABASE_CACHE); const cachedResponse = await supabaseCache.match(event.request); // Apply the appropriate caching strategy based on config switch (cacheConfig.strategy) { case 'cache-first': { // If we have a valid cache that's not expired, use it if (cachedResponse && !isCacheExpired(cachedResponse, cacheConfig.maxAgeSeconds)) { return cachedResponse; } // Otherwise fetch and update cache try { const response = await fetch(event.request); if (response.ok) { const timestampedResponse = addTimestampToResponse(response); supabaseCache.put(event.request, timestampedResponse.clone()); return timestampedResponse; } return response; } catch (err) { // If offline and we have any cached version, return it even if expired if (cachedResponse) return cachedResponse; throw err; } } case 'stale-while-revalidate': { // Start network fetch const fetchPromise = fetch(event.request) .then(response => { if (response.ok) { const timestampedResponse = addTimestampToResponse(response); supabaseCache.put(event.request, timestampedResponse.clone()); return timestampedResponse; } return response; }) .catch(() => { // If network fails, we'll use cache if available if (cachedResponse) return cachedResponse; throw new Error('Network request failed and no cache available'); }); // Return cached response immediately if available, otherwise wait for network return cachedResponse || fetchPromise; } case 'network-first': default: { try { const response = await fetch(event.request); if (response.ok) { const timestampedResponse = addTimestampToResponse(response); supabaseCache.put(event.request, timestampedResponse.clone()); return timestampedResponse; } return response; } catch (err) { // If offline, try to return cached response if (cachedResponse) return cachedResponse; throw err; } } } } // For everything else, use dynamic cache with network-first strategy const dynamicCache = await caches.open(DYNAMIC_CACHE); try { const response = await fetch(event.request); if (response.status === 200) { dynamicCache.put(event.request, response.clone()); } return response; } catch (err) { const cachedResponse = await dynamicCache.match(event.request); if (cachedResponse) { return cachedResponse; } throw err; } } event.respondWith(respond()); });