diff options
| author | Adam Soltys <[email protected]> | 2026-06-15 15:41:23 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-06-15 15:41:23 -0700 |
| commit | c57f06e47eb5b662a00cddf27cd4270e8418d163 (patch) | |
| tree | cf72cd6204e07ac29b37d30839e58be216876ef4 /src | |
| parent | e2556617ef4c1632217b3731afe8e519369cbeab (diff) | |
| download | coinosv3-c57f06e47eb5b662a00cddf27cd4270e8418d163.tar.gz coinosv3-c57f06e47eb5b662a00cddf27cd4270e8418d163.zip | |
Cache stable addresses; poll only the live frontier
Idle polls were re-querying every used address (~N requests each cycle). Now
the background poll checks only addresses that can change — those holding
coins and the next fresh receive/change address — and escalates to a full
scan only when one of them moves. Stable historical addresses stay cached.
A full scan still runs every 5 min as a safety net. Cuts steady-state request
volume dramatically for wallets with many used addresses.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Diffstat (limited to 'src')
| -rw-r--r-- | src/wallet.js | 54 |
1 files changed, 50 insertions, 4 deletions
diff --git a/src/wallet.js b/src/wallet.js index b0d37a9..241572a 100644 --- a/src/wallet.js +++ b/src/wallet.js @@ -84,6 +84,8 @@ export class Wallet { this._wsRetry = null; this._refreshTimer = null; this._pollTimer = null; + this._deepTimer = null; // periodic full-scan safety net + this._polling = false; // a light poll is in flight this._hbTimer = null; // heartbeat / liveness watchdog this._lastMsg = 0; // time of last WS message (incl. pong) this._wakeHooked = false; @@ -222,6 +224,48 @@ export class Wallet { }); } + _addrInfo(chain, index) { + const arr = chain === 0 ? this.receive : this.change; + return arr.find((a) => a.index === index); + } + + // Light poll: only re-query the addresses that can actually change — ones + // holding coins (could be spent) and the next fresh receive/change address + // (could receive). Stable historical addresses stay cached and aren't + // re-fetched. If anything moved, fall back to a full scan to reconcile. + async refreshLive() { + if (this.offline || this._refreshing || this._polling) return; + this._polling = true; + try { + const targets = new Map(); + for (const u of this.utxos) targets.set(`${u.chain}/${u.index}`, { chain: u.chain, index: u.index }); + targets.set(`0/${this.nextReceiveIndex}`, { chain: 0, index: this.nextReceiveIndex }); + targets.set(`1/${this.nextChangeIndex}`, { chain: 1, index: this.nextChangeIndex }); + + let changed = false; + for (const { chain, index } of targets.values()) { + const { address } = this.derive(chain, index); + const info = await this.api.addressInfo(address); + const cs = info.chain_stats || {}; + const ms = info.mempool_stats || {}; + const confirmed = (cs.funded_txo_sum || 0) - (cs.spent_txo_sum || 0); + const pending = (ms.funded_txo_sum || 0) - (ms.spent_txo_sum || 0); + const used = (cs.tx_count || 0) > 0 || (ms.tx_count || 0) > 0; + const prev = this._addrInfo(chain, index); + if (!prev || prev.confirmed !== confirmed || prev.pending !== pending || prev.used !== used) { + changed = true; + break; + } + } + if (changed) { + this._polling = false; + await this.scan({ silent: true }); + } + } finally { + this._polling = false; + } + } + // silent=false: foreground load (shows loading state, always re-renders). // silent=true: background refresh (poll / WS) — no spinner, and only emits // if the visible state changed, so it never disrupts typing. @@ -455,10 +499,11 @@ export class Wallet { startRealtime() { if (this.offline) return; this.stopRealtime(); - // Safety-net poll: the WebSocket gives instant updates, but its pushes can - // be missed (connection drops, server quirks). A light periodic silent - // refresh guarantees pending balances appear within a few seconds. - this._pollTimer = setInterval(() => this.scan({ silent: true }), 12000); + // Safety-net polling (the WebSocket gives instant updates, but its pushes + // can be missed): a light poll of just the live addresses every 15s, plus + // an occasional full scan to catch anything the frontier check can't. + this._pollTimer = setInterval(() => this.refreshLive(), 15000); + this._deepTimer = setInterval(() => this.scan({ silent: true }), 300000); if (typeof WebSocket === 'undefined') return; this._wsWant = true; @@ -623,6 +668,7 @@ export class Wallet { clearTimeout(this._wsRetry); clearTimeout(this._refreshTimer); clearInterval(this._pollTimer); + clearInterval(this._deepTimer); clearInterval(this._hbTimer); if (this._ws) { try { |
