@qaick/executor 0.1.0-alpha.3 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/executor.js CHANGED
@@ -132,11 +132,18 @@ export class DefaultExecutor {
132
132
  const definition = findDefinition(run.request.executionPackage.steps, stepId, invocationPath);
133
133
  if (!located || !definition?.children?.length || definition.control || definition.kind !== 'operation')
134
134
  return;
135
- const nested = await this.executeFlow(run, definition.children, located.inputs, [...(located.invocationPath ?? []).slice(0, -1), definition.id], new Date().toISOString());
135
+ const arguments_ = resolveDeclaredInputs(definition.inputDeclarations, located.inputs);
136
+ if (arguments_.issues.length)
137
+ return;
138
+ const childScope = { ...run.request.inputs, ...arguments_.inputs };
139
+ const nested = await this.executeFlow(run, definition.children, childScope, [...(located.invocationPath ?? []).slice(0, -1), definition.id], new Date().toISOString());
140
+ const outputs = definition.outputBindings && nested.state === 'completed'
141
+ ? { ...definition.staticOutputs, ...resolveBindings(definition.outputBindings, childScope, new Map(nested.steps.map(child => [child.id, child.outputs]))) }
142
+ : nested.steps.at(-1)?.outputs ?? {};
136
143
  const previous = located.attempts ?? [];
137
- located.attempts = [...previous, { attempt: previous.length + 1, kind: 'interactive', state: nested.state === 'completed' ? 'finished' : 'failed', startedAt: nested.startedAt, finishedAt: nested.finishedAt, inputs: located.inputs, outputs: nested.steps.at(-1)?.outputs ?? {}, durationMs: nested.steps.reduce((sum, child) => sum + child.durationMs, 0), children: nested.steps, ...(nested.error ? { error: nested.error } : {}) }];
144
+ located.attempts = [...previous, { attempt: previous.length + 1, kind: 'interactive', state: nested.state === 'completed' ? 'finished' : 'failed', startedAt: nested.startedAt, finishedAt: nested.finishedAt, inputs: located.inputs, outputs, durationMs: nested.steps.reduce((sum, child) => sum + child.durationMs, 0), children: nested.steps, ...(nested.error ? { error: nested.error } : {}) }];
138
145
  located.children = nested.steps;
139
- located.outputs = nested.steps.at(-1)?.outputs ?? {};
146
+ located.outputs = outputs;
140
147
  located.state = nested.state === 'completed' ? 'finished' : nested.state === 'cancelled' ? 'cancelled' : 'failed';
141
148
  located.terminalReason = nested.terminalReason ?? (located.state === 'finished' ? 'completed' : 'failed');
142
149
  located.error = nested.error;
@@ -238,7 +245,7 @@ export class DefaultExecutor {
238
245
  const results = [];
239
246
  let state = 'completed';
240
247
  let error;
241
- for (const step of orderSteps(steps)) {
248
+ for (const step of run.request.executionPackage.ordering === 'routine' ? steps : orderSteps(steps)) {
242
249
  if (run.cancelled || signal.aborted)
243
250
  throw new Error('Execution cancelled.');
244
251
  await this.boundary(run, step, [...path, step.id], signal);
@@ -276,8 +283,17 @@ export class DefaultExecutor {
276
283
  return { ...base, ...controlResult, inputs, durationMs: Date.now() - startedMs };
277
284
  }
278
285
  if (step.kind === 'operation' && step.children) {
279
- const nested = await this.executeFlow(run, step.children, { ...scope, ...inputs }, [...path, step.id], new Date().toISOString(), run.request.executionPackage.target, signal);
280
- const outputs = nested.steps.at(-1)?.outputs ?? {};
286
+ const arguments_ = resolveDeclaredInputs(step.inputDeclarations, inputs);
287
+ if (arguments_.issues.length)
288
+ throw failure('BINDING_RESOLUTION_ERROR', arguments_.issues.map(issue => issue.message).join(' '));
289
+ const childScope = { ...scope, ...arguments_.inputs };
290
+ for (const declaration of step.inputDeclarations ?? [])
291
+ if (!Object.hasOwn(arguments_.inputs, declaration.name))
292
+ delete childScope[declaration.name];
293
+ const nested = await this.executeFlow(run, step.children, childScope, [...path, step.id], new Date().toISOString(), run.request.executionPackage.target, signal);
294
+ const outputs = step.outputBindings && nested.state === 'completed'
295
+ ? { ...step.staticOutputs, ...resolveBindings(step.outputBindings, childScope, new Map(nested.steps.map(child => [child.id, child.outputs]))) }
296
+ : nested.steps.at(-1)?.outputs ?? {};
281
297
  const result = { ...base, state: nested.state === 'completed' ? 'finished' : nested.state, inputs, outputs, durationMs: Date.now() - startedMs, children: nested.steps, terminalReason: nested.terminalReason ?? (nested.state === 'completed' ? 'completed' : 'failed'), error: nested.error };
282
298
  this.emit(run, 'step_finished', result.state, `${step.name} ${result.state}.`, step);
283
299
  return result;
@@ -304,9 +320,12 @@ export class DefaultExecutor {
304
320
  if (control.condition !== undefined && !evaluateCondition(control.condition, { ...scope, ...inputs }, outputs))
305
321
  return { state: 'skipped', outputs: {}, terminalReason: 'condition-skipped' };
306
322
  if (control.type === 'repeat') {
307
- const total = control.iterations ?? 1;
323
+ const total = run.request.executionPackage.ordering === 'routine' ? Math.max(1, Math.min(control.iterations ?? (control.durationMs !== undefined ? 32 : 1), 32)) : control.iterations ?? 1;
324
+ const loopStarted = Date.now();
308
325
  const children = [];
309
326
  for (let current = 1; current <= total; current++) {
327
+ if (run.request.executionPackage.ordering === 'routine' && control.durationMs !== undefined && Date.now() - loopStarted >= control.durationMs)
328
+ break;
310
329
  const child = await this.executeFlow(run, step.children ?? [], { ...scope, ...inputs }, [...path, step.id, String(current)], new Date().toISOString(), run.request.executionPackage.target, signal);
311
330
  children.push(...child.steps.map(item => ({ ...item, iteration: { loopId: step.id, current, total } })));
312
331
  if (child.state !== 'completed' && control.onError !== 'continue')
@@ -316,6 +335,18 @@ export class DefaultExecutor {
316
335
  }
317
336
  return { state: 'finished', outputs: children.at(-1)?.outputs ?? {}, children, terminalReason: 'completed' };
318
337
  }
338
+ if (control.type === 'branch' && run.request.executionPackage.ordering === 'routine') {
339
+ const children = [];
340
+ for (const branch of control.branches ?? []) {
341
+ if (!evaluateCondition(branch.condition, { ...scope, ...inputs }, outputs))
342
+ continue;
343
+ const nested = await this.executeFlow(run, branch.children, { ...scope, ...inputs }, [...path, step.id, branch.id], new Date().toISOString(), run.request.executionPackage.target, signal);
344
+ children.push(...nested.steps);
345
+ if (nested.state !== 'completed' && control.onError !== 'continue')
346
+ return { state: nested.state, outputs: nested.steps.at(-1)?.outputs ?? {}, children, terminalReason: nested.terminalReason, error: nested.error };
347
+ }
348
+ return { state: children.length ? 'finished' : 'skipped', outputs: children.at(-1)?.outputs ?? {}, children, terminalReason: children.length ? 'completed' : 'branch-skipped' };
349
+ }
319
350
  if (control.type === 'branch') {
320
351
  const branch = control.branches?.find(candidate => evaluateCondition(candidate.condition, { ...scope, ...inputs }, outputs));
321
352
  if (!branch)
@@ -325,13 +356,18 @@ export class DefaultExecutor {
325
356
  }
326
357
  if (control.type === 'parallel') {
327
358
  const branches = control.branches ?? [];
359
+ const join = run.request.executionPackage.ordering === 'routine' ? 'all' : control.join;
328
360
  const selected = branches.filter(branch => evaluateCondition(branch.condition, { ...scope, ...inputs }, outputs));
329
361
  const controllers = selected.map(() => new AbortController());
362
+ const abortBranches = () => controllers.forEach(controller => controller.abort());
363
+ signal.addEventListener('abort', abortBranches, { once: true });
364
+ if (signal.aborted)
365
+ abortBranches();
330
366
  let winnerIndex;
331
367
  const branchRuns = await Promise.all(selected.map(async (branch, index) => {
332
368
  try {
333
369
  const result = await this.executeFlow(run, branch.children, { ...scope, ...inputs }, [...path, step.id, branch.id], new Date().toISOString(), run.request.executionPackage.target, controllers[index].signal);
334
- if (control.join === 'first' && result.state === 'completed' && winnerIndex === undefined) {
370
+ if (join === 'first' && result.state === 'completed' && winnerIndex === undefined) {
335
371
  winnerIndex = index;
336
372
  controllers.forEach((controller, candidate) => { if (candidate !== index)
337
373
  controller.abort(); });
@@ -342,12 +378,13 @@ export class DefaultExecutor {
342
378
  return { branch, result: this.baseResult(run, new Date().toISOString(), controllers[index].signal.aborted ? 'cancelled' : 'failed', this.failure(cause, run, controllers[index].signal)) };
343
379
  }
344
380
  }));
381
+ signal.removeEventListener('abort', abortBranches);
345
382
  const nested = branchRuns.map(item => item.result);
346
383
  const children = nested.flatMap(item => item.steps);
347
- const considered = control.join === 'first' && winnerIndex !== undefined ? [branchRuns[winnerIndex]] : branchRuns;
384
+ const considered = join === 'first' && winnerIndex !== undefined ? [branchRuns[winnerIndex]] : branchRuns;
348
385
  const failed = considered.find(item => item.result.state !== 'completed');
349
386
  const branchOutputs = considered.map(({ branch, result }) => ({ id: branch.id, outputs: result.steps.at(-1)?.outputs ?? {} }));
350
- const mergedOutputs = mergeBranchOutputs(branchOutputs, control.mergeBindings, control.merge ?? (control.join === 'first' ? 'first' : 'collect'));
387
+ const mergedOutputs = mergeBranchOutputs(branchOutputs, control.mergeBindings, control.merge ?? (join === 'first' ? 'first' : 'collect'));
351
388
  return { state: failed ? invocationState(failed.result.state) : 'finished', outputs: mergedOutputs, children, terminalReason: failed?.result.terminalReason ?? 'parallel-joined', error: failed?.result.error };
352
389
  }
353
390
  const nested = await this.executeFlow(run, step.children ?? [], { ...scope, ...inputs }, [...path, step.id], new Date().toISOString(), run.request.executionPackage.target, signal);
@@ -6,6 +6,8 @@ export declare function createMigratedExecutor(configuration?: ExecutorConfigura
6
6
  * remains the migrated Runner implementation.
7
7
  */
8
8
  export declare class MigratedExecutor implements Executor {
9
+ private readonly routineRuns;
10
+ private readonly routine;
9
11
  private readonly adapters;
10
12
  private readonly engine;
11
13
  constructor(configuration?: ExecutorConfiguration);
@@ -1,3 +1,4 @@
1
+ import { createExecutor } from './executor.js';
1
2
  import { MigratedRunnerEngine } from './migrated-runner-engine.js';
2
3
  import { MigratedProjectRunnerEngine } from './migrated-project-engine.js';
3
4
  export function createMigratedExecutor(configuration = {}) {
@@ -9,20 +10,30 @@ export function createMigratedExecutor(configuration = {}) {
9
10
  * remains the migrated Runner implementation.
10
11
  */
11
12
  export class MigratedExecutor {
13
+ routineRuns = new Set();
14
+ routine;
12
15
  adapters;
13
16
  engine;
14
17
  constructor(configuration = {}) {
18
+ this.routine = createExecutor(configuration);
15
19
  this.adapters = (configuration.actionExecutors ?? []).map(toActionAdapter);
16
20
  this.engine = new MigratedProjectRunnerEngine(new MigratedRunnerEngine(this.adapters));
17
21
  }
18
- registerActionExecutor(executor) { this.adapters.push(toActionAdapter(executor)); }
22
+ registerActionExecutor(executor) { this.routine.registerActionExecutor(executor); this.adapters.push(toActionAdapter(executor)); }
19
23
  async execute(request) {
24
+ if (request.executionPackage.ordering === 'routine') {
25
+ this.routineRuns.add(request.requestId);
26
+ return this.routine.execute(request);
27
+ }
20
28
  const runnerRequest = request;
21
29
  await this.engine.submit(runnerRequest);
22
30
  return { requestId: request.requestId, result: this.result(request.requestId) };
23
31
  }
24
- async cancel(requestId) { await this.engine.cancel(requestId); }
32
+ async cancel(requestId) { if (this.routineRuns.has(requestId))
33
+ return this.routine.cancel(requestId); await this.engine.cancel(requestId); }
25
34
  async control(requestId, command) {
35
+ if (this.routineRuns.has(requestId))
36
+ return this.routine.control(requestId, command);
26
37
  if (command.type === 'pause')
27
38
  return this.engine.pauseExecution(requestId);
28
39
  if (command.type === 'resume')
@@ -34,12 +45,18 @@ export class MigratedExecutor {
34
45
  if (command.type === 'stop-after')
35
46
  return;
36
47
  }
37
- controlState(requestId) { return this.engine.controlState(requestId); }
38
- async *events(requestId) { yield* this.engine.events(requestId); }
39
- async result(requestId) { return this.engine.result(requestId); }
40
- async retryAction(requestId, stepId, invocationPath) { await this.engine.retryAction(requestId, stepId, invocationPath); }
41
- async retryOperation(requestId, stepId, invocationPath) { await this.engine.retryOperation(requestId, stepId, invocationPath); }
48
+ controlState(requestId) { return this.routineRuns.has(requestId) ? this.routine.controlState(requestId) : this.engine.controlState(requestId); }
49
+ async *events(requestId) { yield* (this.routineRuns.has(requestId) ? this.routine.events(requestId) : this.engine.events(requestId)); }
50
+ async result(requestId) { return (this.routineRuns.has(requestId) ? this.routine.result(requestId) : this.engine.result(requestId)); }
51
+ async retryAction(requestId, stepId, invocationPath) { await (this.routineRuns.has(requestId) ? this.routine.retryAction(requestId, stepId, invocationPath) : this.engine.retryAction(requestId, stepId, invocationPath)); }
52
+ async retryOperation(requestId, stepId, invocationPath) { await (this.routineRuns.has(requestId) ? this.routine.retryOperation(requestId, stepId, invocationPath) : this.engine.retryOperation(requestId, stepId, invocationPath)); }
42
53
  async runFromHere(requestId, stepId, invocationPath) {
54
+ if (this.routineRuns.has(requestId)) {
55
+ const targeted = await this.routine.runFromHere(requestId, stepId, invocationPath);
56
+ if (targeted)
57
+ this.routineRuns.add(targeted.requestId);
58
+ return targeted;
59
+ }
43
60
  const accepted = await this.engine.runFromHere(requestId, stepId, invocationPath);
44
61
  return accepted ? { requestId: accepted.requestId, result: this.result(accepted.requestId) } : undefined;
45
62
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qaick/executor",
3
- "version": "0.1.0-alpha.3",
3
+ "version": "0.1.0",
4
4
  "description": "Host-neutral QAick action execution primitives.",
5
5
  "files": [
6
6
  "dist",
@@ -11,7 +11,7 @@
11
11
  },
12
12
  "publishConfig": {
13
13
  "access": "public",
14
- "tag": "alpha"
14
+ "tag": "latest"
15
15
  },
16
16
  "repository": {
17
17
  "type": "git",
@@ -34,7 +34,7 @@
34
34
  "prepublishOnly": "npm run typecheck && npm test && npm run build"
35
35
  },
36
36
  "dependencies": {
37
- "@qaick/contract": "0.2.12"
37
+ "@qaick/contract": "0.2.13"
38
38
  },
39
39
  "devDependencies": {
40
40
  "@types/node": "^24.13.3",