aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/src
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-09-09 02:45:23 +0200
committerChristophe Besson <cbesson@gmail.com>2026-09-09 02:45:23 +0200
commit88de521725d8de10eb8bf956b31cf9ce70a81f33 (patch)
treeff8e39a71d05ae12ffa1e6a7039fe7de33707e5d /packages/meshbay-hub/src
parent7e480254a014a3e72815e8b971d5560d42872c5a (diff)
downloadmeshbay-88de521725d8de10eb8bf956b31cf9ce70a81f33.tar.gz
fix(spa): nothing on the worker path may wait for ever
Four downloads on Firefox sat at "preparing" indefinitely, with the target queue already bypassed there, so each opening was hanging on its own. The node journal showed `d=0/8(q0) u=0/8(q0)` — no transfer had been asked for yet. `_claimController` had two waits with no deadline at all, `navigator.serviceWorker.register()` and `navigator.serviceWorker.ready`, while SW_CONTROL_BUDGET_MS bounded only the wait that comes after them. `_swPromise` is shared, so one unsettled wait left every download on the page suspended on the same promise for the life of the tab. Measured on Firefox 154, against a local 127.0.0.1 site so no hub was involved: a worker that installs gives register() in 8ms and ready in 0ms; a worker whose install handler rejects gives register() in 7ms and a `ready` that never settles — still pending past ten seconds. register() resolves as soon as the registration object exists, carrying nothing but an *installing* worker; ready is what waits for an active one. Every wait is now inside one budget, with two carve-outs so that a deadline never costs a capability. A `ready` that times out while registration.active is set is not fatal: ready may be waiting on a newer worker that cannot install while an older one serves perfectly well. And the mbdl-claim recovery keeps its own budget outside the deadline, because giving up there would cost Firefox the only unbounded way it has to write a download to disk. A deadline alone would have been a better-explained failure rather than a fix: a registration stuck with nothing but an installing worker does not heal, and every later visit finds the same one. So when ready times out with no active worker, the registration is discarded and asked for once more with a fresh budget, and the page repairs itself instead of needing developer tools. Four cases pinned, each checked against the unfixed source. Hub suite 830 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')
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/downloads.js84
1 files changed, 77 insertions, 7 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/downloads.js b/packages/meshbay-hub/src/meshbay_hub/static/downloads.js
index cfb0051..7e4802a 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/downloads.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/downloads.js
@@ -261,19 +261,89 @@ function _awaitControl(budgetMs) {
});
}
+/**
+ * The promise's value, or `TIMED_OUT` once `ms` is spent.
+ *
+ * A rejection is still a rejection — the caller reports those — and the timer
+ * is cleared either way, so nothing is left running behind a fast answer.
+ */
+const TIMED_OUT = Symbol('timed out');
+
+function _within(promise, ms) {
+ return new Promise((resolve, reject) => {
+ const timer = setTimeout(() => resolve(TIMED_OUT), ms);
+ promise.then((v) => { clearTimeout(timer); resolve(v); },
+ (err) => { clearTimeout(timer); reject(err); });
+ });
+}
+
async function _claimController(budgetMs) {
- const reg = await navigator.serviceWorker.register(SW_PATH, { scope: '/' });
- // `ready` resolves on an *active* registration; being active is not being in
- // control. An uncontrolled page's 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 this was found.
- await navigator.serviceWorker.ready;
- const controller = await _awaitControl(budgetMs);
+ // Every wait in here is inside one budget. Neither of the first two used to
+ // have any deadline at all, and `_swPromise` is shared, so a single one of
+ // them left every download on the page waiting on the same promise for ever
+ // — four rows stuck at "preparing", with nothing in the node's journal
+ // because no transfer had been asked for yet.
+ const deadline = Date.now() + budgetMs;
+ const left = () => Math.max(0, deadline - Date.now());
+
+ let reg = await _within(
+ navigator.serviceWorker.register(SW_PATH, { scope: '/' }), left());
+ if (reg === TIMED_OUT) {
+ _lastFailure = `the worker did not register within ${budgetMs / 1000}s`;
+ return null;
+ }
+ // `register()` resolves as soon as the registration object exists, with
+ // nothing but an *installing* worker; `ready` is what waits for an active
+ // one. A worker that never finishes installing leaves `ready` pending
+ // indefinitely — measured on Firefox 154: an install handler that rejects
+ // leaves `ready` unsettled past ten seconds while `register()` returns in
+ // seven milliseconds.
+ let ready = await _within(navigator.serviceWorker.ready, left());
+ if (ready === TIMED_OUT && !reg.active) {
+ // A registration stuck with nothing but an installing worker does not heal
+ // on its own: every later visit finds the same registration and waits on
+ // the same `ready`. Left alone it is permanent, and it costs Firefox the
+ // only unbounded way it has to write a download to disk — so the stuck
+ // registration is thrown away and asked for once more, with its own budget,
+ // rather than reported and lived with.
+ console.warn('[MeshBay] the download worker never became active; '
+ + 'discarding the registration and asking again');
+ try {
+ await _within(reg.unregister(), budgetMs);
+ } catch (err) {
+ console.warn('[MeshBay] could not discard it:', err.message);
+ }
+ const again = await _within(
+ navigator.serviceWorker.register(SW_PATH, { scope: '/' }), budgetMs);
+ if (again === TIMED_OUT) {
+ _lastFailure = `the worker did not register within ${budgetMs / 1000}s`;
+ return null;
+ }
+ reg = again;
+ ready = await _within(navigator.serviceWorker.ready, budgetMs);
+ if (ready === TIMED_OUT && !reg.active) {
+ _lastFailure = `the worker did not become active within ${budgetMs / 1000}s`
+ + ', even after its registration was discarded';
+ return null;
+ }
+ }
+ // Past here `ready` may still be waiting on a *newer* worker that cannot
+ // install while an older one is perfectly able to serve. An active worker is
+ // all this path needs, so a stuck `ready` is not on its own a reason to give
+ // up a capability Firefox has nothing else to offer for.
+ //
+ // Being active is not being in control, either. An uncontrolled page's
+ // 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.
+ const controller = await _awaitControl(left());
if (controller) return controller;
// Active but not controlling after the whole budget. `sw.js` calls
// `clients.claim()` on activate, so this is rare; when it happens the page
// was loaded before any worker existed and the claim was missed. Ask the
// active worker to claim again rather than declare the path unavailable.
+ // This wait is deliberately outside the budget above: giving up here would
+ // cost Firefox the only unbounded way it has to write a download to disk.
if (reg.active) {
try { reg.active.postMessage({ type: 'mbdl-claim' }); } catch { /* gone */ }
return await _awaitControl(2000);