1/** 2 * Service Worker for aggressive asset caching 3 * Only active in production environments 4 */ 5 6const CACHE_VERSION = 'v3'; 7const IMAGE_CACHE = `images-${CACHE_VERSION}`; 8const STATIC_CACHE = `static-${CACHE_VERSION}`; 9 10// Explicit production hostnames 11const PRODUCTION_HOSTS = [ 12 'drophighlight.com', 13 'www.drophighlight.com', 14 'drop-highlight.lovable.app' 15]; 16 17// Patterns that indicate non-production environments (take precedence) 18const NON_PRODUCTION_PATTERNS = [ 19 'localhost', 20 '127.0.0.1', 21 '-preview--', // Lovable preview 22 '.pages.dev' // Cloudflare Pages 23]; 24 25/** 26 * Check if hostname is a production environment 27 */ 28function isProductionHost(hostname) { 29 // Non-production patterns take precedence 30 if (NON_PRODUCTION_PATTERNS.some(pattern => hostname.includes(pattern))) { 31 return false; 32 } 33 34 // Check for exact production host or subdomain 35 return PRODUCTION_HOSTS.includes(hostname) || 36 hostname.endsWith('.drophighlight.com'); 37} 38 39/** 40 * Cache-first strategy with expiration 41 */ 42async function cacheFirst(request, cacheName, maxAgeSeconds) { 43 const cache = await caches.open(cacheName); 44 const cached = await cache.match(request); 45 46 if (cached) { 47 const dateHeader = cached.headers.get('sw-cached-at'); 48 if (dateHeader) { 49 const age = Date.now() - parseInt(dateHeader, 10); 50 if (age < maxAgeSeconds * 1000) { 51 return cached; 52 } 53 } 54 } 55 56 try { 57 const response = await fetch(request); 58 if (response.ok) { 59 const responseToCache = response.clone(); 60 const headers = new Headers(responseToCache.headers); 61 headers.set('sw-cached-at', Date.now().toString()); 62 63 const modifiedResponse = new Response(responseToCache.body, { 64 status: responseToCache.status, 65 statusText: responseToCache.statusText, 66 headers 67 }); 68 69 cache.put(request, modifiedResponse); 70 } 71 return response; 72 } catch (error) { 73 // If fetch fails and we have cached version (even expired), use it 74 if (cached) return cached; 75 throw error; 76 } 77} 78 79/** 80 * Stale-while-revalidate strategy 81 */ 82async function staleWhileRevalidate(request, cacheName, maxAgeSeconds) { 83 const cache = await caches.open(cacheName); 84 const cached = await cache.match(request); 85 86 // Start fetch in background 87 const fetchPromise = fetch(request).then(response => { 88 if (response.ok) { 89 const responseToCache = response.clone(); 90 const headers = new Headers(responseToCache.headers); 91 headers.set('sw-cached-at', Date.now().toString()); 92 93 const modifiedResponse = new Response(responseToCache.body, { 94 status: responseToCache.status, 95 statusText: responseToCache.statusText, 96 headers 97 }); 98 99 cache.put(request, modifiedResponse); 100 } 101 return response; 102 }).catch(() => cached); 103 104 // Return cached immediately if available 105 if (cached) { 106 return cached; 107 } 108 109 return fetchPromise; 110} 111 112// Install event - skip waiting to activate immediately 113self.addEventListener('install', () => { 114 self.skipWaiting(); 115}); 116 117// Activate event - claim clients immediately 118self.addEventListener('activate', (event) => { 119 event.waitUntil( 120 Promise.all([ 121 clients.claim(), 122 // Clean old caches 123 caches.keys().then(keys => { 124 return Promise.all( 125 keys 126 .filter(key => !key.includes(CACHE_VERSION)) 127 .map(key => caches.delete(key)) 128 ); 129 }) 130 ]) 131 ); 132}); 133 134// Fetch event - intercept requests 135self.addEventListener('fetch', (event) => { 136 // â ï¸ CRITICAL: Only cache in production 137 if (!isProductionHost(self.location.hostname)) { 138 return; // Bypass - let browser handle fetch normally 139 } 140 141 const url = new URL(event.request.url); 142 143 // Only handle GET requests 144 if (event.request.method !== 'GET') { 145 return; 146 } 147 148 // Supabase Storage images - aggressive cache (7 days) 149 if (url.hostname.includes('supabase.co') && 150 (url.pathname.includes('/thumbnails/') || 151 url.pathname.includes('/covers/') || 152 url.pathname.includes('/content/') || 153 url.pathname.includes('/logos/'))) { 154 event.respondWith(cacheFirst(event.request, IMAGE_CACHE, 7 * 24 * 60 * 60)); 155 return; 156 } 157 158 // Local static images - cache for 1 day 159 if (url.pathname.match(/\.(png|jpg|jpeg|webp|ico|svg|gif)$/) && 160 url.hostname === self.location.hostname) { 161 event.respondWith(cacheFirst(event.request, STATIC_CACHE, 24 * 60 * 60)); 162 return; 163 } 164 165 // Fonts - cache for 30 days 166 if (url.pathname.match(/\.(woff|woff2|ttf|otf)$/)) { 167 event.respondWith(cacheFirst(event.request, STATIC_CACHE, 30 * 24 * 60 * 60)); 168 return; 169 } 170});
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.