fix(mobile): clarify bounded host connection state

This commit is contained in:
Brennan Benson
2026-07-30 21:26:55 -07:00
parent 0d7888bea3
commit 2ac2c4182f
5 changed files with 136 additions and 9 deletions
+14 -3
View File
@@ -32,7 +32,10 @@ import type { RpcClient } from '../src/transport/rpc-client'
import { sendSingleFlightRequest } from '../src/transport/request-single-flight'
import { useCloseHost, useForceReconnect, usePrimeHosts } from '../src/transport/client-context'
import { useAllHostClients } from '../src/transport/all-host-client-connections'
import { selectHomeAutoConnectHostIds } from '../src/transport/home-host-auto-connect'
import {
resolveHomeHostConnectionState,
selectHomeAutoConnectHostIds
} from '../src/transport/home-host-auto-connect'
import { classifyConnection } from '../src/transport/connection-health'
import { subscribeToDesktopNotifications } from '../src/notifications/mobile-notifications'
import {
@@ -777,7 +780,11 @@ export default function HomeScreen() {
}
ItemSeparatorComponent={CardGap}
renderItem={({ item }) => {
const state = hostStates[item.id] ?? 'connecting'
const state = resolveHomeHostConnectionState(
item.id,
hostStates[item.id],
autoConnectHostIds
)
const attempts = hostAttempts[item.id] ?? 0
const lastConnectedAt = hostLastConnected[item.id] ?? null
const info = worktreeInfo[item.id]
@@ -975,7 +982,11 @@ export default function HomeScreen() {
if (!host) {
return []
}
const state = hostStates[host.id] ?? 'connecting'
const state = resolveHomeHostConnectionState(
host.id,
hostStates[host.id],
autoConnectHostIds
)
const isLive =
state === 'connected' ||
state === 'connecting' ||
@@ -38,7 +38,6 @@ export function useAllHostClients(hostIds: string[], options?: UseAllHostClients
ctx.release(id)
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [key])
return useMemo(() => {
@@ -56,6 +55,5 @@ export function useAllHostClients(hostIds: string[], options?: UseAllHostClients
? [{ hostId, client, state: ctx.getState(hostId), path: ctx.getActivePath(hostId) }]
: []
})
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [key, tick])
}
+93 -1
View File
@@ -20,8 +20,9 @@ vi.mock('./connection-revival-triggers', () => ({
subscribeConnectionRevivalTriggers: () => () => {}
}))
import { RpcClientProvider, useCloseHost, useHostClient } from './client-context'
import { RpcClientProvider, useCloseHost, useForceReconnect, useHostClient } from './client-context'
import { useAllHostClients } from './all-host-client-connections'
import { selectHomeAutoConnectHostIds } from './home-host-auto-connect'
type FakeClient = RpcClient & {
emitState: (state: ConnectionState) => void
@@ -344,4 +345,95 @@ describe('useAllHostClients', () => {
act(() => renderer?.unmount())
}
})
it('keeps startup connection fanout constant for a large saved-host list', async () => {
const hosts = Array.from({ length: 1_000 }, (_, index) => ({
...HOST,
id: `host-${index}`,
name: `Host ${index}`,
lastConnected: index
}))
const hostIds = hosts.map((host) => host.id)
const autoConnectHostIds = selectHomeAutoConnectHostIds(hosts)
connectMock.mockReturnValue(makeFakeClient('connected'))
loadHostsMock.mockResolvedValue(hosts)
let renderer: ReactTestRenderer | null = null
function Probe(): null {
useAllHostClients(hostIds, { autoConnectHostIds })
return null
}
const restore = suppressReactTestRendererDeprecationWarning()
try {
await act(async () => {
renderer = create(createElement(RpcClientProvider, null, createElement(Probe)))
await Promise.resolve()
})
expect(connectMock).toHaveBeenCalledTimes(3)
expect(connectMock.mock.calls.map(([host]) => host.id)).toEqual([
'host-999',
'host-998',
'host-997'
])
} finally {
restore()
act(() => renderer?.unmount())
}
})
it('retains connect-all behavior when no startup subset is provided', async () => {
const host2 = { ...HOST, id: 'host-2', name: 'Host 2' }
connectMock.mockReturnValue(makeFakeClient('connected'))
loadHostsMock.mockResolvedValue([HOST, host2])
let renderer: ReactTestRenderer | null = null
function Probe(): null {
useAllHostClients([HOST.id, host2.id])
return null
}
const restore = suppressReactTestRendererDeprecationWarning()
try {
await act(async () => {
renderer = create(createElement(RpcClientProvider, null, createElement(Probe)))
await Promise.resolve()
})
expect(connectMock).toHaveBeenCalledTimes(2)
} finally {
restore()
act(() => renderer?.unmount())
}
})
it('allows an excluded host to connect manually', async () => {
connectMock.mockReturnValue(makeFakeClient('connected'))
loadHostsMock.mockResolvedValue([HOST])
let reconnect: ((hostId: string) => Promise<void>) | null = null
let renderer: ReactTestRenderer | null = null
function Probe(): null {
useAllHostClients([HOST.id], { autoConnectHostIds: [] })
reconnect = useForceReconnect()
return null
}
const restore = suppressReactTestRendererDeprecationWarning()
try {
await act(async () => {
renderer = create(createElement(RpcClientProvider, null, createElement(Probe)))
})
expect(connectMock).not.toHaveBeenCalled()
if (!reconnect) {
throw new Error('reconnect harness did not initialize')
}
await act(async () => {
await reconnect?.(HOST.id)
})
expect(connectMock).toHaveBeenCalledOnce()
} finally {
restore()
act(() => renderer?.unmount())
}
})
})
@@ -1,6 +1,10 @@
import { describe, expect, it } from 'vitest'
import type { HostProfile } from './types'
import { HOME_AUTO_CONNECT_LIMIT, selectHomeAutoConnectHostIds } from './home-host-auto-connect'
import {
HOME_AUTO_CONNECT_LIMIT,
resolveHomeHostConnectionState,
selectHomeAutoConnectHostIds
} from './home-host-auto-connect'
function host(id: string, lastConnected: number, credentials = true): HostProfile {
return {
@@ -13,7 +17,7 @@ function host(id: string, lastConnected: number, credentials = true): HostProfil
}
}
describe('selectHomeAutoConnectHostIds', () => {
describe('home host auto-connect', () => {
it('limits startup connections to the most recently used credentialed hosts', () => {
const hosts = [
host('old', 1),
@@ -35,4 +39,18 @@ describe('selectHomeAutoConnectHostIds', () => {
expect(hosts.map((item) => item.id)).toEqual(['old', 'new'])
})
it('only presents hosts in the startup subset as connecting before clients open', () => {
const autoConnectHostIds = ['recent']
expect(resolveHomeHostConnectionState('recent', undefined, autoConnectHostIds)).toBe(
'connecting'
)
expect(resolveHomeHostConnectionState('stale', undefined, autoConnectHostIds)).toBe(
'disconnected'
)
expect(resolveHomeHostConnectionState('stale', 'connected', autoConnectHostIds)).toBe(
'connected'
)
})
})
@@ -1,4 +1,4 @@
import type { HostProfile } from './types'
import type { ConnectionState, HostProfile } from './types'
export const HOME_AUTO_CONNECT_LIMIT = 3
@@ -14,3 +14,11 @@ export function selectHomeAutoConnectHostIds(
.slice(0, Math.max(0, limit))
.map((host) => host.id)
}
export function resolveHomeHostConnectionState(
hostId: string,
state: ConnectionState | undefined,
autoConnectHostIds: readonly string[]
): ConnectionState {
return state ?? (autoConnectHostIds.includes(hostId) ? 'connecting' : 'disconnected')
}