diff options
Diffstat (limited to 'packages/meshbay-hub/src')
| -rw-r--r-- | packages/meshbay-hub/src/meshbay_hub/static/downloads.js | 58 | ||||
| -rw-r--r-- | packages/meshbay-hub/src/meshbay_hub/static/sw.js | 15 |
2 files changed, 72 insertions, 1 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/downloads.js b/packages/meshbay-hub/src/meshbay_hub/static/downloads.js index ce1901c..c790394 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/downloads.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/downloads.js @@ -232,6 +232,16 @@ const SW_KEEPALIVE_MS = 10000; // target does not ping for ever. Bounded because the alternative is a timer // whose lifetime depends on every caller remembering to close its sink. const SW_KEEPALIVE_IDLE_MS = 120000; +// How long to spend waking the worker, and then confirming it holds the stream, +// before starting the navigation that has to find it. +// +// Both are answered in milliseconds when the worker is alive. They exist for +// when it is not: `pending` lives in the worker's memory, and one with nothing +// to do is terminated within tens of seconds — which a long upload spends +// without giving it a single event. A stream handed to a worker in that state +// is lost, and the iframe then wakes it with nothing to find, which is a 404 +// from the hub and fifteen seconds of silence per attempt. +const SW_WAKE_BUDGET_MS = 3000; // Holds a *successful* controller, or an in-flight attempt. Never a failure — // see serviceWorker(). The previous version cached the rejected/null result @@ -504,6 +514,35 @@ export async function openStreamedDownload(filename, size = 0, { return null; } +/** + * Get the worker running, and know that it is. + * + * `mbdl-ping` exists already — the page sends it every ten seconds *while* + * writing, because a streaming response does not count as activity and Firefox + * kills an idle worker mid-download. Nothing sent one before *starting* a + * download, which is the case that fails after a long upload has left the + * worker with nothing to do for minutes. + * + * Never fatal: a worker that does not answer may still be perfectly able to + * serve, and the caller finds that out the honest way. + */ +async function _wake(worker) { + const chan = new MessageChannel(); + const pong = new Promise((resolve) => { + chan.port1.onmessage = () => resolve(true); + }); + try { + worker.postMessage({ type: 'mbdl-ping' }, [chan.port2]); + } catch { + return false; + } + const awake = await Promise.race([ + pong, new Promise((r) => setTimeout(() => r(false), SW_WAKE_BUDGET_MS)), + ]); + try { chan.port1.close(); } catch { /* already gone */ } + return awake; +} + async function _attemptStreamedDownload(filename, size, attempt, controlMs, servedMs) { const worker = await serviceWorker(controlMs); @@ -517,12 +556,23 @@ async function _attemptStreamedDownload(filename, size, attempt, // backpressure that will never be relieved, which reads as a download frozen // after one chunk rather than as an error. const chan = new MessageChannel(); + let markReady = null; + const held = new Promise((resolve) => { markReady = resolve; }); const serving = new Promise((resolve) => { chan.port1.onmessage = (e) => { - if (e.data && e.data.type === 'mbdl-serving') resolve(true); + if (!e.data) return; + // The worker says it has the stream. Waiting for this is what stops the + // navigation racing a worker that was asleep when we posted. + if (e.data.type === 'mbdl-ready') markReady(true); + if (e.data.type === 'mbdl-serving') resolve(true); }; }); + // Wake it first, and wait for the answer. A worker that has been idle through + // a long upload is terminated, and a message posted to it in that state is + // lost — silently, which is the whole difficulty. + await _wake(worker); + try { worker.postMessage({ type: 'mbdl', id, filename, size, readable, port: chan.port2 }, [readable, chan.port2]); @@ -536,6 +586,12 @@ async function _attemptStreamedDownload(filename, size, attempt, return null; } + // Confirmed, not assumed. A worker that predates this sends no answer, and + // then navigating anyway is exactly what this code did before. + await Promise.race([ + held, new Promise((r) => setTimeout(r, SW_WAKE_BUDGET_MS)), + ]); + const frame = document.createElement('iframe'); frame.hidden = true; frame.src = `${PREFIX_PATH}${id}`; diff --git a/packages/meshbay-hub/src/meshbay_hub/static/sw.js b/packages/meshbay-hub/src/meshbay_hub/static/sw.js index 309ecc1..5f663f9 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/sw.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/sw.js @@ -89,6 +89,21 @@ self.addEventListener('message', (event) => { // stream and the page's first write blocks for good. port: data.port || null, }); + // Say so, on the port the page is already listening to. + // + // `pending` is in memory, and a worker with nothing to do is terminated: + // Chrome after tens of seconds, which a long upload spends without giving + // this worker a single event. A stream posted to a worker in that state is + // lost, the iframe then wakes it with no entry to find, and the request falls + // through to the network — measured as a 404 from the hub and thirty seconds + // of nothing, twice, before the download started at all. + // + // The page waits for this before navigating, so the entry is known to be here + // rather than hoped to be. A page talking to an older worker gets no answer + // and navigates anyway, which is what it did before. + if (data.port) { + try { data.port.postMessage({ type: 'mbdl-ready', id: data.id }); } catch { /* gone */ } + } // A tab that is closed before it navigates would leave a stream here for the // life of the worker. setTimeout(() => pending.delete(data.id), 60000); |