/* ihasmail service worker. Two jobs: app-shell caching for installability and fast loads (API requests are never cached), and Web Push, which is the only part of ihasmail that runs when no tab is open. */ const VERSION = "ihasmail-v2"; /* * The mount, worked out rather than configured. * * This file is copied to the build verbatim -- Vite's `base` never touches * public/ -- so there is nothing to substitute BASE_PATH into. It does not * need one: the worker is served from the mount, so its own address says * where that is. `/mail/sw.js` gives `/mail`, `/sw.js` gives `""`, which is * the same canonical form the rest of the app uses. * * Deriving it here also means the worker cannot disagree with the page that * registered it, which a second copy of the value in a build-time constant * eventually would. */ const BASE = new URL("./", self.location).pathname.replace(/\/$/, ""); const SHELL = [`${BASE}/`, `${BASE}/manifest.webmanifest`, `${BASE}/img/logo.png`, `${BASE}/img/icon-192.png`, `${BASE}/favicon.ico`]; self.addEventListener("install", (event) => { event.waitUntil(caches.open(VERSION).then((c) => c.addAll(SHELL)).then(() => self.skipWaiting())); }); self.addEventListener("activate", (event) => { event.waitUntil( caches.keys() .then((keys) => Promise.all(keys.filter((k) => k !== VERSION).map((k) => caches.delete(k)))) .then(() => tidy()) .catch(() => {}) .then(() => self.clients.claim()) ); }); /* * Keeping the cache to what the current build uses. * * Build assets are cached on first use and their names change with every * build, and nothing used to take them out again: every deploy's chunks stayed * in the browser for good. Worse, whatever the server answered was kept -- a * 404 for a chunk asked for while a deploy was changing over became that * chunk, from then on, in that browser. * * The rule now: only a successful response is cached, and whenever the app * page changes, the assets it no longer names are dropped. A lazily loaded * chunk the page does not name is dropped too, and fetched again the next time * it is wanted -- a hash that did not change is still on the server. * * The cache name stays as it is. The same cache carries what the worker leaves * for a tab to collect -- a push verification, a share, the facts it notifies * from -- and a new name would throw those away along with the rubbish. */ const ASSETS = `${BASE}/assets/`; const SHELL_KEY = `${BASE}/`; function assetsNamedIn(html) { const out = new Set(); for (const m of html.matchAll(/["']([^"']*\/assets\/[^"']+)["']/g)) { try { out.add(new URL(m[1], self.location).pathname); } catch { /* not a URL */ } } return out; } /** * Drop failed responses, and assets the cached app page does not name. `also` * is a page whose assets are kept as well: the one just replaced, which a tab * opened from the kept copy may still be running. */ async function tidy(also = "") { const cache = await caches.open(VERSION); const shell = await cache.match(SHELL_KEY); // Without a page to go by, which assets are current is unknown; keep them. const keep = shell ? assetsNamedIn(await shell.text()) : null; if (keep) for (const path of assetsNamedIn(also)) keep.add(path); for (const req of await cache.keys()) { const path = new URL(req.url).pathname; if (path.startsWith(ASSETS)) { if (keep && !keep.has(path)) { await cache.delete(req); continue; } } const res = await cache.match(req); if (res && !res.ok) await cache.delete(req); } } /** Keep the offline copy of the app page current, tidy when it changes, and fill in what it lists. */ async function refreshShell(res) { const html = await res.text(); const cache = await caches.open(VERSION); const prev = await cache.match(SHELL_KEY); const prevHtml = prev ? await prev.text() : ""; if (prevHtml !== html) { await cache.put(SHELL_KEY, new Response(html, { headers: { "content-type": "text/html; charset=utf-8" } })); await tidy(prevHtml); } await precache(html); } /* * Fetching the rest of the build before it is asked for. * * The app page lists every file of its build (see the asset-list plugin in * vite.config.ts). Without this, the first time after a deploy that a reader * opened the composer, settings or a viewer, it waited on the server for the * code -- on a distant link, a visible pause. Now those files are fetched * quietly once a page names them, a few at a time, and only those not held * already; a load cut short is carried on at the next navigation, which calls * this again. Language catalogs are left to be cached when used, and nothing * is fetched ahead when the reader has asked the browser to save data. */ const PRECACHE_PARALLEL = 3; function precacheList(html) { const m = html.match(/