diff --git a/packages/platform-apple/src/snapshot-observability.ts b/packages/platform-apple/src/snapshot-observability.ts index 91ff821b3d..ca2dffdcc0 100644 --- a/packages/platform-apple/src/snapshot-observability.ts +++ b/packages/platform-apple/src/snapshot-observability.ts @@ -7,7 +7,7 @@ import type { PlatformRuntimeHost } from '@agent-device/contracts/platform-runti import type { DeviceInfo } from '@agent-device/kernel/device'; import type { SimulatorSnapshotSource } from './snapshot-source-facade.ts'; import { - isSimulatorTargetDiscoveryPending, + resolveSimulatorTargetJoiningDiscovery, type SimulatorSnapshotTarget, type SimulatorSnapshotTargetResolver, } from './snapshot-target.ts'; @@ -111,12 +111,19 @@ async function resolveLaunchedTarget( appBundleId: string, signal: AbortSignal, ): Promise { - for (;;) { - try { - return await resolveTarget(device, appBundleId, signal); - } catch (error) { - signal.throwIfAborted(); - if (!isSimulatorTargetDiscoveryPending(error)) return undefined; - } + try { + return await resolveSimulatorTargetJoiningDiscovery( + resolveTarget, + device, + appBundleId, + signal, + () => { + signal.throwIfAborted(); + return true; + }, + ); + } catch { + signal.throwIfAborted(); + return undefined; } } diff --git a/packages/platform-apple/src/snapshot-route.ts b/packages/platform-apple/src/snapshot-route.ts index 1961abdca5..6a3cb01df8 100644 --- a/packages/platform-apple/src/snapshot-route.ts +++ b/packages/platform-apple/src/snapshot-route.ts @@ -29,7 +29,7 @@ import { } from './snapshot-observability.ts'; import { createSimulatorSnapshotTargetResolver, - isSimulatorTargetDiscoveryPending, + resolveSimulatorTargetJoiningDiscovery, type SimulatorSnapshotTarget, type SimulatorSnapshotTargetResolver, } from './snapshot-target.ts'; @@ -236,15 +236,14 @@ async function resolveTargetForObservation( signal: AbortSignal, ): Promise { const appBundleId = input.options!.appBundleId!; - for (;;) { - try { - return await resolveTarget(device, appBundleId, signal); - } catch (error) { - if (!isSimulatorTargetDiscoveryPending(error)) throw error; - const execution = { requestId: input.execution?.requestId }; - if (await host.appleApplications.hasLiveRunnerSession(device, execution)) throw error; - } - } + const execution = { requestId: input.execution?.requestId }; + return await resolveSimulatorTargetJoiningDiscovery( + resolveTarget, + device, + appBundleId, + signal, + async () => !(await host.appleApplications.hasLiveRunnerSession(device, execution)), + ); } function isEligible(device: DeviceInfo, input: CaptureSnapshotInput): boolean { diff --git a/packages/platform-apple/src/snapshot-target.ts b/packages/platform-apple/src/snapshot-target.ts index b24f7178d5..2d707a1d57 100644 --- a/packages/platform-apple/src/snapshot-target.ts +++ b/packages/platform-apple/src/snapshot-target.ts @@ -73,10 +73,34 @@ export function createSimulatorSnapshotTargetResolver(): SimulatorSnapshotTarget * Whether a resolver failure only says the discovery is still running. The discovery keeps going * under its own deadline, so asking again joins it rather than starting another. */ -export function isSimulatorTargetDiscoveryPending(error: unknown): boolean { +function isSimulatorTargetDiscoveryPending(error: unknown): boolean { return error instanceof AppError && error.details?.reason === TARGET_DISCOVERY_PENDING; } +/** + * Resolves `resolveTarget`, re-asking it for as long as the only thing standing in the way is a + * discovery still in flight: the discovery's own deadline, or `resolveTarget` itself observing the + * request signal, is what eventually turns that into a real answer or a non-pending failure, which + * this always rethrows at once. `keepWaiting` is the one thing callers differ on: whether riding + * the discovery out is still worth it before the next wait slice. + */ +export async function resolveSimulatorTargetJoiningDiscovery( + resolveTarget: SimulatorSnapshotTargetResolver, + device: DeviceInfo, + appBundleId: string, + signal: AbortSignal, + keepWaiting: () => Promise | boolean, +): Promise { + for (;;) { + try { + return await resolveTarget(device, appBundleId, signal); + } catch (error) { + if (!isSimulatorTargetDiscoveryPending(error)) throw error; + if (!(await keepWaiting())) throw error; + } + } +} + async function resolveSimulatorSnapshotTarget( device: DeviceInfo, appBundleId: string,