From 90dc3cadb8e9a646af47355813ec2edbed6b02c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Wed, 23 Sep 2026 19:52:11 +0200 Subject: [PATCH] refactor(ios): share the discovery-pending retry loop between the launch probe and the capture route resolveLaunchedTarget and resolveTargetForObservation each looped on simulator-target-discovery-pending themselves, differing only in what ends the retry: one gives up after any non-pending failure, the other also gives up once a live runner can answer instead. Both now call resolveSimulatorTargetJoiningDiscovery in snapshot-target.ts, passing their own continuation rule; isSimulatorTargetDiscoveryPending drops its export since the shared loop is now its only caller. --- .../src/snapshot-observability.ts | 23 ++++++++++------ packages/platform-apple/src/snapshot-route.ts | 19 +++++++------- .../platform-apple/src/snapshot-target.ts | 26 ++++++++++++++++++- 3 files changed, 49 insertions(+), 19 deletions(-) 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,