1// Bump on any change to caching behavior so installed PWAs purge prior caches 2// (including poisoned redirect entries) on activate. v3 cached the legacy 3// `/dashboard` shell â a route that now only 307-redirects to 4// `/app/{org}/{client}` â which broke post-login navigation on installed 5// (mobile) clients; v4 stops caching redirects entirely; v5 stops caching 6// partial/range responses (which made `cache.put` throw "Unexpected internal 7// error") and purges any such poisoned entries written by v4. 8const CACHE_VERSION = 'v5'; 9const STATIC_CACHE = `guestify-static-${CACHE_VERSION}`; 10const DYNAMIC_CACHE = `guestify-dynamic-${CACHE_VERSION}`; 11 12// Install: activate immediately. There is no app shell to pre-cache â the 13// authed app lives under `/app/{org}/{client}` and is session- and 14// redirect-gated, so it must never be baked into a static cache. 15self.addEventListener('install', () => { 16 self.skipWaiting(); 17}); 18 19// Activate: clean up old cache versions 20self.addEventListener('activate', (event) => { 21 event.waitUntil( 22 caches.keys().then((cacheNames) => { 23 return Promise.all( 24 cacheNames 25 .filter((name) => name !== STATIC_CACHE && name !== DYNAMIC_CACHE) 26 .map((name) => caches.delete(name)) 27 ); 28 }) 29 ); 30 self.clients.claim(); 31}); 32 33// Determine if a request is an API route 34function isApiRoute(url) { 35 return url.pathname.startsWith('/api/'); 36} 37 38// Determine if a request is a navigation request 39function isNavigationRequest(request) { 40 return request.mode === 'navigate'; 41} 42 43// Network-first strategy for dynamic content 44async function networkFirst(request) { 45 try { 46 const response = await fetch(request); 47 // Only cache clean, final 200 GET responses. A redirected response 48 // (`response.redirected === true`, e.g. the 307 from `/dashboard` or the 49 // `/app` entry hops) must never be cached: replaying it for a navigation 50 // request is rejected by the browser / serves a stale shell, which lands 51 // the user on the wrong page after login. Skipping these lets the redirect 52 // chain resolve to the real `/app/{org}/{client}` page on every load. 53 // 54 // Partial/range responses are also excluded. The media-kit builder loads 55 // media (audio/video/large images) with `Range` headers; `cache.put` 56 // rejects with "Failed to execute 'put' on 'Cache': Unexpected internal 57 // error" for these, and because the write below is fire-and-forget the 58 // rejection used to surface as a noisy "Uncaught (in promise)" in the 59 // console. We skip range requests / non-`basic` (opaque, partial) responses 60 // up front and additionally swallow any residual `cache.put` rejection 61 // (e.g. QuotaExceeded under storage pressure) so a cache-write failure can 62 // never break the page or spam the console. 63 if ( 64 request.method === 'GET' && 65 response.status === 200 && 66 response.type === 'basic' && 67 !response.redirected && 68 !request.headers.has('range') 69 ) { 70 const cache = await caches.open(DYNAMIC_CACHE); 71 cache.put(request, response.clone()).catch(() => {}); 72 } 73 return response; 74 } catch { 75 const cached = await caches.match(request); 76 if (cached) return cached; 77 78 return new Response('Offline', { status: 503, statusText: 'Service Unavailable' }); 79 } 80} 81 82// Fetch handler 83self.addEventListener('fetch', (event) => { 84 const url = new URL(event.request.url); 85 86 // Only handle same-origin requests 87 if (url.origin !== self.location.origin) return; 88 89 // Never intercept service worker assets. The browser must always fetch these 90 // from the network so updates are not blocked by stale dynamic cache entries. 91 if (url.pathname === '/sw.js' || url.pathname === '/sw-push.js') return; 92 93 // Never intercept auth routes â they rely on Set-Cookie headers and CSRF tokens 94 if (url.pathname.startsWith('/api/auth/')) return; 95 96 // Never intercept session/redirect-gated entry points. `/auth/*` (login), 97 // `/dashboard*` (legacy shim that 307s to the tenant path), and the bare 98 // `/app` resolver all return redirects that depend on the live session and 99 // must reach the network so the post-login chain resolves to the real 100 // `/app/{org}/{client}` page. Concrete tenant pages under `/app/{org}/...` 101 // are not matched here and still go through network-first below.
102 if ( 103 url.pathname.startsWith('/auth/') || 104 url.pathname === '/auth' || 105 url.pathname.startsWith('/dashboard/') || 106 url.pathname === '/dashboard' || 107 url.pathname === '/app' || 108 url.pathname === '/app/' 109 ) { 110 return; 111 } 112 113 // Never intercept Next.js static assets â they are content-hashed and 114 // already have optimal HTTP cache headers. Caching them in the SW causes 115 // stale bundles to be served after deployments. 116 if (url.pathname.startsWith('/_next/')) return; 117 118 // API routes and HTML pages: network-first 119 if (isApiRoute(url) || isNavigationRequest(event.request)) { 120 event.respondWith(networkFirst(event.request)); 121 return; 122 } 123 124 // Default: network-first 125 event.respondWith(networkFirst(event.request)); 126});
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.