perf(renderer): index detectedWorktreesByRepo so SSH panes and cards stop walking the full detected catalog on every store write (#12421)
* perf(renderer): index detected worktrees for owner lookups * test(renderer): make detected index perf gate deterministic * test(renderer): cover detected index owner hot path
This commit is contained in:
parent
9f638da62d
commit
c498d2d405
|
|
@ -7,7 +7,11 @@ import {
|
|||
import { parseWorkspaceKey } from '../../../shared/workspace-scope'
|
||||
import { getRepoIdFromWorktreeId } from '@/store/slices/worktree-helpers'
|
||||
import { addRoute, resolveExactWorktreeRoute, routeForOwner } from './worktree-owner-route'
|
||||
import { resolveIndexedWorktreeOwner } from './worktree-runtime-owner-index'
|
||||
import {
|
||||
findIndexedDetectedWorktrees,
|
||||
hasIndexedDetectedWorktree,
|
||||
resolveIndexedWorktreeOwner
|
||||
} from './worktree-runtime-owner-index'
|
||||
import {
|
||||
findFolderWorkspaceOwner,
|
||||
getExecutionHostIdForFolderWorkspace,
|
||||
|
|
@ -51,11 +55,7 @@ function ownerRecordsOnHost(
|
|||
executionHostId: ExecutionHostId
|
||||
): WorktreeOperationOwnerRecord[] {
|
||||
const owners: WorktreeOperationOwnerRecord[] = []
|
||||
const catalogs = [
|
||||
...Object.values(state.worktreesByRepo ?? {}),
|
||||
...Object.values(state.detectedWorktreesByRepo ?? {}).map((result) => result.worktrees)
|
||||
]
|
||||
for (const worktrees of catalogs) {
|
||||
for (const worktrees of Object.values(state.worktreesByRepo ?? {})) {
|
||||
for (const worktree of worktrees) {
|
||||
if (
|
||||
worktree.id === worktreeId &&
|
||||
|
|
@ -65,6 +65,11 @@ function ownerRecordsOnHost(
|
|||
}
|
||||
}
|
||||
}
|
||||
for (const worktree of findIndexedDetectedWorktrees(state.detectedWorktreesByRepo, worktreeId)) {
|
||||
if (parseExecutionHostId(worktree.hostId)?.id === executionHostId) {
|
||||
owners.push(worktree)
|
||||
}
|
||||
}
|
||||
return owners
|
||||
}
|
||||
|
||||
|
|
@ -135,12 +140,8 @@ export function resolveWorktreeOperationRouteResult(
|
|||
}
|
||||
|
||||
const hasKnownWorktree =
|
||||
Object.values(state.worktreesByRepo ?? {}).some((worktrees) =>
|
||||
worktrees.some((worktree) => worktree.id === worktreeId)
|
||||
) ||
|
||||
Object.values(state.detectedWorktreesByRepo ?? {}).some((result) =>
|
||||
result.worktrees.some((worktree) => worktree.id === worktreeId)
|
||||
)
|
||||
resolveIndexedWorktreeOwner(state.worktreesByRepo, worktreeId).kind !== 'missing' ||
|
||||
hasIndexedDetectedWorktree(state.detectedWorktreesByRepo, worktreeId)
|
||||
const repoId = getRepoIdFromWorktreeId(worktreeId)
|
||||
const hasKnownRepo = state.repos?.some((repo) => repo.id === repoId) === true
|
||||
if (!hasKnownWorktree && !hasKnownRepo) {
|
||||
|
|
@ -216,18 +217,14 @@ export function resolveExplicitWorktreeOperationRouteResult(
|
|||
addRoute(exactRoutes, resolution.route)
|
||||
}
|
||||
}
|
||||
for (const result of Object.values(state.detectedWorktreesByRepo ?? {})) {
|
||||
for (const worktree of result.worktrees) {
|
||||
if (worktree.id === worktreeId) {
|
||||
exactRepoIds.add(worktree.repoId)
|
||||
const resolution = resolveExactWorktreeRoute(state, worktree)
|
||||
if (resolution.kind === 'ambiguous') {
|
||||
return resolution
|
||||
}
|
||||
if (resolution.kind === 'resolved') {
|
||||
addRoute(exactRoutes, resolution.route)
|
||||
}
|
||||
}
|
||||
for (const worktree of findIndexedDetectedWorktrees(state.detectedWorktreesByRepo, worktreeId)) {
|
||||
exactRepoIds.add(worktree.repoId)
|
||||
const resolution = resolveExactWorktreeRoute(state, worktree)
|
||||
if (resolution.kind === 'ambiguous') {
|
||||
return resolution
|
||||
}
|
||||
if (resolution.kind === 'resolved') {
|
||||
addRoute(exactRoutes, resolution.route)
|
||||
}
|
||||
}
|
||||
if (exactRoutes.size > 0) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,200 @@
|
|||
import { writeFileSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import type { ExecutionHostId } from '../../../shared/execution-host'
|
||||
import { hasIndexedDetectedWorktree } from './worktree-runtime-owner-index'
|
||||
import { getExplicitRuntimeEnvironmentIdForWorktree } from './worktree-runtime-owner'
|
||||
import type { WorktreeRuntimeOwnerState } from './worktree-runtime-owner-state'
|
||||
|
||||
const REPO_COUNT = 20
|
||||
const WORKTREES_PER_REPO = 100
|
||||
const LOOKUPS_PER_SWEEP = 200
|
||||
const SWEEPS = 40
|
||||
|
||||
type OwnerRecord = {
|
||||
id: string
|
||||
repoId: string
|
||||
hostId?: ExecutionHostId
|
||||
runtimeOwnerEnvironmentId?: string
|
||||
}
|
||||
type DetectedByRepo = Record<string, { worktrees: readonly OwnerRecord[] }>
|
||||
|
||||
// The pre-index expression, kept as the "before" leg of the benchmark.
|
||||
function walkHasDetected(detectedWorktreesByRepo: DetectedByRepo | undefined, id: string): boolean {
|
||||
return Object.values(detectedWorktreesByRepo ?? {}).some((result) =>
|
||||
result.worktrees.some((worktree) => worktree.id === id)
|
||||
)
|
||||
}
|
||||
|
||||
function buildDetectedCatalog(generation: number, counter?: { reads: number }): DetectedByRepo {
|
||||
const catalog: DetectedByRepo = {}
|
||||
for (let repoIndex = 0; repoIndex < REPO_COUNT; repoIndex += 1) {
|
||||
const repoId = `repo-${repoIndex}`
|
||||
const worktrees: OwnerRecord[] = []
|
||||
for (let index = 0; index < WORKTREES_PER_REPO; index += 1) {
|
||||
const id = `${repoId}::detected-${index}-gen-${generation}`
|
||||
const record: OwnerRecord = { id, repoId, hostId: 'ssh:target-a' }
|
||||
if (counter) {
|
||||
Object.defineProperty(record, 'id', {
|
||||
get: () => {
|
||||
counter.reads += 1
|
||||
return id
|
||||
},
|
||||
enumerable: true
|
||||
})
|
||||
}
|
||||
worktrees.push(record)
|
||||
}
|
||||
catalog[repoId] = { worktrees }
|
||||
}
|
||||
return catalog
|
||||
}
|
||||
|
||||
const publishedRepos = Array.from({ length: REPO_COUNT }, (_unused, index) => ({
|
||||
id: `repo-${index}`,
|
||||
connectionId: `target-${index}`
|
||||
}))
|
||||
const publishedWorktreesByRepo: Record<string, OwnerRecord[]> = Object.fromEntries(
|
||||
publishedRepos.map((repo) => [
|
||||
repo.id,
|
||||
Array.from({ length: WORKTREES_PER_REPO }, (_unused, index) => ({
|
||||
id: `${repo.id}::worktree-${index}`,
|
||||
repoId: repo.id,
|
||||
hostId: 'ssh:target-a' as const
|
||||
}))
|
||||
])
|
||||
)
|
||||
|
||||
// Only the detected catalog changes identity: that is what isolates the collection under test.
|
||||
function buildOwnerState(detectedWorktreesByRepo: DetectedByRepo): WorktreeRuntimeOwnerState {
|
||||
return {
|
||||
repos: publishedRepos,
|
||||
worktreesByRepo: publishedWorktreesByRepo,
|
||||
detectedWorktreesByRepo,
|
||||
activeWorktreeId: null,
|
||||
activeWorkspaceExecutionHostId: null,
|
||||
runtimeEnvironments: []
|
||||
}
|
||||
}
|
||||
|
||||
// Probe ids are published but never detected — the SSH-pane case, and the walk's worst case.
|
||||
const PROBE_IDS = Array.from(
|
||||
{ length: LOOKUPS_PER_SWEEP },
|
||||
(_unused, index) => `repo-${index % REPO_COUNT}::worktree-${index % WORKTREES_PER_REPO}`
|
||||
)
|
||||
|
||||
const WARMUP_SWEEPS = 5
|
||||
const warmCatalog = buildDetectedCatalog(-1)
|
||||
|
||||
// Each fresh leg needs its own never-indexed catalogs; a shared pool would leave the index warm
|
||||
// for whichever leg runs second and silently erase the rebuild it is supposed to measure.
|
||||
let catalogGeneration = 0
|
||||
function freshCatalogPool(): DetectedByRepo[] {
|
||||
return Array.from({ length: SWEEPS + WARMUP_SWEEPS }, () =>
|
||||
buildDetectedCatalog((catalogGeneration += 1))
|
||||
)
|
||||
}
|
||||
|
||||
function measureSweeps(run: (sweep: number) => void): number {
|
||||
for (let warmup = 0; warmup < WARMUP_SWEEPS; warmup += 1) {
|
||||
run(warmup)
|
||||
}
|
||||
const started = performance.now()
|
||||
for (let sweep = 0; sweep < SWEEPS; sweep += 1) {
|
||||
run(WARMUP_SWEEPS + sweep)
|
||||
}
|
||||
return (performance.now() - started) / SWEEPS
|
||||
}
|
||||
|
||||
describe('detected worktree index performance', () => {
|
||||
it('answers repeated owner lookups without rescanning the detected catalog', () => {
|
||||
const counter = { reads: 0 }
|
||||
const catalog = buildDetectedCatalog(0, counter)
|
||||
const state = buildOwnerState(catalog)
|
||||
|
||||
getExplicitRuntimeEnvironmentIdForWorktree(state, 'repo-0::warm-the-index')
|
||||
counter.reads = 0
|
||||
for (const probeId of PROBE_IDS) {
|
||||
getExplicitRuntimeEnvironmentIdForWorktree(state, probeId)
|
||||
}
|
||||
const indexedReads = counter.reads
|
||||
|
||||
counter.reads = 0
|
||||
for (const probeId of PROBE_IDS) {
|
||||
walkHasDetected(catalog, probeId)
|
||||
}
|
||||
const walkedReads = counter.reads
|
||||
|
||||
expect(indexedReads).toBe(0)
|
||||
expect(walkedReads).toBeGreaterThan(100_000)
|
||||
})
|
||||
|
||||
it('records lookup-path timing without making wall clock a CI gate', () => {
|
||||
const walkPool = freshCatalogPool()
|
||||
const walkMs = measureSweeps((sweep) => {
|
||||
const catalog = walkPool[sweep]!
|
||||
for (const probeId of PROBE_IDS) {
|
||||
walkHasDetected(catalog, probeId)
|
||||
}
|
||||
})
|
||||
const walkWarmMs = measureSweeps(() => {
|
||||
for (const probeId of PROBE_IDS) {
|
||||
walkHasDetected(warmCatalog, probeId)
|
||||
}
|
||||
})
|
||||
// Unrelated store writes leave the detected catalog identical, so the index stays warm.
|
||||
const warmMs = measureSweeps(() => {
|
||||
for (const probeId of PROBE_IDS) {
|
||||
hasIndexedDetectedWorktree(warmCatalog, probeId)
|
||||
}
|
||||
})
|
||||
// A landed worktree scan republishes the catalog: one rebuild amortized over the sweep.
|
||||
const freshPool = freshCatalogPool()
|
||||
const freshMs = measureSweeps((sweep) => {
|
||||
const catalog = freshPool[sweep]!
|
||||
for (const probeId of PROBE_IDS) {
|
||||
hasIndexedDetectedWorktree(catalog, probeId)
|
||||
}
|
||||
})
|
||||
|
||||
const statePool = freshCatalogPool().map(buildOwnerState)
|
||||
const warmState = buildOwnerState(warmCatalog)
|
||||
const explicitOwnerFreshMs = measureSweeps((sweep) => {
|
||||
const state = statePool[sweep]!
|
||||
for (const probeId of PROBE_IDS) {
|
||||
getExplicitRuntimeEnvironmentIdForWorktree(state, probeId)
|
||||
}
|
||||
})
|
||||
const explicitOwnerWarmMs = measureSweeps(() => {
|
||||
for (const probeId of PROBE_IDS) {
|
||||
getExplicitRuntimeEnvironmentIdForWorktree(warmState, probeId)
|
||||
}
|
||||
})
|
||||
|
||||
const round = (value: number): number => Number(value.toFixed(4))
|
||||
const report = {
|
||||
entries: REPO_COUNT * WORKTREES_PER_REPO,
|
||||
lookupsPerSweep: LOOKUPS_PER_SWEEP,
|
||||
sweeps: SWEEPS,
|
||||
lookupPath: {
|
||||
walkFreshMsPerSweep: round(walkMs),
|
||||
walkWarmMsPerSweep: round(walkWarmMs),
|
||||
indexedWarmMsPerSweep: round(warmMs),
|
||||
indexedFreshMsPerSweep: round(freshMs),
|
||||
warmSpeedup: round(walkWarmMs / warmMs),
|
||||
freshSpeedup: round(walkMs / freshMs)
|
||||
},
|
||||
getExplicitRuntimeEnvironmentIdForWorktree: {
|
||||
warmMsPerSweep: round(explicitOwnerWarmMs),
|
||||
freshMsPerSweep: round(explicitOwnerFreshMs)
|
||||
}
|
||||
}
|
||||
writeFileSync(
|
||||
join(tmpdir(), 'orca-detected-worktree-index-bench.json'),
|
||||
`${JSON.stringify(report, null, 2)}\n`
|
||||
)
|
||||
|
||||
// Why: shared-runner timing is noisy; the read-count test above is the deterministic CI gate.
|
||||
})
|
||||
})
|
||||
|
|
@ -0,0 +1,182 @@
|
|||
import { describe, expect, it } from 'vitest'
|
||||
import type { ExecutionHostId } from '../../../shared/execution-host'
|
||||
import {
|
||||
findIndexedDetectedWorktrees,
|
||||
hasIndexedDetectedWorktree,
|
||||
resolveIndexedWorktreeOwner
|
||||
} from './worktree-runtime-owner-index'
|
||||
|
||||
type OwnerRecord = {
|
||||
id: string
|
||||
repoId: string
|
||||
hostId?: ExecutionHostId
|
||||
runtimeOwnerEnvironmentId?: string
|
||||
}
|
||||
type DetectedByRepo = Record<string, { worktrees: readonly OwnerRecord[] }>
|
||||
|
||||
// The pre-index expressions this module replaced, kept verbatim as parity oracles.
|
||||
function walkHasDetected(detectedWorktreesByRepo: DetectedByRepo | undefined, id: string): boolean {
|
||||
return Object.values(detectedWorktreesByRepo ?? {}).some((result) =>
|
||||
result.worktrees.some((worktree) => worktree.id === id)
|
||||
)
|
||||
}
|
||||
|
||||
function walkDetectedMatches(
|
||||
detectedWorktreesByRepo: DetectedByRepo | undefined,
|
||||
id: string
|
||||
): OwnerRecord[] {
|
||||
const matches: OwnerRecord[] = []
|
||||
for (const result of Object.values(detectedWorktreesByRepo ?? {})) {
|
||||
for (const worktree of result.worktrees) {
|
||||
if (worktree.id === id) {
|
||||
matches.push(worktree)
|
||||
}
|
||||
}
|
||||
}
|
||||
return matches
|
||||
}
|
||||
|
||||
function walkHasKnown(
|
||||
worktreesByRepo: Record<string, readonly OwnerRecord[]> | undefined,
|
||||
id: string
|
||||
): boolean {
|
||||
return Object.values(worktreesByRepo ?? {}).some((worktrees) =>
|
||||
worktrees.some((worktree) => worktree.id === id)
|
||||
)
|
||||
}
|
||||
|
||||
// Deterministic LCG so a parity failure reproduces from the printed case index.
|
||||
function makeRandom(seed: number): () => number {
|
||||
let state = seed >>> 0
|
||||
return () => {
|
||||
state = (state * 1664525 + 1013904223) >>> 0
|
||||
return state / 0x100000000
|
||||
}
|
||||
}
|
||||
|
||||
const HOST_IDS: (ExecutionHostId | undefined)[] = [
|
||||
undefined,
|
||||
'local',
|
||||
'ssh:target-a',
|
||||
'ssh:target-b',
|
||||
'runtime:hub-a'
|
||||
]
|
||||
|
||||
function buildCase(random: () => number): {
|
||||
detectedWorktreesByRepo: DetectedByRepo | undefined
|
||||
worktreesByRepo: Record<string, readonly OwnerRecord[]>
|
||||
probeIds: string[]
|
||||
} {
|
||||
const shape = random()
|
||||
if (shape < 0.05) {
|
||||
return { detectedWorktreesByRepo: undefined, worktreesByRepo: {}, probeIds: ['repo-0::absent'] }
|
||||
}
|
||||
if (shape < 0.1) {
|
||||
return { detectedWorktreesByRepo: {}, worktreesByRepo: {}, probeIds: ['repo-0::absent'] }
|
||||
}
|
||||
const repoCount = 1 + Math.floor(random() * 6)
|
||||
const detectedWorktreesByRepo: DetectedByRepo = {}
|
||||
const worktreesByRepo: Record<string, readonly OwnerRecord[]> = {}
|
||||
const knownIds: string[] = []
|
||||
for (let repoIndex = 0; repoIndex < repoCount; repoIndex += 1) {
|
||||
const repoId = `repo-${repoIndex}`
|
||||
// Empty arrays are a real store shape: a scan that found nothing still publishes its bucket.
|
||||
const worktreeCount = random() < 0.25 ? 0 : 1 + Math.floor(random() * 5)
|
||||
const detected: OwnerRecord[] = []
|
||||
const published: OwnerRecord[] = []
|
||||
for (let index = 0; index < worktreeCount; index += 1) {
|
||||
// Duplicate ids across repos are how rival publications collide, so allow a shared pool.
|
||||
const id = random() < 0.3 ? `shared::worktree-${index}` : `${repoId}::worktree-${index}`
|
||||
const record: OwnerRecord = {
|
||||
id,
|
||||
repoId,
|
||||
hostId: HOST_IDS[Math.floor(random() * HOST_IDS.length)],
|
||||
...(random() < 0.3 ? { runtimeOwnerEnvironmentId: `hub-${Math.floor(random() * 3)}` } : {})
|
||||
}
|
||||
knownIds.push(id)
|
||||
if (random() < 0.8) {
|
||||
detected.push(record)
|
||||
}
|
||||
if (random() < 0.5) {
|
||||
published.push(record)
|
||||
}
|
||||
}
|
||||
detectedWorktreesByRepo[repoId] = { worktrees: detected }
|
||||
worktreesByRepo[repoId] = published
|
||||
}
|
||||
const probeIds = [
|
||||
knownIds[0] ?? 'repo-0::absent',
|
||||
knownIds.at(-1) ?? 'repo-0::absent',
|
||||
knownIds[Math.floor(random() * Math.max(knownIds.length, 1))] ?? 'repo-0::absent',
|
||||
'shared::worktree-0',
|
||||
'never-published::worktree'
|
||||
]
|
||||
return { detectedWorktreesByRepo, worktreesByRepo, probeIds }
|
||||
}
|
||||
|
||||
describe('detected worktree index', () => {
|
||||
it('matches the pre-index catalog walk across randomized store shapes', () => {
|
||||
const random = makeRandom(0x5eed)
|
||||
for (let caseIndex = 0; caseIndex < 240; caseIndex += 1) {
|
||||
const { detectedWorktreesByRepo, worktreesByRepo, probeIds } = buildCase(random)
|
||||
for (const probeId of probeIds) {
|
||||
expect(
|
||||
{
|
||||
case: caseIndex,
|
||||
probeId,
|
||||
has: hasIndexedDetectedWorktree(detectedWorktreesByRepo, probeId)
|
||||
},
|
||||
`case ${caseIndex} / ${probeId}`
|
||||
).toEqual({
|
||||
case: caseIndex,
|
||||
probeId,
|
||||
has: walkHasDetected(detectedWorktreesByRepo, probeId)
|
||||
})
|
||||
expect(
|
||||
findIndexedDetectedWorktrees(detectedWorktreesByRepo, probeId),
|
||||
`case ${caseIndex} / ${probeId}`
|
||||
).toEqual(walkDetectedMatches(detectedWorktreesByRepo, probeId))
|
||||
expect(
|
||||
resolveIndexedWorktreeOwner(worktreesByRepo, probeId).kind !== 'missing',
|
||||
`case ${caseIndex} / ${probeId}`
|
||||
).toBe(walkHasKnown(worktreesByRepo, probeId))
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
it('returns rival publications in catalog order with identity preserved', () => {
|
||||
const first = { id: 'shared', repoId: 'repo-a', hostId: 'ssh:target-a' as const }
|
||||
const second = { id: 'shared', repoId: 'repo-b', hostId: 'runtime:hub-a' as const }
|
||||
const detectedWorktreesByRepo = {
|
||||
'repo-a': { worktrees: [first] },
|
||||
'repo-b': { worktrees: [second] }
|
||||
}
|
||||
|
||||
const matches = findIndexedDetectedWorktrees(detectedWorktreesByRepo, 'shared')
|
||||
expect(matches).toHaveLength(2)
|
||||
expect(matches[0]).toBe(first)
|
||||
expect(matches[1]).toBe(second)
|
||||
})
|
||||
|
||||
it('treats undefined, empty, and empty-bucket catalogs as unpublished', () => {
|
||||
expect(hasIndexedDetectedWorktree(undefined, 'repo::wt')).toBe(false)
|
||||
expect(findIndexedDetectedWorktrees(undefined, 'repo::wt')).toEqual([])
|
||||
expect(hasIndexedDetectedWorktree({}, 'repo::wt')).toBe(false)
|
||||
expect(hasIndexedDetectedWorktree({ repo: { worktrees: [] } }, 'repo::wt')).toBe(false)
|
||||
})
|
||||
|
||||
it('re-indexes a new catalog identity instead of serving stale hits', () => {
|
||||
const worktree = { id: 'repo::wt', repoId: 'repo' }
|
||||
const before = { repo: { worktrees: [worktree] } }
|
||||
expect(hasIndexedDetectedWorktree(before, 'repo::wt')).toBe(true)
|
||||
|
||||
const afterRemoval = { repo: { worktrees: [] } }
|
||||
expect(hasIndexedDetectedWorktree(afterRemoval, 'repo::wt')).toBe(false)
|
||||
|
||||
const afterReadd = { repo: { worktrees: [worktree] } }
|
||||
expect(findIndexedDetectedWorktrees(afterReadd, 'repo::wt')).toEqual([worktree])
|
||||
// The prior identity keeps its own cached answer; nothing bleeds between snapshots.
|
||||
expect(hasIndexedDetectedWorktree(afterRemoval, 'repo::wt')).toBe(false)
|
||||
expect(hasIndexedDetectedWorktree(before, 'repo::wt')).toBe(true)
|
||||
})
|
||||
})
|
||||
|
|
@ -8,6 +8,7 @@ import {
|
|||
} from '../../../shared/execution-host'
|
||||
|
||||
type WorktreeOwnerRecord = Pick<Worktree, 'id' | 'repoId' | 'hostId' | 'runtimeOwnerEnvironmentId'>
|
||||
type DetectedWorktreeListing = { worktrees: readonly WorktreeOwnerRecord[] }
|
||||
type RepoOwnerRecord = Pick<Repo, 'id' | 'connectionId' | 'executionHostId'>
|
||||
type FolderWorkspaceOwnerRecord = Pick<
|
||||
FolderWorkspace,
|
||||
|
|
@ -33,6 +34,12 @@ const projectGroupOwnerIndexCache = new WeakMap<
|
|||
readonly ProjectGroupOwnerRecord[],
|
||||
ReadonlyMap<string, IndexedProjectGroupOwnerResolution>
|
||||
>()
|
||||
const detectedWorktreeIndexCache = new WeakMap<
|
||||
Record<string, DetectedWorktreeListing>,
|
||||
ReadonlyMap<string, readonly WorktreeOwnerRecord[]>
|
||||
>()
|
||||
|
||||
const NO_DETECTED_WORKTREES: readonly WorktreeOwnerRecord[] = []
|
||||
|
||||
type IndexedFolderWorkspaceOwnerResolution =
|
||||
| { kind: 'resolved'; owner: FolderWorkspaceOwnerRecord }
|
||||
|
|
@ -210,6 +217,43 @@ export function resolveIndexedWorktreeOwner(
|
|||
return index.get(worktreeId) ?? { kind: 'missing' }
|
||||
}
|
||||
|
||||
/**
|
||||
* Every detected publication of `worktreeId`, in catalog order. Rival repos may publish the same
|
||||
* id, so callers that fail closed on conflicts need all matches rather than one resolved owner.
|
||||
*/
|
||||
export function findIndexedDetectedWorktrees(
|
||||
detectedWorktreesByRepo: Record<string, DetectedWorktreeListing> | undefined,
|
||||
worktreeId: string
|
||||
): readonly WorktreeOwnerRecord[] {
|
||||
if (!detectedWorktreesByRepo) {
|
||||
return NO_DETECTED_WORKTREES
|
||||
}
|
||||
let index = detectedWorktreeIndexCache.get(detectedWorktreesByRepo)
|
||||
if (!index) {
|
||||
const next = new Map<string, WorktreeOwnerRecord[]>()
|
||||
for (const listing of Object.values(detectedWorktreesByRepo)) {
|
||||
for (const worktree of listing.worktrees) {
|
||||
const matches = next.get(worktree.id)
|
||||
if (matches) {
|
||||
matches.push(worktree)
|
||||
} else {
|
||||
next.set(worktree.id, [worktree])
|
||||
}
|
||||
}
|
||||
}
|
||||
index = next
|
||||
detectedWorktreeIndexCache.set(detectedWorktreesByRepo, index)
|
||||
}
|
||||
return index.get(worktreeId) ?? NO_DETECTED_WORKTREES
|
||||
}
|
||||
|
||||
export function hasIndexedDetectedWorktree(
|
||||
detectedWorktreesByRepo: Record<string, DetectedWorktreeListing> | undefined,
|
||||
worktreeId: string
|
||||
): boolean {
|
||||
return findIndexedDetectedWorktrees(detectedWorktreesByRepo, worktreeId).length > 0
|
||||
}
|
||||
|
||||
export function findIndexedRepoOwner(
|
||||
repos: readonly RepoOwnerRecord[] | undefined,
|
||||
repoId: string
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import { getRepoIdFromWorktreeId } from '@/store/slices/worktree-helpers'
|
|||
import {
|
||||
findIndexedRepoOwner as findRepoRecord,
|
||||
findIndexedWorktreeOwner as findWorktreeRecord,
|
||||
hasIndexedDetectedWorktree,
|
||||
resolveIndexedRepoOwner,
|
||||
resolveIndexedWorktreeOwner
|
||||
} from './worktree-runtime-owner-index'
|
||||
|
|
@ -79,9 +80,7 @@ export function getRuntimeEnvironmentIdForWorktree(
|
|||
const owner = indexedOwner.owner
|
||||
const projectedRuntimeOwner = getProjectedRuntimeOwnerEnvironmentId(owner)
|
||||
const parsedHost = parseExecutionHostId(owner.hostId)
|
||||
const hasDetectedOwner = Object.values(state.detectedWorktreesByRepo ?? {}).some((result) =>
|
||||
result.worktrees.some((worktree) => worktree.id === worktreeId)
|
||||
)
|
||||
const hasDetectedOwner = hasIndexedDetectedWorktree(state.detectedWorktreesByRepo, worktreeId)
|
||||
if (!hasDetectedOwner && (projectedRuntimeOwner || parsedHost)) {
|
||||
return (
|
||||
projectedRuntimeOwner || (parsedHost?.kind === 'runtime' ? parsedHost.environmentId : null)
|
||||
|
|
@ -125,9 +124,7 @@ export function getExplicitRuntimeEnvironmentIdForWorktree(
|
|||
workspaceScope.folderWorkspaceId
|
||||
)
|
||||
}
|
||||
const hasDetectedOwner = Object.values(state.detectedWorktreesByRepo ?? {}).some((result) =>
|
||||
result.worktrees.some((worktree) => worktree.id === worktreeId)
|
||||
)
|
||||
const hasDetectedOwner = hasIndexedDetectedWorktree(state.detectedWorktreesByRepo, worktreeId)
|
||||
if (hasDetectedOwner) {
|
||||
// Why: detected-only rows are selectable before the primary catalog lands; use the same
|
||||
// ambiguity-aware explicit provenance as filesystem and terminal operations.
|
||||
|
|
@ -177,9 +174,7 @@ export function getExecutionHostIdForWorktree(
|
|||
if (workspaceScope?.type === 'folder') {
|
||||
return getExecutionHostIdForFolderWorkspace(state, workspaceScope.folderWorkspaceId)
|
||||
}
|
||||
const hasDetectedOwner = Object.values(state.detectedWorktreesByRepo ?? {}).some((result) =>
|
||||
result.worktrees.some((worktree) => worktree.id === worktreeId)
|
||||
)
|
||||
const hasDetectedOwner = hasIndexedDetectedWorktree(state.detectedWorktreesByRepo, worktreeId)
|
||||
if (hasDetectedOwner) {
|
||||
const resolution = resolveExplicitWorktreeOperationRouteResult(state, worktreeId)
|
||||
if (resolution.kind === 'resolved') {
|
||||
|
|
|
|||
Loading…
Reference in New Issue