107 lines
3.3 KiB
JavaScript
107 lines
3.3 KiB
JavaScript
// TouchBase service worker — minimal, hand-rolled.
|
|
//
|
|
// Strategy:
|
|
// - HTML pages (text/html): network-first → cache fallback for offline.
|
|
// - Static assets (/_next/static, /icon.svg, fonts): stale-while-revalidate.
|
|
// - API + auth + booking server actions: pass-through, NEVER cached.
|
|
// Stale appointment data is dangerous; better to fail than to lie.
|
|
//
|
|
// Cache name is versioned. Bump CACHE_VERSION on any deploy that should
|
|
// invalidate the cache for returning users.
|
|
|
|
const CACHE_VERSION = "v1";
|
|
const HTML_CACHE = `tb-html-${CACHE_VERSION}`;
|
|
const STATIC_CACHE = `tb-static-${CACHE_VERSION}`;
|
|
const ALL_CACHES = [HTML_CACHE, STATIC_CACHE];
|
|
|
|
self.addEventListener("install", () => {
|
|
// Activate the new SW immediately; users get the new caching rules on next nav.
|
|
self.skipWaiting();
|
|
});
|
|
|
|
self.addEventListener("activate", (event) => {
|
|
event.waitUntil(
|
|
(async () => {
|
|
const names = await caches.keys();
|
|
await Promise.all(
|
|
names
|
|
.filter((n) => n.startsWith("tb-") && !ALL_CACHES.includes(n))
|
|
.map((n) => caches.delete(n)),
|
|
);
|
|
await self.clients.claim();
|
|
})(),
|
|
);
|
|
});
|
|
|
|
self.addEventListener("fetch", (event) => {
|
|
const req = event.request;
|
|
|
|
// Only handle GET. Server actions (POST), auth callbacks, etc. always pass through.
|
|
if (req.method !== "GET") return;
|
|
|
|
const url = new URL(req.url);
|
|
|
|
// Same-origin only — don't cache cross-origin (analytics, fonts CDN, etc.).
|
|
if (url.origin !== self.location.origin) return;
|
|
|
|
// Never cache API, auth, or anything in the booking flow that could go stale.
|
|
// Stale slot data could let a customer "see" an already-taken slot.
|
|
if (
|
|
url.pathname.startsWith("/api/") ||
|
|
url.pathname.startsWith("/_next/data/") ||
|
|
url.pathname.startsWith("/admin/") ||
|
|
url.pathname.startsWith("/account/") ||
|
|
url.pathname.startsWith("/therapist/") ||
|
|
url.pathname.startsWith("/book")
|
|
) {
|
|
return; // pass through
|
|
}
|
|
|
|
// Static assets → stale-while-revalidate
|
|
if (
|
|
url.pathname.startsWith("/_next/static/") ||
|
|
url.pathname === "/icon.svg" ||
|
|
url.pathname === "/icon-mask.svg" ||
|
|
url.pathname === "/favicon.ico" ||
|
|
url.pathname === "/manifest.webmanifest"
|
|
) {
|
|
event.respondWith(staleWhileRevalidate(req, STATIC_CACHE));
|
|
return;
|
|
}
|
|
|
|
// HTML navigation → network-first → cache fallback
|
|
if (req.mode === "navigate" || req.headers.get("accept")?.includes("text/html")) {
|
|
event.respondWith(networkFirst(req, HTML_CACHE));
|
|
return;
|
|
}
|
|
});
|
|
|
|
async function networkFirst(request, cacheName) {
|
|
const cache = await caches.open(cacheName);
|
|
try {
|
|
const fresh = await fetch(request);
|
|
if (fresh && fresh.ok) {
|
|
cache.put(request, fresh.clone());
|
|
}
|
|
return fresh;
|
|
} catch (err) {
|
|
const cached = await cache.match(request);
|
|
if (cached) return cached;
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
async function staleWhileRevalidate(request, cacheName) {
|
|
const cache = await caches.open(cacheName);
|
|
const cached = await cache.match(request);
|
|
const fetchPromise = fetch(request)
|
|
.then((response) => {
|
|
if (response && response.ok) {
|
|
cache.put(request, response.clone());
|
|
}
|
|
return response;
|
|
})
|
|
.catch(() => undefined);
|
|
return cached ?? (await fetchPromise) ?? new Response("offline", { status: 503 });
|
|
}
|