aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/src/meshbay_hub/static/file-utils.js
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-10-07 11:50:48 +0200
committerChristophe Besson <cbesson@gmail.com>2026-10-07 11:50:48 +0200
commit00caeb3e10bfbf86892e7f31ad81f91ce8e50da7 (patch)
treecc9b9bf52377fb379a0eb41c14da50050b308ded /packages/meshbay-hub/src/meshbay_hub/static/file-utils.js
parent219093c823494bc8d760c99a7a55c4a23059abb5 (diff)
downloadmeshbay-00caeb3e10bfbf86892e7f31ad81f91ce8e50da7.tar.gz
fix(hub): let a download wait out a reconnect instead of failing
Chunks sent while the reconnect's connect() runs throw at once, and six retries 1.5 s apart ran out before the reconnect landed. Wait for it, up to two minutes, without spending retries. Follow-ups parked in §15.3. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub/static/file-utils.js')
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/file-utils.js45
1 files changed, 38 insertions, 7 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/file-utils.js b/packages/meshbay-hub/src/meshbay_hub/static/file-utils.js
index 2ff2bd4..294455a 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/file-utils.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/file-utils.js
@@ -297,12 +297,23 @@ function _saveBlob(blob, filename) {
// problem, and the file already on disk (writable has real bytes in it by
// now) is worth more than an all-or-nothing download: retry the same chunk
// instead of letting one bad moment abort the whole transfer. Each retry
-// re-enters transport.fetchChunk, whose own _sendAndWait waits out an
-// in-flight reconnect before trying again, so this loop is mostly just
-// giving that reconnect the time and the attempts to land.
+// re-enters transport.fetchChunk. These attempts are for a request that
+// failed on a connection that is otherwise fine; a reconnect is waited out
+// separately (below) and costs none of them.
const CHUNK_RETRY_ATTEMPTS = 6;
const CHUNK_RETRY_DELAY_MS = 1500;
+// How long one chunk waits for a reconnect to land before its download fails.
+//
+// _sendAndWait does not wait while the reconnect's own connect() is running
+// (`_inReconnectAttempt`, which has to let the handshake through), so every
+// chunk asked for in that window throws "DataChannel not open (state:
+// connecting)" at once. Six attempts 1.5 s apart were gone in under ten
+// seconds, while one connect() can take longer than that on its own: found
+// live, a reconnect whose first attempt ended in a 504 failed the download
+// that was running, and the reconnect landed a moment later.
+const RECONNECT_WAIT_MS = 120000;
+
// How long one megabyte may take to reach the disk before we call it stuck.
//
// Every other await on this path is bounded and says so when it expires:
@@ -339,15 +350,34 @@ function _isRetryableTransportError(err) {
|| (err.message || '').startsWith('DataChannel not open');
}
-async function _fetchChunkResilient(transport, fileId, index, tr = '') {
+/** True once a reconnect that was in flight has landed. Gives up at
+ * `deadline`, or as soon as the transfer is cancelled. */
+async function _sitOutReconnect(transport, signal, deadline) {
+ while (transport.reconnecting && Date.now() < deadline
+ && !(signal && signal.aborted)) {
+ await transport.waitForReconnect(Math.min(250, deadline - Date.now()));
+ }
+ return !transport.reconnecting && transport.connected;
+}
+
+async function _fetchChunkResilient(transport, fileId, index, tr = '', signal = null) {
let lastErr;
- for (let attempt = 0; attempt < CHUNK_RETRY_ATTEMPTS; attempt++) {
+ const deadline = Date.now() + RECONNECT_WAIT_MS;
+ for (let attempt = 0; attempt < CHUNK_RETRY_ATTEMPTS;) {
try {
return await transport.fetchChunk(fileId, index, tr);
} catch (err) {
if (!_isRetryableTransportError(err)) throw err;
lastErr = err;
- if (attempt < CHUNK_RETRY_ATTEMPTS - 1) {
+ if (transport.reconnecting
+ && await _sitOutReconnect(transport, signal, deadline)) continue;
+ if (signal && signal.aborted) {
+ const abort = new Error('Cancelled');
+ abort.name = 'AbortError';
+ throw abort;
+ }
+ attempt++;
+ if (attempt < CHUNK_RETRY_ATTEMPTS) {
await new Promise((r) => setTimeout(r, CHUNK_RETRY_DELAY_MS));
}
}
@@ -364,7 +394,8 @@ async function pipelinedDownload(transport, gekKey, fileId, totalChunks, onChunk
const fire = () => {
while (nextSend < totalChunks && nextSend - nextRecv < PIPELINE_WINDOW) {
- inflight[nextSend] = _fetchChunkResilient(transport, fileId, nextSend, tr);
+ inflight[nextSend] = _fetchChunkResilient(transport, fileId, nextSend, tr,
+ signal);
nextSend++;
}
};