From 006f241d8c6e10b850af39bbf32ca79cd6b28313 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Thu, 8 Oct 2026 01:10:42 -0400 Subject: [PATCH] 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 --- ...ile-relay-host-catalog.integration.test.ts | 2 +- .../mobile-relay-host-catalog.test.ts | 20 +++++---- .../mobile-relay-host-catalog.ts | 45 ++++++++++++------- src/shared/mobile-relay-hosts-contract.ts | 13 ------ 4 files changed, 42 insertions(+), 38 deletions(-) diff --git a/src/main/runtime/mobile-desktop-relay/mobile-relay-host-catalog.integration.test.ts b/src/main/runtime/mobile-desktop-relay/mobile-relay-host-catalog.integration.test.ts index fb8e79db761..79b8bf77eb2 100644 --- a/src/main/runtime/mobile-desktop-relay/mobile-relay-host-catalog.integration.test.ts +++ b/src/main/runtime/mobile-desktop-relay/mobile-relay-host-catalog.integration.test.ts @@ -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({ diff --git a/src/main/runtime/mobile-desktop-relay/mobile-relay-host-catalog.test.ts b/src/main/runtime/mobile-desktop-relay/mobile-relay-host-catalog.test.ts index 26aa8d967c8..0016054a0ca 100644 --- a/src/main/runtime/mobile-desktop-relay/mobile-relay-host-catalog.test.ts +++ b/src/main/runtime/mobile-desktop-relay/mobile-relay-host-catalog.test.ts @@ -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) => 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) }) }) diff --git a/src/main/runtime/mobile-desktop-relay/mobile-relay-host-catalog.ts b/src/main/runtime/mobile-desktop-relay/mobile-relay-host-catalog.ts index cc32e3febfb..6730ebde725 100644 --- a/src/main/runtime/mobile-desktop-relay/mobile-relay-host-catalog.ts +++ b/src/main/runtime/mobile-desktop-relay/mobile-relay-host-catalog.ts @@ -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( + environmentId: string, + rows: readonly Row[] +): (Row & { hostId: `runtime:${string}` })[] { + const hostId = toRuntimeExecutionHostId(environmentId) + return rows.map((row) => ({ ...row, hostId })) +} diff --git a/src/shared/mobile-relay-hosts-contract.ts b/src/shared/mobile-relay-hosts-contract.ts index fdea3b6e7fc..11defa4482a 100644 --- a/src/shared/mobile-relay-hosts-contract.ts +++ b/src/shared/mobile-relay-hosts-contract.ts @@ -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( - environmentId: string, - rows: readonly Row[] -): (Row & { hostId: `runtime:${string}` })[] { - const hostId = toRuntimeExecutionHostId(environmentId) - return rows.map((row) => ({ ...row, hostId })) -}