Removing the worker, not replacing it with an empty one
A correction to the last commit. It said that with no `flutter_service_worker.js` to fetch, a browser update check would 404 and drop the registration. `--pwa-strategy=none` does still emit that file -- empty, a 200 of zero bytes -- so a browser holding the old worker replaces it with a no-op rather than losing it, and waits its turn to do even that. A no-op registration is still a registration. So the page removes it: `index.html` unregisters any worker and drops any cache it left. Nothing is lost with them, this app having no offline worth the name, and what they cost was a deploy that would not arrive. What is verified and what is not. A fresh load registers nothing -- checked in a browser against the built bundle: no registrations, no controller, no caches. The deployed origin now answers `cache-control: no-cache` and 304s a conditional request, both checked against the live URL. The cleanup path itself is not: the preview browser refuses to register a worker over this local server at all, so there was no old one to remove. It is wrapped in catch either way. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1c7cb6d parent: 364ea65 modified
.modal/web/Dockerfile +3 -1 | @@ -32,7 +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. The core is compiled to | |
| 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 | |
| 36 | 38 | # JavaScript and copied into `flutter/web/` rather than into the output, |
| 37 | 39 | # because `flutter build web` copies that directory into the bundle -- which |
| 38 | 40 | # is what makes the page's `<script src="frq_core.js">` resolve the same |
| @@ -32,7 +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. The core is compiled to | 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 | ||
| 36 | # JavaScript and copied into `flutter/web/` rather than into the output, | 38 | # JavaScript and copied into `flutter/web/` rather than into the output, |
| 37 | # because `flutter build web` copies that directory into the bundle -- which | 39 | # because `flutter build web` copies that directory into the bundle -- which |
| 38 | # 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 |
modified
flutter/web/index.html +28 -0 | @@ -37,6 +37,34 @@ | ||
| 37 | 37 | uses it, which this page needs because freeq's broker will only redirect |
| 38 | 38 | back to loopback or its own origins — and this is neither. |
| 39 | 39 | --> |
| 40 | + <!-- | |
| 41 | + No service worker, and no leftovers of one. | |
| 42 | + | |
| 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. | |
| 48 | + | |
| 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. | |
| 54 | + --> | |
| 55 | + <script> | |
| 56 | + 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 */ }); | |
| 65 | + } | |
| 66 | + </script> | |
| 67 | + | |
| 40 | 68 | <script src="frq_core.js"></script> |
| 41 | 69 | <script src="frq_dpop.js"></script> |
| 42 | 70 | <script src="frq_oauth.js"></script> |
| @@ -37,6 +37,34 @@ | |||
| 37 | uses it, which this page needs because freeq's broker will only redirect | 37 | uses it, which this page needs because freeq's broker will only redirect |
| 38 | back to loopback or its own origins — and this is neither. | 38 | back to loopback or its own origins — and this is neither. |
| 39 | --> | 39 | --> |
| 40 | + <!-- | ||
| 41 | + No service worker, and no leftovers of one. | ||
| 42 | + | ||
| 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. | ||
| 48 | + | ||
| 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. | ||
| 54 | + --> | ||
| 55 | + <script> | ||
| 56 | + 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 */ }); | ||
| 65 | + } | ||
| 66 | + </script> | ||
| 67 | + | ||
| 40 | <script src="frq_core.js"></script> | 68 | <script src="frq_core.js"></script> |
| 41 | <script src="frq_dpop.js"></script> | 69 | <script src="frq_dpop.js"></script> |
| 42 | <script src="frq_oauth.js"></script> | 70 | <script src="frq_oauth.js"></script> |
modified
justfile +7 -3 | @@ -203,9 +203,13 @@ _web-bundle: | ||
| 203 | 203 | # the only cure was clearing the site data by hand. Twice in one |
| 204 | 204 | # afternoon a fix looked broken because of it. |
| 205 | 205 | # |
| 206 | - # It also retires the workers already installed out there: with no | |
| 207 | - # `flutter_service_worker.js` left to fetch, a browser update check 404s | |
| 208 | - # and the registration is dropped. | |
| 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. | |
| 209 | 213 | # |
| 210 | 214 | # The comment lives out here rather than inside the quoted script below, |
| 211 | 215 | # which is single-quoted -- an apostrophe in there ends the string, and |
| @@ -203,9 +203,13 @@ _web-bundle: | |||
| 203 | # the only cure was clearing the site data by hand. Twice in one | 203 | # the only cure was clearing the site data by hand. Twice in one |
| 204 | # afternoon a fix looked broken because of it. | 204 | # afternoon a fix looked broken because of it. |
| 205 | # | 205 | # |
| 206 | - # It also retires the workers already installed out there: with no | 206 | + # For workers already installed out there it is less immediate than it |
| 207 | - # `flutter_service_worker.js` left to fetch, a browser update check 404s | 207 | + # looks. `none` still emits `flutter_service_worker.js`, empty -- a 200 |
| 208 | - # and the registration is dropped. | 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. | ||
| 209 | # | 213 | # |
| 210 | # The comment lives out here rather than inside the quoted script below, | 214 | # The comment lives out here rather than inside the quoted script below, |
| 211 | # which is single-quoted -- an apostrophe in there ends the string, and | 215 | # which is single-quoted -- an apostrophe in there ends the string, and |