1const CACHE_NAME = 'cruisehuddle-v2144'; 2const STATIC_ASSETS = [ 3 '/', 4 '/manifest.json', 5 '/pwa-icons/icon-192x192.png', 6 '/pwa-icons/icon-512x512.png', 7]; 8 9self.addEventListener('install', event => { 10 event.waitUntil( 11 caches.open(CACHE_NAME).then(cache => cache.addAll(STATIC_ASSETS)) 12 ); 13 self.skipWaiting(); 14}); 15 16self.addEventListener('activate', event => { 17 event.waitUntil( 18 caches.keys().then(keys => 19 Promise.all(keys.filter(k => k !== CACHE_NAME).map(k => caches.delete(k))) 20 ) 21 ); 22 self.clients.claim(); 23}); 24 25// Network first, fallback to cache 26self.addEventListener('fetch', event => { 27 if (event.request.method !== 'GET') return; 28 if (event.request.url.includes('/admin')) return; // Don't cache admin 29 if (event.request.url.includes('livewire')) return; // Don't cache livewire 30 31 // Inertia visits are XHR, not page loads. They are never useful from cache, 32 // and Inertia cannot parse the offline HTML shell below - it just fails the 33 // visit. Worse, Inertia cancels superseded visits, which rejects the fetch 34 // and lands in the catch, so an ordinary cancelled navigation was being 35 // turned into a 503 offline page. Let these go straight to the network. 36 if (event.request.headers.get('X-Inertia')) return; 37 38 // Images are handled entirely by the HTTP cache. Intercepting them here 39 // did two harmful things: every listing photo was copied into Cache 40 // Storage on the member's device (5.2GB of photos exist), and the 41 // service-worker fetch bypassed Cloudflare's content negotiation, so 42 // Polish served the original JPEG instead of the WebP. Images already 43 // carry Cache-Control: max-age=31536000, immutable. 44 if (event.request.destination === 'image') return; 45 if (/\.(jpe?g|png|gif|webp|avif|svg|ico)(\?|$)/i.test(event.request.url)) return; 46 if (event.request.url.includes('/storage/')) return; 47 48 event.respondWith( 49 fetch(event.request) 50 .then(response => { 51 // Never store HTML documents. Cache Storage ignores 52 // Cache-Control, so 'no-store' from the server was being 53 // overridden here and every page a member visited was 54 // kept - Inertia props, auth state and all. 55 // 56 // It also caused the cold-start bug: tapping a push 57 // notification wakes the app before the radio is ready, 58 // the fetch fails, the catch below finds a cached 59 // document and serves a stale page that needs a manual 60 // refresh. A warm app never saw it, because Inertia 61 // visits skip this handler and the network is already up. 62 const isDocument = event.request.mode === 'navigate' 63 || event.request.destination === 'document'; 64 65 if (response.ok && ! isDocument) { 66 const clone = response.clone(); 67 caches.open(CACHE_NAME).then(cache => cache.put(event.request, clone)); 68 } 69 return response; 70 }) 71 .catch(async (err) => { 72 // An aborted request is not an offline condition. Rethrow so the 73 // caller sees the abort it caused, rather than a fake 503. 74 if (err && err.name === 'AbortError') throw err; 75 76 // caches.match() resolves to undefined on a miss, and 77 // respondWith(undefined) throws "Failed to convert value to 78 // 'Response'". That killed the whole fetch handler, so a single 79 // failed request could leave a member stuck on a stale build 80 // with no way out short of clearing site data. 81 const cached = await caches.match(event.request); 82 if (cached) return cached; 83 84 if (event.request.mode === 'navigate') { 85 return new Response( 86 '<!doctype html><meta charset="utf-8">' 87 + '<meta name="viewport" content="width=device-width,initial-scale=1">' 88 + '<title>Offline | CruiseHuddle</title>' 89 + '<body style="margin:0;display:grid;place-items:center;min-height:100vh;' 90 + 'background:#050B14;color:#fff;font-family:system-ui,-apple-system,sans-serif;text-align:center;padding:2rem">' 91 + '<div><h1 style="margin:0 0 .5rem;font-size:1.25rem;font-weight:800">You are offline</h1>' 92 + '<p style="margin:0;color:#94a3b8;font-size:.95rem">Check your connection and try again.</p></div>', 93 { status: 503, headers: { 'Content-Type': 'text/html; charset=utf-8' } } 94 ); 95 } 96 97 return new Response('', { status: 503, statusText: 'Offline' }); 98 }) 99 ); 100}); 101
102// Push notifications 103self.addEventListener('push', event => { 104 const data = event.data ? event.data.json() : {}; 105 event.waitUntil( 106 self.registration.showNotification(data.title || 'CruiseHuddle', { 107 body: data.body || 'You have a new message', 108 icon: '/pwa-icons/icon-192x192.png', 109 badge: '/pwa-icons/icon-96x96.png', 110 data: { url: data.url || '/messages' }, 111 vibrate: [200, 100, 200], 112 }) 113 ); 114}); 115 116self.addEventListener('notificationclick', event => { 117 event.notification.close(); 118 event.waitUntil( 119 clients.openWindow(event.notification.data.url || '/messages') 120 ); 121});
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.