Reported from the built site: leaving Home for /nodes and coming back makes the donation section sit on its loading state for several seconds before any number appears. Nothing is broken — it is a cold rescan every time.
What actually happens
fetch_donation_totals() runs from Home's componentDidMount, and Home is a class component, so donationsSettled resets to false on every remount. The scan behind it is not cheap:
|
|
t3YcVbiQ…u9Zjrr (current, since 12 Sep) |
pagesTotal: 0 — 1 request |
t1ebxupk…qwLmUG (OLD_ADDRESS_FLUX) |
pagesTotal: 18 — 18 requests |
| Per cold scan |
19 requests, issued sequentially |
scanDonationAddress walks the pages in a for loop with await inside, deliberately — it says so: "Sequentially rather than all at once, to stay under the explorer's limits." That is the right call, and it is also exactly why the wait is visible.
The only thing standing between a remount and those 19 requests is DONATION_SCAN_TTL_MS = 60 * 1000 (client/src/api/globalStats.js:284). _donationScanCache is a module-level variable — in-memory only. So:
- back on Home within 60s → instant
- back after 60s → full 19-request rescan
- any page reload → full rescan regardless
The 60s figure came from #314, where the problem being solved was three callers each running their own scan on one page load. Sharing fixed that. Surviving a remount was never in scope.
The explorer is tighter than the TTL assumes
While measuring this I hit HTTP 429 twice from roughly four manual requests spread over two minutes. Per explorer.js, a 429 reaches the browser as a CORS error, because a rate-limited response omits its CORS headers. So the current design is not only slow on return visits, it is spending its request budget re-fetching bytes it already had — and the failure mode when it runs out does not look like rate limiting.
Proposal
Three parts, smallest first:
1. Raise the in-memory TTL. 60s → 5 minutes, matching RAW_APP_SPECS_CACHE_TTL in api/specs.js. One line; covers the ordinary Home → Nodes → Home round trip outright.
2. Persist a trimmed scan to localStorage. Key donationScan_v1, same shape as homeAppSpecsRaw_v1 (api/specs.js) and donorStatus_v2 (donor/donorStatus.js) already use. Read it synchronously on first call so Home renders last-known values immediately, then revalidate in the background and swap when the fresh scan lands.
Store a trimmed tx, not the raw one — _trimSpecForCache in api/specs.js is the existing precedent. The three callers between them read only txid, time, blockheight, vin[].addr, and vout[].value / vout[].scriptPubKey.addresses. Dropping script hex should take ~180 txs from a few hundred KB to tens of KB. Measure this during implementation rather than trusting the estimate — if it does not comfortably fit, fall back to persisting only the derived totals and let fetch_wallet_donation_summary stay network-bound.
3. Say when the number is from. fluxinfo.js already carries a 'stale' status marker for its persisted aggregate; the donation panel should do the same rather than presenting a cached total as live.
One correctness trap
buildDonationRows(txs, { nowMs }) applies a rolling WINDOW_SEC cutoff. Caching the derived rows would freeze that window and quietly show donations that have since aged out. Cache the trimmed transactions and rebuild rows at read time with a fresh nowMs. The all-time total from aggregateDonations has no such window and is safe either way.
Also: scanBothDonationAddresses deliberately refuses to cache a scan where every address returned null, so a single 429 cannot become a minute of confident zeros. Persisting must keep that guard — a bad scan written to localStorage would become a confident wrong zero that survives reloads, which is strictly worse than the bug this fixes.
Acceptance
- Home → Nodes → Home renders donation figures with no loading state, from cache, before any network call resolves
- A hard reload renders last-known values immediately, then updates
- Cached values are visibly marked as such until the refresh lands
- A failed scan never overwrites good cached values
- Rows still respect the rolling window after a cache read
totalDonations.test.js stays green — note it already uses jest.isolateModules per test because of the existing module-level cache; persistence will need the same treatment plus a localStorage stub
Refs #314 (which introduced the shared scan and the 60s TTL), #258, #315.
Reported from the built site: leaving Home for
/nodesand coming back makes the donation section sit on its loading state for several seconds before any number appears. Nothing is broken — it is a cold rescan every time.What actually happens
fetch_donation_totals()runs from Home'scomponentDidMount, and Home is a class component, sodonationsSettledresets tofalseon every remount. The scan behind it is not cheap:t3YcVbiQ…u9Zjrr(current, since 12 Sep)pagesTotal: 0— 1 requestt1ebxupk…qwLmUG(OLD_ADDRESS_FLUX)pagesTotal: 18— 18 requestsscanDonationAddresswalks the pages in aforloop withawaitinside, deliberately — it says so: "Sequentially rather than all at once, to stay under the explorer's limits." That is the right call, and it is also exactly why the wait is visible.The only thing standing between a remount and those 19 requests is
DONATION_SCAN_TTL_MS = 60 * 1000(client/src/api/globalStats.js:284)._donationScanCacheis a module-level variable — in-memory only. So:The 60s figure came from #314, where the problem being solved was three callers each running their own scan on one page load. Sharing fixed that. Surviving a remount was never in scope.
The explorer is tighter than the TTL assumes
While measuring this I hit
HTTP 429twice from roughly four manual requests spread over two minutes. Perexplorer.js, a 429 reaches the browser as a CORS error, because a rate-limited response omits its CORS headers. So the current design is not only slow on return visits, it is spending its request budget re-fetching bytes it already had — and the failure mode when it runs out does not look like rate limiting.Proposal
Three parts, smallest first:
1. Raise the in-memory TTL. 60s → 5 minutes, matching
RAW_APP_SPECS_CACHE_TTLinapi/specs.js. One line; covers the ordinary Home → Nodes → Home round trip outright.2. Persist a trimmed scan to
localStorage. KeydonationScan_v1, same shape ashomeAppSpecsRaw_v1(api/specs.js) anddonorStatus_v2(donor/donorStatus.js) already use. Read it synchronously on first call so Home renders last-known values immediately, then revalidate in the background and swap when the fresh scan lands.Store a trimmed tx, not the raw one —
_trimSpecForCacheinapi/specs.jsis the existing precedent. The three callers between them read onlytxid,time,blockheight,vin[].addr, andvout[].value/vout[].scriptPubKey.addresses. Dropping script hex should take ~180 txs from a few hundred KB to tens of KB. Measure this during implementation rather than trusting the estimate — if it does not comfortably fit, fall back to persisting only the derived totals and letfetch_wallet_donation_summarystay network-bound.3. Say when the number is from.
fluxinfo.jsalready carries a'stale'status marker for its persisted aggregate; the donation panel should do the same rather than presenting a cached total as live.One correctness trap
buildDonationRows(txs, { nowMs })applies a rollingWINDOW_SECcutoff. Caching the derived rows would freeze that window and quietly show donations that have since aged out. Cache the trimmed transactions and rebuild rows at read time with a freshnowMs. The all-time total fromaggregateDonationshas no such window and is safe either way.Also:
scanBothDonationAddressesdeliberately refuses to cache a scan where every address returnednull, so a single 429 cannot become a minute of confident zeros. Persisting must keep that guard — a bad scan written tolocalStoragewould become a confident wrong zero that survives reloads, which is strictly worse than the bug this fixes.Acceptance
totalDonations.test.jsstays green — note it already usesjest.isolateModulesper test because of the existing module-level cache; persistence will need the same treatment plus alocalStoragestubRefs #314 (which introduced the shared scan and the 60s TTL), #258, #315.