diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-10-07 11:50:48 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-10-07 11:50:48 +0200 |
| commit | 00caeb3e10bfbf86892e7f31ad81f91ce8e50da7 (patch) | |
| tree | cc9b9bf52377fb379a0eb41c14da50050b308ded /packages/meshbay-hub/src/meshbay_hub | |
| parent | 219093c823494bc8d760c99a7a55c4a23059abb5 (diff) | |
| download | meshbay-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')
| -rw-r--r-- | packages/meshbay-hub/src/meshbay_hub/static/file-utils.js | 45 | ||||
| -rw-r--r-- | packages/meshbay-hub/src/meshbay_hub/static/transport.js | 2 |
2 files changed, 40 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++; } }; diff --git a/packages/meshbay-hub/src/meshbay_hub/static/transport.js b/packages/meshbay-hub/src/meshbay_hub/static/transport.js index 5ed6fa9..9dcea4f 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/transport.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/transport.js @@ -621,6 +621,8 @@ class MeshBayTransport { } get connected() { return this._connected; } + /** True while _reconnectLoop is running, backoff included. */ + get reconnecting() { return !!this._reconnectPromise; } set onChat(fn) { this._onChat = fn; } set onStreamInit(fn) { this._onStreamInit = fn; } |