Skip to content

opt(cardinal): Resolve flicker when search results updated#175

Open
ldm0 wants to merge 1 commit intomasterfrom
avoid_flicker
Open

opt(cardinal): Resolve flicker when search results updated#175
ldm0 wants to merge 1 commit intomasterfrom
avoid_flicker

Conversation

@ldm0
Copy link
Copy Markdown
Member

@ldm0 ldm0 commented Apr 7, 2026

No description provided.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR aims to eliminate UI flicker when file search results update by preventing stale, in-flight node info responses from mutating the current cache, and by reusing already-hydrated row data across backend result-set updates.

Changes:

  • Introduces a monotonic “request epoch” in useDataLoader to drop late get_nodes_info responses after a result-set change.
  • Stops clearing the row cache on dataResultsVersion changes to reuse previously hydrated rows and reduce visible blank/loading placeholders.
  • Updates and expands useDataLoader tests to cover cache reuse and stale-response suppression.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
cardinal/src/hooks/useDataLoader.ts Adds request-epoch guarding and changes cache lifecycle behavior across result-set updates.
cardinal/src/hooks/tests/useDataLoader.test.ts Updates tests to validate cache reuse and ignoring stale responses.
cardinal/src/components/VirtualList.tsx Updates prop comment to reflect new epoch behavior.
cardinal/src/components/FilesTabContent.tsx Updates prop comment to reflect stale-fetch rejection behavior.
cardinal/src/App.tsx Updates inline documentation for dataResultsVersion semantics.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +36 to 41
// cannot write into the cache for a newer query. Cached rows stay reusable by slab index.
useEffect(() => {
versionRef.current += 1;
// Clear "currently loading" bookkeeping so the next visible range can request any slab
// indices that were pending for the previous result-set but never resolved into this one.
loadingRef.current.clear();
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

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

On dataResultsVersion change you no longer reset/prune cacheRef/iconOverridesRef, which means entries can accumulate indefinitely (memory growth across searches). More importantly, this assumes a SlabIndex always refers to the same node across result-set versions; however the backend uses a slab with a freelist and removes nodes, so indices can be recycled. If a SlabIndex is reused for a different path after a rescan/deletion, ensureRangeLoaded will treat it as a cache hit and never refetch, potentially rendering the wrong row data. Consider either (a) keeping the epoch-based stale-response guard but pruning/clearing cache and icon overrides on dataResultsVersion, or (b) tracking per-entry generation/epoch so cached items from prior result-sets are not reused without a refresh.

Suggested change
// cannot write into the cache for a newer query. Cached rows stay reusable by slab index.
useEffect(() => {
versionRef.current += 1;
// Clear "currently loading" bookkeeping so the next visible range can request any slab
// indices that were pending for the previous result-set but never resolved into this one.
loadingRef.current.clear();
// cannot write into the cache for a newer query. Because slab indices can be recycled
// by the backend across result-set versions, clear all slab-index keyed caches here.
useEffect(() => {
versionRef.current += 1;
// Clear "currently loading" bookkeeping so the next visible range can request any slab
// indices that were pending for the previous result-set but never resolved into this one.
loadingRef.current.clear();
const nextCache = new Map<SlabIndex, SearchResultItem>();
cacheRef.current = nextCache;
iconOverridesRef.current = new Map<SlabIndex, IconOverrideValue>();
setCache(nextCache);

Copilot uses AI. Check for mistakes.
Comment on lines 100 to 107
if (needLoading.length === 0) return;
const versionAtRequest = versionRef.current;
const fetched = await invoke<NodeInfoResponse[]>('get_nodes_info', { results: needLoading });
if (versionRef.current !== versionAtRequest) {
// The result-set changed while this request was in flight. Drop the payload instead of
// merging stale rows into the cache for the new query.
releaseLoadingBatch(needLoading);
return;
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

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

invoke('get_nodes_info', ...) is awaited without a try/catch. If the invoke rejects (backend error, IPC failure, etc.), the loadingRef entries for needLoading will never be released, so those rows can get stuck permanently as "loading" (and never be requested again until dataResultsVersion changes and clears the set). Wrap the invoke in a try/catch/finally and ensure releaseLoadingBatch(needLoading) runs on the error path.

Copilot uses AI. Check for mistakes.
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