clone

an offline sliding-block puzzle game in gleam and lustre, with an endless ladder of puzzles ranked by search

The shell has one canonical address

Dami committed b9ed985
Pages redirects /index.html to /, and the response kept that redirect history when the service worker precached it. Chrome refuses to reuse such a response for a navigation, so the first controlled reload ended at ERR_FAILED even though the shell was present.\n\nPrecache the canonical address instead. A navigation also verifies the complete versioned cache before releasing the shell, and every asset branch resolves to a real offline response rather than rejecting respondWith.
commit
b9ed9858726d3d3a2a389c193f58da27583e0f03
parent
81549a9cf08bcb2d13c785adf3bf62867f35ffd3
author
Dami <dami.etoile@gmail.com>
2 files changed, +45 -43
Massets/sw.js80+++++++++++++++---------------
Mtools/bundle.mjs8++--
M assets/sw.js +41 -39
@@ -18,7 +18,11 @@
1818 const VERSION = "__VERSION__";1919 const CORE = __CORE__;2020 21-const SHELL = "/index.html";21+// Pages redirects /index.html to /. A redirected response cannot be returned22+// from a service worker for a navigation request, so caching /index.html makes23+// the first controlled reload fail with ERR_FAILED even though the response is24+// present. Cache the canonical URL and the response has no redirect history.25+const SHELL = "/";2226 2327 // What is served when there is no shell and no network. It exists so that the2428 // one failure this app is supposed to be incapable of does not arrive as a
@@ -82,34 +86,31 @@ self.addEventListener("fetch", (event) => {
8286 // installing a new worker, which is the only thing that should move an8387 // offline app forward.8488 //85- // Unless the shell is not there, which is a state this used to have no way86- // out of. The precache is written once, by `install`, and a browser only runs87- // `install` again when the bytes of this file change — so an empty cache88- // stayed empty until the next deploy. Cache storage is best effort and can be89- // evicted while the registration that filled it survives, and on a machine90- // where the app is being worked on it is also cleared by hand several times a91- // day. Every reload after that was `ERR_FAILED`: the promise below rejected,92- // and a rejected `respondWith` is a network error page with nothing on it.89+ // Unless some of the precache is not there, which is a state this used to90+ // have no way out of. The precache is written once, by `install`, and a91+ // browser only runs `install` again when the bytes of this file change — so a92+ // damaged cache stayed damaged until the next deploy. Cache storage is best93+ // effort and can be evicted while the registration that filled it survives,94+ // and on a machine where the app is being worked on it is also cleared by95+ // hand several times a day.9396 //94- // So a missing shell repairs the whole precache rather than the one file the95- // navigation asked for — an eviction takes the assets too, and a shell on its96- // own renders nothing. `SHELL` is fetched by name rather than the request,97- // because a request for some other path is answered with a 404 page by both98- // the local server and Pages, and caching *that* as the shell would be a99- // worse failure than the one being fixed.97+ // A navigation therefore verifies the whole versioned precache before it98+ // releases the shell. A shell on its own only moves the failure into a script99+ // or stylesheet request, where a rejected `respondWith` is still ERR_FAILED100+ // and leaves a blank page. Repairing through `addAll` also keeps the shell and101+ // every URL it names on one build.100102 if (request.mode === "navigate") {101103 event.respondWith(102104 caches103- .match(SHELL)104- .then((cached) => {105- if (cached) return cached;106- event.waitUntil(107- caches108- .open(VERSION)109- .then((cache) => cache.addAll(CORE))110- .catch(() => {}),111- );112- return fetch(SHELL);105+ .open(VERSION)106+ .then(async (cache) => {107+ const core = await Promise.all(CORE.map((path) => cache.match(path)));108+ if (core.some((response) => !response)) {109+ await cache.addAll(CORE);110+ }111+ const shell = await cache.match(SHELL);112+ if (!shell) throw new Error("precache completed without a shell");113+ return shell;113114 })114115 .catch(() => new Response(OFFLINE, OFFLINE_INIT)),115116 );
@@ -119,17 +120,18 @@ self.addEventListener("fetch", (event) => {
119120 // Fingerprinted, so the copy on disk is the only copy that URL will ever have.120121 if (url.pathname.startsWith("/assets/")) {121122 event.respondWith(122- caches.match(request).then(123- (cached) =>124- cached ||125- fetch(request).then((response) => {126- if (response.ok) {127- const copy = response.clone();128- caches.open(VERSION).then((cache) => cache.put(request, copy));129- }130- return response;131- }),132- ),123+ caches124+ .open(VERSION)125+ .then(async (cache) => {126+ const cached = await cache.match(request);127+ if (cached) return cached;128+ const response = await fetch(request);129+ if (response.ok) {130+ await cache.put(request, response.clone());131+ }132+ return response;133+ })134+ .catch(() => new Response("Offline", { status: 503, statusText: "Offline" })),133135 );134136 return;135137 }
@@ -137,10 +139,10 @@ self.addEventListener("fetch", (event) => {
137139 // Everything else keeps its name, so it is asked about and only falls back.138140 event.respondWith(139141 fetch(request)140- .then((response) => {142+ .then(async (response) => {141143 if (response.ok) {142- const copy = response.clone();143- caches.open(VERSION).then((cache) => cache.put(request, copy));144+ const cache = await caches.open(VERSION);145+ await cache.put(request, response.clone());144146 }145147 return response;146148 })
M tools/bundle.mjs +4 -4