Fix wallet connect performance regression with bounded address data fetching#757
Conversation
The fresh-connect path (getWalletDataProgressive) fetched per-address data inside discovery's onAddressFound callback, which was invoked without await. This fired 3 Esplora calls per address with unbounded concurrency, flooding the provider (429/5xx -> retry-backoff) and racing the snapshot build so the last-discovered addresses' data could be dropped. For wallets with many addresses this stretched the connect->dashboard flow into minutes. Changes: - Extract the bounded worker-queue (concurrency 6) from fetchWalletSnapshot into a shared fetchAddressDataConcurrent helper; refactor fetchWalletSnapshot to use it. - Progressive discovery now only COLLECTS addresses (plus the /address stats already fetched during discovery); heavy /txs + /utxo fetching runs afterwards via the bounded pool and is awaited before building the snapshot, removing both the flood and the race. - Reuse discovery stats as the address `info`, skipping a redundant /address call per address. - Restore live progress: emit discovery-phase counts (skeleton keeps showing "Found N addresses") and stream real data during the fetch phase. Guard the wallet-context handler so empty discovery-phase updates don't clobber the loading skeleton. - Wrap progressive discovery in the 4-minute discovery timeout for parity with the cached path. - Bump PARALLEL_BATCH_SIZE to 20 so a full gap window resolves in one parallel round-trip. Adds behavioral tests for the bounded fetcher (concurrency cap, race fix, stats reuse, utxo retry) and source-level regression guards. https://claude.ai/code/session_013r8mPRVH2dz81nP9VBm8uC
mock.calls is typed any[][]; the destructured [path]: [string] parameter annotation didn't satisfy the filter overload, failing the CI typecheck step. Index the call args directly instead. https://claude.ai/code/session_013r8mPRVH2dz81nP9VBm8uC
|
@claude can you code review the pr 757 and make any changes required |
Code review of the progressive-connect change surfaced three issues: 1. (Bug) Discovery fires a final onBatchComplete with isComplete:true when address discovery ends - but the heavy per-address fetch still follows. emitDiscoveryProgress forwarded that as an empty-data isComplete:true update, so the dashboard dropped its loading state and showed a zero-balance wallet (clobbering any cached snapshot already on screen) before real data arrived. Discovery-phase emits now force isComplete:false; only the genuine final update completes. 2. (Hardening) Added a receivedComplete latch in the wallet-context onProgress handler so a late or out-of-order partial emit can't revert a finished dashboard back to a partial view. 3. (Efficiency) Hoisted `new Set(usedAddresses)` out of the per-tx map in buildPartialWalletData; it was rebuilt for every transaction, and that function now runs repeatedly as data streams in during connect. Adds a regression guard that discovery-phase emits never report isComplete:true. https://claude.ai/code/session_013r8mPRVH2dz81nP9VBm8uC
|
Ran a code review of the PR. Found one real bug it introduced plus a couple of hardening/perf items — all fixed in 1. Premature "complete" with empty data (bug). Address discovery fires a final 2. Late-emit hardening. Added a 3. Efficiency. Hoisted Added a regression guard that discovery-phase emits never report Lower-severity items I considered but left as-is: no streaming feedback for wallets with <5 addresses (the final emit covers it; those load fast), and the pre-existing Generated by Claude Code |
Summary
Fixes a critical performance regression in the progressive wallet connect flow where per-address data fetches (txs, utxos, address stats) were firing with unbounded concurrency, flooding the Esplora provider with 429/5xx errors and triggering retry-backoff that stretched wallet loading into minutes. Additionally, the unbounded fetches raced the snapshot build, causing data loss for the last-discovered addresses.
Root cause: The
onAddressFoundcallback ingetWalletDataProgressivewas immediately fetching per-address data for each discovered address without concurrency limits, and these fetches were not awaited before the snapshot build completed.Solution:
fetchAddressDataConcurrent()— a shared bounded worker pool (default 6 concurrent) used by both the cached and progressive paths/addressstats already fetched during discovery to avoid redundant API callsType of Change
Changes
Core Fixes
fetchAddressDataConcurrent()(new): Bounded worker pool for fetching txs/utxos/address stats with configurable concurrency. Reuses cached/addressresponses from discovery to skip redundant API calls.getWalletDataProgressive(): Refactored to separate discovery (collect addresses) from data fetching (bounded pool). Discovery now wrapped in timeout for parity with cached path. Partial wallet data streamed to UI during fetch phase.onAddressFoundcallback: Now receivesstatsparameter (the/addressresponse already fetched during discovery) so callers can reuse it instead of re-fetching.fetchWalletSnapshot(): Updated to use the sharedfetchAddressDataConcurrent()helper.Constants
PARALLEL_BATCH_SIZE: Increased from 10 to 20 (matchesGAP_LIMITso a full gap window resolves in one round-trip)ADDRESS_DATA_CONCURRENCY: New constant (6) for bounded per-address data fetchingUI Improvements
wallet-context.tsx: Only update wallet data when there's content to show (discovery phase emits progress-only updates; dashboard keeps skeleton until data arrives or discovery completes)Test Plan
address-data-concurrency.test.ts): Validates bounded concurrency, data completeness (no dropped addresses even when slow), stats reuse, and UTXO retry logicaddress-discovery-unit.test.ts): Ensures both paths use the shared fetcher, discovery callback doesn't fetch per-address data, and progressive discovery is wrapped in timeoutvalidate-initial-check-limit.test.ts): Updated to reflectPARALLEL_BATCH_SIZE = 20Checklist
npm run typecheckpassesnpm run lintpassesnpm run testpasses (new tests added)https://claude.ai/code/session_013r8mPRVH2dz81nP9VBm8uC