nexus/ui/public/sw.js
Nexus Dev 471a9daaa6 feat(26): merge worktree code from plans 26-00, 26-01, 26-03
SW cache-first rewrite, React.lazy code splitting, PWA types/test stubs,
install prompt, offline banner, offline queue, ChatPanel wiring.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 03:55:48 +00:00

88 lines
3.3 KiB
JavaScript

const CACHE_NAME = "nexus-v1";
const PRECACHE_URLS = ["/", "/index.html"];
const STATIC_EXTENSION_RE = /\.(js|css|woff2?|png|svg|ico|webmanifest)$/;
// ── Install ──────────────────────────────────────────────────────────────────
self.addEventListener("install", (event) => {
self.skipWaiting();
event.waitUntil(
caches.open(CACHE_NAME).then((cache) => cache.addAll(PRECACHE_URLS))
);
});
// ── Activate ─────────────────────────────────────────────────────────────────
// Delete all caches except nexus-v1 (removes any stale caches from prior versions).
self.addEventListener("activate", (event) => {
event.waitUntil(
caches
.keys()
.then((keys) =>
Promise.all(
keys
.filter((key) => key !== CACHE_NAME)
.map((key) => caches.delete(key))
)
)
.then(() => self.clients.claim())
);
});
// ── Fetch ─────────────────────────────────────────────────────────────────────
self.addEventListener("fetch", (event) => {
const { request } = event;
const url = new URL(request.url);
// API calls — network only, no interception
if (url.pathname.startsWith("/api")) {
return;
}
// Navigation requests — cache-first: serve shell from cache, fall back to network
if (request.mode === "navigate") {
event.respondWith(
caches.match("/").then((cached) => cached || fetch(request))
);
return;
}
// Static assets — cache-first: serve from cache, on miss fetch and cache
if (STATIC_EXTENSION_RE.test(url.pathname)) {
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((cache) => cache.put(request, clone));
}
return response;
});
})
);
return;
}
// All other requests — pass through, no interception
});
// ── Push Notifications ────────────────────────────────────────────────────────
self.addEventListener("push", (event) => {
const data = event.data ? event.data.json() : {};
const { title = "Nexus", body = "", icon, data: notifData } = data;
event.waitUntil(
self.registration.showNotification(title, {
body,
icon: icon || "/android-chrome-192x192.png",
badge: "/favicon-32x32.png",
data: notifData,
})
);
});
// ── Notification Click ────────────────────────────────────────────────────────
self.addEventListener("notificationclick", (event) => {
event.notification.close();
const targetUrl = event.notification.data?.url || "/";
event.waitUntil(clients.openWindow(targetUrl));
});