aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/src/meshbay_hub/static/downloads.js
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-09-09 10:05:30 +0200
committerChristophe Besson <cbesson@gmail.com>2026-09-09 10:05:30 +0200
commitfc148e185c01b2e25361c7625a67d310d2e1d288 (patch)
tree63d5b8f290a424cec235c47f55e42c290b4a3e46 /packages/meshbay-hub/src/meshbay_hub/static/downloads.js
parent7000082b748bf608484f94d30d63b2c6b83e6814 (diff)
downloadmeshbay-fc148e185c01b2e25361c7625a67d310d2e1d288.tar.gz
fix(spa): repair a page the download worker cannot serve
Downloads on Firefox failed with "the worker did not answer the download within 15s", every time, for one operator, while the same profile driven from here succeeded every time. Their own test sequence found it: a freshly started browser downloaded four files out of four, twice; one Ctrl+F5 and every attempt afterwards failed; restart, fine again; Ctrl+F5 before any attempt and the very first one failed. A document fetched by a hard reload is loaded with the service worker bypassed. It can still be claimed afterwards, so `navigator.serviceWorker.controller` comes back and every check in `_claimController` passes — but the navigations that document starts keep missing the worker, and the hidden iframe a streamed download needs is a navigation. On Firefox and Safari that is the only way to write a file too large to hold in memory, so the download cannot happen at all, for the life of the page. Being controlled is not being servable, so priming now asks the question directly instead of inferring it: a four-byte stream and a hidden iframe, exactly as a real download would, torn down completely so nothing lands in the download folder. When it goes unanswered the page reloads once, ordinarily, which puts it back under the worker. The flag lives in sessionStorage rather than a variable because it has to survive the reload it triggers, and because a page that is still unservable afterwards must stop rather than loop. Also stops telling people to change browser. The message said "use the desktop app, or Chrome or Edge" for a state an ordinary reload undoes, on the one path Firefox has no alternative to; all ten catalogues now say to reload first. The hard reloads were on my instruction: the SPA's HTML is served `no-store`, so a plain reload has always picked up a new build and Ctrl+F5 was never needed. Hub suite 834 passed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HCGdheDLxGReuKHga3BtST
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub/static/downloads.js')
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/downloads.js90
1 files changed, 88 insertions, 2 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/downloads.js b/packages/meshbay-hub/src/meshbay_hub/static/downloads.js
index 7e4802a..b1eade6 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/downloads.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/downloads.js
@@ -200,6 +200,8 @@ export const BLOB_LIMIT = 512 * 1024 * 1024;
// ── Streaming to disk without the File System Access API ────────────────────
const SW_PATH = '/sw.js';
+// Kept in step with sw.js's own PREFIX.
+const PREFIX_PATH = '/_mbdl/';
// On Firefox and Safari this worker is not a nicety, it is the only unbounded
// way to write a download to disk: the File System Access API does not exist
@@ -365,7 +367,91 @@ async function _claimController(budgetMs) {
*/
export function primeServiceWorker() {
if (!STREAMS_VIA_SW) return;
- serviceWorker().catch(() => {});
+ serviceWorker().then((worker) => worker && _repairIfBypassed(worker))
+ .catch(() => {});
+}
+
+// How long to wait for the worker to answer the one-byte self-test below.
+// Milliseconds when it works; a page that cannot stream at all is worth four
+// seconds to find out about, once, at boot.
+const SELF_TEST_BUDGET_MS = 4000;
+// Set for the life of this tab, so the repair below can happen at most once and
+// can never become a reload loop.
+const REPAIRED_KEY = 'meshbay.sw-repaired';
+
+/**
+ * Can this document actually have a download served, or only talk to the worker?
+ *
+ * Being controlled is not the same thing, and the gap between them is a real
+ * failure people hit. A document fetched by a **hard** reload — Ctrl+F5,
+ * Ctrl+Shift+R — is loaded with the service worker bypassed. It can still be
+ * claimed afterwards, so `navigator.serviceWorker.controller` comes back and
+ * every check in `_claimController` passes; but the navigations that document
+ * starts keep missing the worker, and the hidden iframe a streamed download
+ * needs *is* a navigation. Every download then fails with "the worker did not
+ * answer", for the life of that page — on Firefox and Safari, the only path
+ * there is for a file too large to hold in memory.
+ *
+ * Reported after an operator was told, by this author, to hard-reload after
+ * each deployment: four downloads out of four worked on a freshly started
+ * browser, and the first attempt after a Ctrl+F5 failed, every time.
+ *
+ * This asks the question directly rather than inferring it: a four-byte stream
+ * and a hidden iframe, exactly as a real download would.
+ */
+async function _canServeDownloads(worker) {
+ const id = `selftest-${Math.random().toString(36).slice(2, 10)}`;
+ let readable, writable;
+ try {
+ ({ readable, writable } = new TransformStream());
+ } catch {
+ return true; // No transferable streams: a different failure, not this one.
+ }
+ const chan = new MessageChannel();
+ const serving = new Promise((resolve) => {
+ chan.port1.onmessage = (e) => {
+ if (e.data && e.data.type === 'mbdl-serving') resolve(true);
+ };
+ });
+ try {
+ worker.postMessage({ type: 'mbdl', id, filename: 'meshbay-selftest.bin',
+ size: 4, readable, port: chan.port2 },
+ [readable, chan.port2]);
+ } catch {
+ return true; // Same: not the bypass this is looking for.
+ }
+ const frame = document.createElement('iframe');
+ frame.hidden = true;
+ frame.src = `${PREFIX_PATH}${id}`;
+ document.body.appendChild(frame);
+ const served = await Promise.race([
+ serving,
+ new Promise((r) => setTimeout(() => r(false), SELF_TEST_BUDGET_MS)),
+ ]);
+ frame.remove();
+ try { chan.port1.close(); } catch { /* already gone */ }
+ // Never completed, so the browser has nothing to save and no file appears.
+ try { await writable.abort('self-test'); } catch { /* already gone */ }
+ return served;
+}
+
+/**
+ * An ordinary reload puts the document back under the worker, so do that once.
+ *
+ * Only at boot, where nothing is in flight and the reload costs a flicker.
+ * Guarded by a session flag rather than a variable: the point is to survive the
+ * reload it triggers, and to stop rather than loop if reloading does not help.
+ */
+async function _repairIfBypassed(worker) {
+ let repaired = false;
+ try { repaired = sessionStorage.getItem(REPAIRED_KEY) === '1'; } catch { /* blocked */ }
+ if (repaired) return;
+ if (await _canServeDownloads(worker)) return;
+ console.warn('[MeshBay] this page cannot be served by the download worker — '
+ + 'reloading once to put it back under the worker\u2019s control '
+ + '(a hard reload leaves a page in this state)');
+ try { sessionStorage.setItem(REPAIRED_KEY, '1'); } catch { /* blocked */ }
+ location.reload();
}
async function serviceWorker(controlMs = SW_CONTROL_BUDGET_MS) {
@@ -458,7 +544,7 @@ async function _attemptStreamedDownload(filename, size, attempt,
const frame = document.createElement('iframe');
frame.hidden = true;
- frame.src = `/_mbdl/${id}`;
+ frame.src = `${PREFIX_PATH}${id}`;
document.body.appendChild(frame);
const answered = await Promise.race([