fix(diff): drop deferred reload records when the entry set is rebuilt

A refusal recorded against the old entry set outlived a tree/commit mode switch, charging the
next save of that path a git diff the freshly loaded row did not need. Also mark the load
registry live during render rather than in its effect: child effects run first, so StrictMode's
replayed mount briefly read the flag as false while the viewer was live.
This commit is contained in:
Neil
2026-09-10 20:56:33 -07:00
parent a117622c27
commit 86258228cc
3 changed files with 38 additions and 1 deletions
@@ -59,13 +59,15 @@ export function useCombinedDiffSectionLoadRegistry(
loadSchedulerRef.current ??= createCombinedDiffLoadScheduler({
loadSection: (index) => loadSectionRef.current(index)
})
// Why here and not in the effect below: child effects run before this parent's, so StrictMode's
// replayed mount would leave a window where the ref reads false while the viewer is live.
registryLiveRef.current = true
useEffect(() => {
// Why: React StrictMode replays effect cleanup in dev; reset revives the scheduler for the replayed mount.
const scheduler = loadSchedulerRef.current
const reloadTimers = reloadTimersRef.current
scheduler.reset()
registryLiveRef.current = true
return () => {
registryLiveRef.current = false
clearPendingSectionReloadTimers(reloadTimers)
@@ -37,6 +37,10 @@ function buildAllModeEntrySet(
}
}
function statusEntry(path: string): GitStatusEntry {
return { path, status: 'modified', area: 'unstaged', added: 1 }
}
function restoreSections(entrySet: CombinedDiffEntrySet, viewStateKey: string): DiffSection[] {
let sections: DiffSection[] = []
renderHook(() => {
@@ -148,3 +152,29 @@ describe('useCombinedDiffViewRestore deferral', () => {
expect(sections.map((section) => section.loadOnDemand)).toEqual([false, false])
})
})
it('clears deferred reload records when the entry set is rebuilt', () => {
// Why: the rebuilt rows load fresh, so a refusal recorded against the old set would charge the
// next save of that path a git diff it no longer needs.
let keys: Set<string> | undefined
const view = renderHook(
({ entrySet }: { entrySet: CombinedDiffEntrySet }) => {
const registry = useCombinedDiffSectionLoadRegistry(useRef([]))
keys = registry.deferredReloadKeysRef.current
useCombinedDiffViewRestore({
entrySet,
gitStatusEntries: [],
registry,
setGeneration: () => {},
setSectionHeights: () => {},
setSections: () => {},
setSideBySide: () => {},
viewStateKey: 'rebuild'
})
},
{ initialProps: { entrySet: buildAllModeEntrySet([statusEntry('a.ts')], []) } }
)
keys?.add('a.ts')
view.rerender({ entrySet: buildAllModeEntrySet([statusEntry('a.ts'), statusEntry('b.ts')], []) })
expect([...(keys ?? [])]).toEqual([])
})
@@ -57,6 +57,7 @@ export function useCombinedDiffViewRestore({
} = entrySet
const {
generationRef,
deferredReloadKeysRef,
deferredLoadRequestsRef,
loadSchedulerRef,
loadedIndicesRef,
@@ -180,6 +181,9 @@ export function useCombinedDiffViewRestore({
loadedIndicesRef.current.clear()
loadingIndicesRef.current.clear()
sectionLoadTokensRef.current.clear()
// Why: the rebuilt rows load fresh, so a refusal recorded against the old set would charge the
// next save of that path a git diff it no longer needs.
deferredReloadKeysRef.current.clear()
clearPendingSectionReloadTimers(reloadTimersRef.current)
loadSchedulerRef.current.reset()
generationRef.current += 1
@@ -188,6 +192,7 @@ export function useCombinedDiffViewRestore({
entries,
entrySignature,
generationRef,
deferredReloadKeysRef,
deferredLoadRequestsRef,
gitStatusEntries,
hasUncommittedEntriesSnapshot,