~ knotless Search/
↑↓ move↵ open⇧↵ all results

grain.social/grain

photography-focused photo sharing platform

grain.social
1 collaborator
0
2
93 lines · 3.1 KB Raw History
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)),
  );
});