A reload offer only for a page that is older, and a tab switch that moves one scroller
The update toast fired whenever a service worker was waiting, so after every release it appeared on pages that were already the release -- including one fetched with Ctrl+Shift+R. It now compares the waiting worker's release with the page's own, and so does the reload that follows another tab accepting it. On Admin -> Prompts, visually hidden labels were positioned against the page and made the document 6771px tall behind an overflow-hidden root; the tab handler's scrollIntoView then scrolled that root and lifted the shell 56px. Every scroll region is now a containing block, and the handler moves only the container that scrolls. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1099,27 +1099,59 @@
|
||||
The worker no longer takes over open pages on its own -- see sw.js -- so
|
||||
something has to say that one is waiting, and the reader decides. A toast
|
||||
rather than a reload: an application with a reply streaming into it must
|
||||
not be navigated out from under somebody. */
|
||||
not be navigated out from under somebody.
|
||||
|
||||
🚨 "A worker is waiting" is not the same as "this page is out of date",
|
||||
and the toast used to treat them as one. After a release it offered a
|
||||
reload on every page, including one just fetched with Ctrl+Shift+R, and
|
||||
reloading could not make it stop. A page is always fetched from the network
|
||||
and every asset it names carries `?v=<release>`, so a page loaded after
|
||||
the update IS the update, whichever worker happens to control it. And the
|
||||
worker that controls it is nearly always the previous one: a reload
|
||||
creates the new page before the old one goes away, so the old worker
|
||||
never runs out of pages and the new one never stops waiting.
|
||||
|
||||
So the question is asked of the page. The worker's release is in its own
|
||||
script URL (`/sw.js?v=`), and the page's is `window.lembasRelease` from
|
||||
base.html. When the two match there is nothing newer to reload into, and
|
||||
the worker is left to take over once the old tabs are closed. */
|
||||
var PAGE_RELEASE = window.lembasRelease || "";
|
||||
|
||||
function releaseOf(worker) {
|
||||
try {
|
||||
return new URL(worker.scriptURL).searchParams.get("v") || "";
|
||||
} catch (error) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
/* Unknown on either side counts as newer: better an extra offer than a
|
||||
release nobody is told about. */
|
||||
function isNewerThanThisPage(worker) {
|
||||
var release = releaseOf(worker);
|
||||
return !PAGE_RELEASE || !release || release !== PAGE_RELEASE;
|
||||
}
|
||||
|
||||
function offerReload(worker) {
|
||||
window.lembas.notify(
|
||||
"A new version is ready. Reload to use it.",
|
||||
{ kind: "info", action: { label: "Reload", run: function () {
|
||||
worker.postMessage({ type: "SKIP_WAITING" });
|
||||
} } }
|
||||
);
|
||||
}
|
||||
|
||||
function watchForUpdate(registration) {
|
||||
function offer(worker) {
|
||||
if (!worker || !navigator.serviceWorker.controller) return;
|
||||
worker.addEventListener("statechange", function () {
|
||||
if (worker.state !== "installed") return;
|
||||
window.lembas.notify(
|
||||
"A new version is ready. Reload to use it.",
|
||||
{ kind: "info", action: { label: "Reload", run: function () {
|
||||
worker.postMessage({ type: "SKIP_WAITING" });
|
||||
} } }
|
||||
);
|
||||
if (isNewerThanThisPage(worker)) offerReload(worker);
|
||||
});
|
||||
}
|
||||
if (registration.waiting && navigator.serviceWorker.controller) {
|
||||
window.lembas.notify(
|
||||
"A new version is ready. Reload to use it.",
|
||||
{ kind: "info", action: { label: "Reload", run: function () {
|
||||
registration.waiting.postMessage({ type: "SKIP_WAITING" });
|
||||
} } }
|
||||
);
|
||||
if (registration.waiting && navigator.serviceWorker.controller &&
|
||||
isNewerThanThisPage(registration.waiting)) {
|
||||
offerReload(registration.waiting);
|
||||
}
|
||||
registration.addEventListener("updatefound", function () {
|
||||
offer(registration.installing);
|
||||
@@ -1130,17 +1162,24 @@
|
||||
the right answer to it -- the page is now being served by a worker whose
|
||||
cache it did not start from.
|
||||
|
||||
Two guards, and the second is the one that is easy to miss. A flag, because
|
||||
`controllerchange` can fire more than once. And `hadController`, because on
|
||||
a *first* visit there is no worker at all: the one that installs then calls
|
||||
`clients.claim()`, which fires this event for the first time -- so without
|
||||
it, the very first page anybody loads reloads itself in front of them for
|
||||
no reason they could possibly work out. */
|
||||
Three guards, and the second is the one that is easy to miss. A flag,
|
||||
because `controllerchange` can fire more than once. And `hadController`,
|
||||
because on a *first* visit there is no worker at all: the one that
|
||||
installs then calls `clients.claim()`, which fires this event for the
|
||||
first time -- so without it, the very first page anybody loads reloads
|
||||
itself in front of them for no reason they could possibly work out.
|
||||
|
||||
The third is the same question as the toast's. Somebody pressing Reload in
|
||||
one tab activates the worker for all of them, and a tab that was already
|
||||
rendered by that release has nothing to gain from a reload -- and may have
|
||||
a reply streaming into it. */
|
||||
var reloading = false;
|
||||
if ("serviceWorker" in navigator) {
|
||||
var hadController = !!navigator.serviceWorker.controller;
|
||||
navigator.serviceWorker.addEventListener("controllerchange", function () {
|
||||
if (reloading || !hadController) return;
|
||||
var controller = navigator.serviceWorker.controller;
|
||||
if (controller && !isNewerThanThisPage(controller)) return;
|
||||
reloading = true;
|
||||
window.location.reload();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user