aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-06-24 23:23:50 -0700
committerAdam Soltys <[email protected]>2026-06-24 23:23:50 -0700
commit7af3c430d49d7817e9a0cf28ab49143edba4c00f (patch)
tree33843b3dde033b0506db84d689906967ca9812b2 /src
parent4d1a9aea0f6b234dfe8eb36d398002ccea6a5680 (diff)
downloadcoinosv3-7af3c430d49d7817e9a0cf28ab49143edba4c00f.tar.gz
coinosv3-7af3c430d49d7817e9a0cf28ab49143edba4c00f.zip
Make pending receives fast: cut REST load, surface live/polling state
Incoming deposits show instantly when the mempool.space WebSocket is healthy (_ingestWs updates immediately). The lag came from the socket being starved: a 20s frontier poll plus a full wallet re-scan every 120s generated enough REST volume to trip mempool's per-IP rate limit (the 429/CORS errors), which in turn degraded the socket — a feedback loop. - While the socket is live, skip the periodic full scan entirely (the WS pushes confirmations and block events) and poll the frontier only every ~25s as a half-open-socket safety net — near-zero recurring REST. - While the socket is down, poll the frontier every ~6s (was 20s) so receives still show quickly, and keep the 120s reconcile scan. - Refresh the frontier on WS (re)connect to catch deposits that landed while disconnected. - Surface a Live/Polling badge on the balance card so a stuck socket is visible. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Diffstat (limited to 'src')
-rw-r--r--src/app.js9
-rw-r--r--src/i18n.js2
-rw-r--r--src/wallet.js31
3 files changed, 34 insertions, 8 deletions
diff --git a/src/app.js b/src/app.js
index 95a5625..86e62a0 100644
--- a/src/app.js
+++ b/src/app.js
@@ -1582,7 +1582,14 @@ function balanceCard() {
return h(
'div',
{ class: 'card balance' },
- h('div', { class: 'small faint', style: 'text-transform:uppercase;letter-spacing:.05em' }, t('balance')),
+ h('div', { class: 'row between', style: 'align-items:center' },
+ h('div', { class: 'small faint', style: 'text-transform:uppercase;letter-spacing:.05em' }, t('balance')),
+ // Live = receiving instant WebSocket pushes; otherwise we're polling, so
+ // deposits can lag a few seconds. Surfaced so a stuck socket is visible.
+ wallet.offline ? null
+ : h('span', { class: 'badge ' + (wallet.live ? 'live' : 'off') + ' dot', style: 'font-size:11px;padding:2px 8px' },
+ wallet.live ? t('liveTag') : t('pollingTag'))
+ ),
// Headline is the spendable balance: confirmed coins plus our own pending
// change, minus gift locks — so a pending spend debits immediately, while a
// pending incoming receive stays out of it until it confirms.
diff --git a/src/i18n.js b/src/i18n.js
index 28ee249..84ed882 100644
--- a/src/i18n.js
+++ b/src/i18n.js
@@ -174,6 +174,8 @@ const STR = {
received: 'Received',
sent: 'Sent',
pendingTag: '⏳ pending',
+ liveTag: 'Live',
+ pollingTag: 'Polling',
awaitingConfirmation: 'awaiting confirmation',
feeShort: 'fee {x}',
historyOffline: 'Transaction history is unavailable in offline mode.',
diff --git a/src/wallet.js b/src/wallet.js
index af827f3..f52efdf 100644
--- a/src/wallet.js
+++ b/src/wallet.js
@@ -92,6 +92,7 @@ export class Wallet {
this._pollTimer = null;
this._deepTimer = null; // periodic full-scan safety net
this._polling = false; // a light poll is in flight
+ this._lastPoll = 0; // last frontier poll (throttles the adaptive poll timer)
this._hbTimer = null; // heartbeat / liveness watchdog
this._lastMsg = 0; // time of last WS message (incl. pong)
this._wakeHooked = false;
@@ -1066,13 +1067,26 @@ export class Wallet {
startRealtime() {
if (this.offline) return;
this.stopRealtime();
- // Safety-net poll: only the fresh frontier (next receive/change address),
- // in case the WebSocket misses a deposit. Already-scanned addresses are
- // never re-polled — the deep scan below covers spends/confirmations.
- this._pollTimer = setInterval(() => this.refreshLive(), 20000);
- // Periodic full scan: reconciles everything refreshLive can't see on its own
- // — pending→confirmed history, spends, and stale cache/relay state.
- this._deepTimer = setInterval(() => this.scan({ silent: true }).catch(() => {}), 120000);
+ // Frontier poll (next receive/change address only). While the socket is live
+ // it pushes deposits instantly, so we poll just as a slow half-open-socket
+ // safety net (~25s). When the socket is down, poll fast (~6s) so receives
+ // still show quickly. The 3s tick is a cheap time check — it issues no
+ // request unless one is due, which keeps REST volume (and 429s) low.
+ this._pollTimer = setInterval(() => {
+ if (this.offline) return;
+ const due = this.live ? 25000 : 6000;
+ if (Date.now() - (this._lastPoll || 0) < due) return;
+ this._lastPoll = Date.now();
+ this.refreshLive();
+ }, 3000);
+ // Periodic full scan: reconciles pending→confirmed history, spends, and
+ // stale cache/relay state — but only when the socket is down. While live the
+ // WS already pushes confirmations and block events, so a full re-scan would
+ // just burn the rate limit (and risk starving the very socket we rely on).
+ this._deepTimer = setInterval(() => {
+ if (this.live) return;
+ this.scan({ silent: true }).catch(() => {});
+ }, 120000);
// Only mempool.space has a live socket; other explorers rely on the poll
// and deep scan above (wsUrl returns null → no socket).
if (typeof WebSocket === 'undefined' || !this.wsUrl()) return;
@@ -1136,6 +1150,9 @@ export class Wallet {
this._lastMsg = Date.now();
this.retrack();
this.emit();
+ // Catch any deposit that landed while we were disconnected — mempool only
+ // pushes new activity after tracking starts, not what's already there.
+ this.refreshLive().catch(() => {});
};
ws.onmessage = (ev) => {
this._lastMsg = Date.now();