mirror of
https://github.com/stablyai/orca.git
synced 2026-10-20 08:03:28 +00:00
perf(mobile): avoid unchanged worktree catalog payloads
This commit is contained in:
@@ -94,6 +94,7 @@ import {
|
||||
import { useWorkspaceSections } from '../../../src/worktree/use-workspace-sections'
|
||||
import { getMobileWorkspaceLineageGroupKey } from '../../../src/worktree/mobile-workspace-lineage'
|
||||
import { areWorktreeListsEqual } from '../../../src/worktree/worktree-list-snapshot'
|
||||
import { WorktreeCatalogSnapshotClient } from '../../../src/worktree/worktree-catalog-snapshot-client'
|
||||
import { repoColor } from '../../../src/worktree/repo-color'
|
||||
import {
|
||||
WORKSPACE_GROUP_OPTIONS as GROUP_OPTIONS,
|
||||
@@ -141,6 +142,7 @@ export function HostScreen({
|
||||
const lastConnectedAt = useLastConnectedAt(hostId)
|
||||
const clientRef = useRef<RpcClient | null>(null)
|
||||
const fetchWorktreesInFlightRef = useRef(false)
|
||||
const worktreeCatalog = useMemo(() => new WorktreeCatalogSnapshotClient(), [])
|
||||
const fetchRepoMetadataInFlightRef = useRef(new WeakSet<RpcClient>())
|
||||
const fetchRepoMetadataPendingRef = useRef(new WeakSet<RpcClient>())
|
||||
const repoMetadataFetchedAtRef = useRef(0)
|
||||
@@ -423,27 +425,28 @@ export function HostScreen({
|
||||
const requestHostId = hostId
|
||||
|
||||
try {
|
||||
// Why: worktree.ps silently truncates at 200; use a high cap so large hosts don't drop workspaces.
|
||||
const response = await requestClient.sendRequest('worktree.ps', { limit: 10000 })
|
||||
const pendingCatalog = await worktreeCatalog.fetch(requestClient, requestHostId)
|
||||
if (clientRef.current !== requestClient || hostId !== requestHostId) {
|
||||
return
|
||||
}
|
||||
if (!options.allowDuringModal && newWorktreeModalVisibleRef.current) {
|
||||
return
|
||||
}
|
||||
if (response.ok) {
|
||||
const result = (response as RpcSuccess).result as { worktrees: Worktree[] }
|
||||
// Why: reuse the existing array on identical snapshots to keep SectionList/sort rebuilds off the tap path.
|
||||
setWorktrees((current) =>
|
||||
areWorktreeListsEqual(current, result.worktrees) ? current : result.worktrees
|
||||
)
|
||||
setLastKnownWorktrees((current) =>
|
||||
areWorktreeListsEqual(current, result.worktrees) ? current : result.worktrees
|
||||
)
|
||||
setWorktreesLoaded(true)
|
||||
// Why (#8498): overwrite the home-written cache with the confirmed snapshot so a reconnect/remount can't serve a stale list.
|
||||
if (hostId) {
|
||||
setCachedWorktrees(hostId, result.worktrees)
|
||||
const result = worktreeCatalog.admit(pendingCatalog)
|
||||
if (result) {
|
||||
if (result.changed) {
|
||||
// Why: reuse the existing array on identical snapshots to keep SectionList/sort rebuilds off the tap path.
|
||||
setWorktrees((current) =>
|
||||
areWorktreeListsEqual(current, result.worktrees) ? current : result.worktrees
|
||||
)
|
||||
setLastKnownWorktrees((current) =>
|
||||
areWorktreeListsEqual(current, result.worktrees) ? current : result.worktrees
|
||||
)
|
||||
setWorktreesLoaded(true)
|
||||
// Why (#8498): overwrite the home-written cache with the confirmed snapshot so a reconnect/remount can't serve a stale list.
|
||||
if (hostId) {
|
||||
setCachedWorktrees(hostId, result.worktrees)
|
||||
}
|
||||
}
|
||||
// Drop the optimistic active override once the host reports it active, so later desktop changes win.
|
||||
setOptimisticActiveWorktreeId((pending) =>
|
||||
@@ -487,7 +490,7 @@ export function HostScreen({
|
||||
fetchWorktreesInFlightRef.current = false
|
||||
}
|
||||
},
|
||||
[client, connState, hostId]
|
||||
[client, connState, hostId, worktreeCatalog]
|
||||
)
|
||||
|
||||
useFocusEffect(
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import type { RpcClient } from '../transport/rpc-client'
|
||||
import {
|
||||
admitWorktreeCatalogResponse,
|
||||
WorktreeCatalogSnapshotClient
|
||||
} from './worktree-catalog-snapshot-client'
|
||||
|
||||
describe('admitWorktreeCatalogResponse', () => {
|
||||
it('accepts new-host full and unchanged responses', () => {
|
||||
const full = admitWorktreeCatalogResponse(
|
||||
{ worktrees: [{ id: 'worktree-1' }], snapshotId: 'snapshot-1' },
|
||||
null
|
||||
)
|
||||
const unchanged = admitWorktreeCatalogResponse(
|
||||
{ unchanged: true, snapshotId: 'snapshot-1' },
|
||||
'snapshot-1'
|
||||
)
|
||||
|
||||
expect(full).toEqual({
|
||||
kind: 'full',
|
||||
snapshotId: 'snapshot-1',
|
||||
worktrees: [{ id: 'worktree-1' }]
|
||||
})
|
||||
expect(unchanged).toEqual({ kind: 'unchanged', snapshotId: 'snapshot-1' })
|
||||
})
|
||||
|
||||
it('treats an old-host full response as authoritative and clears the token', () => {
|
||||
expect(
|
||||
admitWorktreeCatalogResponse({ worktrees: [{ id: 'worktree-1' }] }, 'stale-token')
|
||||
).toEqual({
|
||||
kind: 'full',
|
||||
snapshotId: null,
|
||||
worktrees: [{ id: 'worktree-1' }]
|
||||
})
|
||||
})
|
||||
|
||||
it('rejects unchanged responses for a snapshot the client does not own', () => {
|
||||
expect(
|
||||
admitWorktreeCatalogResponse({ unchanged: true, snapshotId: 'snapshot-2' }, 'snapshot-1')
|
||||
).toEqual({ kind: 'invalid' })
|
||||
expect(admitWorktreeCatalogResponse({ unchanged: true }, 'snapshot-1')).toEqual({
|
||||
kind: 'invalid'
|
||||
})
|
||||
})
|
||||
|
||||
it('rejects malformed success payloads', () => {
|
||||
expect(admitWorktreeCatalogResponse(null, 'snapshot-1')).toEqual({ kind: 'invalid' })
|
||||
expect(
|
||||
admitWorktreeCatalogResponse({ unchanged: false, snapshotId: 'snapshot-1' }, 'snapshot-1')
|
||||
).toEqual({ kind: 'invalid' })
|
||||
expect(
|
||||
admitWorktreeCatalogResponse({ worktrees: [], snapshotId: 'x'.repeat(129) }, null)
|
||||
).toEqual({ kind: 'full', snapshotId: null, worktrees: [] })
|
||||
})
|
||||
})
|
||||
|
||||
function clientWithResults(...results: unknown[]): RpcClient {
|
||||
return {
|
||||
sendRequest: vi.fn(
|
||||
async () =>
|
||||
({
|
||||
id: 'request',
|
||||
ok: true,
|
||||
result: results.shift(),
|
||||
_meta: { runtimeId: 'runtime' }
|
||||
}) as const
|
||||
)
|
||||
} as unknown as RpcClient
|
||||
}
|
||||
|
||||
describe('WorktreeCatalogSnapshotClient', () => {
|
||||
it('retains confirmed rows while admitting unchanged responses', async () => {
|
||||
const client = clientWithResults(
|
||||
{ worktrees: [{ worktreeId: 'worktree-1' }], snapshotId: 'snapshot-1' },
|
||||
{ unchanged: true, snapshotId: 'snapshot-1' }
|
||||
)
|
||||
const snapshots = new WorktreeCatalogSnapshotClient()
|
||||
|
||||
const first = snapshots.admit(await snapshots.fetch(client, 'host-1'))
|
||||
const second = snapshots.admit(await snapshots.fetch(client, 'host-1'))
|
||||
|
||||
expect(first).toMatchObject({ changed: true })
|
||||
expect(second).toEqual({ changed: false, worktrees: first?.worktrees })
|
||||
})
|
||||
|
||||
it('does not advance the token until the caller admits a response', async () => {
|
||||
const client = clientWithResults(
|
||||
{ worktrees: [], snapshotId: 'snapshot-1' },
|
||||
{ worktrees: [], snapshotId: 'snapshot-1' }
|
||||
)
|
||||
const snapshots = new WorktreeCatalogSnapshotClient()
|
||||
|
||||
await snapshots.fetch(client, 'host-1')
|
||||
snapshots.admit(await snapshots.fetch(client, 'host-1'))
|
||||
|
||||
expect(client.sendRequest).toHaveBeenNthCalledWith(2, 'worktree.ps', {
|
||||
limit: 10_000,
|
||||
afterSnapshotId: null
|
||||
})
|
||||
})
|
||||
|
||||
it('preserves the last admitted token across transport failures', async () => {
|
||||
const client = clientWithResults({ worktrees: [], snapshotId: 'snapshot-1' })
|
||||
const snapshots = new WorktreeCatalogSnapshotClient()
|
||||
|
||||
snapshots.admit(await snapshots.fetch(client, 'host-1'))
|
||||
snapshots.admit(null)
|
||||
await snapshots.fetch(client, 'host-1')
|
||||
|
||||
expect(client.sendRequest).toHaveBeenNthCalledWith(2, 'worktree.ps', {
|
||||
limit: 10_000,
|
||||
afterSnapshotId: 'snapshot-1'
|
||||
})
|
||||
})
|
||||
|
||||
it('clears snapshot ownership after a mismatched unchanged response', async () => {
|
||||
const client = clientWithResults(
|
||||
{ worktrees: [], snapshotId: 'snapshot-1' },
|
||||
{ unchanged: true, snapshotId: 'snapshot-2' },
|
||||
{ worktrees: [], snapshotId: 'snapshot-3' }
|
||||
)
|
||||
const snapshots = new WorktreeCatalogSnapshotClient()
|
||||
|
||||
snapshots.admit(await snapshots.fetch(client, 'host-1'))
|
||||
snapshots.admit(await snapshots.fetch(client, 'host-1'))
|
||||
await snapshots.fetch(client, 'host-1')
|
||||
|
||||
expect(client.sendRequest).toHaveBeenNthCalledWith(3, 'worktree.ps', {
|
||||
limit: 10_000,
|
||||
afterSnapshotId: null
|
||||
})
|
||||
})
|
||||
|
||||
it('resets snapshot ownership when the client or host changes', async () => {
|
||||
const firstClient = clientWithResults({ worktrees: [], snapshotId: 'snapshot-1' })
|
||||
const secondClient = clientWithResults({ worktrees: [], snapshotId: 'snapshot-2' })
|
||||
const snapshots = new WorktreeCatalogSnapshotClient()
|
||||
|
||||
snapshots.admit(await snapshots.fetch(firstClient, 'host-1'))
|
||||
await snapshots.fetch(secondClient, 'host-2')
|
||||
|
||||
expect(secondClient.sendRequest).toHaveBeenCalledWith('worktree.ps', {
|
||||
limit: 10_000,
|
||||
afterSnapshotId: null
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,108 @@
|
||||
import type { RpcClient } from '../transport/rpc-client'
|
||||
import type { RpcSuccess } from '../transport/types'
|
||||
import type { Worktree } from './workspace-list-sections'
|
||||
|
||||
export type WorktreeCatalogAdmission<T> =
|
||||
| { kind: 'full'; snapshotId: string | null; worktrees: T[] }
|
||||
| { kind: 'unchanged'; snapshotId: string }
|
||||
| { kind: 'invalid' }
|
||||
|
||||
type PendingWorktreeCatalog = {
|
||||
admission: WorktreeCatalogAdmission<Worktree>
|
||||
client: RpcClient
|
||||
hostId: string
|
||||
}
|
||||
|
||||
export type AdmittedWorktreeCatalog = {
|
||||
changed: boolean
|
||||
worktrees: Worktree[]
|
||||
}
|
||||
|
||||
function validSnapshotId(value: unknown): string | null {
|
||||
return typeof value === 'string' && value.length > 0 && value.length <= 128 ? value : null
|
||||
}
|
||||
|
||||
export function admitWorktreeCatalogResponse<T>(
|
||||
result: unknown,
|
||||
requestedSnapshotId: string | null
|
||||
): WorktreeCatalogAdmission<T> {
|
||||
if (!result || typeof result !== 'object') {
|
||||
return { kind: 'invalid' }
|
||||
}
|
||||
const response = result as {
|
||||
snapshotId?: unknown
|
||||
unchanged?: unknown
|
||||
worktrees?: unknown
|
||||
}
|
||||
if ('unchanged' in response) {
|
||||
const snapshotId = validSnapshotId(response.snapshotId)
|
||||
if (response.unchanged === true && snapshotId && snapshotId === requestedSnapshotId) {
|
||||
return { kind: 'unchanged', snapshotId }
|
||||
}
|
||||
return { kind: 'invalid' }
|
||||
}
|
||||
|
||||
if (!Array.isArray(response.worktrees)) {
|
||||
return { kind: 'invalid' }
|
||||
}
|
||||
return {
|
||||
kind: 'full',
|
||||
snapshotId: validSnapshotId(response.snapshotId),
|
||||
worktrees: response.worktrees as T[]
|
||||
}
|
||||
}
|
||||
|
||||
export class WorktreeCatalogSnapshotClient {
|
||||
private client: RpcClient | null = null
|
||||
private hostId: string | null = null
|
||||
private snapshotId: string | null = null
|
||||
private confirmedWorktrees: Worktree[] | null = null
|
||||
|
||||
async fetch(client: RpcClient, hostId: string): Promise<PendingWorktreeCatalog | null> {
|
||||
if (this.client !== client || this.hostId !== hostId) {
|
||||
this.client = client
|
||||
this.hostId = hostId
|
||||
this.snapshotId = null
|
||||
this.confirmedWorktrees = null
|
||||
}
|
||||
const requestedSnapshotId = this.snapshotId
|
||||
const response = await client.sendRequest('worktree.ps', {
|
||||
limit: 10_000,
|
||||
afterSnapshotId: requestedSnapshotId
|
||||
})
|
||||
if (!response.ok) {
|
||||
return null
|
||||
}
|
||||
return {
|
||||
admission: admitWorktreeCatalogResponse<Worktree>(
|
||||
(response as RpcSuccess).result,
|
||||
requestedSnapshotId
|
||||
),
|
||||
client,
|
||||
hostId
|
||||
}
|
||||
}
|
||||
|
||||
admit(pending: PendingWorktreeCatalog | null): AdmittedWorktreeCatalog | null {
|
||||
if (!pending) {
|
||||
return null
|
||||
}
|
||||
if (
|
||||
pending.client !== this.client ||
|
||||
pending.hostId !== this.hostId ||
|
||||
pending.admission.kind === 'invalid'
|
||||
) {
|
||||
this.snapshotId = null
|
||||
return null
|
||||
}
|
||||
|
||||
const admission = pending.admission
|
||||
this.snapshotId = admission.snapshotId
|
||||
if (admission.kind === 'full') {
|
||||
this.confirmedWorktrees = admission.worktrees
|
||||
}
|
||||
return this.confirmedWorktrees
|
||||
? { changed: admission.kind === 'full', worktrees: this.confirmedWorktrees }
|
||||
: null
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import type { OrcaRuntimeService } from '../../orca-runtime'
|
||||
import { RpcDispatcher } from '../dispatcher'
|
||||
import { WORKTREE_METHODS } from './worktree'
|
||||
|
||||
function makeRuntime() {
|
||||
return {
|
||||
getRuntimeId: () => 'test-runtime',
|
||||
getWorktreePs: vi.fn().mockResolvedValue({
|
||||
worktrees: [],
|
||||
totalCount: 0,
|
||||
truncated: false
|
||||
})
|
||||
} as unknown as OrcaRuntimeService
|
||||
}
|
||||
|
||||
describe('worktree.ps catalog snapshots', () => {
|
||||
it('preserves the exact legacy response when no snapshot field is sent', async () => {
|
||||
const runtime = makeRuntime()
|
||||
const dispatcher = new RpcDispatcher({ runtime, methods: WORKTREE_METHODS })
|
||||
const response = await dispatcher.dispatch({
|
||||
id: 'legacy',
|
||||
authToken: 'token',
|
||||
method: 'worktree.ps',
|
||||
params: { limit: 10_000 }
|
||||
})
|
||||
|
||||
expect(response).toMatchObject({
|
||||
ok: true,
|
||||
result: { worktrees: [], totalCount: 0, truncated: false }
|
||||
})
|
||||
expect((response as { result: unknown }).result).not.toHaveProperty('snapshotId')
|
||||
})
|
||||
|
||||
it('returns a full snapshot followed by a tiny unchanged response', async () => {
|
||||
const runtime = makeRuntime()
|
||||
const dispatcher = new RpcDispatcher({ runtime, methods: WORKTREE_METHODS })
|
||||
const first = await dispatcher.dispatch({
|
||||
id: 'first',
|
||||
authToken: 'token',
|
||||
method: 'worktree.ps',
|
||||
params: { limit: 10_000, afterSnapshotId: null }
|
||||
})
|
||||
const snapshotId = (first as { result: { snapshotId: string } }).result.snapshotId
|
||||
|
||||
const second = await dispatcher.dispatch({
|
||||
id: 'second',
|
||||
authToken: 'token',
|
||||
method: 'worktree.ps',
|
||||
params: { limit: 10_000, afterSnapshotId: snapshotId }
|
||||
})
|
||||
|
||||
expect(snapshotId).toEqual(expect.any(String))
|
||||
expect(second).toMatchObject({
|
||||
ok: true,
|
||||
result: { unchanged: true, snapshotId }
|
||||
})
|
||||
expect(runtime.getWorktreePs).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
})
|
||||
@@ -57,7 +57,8 @@ export const WorktreeTeardownMissingTerminalsParams = WorktreeDetectedListParams
|
||||
})
|
||||
|
||||
export const WorktreePsParams = z.object({
|
||||
limit: OptionalFiniteNumber
|
||||
limit: OptionalFiniteNumber,
|
||||
afterSnapshotId: z.string().min(1).max(128).nullable().optional()
|
||||
})
|
||||
|
||||
export const WorktreeSortOrder = z.object({
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
} from '../../../automations/workspace-provenance'
|
||||
import { buildCliWorkspaceProvenance } from '../../../../shared/cli-workspace-provenance'
|
||||
import { defineMethod, type RpcMethod } from '../core'
|
||||
import { resolveWorktreeCatalogSnapshot } from '../worktree-catalog-snapshot'
|
||||
import { resolveRuntimeNavigationTarget } from '../../../../shared/runtime-navigation'
|
||||
import {
|
||||
WorktreeCreate,
|
||||
@@ -27,7 +28,13 @@ export const WORKTREE_METHODS: RpcMethod[] = [
|
||||
defineMethod({
|
||||
name: 'worktree.ps',
|
||||
params: WorktreePsParams,
|
||||
handler: async (params, { runtime }) => runtime.getWorktreePs(params.limit)
|
||||
handler: async (params, { runtime }) => {
|
||||
const result = await runtime.getWorktreePs(params.limit)
|
||||
if (params.afterSnapshotId === undefined) {
|
||||
return result
|
||||
}
|
||||
return resolveWorktreeCatalogSnapshot(runtime, params.limit, result, params.afterSnapshotId)
|
||||
}
|
||||
}),
|
||||
defineMethod({
|
||||
name: 'worktree.list',
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import type { RuntimeWorktreePsResult } from '../../../shared/runtime-types'
|
||||
import { WorktreeCatalogSnapshotCache } from './worktree-catalog-snapshot'
|
||||
|
||||
function result(totalCount: number): RuntimeWorktreePsResult {
|
||||
return { worktrees: [], totalCount, truncated: false }
|
||||
}
|
||||
|
||||
describe('WorktreeCatalogSnapshotCache', () => {
|
||||
it('returns unchanged only when the caller owns the exact completed snapshot', () => {
|
||||
const snapshots = new WorktreeCatalogSnapshotCache()
|
||||
const first = snapshots.resolve(10_000, result(1), null)
|
||||
expect(first).toMatchObject({ totalCount: 1 })
|
||||
expect('snapshotId' in first).toBe(true)
|
||||
|
||||
const snapshotId = first.snapshotId
|
||||
expect(snapshots.resolve(10_000, result(1), snapshotId)).toEqual({
|
||||
unchanged: true,
|
||||
snapshotId
|
||||
})
|
||||
expect(snapshots.resolve(10_000, result(1), 'unknown')).toEqual({
|
||||
...result(1),
|
||||
snapshotId
|
||||
})
|
||||
})
|
||||
|
||||
it('issues a new snapshot for changed content or a different limit', () => {
|
||||
const snapshots = new WorktreeCatalogSnapshotCache()
|
||||
const first = snapshots.resolve(10_000, result(1), null)
|
||||
const changed = snapshots.resolve(10_000, result(2), first.snapshotId)
|
||||
const differentLimit = snapshots.resolve(200, result(2), changed.snapshotId)
|
||||
|
||||
expect(changed.snapshotId).not.toBe(first.snapshotId)
|
||||
expect(differentLimit.snapshotId).not.toBe(changed.snapshotId)
|
||||
})
|
||||
|
||||
it('repairs completion-order cache replacement with a full response', () => {
|
||||
const snapshots = new WorktreeCatalogSnapshotCache()
|
||||
const older = snapshots.resolve(10_000, result(1), null)
|
||||
const newer = snapshots.resolve(10_000, result(2), older.snapshotId)
|
||||
snapshots.resolve(10_000, result(1), older.snapshotId)
|
||||
|
||||
const repaired = snapshots.resolve(10_000, result(2), newer.snapshotId)
|
||||
expect(repaired).toMatchObject({ totalCount: 2 })
|
||||
expect(repaired).not.toHaveProperty('unchanged')
|
||||
})
|
||||
|
||||
it('scopes snapshot ids to one runtime cache', () => {
|
||||
const left = new WorktreeCatalogSnapshotCache().resolve(10_000, result(1), null)
|
||||
const right = new WorktreeCatalogSnapshotCache().resolve(10_000, result(1), null)
|
||||
|
||||
expect(left.snapshotId).not.toBe(right.snapshotId)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,52 @@
|
||||
import { randomUUID } from 'node:crypto'
|
||||
import { isDeepStrictEqual } from 'node:util'
|
||||
import type {
|
||||
RuntimeWorktreePsConditionalResult,
|
||||
RuntimeWorktreePsResult
|
||||
} from '../../../shared/runtime-types'
|
||||
|
||||
type CachedWorktreeCatalog = {
|
||||
limit: number | undefined
|
||||
result: RuntimeWorktreePsResult
|
||||
snapshotId: string
|
||||
}
|
||||
|
||||
export class WorktreeCatalogSnapshotCache {
|
||||
private readonly runtimeScope = randomUUID()
|
||||
private sequence = 0
|
||||
private cached: CachedWorktreeCatalog | null = null
|
||||
|
||||
resolve(
|
||||
limit: number | undefined,
|
||||
result: RuntimeWorktreePsResult,
|
||||
afterSnapshotId: string | null
|
||||
): RuntimeWorktreePsConditionalResult {
|
||||
const cached = this.cached
|
||||
if (cached !== null && cached.limit === limit && isDeepStrictEqual(cached.result, result)) {
|
||||
if (afterSnapshotId === cached.snapshotId) {
|
||||
return { unchanged: true, snapshotId: cached.snapshotId }
|
||||
}
|
||||
return { ...result, snapshotId: cached.snapshotId }
|
||||
}
|
||||
|
||||
const snapshotId = `${this.runtimeScope}:${++this.sequence}`
|
||||
this.cached = { limit, result, snapshotId }
|
||||
return { ...result, snapshotId }
|
||||
}
|
||||
}
|
||||
|
||||
const snapshotsByRuntime = new WeakMap<object, WorktreeCatalogSnapshotCache>()
|
||||
|
||||
export function resolveWorktreeCatalogSnapshot(
|
||||
runtime: object,
|
||||
limit: number | undefined,
|
||||
result: RuntimeWorktreePsResult,
|
||||
afterSnapshotId: string | null
|
||||
): RuntimeWorktreePsConditionalResult {
|
||||
let snapshots = snapshotsByRuntime.get(runtime)
|
||||
if (!snapshots) {
|
||||
snapshots = new WorktreeCatalogSnapshotCache()
|
||||
snapshotsByRuntime.set(runtime, snapshots)
|
||||
}
|
||||
return snapshots.resolve(limit, result, afterSnapshotId)
|
||||
}
|
||||
@@ -843,6 +843,19 @@ export type RuntimeWorktreePsResult = {
|
||||
truncated: boolean
|
||||
}
|
||||
|
||||
export type RuntimeWorktreePsSnapshotResult = RuntimeWorktreePsResult & {
|
||||
snapshotId: string
|
||||
}
|
||||
|
||||
export type RuntimeWorktreePsUnchangedResult = {
|
||||
unchanged: true
|
||||
snapshotId: string
|
||||
}
|
||||
|
||||
export type RuntimeWorktreePsConditionalResult =
|
||||
| RuntimeWorktreePsSnapshotResult
|
||||
| RuntimeWorktreePsUnchangedResult
|
||||
|
||||
export type RuntimeRepoList = {
|
||||
repos: Repo[]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user