mirror of
https://github.com/HKUDS/nanobot.git
synced 2026-08-15 16:49:24 +03:00
fix(webui): harden PWA service worker caching and registration
- Exclude /webui/* endpoints from the service worker cache; the /webui/bootstrap endpoint issues fresh gateway credentials on every load and must never be cached or replayed offline. - Restrict cache-first handling to hashed /assets/ files (served immutable). Un-hashed brand icons and the favicon stay network-first so future icon swaps reach installed clients. - Prune stale hashed assets whenever the app shell refreshes, so old build assets cannot accumulate even when sw.js itself is unchanged. - Serve the cached app shell for offline deep-link navigations. - Ignore service worker registration failures; add unit tests for the service worker and the main-entry registration.
This commit is contained in:
+72
-13
@@ -8,15 +8,52 @@ self.addEventListener("install", (event) => {
|
||||
self.skipWaiting();
|
||||
});
|
||||
|
||||
// Collect same-origin paths referenced by the given HTML document.
|
||||
function referencedAssetPaths(html) {
|
||||
const refs = new Set();
|
||||
const re = /(?:src|href)="(\/[^"]*)"/g;
|
||||
let match;
|
||||
while ((match = re.exec(html))) {
|
||||
const url = new URL(match[1], self.location.origin);
|
||||
if (url.origin === self.location.origin) refs.add(url.pathname + url.search);
|
||||
}
|
||||
return refs;
|
||||
}
|
||||
|
||||
// Drop cached entries that the current index.html no longer references.
|
||||
// CACHE_NAME is stable across deployments, so without this, hashed assets from
|
||||
// previous builds would pile up in the same cache forever. The cached
|
||||
// index.html is the latest one this client saw (navigation is network-first
|
||||
// and overwrites it on every successful visit), so pruning against it keeps
|
||||
// the offline shell consistent with the last loaded build.
|
||||
async function pruneStaleEntries() {
|
||||
const cache = await caches.open(CACHE_NAME);
|
||||
const cachedIndex = await cache.match("/");
|
||||
if (!cachedIndex) return;
|
||||
const refs = referencedAssetPaths(await cachedIndex.text());
|
||||
const keys = await cache.keys();
|
||||
await Promise.all(
|
||||
keys.map(async (request) => {
|
||||
const url = new URL(request.url);
|
||||
if (url.pathname === "/" || url.pathname === "/manifest.json") return;
|
||||
if (refs.has(url.pathname + url.search)) return;
|
||||
await cache.delete(request);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
self.addEventListener("activate", (event) => {
|
||||
event.waitUntil(
|
||||
caches.keys().then((keys) =>
|
||||
Promise.all(
|
||||
keys
|
||||
.filter((k) => k !== CACHE_NAME)
|
||||
.map((k) => caches.delete(k))
|
||||
caches
|
||||
.keys()
|
||||
.then((keys) =>
|
||||
Promise.all(
|
||||
keys
|
||||
.filter((k) => k !== CACHE_NAME)
|
||||
.map((k) => caches.delete(k))
|
||||
)
|
||||
)
|
||||
)
|
||||
.then(() => pruneStaleEntries())
|
||||
);
|
||||
self.clients.claim();
|
||||
});
|
||||
@@ -24,24 +61,37 @@ self.addEventListener("activate", (event) => {
|
||||
self.addEventListener("fetch", (event) => {
|
||||
const { request } = event;
|
||||
|
||||
// Only handle same-origin GET requests
|
||||
// Only handle same-origin GET requests. Requests are handed to fetch() as-is
|
||||
// (never reconstructed), so their credentials mode is preserved and gateway
|
||||
// auth cookies flow through on every path we touch. WebSocket upgrades are
|
||||
// never dispatched to a service worker's fetch handler, so the WS endpoint
|
||||
// cannot be cached; the /__nanobot exclusion below still protects its HTTP
|
||||
// polling/socket bootstrap endpoints.
|
||||
if (request.method !== "GET") return;
|
||||
if (new URL(request.url).origin !== self.location.origin) return;
|
||||
|
||||
const url = new URL(request.url);
|
||||
const path = url.pathname;
|
||||
|
||||
// Never cache API, auth, WebSocket, or HMR paths
|
||||
// Never cache API, auth, WebSocket, HMR, or WebUI endpoint paths. In
|
||||
// particular /webui/bootstrap issues fresh gateway credentials on every page
|
||||
// load and must never be cached or replayed offline. The /auth prefix covers
|
||||
// the default token endpoint; custom token_issue_path values should be kept
|
||||
// under one of these prefixes.
|
||||
if (
|
||||
path.startsWith("/api") ||
|
||||
path.startsWith("/auth") ||
|
||||
path.startsWith("/__nanobot")
|
||||
path.startsWith("/__nanobot") ||
|
||||
path.startsWith("/webui")
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Static assets: cache-first (immutable by gateway)
|
||||
if (/\.(js|css|png|webp|ico|svg|woff2?|ttf|eot)$/.test(path)) {
|
||||
// Static assets: cache-first. Only files under /assets/ carry content hashes
|
||||
// (the gateway serves them immutable); brand icons, the favicon and other
|
||||
// un-hashed files can change between releases and stay on the network-first
|
||||
// path below so updates reach installed clients.
|
||||
if (path.startsWith("/assets/")) {
|
||||
event.respondWith(
|
||||
caches.match(request).then((cached) => {
|
||||
if (cached) return cached;
|
||||
@@ -57,16 +107,25 @@ self.addEventListener("fetch", (event) => {
|
||||
return;
|
||||
}
|
||||
|
||||
// Everything else: network-first (index.html, manifest, etc.)
|
||||
// Everything else: network-first (index.html, manifest, brand assets, etc.)
|
||||
event.respondWith(
|
||||
fetch(request)
|
||||
.then((response) => {
|
||||
if (response.ok) {
|
||||
const clone = response.clone();
|
||||
caches.open(CACHE_NAME).then((c) => c.put(request, clone));
|
||||
// The shell just changed; prune entries the new index.html no longer
|
||||
// references so hashed assets from old builds do not accumulate even
|
||||
// when sw.js itself is unchanged between deployments.
|
||||
if (path === "/") pruneStaleEntries();
|
||||
}
|
||||
return response;
|
||||
})
|
||||
.catch(() => caches.match(request))
|
||||
.catch(() => {
|
||||
// Offline: serve the app shell for navigations (deep links resolve
|
||||
// client-side), the last cached copy for everything else.
|
||||
if (request.mode === "navigate") return caches.match("/");
|
||||
return caches.match(request);
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user