From f4fd6db8faa15bf02f38a14b652cc46936f8d6bf Mon Sep 17 00:00:00 2001 From: Christophe Besson Date: Thu, 17 Sep 2026 03:40:52 +0200 Subject: spa: a blank page can never be silent again MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit boot-guard.js is a classic script loaded before the module graph, so it survives the graph failing to link. If #app is still empty after ten seconds it names what failed and offers a reset of this origin — cache, storage, databases and the service worker, which clearing the cache does not touch. Two real defects found building it: openDB never settled when an upgrade was blocked by another tab, and a connection it gave up on stayed open and squatted the database. Co-Authored-By: Claude Opus 5 --- packages/meshbay-hub/src/meshbay_hub/api/webapp.py | 4 + .../src/meshbay_hub/static/boot-guard.js | 200 +++++++++++++++++++++ .../src/meshbay_hub/static/hub-client.js | 37 +++- 3 files changed, 239 insertions(+), 2 deletions(-) create mode 100644 packages/meshbay-hub/src/meshbay_hub/static/boot-guard.js (limited to 'packages/meshbay-hub/src') diff --git a/packages/meshbay-hub/src/meshbay_hub/api/webapp.py b/packages/meshbay-hub/src/meshbay_hub/api/webapp.py index 6ac5cdb..3e23961 100644 --- a/packages/meshbay-hub/src/meshbay_hub/api/webapp.py +++ b/packages/meshbay-hub/src/meshbay_hub/api/webapp.py @@ -180,6 +180,10 @@ _HTML = """\ and app.js's own relative imports inherit the prefix, which is the only way the module graph is guaranteed not to be a mixture of two builds. See _asset_version() and VersionedStatics. --> + + diff --git a/packages/meshbay-hub/src/meshbay_hub/static/boot-guard.js b/packages/meshbay-hub/src/meshbay_hub/static/boot-guard.js new file mode 100644 index 0000000..f5e3aa1 --- /dev/null +++ b/packages/meshbay-hub/src/meshbay_hub/static/boot-guard.js @@ -0,0 +1,200 @@ +/** + * The page can never stay silently blank. + * + * A reader once had nothing but white for an evening: the shell arrived, the + * modules were served from the browser's own store so not one request reached + * the hub, and nothing ever rendered. No message, no error on screen, nothing + * in the server's log to look at — the same account worked in a private window + * and in another browser, which is the signature of something wrong in this + * origin's stored state rather than in what was deployed. Clearing the site's + * data fixed it. Clearing the *cache*, three times, had not: a service worker + * and IndexedDB are not the cache, and nothing on screen said so. + * + * This answers the *silence*, not whatever caused it. A blank page is a bug + * report nobody can write, and a phone has no console to open. + * + * **A classic script, deliberately, and first.** The failure it guards against + * includes the module graph never linking — one bad module and no module code + * runs at all, so a guard inside `app.js` would be part of what failed. This + * one has no imports and cannot be stopped by them. + * + * It does nothing when the application mounts, which is the ordinary case: it + * looks once, late, and speaks only to a reader already staring at nothing. + */ +(function () { + 'use strict'; + + // Long enough that a cold phone on a slow network is never interrupted — + // argon2's wasm, forty modules and a catalogue — and short enough that + // nobody sits in front of white wondering. The app mounts in well under two + // seconds when it mounts at all. + var GIVE_UP_MS = 10000; + var problems = []; + + function note(what) { + if (problems.length < 5) problems.push(String(what).slice(0, 200)); + } + + // Capture phase: a `