From 2b4731185ce58f2adfecdbda7fa79fa412be10ff Mon Sep 17 00:00:00 2001 From: Alexander Heldt Date: Sun, 12 Jul 2026 17:13:13 +0000 Subject: [PATCH] Keep PWA builds coherent by bypassing the HTTP cache MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The service worker populated a new build's cache with plain addAll(), which the browser may satisfy from its HTTP cache — and static assets were served without Cache-Control, so Safari's heuristic caching could hold app.js for days. Together that could install a mixed build: a fresh index.html whose buttons reference listeners a stale app.js never registers. Install now fetches assets with cache: "reload", and the server marks all static assets no-cache (revalidation is a cheap 304; fast/offline loads are the SW cache's job anyway). --- server/main.go | 9 +++++---- src/sw.js | 8 +++++++- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/server/main.go b/server/main.go index 485b94e..9452812 100644 --- a/server/main.go +++ b/server/main.go @@ -806,12 +806,13 @@ func main() { serveSW(w, *staticDir, swVer) return } - // PWA: manifest.json must revalidate so updates propagate. - if r.URL.Path == "/manifest.json" { - w.Header().Set("Cache-Control", "no-cache") - } // SPA fallback: unknown paths -> index.html (so deep links work). if !strings.HasPrefix(r.URL.Path, "/api/") { + // All static assets revalidate on every request (cheap 304s via + // Last-Modified). Offline/fast loads are the service worker + // cache's job; leaving these to the browser's heuristic HTTP + // caching let a stale app.js pair with a fresh index.html. + w.Header().Set("Cache-Control", "no-cache") candidate := filepath.Join(*staticDir, filepath.FromSlash(r.URL.Path)) if r.URL.Path != "/" { if info, err := os.Stat(candidate); err != nil || info.IsDir() { diff --git a/src/sw.js b/src/sw.js index 588cedb..b3a05c8 100644 --- a/src/sw.js +++ b/src/sw.js @@ -16,8 +16,14 @@ const ASSETS = [ ]; self.addEventListener("install", (event) => { + // cache: "reload" bypasses the browser's HTTP cache, so a new build always + // caches assets fetched fresh from the server. Without it, addAll could mix + // a fresh index.html with a heuristically-cached stale app.js and install a + // build whose markup references listeners the old script never registers. event.waitUntil( - caches.open(CACHE).then((cache) => cache.addAll(ASSETS)) + caches.open(CACHE).then((cache) => + cache.addAll(ASSETS.map((u) => new Request(u, { cache: "reload" }))) + ) ); // No skipWaiting() here: a new worker stays in "waiting" while an old one is // controlling a tab, so the page can prompt before swapping assets out from