Skip to content

feat: cache merged home page data for instant startup display#10070

Draft
huhuanming wants to merge 3 commits intoxfrom
feat/home-page-cache-instant-startup
Draft

feat: cache merged home page data for instant startup display#10070
huhuanming wants to merge 3 commits intoxfrom
feat/home-page-cache-instant-startup

Conversation

@huhuanming
Copy link
Copy Markdown
Contributor

@huhuanming huhuanming commented Feb 9, 2026

Summary

  • Add a new SimpleDbEntityHomePageData entity to cache the final merged home page state (balance + token list) per account+network
  • On cold start, load the single cached snapshot instead of N per-network reads + merge, eliminating skeleton/loading flash
  • Save cache (debounced 3s) after every successful data load for next startup

Changes

  • New: packages/kit-bg/src/dbs/simple/entity/SimpleDbEntityHomePageData.ts — SimpleDB entity for home page cache
  • Modified: SimpleDb.ts / SimpleDbProxy.ts — Register new entity
  • Modified: ServiceToken.ts — Add getHomePageCache / saveHomePageCache methods
  • Modified: TokenListBlock.tsx — Load cache on init to skip skeleton; save cache after data updates
  • Modified: HomeOverviewContainer.tsx — Load cached accountWorth to avoid $0 flash

Test plan

  • Cold start with cache: balance and token list show immediately (no skeleton)
  • Cold start without cache (first time): skeleton shows as before
  • Background refresh updates data after cached display
  • Switch accounts: correct cached data for each account
  • Single network mode: existing cache flow unaffected

Open with Devin

Cache the final merged token list and balance on every successful load,
so the next cold start can show data immediately without skeleton/loading
state. Uses a new SimpleDB entity (homePageData) for single-read access
instead of N per-network reads + merge.
@huhuanming huhuanming enabled auto-merge (squash) February 9, 2026 06:56
@revan-zhang
Copy link
Copy Markdown
Contributor

revan-zhang commented Feb 9, 2026

Snyk checks have passed. No issues have been found so far.

Status Scanner Critical High Medium Low Total (0)
Open Source Security 0 0 0 0 0 issues
Licenses 0 0 0 0 0 issues

💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse.

@huhuanming huhuanming marked this pull request as draft February 9, 2026 06:58
auto-merge was automatically disabled February 9, 2026 06:58

Pull request was converted to draft

Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 2 potential issues.

View 6 additional findings in Devin Review.

Open in Devin Review

Comment on lines +98 to +117
void (async () => {
const cache = await backgroundApiProxy.serviceToken.getHomePageCache({
accountId: account.id,
networkId: network.id,
});
if (cache) {
updateAccountWorth({
accountId: cache.accountWorth.accountId,
worth: cache.accountWorth.worth,
createAtNetworkWorth: cache.accountWorth.createAtNetworkWorth,
initialized: true,
});
} else {
updateAccountWorth({
accountId: account.id,
worth: {},
initialized: false,
});
}
})();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Race condition: unmanaged async cache fetch can apply stale account data after account switch

The useEffect at lines 98-117 fires an async IIFE (void (async () => {...})()) to fetch the home page cache, but provides no cleanup or cancellation mechanism. When the user switches accounts, the previous async getHomePageCache call may resolve after the new effect has already set state for the new account.

Root Cause & Scenario
  1. User is on Account A. Effect fires, starts getHomePageCache({accountId: A, ...}).
  2. User switches to Account B. Effect fires again, starts getHomePageCache({accountId: B, ...}).
  3. If B has no cache → synchronous path immediately sets updateAccountWorth({accountId: B, worth: {}, initialized: false}).
  4. A's cache resolves after B's → sets updateAccountWorth({accountId: A, worth: {A's data}, initialized: true}).
  5. Now the active account is B, but accountWorth holds A's data with initialized: true.

The display code at HomeOverviewContainer.tsx:206-208 checks account.id === accountWorth.accountId, so A's balance won't render for B. However, accountWorth.initialized is now true, which may prevent the skeleton from appearing — the user sees $0 or empty state for Account B until real data arrives, exactly the "$0 flash" this PR aimed to prevent.

A standard fix is to use a boolean ref (let cancelled = false) checked before calling updateAccountWorth, with the cleanup function setting cancelled = true.

Impact: On fast account switching, the cached worth from the previous account can corrupt the initialization state of the new account, causing a brief incorrect display.

Suggested change
void (async () => {
const cache = await backgroundApiProxy.serviceToken.getHomePageCache({
accountId: account.id,
networkId: network.id,
});
if (cache) {
updateAccountWorth({
accountId: cache.accountWorth.accountId,
worth: cache.accountWorth.worth,
createAtNetworkWorth: cache.accountWorth.createAtNetworkWorth,
initialized: true,
});
} else {
updateAccountWorth({
accountId: account.id,
worth: {},
initialized: false,
});
}
})();
void (async () => {
let cancelled = false;
cleanupRef.current = () => { cancelled = true; };
const cache = await backgroundApiProxy.serviceToken.getHomePageCache({
accountId: account.id,
networkId: network.id,
});
if (cancelled) return;
if (cache) {
updateAccountWorth({
accountId: cache.accountWorth.accountId,
worth: cache.accountWorth.worth,
createAtNetworkWorth: cache.accountWorth.createAtNetworkWorth,
initialized: true,
});
} else {
updateAccountWorth({
accountId: account.id,
worth: {},
initialized: false,
});
}
})();
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +1130 to +1151
_saveHomePageCacheDebounced = debounce(
async ({
accountId,
networkId,
data,
}: {
accountId: string;
networkId: string;
data: IHomePageCacheItem;
}) => {
await this.backgroundApi.simpleDb.homePageData.setCache({
accountId,
networkId,
data,
});
},
3000,
{
leading: false,
trailing: true,
},
);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Shared debounce drops cache saves for rapidly-switched accounts

The _saveHomePageCacheDebounced function in ServiceToken.ts is a single lodash debounce instance shared across all accounts. When saveHomePageCache is called for Account A, and then called again for Account B within the 3-second debounce window, lodash replaces the pending arguments entirely — only Account B's data is persisted, and Account A's cache update is silently dropped.

Root Cause & Scenario

Lodash debounce with trailing: true keeps only the most recent invocation's arguments. The flow:

  1. handleAllNetworkCacheData fires for Account A → calls saveHomePageCache({accountId: A, networkId: 'all', data: dataA})
  2. User switches to Account B within 3 seconds
  3. handleAllNetworkCacheData fires for Account B → calls saveHomePageCache({accountId: B, networkId: 'all', data: dataB})
  4. After 3s, only {accountId: B, ...} fires on the trailing edge
  5. Account A's cache is never written

On next cold start for Account A, either stale or no cached data is displayed, forcing the user through the loading skeleton — defeating the purpose of this caching feature for that account.

Impact: Accounts that are quickly switched away from never get their home page cache updated. The instant startup optimization is unreliable for non-primary accounts.

Prompt for agents
Replace the single shared debounce with a per-key debounce approach. Instead of one _saveHomePageCacheDebounced function, maintain a Map<string, DebouncedFunction> keyed by accountId_networkId. In saveHomePageCache, look up or create a debounced function for the specific key, ensuring each account+network pair gets its own independent debounce timer. This ensures that switching accounts does not drop the pending save for the previous account. Remember to clean up old debounced functions if the map grows too large (e.g., evict entries that haven't been used recently).
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

- Create homeHeader context (atoms + actions) for pre-computed balance display
- Add homeTokenListDataAtom to tokenList context for flat data mapping
- Add buildHomePageData/saveHomePageCacheV2 to ServiceToken
- Create useHomePageDataController hook for cache orchestration
- Remove all V1 cache code (IHomePageCacheItem, getCache, setCache, saveHomePageCache)
- Fix balance showing '0' by showing skeleton until stores are initialized
- HomeOverviewContainer reads from homeHeaderDataAtom with legacy fallback
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants