From 5de1b09a26745f3ba1ba54e3e666266c2620352c Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Sat, 27 Jun 2026 13:43:39 -0700 Subject: Harden Electrum WS recovery: fail in-flight calls on drop, faster half-open detect MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit After a socket drop the in-flight _rpcCall requests were never rejected (only stopRealtime did that), so dead calls held every _rpcPump slot until they timed out — stalling the post-reconnect refresh by ~10s even though the new socket and its pushes were already live. Now reject pending calls in ws.onclose and in _reconnectNow (which nulls onclose), so the pump frees instantly and the on-open refreshLive runs right away. Also tighten the half-open watchdog: ping every 10s and recycle the socket after 21s of silence (two unanswered pings) instead of 45s, so a silently-dead connection — no onclose, no online/visibility event — resumes pushing in ~20s instead of ~45s. Verified via CDP: force-closing the live socket reconnects in ~0.6s and the next deposit pushes in ~0.4s (UI ~1.2s). Co-Authored-By: Claude Opus 4.8 --- src/wallet.js | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) (limited to 'src') diff --git a/src/wallet.js b/src/wallet.js index 5882ee2..7236a9f 100644 --- a/src/wallet.js +++ b/src/wallet.js @@ -1481,16 +1481,18 @@ export class Wallet { if (typeof WebSocket === 'undefined' || !this.wsUrl()) return; this._wsWant = true; - // Heartbeat: ping every 15s; if no message (incl. pong) for 45s the socket - // is half-open (died without firing onclose) — force a reconnect. + // Heartbeat: ping every 10s; if nothing comes back (incl. our own pong) for + // 21s the socket is half-open (died without firing onclose) — recycle it. + // The trigger is two unanswered pings, so one slow round-trip won't churn, + // but a silently-dead socket now recovers in ~20s instead of ~45s. this._hbTimer = setInterval(() => { if (this.offline || !this._ws || this._ws.readyState !== 1) return; - if (Date.now() - this._lastMsg > 45000) { + if (Date.now() - this._lastMsg > 21000) { this._reconnectNow(); } else { this._rpcSend('server.ping', []); } - }, 15000); + }, 10000); // Reconnect + refresh when the tab refocuses or the network returns. if (!this._wakeHooked && typeof document !== 'undefined') { @@ -1518,6 +1520,8 @@ export class Wallet { } catch {} this._ws = null; } + // onclose was nulled above so it won't fire — fail in-flight calls here too. + this._rejectPending('socket recycled'); this.live = false; this._connectWs(); } @@ -1581,6 +1585,10 @@ export class Wallet { ws.onclose = () => { this.live = false; this._ws = null; + // Fail any in-flight data calls so the _rpcCall pump frees its slots + // immediately; otherwise dead calls hold the pump until they time out and + // the post-reconnect refresh stalls behind them (~10s of stale UI). + this._rejectPending('socket closed'); this.emit(); this._scheduleReconnect(); }; -- cgit v1.2.3