Skip to content
← Public packages

@kody/mailroom

Offline-first PWA for your Kody mailbox: sent and received threads, copy-prompt for agents, no send.

src/ui/sw-script.ts

78 lines · 2.4 KB · TypeScript
import { scopedAssetPath } from './app-url.ts'

export const MAILROOM_SW_VERSION = 'mailroom-v2'

export function serviceWorkerScript(scope: string) {
	const root = scope.endsWith('/') ? scope : `${scope}/`
	const shell = [
		scopedAssetPath(root, 'manifest.webmanifest'),
		scopedAssetPath(root, 'favicon.png'),
		scopedAssetPath(root, 'apple-touch-icon.png'),
		scopedAssetPath(root, 'pwa-192.png'),
		scopedAssetPath(root, 'pwa-512.png'),
		scopedAssetPath(root, 'pwa-192-maskable.png'),
		scopedAssetPath(root, 'pwa-512-maskable.png'),
	]
	return `const SCOPE = ${JSON.stringify(root)};
const VERSION = ${JSON.stringify(MAILROOM_SW_VERSION)};
const SHELL = ${JSON.stringify(shell)};

self.addEventListener('install', (event) => {
  event.waitUntil(
    caches.open(VERSION).then((cache) =>
      Promise.all(SHELL.map((url) => cache.add(url).catch(() => undefined))),
    ).then(() => self.skipWaiting()),
  );
});

self.addEventListener('activate', (event) => {
  event.waitUntil(
    caches.keys().then((keys) =>
      Promise.all(keys.filter((key) => key !== VERSION).map((key) => caches.delete(key))),
    ).then(() => self.clients.claim()),
  );
});

self.addEventListener('message', (event) => {
  if (event.data === 'SKIP_WAITING') self.skipWaiting();
});

async function networkFirst(request) {
  const cache = await caches.open(VERSION);
  try {
    const response = await fetch(request);
    if (response.ok) cache.put(request, response.clone());
    return response;
  } catch (error) {
    const cached = await cache.match(request);
    if (cached) return cached;
    throw error;
  }
}

async function cacheFirst(request) {
  const cache = await caches.open(VERSION);
  const cached = await cache.match(request);
  const network = fetch(request).then((response) => {
    if (response.ok) cache.put(request, response.clone());
    return response;
  });
  if (cached) {
    network.catch(() => undefined);
    return cached;
  }
  return network;
}

self.addEventListener('fetch', (event) => {
  const request = event.request;
  if (request.method !== 'GET') return;
  const url = new URL(request.url);
  if (url.origin !== self.location.origin) return;
  if (!url.pathname.startsWith(SCOPE) && url.pathname !== SCOPE.slice(0, -1)) return;
  if (url.pathname.includes('/api/')) return;
  const isDocument = request.mode === 'navigate' || url.pathname === SCOPE || url.pathname === SCOPE.slice(0, -1);
  event.respondWith(isDocument ? networkFirst(request) : cacheFirst(request));
});
`
}