mirror of
https://github.com/stablyai/orca.git
synced 2026-10-20 08:03:28 +00:00
fix(mobile): bound deleted folder session tombstones
This commit is contained in:
@@ -5176,6 +5176,63 @@ describe('Store', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('keeps only the newest deleted-folder tombstones when loading older state', async () => {
|
||||
const initial = await createStore()
|
||||
initial.flush()
|
||||
const state = readDataFile() as PersistedState
|
||||
const workspaceKeys = Array.from({ length: 514 }, (_, index) =>
|
||||
folderWorkspaceKey(`deleted-${index}`)
|
||||
)
|
||||
state.deletedFolderWorkspaceSessionTombstones = Object.fromEntries(
|
||||
workspaceKeys.map((workspaceKey) => [
|
||||
workspaceKey,
|
||||
{ connectionId: null, hostIds: ['local'], tabConnectionIdsByHostId: {} }
|
||||
])
|
||||
) as PersistedState['deletedFolderWorkspaceSessionTombstones']
|
||||
writeDataFile(state)
|
||||
|
||||
vi.useFakeTimers()
|
||||
try {
|
||||
const restored = await createStore()
|
||||
await vi.advanceTimersByTimeAsync(1_000)
|
||||
await restored.waitForPendingWrite()
|
||||
} finally {
|
||||
vi.useRealTimers()
|
||||
}
|
||||
|
||||
const tombstones = (readDataFile() as PersistedState).deletedFolderWorkspaceSessionTombstones
|
||||
expect(Object.keys(tombstones ?? {})).toHaveLength(512)
|
||||
expect(tombstones?.[workspaceKeys[0]!]).toBeUndefined()
|
||||
expect(tombstones?.[workspaceKeys[1]!]).toBeUndefined()
|
||||
expect(tombstones?.[workspaceKeys.at(-1)!]).toBeDefined()
|
||||
})
|
||||
|
||||
it('bounds deleted-folder tombstones during repeated workspace churn', async () => {
|
||||
const store = await createStore()
|
||||
const group = store.createProjectGroup({
|
||||
name: 'Tombstone churn',
|
||||
parentPath: '/workspace/churn',
|
||||
createdFrom: 'folder-scan'
|
||||
})
|
||||
const workspaceKeys: ReturnType<typeof folderWorkspaceKey>[] = []
|
||||
for (let index = 0; index < 514; index += 1) {
|
||||
const workspace = store.createFolderWorkspace({
|
||||
projectGroupId: group.id,
|
||||
name: `Deleted ${index}`
|
||||
})
|
||||
workspaceKeys.push(folderWorkspaceKey(workspace.id))
|
||||
expect(store.removeFolderWorkspace(workspace.id)).toBe(true)
|
||||
}
|
||||
|
||||
store.flush()
|
||||
|
||||
const tombstones = (readDataFile() as PersistedState).deletedFolderWorkspaceSessionTombstones
|
||||
expect(Object.keys(tombstones ?? {})).toHaveLength(512)
|
||||
expect(tombstones?.[workspaceKeys[0]!]).toBeUndefined()
|
||||
expect(tombstones?.[workspaceKeys[1]!]).toBeUndefined()
|
||||
expect(tombstones?.[workspaceKeys.at(-1)!]).toBeDefined()
|
||||
})
|
||||
|
||||
it('backfills folder-scope SSH provenance from unambiguous child repos on load', async () => {
|
||||
writeDataFile({
|
||||
schemaVersion: 1,
|
||||
|
||||
+52
-9
@@ -567,6 +567,7 @@ const WORKSPACE_SESSION_PATCH_OWNER_KEYS = new Set<keyof WorkspaceSessionState>(
|
||||
'terminalSurfaceTombstonesByPaneKey',
|
||||
'activeWorkspaceExecutionHostId'
|
||||
])
|
||||
const MAX_DELETED_FOLDER_WORKSPACE_SESSION_TOMBSTONES = 512
|
||||
|
||||
function logPersistenceStartupMilestone(
|
||||
event: string,
|
||||
@@ -2926,6 +2927,14 @@ function normalizeDeletedFolderWorkspaceSessionTombstones(
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
const workspaceKeys = Object.keys(tombstones)
|
||||
for (const workspaceKey of workspaceKeys.slice(
|
||||
0,
|
||||
Math.max(0, workspaceKeys.length - MAX_DELETED_FOLDER_WORKSPACE_SESSION_TOMBSTONES)
|
||||
)) {
|
||||
delete tombstones[workspaceKey as WorkspaceKey]
|
||||
changed = true
|
||||
}
|
||||
return { tombstones, changed }
|
||||
}
|
||||
|
||||
@@ -4546,14 +4555,12 @@ export class Store {
|
||||
hostIds.add(hostId)
|
||||
}
|
||||
}
|
||||
const tombstones = { ...this.state.deletedFolderWorkspaceSessionTombstones }
|
||||
const prior = tombstones[workspaceKey]
|
||||
tombstones[workspaceKey] = {
|
||||
const prior = this.state.deletedFolderWorkspaceSessionTombstones?.[workspaceKey as WorkspaceKey]
|
||||
this.setDeletedFolderWorkspaceSessionTombstone(workspaceKey, {
|
||||
connectionId: connectionId ?? prior?.connectionId ?? null,
|
||||
hostIds: [...new Set([...(prior?.hostIds ?? []), ...hostIds])],
|
||||
tabConnectionIdsByHostId: prior?.tabConnectionIdsByHostId ?? {}
|
||||
}
|
||||
this.state.deletedFolderWorkspaceSessionTombstones = tombstones
|
||||
})
|
||||
for (const { hostId, session } of partitions) {
|
||||
const tabIds = new Set<string>()
|
||||
const tabPtyIdByTabId = new Map<string, string>()
|
||||
@@ -4683,14 +4690,13 @@ export class Store {
|
||||
owners.set(workspaceKey, connectionId ?? owners.get(workspaceKey) ?? null)
|
||||
ownersByTabId.set(tabId, owners)
|
||||
this.deletedFolderOwnersByHostAndTabId.set(hostId, ownersByTabId)
|
||||
const tombstones = { ...this.state.deletedFolderWorkspaceSessionTombstones }
|
||||
const prior = tombstones[workspaceKey]
|
||||
const prior = this.state.deletedFolderWorkspaceSessionTombstones?.[workspaceKey as WorkspaceKey]
|
||||
const tabConnectionIdsByHostId = { ...prior?.tabConnectionIdsByHostId }
|
||||
tabConnectionIdsByHostId[hostId] = {
|
||||
...tabConnectionIdsByHostId[hostId],
|
||||
[tabId]: connectionId ?? tabConnectionIdsByHostId[hostId]?.[tabId] ?? null
|
||||
}
|
||||
tombstones[workspaceKey] = {
|
||||
this.setDeletedFolderWorkspaceSessionTombstone(workspaceKey, {
|
||||
connectionId: prior?.connectionId ?? connectionId,
|
||||
hostIds: [
|
||||
...new Set([
|
||||
@@ -4700,10 +4706,42 @@ export class Store {
|
||||
])
|
||||
],
|
||||
tabConnectionIdsByHostId
|
||||
})
|
||||
}
|
||||
|
||||
private setDeletedFolderWorkspaceSessionTombstone(
|
||||
workspaceKey: string,
|
||||
tombstone: DeletedFolderWorkspaceSessionTombstone
|
||||
): void {
|
||||
const tombstones = { ...this.state.deletedFolderWorkspaceSessionTombstones }
|
||||
delete tombstones[workspaceKey as WorkspaceKey]
|
||||
tombstones[workspaceKey as WorkspaceKey] = tombstone
|
||||
const workspaceKeys = Object.keys(tombstones)
|
||||
for (const evictedWorkspaceKey of workspaceKeys.slice(
|
||||
0,
|
||||
Math.max(0, workspaceKeys.length - MAX_DELETED_FOLDER_WORKSPACE_SESSION_TOMBSTONES)
|
||||
)) {
|
||||
delete tombstones[evictedWorkspaceKey as WorkspaceKey]
|
||||
this.forgetDeletedFolderWorkspaceSessionTombstone(evictedWorkspaceKey)
|
||||
}
|
||||
this.state.deletedFolderWorkspaceSessionTombstones = tombstones
|
||||
}
|
||||
|
||||
private forgetDeletedFolderWorkspaceSessionTombstone(workspaceKey: string): void {
|
||||
this.deletedFolderConnectionIdByWorkspaceKey.delete(workspaceKey)
|
||||
for (const [hostId, ownersByTabId] of this.deletedFolderOwnersByHostAndTabId) {
|
||||
for (const [tabId, owners] of ownersByTabId) {
|
||||
owners.delete(workspaceKey)
|
||||
if (owners.size === 0) {
|
||||
ownersByTabId.delete(tabId)
|
||||
}
|
||||
}
|
||||
if (ownersByTabId.size === 0) {
|
||||
this.deletedFolderOwnersByHostAndTabId.delete(hostId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private removeSshRemotePtyLeasesForWorkspaceOwners(ownerKeys: ReadonlySet<string>): void {
|
||||
if (ownerKeys.size === 0 || !this.state.sshRemotePtyLeases) {
|
||||
return
|
||||
@@ -4832,12 +4870,17 @@ export class Store {
|
||||
|
||||
private getDeletedFolderConnectionIdsForHost(hostId: ExecutionHostId): Set<string> {
|
||||
const connectionIds = new Set<string>()
|
||||
const liveFolderIds = new Set(
|
||||
(this.state.folderWorkspaces ?? []).map((workspace) => workspace.id)
|
||||
)
|
||||
for (const [workspaceKey, tombstone] of Object.entries(
|
||||
this.state.deletedFolderWorkspaceSessionTombstones ?? {}
|
||||
)) {
|
||||
const scope = parseWorkspaceKey(workspaceKey)
|
||||
if (
|
||||
!tombstone ||
|
||||
!this.isDeletedFolderWorkspaceKey(workspaceKey) ||
|
||||
scope?.type !== 'folder' ||
|
||||
liveFolderIds.has(scope.folderWorkspaceId) ||
|
||||
!tombstone.hostIds.includes(hostId)
|
||||
) {
|
||||
continue
|
||||
|
||||
Reference in New Issue
Block a user