From e3dc32480918b8762e80e4b5b376751da157234f Mon Sep 17 00:00:00 2001 From: Steven Vandevelde Date: Tue, 19 May 2026 10:50:59 +0200 Subject: [PATCH] chore: service worker improvements --- src/common/loader.js | 14 ++++++++++++++ src/default-layout.js | 18 ++++++++++-------- 2 files changed, 24 insertions(+), 8 deletions(-) diff --git a/src/common/loader.js b/src/common/loader.js index 2660555e..eee42d9a 100644 --- a/src/common/loader.js +++ b/src/common/loader.js @@ -12,6 +12,19 @@ import { import * as CID from "~/common/cid.js"; import { effect } from "~/common/signal.js"; +// When the service worker takes control (clients.claim()), the page is about +// to reload (see default-layout.js sw-activated handler). Any fetch() calls +// in flight at that moment will be cancelled by the navigation and throw a +// NetworkError. We detect the controller change here so we can suppress those +// spurious errors rather than flashing an error UI before the reload. +let swControllerChanging = false; + +if ("serviceWorker" in navigator) { + navigator.serviceWorker.addEventListener("controllerchange", () => { + swControllerChanging = true; + }); +} + /** * @import {SignalReader} from "~/common/signal.d.ts" */ @@ -103,6 +116,7 @@ export function createLoader(config) { // Make sure HTML is loaded when a URI is specified await ensureHTML(item).catch((err) => { + if (swControllerChanging) return; renderError(container, `Failed to load URI: ${item.uri}`, { context: err, throw: true, diff --git a/src/default-layout.js b/src/default-layout.js index e3e9a528..e2b8a7b3 100644 --- a/src/default-layout.js +++ b/src/default-layout.js @@ -9,18 +9,20 @@ if ("serviceWorker" in navigator) { }); // When the SW activates it sends "sw-activated". Reload so the page runs - // fresh code under the new SW. The sessionStorage flag skips one reload on - // the very next page load to break the loop caused by the SW script URL - // changing during the reload (e.g. esbuild chunk-hash churn in development). - // The listener is always attached so a second activation in the same load - // isn't silently dropped. + // fresh code under the new SW. To break the loop that can occur when the SW + // script itself changes between reloads (esbuild chunk-hash churn in dev), + // we store a timestamp. If a second sw-activated arrives within a few seconds + // of the previous reload it is the loop — skip it. A stale timestamp means a + // genuinely new SW arrived later, so we reload again. + const RELOAD_GUARD_MS = 5000; navigator.serviceWorker.addEventListener("message", (event) => { if (event.data?.type !== "sw-activated") return; - if (sessionStorage.getItem("sw-activated-reload")) { + const flag = sessionStorage.getItem("sw-activated-reload"); + if (flag) { sessionStorage.removeItem("sw-activated-reload"); - return; + if (Date.now() - Number(flag) < RELOAD_GUARD_MS) return; } - sessionStorage.setItem("sw-activated-reload", "1"); + sessionStorage.setItem("sw-activated-reload", String(Date.now())); location.reload(); }); } -- 2.51.2