refactor(mobile): list server rows through one desktop fetch, with no side effect on listing

The phone now always lists a server's rows through mobileRelay.hosts.worktrees, so the
desktop's own fetch is both the live source and the cache, and stamping stays in main.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
This commit is contained in:
Jinwoo-H
2026-10-08 01:10:42 -04:00
parent aca7f12aae
commit 006f241d8c
4 changed files with 42 additions and 38 deletions
@@ -148,7 +148,7 @@ describe('mobile relay hosts: phone -> desktop lists the servers it shows', () =
return { box, contact, phoneCall }
}
it('lists cached rows for an unreachable server and live rows for an update-needed one', async () => {
it('lists rows live through the desktop, then its last rows as stale once a server is unreachable', async () => {
const { box, contact, phoneCall } = await startTopology()
await expect(phoneCall('mobileRelay.hosts.list')).resolves.toEqual({
@@ -89,7 +89,7 @@ function catalog(hosts: MobileDesktopRelayHosts, labels = new Map()) {
}
describe('mobile relay host catalog', () => {
it("lists every configured server with the desktop's health and whether it can relay", () => {
it("lists every configured server with the desktop's health and whether it can relay", async () => {
const { hosts, call } = fakeHosts([
{ id: 'ready', name: 'Box', snapshot: snapshot('ready', 'live') },
{ id: 'old', name: 'ThinkPad', snapshot: snapshot('old', 'live', false) },
@@ -108,6 +108,9 @@ describe('mobile relay host catalog', () => {
{ hostId: 'runtime:old-down', label: 'Pi', health: 'connecting', relay: 'update-needed' },
{ hostId: 'runtime:new', label: 'Fresh', health: 'connecting', relay: 'unavailable' }
])
// Listing reads what the desktop already knows; it never contacts a server.
await new Promise((resolve) => setTimeout(resolve, 0))
expect(call).not.toHaveBeenCalled()
})
it('encodes an environment id into the host id the desktop uses', () => {
@@ -156,8 +159,7 @@ describe('mobile relay host catalog', () => {
])
call.mockResolvedValue(psReply([{ worktreeId: 'w', hostId: 'local', status: 'working' }]))
const { catalog: hostCatalog, advance } = catalog(hosts)
hostCatalog.list()
await vi.waitFor(() => expect(call).toHaveBeenCalledTimes(1))
await hostCatalog.worktrees('runtime:env')
advance(5_000)
state.environments = [{ id: 'env', name: 'Box', snapshot: snapshot('env', 'unreachable') }]
@@ -209,19 +211,21 @@ describe('mobile relay host catalog', () => {
await expect(hostCatalog.worktrees('runtime:env')).resolves.toEqual({ worktrees: null })
})
it('shares one in-flight fetch per server between listing and a rows request', async () => {
it('shares one in-flight fetch per server between concurrent rows requests', async () => {
const { hosts, call } = fakeHosts([
{ id: 'env', name: 'Box', snapshot: snapshot('env', 'live') }
])
let answerFetch: (response: RuntimeRpcResponse<unknown>) => void = () => {}
call.mockReturnValue(new Promise((resolve) => (answerFetch = resolve)))
const { catalog: hostCatalog } = catalog(hosts)
hostCatalog.list()
hostCatalog.list()
const rows = hostCatalog.worktrees('runtime:env')
const first = hostCatalog.worktrees('runtime:env')
const second = hostCatalog.worktrees('runtime:env')
await vi.waitFor(() => expect(call).toHaveBeenCalledTimes(1))
answerFetch(psReply([{ worktreeId: 'w', hostId: 'local' }]))
await expect(rows).resolves.toMatchObject({ stale: false })
await expect(Promise.all([first, second])).resolves.toMatchObject([
{ stale: false },
{ stale: false }
])
expect(call).toHaveBeenCalledTimes(1)
})
})
@@ -1,14 +1,17 @@
import { z } from 'zod'
import { DELEGATED_MOBILE_DEVICES_RUNTIME_CAPABILITY } from '../../../shared/delegated-mobile-device-contract'
import { parseExecutionHostId, type ExecutionHostId } from '../../../shared/execution-host'
import { buildExecutionHostRegistry } from '../../../shared/execution-host-registry'
import {
stampServerWorktreeRows,
type MobileRelayHost,
type MobileRelayHostRelay,
type MobileRelayHostsListResult,
type MobileRelayHostWorktreesResult,
type MobileRelayServerWorktreeRow
parseExecutionHostId,
toRuntimeExecutionHostId,
type ExecutionHostId
} from '../../../shared/execution-host'
import { buildExecutionHostRegistry } from '../../../shared/execution-host-registry'
import type {
MobileRelayHost,
MobileRelayHostRelay,
MobileRelayHostsListResult,
MobileRelayHostWorktreesResult,
MobileRelayServerWorktreeRow
} from '../../../shared/mobile-relay-hosts-contract'
import { lastVerifiedRuntimeStatus } from '../../../shared/runtime-host-status'
import type { RuntimeStatus } from '../../../shared/runtime-types'
@@ -49,15 +52,13 @@ export class MobileRelayHostCatalog {
) {}
list(): MobileRelayHostsListResult {
const hosts = this.describe()
for (const host of hosts) {
// Why: keeps the last list warm while the server answers, for when it stops answering.
if (host.health === 'available') {
void this.refresh(host)
}
}
return {
hosts: hosts.map(({ hostId, label, health, relay }) => ({ hostId, label, health, relay }))
hosts: this.describe().map(({ hostId, label, health, relay }) => ({
hostId,
label,
health,
relay
}))
}
}
@@ -164,3 +165,15 @@ function relayVerdict(
}
return health === 'available' ? 'ready' : 'unavailable'
}
/**
* A server's rows name hosts relative to that server (`local`, its own `ssh:` targets). Like the
* desktop sidebar, the phone shows every one of them under the server, so all become its host id.
*/
function stampServerWorktreeRows<Row extends { hostId?: string }>(
environmentId: string,
rows: readonly Row[]
): (Row & { hostId: `runtime:${string}` })[] {
const hostId = toRuntimeExecutionHostId(environmentId)
return rows.map((row) => ({ ...row, hostId }))
}
-13
View File
@@ -1,5 +1,4 @@
import { z } from 'zod'
import { toRuntimeExecutionHostId } from './execution-host'
import type { ExecutionHostHealth } from './execution-host-health'
// Why: both are answered by the paired desktop itself; the phone lists servers the desktop shows.
@@ -38,15 +37,3 @@ export type MobileRelayHostWorktreesResult =
stale: boolean
}
| { worktrees: null }
/**
* A server's rows name hosts relative to that server (`local`, its own `ssh:` targets). Like the
* desktop sidebar, the phone shows every one of them under the server, so all become its host id.
*/
export function stampServerWorktreeRows<Row extends { hostId?: string }>(
environmentId: string,
rows: readonly Row[]
): (Row & { hostId: `runtime:${string}` })[] {
const hostId = toRuntimeExecutionHostId(environmentId)
return rows.map((row) => ({ ...row, hostId }))
}