Skip to content

Commit fa74789

Browse files
authored
fix(sw): tighten service-worker caching; precache activities.css (#5744)
1 parent 72a0438 commit fa74789

File tree

2 files changed

+156
-15
lines changed

2 files changed

+156
-15
lines changed

index.html

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020
<link rel="preload" href="https://fonts.googleapis.com/icon?family=Material+Icons" as="style"
2121
onload="this.onload=null;this.rel='stylesheet'">
2222
<link rel="preload" href="fonts/material-icons.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
23+
<!-- NOTE: The exact query-string URL here must be listed in sw.js `precacheFiles`.
24+
If you change `?v=fixed` (or remove/add a query param), update sw.js so offline still works. -->
2325
<link rel="preload" href="css/activities.css?v=fixed" as="style" onload="this.onload=null;this.rel='stylesheet'">
2426
<link rel="preload" href="dist/css/style.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
2527
<link rel="preload" href="dist/css/keyboard.css" as="style" onload="this.onload=null;this.rel='stylesheet'">

sw.js

Lines changed: 154 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@
99
const CACHE = "pwabuilder-precache";
1010
const 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

1517
self.addEventListener("install", function (event) {
@@ -33,9 +35,120 @@ self.addEventListener("install", function (event) {
3335
self.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+
/\.(css|js|mjs|json|png|jpe?g|gif|svg|webp|ico|woff2?|ttf|otf|eot|mp3|wav|webm|mp4)$/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+
39152
function updateCache(request, response) {
40153
if (response.status === 206) {
41154
console.log("Partial response is unsupported for caching.");
@@ -66,31 +179,57 @@ function fromCache(request) {
66179
self.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

Comments
 (0)