Download the whole app build behind a loading page

In production, a page request without the running build's cookie gets a
small loading page instead of the app. It downloads every file in
.bunext/public (listed at /build-info.json) into that build's cache,
checks they are all there, hands over to a new service worker waiting
behind an older one, sets the cookie and reloads. The service worker
then serves the app's code from the cache, so later visits load it from
the device.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
2026-10-20 19:33:15 +01:00
co-authored by Claude Opus 5.5
parent 9778d0b9ff
commit b3057119df
16 changed files with 991 additions and 45 deletions
+3 -2
View File
@@ -3,9 +3,10 @@
* (`chunk-AEHOZS24.js`, `admin/bots/H6VDJMBN.js`, `X4TQCLSA.css`): one
* at such a path never changes, so it can be cached for good. The
* vendor files (`/.bunext/public/vendor/react.js`) keep fixed names and
* are not. `public/sw.js` repeats this pattern.
* are not. The loading page (`public/loading.html`) is generated with
* this pattern.
*/
const HashedAssetPattern = /^\/\.bunext\/public\/pages\/(?:.*\/)?(?:chunk-)?[A-Z0-9]{8}\.(?:js|css)$/;
export const HashedAssetPattern = /^\/\.bunext\/public\/pages\/(?:.*\/)?(?:chunk-)?[A-Z0-9]{8}\.(?:js|css)$/;
type Params = {
pathname: string;
+50
View File
@@ -0,0 +1,50 @@
import { describe, expect, test } from "bun:test";
import needsPrecachePage from "./needs-precache-page";
const page = {
method: "GET",
pathname: "/admin/bots/1",
fetch_dest: "document",
build_id: "build-2",
};
describe("needsPrecachePage", () => {
test("stands in for a page on a build the browser hasn't downloaded", () => {
expect(needsPrecachePage(page)).toBe(true);
expect(needsPrecachePage({ ...page, cookie_build_id: "build-1" })).toBe(true);
expect(
needsPrecachePage({ ...page, fetch_dest: "empty", fetch_mode: "navigate" }),
).toBe(true);
expect(
needsPrecachePage({ ...page, fetch_dest: null, accept: "text/html,*/*" }),
).toBe(true);
});
test("lets the current build through", () => {
expect(needsPrecachePage({ ...page, cookie_build_id: "build-2" })).toBe(false);
});
test("never runs in development", () => {
expect(needsPrecachePage({ ...page, build_id: null })).toBe(false);
});
test("leaves everything that isn't a page navigation", () => {
expect(needsPrecachePage({ ...page, method: "POST" })).toBe(false);
expect(needsPrecachePage({ ...page, fetch_dest: "script" })).toBe(false);
expect(
needsPrecachePage({
...page,
fetch_dest: "empty",
fetch_mode: "cors",
accept: "text/html",
}),
).toBe(false);
expect(needsPrecachePage({ ...page, fetch_dest: null, accept: "*/*" })).toBe(false);
expect(needsPrecachePage({ ...page, pathname: "/api/admin/bots/send-message" })).toBe(false);
expect(needsPrecachePage({ ...page, pathname: "/.bunext/public/vendor/react.js" })).toBe(false);
expect(needsPrecachePage({ ...page, pathname: "/computers/2/vnc/vnc.html" })).toBe(false);
expect(needsPrecachePage({ ...page, pathname: "/media/1/private/a" })).toBe(false);
expect(needsPrecachePage({ ...page, pathname: "/build-info.json" })).toBe(false);
expect(needsPrecachePage({ ...page, pathname: "/sw.js" })).toBe(false);
});
});
+54
View File
@@ -0,0 +1,54 @@
/**
* Paths the loading page never stands in for: the app's code, live data,
* media, proxied computers and the files the loading page itself needs.
*/
const OpenPrefixes = ["/.bunext/", "/api/", "/ws", "/media", "/computers/"];
type Params = {
method: string;
pathname: string;
/**
* `Sec-Fetch-Mode` and `Sec-Fetch-Dest`, when the browser sends them.
* A navigation the service worker passes on keeps `navigate` but
* arrives with the destination `empty`.
*/
fetch_mode?: string | null;
fetch_dest?: string | null;
accept?: string | null;
/**
* The build in the browser's cookie, if any.
*/
cookie_build_id?: string | null;
/**
* The running build; null in development.
*/
build_id?: string | null;
};
/**
* Whether a request is a browser opening a page of the app on a build it
* hasn't downloaded yet, so it gets the loading page instead. Only page
* navigations: anything with a file extension (`/favicon.ico`,
* `/sw.js`, `/build-info.json`) goes through as usual.
*/
export default function needsPrecachePage({
method,
pathname,
fetch_mode,
fetch_dest,
accept,
cookie_build_id,
build_id,
}: Params): boolean {
if (!build_id) return false;
if (cookie_build_id == build_id) return false;
if (method != "GET") return false;
if (OpenPrefixes.some((prefix) => pathname.startsWith(prefix))) return false;
if (pathname.split("/").pop()?.includes(".")) return false;
if (fetch_mode || fetch_dest) {
return fetch_mode == "navigate" || fetch_dest == "document";
}
return Boolean(accept?.includes("text/html"));
}