Files
route-commerce/public/sw-field.js
T
Tyler 772e23ded8
Deploy to route.crispygoat.com / deploy (push) Successful in 4m7s
fix(pwa): bump field-shell cache to v2 — evicts stale UI for installed PWAs
The Apple HIG polish pass (liquid-glass chrome, iOS SegmentedControl,
ThresholdMeter) was already on origin/main and live, but anyone with
the /water PWA installed on their home screen kept seeing the OLD UI
because the SW cache name never bumped.

Old SW had field-shell-v1 baked into its bytes for every prior deploy.
The activate handler only deletes caches that aren't SHELL_CACHE or
DATA_CACHE and start with 'field-'. Same name = no eviction = stale
HTML + chunks forever.

Bumping SHELL_CACHE to 'field-shell-v2' makes the SW bytes change,
which triggers the normal SW upgrade flow:
  install → skipWaiting → activate → claim clients
The activate handler then evicts field-shell-v1 (it's now a foreign
field-* cache). On next navigation the new SHELL_CACHE is repopulated
with the fresh /water HTML + static assets, and the user gets the new
UI.
2026-07-06 10:39:52 -06:00

135 lines
4.1 KiB
JavaScript

// public/sw-field.js — Tuxedo field-worker service worker.
//
// Cycle 6 sub-PWA, scoped to /water. Tighter cache strategy than
// the main shell SW because:
// - The field app is a thin surface (PIN, headgate list, log form,
// time tab). Few async chunks; mostly static + the latest data
// snapshot.
// - "Offline" here means "still see the cached PIN screen + headgate
// list" — water entry without connectivity is a degraded but
// non-blocking experience (the form queue handles drafts).
//
// Cache names are deliberately prefixed with `field-` so they never
// collide with the main `rc-shell-v*` / `rc-data-v*` caches.
// Bumped to v2 after the Apple HIG polish pass (liquid-glass chrome,
// iOS SegmentedControl, ThresholdMeter). The activate handler keeps
// only this name + DATA_CACHE — anything else starting with `field-`
// (including the old v1) is evicted on the next SW activation, which
// happens automatically because the bytes of this file changed.
const SHELL_CACHE = "field-shell-v2";
const DATA_CACHE = "field-data-v1";
const OFFLINE_URL = "/offline.html";
const SHELL_ASSETS = [
"/water",
"/manifest-field.json",
"/favicon.svg",
"/og-default.svg",
"/offline.html",
"/icons/icon-192x192.png",
"/icons/icon-512x512.png",
];
self.addEventListener("install", (event) => {
event.waitUntil(
caches.open(SHELL_CACHE).then((cache) => cache.addAll(SHELL_ASSETS))
);
self.skipWaiting();
});
self.addEventListener("activate", (event) => {
// Only manage field-* caches — never touch the main app's caches.
event.waitUntil(
Promise.all([
caches.keys().then((cacheNames) => {
const toDelete = [];
for (const name of cacheNames) {
if (
name !== SHELL_CACHE &&
name !== DATA_CACHE &&
name.startsWith("field-")
) {
toDelete.push(caches.delete(name));
}
}
return Promise.all(toDelete);
}),
self.clients.claim(),
])
);
});
self.addEventListener("fetch", (event) => {
const req = event.request;
if (req.method !== "GET") return;
const url = new URL(req.url);
if (url.origin !== self.location.origin) return;
// Out-of-scope requests: skip (let the main app SW handle them
// if it's installed for that scope; fall through to network).
if (!url.pathname.startsWith("/water")) return;
// Navigations → network-first, fall back to cache, fall back to offline page.
if (req.mode === "navigate") {
event.respondWith(
fetch(req)
.then((res) => {
const clone = res.clone();
caches.open(SHELL_CACHE).then((cache) => cache.put(req, clone));
return res;
})
.catch(() =>
caches.match(req).then(
(cached) => cached ?? caches.match(OFFLINE_URL),
),
),
);
return;
}
// Static assets → cache-first
if (
url.pathname.startsWith("/_next/static/") ||
url.pathname.startsWith("/icons/") ||
url.pathname.endsWith(".svg") ||
url.pathname.endsWith(".woff2")
) {
event.respondWith(
caches.match(req).then((cached) => {
if (cached) return cached;
return fetch(req).then((res) => {
const clone = res.clone();
if (res.status === 200) {
caches.open(SHELL_CACHE).then((cache) => cache.put(req, clone));
}
return res;
});
}),
);
return;
}
// /api/water* GETs → stale-while-revalidate (so the headgate list
// and worker availability can hydrate from cache while we re-fetch).
if (url.pathname.startsWith("/api/water")) {
event.respondWith(
caches.open(DATA_CACHE).then((cache) =>
cache.match(req).then((cached) => {
const network = fetch(req)
.then((res) => {
if (res.status === 200) cache.put(req, res.clone());
return res;
})
.catch(() => cached);
return cached ?? network;
}),
),
);
return;
}
// Default: try network, fall back to cache.
event.respondWith(fetch(req).catch(() => caches.match(req)));
});