mirror of
https://github.com/stablyai/orca.git
synced 2026-10-30 16:47:34 +00:00
fix(editor): drop per-tab probe and disk-check generations once superseded or closed (#31128)
Follow-up to #30415. A watch event that superseded a pending overflow existence probe left the tab's entry behind forever, and the dirty-tab disk-check request map never removed anything. Superseding now deletes the entry, and closed tabs' disk-check requests are dropped by the scan's existing open-tabs subscription. The disk-check map uses a global sequence so a reopened tab can't reuse an old read's number.
This commit is contained in:
@@ -23,6 +23,7 @@ vi.mock('@/lib/file-owner-runtime-target', () => {
|
||||
import { createTestStore } from '@/store/slices/store-test-helpers'
|
||||
import {
|
||||
attachRestoredTabConflictScan,
|
||||
getRestoredTabReverificationRequestCountForTest,
|
||||
MAX_VERIFY_ATTEMPTS,
|
||||
requestRestoredTabReverification
|
||||
} from './editor-restored-tab-conflict-scan'
|
||||
@@ -129,3 +130,13 @@ it('does not let an absence answer from before a newer check resolve that check'
|
||||
expect(tab().externalMutation).toBe('changed')
|
||||
expect(tab().pendingDiskBaselineVerification).toBeFalsy()
|
||||
})
|
||||
|
||||
it("forgets a tab's check requests once the tab closes", async () => {
|
||||
mocks.readRuntimeFileContent.mockResolvedValue({ content: 'v1', isBinary: false })
|
||||
detach = attachRestoredTabConflictScan(store)
|
||||
requestRestoredTabReverification(FILE)
|
||||
expect(getRestoredTabReverificationRequestCountForTest()).toBeGreaterThan(0)
|
||||
|
||||
store.getState().closeFile(FILE)
|
||||
expect(getRestoredTabReverificationRequestCountForTest()).toBe(0)
|
||||
})
|
||||
|
||||
@@ -26,14 +26,30 @@ const MAX_CONCURRENT_VERIFY_READS = 3
|
||||
export const MAX_VERIFY_ATTEMPTS = VERIFY_FAST_ATTEMPTS + 10
|
||||
|
||||
// Why: a disk check requested while one is in flight must not be answered by the older read.
|
||||
// A global sequence, so dropping a closed tab's entry can't let a reopened tab's request reuse an old read's number.
|
||||
const verifyRequestGenerationByFileId = new Map<string, number>()
|
||||
let verifyRequestGenerationSeq = 0
|
||||
|
||||
/** Asks for a fresh disk check of a tab; a read already in flight is followed by one more. */
|
||||
export function requestRestoredTabReverification(fileId: string): void {
|
||||
verifyRequestGenerationByFileId.set(
|
||||
fileId,
|
||||
(verifyRequestGenerationByFileId.get(fileId) ?? 0) + 1
|
||||
)
|
||||
verifyRequestGenerationSeq += 1
|
||||
verifyRequestGenerationByFileId.set(fileId, verifyRequestGenerationSeq)
|
||||
}
|
||||
|
||||
export function getRestoredTabReverificationRequestCountForTest(): number {
|
||||
return verifyRequestGenerationByFileId.size
|
||||
}
|
||||
|
||||
function forgetClosedTabReverificationRequests(openFiles: readonly OpenFile[]): void {
|
||||
if (verifyRequestGenerationByFileId.size === 0) {
|
||||
return
|
||||
}
|
||||
const openIds = new Set(openFiles.map((file) => file.id))
|
||||
for (const fileId of verifyRequestGenerationByFileId.keys()) {
|
||||
if (!openIds.has(fileId)) {
|
||||
verifyRequestGenerationByFileId.delete(fileId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function verifyRequestGeneration(fileId: string): number {
|
||||
@@ -284,6 +300,7 @@ export function attachRestoredTabConflictScan(store: AppStoreApi): () => void {
|
||||
return
|
||||
}
|
||||
previousOpenFiles = nextOpenFiles
|
||||
forgetClosedTabReverificationRequests(nextOpenFiles)
|
||||
scan()
|
||||
})
|
||||
scan()
|
||||
|
||||
@@ -26,6 +26,7 @@ vi.mock('@/components/editor/combined-diff/remember-view/combined-diff-view-cach
|
||||
|
||||
import type { OpenFile } from '@/store/slices/editor'
|
||||
import {
|
||||
getPendingOverflowProbeCountForTest,
|
||||
reconcileEditorTabsAfterOverflow,
|
||||
supersedeOverflowProbesForBatch
|
||||
} from './editor-external-watch-overflow'
|
||||
@@ -192,9 +193,27 @@ describe('reconcileEditorTabsAfterOverflow', () => {
|
||||
},
|
||||
[touched]
|
||||
)
|
||||
expect(getPendingOverflowProbeCountForTest()).toBe(0)
|
||||
|
||||
answer([{ exists: true }])
|
||||
await new Promise((resolve) => setTimeout(resolve, 0))
|
||||
expect(harness.scheduleReload).not.toHaveBeenCalled()
|
||||
expect(getPendingOverflowProbeCountForTest()).toBe(0)
|
||||
})
|
||||
|
||||
it('leaves no probe entry behind for a tab closed while its probe was pending', async () => {
|
||||
setTabs(tab('a.ts'))
|
||||
let answer: (rows: unknown) => void = () => {}
|
||||
harness.runtimePathsExist.mockImplementationOnce(
|
||||
() => new Promise((resolve) => (answer = resolve))
|
||||
)
|
||||
reconcileEditorTabsAfterOverflow(target)
|
||||
expect(getPendingOverflowProbeCountForTest()).toBe(1)
|
||||
|
||||
setTabs()
|
||||
answer([{ exists: true }])
|
||||
await new Promise((resolve) => setTimeout(resolve, 0))
|
||||
expect(harness.scheduleReload).not.toHaveBeenCalled()
|
||||
expect(getPendingOverflowProbeCountForTest()).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -33,6 +33,10 @@ function claimProbeGeneration(fileId: string): number {
|
||||
return probeGenerationSeq
|
||||
}
|
||||
|
||||
export function getPendingOverflowProbeCountForTest(): number {
|
||||
return probeGenerationByFileId.size
|
||||
}
|
||||
|
||||
/** Drops pending existence answers for tabs this watch batch touched. */
|
||||
export function supersedeOverflowProbesForBatch(
|
||||
batchPaths: EditorExternalWatchBatchPathIndex,
|
||||
@@ -52,10 +56,9 @@ export function supersedeOverflowProbesForBatch(
|
||||
touched.add(file.id)
|
||||
}
|
||||
}
|
||||
// Why: the pending probe's generation can never match again, so its completion no longer owns an entry to delete.
|
||||
for (const fileId of touched) {
|
||||
if (probeGenerationByFileId.has(fileId)) {
|
||||
claimProbeGeneration(fileId)
|
||||
}
|
||||
probeGenerationByFileId.delete(fileId)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user