Files
astro-website/site/public/sw.js
Santhosh Janardhanan daac2eec20
Some checks failed
ci / site (push) Has been cancelled
publish-image / publish (push) Has been cancelled
fix for SW
2026-02-10 18:02:37 -05:00

171 lines
5.1 KiB
JavaScript

/* Service Worker: lightweight caching for a static Astro site.
- Navigations: network-first (avoid stale HTML indefinitely)
- Site shell: pre-cache on install
- Images: cache-first with bounded eviction
*/
// Bump this value on deploy to invalidate caches.
const CACHE_VERSION = "v4";
const CACHE_SHELL = `shell-${CACHE_VERSION}`;
const CACHE_PAGES = `pages-${CACHE_VERSION}`;
const CACHE_MEDIA = `media-${CACHE_VERSION}`;
const SHELL_ASSETS = ["/", "/styles/global.css", "/favicon.png", "/robots.txt"];
// Cache.addAll() throws if there are any duplicate requests.
const SHELL_ASSETS_UNIQUE = Array.from(new Set(SHELL_ASSETS));
// Keep media cache bounded so we don't grow indefinitely.
const MAX_MEDIA_ENTRIES = 80;
const isGet = (request) => request && request.method === "GET";
const isNavigationRequest = (request) =>
request.mode === "navigate" || request.destination === "document";
const isImageRequest = (request, url) => {
if (request.destination === "image") return true;
const p = url.pathname.toLowerCase();
return (
p.endsWith(".png") ||
p.endsWith(".jpg") ||
p.endsWith(".jpeg") ||
p.endsWith(".webp") ||
p.endsWith(".gif") ||
p.endsWith(".avif") ||
p.endsWith(".svg")
);
};
async function trimCache(cacheName, maxEntries) {
const cache = await caches.open(cacheName);
const keys = await cache.keys();
const extra = keys.length - maxEntries;
if (extra <= 0) return;
// Cache keys are returned in insertion order in practice; delete the oldest.
for (let i = 0; i < extra; i += 1) {
await cache.delete(keys[i]);
}
}
async function cachePutSafe(cacheName, request, response) {
// Only cache successful or opaque responses. Avoid caching 404/500 HTML.
if (!response) return;
if (response.type !== "opaque" && !response.ok) return;
const cache = await caches.open(cacheName);
await cache.put(request, response);
}
self.addEventListener("install", (event) => {
event.waitUntil(
(async () => {
const cache = await caches.open(CACHE_SHELL);
await cache.addAll(SHELL_ASSETS_UNIQUE);
// Activate new worker ASAP to pick up new caching rules.
await self.skipWaiting();
})(),
);
});
self.addEventListener("activate", (event) => {
event.waitUntil(
(async () => {
const keep = new Set([CACHE_SHELL, CACHE_PAGES, CACHE_MEDIA]);
const keys = await caches.keys();
await Promise.all(keys.map((k) => (keep.has(k) ? Promise.resolve() : caches.delete(k))));
await self.clients.claim();
})(),
);
});
self.addEventListener("fetch", (event) => {
const { request } = event;
if (!isGet(request)) return;
const url = new URL(request.url);
// Only handle http(s).
if (url.protocol !== "http:" && url.protocol !== "https:") return;
// Network-first for navigations (HTML documents). Cache as fallback only.
if (isNavigationRequest(request)) {
event.respondWith(
(async () => {
try {
const fresh = await fetch(request);
// Cache a clone so we can serve it when offline.
await cachePutSafe(CACHE_PAGES, request, fresh.clone());
return fresh;
} catch {
const cached = await caches.match(request);
if (cached) return cached;
// Fallback: try cached homepage shell.
const home = await caches.match("/");
if (home) return home;
throw new Error("No cached navigation fallback.");
}
})(),
);
return;
}
// Cache-first for images/media with bounded cache size.
if (isImageRequest(request, url)) {
event.respondWith(
(async () => {
const cached = await caches.match(request);
if (cached) return cached;
const res = await fetch(request);
await cachePutSafe(CACHE_MEDIA, request, res.clone());
await trimCache(CACHE_MEDIA, MAX_MEDIA_ENTRIES);
return res;
})(),
);
return;
}
// Network-first for global CSS to avoid serving stale styling after deploy.
// (The SW already caches styles, but network-first prevents a "stuck" old CSS experience.)
if (url.origin === self.location.origin && url.pathname === "/styles/global.css") {
event.respondWith(
(async () => {
try {
const fresh = await fetch(request);
await cachePutSafe(CACHE_SHELL, request, fresh.clone());
return fresh;
} catch {
const cached = await caches.match(request);
if (cached) return cached;
return fetch(request);
}
})(),
);
return;
}
// Stale-while-revalidate for styles/scripts/fonts from same-origin.
if (
url.origin === self.location.origin &&
(request.destination === "style" ||
request.destination === "script" ||
request.destination === "font")
) {
event.respondWith(
(async () => {
const cached = await caches.match(request);
const networkPromise = fetch(request)
.then(async (res) => {
await cachePutSafe(CACHE_SHELL, request, res.clone());
return res;
})
.catch(() => null);
return cached || (await networkPromise) || fetch(request);
})(),
);
}
});