Skip to content

Commit 91d1b4c

Browse files
authored
refactor: dedup daemon handler capability/session/recordAction boilerplate (#904)
Collapse three repeated daemon-handler patterns into shared helpers in src/daemon/handlers/response.ts and handler-utils.ts: - requireCommandSupported(command, device, { message?, hint? }) replaces ~21 verbatim isCommandSupportedOnDevice UNSUPPORTED_OPERATION guards. The richest variant (generic dispatch) folds the unsupportedHintForDevice hint behind the `hint` option; custom-message sites pass `message`. - noActiveSessionError() / NO_ACTIVE_SESSION_MESSAGE replace the duplicated 'No active session. Run open first.' SESSION_NOT_FOUND literal across handlers (and the inline router/AppError variants). - recordSessionAction gains optional positionals/flags overrides so ~15 inline sessionStore.recordAction({ command, positionals, flags, result }) literals (find, record-trace, record-trace-recording, session, session-close, session-perf-xctrace, install-source) route through the one wrapper. behaviorless: every site keeps its exact error code/message (and hint where present) and recorded action shape; helpers reproduce each variant.
1 parent d291e4c commit 91d1b4c

24 files changed

Lines changed: 193 additions & 249 deletions

src/daemon/handlers/find.ts

Lines changed: 52 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ import {
1818
} from '../../core/interaction-targeting.ts';
1919
import { isSnapshotNodeInteractionBlocked } from '../../utils/snapshot-occlusion.ts';
2020
import { readTextForNode } from './interaction-read.ts';
21-
import { errorResponse } from './response.ts';
21+
import { errorResponse, noActiveSessionError } from './response.ts';
22+
import { recordSessionAction } from './handler-utils.ts';
2223
import { stripInternalInteractionFlags } from '../interaction-outcome-policy.ts';
2324
import { dispatchFindReadOnlyViaRuntime } from '../selector-runtime.ts';
2425
import { createSelectorCaptureRuntime } from '../selector-capture-runtime.ts';
@@ -88,7 +89,7 @@ export async function handleFindCommands(params: {
8889
const session = sessionStore.get(sessionName);
8990
const isReadOnly = isReadOnlyFindAction(action);
9091
if (!session && !isReadOnly) {
91-
return errorResponse('SESSION_NOT_FOUND', 'No active session. Run open first.');
92+
return noActiveSessionError();
9293
}
9394
const device = session?.device ?? (await resolveTargetDevice(req.flags ?? {}));
9495
if (!session) {
@@ -428,14 +429,14 @@ async function handleFindWait(
428429
const match = findBestMatchesByLocator(nodes, locator, query, { requireRect: false })
429430
.matches[0];
430431
if (match) {
431-
if (session) {
432-
sessionStore.recordAction(session, {
433-
command,
434-
positionals: req.positionals ?? [],
435-
flags: publicFlags,
436-
result: { found: true, waitedMs: Date.now() - start },
437-
});
438-
}
432+
recordSessionAction(
433+
sessionStore,
434+
session,
435+
req,
436+
command,
437+
{ found: true, waitedMs: Date.now() - start },
438+
{ flags: publicFlags },
439+
);
439440
return { ok: true, data: { found: true, waitedMs: Date.now() - start } };
440441
}
441442
await sleep(300);
@@ -446,14 +447,7 @@ async function handleFindWait(
446447

447448
async function handleFindExists(ctx: FindContext): Promise<DaemonResponse> {
448449
const { req, sessionStore, session, command, publicFlags } = ctx;
449-
if (session) {
450-
sessionStore.recordAction(session, {
451-
command,
452-
positionals: req.positionals ?? [],
453-
flags: publicFlags,
454-
result: { found: true },
455-
});
456-
}
450+
recordSessionAction(sessionStore, session, req, command, { found: true }, { flags: publicFlags });
457451
return { ok: true, data: { found: true } };
458452
}
459453

@@ -469,27 +463,27 @@ async function handleFindGetText(ctx: FindContext, match: ResolvedMatch): Promis
469463
contextFromFlags: (flags, appBundleId, traceLogPath) =>
470464
contextFromFlags(logPath, flags, appBundleId, traceLogPath),
471465
});
472-
if (session) {
473-
sessionStore.recordAction(session, {
474-
command,
475-
positionals: req.positionals ?? [],
476-
flags: publicFlags,
477-
result: { ref: match.ref, action: 'get text', text },
478-
});
479-
}
466+
recordSessionAction(
467+
sessionStore,
468+
session,
469+
req,
470+
command,
471+
{ ref: match.ref, action: 'get text', text },
472+
{ flags: publicFlags },
473+
);
480474
return { ok: true, data: { ref: match.ref, text, node: match.node } };
481475
}
482476

483477
async function handleFindGetAttrs(ctx: FindContext, match: ResolvedMatch): Promise<DaemonResponse> {
484478
const { req, sessionStore, session, command, publicFlags } = ctx;
485-
if (session) {
486-
sessionStore.recordAction(session, {
487-
command,
488-
positionals: req.positionals ?? [],
489-
flags: publicFlags,
490-
result: { ref: match.ref, action: 'get attrs' },
491-
});
492-
}
479+
recordSessionAction(
480+
sessionStore,
481+
session,
482+
req,
483+
command,
484+
{ ref: match.ref, action: 'get attrs' },
485+
{ flags: publicFlags },
486+
);
493487
return { ok: true, data: { ref: match.ref, node: match.node } };
494488
}
495489

@@ -514,14 +508,14 @@ async function handleFindClick(ctx: FindContext, match: ResolvedMatch): Promise<
514508
matchData.x = matchCoords.x;
515509
matchData.y = matchCoords.y;
516510
}
517-
if (session) {
518-
sessionStore.recordAction(session, {
519-
command,
520-
positionals: req.positionals ?? [],
521-
flags: publicFlags,
522-
result: { ref: match.ref, action: 'click', locator, query },
523-
});
524-
}
511+
recordSessionAction(
512+
sessionStore,
513+
session,
514+
req,
515+
command,
516+
{ ref: match.ref, action: 'click', locator, query },
517+
{ flags: publicFlags },
518+
);
525519
return { ok: true, data: matchData };
526520
}
527521

@@ -542,14 +536,14 @@ async function handleFindFill(
542536
flags: match.actionFlags,
543537
});
544538
if (!response.ok) return response;
545-
if (session) {
546-
sessionStore.recordAction(session, {
547-
command,
548-
positionals: req.positionals ?? [],
549-
flags: publicFlags,
550-
result: { ref: match.ref, action: 'fill' },
551-
});
552-
}
539+
recordSessionAction(
540+
sessionStore,
541+
session,
542+
req,
543+
command,
544+
{ ref: match.ref, action: 'fill' },
545+
{ flags: publicFlags },
546+
);
553547
return response;
554548
}
555549

@@ -617,14 +611,14 @@ function rejectCoveredFindMatch(match: ResolvedMatch, interaction: string): Daem
617611

618612
function recordFindAction(ctx: FindContext, match: ResolvedMatch, action: string): void {
619613
const { req, sessionStore, session, command, publicFlags } = ctx;
620-
if (session) {
621-
sessionStore.recordAction(session, {
622-
command,
623-
positionals: req.positionals ?? [],
624-
flags: publicFlags,
625-
result: { ref: match.ref, action },
626-
});
627-
}
614+
recordSessionAction(
615+
sessionStore,
616+
session,
617+
req,
618+
command,
619+
{ ref: match.ref, action },
620+
{ flags: publicFlags },
621+
);
628622
}
629623

630624
// --- Helpers ---

src/daemon/handlers/handler-utils.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,23 @@ import type { DaemonRequest, SessionState } from '../types.ts';
44

55
/**
66
* Record a session action if a session is active. No-op when session is undefined.
7+
*
8+
* By default the recorded positionals/flags mirror the request; pass `overrides` to
9+
* record a different set (e.g. resolved positionals or stripped public flags).
710
*/
811
export function recordSessionAction(
912
sessionStore: SessionStore,
1013
session: SessionState | undefined,
1114
req: DaemonRequest,
1215
command: string,
1316
result: Record<string, unknown> | undefined,
17+
overrides?: { positionals?: string[]; flags?: CommandFlags },
1418
): void {
1519
if (!session) return;
1620
sessionStore.recordAction(session, {
1721
command,
18-
positionals: req.positionals ?? [],
19-
flags: (req.flags ?? {}) as CommandFlags,
22+
positionals: overrides?.positionals ?? req.positionals ?? [],
23+
flags: overrides?.flags ?? ((req.flags ?? {}) as CommandFlags),
2024
result: result ?? {},
2125
});
2226
}

src/daemon/handlers/install-source.ts

Lines changed: 7 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { isCommandSupportedOnDevice } from '../../core/capabilities.ts';
21
import { resolveTargetDevice, type CommandFlags } from '../../core/dispatch.ts';
32
import { ensureDeviceReady } from '../device-ready.ts';
43
import { getRequestSignal } from '../request-cancel.ts';
@@ -14,7 +13,8 @@ import type { DaemonRequest, DaemonResponse, SessionState } from '../types.ts';
1413
import { resolveInstallFromSourceResultTarget } from '../../client-shared.ts';
1514
import { AppError, normalizeError } from '../../utils/errors.ts';
1615
import { withSuccessText } from '../../utils/success-text.ts';
17-
import { errorResponse } from './response.ts';
16+
import { requireCommandSupported } from './response.ts';
17+
import { recordSessionAction } from './handler-utils.ts';
1818

1919
type PreparedInstallArtifact = {
2020
archivePath?: string;
@@ -121,15 +121,7 @@ function recordInstallFromSourceAction(params: {
121121
data: InstallFromSourceResult & Record<string, unknown>;
122122
}): void {
123123
const { session, sessionStore, req, data } = params;
124-
if (!session) {
125-
return;
126-
}
127-
sessionStore.recordAction(session, {
128-
command: 'install_source',
129-
positionals: [],
130-
flags: req.flags ?? {},
131-
result: data,
132-
});
124+
recordSessionAction(sessionStore, session, req, 'install_source', data, { positionals: [] });
133125
}
134126

135127
export async function handleInstallFromSourceCommand(params: {
@@ -146,12 +138,10 @@ export async function handleInstallFromSourceCommand(params: {
146138
session,
147139
flags: req.flags,
148140
});
149-
if (!isCommandSupportedOnDevice('install', device)) {
150-
return errorResponse(
151-
'UNSUPPORTED_OPERATION',
152-
'install_from_source is not supported on this device',
153-
);
154-
}
141+
const unsupported = requireCommandSupported('install', device, {
142+
message: 'install_from_source is not supported on this device',
143+
});
144+
if (unsupported) return unsupported;
155145

156146
const requestSignal = getRequestSignal(req.meta?.requestId);
157147
const completeInstall = async (

src/daemon/handlers/interaction-runtime.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,15 @@ import { createDaemonRuntimePolicy } from '../runtime-policy.ts';
1414
import { createDaemonRuntimeSessionStore } from '../runtime-session.ts';
1515
import { resolveWebProvider, type WebProvider } from '../../platforms/web/provider.ts';
1616
import { stripAtPrefix } from './interaction-touch-targets.ts';
17+
import { NO_ACTIVE_SESSION_MESSAGE } from './response.ts';
1718

1819
export function createInteractionRuntime(
1920
params: InteractionHandlerParams & {
2021
captureSnapshotForSession: CaptureSnapshotForSession;
2122
},
2223
) {
2324
const session = params.sessionStore.get(params.sessionName);
24-
if (!session) throw new AppError('SESSION_NOT_FOUND', 'No active session. Run open first.');
25+
if (!session) throw new AppError('SESSION_NOT_FOUND', NO_ACTIVE_SESSION_MESSAGE);
2526
return createAgentDevice({
2627
backend: createInteractionBackend({ ...params, session }),
2728
...createDaemonRuntimePolicy('interaction commands', { plural: true }),

src/daemon/handlers/interaction-touch.ts

Lines changed: 8 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { isCommandSupportedOnDevice } from '../../core/capabilities.ts';
21
import type { GestureReferenceFrame } from '../../core/scroll-gesture.ts';
32
import {
43
buttonTag,
@@ -25,7 +24,7 @@ import {
2524
resolveDirectTouchReferenceFrameSafely,
2625
} from './interaction-touch-reference-frame.ts';
2726
import { unsupportedMacOsDesktopSurfaceInteraction } from './interaction-touch-policy.ts';
28-
import { errorResponse } from './response.ts';
27+
import { errorResponse, noActiveSessionError, requireCommandSupported } from './response.ts';
2928
import {
3029
assertAndroidPressStayedInApp,
3130
isAndroidEscapeError,
@@ -79,7 +78,7 @@ async function dispatchTargetedTouchViaRuntime(
7978
): Promise<DaemonResponse> {
8079
const { req, sessionName, sessionStore } = params;
8180
const session = sessionStore.get(sessionName);
82-
if (!session) return errorResponse('SESSION_NOT_FOUND', 'No active session. Run open first.');
81+
if (!session) return noActiveSessionError();
8382

8483
const commandLabel = command === 'click' ? 'click' : command;
8584
const capabilityCommand = command === 'longpress' ? 'longpress' : 'press';
@@ -88,12 +87,8 @@ async function dispatchTargetedTouchViaRuntime(
8887
commandLabel,
8988
);
9089
if (unsupportedSurfaceResponse) return unsupportedSurfaceResponse;
91-
if (!isCommandSupportedOnDevice(capabilityCommand, session.device)) {
92-
return errorResponse(
93-
'UNSUPPORTED_OPERATION',
94-
`${capabilityCommand} is not supported on this device`,
95-
);
96-
}
90+
const unsupported = requireCommandSupported(capabilityCommand, session.device);
91+
if (unsupported) return unsupported;
9792

9893
const clickButton = resolveClickButton(req.flags);
9994
const resultButtonTag = buttonTag(clickButton);
@@ -410,11 +405,10 @@ async function dispatchFillViaRuntime(
410405
if (session) {
411406
const unsupportedSurfaceResponse = unsupportedMacOsDesktopSurfaceInteraction(session, 'fill');
412407
if (unsupportedSurfaceResponse) return unsupportedSurfaceResponse;
408+
const unsupported = requireCommandSupported('fill', session.device);
409+
if (unsupported) return unsupported;
413410
}
414-
if (session && !isCommandSupportedOnDevice('fill', session.device)) {
415-
return errorResponse('UNSUPPORTED_OPERATION', 'fill is not supported on this device');
416-
}
417-
if (!session) return errorResponse('SESSION_NOT_FOUND', 'No active session. Run open first.');
411+
if (!session) return noActiveSessionError();
418412

419413
const parsedTarget = parseFillTarget(req.positionals ?? []);
420414
if (!parsedTarget.ok) return parsedTarget.response;
@@ -529,7 +523,7 @@ async function dispatchRuntimeInteraction<
529523
},
530524
): Promise<DaemonResponse> {
531525
const session = params.sessionStore.get(params.sessionName);
532-
if (!session) return errorResponse('SESSION_NOT_FOUND', 'No active session. Run open first.');
526+
if (!session) return noActiveSessionError();
533527
const runtime = createInteractionRuntime(params);
534528
const actionStartedAt = Date.now();
535529
try {

src/daemon/handlers/interaction.ts

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,8 @@ import { refSnapshotFlagGuardResponse } from './interaction-flags.ts';
66
import { dispatchGetViaRuntime, dispatchIsViaRuntime } from '../selector-runtime.ts';
77
import { createInteractionRuntime } from './interaction-runtime.ts';
88
import { finalizeTouchInteraction } from './interaction-common.ts';
9-
import { errorResponse } from './response.ts';
9+
import { errorResponse, noActiveSessionError, requireCommandSupported } from './response.ts';
1010
import { PUBLIC_COMMANDS } from '../../command-catalog.ts';
11-
import { isCommandSupportedOnDevice } from '../../core/capabilities.ts';
1211
import { normalizeError } from '../../utils/errors.ts';
1312
import { successText } from '../../utils/success-text.ts';
1413
import {
@@ -50,10 +49,9 @@ async function dispatchTypeViaRuntime(
5049
): Promise<DaemonResponse> {
5150
const { sessionName, sessionStore } = params;
5251
const session = sessionStore.get(sessionName);
53-
if (!session) return errorResponse('SESSION_NOT_FOUND', 'No active session. Run open first.');
54-
if (!isCommandSupportedOnDevice(PUBLIC_COMMANDS.type, session.device)) {
55-
return errorResponse('UNSUPPORTED_OPERATION', 'type is not supported on this device');
56-
}
52+
if (!session) return noActiveSessionError();
53+
const unsupported = requireCommandSupported(PUBLIC_COMMANDS.type, session.device);
54+
if (unsupported) return unsupported;
5755
const recordingRecoveryResponse = await recoverAndroidRecordingDialogForType(session);
5856
if (recordingRecoveryResponse) return recordingRecoveryResponse;
5957

src/daemon/handlers/react-native.ts

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { dispatchCommand } from '../../core/dispatch.ts';
2-
import { isCommandSupportedOnDevice } from '../../core/capabilities.ts';
32
import { PUBLIC_COMMANDS } from '../../command-catalog.ts';
43
import {
54
analyzeReactNativeOverlay,
@@ -14,7 +13,7 @@ import {
1413
type SnapshotQualityVerdict,
1514
} from '../../utils/snapshot-quality.ts';
1615
import type { DaemonResponse, SessionState } from '../types.ts';
17-
import { errorResponse } from './response.ts';
16+
import { errorResponse, noActiveSessionError, requireCommandSupported } from './response.ts';
1817
import { captureSnapshotForSession } from './interaction-snapshot.ts';
1918
import { finalizeTouchInteraction, type InteractionHandlerParams } from './interaction-common.ts';
2019
import { readSnapshotNodesReferenceFrame } from './interaction-touch-reference-frame.ts';
@@ -28,13 +27,11 @@ export async function handleReactNativeCommands(
2827
if (!parsed.ok) return parsed.response;
2928

3029
const session = sessionStore.get(sessionName);
31-
if (!session) return errorResponse('SESSION_NOT_FOUND', 'No active session. Run open first.');
32-
if (!isCommandSupportedOnDevice(PUBLIC_COMMANDS.reactNative, session.device)) {
33-
return errorResponse(
34-
'UNSUPPORTED_OPERATION',
35-
'react-native dismiss-overlay is not supported on this device',
36-
);
37-
}
30+
if (!session) return noActiveSessionError();
31+
const unsupported = requireCommandSupported(PUBLIC_COMMANDS.reactNative, session.device, {
32+
message: 'react-native dismiss-overlay is not supported on this device',
33+
});
34+
if (unsupported) return unsupported;
3835

3936
try {
4037
const snapshot = await captureSnapshotForSession(

0 commit comments

Comments
 (0)