perf(mobile): avoid unchanged worktree catalog payloads (#11735)

* perf(mobile): avoid unchanged worktree catalog payloads

* fix(mobile): isolate catalog snapshots by limit

* review: reassert host truth on unchanged polls; content-address snapshots

Client — the `changed` gate meant an unchanged poll skipped setWorktrees /
setLastKnownWorktrees / setCachedWorktrees, so optimistic local edits
(togglePin, handleDeleteWorktree's failure re-add) and the #8498 cache guard
were no longer repaired while the host catalog was stable. The gate bought
nothing: setCachedWorktrees is an in-memory Map write and areWorktreeListsEqual
already ran every poll, so the steady state still short-circuits on array
identity. All wire savings are unaffected. admit() now just returns the
confirmed rows and HostScreen applies them exactly as it did pre-PR.

Also on the client:
- a stale response from a superseded client/host no longer clears the token the
  current client/host just established
- discriminate on `worktrees` rather than on `'unchanged' in response`, so a
  future catalog field named `unchanged` can't reclassify a full response
- useRef over useMemo for the snapshot client; React may discard memoized values
- hoist WORKTREE_PS_FULL_LIMIT so the truncates-at-200 rationale travels with it

Host — replace the per-limit snapshot cache with a content-addressed id (ETag
semantics). Ownership lives in the id, so concurrent clients, differing limits,
and runtime restarts are correct by construction; this drops the LRU, the
eviction policy, the per-runtime WeakMap, and the retention of up to 8 full
catalogs. The remaining cache is a pure memo: because ids derive from content,
dropping or thrashing it costs CPU and nothing else. Keeping the memo also
keeps the measured steady-state cost — hashing every poll instead measured
2.24ms vs 0.75ms for the compare on a 310KB catalog.

Verified: mobile 2784 passed / 3 skipped, src/main/runtime/rpc 1064 passed,
node + mobile typechecks, oxlint, oxfmt, max-lines ratchet.

* fix(runtime): isolate catalog snapshot memo

---------

Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
This commit is contained in:
Neil
2026-07-31 23:12:13 -07:00
committed by GitHub
co-authored by Brennan Benson
parent 4c03cdff72
commit 6e7ceafd07
10 changed files with 523 additions and 16 deletions
+15 -12
View File
@@ -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,9 @@ export function HostScreen({
const lastConnectedAt = useLastConnectedAt(hostId)
const clientRef = useRef<RpcClient | null>(null)
const fetchWorktreesInFlightRef = useRef(false)
// Why: useRef, not useMemo — React may discard memoized values, which would silently
// reset the snapshot token this object exists to own.
const worktreeCatalogRef = useRef(new WorktreeCatalogSnapshotClient())
const fetchRepoMetadataInFlightRef = useRef(new WeakSet<RpcClient>())
const fetchRepoMetadataPendingRef = useRef(new WeakSet<RpcClient>())
const repoMetadataFetchedAtRef = useRef(0)
@@ -423,31 +427,32 @@ 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 worktreeCatalogRef.current.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: unchanged responses still yield the confirmed rows, so every poll reasserts
// host truth over optimistic local edits regardless of payload size.
const confirmed = worktreeCatalogRef.current.admit(pendingCatalog)
if (confirmed) {
// 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
areWorktreeListsEqual(current, confirmed) ? current : confirmed
)
setLastKnownWorktrees((current) =>
areWorktreeListsEqual(current, result.worktrees) ? current : result.worktrees
areWorktreeListsEqual(current, confirmed) ? current : confirmed
)
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)
setCachedWorktrees(hostId, confirmed)
}
// Drop the optimistic active override once the host reports it active, so later desktop changes win.
setOptimisticActiveWorktreeId((pending) =>
pending && result.worktrees.some((w) => w.worktreeId === pending && w.isActive)
pending && confirmed.some((w) => w.worktreeId === pending && w.isActive)
? null
: pending
)
@@ -459,7 +464,7 @@ export function HostScreen({
}
const still = new Set<string>()
for (const id of prev) {
const wt = result.worktrees.find((w) => w.worktreeId === id)
const wt = confirmed.find((w) => w.worktreeId === id)
if (wt && wt.liveTerminalCount > 0) {
still.add(id)
}
@@ -468,9 +473,7 @@ export function HostScreen({
})
// Sync pin state from server so desktop-initiated pins reflect without relying on stale AsyncStorage.
const serverPinned = new Set(
result.worktrees.filter((w) => w.isPinned).map((w) => w.worktreeId)
)
const serverPinned = new Set(confirmed.filter((w) => w.isPinned).map((w) => w.worktreeId))
setPinnedIds((prev) => {
if (serverPinned.size === prev.size && [...serverPinned].every((id) => prev.has(id))) {
return prev
+2 -2
View File
@@ -19,6 +19,7 @@ import { loadHosts } from '../src/transport/host-store'
import { navigateToMobileHostEdit } from '../src/transport/host-edit-navigation'
import { removeHostAndCloseClient } from '../src/transport/host-removal-lifecycle'
import { pickResumeWorktree } from '../src/worktree/resume-worktree'
import { WORKTREE_PS_FULL_LIMIT } from '../src/worktree/worktree-catalog-snapshot-client'
import type { RpcClient } from '../src/transport/rpc-client'
import { sendSingleFlightRequest } from '../src/transport/request-single-flight'
import {
@@ -178,8 +179,7 @@ function fetchWorktreeInfo(
})
}
// Why: worktree.ps defaults to 200 and silently truncates; request all so counts are accurate.
sendSingleFlightRequest(client, hostId, 'worktree.ps', { limit: 10000 })
sendSingleFlightRequest(client, hostId, 'worktree.ps', { limit: WORKTREE_PS_FULL_LIMIT })
.then((response) => {
if (disposed()) {
return
@@ -0,0 +1,182 @@
import { describe, expect, it, vi } from 'vitest'
import type { RpcClient } from '../transport/rpc-client'
import {
admitWorktreeCatalogResponse,
WORKTREE_PS_FULL_LIMIT,
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('classifies by rows, so a future `unchanged` catalog field cannot hide a full response', () => {
expect(
admitWorktreeCatalogResponse(
{ worktrees: [{ id: 'worktree-1' }], unchanged: false, snapshotId: 'snapshot-1' },
'snapshot-1'
)
).toEqual({
kind: 'full',
snapshotId: 'snapshot-1',
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 }, 'snapshot-1')).toEqual({
kind: 'invalid'
})
})
it('accepts a full response but ignores an out-of-bounds snapshot id', () => {
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('returns the confirmed rows on unchanged responses so callers can reassert them', async () => {
const rows = [{ worktreeId: 'worktree-1' }]
const client = clientWithResults(
{ worktrees: rows, 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).toEqual(rows)
expect(second).toEqual(rows)
})
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: WORKTREE_PS_FULL_LIMIT,
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: WORKTREE_PS_FULL_LIMIT,
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: WORKTREE_PS_FULL_LIMIT,
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: WORKTREE_PS_FULL_LIMIT,
afterSnapshotId: null
})
})
it('drops a superseded host response without invalidating the current token', async () => {
const firstClient = clientWithResults({ worktrees: [], snapshotId: 'snapshot-1' })
const secondClient = clientWithResults({ worktrees: [], snapshotId: 'snapshot-2' })
const snapshots = new WorktreeCatalogSnapshotClient()
// Host A's response is still in flight when the screen switches to host B.
const stale = await snapshots.fetch(firstClient, 'host-1')
snapshots.admit(await snapshots.fetch(secondClient, 'host-2'))
expect(snapshots.admit(stale)).toBeNull()
await snapshots.fetch(secondClient, 'host-2')
expect(secondClient.sendRequest).toHaveBeenNthCalledWith(2, 'worktree.ps', {
limit: WORKTREE_PS_FULL_LIMIT,
afterSnapshotId: 'snapshot-2'
})
})
})
@@ -0,0 +1,107 @@
import type { RpcClient } from '../transport/rpc-client'
import type { RpcSuccess } from '../transport/types'
import type { Worktree } from './workspace-list-sections'
// Why: worktree.ps silently truncates at 200; use a high cap so large hosts don't drop workspaces.
export const WORKTREE_PS_FULL_LIMIT = 10_000
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
}
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
}
// Why: a full response is defined by carrying rows, so discriminate on that rather
// than on the presence of `unchanged` — the latter could collide with a future
// catalog field and silently reclassify a full response.
if (Array.isArray(response.worktrees)) {
return {
kind: 'full',
snapshotId: validSnapshotId(response.snapshotId),
worktrees: response.worktrees as T[]
}
}
const snapshotId = validSnapshotId(response.snapshotId)
if (response.unchanged === true && snapshotId && snapshotId === requestedSnapshotId) {
return { kind: 'unchanged', snapshotId }
}
return { kind: 'invalid' }
}
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: WORKTREE_PS_FULL_LIMIT,
afterSnapshotId: requestedSnapshotId
})
if (!response.ok) {
return null
}
return {
admission: admitWorktreeCatalogResponse<Worktree>(
(response as RpcSuccess).result,
requestedSnapshotId
),
client,
hostId
}
}
/** The confirmed host catalog to apply, or null when there is nothing to apply. */
admit(pending: PendingWorktreeCatalog | null): Worktree[] | null {
if (!pending) {
return null
}
// Why: a response from a superseded client/host is stale, not wrong — dropping it
// must not invalidate the token the current client/host just established.
if (pending.client !== this.client || pending.hostId !== this.hostId) {
return null
}
if (pending.admission.kind === 'invalid') {
this.snapshotId = null
return null
}
this.snapshotId = pending.admission.snapshotId
if (pending.admission.kind === 'full') {
this.confirmedWorktrees = pending.admission.worktrees
}
// Why: unchanged responses still return the confirmed rows so every poll reasserts
// host truth over optimistic local edits, exactly as the full-payload path did.
return this.confirmedWorktrees
}
}
@@ -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({
+9 -1
View File
@@ -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,14 @@ 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)
// Why: callers that never send the field get the byte-exact legacy response.
if (params.afterSnapshotId === undefined) {
return result
}
return resolveWorktreeCatalogSnapshot(result, params.afterSnapshotId)
}
}),
defineMethod({
name: 'worktree.list',
@@ -0,0 +1,83 @@
import { describe, expect, it } from 'vitest'
import type { RuntimeWorktreePsResult } from '../../../shared/runtime-types'
import { resolveWorktreeCatalogSnapshot } from './worktree-catalog-snapshot'
function result(totalCount: number): RuntimeWorktreePsResult {
return { worktrees: [], totalCount, truncated: false }
}
describe('resolveWorktreeCatalogSnapshot', () => {
it('returns unchanged only when the caller owns the exact current snapshot', () => {
const first = resolveWorktreeCatalogSnapshot(result(1), null)
expect(first).toMatchObject({ totalCount: 1 })
const snapshotId = first.snapshotId
expect(resolveWorktreeCatalogSnapshot(result(1), snapshotId)).toEqual({
unchanged: true,
snapshotId
})
expect(resolveWorktreeCatalogSnapshot(result(1), 'unknown')).toEqual({
...result(1),
snapshotId
})
})
it('issues a new snapshot id for changed content', () => {
const first = resolveWorktreeCatalogSnapshot(result(1), null)
const changed = resolveWorktreeCatalogSnapshot(result(2), first.snapshotId)
expect(changed.snapshotId).not.toBe(first.snapshotId)
expect(changed).not.toHaveProperty('unchanged')
})
it('keeps concurrent callers independent without server-held state', () => {
// Two clients on different catalogs interleave; neither can displace the other.
const wide = resolveWorktreeCatalogSnapshot(result(2), null)
const narrow = resolveWorktreeCatalogSnapshot(result(1), null)
expect(resolveWorktreeCatalogSnapshot(result(2), wide.snapshotId)).toEqual({
unchanged: true,
snapshotId: wide.snapshotId
})
expect(resolveWorktreeCatalogSnapshot(result(1), narrow.snapshotId)).toEqual({
unchanged: true,
snapshotId: narrow.snapshotId
})
})
it('repairs a caller holding a superseded snapshot with a full response', () => {
const older = resolveWorktreeCatalogSnapshot(result(1), null)
const repaired = resolveWorktreeCatalogSnapshot(result(2), older.snapshotId)
expect(repaired).toMatchObject({ totalCount: 2 })
expect(repaired).not.toHaveProperty('unchanged')
expect(repaired.snapshotId).not.toBe(older.snapshotId)
})
it('derives ids from content alone, so memo state cannot change the answer', () => {
// Identical catalogs must yield identical ids whether the memo is warm or was just
// displaced — that is what makes the memo droppable and restarts safe.
const warm = resolveWorktreeCatalogSnapshot(result(1), null).snapshotId
resolveWorktreeCatalogSnapshot(result(99), null)
expect(resolveWorktreeCatalogSnapshot(result(1), null).snapshotId).toBe(warm)
})
it('isolates the memo from mutation of a previously resolved catalog', () => {
const mutable = result(1)
const first = resolveWorktreeCatalogSnapshot(mutable, null)
mutable.totalCount = 2
const changed = resolveWorktreeCatalogSnapshot(mutable, first.snapshotId)
expect(changed).toMatchObject({ totalCount: 2 })
expect(changed).not.toHaveProperty('unchanged')
expect(changed.snapshotId).not.toBe(first.snapshotId)
})
it('produces ids within the request schema bound', () => {
const { snapshotId } = resolveWorktreeCatalogSnapshot(result(1), null)
expect(snapshotId.length).toBeGreaterThan(0)
expect(snapshotId.length).toBeLessThanOrEqual(128)
})
})
@@ -0,0 +1,50 @@
import { createHash } from 'node:crypto'
import { isDeepStrictEqual } from 'node:util'
import type {
RuntimeWorktreePsConditionalResult,
RuntimeWorktreePsResult
} from '../../../shared/runtime-types'
// Why: 192 bits of a sha256 over the serialized catalog. Accidental collision is
// impossible at any realistic poll rate, and this is not an auth boundary.
const SNAPSHOT_ID_LENGTH = 32
type MemoizedCatalogId = {
result: RuntimeWorktreePsResult
snapshotId: string
}
// Why: a pure memo over the most recent catalog. Because ids are derived from content
// rather than from cache state, dropping or thrashing this slot costs CPU and nothing
// else — it can never produce a wrong answer, so it needs no scoping, eviction policy,
// or per-caller isolation. Serializing every poll instead would cost ~3x the compare.
let memoizedId: MemoizedCatalogId | null = null
function worktreeCatalogSnapshotId(result: RuntimeWorktreePsResult): string {
if (memoizedId && isDeepStrictEqual(memoizedId.result, result)) {
return memoizedId.snapshotId
}
const serialized = JSON.stringify(result)
const snapshotId = createHash('sha256')
.update(serialized)
.digest('base64url')
.slice(0, SNAPSHOT_ID_LENGTH)
// Why: caller-owned mutation must not let an old id label new catalog content.
memoizedId = { result: JSON.parse(serialized) as RuntimeWorktreePsResult, snapshotId }
return snapshotId
}
// Why: content-addressed like an HTTP ETag, so snapshot ownership lives entirely in the
// id. Any number of concurrent clients, any `limit`, and any runtime restart stay correct
// by construction: ids match only when the catalogs are byte-identical, which is exactly
// when `unchanged` is the right answer.
export function resolveWorktreeCatalogSnapshot(
result: RuntimeWorktreePsResult,
afterSnapshotId: string | null
): RuntimeWorktreePsConditionalResult {
const snapshotId = worktreeCatalogSnapshotId(result)
if (afterSnapshotId === snapshotId) {
return { unchanged: true, snapshotId }
}
return { ...result, snapshotId }
}
+13
View File
@@ -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[]
}