0
2
import { build, files, version } from "$service-worker";
const CACHE = `cache-${version}`;
const ASSETS = new Set([...build, ...files]);
self.addEventListener("install", (event) => {
event.waitUntil(
caches
.open(CACHE)
.then((cache) => cache.addAll([...ASSETS]))
.then(() => self.skipWaiting()),
);
});
self.addEventListener("activate", (event) => {
event.waitUntil(
caches.keys().then(async (keys) => {
for (const key of keys) {
if (key !== CACHE) await caches.delete(key);
}
self.clients.claim();
}),
);
});
function offlineResponse() {
return new Response("You appear to be offline.", {
status: 503,
statusText: "Offline",
headers: { "Content-Type": "text/plain; charset=utf-8" },
});
}
// caches.match() resolves to undefined on a miss, and respondWith(undefined)
// throws "Failed to convert value to 'Response'" as an unhandled rejection.
// Navigations are never precached (only build + files are), so every network
// blip during one used to hit that. Always hand back a real Response.
async function fromCache(request) {
return (await caches.match(request)) || offlineResponse();
}
self.addEventListener("fetch", (event) => {
if (event.request.method !== "GET") return;
const url = new URL(event.request.url);
// Skip cross-origin requests and API calls
if (url.origin !== self.location.origin) return;
if (url.pathname.startsWith("/xrpc/")) return;
// Sign-in endpoints belong to the server: they set cookies, and /oauth/callback
// answers with a 302 to the client's redirect_uri, which for the mobile apps is
// the grain:// scheme. fetch() here is a subresource load, and the network stack
// fails a redirect to a non-HTTP(S) scheme, so the catch below would answer the
// sign-in with the offline page. Let the browser navigate these itself.
if (url.pathname.startsWith("/oauth/") || url.pathname.startsWith("/auth/")) return;
// For navigation requests, try network first (so new deploys are picked up)
if (event.request.mode === "navigate") {
event.respondWith(fetch(event.request).catch(() => fromCache(event.request)));
return;
}
// For assets, use cache-first with network fallback
if (ASSETS.has(url.pathname)) {
event.respondWith(
caches
.match(event.request)
.then((cached) => cached || fetch(event.request))
.catch(() => offlineResponse()),
);
return;
}
// For everything else, network-first with cache fallback
event.respondWith(
fetch(event.request)
.then((response) => {
// cache.put rejects on partial (206) and opaque responses, which
// surfaces as another unhandled rejection — only store plain 200s, and
// never let a caching failure take down the response itself.
if (response.status === 200 && response.type === "basic") {
const clone = response.clone();
caches
.open(CACHE)
.then((cache) => cache.put(event.request, clone))
.catch(() => {});
}
return response;
})
.catch(() => fromCache(event.request)),
);
});