diff options
Diffstat (limited to 'packages/meshbay-hub/src')
| -rw-r--r-- | packages/meshbay-hub/src/meshbay_hub/static/downloads.js | 84 |
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); |