aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/src/meshbay_hub/static/downloads.js
diff options
context:
space:
mode:
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([