diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-09-17 03:40:52 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-09-17 03:40:52 +0200 |
| commit | f4fd6db8faa15bf02f38a14b652cc46936f8d6bf (patch) | |
| tree | 396cbe7fe188fb565231be937ccfa6419384814f /packages/meshbay-hub/src/meshbay_hub/static/hub-client.js | |
| parent | 929bfcccf8928a30d86d48a8b0137db0dd4a1ee0 (diff) | |
| download | meshbay-f4fd6db8faa15bf02f38a14b652cc46936f8d6bf.tar.gz | |
spa: a blank page can never be silent again
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 <noreply@anthropic.com>
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub/static/hub-client.js')
| -rw-r--r-- | packages/meshbay-hub/src/meshbay_hub/static/hub-client.js | 37 |
1 files changed, 35 insertions, 2 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/hub-client.js b/packages/meshbay-hub/src/meshbay_hub/static/hub-client.js index dbd5037..eead457 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/hub-client.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/hub-client.js @@ -18,6 +18,7 @@ const IDB_NAME = 'meshbay'; // disagreeing about the version is a `VersionError` thrown at whichever of // them happens to run second. const IDB_VERSION = 2; +const OPEN_DB_TIMEOUT_MS = 5000; const IDB_STORE = 'group_indexes'; const IDB_PLAYLISTS = 'playlists'; @@ -27,9 +28,32 @@ function navigate(path) { // ── IndexedDB cache ───────────────────────────────────────────────────────── +/** + * The database, opened — or refused, but never left hanging. + * + * A version upgrade waits for every other connection to this database to + * close. A second tab of this site holding version 1 open is enough to stop + * it, and `indexedDB.open` then fires **neither** `success` nor `error`: it + * fires `blocked`, and if nothing handles that the promise never settles. Every + * `await openDB()` behind it waits for ever, which reads as a feature that + * silently does nothing rather than as a failure anybody can see. + * + * So `blocked` is heard, and a deadline covers the rest. Callers already treat + * a rejection as "no local cache this time" and carry on. + */ function openDB() { return new Promise((resolve, reject) => { const req = indexedDB.open(IDB_NAME, IDB_VERSION); + let settled = false; + const done = (fn, arg) => { if (!settled) { settled = true; fn(arg); } }; + // Generous: the other tab is asked to close and usually does within a + // frame. This is the backstop for the one that cannot — a page suspended + // on a phone, say — not a latency budget. + const deadline = setTimeout(() => done(reject, new Error( + 'IndexedDB open timed out (another tab may hold an older version open)')), + OPEN_DB_TIMEOUT_MS); + req.onblocked = () => done(reject, new Error( + 'IndexedDB upgrade blocked by another tab of this site')); req.onupgradeneeded = () => { const db = req.result; if (!db.objectStoreNames.contains(IDB_STORE)) { @@ -43,8 +67,17 @@ function openDB() { db.createObjectStore(IDB_PLAYLISTS); } }; - req.onsuccess = () => resolve(req.result); - req.onerror = () => reject(req.error); + req.onsuccess = () => { + clearTimeout(deadline); + // Giving up does not cancel the request: the other tab eventually closes, + // the upgrade goes through, and this fires with a live connection nobody + // is waiting for. Left open it squats the database — blocking the next + // upgrade *and* any attempt to delete it, which is the failure this + // whole guard exists to end, arriving through the back door. + if (settled) { try { req.result.close(); } catch { /* already gone */ } return; } + done(resolve, req.result); + }; + req.onerror = () => { clearTimeout(deadline); done(reject, req.error); }; }); } |