aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/src
diff options
context:
space:
mode:
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);