1- import { AppError } from '../utils/errors .ts' ;
2- import type { BatchStep , CommandFlags } from './dispatch .ts' ;
1+ import type { DaemonRequest , DaemonResponse } from '../contracts .ts' ;
2+ import { AppError , asAppError } from '../utils/errors .ts' ;
33
44export const DEFAULT_BATCH_MAX_STEPS = 100 ;
5- const BATCH_BLOCKED_COMMANDS = new Set ( [ 'batch' , 'replay' ] ) ;
5+ export const BATCH_BLOCKED_COMMANDS : ReadonlySet < string > = new Set ( [ 'batch' , 'replay' ] ) ;
66const BATCH_ALLOWED_STEP_KEYS = new Set ( [ 'command' , 'positionals' , 'flags' , 'runtime' ] ) ;
7+ export const INHERITED_PARENT_FLAG_KEYS = [
8+ 'platform' ,
9+ 'target' ,
10+ 'device' ,
11+ 'udid' ,
12+ 'serial' ,
13+ 'verbose' ,
14+ 'out' ,
15+ ] as const ;
16+
17+ export type BatchStep = {
18+ command : string ;
19+ positionals ?: string [ ] ;
20+ flags ?: Record < string , unknown > ;
21+ runtime ?: unknown ;
22+ } ;
23+
24+ export type BatchFlags = Record < string , unknown > & {
25+ batchOnError ?: 'stop' ;
26+ batchMaxSteps ?: number ;
27+ batchSteps ?: BatchStep [ ] ;
28+ } ;
29+
30+ export type BatchRequest = Omit < DaemonRequest , 'flags' > & {
31+ flags ?: BatchFlags | Record < string , unknown > ;
32+ } ;
33+
34+ export type BatchInvoke = ( req : BatchRequest ) => Promise < DaemonResponse > ;
735
836export type NormalizedBatchStep = {
937 command : string ;
1038 positionals : string [ ] ;
11- flags : Partial < CommandFlags > ;
39+ flags : Record < string , unknown > ;
1240 runtime ?: unknown ;
1341} ;
1442
@@ -20,6 +48,68 @@ export type BatchStepResult = {
2048 durationMs : number ;
2149} ;
2250
51+ export async function runBatch (
52+ req : BatchRequest ,
53+ sessionName : string ,
54+ invoke : BatchInvoke ,
55+ ) : Promise < DaemonResponse > {
56+ const flags = readBatchFlags ( req . flags ) ;
57+ const batchOnError = flags ?. batchOnError ?? 'stop' ;
58+ if ( batchOnError !== 'stop' ) {
59+ return batchErrorResponse ( 'INVALID_ARGS' , `Unsupported batch on-error mode: ${ batchOnError } .` ) ;
60+ }
61+ const batchMaxSteps = flags ?. batchMaxSteps ?? DEFAULT_BATCH_MAX_STEPS ;
62+ if ( ! Number . isInteger ( batchMaxSteps ) || batchMaxSteps < 1 || batchMaxSteps > 1000 ) {
63+ return batchErrorResponse (
64+ 'INVALID_ARGS' ,
65+ `Invalid batch max-steps: ${ String ( flags ?. batchMaxSteps ) } ` ,
66+ ) ;
67+ }
68+ try {
69+ const steps = validateAndNormalizeBatchSteps ( flags ?. batchSteps , batchMaxSteps ) ;
70+ const startedAt = Date . now ( ) ;
71+ const partialResults : BatchStepResult [ ] = [ ] ;
72+ for ( let index = 0 ; index < steps . length ; index += 1 ) {
73+ const step = steps [ index ] ;
74+ const stepResponse = await runBatchStep ( req , sessionName , step , invoke , index + 1 ) ;
75+ if ( ! stepResponse . ok ) {
76+ return {
77+ ok : false ,
78+ error : {
79+ code : stepResponse . error . code ,
80+ message : `Batch failed at step ${ stepResponse . step } (${ step . command } ): ${ stepResponse . error . message } ` ,
81+ hint : stepResponse . error . hint ,
82+ diagnosticId : stepResponse . error . diagnosticId ,
83+ logPath : stepResponse . error . logPath ,
84+ details : {
85+ ...( stepResponse . error . details ?? { } ) ,
86+ step : stepResponse . step ,
87+ command : step . command ,
88+ positionals : step . positionals ,
89+ executed : index ,
90+ total : steps . length ,
91+ partialResults,
92+ } ,
93+ } ,
94+ } ;
95+ }
96+ partialResults . push ( stepResponse . result ) ;
97+ }
98+ return {
99+ ok : true ,
100+ data : {
101+ total : steps . length ,
102+ executed : steps . length ,
103+ totalDurationMs : Date . now ( ) - startedAt ,
104+ results : partialResults ,
105+ } ,
106+ } ;
107+ } catch ( error ) {
108+ const appErr = asAppError ( error ) ;
109+ return batchErrorResponse ( appErr . code , appErr . message , appErr . details ) ;
110+ }
111+ }
112+
23113export function parseBatchStepsJson ( raw : string ) : BatchStep [ ] {
24114 let parsed : unknown ;
25115 try {
@@ -34,7 +124,7 @@ export function parseBatchStepsJson(raw: string): BatchStep[] {
34124}
35125
36126export function validateAndNormalizeBatchSteps (
37- steps : CommandFlags [ 'batchSteps' ] ,
127+ steps : unknown ,
38128 maxSteps : number ,
39129) : NormalizedBatchStep [ ] {
40130 if ( ! Array . isArray ( steps ) || steps . length === 0 ) {
@@ -49,7 +139,7 @@ export function validateAndNormalizeBatchSteps(
49139
50140 const normalized : NormalizedBatchStep [ ] = [ ] ;
51141 for ( let index = 0 ; index < steps . length ; index += 1 ) {
52- const step = steps [ index ] ;
142+ const step = steps [ index ] as Partial < BatchStep > ;
53143 if ( ! step || typeof step !== 'object' ) {
54144 throw new AppError ( 'INVALID_ARGS' , `Invalid batch step at index ${ index } .` ) ;
55145 }
@@ -93,9 +183,105 @@ export function validateAndNormalizeBatchSteps(
93183 normalized . push ( {
94184 command,
95185 positionals : positionals as string [ ] ,
96- flags : ( step . flags ?? { } ) as Partial < CommandFlags > ,
186+ flags : ( step . flags ?? { } ) as Record < string , unknown > ,
97187 runtime : step . runtime ,
98188 } ) ;
99189 }
100190 return normalized ;
101191}
192+
193+ export function buildBatchStepFlags (
194+ parentFlags : BatchFlags | Record < string , unknown > | undefined ,
195+ stepFlags : BatchStep [ 'flags' ] | Record < string , unknown > | undefined ,
196+ ) : BatchFlags {
197+ const {
198+ batchSteps : _batchSteps ,
199+ batchOnError : _batchOnError ,
200+ batchMaxSteps : _batchMaxSteps ,
201+ ...merged
202+ } = stepFlags ?? { } ;
203+ return mergeParentFlags ( readBatchFlags ( parentFlags ) , merged as BatchFlags ) ;
204+ }
205+
206+ export function mergeParentFlags < TFlags extends Record < string , unknown > > (
207+ parentFlags : BatchFlags | Record < string , unknown > | undefined ,
208+ childFlags : TFlags ,
209+ ) : TFlags {
210+ const parentRecord = readBatchFlags ( parentFlags ) ?? { } ;
211+ const childRecord = childFlags as Record < string , unknown > ;
212+ for ( const key of INHERITED_PARENT_FLAG_KEYS ) {
213+ if ( childRecord [ key ] === undefined && parentRecord [ key ] !== undefined ) {
214+ childRecord [ key ] = parentRecord [ key ] ;
215+ }
216+ }
217+ return childFlags ;
218+ }
219+
220+ async function runBatchStep (
221+ req : BatchRequest ,
222+ sessionName : string ,
223+ step : NormalizedBatchStep ,
224+ invoke : BatchInvoke ,
225+ stepNumber : number ,
226+ ) : Promise <
227+ | { ok : true ; step : number ; result : BatchStepResult }
228+ | {
229+ ok : false ;
230+ step : number ;
231+ error : {
232+ code : string ;
233+ message : string ;
234+ hint ?: string ;
235+ diagnosticId ?: string ;
236+ logPath ?: string ;
237+ details ?: Record < string , unknown > ;
238+ } ;
239+ }
240+ > {
241+ const stepStartedAt = Date . now ( ) ;
242+ const stepFlags = buildBatchStepFlags ( req . flags , step . flags ) ;
243+ if ( stepFlags . session === undefined ) {
244+ stepFlags . session = sessionName ;
245+ }
246+ const response = await invoke ( {
247+ token : req . token ,
248+ session : sessionName ,
249+ command : step . command ,
250+ positionals : step . positionals ,
251+ flags : stepFlags ,
252+ runtime : ( step . runtime === undefined ? req . runtime : step . runtime ) as DaemonRequest [ 'runtime' ] ,
253+ meta : req . meta ,
254+ } ) ;
255+ const durationMs = Date . now ( ) - stepStartedAt ;
256+ if ( ! response . ok ) {
257+ return { ok : false , step : stepNumber , error : response . error } ;
258+ }
259+ return {
260+ ok : true ,
261+ step : stepNumber ,
262+ result : {
263+ step : stepNumber ,
264+ command : step . command ,
265+ ok : true ,
266+ data : response . data ?? { } ,
267+ durationMs,
268+ } ,
269+ } ;
270+ }
271+
272+ function readBatchFlags (
273+ flags : BatchFlags | Record < string , unknown > | undefined ,
274+ ) : BatchFlags | undefined {
275+ return flags as BatchFlags | undefined ;
276+ }
277+
278+ function batchErrorResponse (
279+ code : string ,
280+ message : string ,
281+ details ?: Record < string , unknown > ,
282+ ) : Extract < DaemonResponse , { ok : false } > {
283+ return {
284+ ok : false ,
285+ error : { code, message, ...( details ? { details } : { } ) } ,
286+ } ;
287+ }
0 commit comments