fix(sw, assets, install): bypass HTTP cache + revalidate assets + mobile-only install banner (t-paliad-043 step 3)
After step 2 deployed the IIFE-wrapped bundles, m's browser still saw
the broken page because /assets/projects.js was being served from the
local HTTP cache (no Cache-Control, just heuristic freshness from
Last-Modified). Even after the new SW activated and cleared its own
caches, its cacheFirst handler did `fetch(req)` which goes through the
browser HTTP cache — re-fed the SW cache from the stale bundle and the
loop perpetuated forever.
Three mutually reinforcing fixes:
1. SW cacheFirst now does `fetch(req, { cache: "reload" })` for the
network leg. Forces the network fetch to bypass the browser's HTTP
cache, so the SW always seeds its own cache from a true network read.
2. Go static handlers for /assets/* and /icons/* set
`Cache-Control: no-cache, must-revalidate`. Combined with the
Last-Modified that http.FileServer already emits, browsers send
If-Modified-Since and the server replies 304 when unchanged — fast
for repeat loads, fresh on every deploy. Users without a SW (or after
the kill-switch unregistered theirs) now also pick up new bundles
immediately.
3. pwa-install.ts gates the install banner on
`(min-width: 768px)` — same breakpoint the BottomNav and other
mobile-shell elements use. Desktop partners no longer get an install
prompt covering their work area.
This commit is contained in:
@@ -56,7 +56,12 @@ async function cacheFirst(req) {
|
||||
const cached = await cache.match(req);
|
||||
if (cached) return cached;
|
||||
try {
|
||||
const res = await fetch(req);
|
||||
// cache: "reload" forces the network leg to BYPASS the browser HTTP
|
||||
// cache. Without this, a stale /assets/projects.js sitting in the
|
||||
// browser's disk cache from a previous deploy would be returned to us,
|
||||
// we'd cache it again, and the user would be stuck on the old bundle
|
||||
// forever — exactly the failure mode that caused t-paliad-043.
|
||||
const res = await fetch(req, { cache: "reload" });
|
||||
if (res && res.ok) cache.put(req, res.clone());
|
||||
return res;
|
||||
} catch (err) {
|
||||
|
||||
@@ -22,6 +22,12 @@ let deferredPrompt: BeforeInstallPromptEvent | null = null;
|
||||
export function initInstallPrompt(): void {
|
||||
if (localStorage.getItem(DISMISS_KEY) === "1") return;
|
||||
|
||||
// The install prompt is part of the PWA mobile shell — same audience as
|
||||
// the BottomNav. On desktop, partners use the sidebar nav and don't want
|
||||
// an install banner overlapping their work area. Gate on the same 768px
|
||||
// breakpoint as global.css's mobile media queries.
|
||||
if (window.matchMedia("(min-width: 768px)").matches) return;
|
||||
|
||||
// Skip when already running as an installed PWA.
|
||||
if (window.matchMedia("(display-mode: standalone)").matches) return;
|
||||
// Safari iOS exposes navigator.standalone for the same signal.
|
||||
|
||||
@@ -10,6 +10,17 @@ import (
|
||||
|
||||
var authClient *auth.Client
|
||||
|
||||
// noCacheAssets wraps a static-file handler so /assets/* and /icons/* always
|
||||
// trigger a conditional GET. http.FileServer already emits Last-Modified, so
|
||||
// browsers send If-Modified-Since on the next visit and the server replies
|
||||
// 304 when the file is unchanged — fast for repeat loads, fresh on deploy.
|
||||
func noCacheAssets(h http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Cache-Control", "no-cache, must-revalidate")
|
||||
h.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
// Services bundles the Phase B + C database-backed services. Pass nil if
|
||||
// DATABASE_URL was unset; the matter-management endpoints will return 503.
|
||||
type Services struct {
|
||||
@@ -71,15 +82,21 @@ func Register(mux *http.ServeMux, client *auth.Client, giteaAPIToken string, svc
|
||||
// doesn't bounce unauthenticated visitors to /login.
|
||||
mux.HandleFunc("GET /{$}", handleRootPage)
|
||||
|
||||
// Static assets (public)
|
||||
mux.Handle("GET /assets/", http.StripPrefix("/assets/", http.FileServer(http.Dir("dist/assets"))))
|
||||
// Static assets (public). Cache-Control: no-cache forces browsers to
|
||||
// revalidate every load. Combined with the Last-Modified header that
|
||||
// http.FileServer emits, repeat visits get a fast 304 when assets are
|
||||
// unchanged but pick up a fresh bundle the moment we deploy. Without
|
||||
// this, browsers were heuristically caching /assets/*.js for the whole
|
||||
// freshness window and continuing to execute the previous deploy's
|
||||
// (broken) bundle even after the SW had been kill-switched (t-paliad-043).
|
||||
mux.Handle("GET /assets/", noCacheAssets(http.StripPrefix("/assets/", http.FileServer(http.Dir("dist/assets")))))
|
||||
|
||||
// PWA static surface (public): manifest, icons, service worker. The SW
|
||||
// must be served from the application origin; its scope is /, so the file
|
||||
// has to live at /sw.js (a SW served from /assets/sw.js could only claim
|
||||
// /assets/* by default).
|
||||
mux.HandleFunc("GET /manifest.json", servePWAManifest)
|
||||
mux.Handle("GET /icons/", http.StripPrefix("/icons/", http.FileServer(http.Dir("dist/icons"))))
|
||||
mux.Handle("GET /icons/", noCacheAssets(http.StripPrefix("/icons/", http.FileServer(http.Dir("dist/icons")))))
|
||||
mux.HandleFunc("GET /sw.js", servePWAServiceWorker)
|
||||
|
||||
// Protected routes
|
||||
|
||||
Reference in New Issue
Block a user