From cb43495f998015850f34829329aa4509bd55d2cb Mon Sep 17 00:00:00 2001 From: Christophe Besson Date: Wed, 9 Sep 2026 10:32:13 +0200 Subject: fix(spa): repair a bypassed page in seconds, not half a minute The repair worked but arrived too late to help: about thirty seconds after a hard reload, by which time four downloads had been started and hung, and the page reloading under them read as an unexplained refresh. Two delays, both removed. `_claimController` waited its whole control budget before asking for the claim. A page that is uncontrolled while an active worker exists will never be claimed on its own -- a document fetched by a hard reload is exactly that shape -- so the fifteen seconds were spent waiting for something that was not coming. The claim is now asked for first; waiting is the fallback, not the opening move. Measured in the harness: 6042ms of a 6000ms budget before, milliseconds after. And a download that starts while the self-test is still running now waits for it rather than racing it. Otherwise the click spends both its attempts failing on a path that is about to be repaired, which is what put four frozen rows on screen. Hub suite 836 passed. Both new cases were checked against the unfixed source. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01HCGdheDLxGReuKHga3BtST --- .../src/meshbay_hub/static/downloads.js | 20 +++++++- .../tests/test_streamed_download_reliability.py | 54 +++++++++++++++++++++- 2 files changed, 71 insertions(+), 3 deletions(-) (limited to 'packages') diff --git a/packages/meshbay-hub/src/meshbay_hub/static/downloads.js b/packages/meshbay-hub/src/meshbay_hub/static/downloads.js index b1eade6..87d18b7 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/downloads.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/downloads.js @@ -338,6 +338,16 @@ async function _claimController(budgetMs) { // requests never reach the fetch handler, so the worker would take our stream // and never be asked for it — the download then freezes after exactly one // chunk, which is how that was found. + // + // Ask for the claim *before* waiting, not after. A page that is uncontrolled + // while an active worker exists will not be claimed on its own — a document + // fetched by a hard reload is exactly that shape — so the whole control + // budget is spent waiting for something that is not coming, and it was: about + // thirty seconds during which somebody clicks download and watches four rows + // hang. Asking first costs one message and makes the common case immediate. + if (!navigator.serviceWorker.controller && reg.active) { + try { reg.active.postMessage({ type: 'mbdl-claim' }); } catch { /* gone */ } + } const controller = await _awaitControl(left()); if (controller) return controller; // Active but not controlling after the whole budget. `sw.js` calls @@ -367,10 +377,17 @@ async function _claimController(budgetMs) { */ export function primeServiceWorker() { if (!STREAMS_VIA_SW) return; - serviceWorker().then((worker) => worker && _repairIfBypassed(worker)) + _priming = serviceWorker() + .then((worker) => worker && _repairIfBypassed(worker)) .catch(() => {}); } +// Resolved once the check below has run. A download that starts while it is +// still in flight waits for it rather than racing it: on a page that turns out +// to be unservable the click would otherwise spend thirty seconds failing on a +// 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. @@ -495,6 +512,7 @@ export async function openStreamedDownload(filename, size = 0, { servedMs = SW_SERVED_BUDGET_MS, attempts = SW_ATTEMPTS, } = {}) { + if (_priming) { try { await _priming; } catch { /* reported already */ } } for (let attempt = 1; attempt <= attempts; attempt++) { const target = await _attemptStreamedDownload( filename, size, attempt, controlMs, servedMs); diff --git a/packages/meshbay-hub/tests/test_streamed_download_reliability.py b/packages/meshbay-hub/tests/test_streamed_download_reliability.py index 3033a95..355fbff 100644 --- a/packages/meshbay-hub/tests/test_streamed_download_reliability.py +++ b/packages/meshbay-hub/tests/test_streamed_download_reliability.py @@ -59,7 +59,15 @@ let controller = null; const pendingByFrame = new Map(); const makeController = () => ({ postMessage: (msg, transfer) => { - if (msg.type === 'mbdl-claim') { log.claims += 1; return; } + if (msg.type === 'mbdl-claim') { + log.claims += 1; + // A worker that actually claims when asked, which is what sw.js does. + if (PLAN.controlOnClaim) { + controller = makeController(); + for (const fn of listeners) fn(); + } + return; + } if (msg.type !== 'mbdl') return; pendingByFrame.set('/_mbdl/' + msg.id, msg.port); }, @@ -152,7 +160,7 @@ const out = {}; def _run(tmp_path, body, *, control_after_ms=0, active=True, serve="always", register_throws=False, control_budget_ms=800, ready_settles=True, register_hangs=False, - active_after_unregister=False): + active_after_unregister=False, control_on_claim=False): module = tmp_path / "downloads.mjs" module.write_text(DOWNLOADS.read_text()) (tmp_path / "package.json").write_text('{"type":"module"}') @@ -164,6 +172,7 @@ def _run(tmp_path, body, *, control_after_ms=0, active=True, "readySettles": ready_settles, "registerHangs": register_hangs, "activeAfterUnregister": active_after_unregister, + "controlOnClaim": control_on_claim, } script = tmp_path / "case.mjs" script.write_text( @@ -451,3 +460,44 @@ def test_the_self_test_leaves_no_file_behind(tmp_path): "the self-test's stream is never aborted, so the browser keeps what it " "was given") assert "frame.remove" in fn + + +# ── The claim is asked for, not waited for ────────────────────────────────── + +def test_an_uncontrolled_page_asks_at_once_rather_than_after_the_budget(tmp_path): + """A page that is uncontrolled while an active worker exists will not be + claimed on its own — a document fetched by a hard reload is exactly that + shape. Waiting the whole control budget first spends it on something that + is not coming: about thirty seconds, measured, during which the person + clicks download and watches four rows hang before the page repairs itself. + """ + out = _run(tmp_path, """ + const t0 = Date.now(); + const target = await M.openStreamedDownload('film.mkv', 20e9, FAST); + out.ms = Date.now() - t0; + out.target = target !== null; + out.claims = log.claims; + // Closing stops the keep-alive; left open, its interval outlives the test. + if (target) await target.writable.close(); + """, control_after_ms=None, control_on_claim=True, control_budget_ms=6000) + assert out["target"] is True + assert out["claims"] >= 1 + assert out["ms"] < 3000, ( + f"took {out['ms']}ms of a 6000ms budget — the claim was asked for only " + "after the wait, not before it") + + +def test_a_download_waits_for_the_self_test(tmp_path): + """A click that lands while the check is still running must not race it. + On a page that turns out to be unservable it would otherwise spend the full + two attempts failing on a path that is about to be repaired.""" + out = _run(tmp_path, """ + M.primeServiceWorker(); + const t0 = Date.now(); + const target = await M.openStreamedDownload('film.mkv', 20e9, FAST); + out.ms = Date.now() - t0; + out.target = target !== null; + if (target) await target.writable.close(); + """) + assert out["target"] is True + assert out["ms"] >= 1, "the download did not wait for priming at all" -- cgit v1.2.3