diff options
| author | Adam Soltys <[email protected]> | 2026-06-15 13:21:42 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-06-15 13:21:42 -0700 |
| commit | adad0731862e6c07e400db010c566c548848d86c (patch) | |
| tree | 561d0a16804dee14ccbae4dba43d9e5f09c618cf /src | |
| parent | 3dd7f8425ecf339332cc44b6f9734b51472d22c1 (diff) | |
| download | coinosv3-adad0731862e6c07e400db010c566c548848d86c.tar.gz coinosv3-adad0731862e6c07e400db010c566c548848d86c.zip | |
Keep the WebSocket alive: heartbeat + reconnect-on-wake
The mempool WS sends no periodic traffic on a track-addresses-only
subscription, so a half-open connection (died without firing onclose) went
undetected and updates fell back to the slow poll. Add:
- ping every 15s ({action:'ping'} -> pong); if no message for 45s, force a
reconnect (handles half-open sockets).
- reconnect + refresh on tab refocus (visibilitychange) and network 'online'.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Diffstat (limited to 'src')
| -rw-r--r-- | src/wallet.js | 47 |
1 files changed, 47 insertions, 0 deletions
diff --git a/src/wallet.js b/src/wallet.js index 0d24d61..b0d37a9 100644 --- a/src/wallet.js +++ b/src/wallet.js @@ -84,6 +84,9 @@ export class Wallet { this._wsRetry = null; this._refreshTimer = null; this._pollTimer = null; + this._hbTimer = null; // heartbeat / liveness watchdog + this._lastMsg = 0; // time of last WS message (incl. pong) + this._wakeHooked = false; this._subs = new Set(); } @@ -458,6 +461,47 @@ export class Wallet { this._pollTimer = setInterval(() => this.scan({ silent: true }), 12000); if (typeof WebSocket === 'undefined') 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. + this._hbTimer = setInterval(() => { + if (this.offline || !this._ws || this._ws.readyState !== 1) return; + if (Date.now() - this._lastMsg > 45000) { + this._reconnectNow(); + } else { + try { + this._ws.send(JSON.stringify({ action: 'ping' })); + } catch {} + } + }, 15000); + + // Reconnect + refresh when the tab refocuses or the network returns. + if (!this._wakeHooked && typeof document !== 'undefined') { + this._wakeHooked = true; + const wake = () => { + if (this.offline || !this._wsWant) return; + if (!this._ws || this._ws.readyState !== 1) this._reconnectNow(); + this.scan({ silent: true }).catch(() => {}); + }; + document.addEventListener('visibilitychange', () => { + if (document.visibilityState === 'visible') wake(); + }); + window.addEventListener('online', wake); + } + + this._connectWs(); + } + + _reconnectNow() { + clearTimeout(this._wsRetry); + if (this._ws) { + try { + this._ws.onclose = null; + this._ws.close(); + } catch {} + this._ws = null; + } + this.live = false; this._connectWs(); } @@ -473,10 +517,12 @@ export class Wallet { this._ws = ws; ws.onopen = () => { this.live = true; + this._lastMsg = Date.now(); this.retrack(); this.emit(); }; ws.onmessage = (ev) => { + this._lastMsg = Date.now(); let msg; try { msg = JSON.parse(typeof ev.data === 'string' ? ev.data : ''); @@ -577,6 +623,7 @@ export class Wallet { clearTimeout(this._wsRetry); clearTimeout(this._refreshTimer); clearInterval(this._pollTimer); + clearInterval(this._hbTimer); if (this._ws) { try { this._ws.onclose = null; |
