mirror of
https://github.com/stablyai/orca.git
synced 2026-10-30 08:37:49 +00:00
perf(renderer): scope runtime connection and parking subscriptions (#32199)
This commit is contained in:
@@ -14,12 +14,12 @@ import es from '../../i18n/locales/es.json'
|
||||
import ja from '../../i18n/locales/ja.json'
|
||||
import ko from '../../i18n/locales/ko.json'
|
||||
import zh from '../../i18n/locales/zh.json'
|
||||
import { useAppStore } from '@/store'
|
||||
import type { RuntimeEnvironmentStatus } from '../../../../shared/runtime-host-status'
|
||||
import { createCompatibleRuntimeStatusResponse } from '@/runtime/runtime-compatibility-test-fixture'
|
||||
|
||||
const runtimeStatusByEnvironmentId = new Map<string, { status?: unknown }>()
|
||||
|
||||
vi.mock('@/store', () => ({
|
||||
useAppStore: (selector: (state: unknown) => unknown) => selector({ runtimeStatusByEnvironmentId })
|
||||
}))
|
||||
const original = useAppStore.getState()
|
||||
let runtimeStatusByEnvironmentId = new Map<string, RuntimeEnvironmentStatus>()
|
||||
|
||||
vi.mock('@/components/ui/tooltip', () => ({
|
||||
Tooltip: ({ children }: { children: ReactNode }) => <>{children}</>,
|
||||
@@ -56,12 +56,14 @@ async function render(
|
||||
describe('NoticeHostGlyph', () => {
|
||||
beforeEach(() => {
|
||||
globalThis.IS_REACT_ACT_ENVIRONMENT = true
|
||||
runtimeStatusByEnvironmentId.clear()
|
||||
runtimeStatusByEnvironmentId = new Map()
|
||||
useAppStore.setState({ runtimeStatusByEnvironmentId })
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
roots.splice(0).forEach((root) => act(() => root.unmount()))
|
||||
document.body.replaceChildren()
|
||||
useAppStore.setState(original, true)
|
||||
})
|
||||
|
||||
it('names the SSH host it would act on', async () => {
|
||||
@@ -74,7 +76,11 @@ describe('NoticeHostGlyph', () => {
|
||||
})
|
||||
|
||||
it('names the paired runtime separately from the SSH host of the same name', async () => {
|
||||
runtimeStatusByEnvironmentId.set('openclaw-env', { status: 'ready' })
|
||||
const response = createCompatibleRuntimeStatusResponse()
|
||||
if (!response.ok) {
|
||||
throw new Error('expected a compatible status')
|
||||
}
|
||||
runtimeStatusByEnvironmentId.set('openclaw-env', { status: response.result, checkedAt: 1 })
|
||||
const container = await render('runtime:openclaw-env')
|
||||
|
||||
expect(container.querySelector('[data-notice-host-kind="runtime"]')).not.toBeNull()
|
||||
@@ -84,7 +90,7 @@ describe('NoticeHostGlyph', () => {
|
||||
})
|
||||
|
||||
it('marks a paired runtime a probe found unreachable as disconnected', async () => {
|
||||
runtimeStatusByEnvironmentId.set('openclaw-env', { status: null })
|
||||
runtimeStatusByEnvironmentId.set('openclaw-env', { status: null, checkedAt: 1 })
|
||||
const container = await render('runtime:openclaw-env')
|
||||
|
||||
expect(container.querySelector('[data-testid="tooltip"]')?.textContent).toBe(
|
||||
|
||||
@@ -2,13 +2,9 @@ import React from 'react'
|
||||
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
|
||||
import { HostRowIcon } from '../host-row-icon'
|
||||
import { useAppStore } from '@/store'
|
||||
import { useRuntimeHostDisconnected } from '@/store/use-runtime-status-view'
|
||||
import { parseExecutionHostId, type ExecutionHostId } from '../../../../shared/execution-host'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import {
|
||||
isDisconnectedRuntimeHostState,
|
||||
runtimeHostConnectionStateForEntry
|
||||
} from '@/runtime/runtime-host-connection-state'
|
||||
|
||||
type NoticeHostGlyphProps = {
|
||||
hostId: ExecutionHostId
|
||||
@@ -32,14 +28,9 @@ export default function NoticeHostGlyph({
|
||||
const host = parseExecutionHostId(hostId)
|
||||
// Why the shared derivation, not raw truthiness: an absent entry means "not probed yet",
|
||||
// which is not the same verdict as a probe that came back unreachable.
|
||||
const isDisconnected = useAppStore((s) => {
|
||||
if (host?.kind !== 'runtime') {
|
||||
return false
|
||||
}
|
||||
return isDisconnectedRuntimeHostState(
|
||||
runtimeHostConnectionStateForEntry(s.runtimeStatusByEnvironmentId.get(host.environmentId))
|
||||
)
|
||||
})
|
||||
const isDisconnected = useRuntimeHostDisconnected(
|
||||
host?.kind === 'runtime' ? host.environmentId : null
|
||||
)
|
||||
|
||||
if (!host) {
|
||||
return null
|
||||
|
||||
@@ -11,10 +11,7 @@ import { getHostDisplayLabelOverrides } from '../../../../shared/host-setting-ov
|
||||
import { getWorkspacePortsByWorktreeId } from '@/lib/workspace-port-groups'
|
||||
import { runtimeEnvironmentIdUnlessTwin } from '@/lib/resolve-owner'
|
||||
import { hydrateRuntimeEnvironmentSshState } from '@/runtime/runtime-environment-ssh-state'
|
||||
import {
|
||||
isDisconnectedRuntimeHostState,
|
||||
runtimeHostConnectionStateForEntry
|
||||
} from '@/runtime/runtime-host-connection-state'
|
||||
import { useRuntimeHostDisconnected } from '@/store/use-runtime-status-view'
|
||||
import { useAppStore } from '@/store'
|
||||
import {
|
||||
selectRuntimeAwareSshStatus,
|
||||
@@ -207,16 +204,7 @@ export function useWorktreeCardFoundation({
|
||||
: null
|
||||
// Why the shared derivation, not raw truthiness: an absent entry means "not probed yet",
|
||||
// which is not the same verdict as a probe that came back unreachable.
|
||||
const isRuntimeDisconnected = useAppStore((s) => {
|
||||
if (!runtimeOwnerEnvironmentId) {
|
||||
return false
|
||||
}
|
||||
return isDisconnectedRuntimeHostState(
|
||||
runtimeHostConnectionStateForEntry(
|
||||
s.runtimeStatusByEnvironmentId.get(runtimeOwnerEnvironmentId)
|
||||
)
|
||||
)
|
||||
})
|
||||
const isRuntimeDisconnected = useRuntimeHostDisconnected(runtimeOwnerEnvironmentId || null)
|
||||
const [titleRenaming, setTitleRenaming] = useState(false)
|
||||
const [showRenameErrorDialog, setShowRenameErrorDialog] = useState(false)
|
||||
// Why: read the target label from its owning host's store instead of exposing HUB-private SSH metadata as client-local state.
|
||||
|
||||
@@ -25,8 +25,6 @@ type PairedRuntimeParkingEnvironmentIdsCache = {
|
||||
environmentIds: ReadonlySet<string>
|
||||
}
|
||||
|
||||
let pairedRuntimeParkingEnvironmentIdsCache: PairedRuntimeParkingEnvironmentIdsCache | null = null
|
||||
|
||||
function haveSameEnvironmentIds(left: ReadonlySet<string>, right: ReadonlySet<string>): boolean {
|
||||
if (left.size !== right.size) {
|
||||
return false
|
||||
@@ -39,34 +37,43 @@ function haveSameEnvironmentIds(left: ReadonlySet<string>, right: ReadonlySet<st
|
||||
return true
|
||||
}
|
||||
|
||||
export function createPairedRuntimeParkingEnvironmentIdsSelector() {
|
||||
let cache: PairedRuntimeParkingEnvironmentIdsCache | null = null
|
||||
return (statuses: PairedRuntimeParkingCapabilityStatuses): ReadonlySet<string> => {
|
||||
const cached = cache
|
||||
if (cached?.statuses === statuses) {
|
||||
return cached.environmentIds
|
||||
}
|
||||
|
||||
const capable = new Set<string>()
|
||||
for (const [environmentId, entry] of statuses) {
|
||||
// Why last-verified: a capability is a fact about the host's build, so an unverifiable
|
||||
// probe must not unpark its terminals. See docs/reference/ssh-execution-boundary.md.
|
||||
// Why the revoked gate: parking discards the client's only copy of the scrollback against
|
||||
// a host-side restore, so a host that refused us must not keep promising one.
|
||||
if (isRuntimeHostContactRevoked(entry)) {
|
||||
continue
|
||||
}
|
||||
const status = lastVerifiedRuntimeStatus<PairedRuntimeParkingCapability>(entry)
|
||||
if (status?.capabilities?.includes(TERMINAL_PAIRED_PARKING_RUNTIME_CAPABILITY)) {
|
||||
capable.add(environmentId)
|
||||
}
|
||||
}
|
||||
const environmentIds =
|
||||
cached && haveSameEnvironmentIds(cached.environmentIds, capable)
|
||||
? cached.environmentIds
|
||||
: capable
|
||||
cache = { statuses, environmentIds }
|
||||
return environmentIds
|
||||
}
|
||||
}
|
||||
|
||||
let selectParkingEnvironmentIds = createPairedRuntimeParkingEnvironmentIdsSelector()
|
||||
|
||||
export function selectPairedRuntimeParkingEnvironmentIds(
|
||||
statuses: PairedRuntimeParkingCapabilityStatuses
|
||||
): ReadonlySet<string> {
|
||||
const cached = pairedRuntimeParkingEnvironmentIdsCache
|
||||
if (cached?.statuses === statuses) {
|
||||
return cached.environmentIds
|
||||
}
|
||||
|
||||
const capable = new Set<string>()
|
||||
for (const [environmentId, entry] of statuses) {
|
||||
// Why last-verified: a capability is a fact about the host's build, so an unverifiable
|
||||
// probe must not unpark its terminals. See docs/reference/ssh-execution-boundary.md.
|
||||
// Why the revoked gate: parking discards the client's only copy of the scrollback against
|
||||
// a host-side restore, so a host that refused us must not keep promising one.
|
||||
if (isRuntimeHostContactRevoked(entry)) {
|
||||
continue
|
||||
}
|
||||
const status = lastVerifiedRuntimeStatus<PairedRuntimeParkingCapability>(entry)
|
||||
if (status?.capabilities?.includes(TERMINAL_PAIRED_PARKING_RUNTIME_CAPABILITY)) {
|
||||
capable.add(environmentId)
|
||||
}
|
||||
}
|
||||
const environmentIds =
|
||||
cached && haveSameEnvironmentIds(cached.environmentIds, capable)
|
||||
? cached.environmentIds
|
||||
: capable
|
||||
pairedRuntimeParkingEnvironmentIdsCache = { statuses, environmentIds }
|
||||
return environmentIds
|
||||
return selectParkingEnvironmentIds(statuses)
|
||||
}
|
||||
|
||||
export function selectPairedRuntimeParkingEnvironmentIdsFromState(state: {
|
||||
@@ -76,5 +83,5 @@ export function selectPairedRuntimeParkingEnvironmentIdsFromState(state: {
|
||||
}
|
||||
|
||||
export function resetPairedRuntimeParkingEnvironmentIdsCacheForTest(): void {
|
||||
pairedRuntimeParkingEnvironmentIdsCache = null
|
||||
selectParkingEnvironmentIds = createPairedRuntimeParkingEnvironmentIdsSelector()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
// @vitest-environment happy-dom
|
||||
import { act } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import { afterEach, expect, it } from 'vitest'
|
||||
import { useAppStore } from '@/store'
|
||||
import { useTerminalWorkspaceFoundation } from './use-terminal-workspace-foundation'
|
||||
import type { RuntimeHostStatusSnapshot } from '../../../shared/runtime-host-status'
|
||||
import { TERMINAL_PAIRED_PARKING_RUNTIME_CAPABILITY } from '../../../shared/protocol-version'
|
||||
|
||||
Reflect.set(globalThis, 'IS_REACT_ACT_ENVIRONMENT', true)
|
||||
const original = useAppStore.getState()
|
||||
const container = document.createElement('div')
|
||||
const root = createRoot(container)
|
||||
|
||||
afterEach(() => {
|
||||
act(() => root.unmount())
|
||||
useAppStore.setState(original, true)
|
||||
})
|
||||
|
||||
it('does not render the workspace root for fresh evidence with unchanged parking', () => {
|
||||
let renders = 0
|
||||
function Probe() {
|
||||
const { pairedRuntimeParkingEnvironmentIds } = useTerminalWorkspaceFoundation()
|
||||
renders += 1
|
||||
return <output>{[...pairedRuntimeParkingEnvironmentIds].join(',')}</output>
|
||||
}
|
||||
act(() => root.render(<Probe />))
|
||||
const mountedRenders = renders
|
||||
for (let checkedAt = 1; checkedAt <= 20; checkedAt += 1) {
|
||||
const snapshot: RuntimeHostStatusSnapshot = {
|
||||
environmentId: 'env-a',
|
||||
pairingRevision: 1,
|
||||
sequence: checkedAt,
|
||||
checkedAt,
|
||||
status: null,
|
||||
verification: 'unavailable',
|
||||
transport: 'ready'
|
||||
}
|
||||
act(() =>
|
||||
useAppStore.setState({
|
||||
runtimeStatusByEnvironmentId: new Map([['env-a', { status: null, checkedAt, snapshot }]])
|
||||
})
|
||||
)
|
||||
}
|
||||
expect(renders - mountedRenders).toBe(0)
|
||||
const snapshot: RuntimeHostStatusSnapshot = {
|
||||
environmentId: 'env-a',
|
||||
pairingRevision: 1,
|
||||
sequence: 21,
|
||||
checkedAt: 21,
|
||||
verification: 'verified',
|
||||
transport: 'ready',
|
||||
status: {
|
||||
runtimeId: 'a',
|
||||
rendererGraphEpoch: 1,
|
||||
graphStatus: 'ready',
|
||||
authoritativeWindowId: 1,
|
||||
liveTabCount: 0,
|
||||
liveLeafCount: 0,
|
||||
capabilities: [TERMINAL_PAIRED_PARKING_RUNTIME_CAPABILITY]
|
||||
}
|
||||
}
|
||||
act(() =>
|
||||
useAppStore.setState({
|
||||
runtimeStatusByEnvironmentId: new Map([
|
||||
['env-a', { status: snapshot.status, snapshot, checkedAt: 21 }]
|
||||
])
|
||||
})
|
||||
)
|
||||
expect(container.textContent).toBe('env-a')
|
||||
expect(renders - mountedRenders).toBe(1)
|
||||
act(() => useAppStore.getState().clearRuntimeEnvironmentStatus('env-a'))
|
||||
expect(container.textContent).toBe('')
|
||||
expect(renders - mountedRenders).toBe(2)
|
||||
})
|
||||
@@ -7,7 +7,7 @@ import { getResolvedExecutionHostIdForWorktree } from '@/lib/resolved-worktree-e
|
||||
import type { WorktreeTabBucketProjection } from '@/lib/worktree-tab-bucket-projection'
|
||||
import { projectWorkspaceSurfaces } from './workspace-surface-projection'
|
||||
import { useReusedArrayIdentity } from './sidebar/worktree-list/listing/use-reused-array-identity'
|
||||
import { selectPairedRuntimeParkingEnvironmentIds } from './terminal-pane/terminal-hidden-view-parking'
|
||||
import { usePairedRuntimeParkingEnvironmentIds } from '@/store/use-runtime-status-view'
|
||||
import { createTerminalWorktreeTopologyProjection } from './terminal-pane/terminal-hidden-worktree-retention'
|
||||
import { isMainTerminalSideEffectAuthorityForPty } from './terminal-pane/terminal-side-effect-facts-handler'
|
||||
import { selectFolderWorkspaces } from '@/store/workspace-record-selectors'
|
||||
@@ -77,11 +77,7 @@ export function useTerminalWorkspaceFoundation() {
|
||||
const terminalSshParkingEnabled = useAppStore(
|
||||
(state) => state.settings?.terminalSshViewParking !== false
|
||||
)
|
||||
const runtimeStatusByEnvironmentId = useAppStore((state) => state.runtimeStatusByEnvironmentId)
|
||||
const pairedRuntimeParkingEnvironmentIds = useMemo(
|
||||
() => selectPairedRuntimeParkingEnvironmentIds(runtimeStatusByEnvironmentId),
|
||||
[runtimeStatusByEnvironmentId]
|
||||
)
|
||||
const pairedRuntimeParkingEnvironmentIds = usePairedRuntimeParkingEnvironmentIds()
|
||||
const terminalRetentionBudgetEnabled = useAppStore(
|
||||
(state) => state.settings?.terminalHiddenWorktreeRetentionBudget !== false
|
||||
)
|
||||
@@ -115,7 +111,6 @@ export function useTerminalWorkspaceFoundation() {
|
||||
pendingStartupByTabId,
|
||||
terminalParkingEnabled,
|
||||
terminalSshParkingEnabled,
|
||||
runtimeStatusByEnvironmentId,
|
||||
pairedRuntimeParkingEnvironmentIds,
|
||||
terminalRetentionBudgetEnabled,
|
||||
browserGuestRetentionBudgetEnabled,
|
||||
|
||||
@@ -0,0 +1,204 @@
|
||||
import { createStore } from 'zustand/vanilla'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import type {
|
||||
RuntimeEnvironmentStatus,
|
||||
RuntimeHostStatusSnapshot
|
||||
} from '../../../shared/runtime-host-status'
|
||||
import { TERMINAL_PAIRED_PARKING_RUNTIME_CAPABILITY } from '../../../shared/protocol-version'
|
||||
import * as connectionState from '@/runtime/runtime-host-connection-state'
|
||||
import { createRuntimeStatusSubscriptions } from './runtime-status-subscriptions'
|
||||
|
||||
function entry(patch: Partial<RuntimeHostStatusSnapshot> = {}): RuntimeEnvironmentStatus {
|
||||
const snapshot: RuntimeHostStatusSnapshot = {
|
||||
environmentId: 'a',
|
||||
pairingRevision: 1,
|
||||
sequence: 1,
|
||||
checkedAt: 1,
|
||||
status: {
|
||||
runtimeId: 'runtime-a',
|
||||
rendererGraphEpoch: 1,
|
||||
graphStatus: 'ready',
|
||||
authoritativeWindowId: 1,
|
||||
liveTabCount: 0,
|
||||
liveLeafCount: 0,
|
||||
capabilities: [TERMINAL_PAIRED_PARKING_RUNTIME_CAPABILITY]
|
||||
},
|
||||
verification: 'verified',
|
||||
transport: 'ready',
|
||||
...patch
|
||||
}
|
||||
return {
|
||||
snapshot,
|
||||
status: snapshot.verification === 'verified' ? snapshot.status : null,
|
||||
checkedAt: snapshot.checkedAt
|
||||
}
|
||||
}
|
||||
|
||||
function setup() {
|
||||
const store = createStore(() => ({
|
||||
runtimeStatusByEnvironmentId: new Map<string, RuntimeEnvironmentStatus>([
|
||||
['a', entry()],
|
||||
['b', entry({ environmentId: 'b' })]
|
||||
]),
|
||||
unrelated: 0
|
||||
}))
|
||||
const subscriptions = createRuntimeStatusSubscriptions(store)
|
||||
function publish(environmentId: string, value: RuntimeEnvironmentStatus) {
|
||||
store.setState({
|
||||
runtimeStatusByEnvironmentId: new Map(store.getState().runtimeStatusByEnvironmentId).set(
|
||||
environmentId,
|
||||
value
|
||||
)
|
||||
})
|
||||
}
|
||||
return { store, subscriptions, publish }
|
||||
}
|
||||
|
||||
afterEach(() => vi.restoreAllMocks())
|
||||
|
||||
describe('runtime status notification isolation', () => {
|
||||
it.each([100, 1000])(
|
||||
'derives once per affected host with %i readers and skips unrelated writes',
|
||||
(readers) => {
|
||||
const { store, subscriptions, publish } = setup()
|
||||
const rootSubscribe = vi.spyOn(store, 'subscribe')
|
||||
const derive = vi.spyOn(connectionState, 'runtimeHostConnectionStateForEntry')
|
||||
const listeners = Array.from({ length: readers }, () => vi.fn())
|
||||
const releases = listeners.map((listener) => subscriptions.subscribeConnection('a', listener))
|
||||
const b = vi.fn()
|
||||
releases.push(subscriptions.subscribeConnection('b', b))
|
||||
const parking = vi.fn()
|
||||
releases.push(subscriptions.subscribeParking(parking))
|
||||
expect(rootSubscribe).toHaveBeenCalledTimes(1)
|
||||
const oldPath = vi.fn((state) =>
|
||||
connectionState.runtimeHostConnectionStateForEntry(
|
||||
state.runtimeStatusByEnvironmentId.get('a')
|
||||
)
|
||||
)
|
||||
const releaseControl = store.subscribe(oldPath)
|
||||
derive.mockClear()
|
||||
store.setState({ unrelated: 1 })
|
||||
expect(derive).toHaveBeenCalledTimes(1)
|
||||
expect(oldPath).toHaveBeenCalledTimes(1)
|
||||
releaseControl()
|
||||
derive.mockClear()
|
||||
publish('b', entry({ environmentId: 'b', sequence: 2, checkedAt: 2 }))
|
||||
expect(derive).toHaveBeenCalledTimes(1)
|
||||
expect(b).not.toHaveBeenCalled()
|
||||
expect(parking).not.toHaveBeenCalled()
|
||||
expect(listeners.every((listener) => listener.mock.calls.length === 0)).toBe(true)
|
||||
publish('a', entry({ verification: 'blocked' }))
|
||||
expect(derive).toHaveBeenCalledTimes(2)
|
||||
expect(listeners.every((listener) => listener.mock.calls.length === 1)).toBe(true)
|
||||
expect(subscriptions.getParkingSnapshot()).toEqual(new Set(['b']))
|
||||
expect(parking).toHaveBeenCalledTimes(1)
|
||||
releases.forEach((release) => release())
|
||||
}
|
||||
)
|
||||
|
||||
it('retains parking through lost contact, withdraws it on refusal, and handles deletion/reset', () => {
|
||||
const { store, subscriptions, publish } = setup()
|
||||
const parking = vi.fn()
|
||||
const connection = vi.fn()
|
||||
const releaseParking = subscriptions.subscribeParking(parking)
|
||||
const releaseConnection = subscriptions.subscribeConnection('a', connection)
|
||||
const initialParking = subscriptions.getParkingSnapshot()
|
||||
publish('a', entry({ verification: 'unavailable', transport: 'disconnected' }))
|
||||
expect(subscriptions.getConnectionSnapshot('a')).toBe('reconnecting')
|
||||
expect(subscriptions.getParkingSnapshot()).toBe(initialParking)
|
||||
expect(parking).not.toHaveBeenCalled()
|
||||
publish('a', entry({ verification: 'blocked', blockedReason: 'access-revoked' }))
|
||||
expect(subscriptions.getConnectionSnapshot('a')).toBe('disconnected')
|
||||
expect(subscriptions.getParkingSnapshot()).toEqual(new Set(['b']))
|
||||
store.setState({ runtimeStatusByEnvironmentId: new Map() })
|
||||
expect(subscriptions.getConnectionSnapshot('a')).toBe('checking')
|
||||
expect(subscriptions.getParkingSnapshot()).toEqual(new Set())
|
||||
publish('a', entry())
|
||||
expect(subscriptions.getConnectionSnapshot('a')).toBe('connected')
|
||||
expect(subscriptions.getParkingSnapshot()).toEqual(new Set(['a']))
|
||||
expect(connection).toHaveBeenCalledTimes(4)
|
||||
expect(parking).toHaveBeenCalledTimes(3)
|
||||
releaseParking()
|
||||
releaseConnection()
|
||||
})
|
||||
|
||||
it('reads current authoritative state before subscription delivery and after a late subscription', () => {
|
||||
const { store, subscriptions, publish } = setup()
|
||||
const seen: string[] = []
|
||||
const earlier = store.subscribe(() => {
|
||||
seen.push(subscriptions.getConnectionSnapshot('a'))
|
||||
expect(subscriptions.getParkingSnapshot()).toEqual(new Set(['b']))
|
||||
})
|
||||
const release = subscriptions.subscribeConnection('a', () => {})
|
||||
publish('a', entry({ retired: true }))
|
||||
expect(seen).toEqual(['disconnected'])
|
||||
earlier()
|
||||
release()
|
||||
publish('a', entry())
|
||||
expect(subscriptions.getConnectionSnapshot('a')).toBe('connected')
|
||||
const notified = vi.fn()
|
||||
const lateRelease = subscriptions.subscribeConnection('a', notified)
|
||||
publish('a', entry({ verification: 'blocked' }))
|
||||
expect(notified).toHaveBeenCalledTimes(1)
|
||||
lateRelease()
|
||||
})
|
||||
|
||||
it('keeps baselines current through nested commits and listener removal', () => {
|
||||
const { store, subscriptions, publish } = setup()
|
||||
const observed: string[] = []
|
||||
let nested = false
|
||||
const first = subscriptions.subscribeConnection('a', () => {
|
||||
if (!nested) {
|
||||
nested = true
|
||||
publish('a', entry())
|
||||
}
|
||||
})
|
||||
const second = subscriptions.subscribeConnection('a', () =>
|
||||
observed.push(subscriptions.getConnectionSnapshot('a'))
|
||||
)
|
||||
publish('a', entry({ verification: 'blocked' }))
|
||||
expect(observed).toEqual(['connected', 'connected'])
|
||||
const length = observed.length
|
||||
store.setState({ unrelated: 2 })
|
||||
publish('a', entry({ sequence: 3 }))
|
||||
expect(observed).toHaveLength(length)
|
||||
first()
|
||||
second()
|
||||
const removed = vi.fn()
|
||||
let remove = () => {}
|
||||
const remover = subscriptions.subscribeConnection('a', () => remove())
|
||||
remove = subscriptions.subscribeConnection('a', removed)
|
||||
publish('a', entry({ verification: 'blocked' }))
|
||||
expect(removed).not.toHaveBeenCalled()
|
||||
remover()
|
||||
})
|
||||
|
||||
it('releases the shared root listener and supports repeated cleanup and store isolation', () => {
|
||||
const { store, subscriptions } = setup()
|
||||
const unsubscribe = vi.fn()
|
||||
const originalSubscribe = store.subscribe
|
||||
vi.spyOn(store, 'subscribe').mockImplementation((listener) => {
|
||||
const release = originalSubscribe(listener)
|
||||
return () => {
|
||||
unsubscribe()
|
||||
release()
|
||||
}
|
||||
})
|
||||
const release = subscriptions.subscribeConnection('a', () => {})
|
||||
release()
|
||||
release()
|
||||
expect(unsubscribe).toHaveBeenCalledTimes(1)
|
||||
const listener = vi.fn()
|
||||
const next = subscriptions.subscribeConnection('a', listener)
|
||||
release()
|
||||
const other = setup()
|
||||
other.publish('a', entry({ retired: true }))
|
||||
expect(listener).not.toHaveBeenCalled()
|
||||
expect(subscriptions.getConnectionSnapshot('a')).toBe('connected')
|
||||
expect(other.subscriptions.getConnectionSnapshot('a')).toBe('disconnected')
|
||||
store.setState({ runtimeStatusByEnvironmentId: new Map() })
|
||||
expect(listener).toHaveBeenCalledTimes(1)
|
||||
next()
|
||||
expect(unsubscribe).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,141 @@
|
||||
import type { RuntimeEnvironmentStatus } from '../../../shared/runtime-host-status'
|
||||
import {
|
||||
runtimeHostConnectionStateForEntry,
|
||||
type RuntimeHostConnectionState
|
||||
} from '@/runtime/runtime-host-connection-state'
|
||||
import { createPairedRuntimeParkingEnvironmentIdsSelector } from '@/components/terminal-pane/paired-runtime-parking-capabilities'
|
||||
|
||||
type RuntimeStatusState = {
|
||||
runtimeStatusByEnvironmentId: ReadonlyMap<string, RuntimeEnvironmentStatus>
|
||||
}
|
||||
type RuntimeStatusStore = {
|
||||
getState: () => RuntimeStatusState
|
||||
subscribe: (listener: () => void) => () => void
|
||||
}
|
||||
type Listener = () => void
|
||||
type ConnectionSubscription = {
|
||||
snapshot: RuntimeHostConnectionState
|
||||
listeners: Set<Listener>
|
||||
}
|
||||
|
||||
export function createRuntimeStatusSubscriptions(store: RuntimeStatusStore) {
|
||||
const connections = new Map<string, ConnectionSubscription>()
|
||||
const parkingListeners = new Set<Listener>()
|
||||
let selectParking = createPairedRuntimeParkingEnvironmentIdsSelector()
|
||||
let parkingSnapshot: ReadonlySet<string> | null = null
|
||||
let previousStatuses: RuntimeStatusState['runtimeStatusByEnvironmentId'] | null = null
|
||||
let unsubscribeStore: (() => void) | null = null
|
||||
|
||||
function getConnectionSnapshot(environmentId: string | null): RuntimeHostConnectionState {
|
||||
return runtimeHostConnectionStateForEntry(
|
||||
environmentId === null
|
||||
? undefined
|
||||
: store.getState().runtimeStatusByEnvironmentId.get(environmentId)
|
||||
)
|
||||
}
|
||||
|
||||
function getParkingSnapshot(): ReadonlySet<string> {
|
||||
return selectParking(store.getState().runtimeStatusByEnvironmentId)
|
||||
}
|
||||
|
||||
function ensureSubscription(): void {
|
||||
if (unsubscribeStore) {
|
||||
return
|
||||
}
|
||||
previousStatuses = store.getState().runtimeStatusByEnvironmentId
|
||||
unsubscribeStore = store.subscribe(() => {
|
||||
const statuses = store.getState().runtimeStatusByEnvironmentId
|
||||
const previous = previousStatuses
|
||||
if (statuses === previous) {
|
||||
return
|
||||
}
|
||||
previousStatuses = statuses
|
||||
const pending: { listeners: Set<Listener>; listener: Listener }[] = []
|
||||
for (const [environmentId, subscription] of connections) {
|
||||
if (statuses.get(environmentId) === previous?.get(environmentId)) {
|
||||
continue
|
||||
}
|
||||
const snapshot = runtimeHostConnectionStateForEntry(statuses.get(environmentId))
|
||||
if (snapshot === subscription.snapshot) {
|
||||
continue
|
||||
}
|
||||
subscription.snapshot = snapshot
|
||||
for (const listener of subscription.listeners) {
|
||||
pending.push({ listeners: subscription.listeners, listener })
|
||||
}
|
||||
}
|
||||
if (parkingListeners.size > 0) {
|
||||
const snapshot = getParkingSnapshot()
|
||||
if (snapshot !== parkingSnapshot) {
|
||||
parkingSnapshot = snapshot
|
||||
for (const listener of parkingListeners) {
|
||||
pending.push({ listeners: parkingListeners, listener })
|
||||
}
|
||||
}
|
||||
}
|
||||
// Establish every comparison baseline before callbacks can commit nested writes.
|
||||
for (const { listeners, listener } of pending) {
|
||||
if (listeners.has(listener)) {
|
||||
listener()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function releaseIfUnused(): void {
|
||||
if (connections.size > 0 || parkingListeners.size > 0) {
|
||||
return
|
||||
}
|
||||
unsubscribeStore?.()
|
||||
unsubscribeStore = null
|
||||
previousStatuses = null
|
||||
parkingSnapshot = null
|
||||
selectParking = createPairedRuntimeParkingEnvironmentIdsSelector()
|
||||
}
|
||||
|
||||
function subscribeConnection(environmentId: string | null, listener: Listener): () => void {
|
||||
if (environmentId === null) {
|
||||
return () => {}
|
||||
}
|
||||
ensureSubscription()
|
||||
let subscription = connections.get(environmentId)
|
||||
if (!subscription) {
|
||||
subscription = { snapshot: getConnectionSnapshot(environmentId), listeners: new Set() }
|
||||
connections.set(environmentId, subscription)
|
||||
}
|
||||
const registered = () => listener()
|
||||
subscription.listeners.add(registered)
|
||||
let released = false
|
||||
return () => {
|
||||
if (released) {
|
||||
return
|
||||
}
|
||||
released = true
|
||||
subscription.listeners.delete(registered)
|
||||
if (subscription.listeners.size === 0) {
|
||||
connections.delete(environmentId)
|
||||
}
|
||||
releaseIfUnused()
|
||||
}
|
||||
}
|
||||
|
||||
function subscribeParking(listener: Listener): () => void {
|
||||
ensureSubscription()
|
||||
if (parkingListeners.size === 0) {
|
||||
parkingSnapshot = getParkingSnapshot()
|
||||
}
|
||||
const registered = () => listener()
|
||||
parkingListeners.add(registered)
|
||||
let released = false
|
||||
return () => {
|
||||
if (released) {
|
||||
return
|
||||
}
|
||||
released = true
|
||||
parkingListeners.delete(registered)
|
||||
releaseIfUnused()
|
||||
}
|
||||
}
|
||||
|
||||
return { getConnectionSnapshot, subscribeConnection, getParkingSnapshot, subscribeParking }
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
// @vitest-environment happy-dom
|
||||
import { act, StrictMode, useLayoutEffect } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import { expect, it } from 'vitest'
|
||||
import { useAppStore } from './index'
|
||||
import { readStoreListenerCount } from './store-listener-census'
|
||||
import {
|
||||
usePairedRuntimeParkingEnvironmentIds,
|
||||
useRuntimeHostDisconnected
|
||||
} from './use-runtime-status-view'
|
||||
|
||||
Reflect.set(globalThis, 'IS_REACT_ACT_ENVIRONMENT', true)
|
||||
|
||||
it('shares one root subscription, rebinds host scopes, and releases Strict Mode remounts', () => {
|
||||
const original = useAppStore.getState()
|
||||
useAppStore.setState({ runtimeStatusByEnvironmentId: new Map() })
|
||||
const baseline = readStoreListenerCount()
|
||||
const container = document.createElement('div')
|
||||
const root = createRoot(container)
|
||||
let renders = 0
|
||||
function Probe({ environmentId }: { environmentId: string | null }) {
|
||||
renders += 1
|
||||
const disconnected = useRuntimeHostDisconnected(environmentId)
|
||||
const parking = usePairedRuntimeParkingEnvironmentIds()
|
||||
return (
|
||||
<span>
|
||||
{String(disconnected)}:{parking.size}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
try {
|
||||
for (const environmentId of ['a', 'b', null, 'a']) {
|
||||
act(() =>
|
||||
root.render(
|
||||
<StrictMode>
|
||||
<Probe environmentId={environmentId} />
|
||||
<Probe environmentId={environmentId} />
|
||||
</StrictMode>
|
||||
)
|
||||
)
|
||||
expect(readStoreListenerCount()).toBe((baseline ?? 0) + 1)
|
||||
expect(container.textContent).toBe('false:0false:0')
|
||||
if (environmentId !== null) {
|
||||
const mountedRenders = renders
|
||||
act(() =>
|
||||
useAppStore.getState().setRuntimeEnvironmentStatus(environmentId, {
|
||||
status: null,
|
||||
checkedAt: 1,
|
||||
snapshot: {
|
||||
environmentId,
|
||||
pairingRevision: 1,
|
||||
checkedAt: 1,
|
||||
sequence: 1,
|
||||
status: null,
|
||||
verification: 'unavailable',
|
||||
transport: 'ready'
|
||||
}
|
||||
})
|
||||
)
|
||||
expect(renders).toBe(mountedRenders)
|
||||
act(() =>
|
||||
useAppStore.getState().setRuntimeEnvironmentStatus(environmentId, {
|
||||
status: null,
|
||||
checkedAt: 2,
|
||||
snapshot: {
|
||||
environmentId,
|
||||
pairingRevision: 1,
|
||||
checkedAt: 2,
|
||||
sequence: 2,
|
||||
status: null,
|
||||
verification: 'blocked',
|
||||
transport: 'ready'
|
||||
}
|
||||
})
|
||||
)
|
||||
expect(container.textContent).toBe('true:0true:0')
|
||||
act(() => useAppStore.getState().clearRuntimeEnvironmentStatus(environmentId))
|
||||
expect(container.textContent).toBe('false:0false:0')
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
act(() => root.unmount())
|
||||
expect(readStoreListenerCount()).toBe(baseline)
|
||||
useAppStore.setState(original, true)
|
||||
}
|
||||
})
|
||||
|
||||
it('observes a commit between render and subscription without waiting for another publication', () => {
|
||||
const original = useAppStore.getState()
|
||||
useAppStore.setState({ runtimeStatusByEnvironmentId: new Map() })
|
||||
const container = document.createElement('div')
|
||||
const root = createRoot(container)
|
||||
function Probe() {
|
||||
return <output>{String(useRuntimeHostDisconnected('race'))}</output>
|
||||
}
|
||||
function CommitBeforeSubscription() {
|
||||
useLayoutEffect(() => {
|
||||
useAppStore.getState().setRuntimeEnvironmentStatus('race', { status: null, checkedAt: 1 })
|
||||
}, [])
|
||||
return null
|
||||
}
|
||||
try {
|
||||
act(() =>
|
||||
root.render(
|
||||
<>
|
||||
<Probe />
|
||||
<CommitBeforeSubscription />
|
||||
</>
|
||||
)
|
||||
)
|
||||
expect(container.textContent).toBe('true')
|
||||
} finally {
|
||||
act(() => root.unmount())
|
||||
useAppStore.setState(original, true)
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,30 @@
|
||||
import { useCallback, useSyncExternalStore } from 'react'
|
||||
import { useAppStore } from './index'
|
||||
import { createRuntimeStatusSubscriptions } from './runtime-status-subscriptions'
|
||||
import { isDisconnectedRuntimeHostState } from '@/runtime/runtime-host-connection-state'
|
||||
|
||||
const runtimeStatusSubscriptions = createRuntimeStatusSubscriptions(useAppStore)
|
||||
|
||||
export function useRuntimeHostDisconnected(environmentId: string | null) {
|
||||
const subscribe = useCallback(
|
||||
(listener: () => void) =>
|
||||
runtimeStatusSubscriptions.subscribeConnection(environmentId, listener),
|
||||
[environmentId]
|
||||
)
|
||||
const getSnapshot = useCallback(
|
||||
() =>
|
||||
isDisconnectedRuntimeHostState(
|
||||
runtimeStatusSubscriptions.getConnectionSnapshot(environmentId)
|
||||
),
|
||||
[environmentId]
|
||||
)
|
||||
return useSyncExternalStore(subscribe, getSnapshot, getSnapshot)
|
||||
}
|
||||
|
||||
export function usePairedRuntimeParkingEnvironmentIds() {
|
||||
return useSyncExternalStore(
|
||||
runtimeStatusSubscriptions.subscribeParking,
|
||||
runtimeStatusSubscriptions.getParkingSnapshot,
|
||||
runtimeStatusSubscriptions.getParkingSnapshot
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
import { test, expect } from './helpers/orca-app'
|
||||
import {
|
||||
seedAccumulatedWorkspaceFixture,
|
||||
cleanupAccumulatedWorkspaceFixture
|
||||
} from './accumulated-workspace-fixture'
|
||||
import {
|
||||
ensureTerminalVisible,
|
||||
getAllWorktreeIds,
|
||||
switchToWorktree,
|
||||
waitForActiveWorktree,
|
||||
waitForSessionReady
|
||||
} from './helpers/store'
|
||||
import { waitForActiveTerminalManager, waitForTerminalOutput } from './helpers/terminal'
|
||||
|
||||
test('runtime probe evidence preserves palette switching and terminal output at workspace scale', async ({
|
||||
orcaPage,
|
||||
electronApp
|
||||
}, testInfo) => {
|
||||
await waitForSessionReady(orcaPage)
|
||||
const source = await waitForActiveWorktree(orcaPage)
|
||||
const target = (await getAllWorktreeIds(orcaPage)).find((id) => id !== source)
|
||||
if (!target) {
|
||||
throw new Error('Expected a second seeded workspace')
|
||||
}
|
||||
await switchToWorktree(orcaPage, target)
|
||||
await ensureTerminalVisible(orcaPage)
|
||||
await waitForActiveTerminalManager(orcaPage)
|
||||
const targetName = await orcaPage.evaluate(
|
||||
(id) =>
|
||||
window
|
||||
.__store!.getState()
|
||||
.getKnownWorktreeById(id)
|
||||
?.branch.replace(/^refs\/heads\//, ''),
|
||||
target
|
||||
)
|
||||
if (!targetName) {
|
||||
throw new Error('Missing workspace name')
|
||||
}
|
||||
await switchToWorktree(orcaPage, source)
|
||||
const fixture = await seedAccumulatedWorkspaceFixture(orcaPage)
|
||||
const cdp = await orcaPage.context().newCDPSession(orcaPage)
|
||||
await cdp.send('Performance.enable')
|
||||
const beforeMetrics = await cdp.send('Performance.getMetrics')
|
||||
const publications: number[] = []
|
||||
const gaps: number[] = []
|
||||
const traffic = orcaPage.evaluate(async () => {
|
||||
const store = window.__store!
|
||||
const durations: number[] = []
|
||||
const gaps: number[] = []
|
||||
let last = performance.now()
|
||||
for (let sequence = 1; sequence <= 120; sequence += 1) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 16))
|
||||
const now = performance.now()
|
||||
gaps.push(now - last)
|
||||
last = now
|
||||
const start = performance.now()
|
||||
store.getState().setRuntimeEnvironmentStatus(
|
||||
'probe-fixture',
|
||||
{
|
||||
checkedAt: sequence,
|
||||
status: null,
|
||||
snapshot: {
|
||||
environmentId: 'probe-fixture',
|
||||
pairingRevision: 1,
|
||||
sequence,
|
||||
checkedAt: sequence,
|
||||
status: null,
|
||||
verification: 'unavailable',
|
||||
transport: 'ready'
|
||||
}
|
||||
},
|
||||
{ suppressDisconnectToast: true }
|
||||
)
|
||||
durations.push(performance.now() - start)
|
||||
}
|
||||
store.getState().clearRuntimeEnvironmentStatus('probe-fixture')
|
||||
return { durations, gaps }
|
||||
})
|
||||
try {
|
||||
await orcaPage.evaluate(() => window.__store!.getState().openModal('worktree-palette'))
|
||||
const palette = orcaPage.getByRole('dialog', { name: 'Jump to...' })
|
||||
await expect(palette).toBeVisible()
|
||||
await palette.getByRole('combobox').fill(targetName)
|
||||
await expect(palette.locator('[cmdk-item][data-selected="true"]')).toContainText(targetName)
|
||||
await palette.screenshot({ path: testInfo.outputPath('palette-during-probes.png') })
|
||||
await palette.getByRole('combobox').press('Enter')
|
||||
await expect(palette).toBeHidden()
|
||||
const row = orcaPage.locator(
|
||||
`[data-worktree-sidebar] [role="option"][data-worktree-id=${JSON.stringify(target)}]`
|
||||
)
|
||||
await expect(row).toHaveAttribute('aria-current', 'page')
|
||||
await waitForActiveTerminalManager(orcaPage)
|
||||
const terminal = orcaPage
|
||||
.locator('[data-terminal-tab-id]:visible .xterm-helper-textarea')
|
||||
.first()
|
||||
await terminal.fill('echo RUNTIME_PROBE_TYPING_OK')
|
||||
await terminal.press('Enter')
|
||||
await waitForTerminalOutput(orcaPage, 'RUNTIME_PROBE_TYPING_OK')
|
||||
await orcaPage.screenshot({ path: testInfo.outputPath('terminal-after-switch.png') })
|
||||
const measured = await traffic
|
||||
publications.push(...measured.durations)
|
||||
gaps.push(...measured.gaps)
|
||||
expect(publications).toHaveLength(120)
|
||||
expect(
|
||||
await electronApp.evaluate(({ BrowserWindow }) =>
|
||||
BrowserWindow.getAllWindows().every((window) => !window.isVisible())
|
||||
)
|
||||
).toBe(true)
|
||||
const afterMetrics = await cdp.send('Performance.getMetrics')
|
||||
const metric = (metrics: { metrics: { name: string; value: number }[] }, name: string) =>
|
||||
metrics.metrics.find((metric) => metric.name === name)?.value ?? 0
|
||||
const sorted = [...publications].sort((a, b) => a - b)
|
||||
const report = {
|
||||
fixture,
|
||||
publications: publications.length,
|
||||
publicationMs: {
|
||||
median: sorted[Math.floor(sorted.length / 2)],
|
||||
max: Math.max(...publications)
|
||||
},
|
||||
timerGapMs: { max: Math.max(...gaps) },
|
||||
rendererTaskMs:
|
||||
(metric(afterMetrics, 'TaskDuration') - metric(beforeMetrics, 'TaskDuration')) * 1000,
|
||||
heapUsedDelta:
|
||||
metric(afterMetrics, 'JSHeapUsedSize') - metric(beforeMetrics, 'JSHeapUsedSize')
|
||||
}
|
||||
console.log('[runtime-probe-workspace]', JSON.stringify(report))
|
||||
await testInfo.attach('runtime-probe-workspace', {
|
||||
body: JSON.stringify(report, null, 2),
|
||||
contentType: 'application/json'
|
||||
})
|
||||
} finally {
|
||||
await traffic
|
||||
await cdp.detach()
|
||||
await cleanupAccumulatedWorkspaceFixture(orcaPage)
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user