094af3906d
changelog.json is a hand-maintained, newest-first list of user-visible changes; it is part of the SW asset cache and the build hash, so even a changelog-only edit rolls a new version. When the update banner appears, the app fetches changelog.json twice: the plain URL is answered cache-first by the old controlling worker (the running build's copy) while a cache-busting query bypasses every SW cache and hits the network (the waiting build's copy). Entries the fresh copy has that the cached one lacks are exactly what the update brings, and are listed under the banner message.
95 lines
3.1 KiB
JavaScript
95 lines
3.1 KiB
JavaScript
// BUILD is substituted per-deploy by the server with a hash of the static
|
|
// assets (see serveSW in server/main.go), so the cache name — and therefore the
|
|
// bytes of this file — change whenever any asset changes. That byte difference
|
|
// is what makes the browser install a new worker and surface the update prompt.
|
|
// Served unsubstituted (dev / a plain static host) it stays a valid constant.
|
|
const BUILD = "__BUILD_HASH__";
|
|
const CACHE = `puppy-tracker-${BUILD}`;
|
|
const PHOTO_CACHE = "puppy-tracker-photos-v1";
|
|
const ASSETS = [
|
|
"./",
|
|
"./index.html",
|
|
"./style.css",
|
|
"./app.js",
|
|
"./manifest.json",
|
|
"./icon.svg",
|
|
"./changelog.json",
|
|
];
|
|
|
|
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.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
|
|
// under it. The page tells us to activate via a SKIP_WAITING message.
|
|
});
|
|
|
|
self.addEventListener("message", (event) => {
|
|
if (event.data && event.data.type === "SKIP_WAITING") self.skipWaiting();
|
|
});
|
|
|
|
self.addEventListener("activate", (event) => {
|
|
event.waitUntil(
|
|
caches.keys().then((keys) =>
|
|
Promise.all(
|
|
keys
|
|
.filter((k) => k !== CACHE && k !== PHOTO_CACHE)
|
|
.map((k) => caches.delete(k))
|
|
)
|
|
)
|
|
);
|
|
self.clients.claim();
|
|
});
|
|
|
|
self.addEventListener("fetch", (event) => {
|
|
const req = event.request;
|
|
if (req.method !== "GET") return;
|
|
|
|
const url = new URL(req.url);
|
|
|
|
// Photos are immutable per UUID — cache-first forever and stuff every
|
|
// successful fetch into a dedicated cache so they survive between visits
|
|
// and are available offline once seen.
|
|
if (url.pathname.includes("/api/photos/")) {
|
|
event.respondWith(
|
|
caches.open(PHOTO_CACHE).then(async (cache) => {
|
|
const cached = await cache.match(req);
|
|
if (cached) return cached;
|
|
try {
|
|
const res = await fetch(req);
|
|
if (res && res.ok) cache.put(req, res.clone());
|
|
return res;
|
|
} catch (err) {
|
|
return cached || Response.error();
|
|
}
|
|
})
|
|
);
|
|
return;
|
|
}
|
|
|
|
// Other API calls: never cache — sync must reflect live server state.
|
|
if (url.pathname.includes("/api/")) return;
|
|
|
|
event.respondWith(
|
|
caches.match(req).then((cached) => {
|
|
if (cached) return cached;
|
|
return fetch(req)
|
|
.then((res) => {
|
|
if (res && res.ok && url.origin === self.location.origin) {
|
|
const clone = res.clone();
|
|
caches.open(CACHE).then((c) => c.put(req, clone));
|
|
}
|
|
return res;
|
|
})
|
|
.catch(() => caches.match("./index.html"));
|
|
})
|
|
);
|
|
});
|