mirror of
https://github.com/HKUDS/nanobot.git
synced 2026-08-12 23:29:16 +03:00
- 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.
132 lines
4.6 KiB
JavaScript
132 lines
4.6 KiB
JavaScript
const CACHE_NAME = "nanobot-static-v1";
|
|
const PRECACHE = ["/", "/manifest.json"];
|
|
|
|
self.addEventListener("install", (event) => {
|
|
event.waitUntil(
|
|
caches.open(CACHE_NAME).then((cache) => cache.addAll(PRECACHE))
|
|
);
|
|
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))
|
|
)
|
|
)
|
|
.then(() => pruneStaleEntries())
|
|
);
|
|
self.clients.claim();
|
|
});
|
|
|
|
self.addEventListener("fetch", (event) => {
|
|
const { request } = event;
|
|
|
|
// 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, 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("/webui")
|
|
) {
|
|
return;
|
|
}
|
|
|
|
// 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;
|
|
return fetch(request).then((response) => {
|
|
if (response.ok) {
|
|
const clone = response.clone();
|
|
caches.open(CACHE_NAME).then((c) => c.put(request, clone));
|
|
}
|
|
return response;
|
|
});
|
|
})
|
|
);
|
|
return;
|
|
}
|
|
|
|
// 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(() => {
|
|
// 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);
|
|
})
|
|
);
|
|
});
|