Installable, with a worker that does not hold a deploy back
A browser offers to install a page only when it has a service worker handling fetches, so a PWA needs one -- and two commits ago I took the service worker out. Both were right about different things. What made the old one worth removing was not that it existed but what it did: Flutter ships an offline-first worker, and a new copy of one waits for every tab on the origin to close before it takes over, so the app went on serving the bundle it already had. Twice in one afternoon a working fix read as broken. So `--pwa-strategy=none` stays and `flutter/web/frq_sw.js` is registered in its place. It is the same idea inverted: `skipWaiting` and `clients.claim`, so a new worker replaces the old one on the next load rather than the next time every tab is shut; and network-first, so what is served is what was deployed and the cache is only what answers when the network does not. Our own GETs alone -- never a POST, never the upload relay, never another origin's bytes, which come from hosts that did not agree to be kept here. What it buys is the shell. Opened from a home screen with nothing to connect to it draws itself and says it is not connected, instead of the browser's dinosaur. It should not pretend to more: this is a window onto a live IRC connection. The manifest wears the app's own colours rather than the Flutter template's blue, and `orientation` is `any`, the layout having been taught to handle both. `tools/webserve.py` speaks HTTP/1.1 now, so a load reuses one connection instead of opening fifteen. That began as a guess at why the preview browser would not register a worker, and it was not the reason -- it refuses whatever it is served, which is why the registration is unverified here and wants a real phone. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
3c639b8 parent: d4b430c modified
.modal/web/Dockerfile +3 -3 | @@ -32,9 +32,9 @@ COPY . . | ||
| 32 | 32 | # and is not worth an install for one call. Inlined means duplicated, so the |
| 33 | 33 | # flags have to be kept in step with that recipe -- `--pwa-strategy=none` |
| 34 | 34 | # below is one of them, and the deployment is the place it matters most, |
| 35 | -# being the one people reach for by URL. See that recipe for what it does | |
| 36 | -# and what it does not: an already-installed worker is replaced by an empty | |
| 37 | -# one rather than removed, and `flutter/web/index.html` removes it. The core is compiled to | |
| 35 | +# being the one people reach for by URL and the only one anybody installs. | |
| 36 | +# It leaves out Flutter's service worker; `flutter/web/frq_sw.js` is | |
| 37 | +# registered instead, and that recipe says why. The core is compiled to | |
| 38 | 38 | # JavaScript and copied into `flutter/web/` rather than into the output, |
| 39 | 39 | # because `flutter build web` copies that directory into the bundle -- which |
| 40 | 40 | # is what makes the page's `<script src="frq_core.js">` resolve the same |
| @@ -32,9 +32,9 @@ COPY . . | |||
| 32 | # and is not worth an install for one call. Inlined means duplicated, so the | 32 | # and is not worth an install for one call. Inlined means duplicated, so the |
| 33 | # flags have to be kept in step with that recipe -- `--pwa-strategy=none` | 33 | # flags have to be kept in step with that recipe -- `--pwa-strategy=none` |
| 34 | # below is one of them, and the deployment is the place it matters most, | 34 | # below is one of them, and the deployment is the place it matters most, |
| 35 | -# being the one people reach for by URL. See that recipe for what it does | 35 | +# being the one people reach for by URL and the only one anybody installs. |
| 36 | -# and what it does not: an already-installed worker is replaced by an empty | 36 | +# It leaves out Flutter's service worker; `flutter/web/frq_sw.js` is |
| 37 | -# one rather than removed, and `flutter/web/index.html` removes it. The core is compiled to | 37 | +# registered instead, and that recipe says why. The core is compiled to |
| 38 | # JavaScript and copied into `flutter/web/` rather than into the output, | 38 | # JavaScript and copied into `flutter/web/` rather than into the output, |
| 39 | # because `flutter build web` copies that directory into the bundle -- which | 39 | # because `flutter build web` copies that directory into the bundle -- which |
| 40 | # is what makes the page's `<script src="frq_core.js">` resolve the same | 40 | # is what makes the page's `<script src="frq_core.js">` resolve the same |
added
flutter/web/frq_sw.js +66 -0 | new file mode 100644 | ||
| @@ -0,0 +1,66 @@ | ||
| 1 | +// The service worker, written the other way round from the one we removed. | |
| 2 | +// | |
| 3 | +// A browser will not offer to install a page unless it has a worker with a | |
| 4 | +// fetch handler, so a PWA needs one. Flutter ships such a worker and it is | |
| 5 | +// the wrong one here: it is offline-first, which means a deploy landed only | |
| 6 | +// after a new worker had waited for every tab on the origin to close, and | |
| 7 | +// until then the app went on serving the bundle it had. Twice in one | |
| 8 | +// afternoon a working fix read as broken because of it. | |
| 9 | +// | |
| 10 | +// This one is network-first and takes over at once. | |
| 11 | +// | |
| 12 | +// * `skipWaiting` in install and `clients.claim` in activate: no waiting | |
| 13 | +// room. A new worker replaces the old one on the next load, not on the | |
| 14 | +// next time every tab happens to be closed. | |
| 15 | +// * every GET goes to the network first, and the cache is what answers when | |
| 16 | +// the network does not. So what is served is what was deployed, and the | |
| 17 | +// cache is a fallback rather than the source of truth. | |
| 18 | +// | |
| 19 | +// What that buys on the install side is the shell: opened from a home | |
| 20 | +// screen with no connection it draws itself and says it is not connected, | |
| 21 | +// rather than showing the browser's dinosaur. It cannot do more than that | |
| 22 | +// and should not pretend to -- this is a window onto a live IRC connection, | |
| 23 | +// and without the network there is nothing to look at. | |
| 24 | +const CACHE = "frq-shell-v1"; | |
| 25 | + | |
| 26 | +self.addEventListener("install", () => { | |
| 27 | + self.skipWaiting(); | |
| 28 | +}); | |
| 29 | + | |
| 30 | +self.addEventListener("activate", (e) => { | |
| 31 | + e.waitUntil((async () => { | |
| 32 | + // Anything an older worker left, ours included once the name changes. | |
| 33 | + for (const k of await caches.keys()) { | |
| 34 | + if (k !== CACHE) await caches.delete(k); | |
| 35 | + } | |
| 36 | + await self.clients.claim(); | |
| 37 | + })()); | |
| 38 | +}); | |
| 39 | + | |
| 40 | +self.addEventListener("fetch", (e) => { | |
| 41 | + const req = e.request; | |
| 42 | + // Only our own GETs. A POST is never replayable, the upload relay least of | |
| 43 | + // all, and another origin's bytes are not ours to keep -- the avatars and | |
| 44 | + // pictures come from hosts that never agreed to be cached here. | |
| 45 | + if (req.method !== "GET") return; | |
| 46 | + const url = new URL(req.url); | |
| 47 | + if (url.origin !== self.location.origin) return; | |
| 48 | + if (url.pathname.startsWith("/api/")) return; | |
| 49 | + | |
| 50 | + e.respondWith((async () => { | |
| 51 | + try { | |
| 52 | + const fresh = await fetch(req); | |
| 53 | + // Only a real answer is kept. A 404 or a 500 cached is a fault that | |
| 54 | + // outlives the deploy that caused it. | |
| 55 | + if (fresh && fresh.ok && fresh.type === "basic") { | |
| 56 | + const c = await caches.open(CACHE); | |
| 57 | + c.put(req, fresh.clone()); | |
| 58 | + } | |
| 59 | + return fresh; | |
| 60 | + } catch (err) { | |
| 61 | + const kept = await caches.match(req); | |
| 62 | + if (kept) return kept; | |
| 63 | + throw err; | |
| 64 | + } | |
| 65 | + })()); | |
| 66 | +}); | |
| new file mode 100644 | |||
| @@ -0,0 +1,66 @@ | |||
| 1 | +// The service worker, written the other way round from the one we removed. | ||
| 2 | +// | ||
| 3 | +// A browser will not offer to install a page unless it has a worker with a | ||
| 4 | +// fetch handler, so a PWA needs one. Flutter ships such a worker and it is | ||
| 5 | +// the wrong one here: it is offline-first, which means a deploy landed only | ||
| 6 | +// after a new worker had waited for every tab on the origin to close, and | ||
| 7 | +// until then the app went on serving the bundle it had. Twice in one | ||
| 8 | +// afternoon a working fix read as broken because of it. | ||
| 9 | +// | ||
| 10 | +// This one is network-first and takes over at once. | ||
| 11 | +// | ||
| 12 | +// * `skipWaiting` in install and `clients.claim` in activate: no waiting | ||
| 13 | +// room. A new worker replaces the old one on the next load, not on the | ||
| 14 | +// next time every tab happens to be closed. | ||
| 15 | +// * every GET goes to the network first, and the cache is what answers when | ||
| 16 | +// the network does not. So what is served is what was deployed, and the | ||
| 17 | +// cache is a fallback rather than the source of truth. | ||
| 18 | +// | ||
| 19 | +// What that buys on the install side is the shell: opened from a home | ||
| 20 | +// screen with no connection it draws itself and says it is not connected, | ||
| 21 | +// rather than showing the browser's dinosaur. It cannot do more than that | ||
| 22 | +// and should not pretend to -- this is a window onto a live IRC connection, | ||
| 23 | +// and without the network there is nothing to look at. | ||
| 24 | +const CACHE = "frq-shell-v1"; | ||
| 25 | + | ||
| 26 | +self.addEventListener("install", () => { | ||
| 27 | + self.skipWaiting(); | ||
| 28 | +}); | ||
| 29 | + | ||
| 30 | +self.addEventListener("activate", (e) => { | ||
| 31 | + e.waitUntil((async () => { | ||
| 32 | + // Anything an older worker left, ours included once the name changes. | ||
| 33 | + for (const k of await caches.keys()) { | ||
| 34 | + if (k !== CACHE) await caches.delete(k); | ||
| 35 | + } | ||
| 36 | + await self.clients.claim(); | ||
| 37 | + })()); | ||
| 38 | +}); | ||
| 39 | + | ||
| 40 | +self.addEventListener("fetch", (e) => { | ||
| 41 | + const req = e.request; | ||
| 42 | + // Only our own GETs. A POST is never replayable, the upload relay least of | ||
| 43 | + // all, and another origin's bytes are not ours to keep -- the avatars and | ||
| 44 | + // pictures come from hosts that never agreed to be cached here. | ||
| 45 | + if (req.method !== "GET") return; | ||
| 46 | + const url = new URL(req.url); | ||
| 47 | + if (url.origin !== self.location.origin) return; | ||
| 48 | + if (url.pathname.startsWith("/api/")) return; | ||
| 49 | + | ||
| 50 | + e.respondWith((async () => { | ||
| 51 | + try { | ||
| 52 | + const fresh = await fetch(req); | ||
| 53 | + // Only a real answer is kept. A 404 or a 500 cached is a fault that | ||
| 54 | + // outlives the deploy that caused it. | ||
| 55 | + if (fresh && fresh.ok && fresh.type === "basic") { | ||
| 56 | + const c = await caches.open(CACHE); | ||
| 57 | + c.put(req, fresh.clone()); | ||
| 58 | + } | ||
| 59 | + return fresh; | ||
| 60 | + } catch (err) { | ||
| 61 | + const kept = await caches.match(req); | ||
| 62 | + if (kept) return kept; | ||
| 63 | + throw err; | ||
| 64 | + } | ||
| 65 | + })()); | ||
| 66 | +}); | ||
modified
flutter/web/index.html +19 -19 | @@ -23,6 +23,10 @@ | ||
| 23 | 23 | |
| 24 | 24 | <link rel="icon" type="image/png" href="favicon.png"/> |
| 25 | 25 | |
| 26 | + <!-- The browser chrome around an installed window, matching `nim_theme.bg` | |
| 27 | + rather than the blue the Flutter template shipped. --> | |
| 28 | + <meta name="theme-color" content="#202833"> | |
| 29 | + | |
| 26 | 30 | <title>frq</title> |
| 27 | 31 | <link rel="manifest" href="manifest.json"> |
| 28 | 32 | |
| @@ -38,30 +42,26 @@ | ||
| 38 | 42 | back to loopback or its own origins — and this is neither. |
| 39 | 43 | --> |
| 40 | 44 | <!-- |
| 41 | - No service worker, and no leftovers of one. | |
| 45 | + Installable, and ours rather than Flutter's. | |
| 46 | + | |
| 47 | + A browser offers to install a page only when it has a service worker | |
| 48 | + with a fetch handler, so a PWA needs one -- and the bundle is still | |
| 49 | + built `--pwa-strategy=none`, because the worker Flutter ships is the one | |
| 50 | + that held deploys back: offline-first, and a new copy of it waits for | |
| 51 | + every tab on the origin to close before it takes over. | |
| 42 | 52 | |
| 43 | - The bundle is built `--pwa-strategy=none`, so nothing here registers | |
| 44 | - one any more. That settles a new visitor, but not a browser that | |
| 45 | - installed one before: `none` still emits `flutter_service_worker.js` as | |
| 46 | - an empty file, so the old worker is replaced by a no-op rather than | |
| 47 | - removed, and a no-op registration is still a registration. | |
| 53 | + `frq_sw.js` is network-first and claims its clients immediately. See | |
| 54 | + that file for what it does and does not promise. | |
| 48 | 55 | |
| 49 | - This removes it. The app is a window onto a live IRC connection and has | |
| 50 | - no offline worth the name, so nothing is lost with the caches -- and | |
| 51 | - what they cost was a deploy that would not arrive: a new worker waits | |
| 52 | - until every tab on the origin is closed, so reloading went on serving | |
| 53 | - the previous bundle and the cure was clearing the site data by hand. | |
| 56 | + Registered after load, so it never competes with the bundle for the | |
| 57 | + connection on a first visit. | |
| 54 | 58 | --> |
| 55 | 59 | <script> |
| 56 | 60 | if ("serviceWorker" in navigator) { |
| 57 | - navigator.serviceWorker.getRegistrations() | |
| 58 | - .then(function (rs) { rs.forEach(function (r) { r.unregister(); }); }) | |
| 59 | - .catch(function () { /* nothing to undo */ }); | |
| 60 | - } | |
| 61 | - if (window.caches && caches.keys) { | |
| 62 | - caches.keys() | |
| 63 | - .then(function (ks) { ks.forEach(function (k) { caches.delete(k); }); }) | |
| 64 | - .catch(function () { /* nothing to undo */ }); | |
| 61 | + window.addEventListener("load", function () { | |
| 62 | + navigator.serviceWorker.register("frq_sw.js") | |
| 63 | + .catch(function () { /* a page that will not install still runs */ }); | |
| 64 | + }); | |
| 65 | 65 | } |
| 66 | 66 | </script> |
| 67 | 67 | |
| @@ -23,6 +23,10 @@ | |||
| 23 | 23 | ||
| 24 | <link rel="icon" type="image/png" href="favicon.png"/> | 24 | <link rel="icon" type="image/png" href="favicon.png"/> |
| 25 | 25 | ||
| 26 | + <!-- The browser chrome around an installed window, matching `nim_theme.bg` | ||
| 27 | + rather than the blue the Flutter template shipped. --> | ||
| 28 | + <meta name="theme-color" content="#202833"> | ||
| 29 | + | ||
| 26 | <title>frq</title> | 30 | <title>frq</title> |
| 27 | <link rel="manifest" href="manifest.json"> | 31 | <link rel="manifest" href="manifest.json"> |
| 28 | 32 | ||
| @@ -38,30 +42,26 @@ | |||
| 38 | back to loopback or its own origins — and this is neither. | 42 | back to loopback or its own origins — and this is neither. |
| 39 | --> | 43 | --> |
| 40 | <!-- | 44 | <!-- |
| 41 | - No service worker, and no leftovers of one. | 45 | + Installable, and ours rather than Flutter's. |
| 46 | + | ||
| 47 | + A browser offers to install a page only when it has a service worker | ||
| 48 | + with a fetch handler, so a PWA needs one -- and the bundle is still | ||
| 49 | + built `--pwa-strategy=none`, because the worker Flutter ships is the one | ||
| 50 | + that held deploys back: offline-first, and a new copy of it waits for | ||
| 51 | + every tab on the origin to close before it takes over. | ||
| 42 | 52 | ||
| 43 | - The bundle is built `--pwa-strategy=none`, so nothing here registers | 53 | + `frq_sw.js` is network-first and claims its clients immediately. See |
| 44 | - one any more. That settles a new visitor, but not a browser that | 54 | + that file for what it does and does not promise. |
| 45 | - installed one before: `none` still emits `flutter_service_worker.js` as | ||
| 46 | - an empty file, so the old worker is replaced by a no-op rather than | ||
| 47 | - removed, and a no-op registration is still a registration. | ||
| 48 | 55 | ||
| 49 | - This removes it. The app is a window onto a live IRC connection and has | 56 | + Registered after load, so it never competes with the bundle for the |
| 50 | - no offline worth the name, so nothing is lost with the caches -- and | 57 | + connection on a first visit. |
| 51 | - what they cost was a deploy that would not arrive: a new worker waits | ||
| 52 | - until every tab on the origin is closed, so reloading went on serving | ||
| 53 | - the previous bundle and the cure was clearing the site data by hand. | ||
| 54 | --> | 58 | --> |
| 55 | <script> | 59 | <script> |
| 56 | if ("serviceWorker" in navigator) { | 60 | if ("serviceWorker" in navigator) { |
| 57 | - navigator.serviceWorker.getRegistrations() | 61 | + window.addEventListener("load", function () { |
| 58 | - .then(function (rs) { rs.forEach(function (r) { r.unregister(); }); }) | 62 | + navigator.serviceWorker.register("frq_sw.js") |
| 59 | - .catch(function () { /* nothing to undo */ }); | 63 | + .catch(function () { /* a page that will not install still runs */ }); |
| 60 | - } | 64 | + }); |
| 61 | - if (window.caches && caches.keys) { | ||
| 62 | - caches.keys() | ||
| 63 | - .then(function (ks) { ks.forEach(function (k) { caches.delete(k); }); }) | ||
| 64 | - .catch(function () { /* nothing to undo */ }); | ||
| 65 | } | 65 | } |
| 66 | </script> | 66 | </script> |
| 67 | 67 | ||
modified
flutter/web/manifest.json +4 -4 | @@ -3,10 +3,10 @@ | ||
| 3 | 3 | "short_name": "frq", |
| 4 | 4 | "start_url": ".", |
| 5 | 5 | "display": "standalone", |
| 6 | - "background_color": "#0175C2", | |
| 7 | - "theme_color": "#0175C2", | |
| 6 | + "background_color": "#202833", | |
| 7 | + "theme_color": "#202833", | |
| 8 | 8 | "description": "freeq client", |
| 9 | - "orientation": "portrait-primary", | |
| 9 | + "orientation": "any", | |
| 10 | 10 | "prefer_related_applications": false, |
| 11 | 11 | "icons": [ |
| 12 | 12 | { |
| @@ -32,4 +32,4 @@ | ||
| 32 | 32 | "purpose": "maskable" |
| 33 | 33 | } |
| 34 | 34 | ] |
| 35 | -} | |
| \ No newline at end of file | ||
| 35 | +} | |
| @@ -3,10 +3,10 @@ | |||
| 3 | "short_name": "frq", | 3 | "short_name": "frq", |
| 4 | "start_url": ".", | 4 | "start_url": ".", |
| 5 | "display": "standalone", | 5 | "display": "standalone", |
| 6 | - "background_color": "#0175C2", | 6 | + "background_color": "#202833", |
| 7 | - "theme_color": "#0175C2", | 7 | + "theme_color": "#202833", |
| 8 | "description": "freeq client", | 8 | "description": "freeq client", |
| 9 | - "orientation": "portrait-primary", | 9 | + "orientation": "any", |
| 10 | "prefer_related_applications": false, | 10 | "prefer_related_applications": false, |
| 11 | "icons": [ | 11 | "icons": [ |
| 12 | { | 12 | { |
| @@ -32,4 +32,4 @@ | |||
| 32 | "purpose": "maskable" | 32 | "purpose": "maskable" |
| 33 | } | 33 | } |
| 34 | ] | 34 | ] |
| 35 | -} | ||
| \ No newline at end of file | \ No newline at end of file | ||
| 35 | +} | ||
modified
justfile +8 -16 | @@ -194,22 +194,14 @@ _web-bundle: | ||
| 194 | 194 | # is `localhost` only when it is. Guest works everywhere. |
| 195 | 195 | python3 tools/client-metadata.py "${FRQ_WEB_ORIGIN:-http://localhost:8000}" \ |
| 196 | 196 | > flutter/web/client-metadata.json |
| 197 | - # No service worker, via `--pwa-strategy=none` below. Flutter installs | |
| 198 | - # one for offline use and this app has no offline: it is a window onto a | |
| 199 | - # live IRC connection, so a cached copy of it can show nothing anybody | |
| 200 | - # wants. What the worker did instead was hold a deploy back -- a new one | |
| 201 | - # downloads, but the spec leaves it *waiting* until every tab on the | |
| 202 | - # origin is closed, so reloading kept serving the previous bundle and | |
| 203 | - # the only cure was clearing the site data by hand. Twice in one | |
| 204 | - # afternoon a fix looked broken because of it. | |
| 205 | - # | |
| 206 | - # For workers already installed out there it is less immediate than it | |
| 207 | - # looks. `none` still emits `flutter_service_worker.js`, empty -- a 200 | |
| 208 | - # of zero bytes rather than a 404 -- so a browser holding the old one | |
| 209 | - # fetches it, sees it changed, and installs the empty worker, which | |
| 210 | - # waits its turn like any other. It has no fetch handler once it takes | |
| 211 | - # over, so nothing is served from a cache after that; and the page | |
| 212 | - # unregisters it outright, which is what `index.html` does. | |
| 197 | + # `--pwa-strategy=none` leaves out Flutter's service worker, and | |
| 198 | + # `flutter/web/frq_sw.js` is registered in its place. The page is still | |
| 199 | + # a PWA -- a browser will not offer to install one without a worker that | |
| 200 | + # handles fetches -- but not that worker: Flutter's is offline-first, so | |
| 201 | + # a new copy of it waits for every tab on the origin to close before it | |
| 202 | + # takes over, and until then the app serves the bundle it already had. | |
| 203 | + # Twice in one afternoon a working fix read as broken because of it. | |
| 204 | + # Ours is network-first and claims its clients at once. | |
| 213 | 205 | # |
| 214 | 206 | # The comment lives out here rather than inside the quoted script below, |
| 215 | 207 | # which is single-quoted -- an apostrophe in there ends the string, and |
| @@ -194,22 +194,14 @@ _web-bundle: | |||
| 194 | # is `localhost` only when it is. Guest works everywhere. | 194 | # is `localhost` only when it is. Guest works everywhere. |
| 195 | python3 tools/client-metadata.py "${FRQ_WEB_ORIGIN:-http://localhost:8000}" \ | 195 | python3 tools/client-metadata.py "${FRQ_WEB_ORIGIN:-http://localhost:8000}" \ |
| 196 | > flutter/web/client-metadata.json | 196 | > flutter/web/client-metadata.json |
| 197 | - # No service worker, via `--pwa-strategy=none` below. Flutter installs | 197 | + # `--pwa-strategy=none` leaves out Flutter's service worker, and |
| 198 | - # one for offline use and this app has no offline: it is a window onto a | 198 | + # `flutter/web/frq_sw.js` is registered in its place. The page is still |
| 199 | - # live IRC connection, so a cached copy of it can show nothing anybody | 199 | + # a PWA -- a browser will not offer to install one without a worker that |
| 200 | - # wants. What the worker did instead was hold a deploy back -- a new one | 200 | + # handles fetches -- but not that worker: Flutter's is offline-first, so |
| 201 | - # downloads, but the spec leaves it *waiting* until every tab on the | 201 | + # a new copy of it waits for every tab on the origin to close before it |
| 202 | - # origin is closed, so reloading kept serving the previous bundle and | 202 | + # takes over, and until then the app serves the bundle it already had. |
| 203 | - # the only cure was clearing the site data by hand. Twice in one | 203 | + # Twice in one afternoon a working fix read as broken because of it. |
| 204 | - # afternoon a fix looked broken because of it. | 204 | + # Ours is network-first and claims its clients at once. |
| 205 | - # | ||
| 206 | - # For workers already installed out there it is less immediate than it | ||
| 207 | - # looks. `none` still emits `flutter_service_worker.js`, empty -- a 200 | ||
| 208 | - # of zero bytes rather than a 404 -- so a browser holding the old one | ||
| 209 | - # fetches it, sees it changed, and installs the empty worker, which | ||
| 210 | - # waits its turn like any other. It has no fetch handler once it takes | ||
| 211 | - # over, so nothing is served from a cache after that; and the page | ||
| 212 | - # unregisters it outright, which is what `index.html` does. | ||
| 213 | # | 205 | # |
| 214 | # The comment lives out here rather than inside the quoted script below, | 206 | # The comment lives out here rather than inside the quoted script below, |
| 215 | # which is single-quoted -- an apostrophe in there ends the string, and | 207 | # which is single-quoted -- an apostrophe in there ends the string, and |
modified
tools/webserve.py +12 -0 | @@ -45,6 +45,18 @@ LIMIT = 12 * 1024 * 1024 | ||
| 45 | 45 | |
| 46 | 46 | |
| 47 | 47 | class Handler(SimpleHTTPRequestHandler): |
| 48 | + # `http.server` answers HTTP/1.0 and closes the connection after every | |
| 49 | + # response, which is a new one for each of the fifteen-odd files a load | |
| 50 | + # asks for. The base class already sends an accurate Content-Length, | |
| 51 | + # which is what 1.1 needs to be honest about, so keep-alive costs | |
| 52 | + # nothing here. | |
| 53 | + # | |
| 54 | + # It was tried first as a guess at why the preview browser would not | |
| 55 | + # register a service worker, and it was not the reason -- that browser | |
| 56 | + # refuses whatever it is served. Kept because connection reuse is worth | |
| 57 | + # having on its own, not because it fixed anything. | |
| 58 | + protocol_version = "HTTP/1.1" | |
| 59 | + | |
| 48 | 60 | def end_headers(self): |
| 49 | 61 | """Revalidate everything, because no name here promises anything. |
| 50 | 62 | |
| @@ -45,6 +45,18 @@ LIMIT = 12 * 1024 * 1024 | |||
| 45 | 45 | ||
| 46 | 46 | ||
| 47 | class Handler(SimpleHTTPRequestHandler): | 47 | class Handler(SimpleHTTPRequestHandler): |
| 48 | + # `http.server` answers HTTP/1.0 and closes the connection after every | ||
| 49 | + # response, which is a new one for each of the fifteen-odd files a load | ||
| 50 | + # asks for. The base class already sends an accurate Content-Length, | ||
| 51 | + # which is what 1.1 needs to be honest about, so keep-alive costs | ||
| 52 | + # nothing here. | ||
| 53 | + # | ||
| 54 | + # It was tried first as a guess at why the preview browser would not | ||
| 55 | + # register a service worker, and it was not the reason -- that browser | ||
| 56 | + # refuses whatever it is served. Kept because connection reuse is worth | ||
| 57 | + # having on its own, not because it fixed anything. | ||
| 58 | + protocol_version = "HTTP/1.1" | ||
| 59 | + | ||
| 48 | def end_headers(self): | 60 | def end_headers(self): |
| 49 | """Revalidate everything, because no name here promises anything. | 61 | """Revalidate everything, because no name here promises anything. |
| 50 | 62 | ||