99const CACHE = "pwabuilder-precache" ;
1010const precacheFiles = [
1111 /* Add an array of files to precache for your app */
12- "./index.html"
12+ "./index.html" ,
13+ // Keep in sync with the query-param URL used in index.html.
14+ "./css/activities.css?v=fixed"
1315] ;
1416
1517self . addEventListener ( "install" , function ( event ) {
@@ -33,9 +35,120 @@ self.addEventListener("install", function (event) {
3335self . addEventListener ( "activate" , function ( event ) {
3436 // eslint-disable-next-line no-console
3537 console . log ( "[PWA Builder] Claiming clients for current page" ) ;
36- event . waitUntil ( self . clients . claim ( ) ) ;
38+
39+ // Cleanup: remove any previously-cached non-static GET responses.
40+ // This prevents serving stale / user-specific / poisoned cache entries
41+ // that older SW versions may have cached.
42+ event . waitUntil (
43+ ( async ( ) => {
44+ await self . clients . claim ( ) ;
45+
46+ const cache = await caches . open ( CACHE ) ;
47+ const keys = await cache . keys ( ) ;
48+ const keepUrls = new Set ( precacheFiles . map ( path => new URL ( path , self . location ) . href ) ) ;
49+
50+ for ( const request of keys ) {
51+ try {
52+ const url = new URL ( request . url ) ;
53+ if ( keepUrls . has ( url . href ) ) continue ;
54+ if ( url . origin !== self . location . origin ) {
55+ await cache . delete ( request ) ;
56+ continue ;
57+ }
58+ if ( url . search ) {
59+ await cache . delete ( request ) ;
60+ continue ;
61+ }
62+
63+ const pathname = url . pathname . toLowerCase ( ) ;
64+ const isStaticPath =
65+ pathname === "/" ||
66+ pathname . endsWith ( "/index.html" ) ||
67+ / \. ( c s s | j s | m j s | j s o n | p n g | j p e ? g | g i f | s v g | w e b p | i c o | w o f f 2 ? | t t f | o t f | e o t | m p 3 | w a v | w e b m | m p 4 ) $ / i. test (
68+ pathname
69+ ) ;
70+
71+ if ( ! isStaticPath ) {
72+ await cache . delete ( request ) ;
73+ }
74+ } catch {
75+ // If the URL can't be parsed, treat it as unsafe.
76+ await cache . delete ( request ) ;
77+ }
78+ }
79+ } ) ( )
80+ ) ;
3781} ) ;
3882
83+ function isPrecachedRequest ( request ) {
84+ try {
85+ const url = new URL ( request . url ) ;
86+ const precached = new Set ( precacheFiles . map ( path => new URL ( path , self . location ) . href ) ) ;
87+ return precached . has ( url . href ) ;
88+ } catch {
89+ return false ;
90+ }
91+ }
92+
93+ function isStaticAssetRequest ( request ) {
94+ // Only allow safe, same-origin, static subresources.
95+ if ( request . method !== "GET" ) return false ;
96+
97+ const url = new URL ( request . url ) ;
98+ if ( url . origin !== self . location . origin ) return false ;
99+
100+ // Never runtime-cache URLs with query params (precache exact URLs instead).
101+ if ( url . search ) return false ;
102+
103+ // Avoid caching programmatic fetch() calls (often API/data requests).
104+ if ( ! request . destination ) return false ;
105+
106+ // Avoid caching requests that explicitly include credentials.
107+ if ( request . credentials === "include" ) return false ;
108+
109+ // Avoid range requests.
110+ if ( request . headers . has ( "range" ) ) return false ;
111+
112+ switch ( request . destination ) {
113+ case "style" :
114+ case "script" :
115+ case "image" :
116+ case "font" :
117+ case "manifest" :
118+ return true ;
119+ default :
120+ return false ;
121+ }
122+ }
123+
124+ function isAppShellNavigation ( request ) {
125+ if ( request . method !== "GET" ) return false ;
126+ if ( request . mode !== "navigate" ) return false ;
127+
128+ const url = new URL ( request . url ) ;
129+ if ( url . origin !== self . location . origin ) return false ;
130+
131+ // Only treat the root and index.html as app-shell.
132+ // Do not cache arbitrary documents.
133+ if ( url . search ) return false ;
134+ return url . pathname === "/" || url . pathname . toLowerCase ( ) . endsWith ( "/index.html" ) ;
135+ }
136+
137+ function shouldCacheResponse ( request , response ) {
138+ if ( ! response ) return false ;
139+ if ( ! response . ok ) return false ;
140+ if ( response . status === 206 ) return false ;
141+
142+ // Only cache same-origin "basic" responses to avoid opaque caching.
143+ if ( response . type !== "basic" ) return false ;
144+
145+ const cacheControl = ( response . headers . get ( "Cache-Control" ) || "" ) . toLowerCase ( ) ;
146+ if ( cacheControl . includes ( "no-store" ) || cacheControl . includes ( "private" ) ) return false ;
147+
148+ // Only cache responses for allowlisted requests (static assets + explicit precache URLs).
149+ return isStaticAssetRequest ( request ) || isPrecachedRequest ( request ) ;
150+ }
151+
39152function updateCache ( request , response ) {
40153 if ( response . status === 206 ) {
41154 console . log ( "Partial response is unsupported for caching." ) ;
@@ -66,31 +179,57 @@ function fromCache(request) {
66179self . addEventListener ( "fetch" , function ( event ) {
67180 if ( event . request . method !== "GET" ) return ;
68181
182+ // App-shell offline support: serve cached index.html for navigations.
183+ if ( isAppShellNavigation ( event . request ) ) {
184+ event . respondWith (
185+ ( async ( ) => {
186+ const indexRequest = new Request ( "./index.html" ) ;
187+ try {
188+ const cached = await fromCache ( indexRequest ) ;
189+ // Update the cached app-shell in the background.
190+ event . waitUntil (
191+ fetch ( indexRequest ) . then ( function ( response ) {
192+ if ( shouldCacheResponse ( indexRequest , response ) ) {
193+ return updateCache ( indexRequest , response . clone ( ) ) ;
194+ }
195+ } )
196+ ) ;
197+ return cached ;
198+ } catch {
199+ // No cached app-shell yet: fall back to network.
200+ return fetch ( event . request ) ;
201+ }
202+ } ) ( )
203+ ) ;
204+ return ;
205+ }
206+
207+ // Only use cache-first for explicit precache URLs and allowlisted static assets.
208+ const canUseCache = isPrecachedRequest ( event . request ) || isStaticAssetRequest ( event . request ) ;
209+ if ( ! canUseCache ) {
210+ // Network-only for everything else (prevents caching/serving user-specific responses).
211+ event . respondWith ( fetch ( event . request ) ) ;
212+ return ;
213+ }
214+
69215 event . respondWith (
70216 fromCache ( event . request ) . then (
71217 function ( response ) {
72- // The response was found in the cache so we responde
73- // with it and update the entry
74-
75- // This is where we call the server to get the newest
76- // version of the file to use the next time we show view
218+ // Cache hit: return immediately, then update in background.
77219 event . waitUntil (
78- fetch ( event . request ) . then ( function ( response ) {
79- if ( response . ok ) {
80- return updateCache ( event . request , response ) ;
220+ fetch ( event . request ) . then ( function ( networkResponse ) {
221+ if ( shouldCacheResponse ( event . request , networkResponse ) ) {
222+ return updateCache ( event . request , networkResponse . clone ( ) ) ;
81223 }
82224 } )
83225 ) ;
84-
85226 return response ;
86227 } ,
87228 async function ( ) {
88- // The response was not found in the cache so we look
89- // for it on the server
229+ // Cache miss: fetch from network and cache if safe.
90230 try {
91231 const response = await fetch ( event . request ) ;
92- // If request was success, add or update it in the cache
93- if ( response . ok ) {
232+ if ( shouldCacheResponse ( event . request , response ) ) {
94233 event . waitUntil ( updateCache ( event . request , response . clone ( ) ) ) ;
95234 }
96235 return response ;
0 commit comments