The icon PNGs still carried the old two-leaf motif; only favicon.svg was updated in the redesign, and Chrome takes the PNGs for the install icon. Re-rendered both from the current logo, added a padded maskable variant so Android's mask no longer clips the leaves, bumped the service-worker cache so existing installs drop the stale icons, and removed the unreferenced old favicon and wordmark files.
70 lines
3.2 KiB
JavaScript
70 lines
3.2 KiB
JavaScript
// Offline support. Nothing here talks to a server at runtime — the document lives in
|
|
// localStorage/IndexedDB — so a cached shell is the whole working editor: dictionaries,
|
|
// fonts and the WASM speller included, each kept the first time it is asked for.
|
|
|
|
const CACHE = 'edentext-v2';
|
|
// The entry document under one key, whichever path asked for it. Relative, so it
|
|
// resolves against this script's own directory — the app root under any base path.
|
|
const SHELL = './';
|
|
|
|
// The build's asset names carry a content hash, and the entry document is the only
|
|
// place they are written down — so reading them out of it precaches the shell with no
|
|
// build step. What the app loads later (the speller, a dictionary, a picture decoder)
|
|
// is kept as it is asked for, which is from the **second** visit on: on the first the
|
|
// worker is still installing while those requests go out.
|
|
async function precache(cache) {
|
|
const response = await fetch(SHELL, { cache: 'reload' });
|
|
await cache.put(SHELL, response.clone());
|
|
const html = await response.text();
|
|
const assets = [...html.matchAll(/(?:src|href)="(\.?\/assets\/[^"]+)"/g)].map((m) => m[1]);
|
|
await cache.addAll([...new Set(assets), './favicon.svg', './manifest.webmanifest']);
|
|
}
|
|
|
|
self.addEventListener('install', (event) => {
|
|
event.waitUntil(caches.open(CACHE).then(precache).then(() => self.skipWaiting()));
|
|
});
|
|
|
|
self.addEventListener('activate', (event) => {
|
|
event.waitUntil(
|
|
caches.keys()
|
|
.then((keys) => Promise.all(keys.filter((k) => k !== CACHE).map((k) => caches.delete(k))))
|
|
.then(() => self.clients.claim()),
|
|
);
|
|
});
|
|
|
|
// The document is network-first: served from the cache it would keep naming the assets
|
|
// of the build it was cached with, and a new one would never be fetched.
|
|
async function documentFrom(request) {
|
|
try {
|
|
const response = await fetch(request);
|
|
if (response.ok) (await caches.open(CACHE)).put(SHELL, response.clone());
|
|
return response;
|
|
} catch {
|
|
const cached = await caches.match(SHELL, { ignoreVary: true });
|
|
if (cached) return cached;
|
|
throw new Error('offline and no cached shell');
|
|
}
|
|
}
|
|
|
|
// Everything else is cache-first: the build's own files carry a content hash, so a
|
|
// cached one can never be stale.
|
|
// ponytail: the cache is never version-swept, so a redeploy's old assets stay in it
|
|
// until the browser evicts them. Sweeping needs the build id, which needs a build step.
|
|
async function assetFrom(request) {
|
|
// ignoreVary: a precached asset was fetched without the page's `Origin`, and a
|
|
// static server that answers `Vary: Origin` would make the two never match.
|
|
const cached = await caches.match(request, { ignoreVary: true });
|
|
if (cached) return cached;
|
|
const response = await fetch(request);
|
|
// An opaque response hides its status, so only a real same-origin one is kept.
|
|
if (response.ok && response.type === 'basic') (await caches.open(CACHE)).put(request, response.clone());
|
|
return response;
|
|
}
|
|
|
|
self.addEventListener('fetch', (event) => {
|
|
const { request } = event;
|
|
if (request.method !== 'GET') return;
|
|
if (new URL(request.url).origin !== self.location.origin) return;
|
|
event.respondWith(request.mode === 'navigate' ? documentFrom(request) : assetFrom(request));
|
|
});
|