A navigation whose shell is not in the cache fell through to the network, and when that failed too the promise rejected — which a browser renders as ERR_FAILED on a page with nothing on it. The state was also permanent. The precache is filled by `install`, and `install` only runs again when this file's bytes change, so an emptied cache stayed empty until the next deploy: the navigate branch never wrote what it fetched, so every reload online worked and every reload offline failed, silently, on an app whose whole point is the second one. A missing shell now repairs the entire precache rather than the one file the navigation asked for, since whatever emptied the cache took the assets with it and a shell alone renders nothing. `SHELL` is fetched by name rather than the request, because a request for another path is answered with a 404 page by both the local server and Pages, and caching that as the shell is a worse failure than the one being fixed. When there is no network either, the reply is a page that says so.
- commit
- 75950bfe34fab8e1f9cdf0f3732441dd7b07aac1
- parent
- a2c47c76a2f0f82a41ea2d60c0689d6b9aa7d1b3
- author
- Dami <dami.etoile@gmail.com>
| M | assets/ | 52 | +++++++++++++++++++++++++++++- |
@@ -20,6 +20,28 @@ const CORE = __CORE__;
2020 2121 const SHELL = "/index.html";2222 23+// What is served when there is no shell and no network. It exists so that the24+// one failure this app is supposed to be incapable of does not arrive as a25+// browser error page, which says `ERR_FAILED` and gives a reader no way to tell26+// a broken install from a broken app. Inline and unstyled on purpose: it is the27+// case where nothing else loaded.28+const OFFLINE = `<!doctype html>29+<meta charset="utf-8">30+<meta name="viewport" content="width=device-width, initial-scale=1">31+<title>klot.ski</title>32+<body style="font:16px/1.5 system-ui,sans-serif;margin:0;display:grid;place-items:center;min-height:100vh;background:#e8f4f2">33+<div style="max-width:24rem;padding:1.5rem">34+<h1 style="font-size:1rem;margin:0 0 .5rem">klot.ski is offline</h1>35+<p style="margin:0 0 1rem">Its offline copy is missing, so there is nothing to open until it can be fetched again.</p>36+<p style="margin:0"><button onclick="location.reload()" style="font:inherit;padding:.4rem .9rem">Try again</button></p>37+</div>`;38+39+const OFFLINE_INIT = {40+ status: 503,41+ statusText: "Offline",42+ headers: { "Content-Type": "text/html; charset=utf-8" },43+};44+2345 self.addEventListener("install", (event) => {2446 event.waitUntil(caches.open(VERSION).then((cache) => cache.addAll(CORE)));2547 // Deliberately no skipWaiting. A new worker waits until the page decides it is@@ -59,9 +81,37 @@ self.addEventListener("fetch", (event) => {
5981 // different builds. The network is not consulted: a new shell arrives by6082 // installing a new worker, which is the only thing that should move an6183 // offline app forward.84+ //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.93+ //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.62100 if (request.mode === "navigate") {63101 event.respondWith(64- caches.match(SHELL).then((cached) => cached || fetch(request)),102+ 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);113+ })114+ .catch(() => new Response(OFFLINE, OFFLINE_INIT)),65115 );66116 return;67117 }