From 27d59cbb2d11d863273e2282257d81ac9911ab97 Mon Sep 17 00:00:00 2001 From: Christophe Besson Date: Wed, 26 Aug 2026 11:17:37 +0200 Subject: fix(transport): auto-reconnect after WebRTC failure, without dropping in-flight streams/downloads MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Confirmed live (client trace + node logs, mobile screen-lock ~5min): ICE goes disconnected -> failed within ~10s on both ends, but the DataChannel's readyState stays "open" throughout, so nothing failed fast — every request just sat out its own 8s/30s timeout, matching the reported symptom (poster spinners, blocked chat, dead new streams). transport.js: on connectionState "failed", reject pending requests immediately (TransportLostError) and start a self-contained reconnect loop (capped exponential backoff, redoes the full signaling handshake — the node already discards the old session on its own "failed"/"closed", so there is nothing lower-level to resume). New hooks: onNeedToken (fetch a fresh JWT, since the captured one may have expired during the outage) and onReconnected (let a consumer resume something that was mid-flight). file-utils.js: pipelinedDownload retries a lost chunk instead of aborting the whole transfer — covers Files downloads, poster/thumbnail fetches, and music-player.js's blob-based track download, all of which go through it. video-player.js: onReconnected reissues the existing seek-to-current-time path, which already knows how to land a new stream_init on the live SourceBuffer without resetting playback. Playing audio is unaffected either way — musicbay.md's design downloads a track to a blob before playing it, so a dead transport was never a network dependency for what is already playing. Stays on this branch until confirmed by real-device testing. --- .../src/meshbay_hub/static/file-utils.js | 37 +++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) (limited to 'packages/meshbay-hub/src/meshbay_hub/static/file-utils.js') 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 b44d105..ba76ac9 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/file-utils.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/file-utils.js @@ -117,6 +117,41 @@ function _b64ToU8(b64) { return arr; } +// A dead transport (screen-lock WebRTC failure, see transport.js's +// _reconnectLoop) surfaces here as a rejected fetchChunk — TransportLostError +// when the pending request was killed outright, a plain timeout if it was +// still waiting when this ran. Either way the chunk itself was never the +// 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. +const CHUNK_RETRY_ATTEMPTS = 6; +const CHUNK_RETRY_DELAY_MS = 1500; + +function _isRetryableTransportError(err) { + return err.name === 'TransportLostError' + || err.message === 'Response timeout' + || (err.message || '').startsWith('DataChannel not open'); +} + +async function _fetchChunkResilient(transport, fileId, index) { + let lastErr; + for (let attempt = 0; attempt < CHUNK_RETRY_ATTEMPTS; attempt++) { + try { + return await transport.fetchChunk(fileId, index); + } catch (err) { + if (!_isRetryableTransportError(err)) throw err; + lastErr = err; + if (attempt < CHUNK_RETRY_ATTEMPTS - 1) { + await new Promise((r) => setTimeout(r, CHUNK_RETRY_DELAY_MS)); + } + } + } + throw lastErr; +} + async function pipelinedDownload(transport, gekKey, fileId, totalChunks, onChunk, writable, signal) { const results = writable ? null : new Array(totalChunks); @@ -125,7 +160,7 @@ async function pipelinedDownload(transport, gekKey, fileId, totalChunks, onChunk const fire = () => { while (nextSend < totalChunks && nextSend - nextRecv < PIPELINE_WINDOW) { - inflight[nextSend] = transport.fetchChunk(fileId, nextSend); + inflight[nextSend] = _fetchChunkResilient(transport, fileId, nextSend); nextSend++; } }; -- cgit v1.2.3