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.js58
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/sw.js15
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);