mirror of
https://github.com/stablyai/orca.git
synced 2026-10-30 16:47:34 +00:00
perf(quick-open): preserve scans across worktree metadata updates (#34838)
* perf(quick-open): preserve scans across worktree metadata updates * test(quick-open): collapse duplicate metadata-update case * fix(quick-open): key exclude request by state instead of render-time ref React Doctor rejects mutating a ref during render. Hold the nested-worktree exclude request in state and replace it only when its key changes, so metadata-only worktree updates keep the same object identity. --------- Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,79 @@
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import { act } from 'react'
|
||||
import { expect, it, vi } from 'vitest'
|
||||
import { useAppStore } from '@/store'
|
||||
import type { Worktree } from '../../../shared/worktree/types'
|
||||
import type { RuntimeFileListState } from './quick-open-file-list'
|
||||
import {
|
||||
cancelRuntimeFileListMock,
|
||||
flushEffects,
|
||||
listRuntimeFilesMock,
|
||||
makeRemoteWorktree,
|
||||
renderProbe,
|
||||
searchRuntimeFilePathsMock,
|
||||
seedRemoteWorktree
|
||||
} from './quick-open-file-list-test-harness'
|
||||
|
||||
vi.mock('@/runtime/runtime-file-search-client', async () => {
|
||||
const mocks = await import('./__mocks__/quick-open-runtime-file-client')
|
||||
return {
|
||||
listRuntimeFiles: mocks.listRuntimeFilesMock,
|
||||
cancelRuntimeFileList: mocks.cancelRuntimeFileListMock,
|
||||
searchRuntimeFilePaths: mocks.searchRuntimeFilePathsMock
|
||||
}
|
||||
})
|
||||
|
||||
async function replaceWorktrees(worktrees: Worktree[]): Promise<void> {
|
||||
await act(async () => {
|
||||
useAppStore.setState({ worktreesByRepo: { 'repo-remote': worktrees } })
|
||||
})
|
||||
await flushEffects()
|
||||
}
|
||||
|
||||
it('keeps a settled inventory through metadata updates', async () => {
|
||||
seedRemoteWorktree()
|
||||
const states: RuntimeFileListState[] = []
|
||||
await renderProbe({ enabled: true, states, worktreeId: 'wt-remote' })
|
||||
const settledFiles = states.at(-1)?.files
|
||||
const initialRequest = listRuntimeFilesMock.mock.calls[0]?.[1]
|
||||
const rendersBeforeUpdates = states.length
|
||||
|
||||
await replaceWorktrees([{ ...makeRemoteWorktree(), head: 'commit-1' }])
|
||||
await replaceWorktrees([{ ...makeRemoteWorktree(), head: 'commit-2' }])
|
||||
|
||||
expect(listRuntimeFilesMock).toHaveBeenCalledOnce()
|
||||
expect(cancelRuntimeFileListMock).not.toHaveBeenCalled()
|
||||
expect(initialRequest?.signal?.aborted).toBe(false)
|
||||
expect(states.slice(rendersBeforeUpdates).every((state) => !state.loading)).toBe(true)
|
||||
expect(states.at(-1)?.files).toBe(settledFiles)
|
||||
})
|
||||
|
||||
it('keeps an in-flight query through metadata updates but replaces it for changed exclusions', async () => {
|
||||
vi.useFakeTimers()
|
||||
seedRemoteWorktree()
|
||||
searchRuntimeFilePathsMock.mockReturnValue(new Promise(() => {}))
|
||||
try {
|
||||
await renderProbe({ enabled: true, states: [], query: 'target', worktreeId: 'wt-remote' })
|
||||
await act(async () => vi.advanceTimersByTimeAsync(120))
|
||||
const initialRequest = searchRuntimeFilePathsMock.mock.calls[0]?.[1]
|
||||
await replaceWorktrees([{ ...makeRemoteWorktree(), comment: 'Updated comment' }])
|
||||
await act(async () => vi.advanceTimersByTimeAsync(120))
|
||||
expect(initialRequest?.signal?.aborted).toBe(false)
|
||||
expect(searchRuntimeFilePathsMock).toHaveBeenCalledOnce()
|
||||
|
||||
const nested = { ...makeRemoteWorktree(), id: 'nested', path: '/srv/remote/nested' }
|
||||
await replaceWorktrees([makeRemoteWorktree(), nested])
|
||||
expect(initialRequest?.signal?.aborted).toBe(true)
|
||||
await act(async () => vi.advanceTimersByTimeAsync(120))
|
||||
expect(searchRuntimeFilePathsMock).toHaveBeenCalledTimes(2)
|
||||
expect(searchRuntimeFilePathsMock.mock.calls[1]?.[1].excludePaths).toEqual([nested.path])
|
||||
|
||||
await replaceWorktrees([makeRemoteWorktree()])
|
||||
await act(async () => vi.advanceTimersByTimeAsync(120))
|
||||
expect(searchRuntimeFilePathsMock).toHaveBeenCalledTimes(3)
|
||||
expect(searchRuntimeFilePathsMock.mock.calls[2]?.[1].excludePaths).toBeUndefined()
|
||||
} finally {
|
||||
vi.useRealTimers()
|
||||
}
|
||||
})
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useState } from 'react'
|
||||
import type { Worktree } from '../../../shared/worktree/types'
|
||||
import { getNestedWorktreeExcludePaths } from './quick-open-nested-worktrees'
|
||||
|
||||
@@ -44,3 +45,14 @@ export function getNestedWorktreeExcludeRequest(
|
||||
// JSON preserves newline-containing paths in stable dependency keys; pass the original path array to IPC.
|
||||
return { paths, key: JSON.stringify(paths) }
|
||||
}
|
||||
|
||||
// Why: worktree metadata updates rebuild the request object; only a key change should restart scans.
|
||||
export function useKeyedExcludeRequest(
|
||||
request: NestedWorktreeExcludeRequest
|
||||
): NestedWorktreeExcludeRequest {
|
||||
const [keyed, setKeyed] = useState(request)
|
||||
if (keyed.key !== request.key) {
|
||||
setKeyed(request)
|
||||
}
|
||||
return keyed.key === request.key ? keyed : request
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { getRuntimeFileListTarget } from './quick-open-file-list-target'
|
||||
import { getRuntimeFileListTarget, useKeyedExcludeRequest } from './quick-open-file-list-target'
|
||||
export {
|
||||
getRuntimeFileListTarget,
|
||||
getNestedWorktreeExcludeRequest
|
||||
@@ -93,7 +93,7 @@ export function useRuntimeFileListForWorktree({
|
||||
() => getRuntimeFileListTarget(worktreeId, worktreePath, repoWorktrees),
|
||||
[repoWorktrees, worktreeId, worktreePath]
|
||||
)
|
||||
const { excludeRequest } = target
|
||||
const excludeRequest = useKeyedExcludeRequest(target.excludeRequest)
|
||||
|
||||
const operationOwnerState = useAppStore(
|
||||
useShallow((state) => ({
|
||||
|
||||
Reference in New Issue
Block a user