Files
HiveGrid-Wireframes/hivegrid-cloud/apps/web/public/sw.js
T

101 lines
3.0 KiB
JavaScript

/* eslint-disable no-restricted-globals */
/**
* Minimal service worker for Toolctrl Boilerplate.
*
* Strategy:
* - Static, hashed asset requests under /assets/ → cache-first (long-term
* immutable cache; the React-Router build emits hashed filenames so we
* can keep stale entries until the next deploy bumps the version).
* - /api/ and /app/ and /admin/ requests → network-first; fall through to
* cache only when offline so SSR routes stay fresh.
* - Everything else → network-first with cache fallback.
*
* Spec: docs/improvement-plan/10-compliance-polish/lighthouse.md
*
* SECURITY: This file is loaded on /sw.js scope = `/`. It must NEVER call
* dynamic import() with untrusted input nor cache POST/PUT/PATCH/DELETE
* responses. Bumping CACHE_VERSION invalidates all old caches on the next
* activate event, which is the rollback path documented in the spec.
*/
const CACHE_VERSION = "v1-2026-05-17";
const ASSET_CACHE = `toolctrl-assets-${CACHE_VERSION}`;
const RUNTIME_CACHE = `toolctrl-runtime-${CACHE_VERSION}`;
self.addEventListener("install", (event) => {
// Activate immediately so cached responses from old versions are evicted.
event.waitUntil(self.skipWaiting());
});
self.addEventListener("activate", (event) => {
event.waitUntil(
(async () => {
const keys = await caches.keys();
await Promise.all(
keys
.filter((key) => key.startsWith("toolctrl-") && key !== ASSET_CACHE && key !== RUNTIME_CACHE)
.map((key) => caches.delete(key))
);
await self.clients.claim();
})()
);
});
self.addEventListener("fetch", (event) => {
const request = event.request;
// Never intercept non-GET — auth/mutation traffic must hit the network.
if (request.method !== "GET") {
return;
}
const url = new URL(request.url);
// Same-origin only. Third-party assets (Sentry, Crisp, recaptcha) stay on the network.
if (url.origin !== self.location.origin) {
return;
}
if (url.pathname.startsWith("/assets/")) {
event.respondWith(cacheFirst(request, ASSET_CACHE));
return;
}
if (
url.pathname.startsWith("/api/") ||
url.pathname.startsWith("/app/") ||
url.pathname.startsWith("/admin/")
) {
event.respondWith(networkFirst(request, RUNTIME_CACHE));
return;
}
event.respondWith(networkFirst(request, RUNTIME_CACHE));
});
async function cacheFirst(request, cacheName) {
const cache = await caches.open(cacheName);
const cached = await cache.match(request);
if (cached) return cached;
const response = await fetch(request);
if (response.ok) {
cache.put(request, response.clone());
}
return response;
}
async function networkFirst(request, cacheName) {
const cache = await caches.open(cacheName);
try {
const response = await fetch(request);
if (response.ok) {
cache.put(request, response.clone());
}
return response;
} catch (err) {
const cached = await cache.match(request);
if (cached) return cached;
throw err;
}
}