diff options
| -rw-r--r-- | packages/meshbay-hub/src/meshbay_hub/static/transport.js | 40 |
1 files changed, 39 insertions, 1 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/transport.js b/packages/meshbay-hub/src/meshbay_hub/static/transport.js index 4eb7ac2..35f729e 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/transport.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/transport.js @@ -237,6 +237,27 @@ class MeshBayTransport { this._inReconnectAttempt = false; this._onReconnected = null; this._onNeedToken = null; + // Cuts the backoff wait short the moment the page is foregrounded again — + // found live to matter: a screen lock throttles the tab's own timers + // along with everything else, so a backoff already counting down when the + // phone locked can run for minutes of *wall clock* past its nominal delay + // before it next gets to run at all. Set once, here, rather than inside + // connect() like the diagnostic listener above it — this one has to + // survive every reconnect attempt, not restart with each one. + this._reconnectWakeResolve = null; + this._onVisibilityWake = () => { + if (document.visibilityState === 'visible') this._wakeReconnect(); + }; + document.addEventListener('visibilitychange', this._onVisibilityWake); + } + + /** Cuts short a reconnect currently backing off (see _reconnectLoop). A + * no-op when nothing is waiting, so this is safe to call unconditionally. */ + _wakeReconnect() { + if (this._reconnectWakeResolve) { + this._reconnectWakeResolve(); + this._reconnectWakeResolve = null; + } } get connected() { return this._connected; } @@ -367,6 +388,14 @@ class MeshBayTransport { // a trace captures exactly what state the connection was in right as the // page comes back from being backgrounded/locked — never active unless // trace mode is on (see TRACE_KEY above). + // + // connect() runs again on every reconnect attempt (see _reconnectLoop), + // and each run used to add its own listener/interval on top of the + // previous one without ever removing it — confirmed live: 8 failed + // attempts during one screen lock left 8 duplicate `visibility` trace + // lines firing off the same real event. Disposing of the prior instance + // first is what keeps this to one. + if (this._diagCleanup) { this._diagCleanup(); this._diagCleanup = null; } if (traceEnabled()) { const healthPing = async (reason) => { const before = { @@ -673,7 +702,14 @@ class MeshBayTransport { this._reconnectAttempts += 1; const delayMs = Math.min(30000, 1000 * 2 ** (this._reconnectAttempts - 1)); trace('reconnect_wait', { attempt: this._reconnectAttempts, delay_ms: delayMs }); - await new Promise((r) => setTimeout(r, delayMs)); + // Interruptible: _wakeReconnect (fired on visibilitychange → visible) + // resolves this immediately instead of waiting out the rest of a + // backoff that was mostly spent while nothing could succeed anyway. + await new Promise((resolve) => { + const timer = setTimeout(resolve, delayMs); + this._reconnectWakeResolve = () => { clearTimeout(timer); resolve(); }; + }); + this._reconnectWakeResolve = null; if (this._closed) return; try { // Best-effort: these are already unusable, but leaving them wired up @@ -1757,6 +1793,8 @@ class MeshBayTransport { // only skips reconnecting because of this flag, not because "closed" is // absent from its own trigger condition. this._closed = true; + document.removeEventListener('visibilitychange', this._onVisibilityWake); + this._wakeReconnect(); if (this._diagCleanup) { this._diagCleanup(); this._diagCleanup = null; } if (this._channel) this._channel.close(); if (this._pc) this._pc.close(); |