Prune stale worktree virtual row cache (#6117)
Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
parent
bf2a6c1040
commit
092fd6cde3
|
|
@ -87,6 +87,7 @@ import {
|
|||
getActiveStickyIndexesForScroll,
|
||||
getStickyHeaderIndexes,
|
||||
getVirtualRowTransform,
|
||||
pruneStaleVirtualRowElementCache,
|
||||
shouldUseHeaderTopSpacing,
|
||||
type RenderRow
|
||||
} from './worktree-list-virtual-rows'
|
||||
|
|
@ -1941,6 +1942,7 @@ const VirtualizedWorktreeViewport = React.memo(function VirtualizedWorktreeViewp
|
|||
() => renderRows.map(getRenderRowKey).join('\n'),
|
||||
[renderRows]
|
||||
)
|
||||
const activeRenderRowKeys = useMemo(() => new Set(renderRows.map(getRenderRowKey)), [renderRows])
|
||||
const totalSize = virtualizer.getTotalSize()
|
||||
const virtualItems = virtualizer.getVirtualItems()
|
||||
const activeStickyIndexes = getActiveStickyIndexesForScroll({
|
||||
|
|
@ -1976,6 +1978,10 @@ const VirtualizedWorktreeViewport = React.memo(function VirtualizedWorktreeViewp
|
|||
)
|
||||
|
||||
useLayoutEffect(() => {
|
||||
pruneStaleVirtualRowElementCache({
|
||||
activeRowKeys: activeRenderRowKeys,
|
||||
virtualizer
|
||||
})
|
||||
// Why: after delete/collapse, TanStack may briefly retain the removed row's
|
||||
// cached element. Measuring that disconnected node reports 0px and corrupts
|
||||
// the next row's slot, so measure only elements whose DOM key still matches
|
||||
|
|
@ -1983,7 +1989,14 @@ const VirtualizedWorktreeViewport = React.memo(function VirtualizedWorktreeViewp
|
|||
measureMountedRows()
|
||||
const frameId = window.requestAnimationFrame(measureMountedRows)
|
||||
return () => window.cancelAnimationFrame(frameId)
|
||||
}, [prCacheLen, issueCacheLen, measureMountedRows, renderRowKeySignature])
|
||||
}, [
|
||||
activeRenderRowKeys,
|
||||
prCacheLen,
|
||||
issueCacheLen,
|
||||
measureMountedRows,
|
||||
renderRowKeySignature,
|
||||
virtualizer
|
||||
])
|
||||
|
||||
useVirtualizedScrollAnchor({
|
||||
anchorRef: scrollAnchorRef,
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import {
|
|||
extractWorktreeVirtualRowIndexes,
|
||||
getActiveStickyIndexesForScroll,
|
||||
getStickyHeaderIndexes,
|
||||
pruneStaleVirtualRowElementCache,
|
||||
type RenderRow
|
||||
} from './worktree-list-virtual-rows'
|
||||
|
||||
|
|
@ -144,3 +145,49 @@ describe('extractWorktreeVirtualRowIndexes', () => {
|
|||
expect(indexes).toContain(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('pruneStaleVirtualRowElementCache', () => {
|
||||
it('removes stale measured row elements before they retain old WorktreeCard scopes', () => {
|
||||
const activeElement = {
|
||||
isConnected: true,
|
||||
getAttribute: (name: string) =>
|
||||
name === 'data-worktree-virtual-row-key' ? 'wt:active' : null
|
||||
} as Element
|
||||
const staleElement = {
|
||||
isConnected: false,
|
||||
getAttribute: (name: string) => (name === 'data-worktree-virtual-row-key' ? 'wt:stale' : null)
|
||||
} as Element
|
||||
const connectedStaleElement = {
|
||||
isConnected: true,
|
||||
getAttribute: (name: string) =>
|
||||
name === 'data-worktree-virtual-row-key' ? 'wt:connected-stale' : null
|
||||
} as Element
|
||||
const retainedScope = {
|
||||
defaultHostId: 'runtime:env-1',
|
||||
handlerName: 'handleOpenReviewInOrca'
|
||||
}
|
||||
Object.assign(staleElement, { __retainedWorktreeCardScopeForTest: retainedScope })
|
||||
|
||||
const virtualizer = {
|
||||
elementsCache: new Map<string, Element>([
|
||||
['wt:active', activeElement],
|
||||
['wt:stale', staleElement],
|
||||
['wt:connected-stale', connectedStaleElement]
|
||||
]),
|
||||
measureElement: (element: Element | null) => {
|
||||
if (element) {
|
||||
throw new Error('stale cache pruning should not remeasure rows')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pruneStaleVirtualRowElementCache({
|
||||
activeRowKeys: new Set(['wt:active']),
|
||||
virtualizer
|
||||
})
|
||||
|
||||
expect(virtualizer.elementsCache.get('wt:active')).toBe(activeElement)
|
||||
expect(virtualizer.elementsCache.has('wt:stale')).toBe(false)
|
||||
expect(virtualizer.elementsCache.get('wt:connected-stale')).toBe(connectedStaleElement)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -76,6 +76,31 @@ export function getVirtualRowTransform(start: number): string {
|
|||
return `translateY(${start}px)`
|
||||
}
|
||||
|
||||
type VirtualRowElementCache<TElement extends Element> = {
|
||||
elementsCache: Map<unknown, TElement>
|
||||
measureElement: (node: TElement | null) => void
|
||||
}
|
||||
|
||||
export function pruneStaleVirtualRowElementCache<TElement extends Element>({
|
||||
activeRowKeys,
|
||||
virtualizer
|
||||
}: {
|
||||
activeRowKeys: ReadonlySet<string>
|
||||
virtualizer: VirtualRowElementCache<TElement>
|
||||
}): void {
|
||||
virtualizer.measureElement(null)
|
||||
for (const [key, element] of virtualizer.elementsCache) {
|
||||
const rowKey = String(key)
|
||||
if (activeRowKeys.has(rowKey) || element.isConnected) {
|
||||
continue
|
||||
}
|
||||
// Why: measured row nodes retain their React fiber tree. Once TanStack's
|
||||
// public null-measure cleanup has run, drop any disconnected stale key left
|
||||
// behind so old WorktreeCard scopes do not survive runtime-host row churn.
|
||||
virtualizer.elementsCache.delete(key)
|
||||
}
|
||||
}
|
||||
|
||||
export function getStickyHeaderIndexes(rows: readonly RenderRow[]): number[] {
|
||||
const indexes: number[] = []
|
||||
rows.forEach((row, index) => {
|
||||
|
|
|
|||
Loading…
Reference in New Issue