aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/src/meshbay_hub/static/hub-client.js
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-09-17 03:40:52 +0200
committerChristophe Besson <cbesson@gmail.com>2026-09-17 03:40:52 +0200
commitf4fd6db8faa15bf02f38a14b652cc46936f8d6bf (patch)
tree396cbe7fe188fb565231be937ccfa6419384814f /packages/meshbay-hub/src/meshbay_hub/static/hub-client.js
parent929bfcccf8928a30d86d48a8b0137db0dd4a1ee0 (diff)
downloadmeshbay-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.js37
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); };
});
}