Skip to content

Commit 05d8291

Browse files
committed
fix: cancel scrollintoview on client disconnect
1 parent 7171e8b commit 05d8291

5 files changed

Lines changed: 61 additions & 6 deletions

File tree

src/core/dispatch.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ export async function dispatchCommand(
7777
positionals: string[],
7878
outPath?: string,
7979
context?: {
80+
requestId?: string;
8081
appBundleId?: string;
8182
activity?: string;
8283
verbose?: boolean;
@@ -97,6 +98,7 @@ export async function dispatchCommand(
9798
},
9899
): Promise<Record<string, unknown> | void> {
99100
const runnerCtx: RunnerContext = {
101+
requestId: context?.requestId,
100102
appBundleId: context?.appBundleId,
101103
verbose: context?.verbose,
102104
logPath: context?.logPath,

src/daemon.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { handleRecordTraceCommands } from './daemon/handlers/record-trace.ts';
1818
import { handleInteractionCommands } from './daemon/handlers/interaction.ts';
1919
import { assertSessionSelectorMatches } from './daemon/session-selector.ts';
2020
import { resolveEffectiveSessionName } from './daemon/session-routing.ts';
21+
import { clearRequestCanceled, isRequestCanceled, markRequestCanceled } from './daemon/request-cancel.ts';
2122
import {
2223
isAgentDeviceDaemonProcess,
2324
readProcessStartTime,
@@ -50,7 +51,11 @@ function contextFromFlags(
5051
appBundleId?: string,
5152
traceLogPath?: string,
5253
): DaemonCommandContext {
53-
return contextFromFlagsWithLog(logPath, flags, appBundleId, traceLogPath);
54+
const requestId = getDiagnosticsMeta().requestId;
55+
return {
56+
...contextFromFlagsWithLog(logPath, flags, appBundleId, traceLogPath, requestId),
57+
requestId,
58+
};
5459
}
5560

5661
async function handleRequest(req: DaemonRequest): Promise<DaemonResponse> {
@@ -295,10 +300,14 @@ function start(): void {
295300
const server = net.createServer((socket) => {
296301
let buffer = '';
297302
let inFlightRequests = 0;
303+
const activeRequestIds = new Set<string>();
298304
let canceledInFlight = false;
299305
const cancelInFlightRunnerSessions = () => {
300306
if (canceledInFlight || inFlightRequests === 0) return;
301307
canceledInFlight = true;
308+
for (const requestId of activeRequestIds) {
309+
markRequestCanceled(requestId);
310+
}
302311
emitDiagnostic({
303312
level: 'warn',
304313
phase: 'request_client_disconnected',
@@ -332,13 +341,25 @@ function start(): void {
332341
}
333342
let response: DaemonResponse;
334343
inFlightRequests += 1;
344+
let requestIdForCleanup: string | undefined;
335345
try {
336346
const req = JSON.parse(line) as DaemonRequest;
347+
requestIdForCleanup = req.meta?.requestId;
348+
if (requestIdForCleanup) {
349+
activeRequestIds.add(requestIdForCleanup);
350+
if (isRequestCanceled(requestIdForCleanup)) {
351+
throw new AppError('COMMAND_FAILED', 'request canceled');
352+
}
353+
}
337354
response = await handleRequest(req);
338355
} catch (err) {
339356
response = { ok: false, error: normalizeError(err) };
340357
} finally {
341358
inFlightRequests -= 1;
359+
if (requestIdForCleanup) {
360+
activeRequestIds.delete(requestIdForCleanup);
361+
clearRequestCanceled(requestIdForCleanup);
362+
}
342363
}
343364
if (!socket.destroyed) {
344365
socket.write(`${JSON.stringify(response)}\n`);

src/daemon/context.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { CommandFlags } from '../core/dispatch.ts';
22

33
export type DaemonCommandContext = {
4+
requestId?: string;
45
appBundleId?: string;
56
activity?: string;
67
verbose?: boolean;
@@ -25,8 +26,10 @@ export function contextFromFlags(
2526
flags: CommandFlags | undefined,
2627
appBundleId?: string,
2728
traceLogPath?: string,
29+
requestId?: string,
2830
): DaemonCommandContext {
2931
return {
32+
requestId,
3033
appBundleId,
3134
activity: flags?.activity,
3235
verbose: flags?.verbose,

src/daemon/request-cancel.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
const canceledRequestIds = new Set<string>();
2+
3+
export function markRequestCanceled(requestId: string | undefined): void {
4+
if (!requestId) return;
5+
canceledRequestIds.add(requestId);
6+
}
7+
8+
export function clearRequestCanceled(requestId: string | undefined): void {
9+
if (!requestId) return;
10+
canceledRequestIds.delete(requestId);
11+
}
12+
13+
export function isRequestCanceled(requestId: string | undefined): boolean {
14+
if (!requestId) return false;
15+
return canceledRequestIds.has(requestId);
16+
}

src/utils/interactors.ts

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,10 @@ import {
2121
screenshotIos,
2222
} from '../platforms/ios/index.ts';
2323
import { runIosRunnerCommand } from '../platforms/ios/runner-client.ts';
24+
import { isRequestCanceled } from '../daemon/request-cancel.ts';
2425

2526
export type RunnerContext = {
27+
requestId?: string;
2628
appBundleId?: string;
2729
verbose?: boolean;
2830
logPath?: string;
@@ -86,6 +88,10 @@ type IoRunnerOverrides = Pick<
8688

8789
function iosRunnerOverrides(device: DeviceInfo, ctx: RunnerContext): IoRunnerOverrides {
8890
const runnerOpts = { verbose: ctx.verbose, logPath: ctx.logPath, traceLogPath: ctx.traceLogPath };
91+
const throwIfCanceled = () => {
92+
if (!isRequestCanceled(ctx.requestId)) return;
93+
throw new AppError('COMMAND_FAILED', 'request canceled');
94+
};
8995

9096
return {
9197
tap: async (x, y) => {
@@ -156,17 +162,24 @@ function iosRunnerOverrides(device: DeviceInfo, ctx: RunnerContext): IoRunnerOve
156162
scrollIntoView: async (text) => {
157163
const maxAttempts = 8;
158164
for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
165+
throwIfCanceled();
159166
const found = (await runIosRunnerCommand(
160167
device,
161168
{ command: 'findText', text, appBundleId: ctx.appBundleId },
162169
runnerOpts,
163170
)) as { found?: boolean };
164171
if (found?.found) return { attempts: attempt + 1 };
165-
await runIosRunnerCommand(
166-
device,
167-
{ command: 'swipe', direction: 'up', appBundleId: ctx.appBundleId },
168-
runnerOpts,
169-
);
172+
// Increase traversal speed on long lists while still checking visibility between chunks.
173+
const swipesPerAttempt = Math.min(4, 1 + Math.floor(attempt / 2));
174+
for (let i = 0; i < swipesPerAttempt; i += 1) {
175+
throwIfCanceled();
176+
await runIosRunnerCommand(
177+
device,
178+
{ command: 'swipe', direction: 'up', appBundleId: ctx.appBundleId },
179+
runnerOpts,
180+
);
181+
}
182+
throwIfCanceled();
170183
await new Promise((resolve) => setTimeout(resolve, 300));
171184
}
172185
throw new AppError('COMMAND_FAILED', `scrollintoview could not find text: ${text}`);

0 commit comments

Comments
 (0)