diff --git a/src/service-worker.ts b/src/service-worker.ts index acd0f71..3aaf9ac 100644 --- a/src/service-worker.ts +++ b/src/service-worker.ts @@ -1,18 +1,62 @@ /// import { build, files, version } from '$service-worker'; -// Create a unique cache name for this deployment -const CACHE = `cache-${version}`; +// 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(CACHE); + const cache = await caches.open(STATIC_CACHE); await cache.addAll(ASSETS); } @@ -23,7 +67,9 @@ self.addEventListener('activate', (event) => { // Remove previous cached data from disk async function deleteOldCaches() { for (const key of await caches.keys()) { - if (key !== CACHE) await caches.delete(key); + if (![STATIC_CACHE, SUPABASE_CACHE, DYNAMIC_CACHE].includes(key)) { + await caches.delete(key); + } } } @@ -31,53 +77,121 @@ self.addEventListener('activate', (event) => { }); self.addEventListener('fetch', (event) => { - // ignore POST requests etc + // 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 - if (url.pathname.startsWith('/auth/')) { + // Skip caching for auth routes and auth requests + if (url.pathname.startsWith('/auth/') || url.pathname.includes('auth/v1')) { return fetch(event.request); } - const cache = await caches.open(CACHE); - - // `build`/`files` can always be served from the cache + // Handle static assets if (ASSETS.includes(url.pathname)) { - const response = await cache.match(url.pathname); - - if (response) { - return response; + const staticCache = await caches.open(STATIC_CACHE); + const cachedResponse = await staticCache.match(url.pathname); + + if (cachedResponse) { + return cachedResponse; } } - - // for everything else, try the network first, but - // fall back to the cache if we're offline + + // 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 we're offline, fetch can return a value that is not a Response - // instead of throwing - and we can't pass this non-Response to respondWith - if (!(response instanceof Response)) { - throw new Error('invalid response from fetch'); - } - + if (response.status === 200) { - cache.put(event.request, response.clone()); + dynamicCache.put(event.request, response.clone()); } - + return response; } catch (err) { - const response = await cache.match(event.request); - - if (response) { - return response; + const cachedResponse = await dynamicCache.match(event.request); + + if (cachedResponse) { + return cachedResponse; } - - // if there's no cache, then just error out - // as there is nothing we can do to respond to this request + throw err; } }