diff options
Diffstat (limited to 'packages/meshbay-hub/src')
| -rw-r--r-- | packages/meshbay-hub/src/meshbay_hub/static/downloads.js | 111 |
1 files changed, 42 insertions, 69 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/downloads.js b/packages/meshbay-hub/src/meshbay_hub/static/downloads.js index 6ee7cf0..ce1901c 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/downloads.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/downloads.js @@ -380,8 +380,8 @@ async function _claimController(budgetMs) { */ export function primeServiceWorker() { if (!STREAMS_VIA_SW) return; - _priming = serviceWorker() - .then((worker) => worker && _repairIfBypassed(worker)) + _priming = _repairIfBypassed() + .then(() => serviceWorker()) .catch(() => {}); } @@ -391,85 +391,58 @@ export function primeServiceWorker() { // path that is about to be repaired. let _priming = null; -// 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 +// Set for the life of this tab, so the repair below happens 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? + * Was this document loaded with the service worker bypassed? * - * 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. + * A document fetched by a **hard** reload — Ctrl+F5, Ctrl+Shift+R — is loaded + * with the worker bypassed. It can still be claimed afterwards, so + * `navigator.serviceWorker.controller` comes back and every control check + * passes; but the navigations it 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 every + * download fails for the life of that page. * - * 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. + * Measured on Chrome, at document start, before anything registers: * - * 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. + * first visit controller false, registration false + * ordinary reload controller true, registration true + * hard reload controller false, registration true + * + * So being uncontrolled while an active registration already exists names the + * case exactly, and costs nothing to ask. * - * 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. + * The first version of this asked by *performing a download* — a four-byte + * stream through a hidden iframe — which was both unreliable and expensive: + * Chrome rations downloads a page starts without a user gesture to about three, + * so the test competed with the person's real downloads for that budget and its + * answer depended on how many had been spent. It reloaded a healthy page on + * every first visit, taking the group's WebRTC session down with it. */ -async function _repairIfBypassed(worker) { +const _controlledAtLoad = STREAMS_VIA_SW + && Boolean(navigator.serviceWorker.controller); +// Started here, at module load, because `register()` would make the answer +// true whatever it was. +const _registeredAtLoad = STREAMS_VIA_SW + ? navigator.serviceWorker.getRegistration('/') + .then((reg) => Boolean(reg && reg.active)).catch(() => false) + : Promise.resolve(false); + +/** An ordinary reload puts the document back under the worker, so do that once. */ +async function _repairIfBypassed() { + if (_controlledAtLoad) return; + // Uncontrolled with nothing registered is a first visit, not a bypass: the + // worker is being installed right now and the page is fine after it claims. + if (!await _registeredAtLoad) return; 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)'); + console.warn('[MeshBay] this page was loaded with the download worker ' + + 'bypassed (a hard reload does that) — reloading once to put it ' + + 'back under the worker\u2019s control'); try { sessionStorage.setItem(REPAIRED_KEY, '1'); } catch { /* blocked */ } location.reload(); } |