mirror of
https://github.com/stablyai/orca.git
synced 2026-10-18 00:03:20 +00:00
Add cancellable, generation-aware git status polling to cut stale scans
- Route git.status through an abortable subscription per requestToken so cancelStatus can actually abort the remote scan instead of being a no-op, preventing wasted work and stale responses overwriting fresher state. - Bump the git status polling generation on push-target changes so an in-flight refresh against the old remote/branch can't apply stale upstream data to the new one. - Guard the stale-conflict poller against writes after unmount. - Retire pre-purge line-stat scans in the cache so an older in-flight scan can't repopulate a key after a token-scoped purge.
This commit is contained in:
@@ -62,6 +62,10 @@ export function useStaleConflictOperationPolling(args: {
|
||||
return
|
||||
}
|
||||
|
||||
// Why: dispose() cannot interrupt an in-flight probe; without this guard a
|
||||
// request resolving after cleanup would overwrite newer conflict state.
|
||||
let active = true
|
||||
|
||||
const pollStale = async (): Promise<void> => {
|
||||
// Why: a backoff-deferred run can fire long after the window hides; skip
|
||||
// the probe instead of running SSH/RPC work nobody can see. The
|
||||
@@ -83,6 +87,9 @@ export function useStaleConflictOperationPolling(args: {
|
||||
worktreePath: path,
|
||||
connectionId
|
||||
})) as GitConflictOperation
|
||||
if (!active) {
|
||||
return
|
||||
}
|
||||
setConflictOperation(id, op)
|
||||
} catch {
|
||||
// ignore — worktree may have been removed
|
||||
@@ -106,6 +113,7 @@ export function useStaleConflictOperationPolling(args: {
|
||||
intervalMs: CONFLICT_POLL_INTERVAL_MS
|
||||
})
|
||||
return () => {
|
||||
active = false
|
||||
pollRunner.dispose()
|
||||
stopVisiblePoll()
|
||||
}
|
||||
|
||||
@@ -190,7 +190,10 @@ export function useGitStatusPolling(options: { enabled?: boolean } = {}): void {
|
||||
statusSchedulerRef.current = null
|
||||
}
|
||||
}
|
||||
}, [activeExecutionHostId, activeWorktreeId, worktreePath])
|
||||
// Why: push-target changes must bump the generation so an in-flight refresh
|
||||
// captured against the old remote/branch can't apply or cache stale upstream
|
||||
// status for the new one.
|
||||
}, [activeExecutionHostId, activePushTarget, activeWorktreeId, worktreePath])
|
||||
|
||||
useEffect(() => {
|
||||
const reconcile = (catchUp: boolean): void => {
|
||||
|
||||
@@ -66,6 +66,7 @@ import {
|
||||
type ExecutionHostId
|
||||
} from '../../../shared/execution-host'
|
||||
import { toRuntimeWorktreeSelector } from '../runtime/runtime-worktree-selector'
|
||||
import { callAbortableRuntimeEnvironment } from '../runtime/abortable-runtime-environment-call'
|
||||
import { normalizeDisabledTuiAgents } from '../../../shared/tui-agent-selection'
|
||||
import {
|
||||
normalizeTuiAgentArgsRecord,
|
||||
@@ -1642,23 +1643,65 @@ function createFileApi(): NonNullable<Partial<PreloadApi>['fs']> {
|
||||
}
|
||||
}
|
||||
|
||||
// Why: track the in-flight abortable status request per token so cancelStatus
|
||||
// can abort the matching subscription and close its remote request context.
|
||||
const webGitStatusAbortControllers = new Map<string, AbortController>()
|
||||
|
||||
async function callAbortableRuntimeStatus<TResult>(
|
||||
requestToken: string,
|
||||
params: unknown
|
||||
): Promise<TResult> {
|
||||
const environment = requireActiveEnvironment()
|
||||
webGitStatusAbortControllers.get(requestToken)?.abort()
|
||||
const controller = new AbortController()
|
||||
webGitStatusAbortControllers.set(requestToken, controller)
|
||||
try {
|
||||
const response = await callAbortableRuntimeEnvironment(
|
||||
environment.id,
|
||||
'git.status',
|
||||
params,
|
||||
undefined,
|
||||
controller.signal
|
||||
)
|
||||
updateEnvironmentFromResponse(environment, response)
|
||||
if (!response.ok) {
|
||||
throw new Error(response.error.message)
|
||||
}
|
||||
return response.result as TResult
|
||||
} finally {
|
||||
if (webGitStatusAbortControllers.get(requestToken) === controller) {
|
||||
webGitStatusAbortControllers.delete(requestToken)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function createGitApi(): NonNullable<Partial<PreloadApi>['git']> {
|
||||
return {
|
||||
status: async ({
|
||||
worktreePath,
|
||||
includeIgnored,
|
||||
bypassEffectiveUpstreamNegativeCache,
|
||||
reuseLineStats
|
||||
reuseLineStats,
|
||||
requestToken
|
||||
}) => {
|
||||
const worktree = await resolveRuntimeWorktreeByPath(worktreePath)
|
||||
return callRuntimeResult('git.status', {
|
||||
const params = {
|
||||
worktree: toRuntimeWorktreeSelector(worktree.id),
|
||||
includeIgnored,
|
||||
bypassEffectiveUpstreamNegativeCache,
|
||||
reuseLineStats
|
||||
})
|
||||
}
|
||||
// Why: without a token there's nothing to cancel, so stay on the pooled
|
||||
// call transport. With one, route through the subscription bridge so
|
||||
// cancelStatus can close the request context and abort the remote scan.
|
||||
if (!requestToken) {
|
||||
return callRuntimeResult('git.status', params)
|
||||
}
|
||||
return callAbortableRuntimeStatus(requestToken, params)
|
||||
},
|
||||
cancelStatus: async ({ requestToken }) => {
|
||||
webGitStatusAbortControllers.get(requestToken)?.abort()
|
||||
},
|
||||
cancelStatus: async () => {},
|
||||
submoduleStatus: async ({ worktreePath, submodulePath, area }) => {
|
||||
const worktree = await resolveRuntimeWorktreeByPath(worktreePath)
|
||||
return callRuntimeResult('git.submoduleStatus', {
|
||||
|
||||
@@ -247,6 +247,10 @@ export function clearGitStatusLineStatsCacheKey(
|
||||
cacheKey,
|
||||
(keyInvalidationGenerationByWorktree.get(cacheKey) ?? 0) + 1
|
||||
)
|
||||
} else {
|
||||
// Why: a token-scoped purge must retire scans that began before it, so an
|
||||
// older in-flight scan can't store pre-purge counts and repopulate this key.
|
||||
bumpBoundedKeyMap(lastStoredBeginSeqByWorktree, cacheKey, writeToken.beginSeq)
|
||||
}
|
||||
lineStatsByWorktree.delete(cacheKey)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user