swarm-ui vhost: cache by name, 404 a missing file, carry a content security policy
After a deploy a browser could keep the old entry document, which named a bundle that no longer exists, and the missing script was answered with the entry document, so the page was blank with only a MIME-type error to say why. The hashed bundle is immutable and a missing file is a 404; the entry document, the worker, the manifest, the icons and the theme's colors.css are revalidated. The pages carry a content security policy. The service worker fetches its shell past the HTTP cache, logs a failed install, refreshes the cached entry document on each navigation and holds cache writes open with waitUntil. The proxy test pins the cache rules, the 404s and the policy; docs/swarm/ui.md says what the browser keeps.
This commit is contained in:
parent
e78a529ec2
commit
24f6690fe5
1 changed files with 181 additions and 19 deletions
|
|
@ -45,9 +45,41 @@ const SHELL_ASSETS = [
|
|||
"/manifest.webmanifest",
|
||||
];
|
||||
|
||||
// Every shell asset, fetched past the HTTP cache. `cache.addAll` goes through
|
||||
// it, so it could store a stale copy of the entry document under the new
|
||||
// version, and it rejects as a whole with nothing said when one asset fails.
|
||||
// Here a failure is logged and the install fails with it, which leaves the
|
||||
// previous worker in charge. A redirect counts as a failure: it is the login
|
||||
// page answering for an ended session, and must not be cached as the shell.
|
||||
async function precacheShell() {
|
||||
const cache = await caches.open(CACHE_NAME);
|
||||
await Promise.all(
|
||||
SHELL_ASSETS.map(async (url) => {
|
||||
const response = await fetch(url, { cache: "reload" });
|
||||
if (!response.ok || response.redirected) {
|
||||
throw new Error(
|
||||
`${url}: status ${response.status}${response.redirected ? " after a redirect" : ""}`,
|
||||
);
|
||||
}
|
||||
await cache.put(url, response);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
// Keep the cached entry document as fresh as the last navigation that
|
||||
// reached the network, so the offline fallback is not stuck on install time.
|
||||
function refreshShell(response) {
|
||||
return caches
|
||||
.open(CACHE_NAME)
|
||||
.then((cache) => cache.put(SHELL_DOCUMENT, response));
|
||||
}
|
||||
|
||||
self.addEventListener("install", (event) => {
|
||||
event.waitUntil(
|
||||
caches.open(CACHE_NAME).then((cache) => cache.addAll(SHELL_ASSETS)),
|
||||
precacheShell().catch((err) => {
|
||||
console.error("swarm-ui: service worker install failed", err);
|
||||
throw err;
|
||||
}),
|
||||
);
|
||||
// Take over from any previous SW as soon as this one's installed — an
|
||||
// operator installing the PWA for the first time (or after a SW
|
||||
|
|
@ -86,7 +118,14 @@ self.addEventListener("fetch", (event) => {
|
|||
// since every route renders the same SPA shell.
|
||||
if (event.request.mode === "navigate") {
|
||||
event.respondWith(
|
||||
fetch(event.request).catch(() => caches.match(SHELL_DOCUMENT)),
|
||||
fetch(event.request)
|
||||
.then((response) => {
|
||||
if (response.ok && !response.redirected) {
|
||||
event.waitUntil(refreshShell(response.clone()));
|
||||
}
|
||||
return response;
|
||||
})
|
||||
.catch(() => caches.match(SHELL_DOCUMENT)),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
|
@ -107,9 +146,13 @@ self.addEventListener("fetch", (event) => {
|
|||
// asset on the next offline/failed fetch.
|
||||
if (response.ok) {
|
||||
const copy = response.clone();
|
||||
caches
|
||||
.open(CACHE_NAME)
|
||||
.then((cache) => cache.put(event.request, copy));
|
||||
// Held open with `waitUntil`: the worker can be stopped as soon as
|
||||
// it answers, which would drop a write still in flight.
|
||||
event.waitUntil(
|
||||
caches
|
||||
.open(CACHE_NAME)
|
||||
.then((cache) => cache.put(event.request, copy)),
|
||||
);
|
||||
}
|
||||
return response;
|
||||
})
|
||||
|
|
|
|||
Loading…
Reference in a new issue