Fetch the rest of a new build in the background (#394)

The app page names only what it loads at start. The composer, settings,
viewers and the rest were fetched when first used, and after every deploy
that first use waited on the server -- and the worker's tidy-up dropped
them again at the next deploy anyway.

The build now writes the list of all its files into the page as an inert
JSON block. The service worker keeps everything listed and, once a page
names files it does not hold, fetches them three at a time; a load cut
short is resumed at the next navigation. Language catalogs are listed
apart and left to be cached when used, and nothing is fetched ahead when
the browser is set to save data.
This commit is contained in:
jcoffey
2026-09-16 13:29:57 -07:00
committed by GitHub
parent e158ebac5a
commit 4c67460450
2 changed files with 86 additions and 6 deletions
+49 -4
View File
@@ -86,14 +86,59 @@ async function tidy() {
}
}
/** Keep the offline copy of the app page current, and tidy when it changes. */
/** 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);
if (prev && (await prev.text()) === html) return;
await cache.put(SHELL_KEY, new Response(html, { headers: { "content-type": "text/html; charset=utf-8" } }));
await tidy();
if (!prev || (await prev.text()) !== html) {
await cache.put(SHELL_KEY, new Response(html, { headers: { "content-type": "text/html; charset=utf-8" } }));
await tidy();
}
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(/<script type="application\/json" id="ihasmail-assets">([^<]*)<\/script>/);
if (!m) return [];
try {
const list = JSON.parse(m[1]).precache;
return Array.isArray(list) ? list.filter((p) => typeof p === "string" && p.startsWith(ASSETS)) : [];
} catch {
return [];
}
}
async function precache(html) {
if (self.navigator.connection && self.navigator.connection.saveData) return;
const cache = await caches.open(VERSION);
const wanted = [];
for (const path of precacheList(html)) if (!(await cache.match(path))) wanted.push(path);
const next = async () => {
for (let path = wanted.shift(); path; path = wanted.shift()) {
try {
const res = await fetch(path, { credentials: "same-origin" });
if (res.ok) await cache.put(path, res);
} catch {
/* offline, or a deploy changing over; the next navigation tries again */
}
}
};
await Promise.all(Array.from({ length: PRECACHE_PARALLEL }, next));
}
/*