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:
+47
-2
@@ -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) {
|
async function refreshShell(res) {
|
||||||
const html = await res.text();
|
const html = await res.text();
|
||||||
const cache = await caches.open(VERSION);
|
const cache = await caches.open(VERSION);
|
||||||
const prev = await cache.match(SHELL_KEY);
|
const prev = await cache.match(SHELL_KEY);
|
||||||
if (prev && (await prev.text()) === html) return;
|
if (!prev || (await prev.text()) !== html) {
|
||||||
await cache.put(SHELL_KEY, new Response(html, { headers: { "content-type": "text/html; charset=utf-8" } }));
|
await cache.put(SHELL_KEY, new Response(html, { headers: { "content-type": "text/html; charset=utf-8" } }));
|
||||||
await tidy();
|
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));
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|||||||
+37
-2
@@ -1,4 +1,4 @@
|
|||||||
import { defineConfig } from "vitest/config";
|
import { defineConfig, type Plugin } from "vitest/config";
|
||||||
import react from "@vitejs/plugin-react";
|
import react from "@vitejs/plugin-react";
|
||||||
import { fileURLToPath, URL } from "node:url";
|
import { fileURLToPath, URL } from "node:url";
|
||||||
import { resolveVersion } from "../scripts/version.mjs";
|
import { resolveVersion } from "../scripts/version.mjs";
|
||||||
@@ -22,9 +22,44 @@ const version = resolveVersion();
|
|||||||
*/
|
*/
|
||||||
const base = baseUrlOf(process.env.BASE_PATH);
|
const base = baseUrlOf(process.env.BASE_PATH);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Every file the build made, written into the app page for the service worker.
|
||||||
|
*
|
||||||
|
* The page names only what it loads at start; the composer, settings, viewers
|
||||||
|
* and the rest arrive when first used, and after each deploy that first use
|
||||||
|
* went back to the server. With the whole list in the page, the worker can
|
||||||
|
* fetch them in the background once a new version is seen, and knows to keep
|
||||||
|
* them. Language catalogs are listed apart: a reader wants one of them, which
|
||||||
|
* is cached when it is first loaded.
|
||||||
|
*
|
||||||
|
* An inert JSON block rather than prefetch links, which the browser would
|
||||||
|
* fetch on every load.
|
||||||
|
*/
|
||||||
|
function assetList(): Plugin {
|
||||||
|
return {
|
||||||
|
name: "ihasmail-asset-list",
|
||||||
|
apply: "build",
|
||||||
|
transformIndexHtml: {
|
||||||
|
order: "post",
|
||||||
|
handler(html, ctx) {
|
||||||
|
if (!ctx.bundle) return html;
|
||||||
|
const precache: string[] = [];
|
||||||
|
const onDemand: string[] = [];
|
||||||
|
for (const file of Object.values(ctx.bundle)) {
|
||||||
|
if (!file.fileName.startsWith("assets/") || file.fileName.endsWith(".map")) continue;
|
||||||
|
const catalog = file.type === "chunk" && file.moduleIds.length > 0 && file.moduleIds.every((id) => /[\\/]src[\\/]locales[\\/][^\\/]+\.ts$/.test(id));
|
||||||
|
(catalog ? onDemand : precache).push(`${base}${file.fileName}`);
|
||||||
|
}
|
||||||
|
const json = JSON.stringify({ precache: precache.sort(), onDemand: onDemand.sort() });
|
||||||
|
return html.replace("</body>", ` <script type="application/json" id="ihasmail-assets">${json}</script>\n </body>`);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
base,
|
base,
|
||||||
plugins: [react()],
|
plugins: [react(), assetList()],
|
||||||
define: { __IHASMAIL_VERSION__: JSON.stringify(version) },
|
define: { __IHASMAIL_VERSION__: JSON.stringify(version) },
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: { "@": fileURLToPath(new URL("./src", import.meta.url)) },
|
alias: { "@": fileURLToPath(new URL("./src", import.meta.url)) },
|
||||||
|
|||||||
Reference in New Issue
Block a user