@usearazzo/runner 0.0.0-stage → 1.0.1-alpha.7
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/LICENSE +202 -0
- package/NOTICE +20 -0
- package/README.md +784 -2
- package/dist/arazzo-runner.browser.js +111247 -0
- package/dist/arazzo-runner.browser.min.js +1 -0
- package/package.json +88 -4
- package/src/action/ActionResolver.cjs +160 -0
- package/src/action/ActionResolver.mjs +150 -0
- package/src/assembler/OpenAPI2DocumentAssembler.cjs +111 -0
- package/src/assembler/OpenAPI2DocumentAssembler.mjs +104 -0
- package/src/assembler/OpenAPI30DocumentAssembler.cjs +110 -0
- package/src/assembler/OpenAPI30DocumentAssembler.mjs +103 -0
- package/src/assembler/OpenAPI31DocumentAssembler.cjs +111 -0
- package/src/assembler/OpenAPI31DocumentAssembler.mjs +104 -0
- package/src/assembler/OpenAPIDocumentAssembler.cjs +58 -0
- package/src/assembler/OpenAPIDocumentAssembler.mjs +50 -0
- package/src/client/HTTPClient.cjs +5 -0
- package/src/client/HTTPClient.mjs +1 -0
- package/src/client/HTTPClientFetch.cjs +16 -0
- package/src/client/HTTPClientFetch.mjs +10 -0
- package/src/client/OpenAPIOperationRequest.cjs +5 -0
- package/src/client/OpenAPIOperationRequest.mjs +1 -0
- package/src/client/OpenAPIOperationResponse.cjs +42 -0
- package/src/client/OpenAPIOperationResponse.mjs +36 -0
- package/src/client/OpenAPIOperationResponseNormalizer.cjs +48 -0
- package/src/client/OpenAPIOperationResponseNormalizer.mjs +40 -0
- package/src/client/ParameterDelivery.cjs +119 -0
- package/src/client/ParameterDelivery.mjs +113 -0
- package/src/constants.cjs +9 -0
- package/src/constants.mjs +3 -0
- package/src/criterion/CriterionEvaluator.cjs +205 -0
- package/src/criterion/CriterionEvaluator.mjs +196 -0
- package/src/criterion/JSONPathCriterionEvaluator.cjs +47 -0
- package/src/criterion/JSONPathCriterionEvaluator.mjs +40 -0
- package/src/criterion/RegexCriterionEvaluator.cjs +67 -0
- package/src/criterion/RegexCriterionEvaluator.mjs +60 -0
- package/src/criterion/SimpleCriterionEvaluator.cjs +50 -0
- package/src/criterion/SimpleCriterionEvaluator.mjs +43 -0
- package/src/criterion/XPathCriterionEvaluator.cjs +117 -0
- package/src/criterion/XPathCriterionEvaluator.mjs +109 -0
- package/src/document/APIDocument.cjs +35 -0
- package/src/document/APIDocument.mjs +30 -0
- package/src/document/ArazzoDocument.cjs +81 -0
- package/src/document/ArazzoDocument.mjs +74 -0
- package/src/document/ArazzoSourceDescriptionIndex.cjs +26 -0
- package/src/document/ArazzoSourceDescriptionIndex.mjs +20 -0
- package/src/document/ArazzoStepIndex.cjs +25 -0
- package/src/document/ArazzoStepIndex.mjs +19 -0
- package/src/document/ArazzoWorkflowIndex.cjs +17 -0
- package/src/document/ArazzoWorkflowIndex.mjs +11 -0
- package/src/document/OpenAPIDocument.cjs +28 -0
- package/src/document/OpenAPIDocument.mjs +21 -0
- package/src/document/OpenAPIOperationIndex.cjs +24 -0
- package/src/document/OpenAPIOperationIndex.mjs +18 -0
- package/src/document/ParameterIdentity.cjs +93 -0
- package/src/document/ParameterIdentity.mjs +88 -0
- package/src/document/openapi-types.cjs +5 -0
- package/src/document/openapi-types.mjs +1 -0
- package/src/errors/ArazzoRunnerError.cjs +10 -0
- package/src/errors/ArazzoRunnerError.mjs +5 -0
- package/src/errors/AssemblerError.cjs +11 -0
- package/src/errors/AssemblerError.mjs +4 -0
- package/src/errors/ClientError.cjs +11 -0
- package/src/errors/ClientError.mjs +4 -0
- package/src/errors/CriterionError.cjs +11 -0
- package/src/errors/CriterionError.mjs +4 -0
- package/src/errors/ExecutionError.cjs +11 -0
- package/src/errors/ExecutionError.mjs +4 -0
- package/src/errors/ExtractionError.cjs +11 -0
- package/src/errors/ExtractionError.mjs +4 -0
- package/src/errors/InvalidEntryDocumentError.cjs +11 -0
- package/src/errors/InvalidEntryDocumentError.mjs +4 -0
- package/src/errors/NormalizationError.cjs +11 -0
- package/src/errors/NormalizationError.mjs +4 -0
- package/src/errors/ResolverError.cjs +11 -0
- package/src/errors/ResolverError.mjs +4 -0
- package/src/errors/RuntimeExpressionError.cjs +11 -0
- package/src/errors/RuntimeExpressionError.mjs +4 -0
- package/src/errors/UnmatchedProviderError.cjs +11 -0
- package/src/errors/UnmatchedProviderError.mjs +4 -0
- package/src/executor/ArazzoWorkflowLocatorNormalizer.cjs +147 -0
- package/src/executor/ArazzoWorkflowLocatorNormalizer.mjs +137 -0
- package/src/executor/OpenAPIOperationExecutor.cjs +457 -0
- package/src/executor/OpenAPIOperationExecutor.mjs +446 -0
- package/src/executor/OpenAPIOperationLocatorNormalizer.cjs +197 -0
- package/src/executor/OpenAPIOperationLocatorNormalizer.mjs +189 -0
- package/src/executor/SourceDescriptionsLocatorNormalizer.cjs +44 -0
- package/src/executor/SourceDescriptionsLocatorNormalizer.mjs +38 -0
- package/src/executor/StepExecutor.cjs +333 -0
- package/src/executor/StepExecutor.mjs +326 -0
- package/src/executor/StepRetryRunner.cjs +226 -0
- package/src/executor/StepRetryRunner.mjs +215 -0
- package/src/executor/StepTransitionInterpreter.cjs +159 -0
- package/src/executor/StepTransitionInterpreter.mjs +151 -0
- package/src/executor/WorkflowCallStack.cjs +116 -0
- package/src/executor/WorkflowCallStack.mjs +106 -0
- package/src/executor/WorkflowExecutor.cjs +1091 -0
- package/src/executor/WorkflowExecutor.mjs +1077 -0
- package/src/executor/abort.cjs +70 -0
- package/src/executor/abort.mjs +60 -0
- package/src/expression/RuntimeExpressionContext.cjs +5 -0
- package/src/expression/RuntimeExpressionContext.mjs +1 -0
- package/src/expression/RuntimeExpressionEvaluator.cjs +337 -0
- package/src/expression/RuntimeExpressionEvaluator.mjs +328 -0
- package/src/extractor/ArazzoStepExtractor.cjs +48 -0
- package/src/extractor/ArazzoStepExtractor.mjs +41 -0
- package/src/extractor/ArazzoWorkflowExtractor.cjs +50 -0
- package/src/extractor/ArazzoWorkflowExtractor.mjs +43 -0
- package/src/extractor/OpenAPIOperationExtractor.cjs +84 -0
- package/src/extractor/OpenAPIOperationExtractor.mjs +77 -0
- package/src/index.cjs +419 -0
- package/src/index.mjs +73 -0
- package/src/normalizer/ArazzoStepNormalizer.cjs +23 -0
- package/src/normalizer/ArazzoStepNormalizer.mjs +17 -0
- package/src/normalizer/ArazzoWorkflowNormalizer.cjs +214 -0
- package/src/normalizer/ArazzoWorkflowNormalizer.mjs +206 -0
- package/src/normalizer/OpenAPI2OperationNormalizer.cjs +128 -0
- package/src/normalizer/OpenAPI2OperationNormalizer.mjs +120 -0
- package/src/normalizer/OpenAPI30OperationNormalizer.cjs +129 -0
- package/src/normalizer/OpenAPI30OperationNormalizer.mjs +121 -0
- package/src/normalizer/OpenAPI31OperationNormalizer.cjs +131 -0
- package/src/normalizer/OpenAPI31OperationNormalizer.mjs +123 -0
- package/src/normalizer/OpenAPIOperationNormalizer.cjs +58 -0
- package/src/normalizer/OpenAPIOperationNormalizer.mjs +50 -0
- package/src/registry/DocumentRegistry.cjs +160 -0
- package/src/registry/DocumentRegistry.mjs +151 -0
- package/src/registry/providers/ArazzoDocumentRegistryProvider.cjs +154 -0
- package/src/registry/providers/ArazzoDocumentRegistryProvider.mjs +146 -0
- package/src/registry/providers/DocumentRegistryProvider.cjs +31 -0
- package/src/registry/providers/DocumentRegistryProvider.mjs +25 -0
- package/src/registry/providers/OpenAPIDocumentRegistryProvider.cjs +122 -0
- package/src/registry/providers/OpenAPIDocumentRegistryProvider.mjs +114 -0
- package/src/resolver/ArazzoValueResolver.cjs +42 -0
- package/src/resolver/ArazzoValueResolver.mjs +37 -0
- package/src/resolver/OpenAPIOperationParameterResolver.cjs +117 -0
- package/src/resolver/OpenAPIOperationParameterResolver.mjs +110 -0
- package/src/resolver/OutputResolver.cjs +71 -0
- package/src/resolver/OutputResolver.mjs +64 -0
- package/src/resolver/RequestBodyResolver.cjs +227 -0
- package/src/resolver/RequestBodyResolver.mjs +220 -0
- package/src/resolver/ResolverScope.cjs +5 -0
- package/src/resolver/ResolverScope.mjs +1 -0
- package/src/resolver/StepParameterResolver.cjs +22 -0
- package/src/resolver/StepParameterResolver.mjs +15 -0
- package/src/resolver/WorkflowParameterResolver.cjs +73 -0
- package/src/resolver/WorkflowParameterResolver.mjs +66 -0
- package/src/state/WorkflowExecutionState.cjs +99 -0
- package/src/state/WorkflowExecutionState.mjs +93 -0
- package/src/vendor/swagger-client.cjs +1 -0
- package/src/vendor/swagger-client.mjs +1 -0
- package/types/runner.d.ts +2341 -0
|
@@ -0,0 +1,1077 @@
|
|
|
1
|
+
import { toValue } from '@speclynx/apidom-core';
|
|
2
|
+
import { isArrayElement, isObjectElement, isStringElement } from '@speclynx/apidom-datamodel';
|
|
3
|
+
import { isStepElement } from '@speclynx/apidom-ns-arazzo-1';
|
|
4
|
+
import RuntimeExpressionEvaluator from "../expression/RuntimeExpressionEvaluator.mjs";
|
|
5
|
+
import ArazzoWorkflowExtractor from "../extractor/ArazzoWorkflowExtractor.mjs";
|
|
6
|
+
import ArazzoWorkflowNormalizer from "../normalizer/ArazzoWorkflowNormalizer.mjs";
|
|
7
|
+
import OutputResolver from "../resolver/OutputResolver.mjs";
|
|
8
|
+
import WorkflowParameterResolver from "../resolver/WorkflowParameterResolver.mjs";
|
|
9
|
+
import WorkflowExecutionState from "../state/WorkflowExecutionState.mjs";
|
|
10
|
+
import { STEP_TARGET_FIELDS } from "./StepExecutor.mjs";
|
|
11
|
+
import ArazzoWorkflowLocatorNormalizer from "./ArazzoWorkflowLocatorNormalizer.mjs";
|
|
12
|
+
import StepRetryRunner from "./StepRetryRunner.mjs";
|
|
13
|
+
import WorkflowCallStack from "./WorkflowCallStack.mjs";
|
|
14
|
+
import StepTransitionInterpreter, { actionTargets } from "./StepTransitionInterpreter.mjs";
|
|
15
|
+
import ExecutionError from "../errors/ExecutionError.mjs";
|
|
16
|
+
import ResolverError from "../errors/ResolverError.mjs";
|
|
17
|
+
import { readAbortSignal, throwIfAborted } from "./abort.mjs";
|
|
18
|
+
/**
|
|
19
|
+
* Options for the WorkflowExecutor.
|
|
20
|
+
* @public
|
|
21
|
+
*/
|
|
22
|
+
/**
|
|
23
|
+
* Per-call options for {@link WorkflowExecutor.execute}.
|
|
24
|
+
* @public
|
|
25
|
+
*/
|
|
26
|
+
/**
|
|
27
|
+
* One run of a `retry` action's `stepId` / `workflowId` reference — "the
|
|
28
|
+
* reference is executed and the context is returned, after which the current
|
|
29
|
+
* step is retried". Fires once per firing of its action (i.e. once before every
|
|
30
|
+
* attempt the action grants, not once for the whole retry chain).
|
|
31
|
+
* @public
|
|
32
|
+
*/
|
|
33
|
+
/**
|
|
34
|
+
* The trace of a single step execution within a workflow run.
|
|
35
|
+
* @public
|
|
36
|
+
*/
|
|
37
|
+
/**
|
|
38
|
+
* The outcome of executing a workflow.
|
|
39
|
+
*
|
|
40
|
+
* `status` is `completed` when the steps ran to the end of the list, `ended`
|
|
41
|
+
* when an `end` action stopped the run early, `failed` when a step failed and no
|
|
42
|
+
* matching `onFailure` action redirected it (the break-and-return default) — or
|
|
43
|
+
* when a `dependsOn` workflow did not complete — and `transferred` when a `goto`
|
|
44
|
+
* action handed control one-way to another workflow: this run's own steps stop
|
|
45
|
+
* at that point, and its `outputs` declaration is never evaluated, since the run
|
|
46
|
+
* never reaches its own end. See {@link WorkflowExecutionResult.settledStatus}
|
|
47
|
+
* for the success/failure verdict a `'transferred'` result defers to.
|
|
48
|
+
* @public
|
|
49
|
+
*/
|
|
50
|
+
/**
|
|
51
|
+
* The result a transfer chain settled on: `result` itself when its `status`
|
|
52
|
+
* isn't `'transferred'`, otherwise the terminal, non-`'transferred'` result at
|
|
53
|
+
* the end of its `transferredTo` chain.
|
|
54
|
+
*
|
|
55
|
+
* For the success/failure verdict alone, prefer
|
|
56
|
+
* {@link WorkflowExecutionResult.settledStatus} — every result already
|
|
57
|
+
* carries it, computed eagerly, so no call is needed. Reach for this function
|
|
58
|
+
* when the terminal result *itself* is wanted (its `outputs`, `steps`,
|
|
59
|
+
* `workflowId`), not just where it landed. Iterative, not recursive: a
|
|
60
|
+
* chain's length is bounded only by `maxWorkflowDepth`, and this must not add
|
|
61
|
+
* a matching bound on call-stack depth.
|
|
62
|
+
* @public
|
|
63
|
+
*/
|
|
64
|
+
export function settledResult(result) {
|
|
65
|
+
let current = result;
|
|
66
|
+
while (current.transferredTo !== undefined) {
|
|
67
|
+
current = current.transferredTo;
|
|
68
|
+
}
|
|
69
|
+
return current;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* How a `#run` invocation ended, handed to `#result` to assemble the final
|
|
74
|
+
* {@link WorkflowExecutionResult} from.
|
|
75
|
+
*
|
|
76
|
+
* A discriminated union rather than a `status` string plus a separately-set
|
|
77
|
+
* `transferredTo` — the shape this replaced, where nothing but convention
|
|
78
|
+
* kept the two in sync. Here, `status: 'transferred'` with no
|
|
79
|
+
* `transferredTo` (or the reverse) is not a state a caller of `#result` can
|
|
80
|
+
* construct, not merely one they are expected not to.
|
|
81
|
+
* @internal
|
|
82
|
+
*/
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* One completed `dependsOn` run, remembered with the inputs it was actually
|
|
86
|
+
* given — see {@link RunScope.dependencyRuns}.
|
|
87
|
+
*/
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* The state shared by every workflow invocation of a single
|
|
91
|
+
* {@link WorkflowExecutor.execute} call — the caller's per-run options plus the
|
|
92
|
+
* two mutable ledgers that must span the whole call tree.
|
|
93
|
+
*/
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* One workflow invocation in progress: the values fixed for a single `#run` that
|
|
97
|
+
* travel together into everything it delegates to.
|
|
98
|
+
*/
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Executes an Arazzo workflow: the stateful loop that turns "run one step" into
|
|
102
|
+
* "run a workflow".
|
|
103
|
+
*
|
|
104
|
+
* It iterates a workflow's steps in list order, delegating each to
|
|
105
|
+
* {@link StepExecutor}, records the resolved step outputs into a
|
|
106
|
+
* {@link WorkflowExecutionState} so later steps read `$steps.{id}.outputs.{name}`,
|
|
107
|
+
* and interprets the {@link SelectedAction} the step executor selects but does
|
|
108
|
+
* not act on — advancing to the next step, jumping via `goto`, transferring
|
|
109
|
+
* one-way to another workflow via a `goto` naming a `workflowId`, or stopping on
|
|
110
|
+
* `end` / the failure break-default. After the loop it resolves the workflow's
|
|
111
|
+
* `outputs` against the final state — unless the run transferred, in which case
|
|
112
|
+
* it never reaches its own end and `outputs` stays `{}`.
|
|
113
|
+
*
|
|
114
|
+
* A step targeting a `workflowId` is a sub-workflow call the executor runs
|
|
115
|
+
* itself, recursively; the workflows named by `dependsOn` are run to completion
|
|
116
|
+
* before the workflow's own steps. Both recurse through one call tree, guarded
|
|
117
|
+
* by a shared step budget, a nesting-depth ceiling, and cycle detection. A
|
|
118
|
+
* `goto`'s transfer joins the same call tree, entered via `'goto'`, but is not
|
|
119
|
+
* charged against the step budget: it fires once per step evaluation, unlike a
|
|
120
|
+
* retry reference, which can fire repeatedly from the same frame.
|
|
121
|
+
*
|
|
122
|
+
* State is created fresh per {@link WorkflowExecutor.execute} call — and per
|
|
123
|
+
* workflow invocation within it — and owned here; the returned result is
|
|
124
|
+
* read-only. Authoring errors (missing workflow, unknown `goto` target, a cycle,
|
|
125
|
+
* budget overflow) throw {@link ExecutionError}, as does a run the caller
|
|
126
|
+
* cancels through {@link WorkflowExecuteOptions.signal}; a step that
|
|
127
|
+
* legitimately fails and breaks, or a dependency that fails, is a normal
|
|
128
|
+
* `status: 'failed'` result, not a throw — the same split {@link StepExecutor}
|
|
129
|
+
* draws.
|
|
130
|
+
*
|
|
131
|
+
* A workflow reference — a sub-workflow step's `workflowId`, a `dependsOn`
|
|
132
|
+
* entry, a `retry` action's reference, a `goto`'s transfer — may also name a
|
|
133
|
+
* workflow of another Arazzo document, as
|
|
134
|
+
* `$sourceDescriptions.{name}.{workflowId}`. The run then follows that
|
|
135
|
+
* document for the foreign workflow: its id lookups, its steps' operations,
|
|
136
|
+
* and its expressions all resolve against the document that owns it, while
|
|
137
|
+
* the `$workflows.{workflowId}` run state stays keyed by the bare id — the
|
|
138
|
+
* only form the runtime-expression grammar can express.
|
|
139
|
+
* @public
|
|
140
|
+
*/
|
|
141
|
+
class WorkflowExecutor {
|
|
142
|
+
static #DEFAULT_MAX_STEPS = 1000;
|
|
143
|
+
static #DEFAULT_MAX_WORKFLOW_DEPTH = 32;
|
|
144
|
+
static #DEFAULT_NOW = () => performance.now();
|
|
145
|
+
static #EXECUTE_OPTION_KEYS = new Set(['inputs', 'executeOptions', 'dependencyInputs', 'runDependencies', 'signal']);
|
|
146
|
+
#document;
|
|
147
|
+
#registry;
|
|
148
|
+
#maxSteps;
|
|
149
|
+
#maxWorkflowDepth;
|
|
150
|
+
#now;
|
|
151
|
+
#extractor = new ArazzoWorkflowExtractor();
|
|
152
|
+
#normalizer = new ArazzoWorkflowNormalizer();
|
|
153
|
+
#outputResolver = new OutputResolver();
|
|
154
|
+
#parameterResolver = new WorkflowParameterResolver();
|
|
155
|
+
#stepExecutor;
|
|
156
|
+
#retryRunner;
|
|
157
|
+
#interpreter = new StepTransitionInterpreter();
|
|
158
|
+
#workflowLocatorNormalizer;
|
|
159
|
+
constructor(options) {
|
|
160
|
+
this.#document = options.document;
|
|
161
|
+
this.#registry = options.registry;
|
|
162
|
+
this.#workflowLocatorNormalizer = new ArazzoWorkflowLocatorNormalizer(options.registry);
|
|
163
|
+
this.#maxSteps = options.maxSteps ?? WorkflowExecutor.#DEFAULT_MAX_STEPS;
|
|
164
|
+
this.#maxWorkflowDepth = options.maxWorkflowDepth ?? WorkflowExecutor.#DEFAULT_MAX_WORKFLOW_DEPTH;
|
|
165
|
+
this.#now = options.now ?? WorkflowExecutor.#DEFAULT_NOW;
|
|
166
|
+
this.#stepExecutor = options.stepExecutor;
|
|
167
|
+
this.#retryRunner = new StepRetryRunner({
|
|
168
|
+
sleep: options.sleep
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Runs the named workflow to completion, returning its outcome.
|
|
174
|
+
*
|
|
175
|
+
* The workflows the target names in `dependsOn` are run first (see
|
|
176
|
+
* {@link WorkflowExecuteOptions.runDependencies}), then its own steps.
|
|
177
|
+
*
|
|
178
|
+
* All run-scoped state — the execution state, the step trace, the control-flow
|
|
179
|
+
* position, the call stack, the step budget, and the completed-dependency
|
|
180
|
+
* ledger — is local to this call, so concurrent `execute` calls on one executor
|
|
181
|
+
* do not interfere.
|
|
182
|
+
*/
|
|
183
|
+
async execute(workflowId, options = {}) {
|
|
184
|
+
this.#rejectUnknownOptions(options);
|
|
185
|
+
|
|
186
|
+
// a signal in the opaque bag was the only channel before this option existed,
|
|
187
|
+
// and it still reaches the transport. Absorbing it here is what makes it a
|
|
188
|
+
// whole cancellation rather than half of one: read only at dispatch, it would
|
|
189
|
+
// abort requests while the loop walked on through the remaining steps.
|
|
190
|
+
const signal = options.signal ?? readAbortSignal(options.executeOptions ?? {});
|
|
191
|
+
const scope = {
|
|
192
|
+
// the first-class signal is also spread into the opaque bag, because that
|
|
193
|
+
// bag is how anything reaches the transport: the executor's own checks
|
|
194
|
+
// stop the run at the next boundary, and this is what cancels the request
|
|
195
|
+
// already in flight. Spread last, so it wins over one passed in the bag.
|
|
196
|
+
executeOptions: {
|
|
197
|
+
...options.executeOptions,
|
|
198
|
+
...(signal === undefined ? {} : {
|
|
199
|
+
signal
|
|
200
|
+
})
|
|
201
|
+
},
|
|
202
|
+
dependencyInputs: options.dependencyInputs ?? {},
|
|
203
|
+
runDependencies: options.runDependencies ?? true,
|
|
204
|
+
signal,
|
|
205
|
+
budget: {
|
|
206
|
+
spent: 0
|
|
207
|
+
},
|
|
208
|
+
dependencyRuns: new Map(),
|
|
209
|
+
workflows: new Map()
|
|
210
|
+
};
|
|
211
|
+
return this.#run({
|
|
212
|
+
document: this.#document,
|
|
213
|
+
workflowId
|
|
214
|
+
}, options.inputs ?? {}, scope, new WorkflowCallStack({
|
|
215
|
+
maxDepth: this.#maxWorkflowDepth,
|
|
216
|
+
entryDocumentURI: this.#document.uri
|
|
217
|
+
}), 'root');
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Rejects an options bag carrying keys this method does not recognize.
|
|
222
|
+
*
|
|
223
|
+
* `execute` used to take `inputs` as its second positional argument, and that
|
|
224
|
+
* bag is now the options object — so the old call reaches here as an object of
|
|
225
|
+
* unrecognized keys. Left unchecked it would run with no inputs at all, and
|
|
226
|
+
* every `$inputs.*` would resolve to `undefined` under lenient evaluation:
|
|
227
|
+
* a completed run issuing wrong requests. A typo in an option name fails the
|
|
228
|
+
* same silent way. Both are worth a loud error.
|
|
229
|
+
*/
|
|
230
|
+
#rejectUnknownOptions(options) {
|
|
231
|
+
const unknown = Object.keys(options).filter(key => !WorkflowExecutor.#EXECUTE_OPTION_KEYS.has(key));
|
|
232
|
+
if (unknown.length === 0) return;
|
|
233
|
+
throw new ExecutionError(`execute received unknown option(s) ${unknown.join(', ')}; it takes (workflowId, { inputs, executeOptions, dependencyInputs, runDependencies, signal }) — workflow inputs go under "inputs"`, {
|
|
234
|
+
reason: 'unknown-execute-option'
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Runs one workflow invocation — the recursive worker every entry point funnels
|
|
240
|
+
* through: the public {@link WorkflowExecutor.execute}, a sub-workflow step,
|
|
241
|
+
* and a `dependsOn` prerequisite alike, each entering the caller's call stack
|
|
242
|
+
* with its own edge.
|
|
243
|
+
*/
|
|
244
|
+
async #run(locator, inputs, scope, callStack, via) {
|
|
245
|
+
const startedAt = this.#now();
|
|
246
|
+
const {
|
|
247
|
+
workflowId
|
|
248
|
+
} = locator;
|
|
249
|
+
// entering yields a new stack rather than mutating the caller's, so leaving is
|
|
250
|
+
// implicit — and a cycle or over-deep nesting throws here, before any of this
|
|
251
|
+
// workflow's own work begins. The entry is identified by the owning
|
|
252
|
+
// document's URI as well as the id, so a foreign document's reference
|
|
253
|
+
// back into the entry document closes a detectable cycle, and two
|
|
254
|
+
// documents' workflows sharing an id do not falsely form one.
|
|
255
|
+
const nested = callStack.enter(workflowId, via, locator.document.uri);
|
|
256
|
+
// the workflow boundary, checked in its own right and not only per step: a
|
|
257
|
+
// run cancelled before any step — or between a prerequisite and the workflow
|
|
258
|
+
// that needed it — must not go on to normalize and enter a workflow nobody
|
|
259
|
+
// is waiting for.
|
|
260
|
+
throwIfAborted(scope.signal, {
|
|
261
|
+
workflowId,
|
|
262
|
+
callStack: nested
|
|
263
|
+
});
|
|
264
|
+
const workflow = await this.#resolveWorkflow(locator, scope);
|
|
265
|
+
const state = new WorkflowExecutionState({
|
|
266
|
+
inputs
|
|
267
|
+
});
|
|
268
|
+
const invocation = {
|
|
269
|
+
workflowId,
|
|
270
|
+
document: locator.document,
|
|
271
|
+
state,
|
|
272
|
+
callStack: nested,
|
|
273
|
+
via
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
// validated before any prerequisite runs: a malformed `steps` is an
|
|
277
|
+
// authoring error, and discovering it only after the dependencies have
|
|
278
|
+
// fired would mean live side effects on the way to a throw.
|
|
279
|
+
const steps = this.#orderedSteps(workflow, workflowId);
|
|
280
|
+
// same reasoning extends to `outputs`: a workflow that cannot possibly
|
|
281
|
+
// produce them is unrunnable from the start, and only a shape check is
|
|
282
|
+
// needed to know that — no run state is required for it. Checking now, not
|
|
283
|
+
// at `#result` where it used to be resolved, means the throw lands before
|
|
284
|
+
// the first request rather than after the last.
|
|
285
|
+
this.#validateWorkflowOutputsShape(workflow, workflowId);
|
|
286
|
+
const dependencies = await this.#runDependencies(workflow, invocation, scope);
|
|
287
|
+
if (dependencies.some(dependency => this.#settledFailed(dependency))) {
|
|
288
|
+
// a declared prerequisite did not complete, so this workflow cannot be
|
|
289
|
+
// processed. That is a runtime failure like any failing step — a `failed`
|
|
290
|
+
// result carrying the dependency trace, not a throw — and none of its own
|
|
291
|
+
// steps run. Unless the run was cancelled, in which case the prerequisite
|
|
292
|
+
// did not fail on its own terms either.
|
|
293
|
+
throwIfAborted(scope.signal, {
|
|
294
|
+
workflowId,
|
|
295
|
+
callStack: nested
|
|
296
|
+
});
|
|
297
|
+
return this.#result(invocation, workflow, [], {
|
|
298
|
+
kind: 'settled',
|
|
299
|
+
status: 'failed'
|
|
300
|
+
}, dependencies, startedAt);
|
|
301
|
+
}
|
|
302
|
+
const trace = [];
|
|
303
|
+
let index = 0;
|
|
304
|
+
// named distinctly from each step's own `outcome` (a StepAttemptOutcome,
|
|
305
|
+
// destructured fresh every iteration below) — the two are unrelated
|
|
306
|
+
// values that happen to share a natural name.
|
|
307
|
+
let runOutcome = {
|
|
308
|
+
kind: 'settled',
|
|
309
|
+
status: 'completed'
|
|
310
|
+
};
|
|
311
|
+
stepLoop: while (index < steps.length) {
|
|
312
|
+
const step = steps[index];
|
|
313
|
+
const stepId = toValue(step.stepId);
|
|
314
|
+
const stepStartedAt = this.#now();
|
|
315
|
+
// the sub-workflow runs this step produces — one per attempt, so a retried
|
|
316
|
+
// sub-workflow step keeps every attempt's trace rather than only the last.
|
|
317
|
+
const subWorkflows = [];
|
|
318
|
+
// the retry reference runs this step's attempts produced, one per firing —
|
|
319
|
+
// separate from `subWorkflows` because a reference belongs to the retry
|
|
320
|
+
// that invoked it, not to the step's own target.
|
|
321
|
+
const retryReferences = [];
|
|
322
|
+
const attempt = this.#chargedAttempt(this.#stepAttempt(step, stepId, invocation, scope, subWorkflows), invocation, scope, stepId);
|
|
323
|
+
|
|
324
|
+
// the retry runner settles any `retry` actions, so `action` is the terminal
|
|
325
|
+
// action a retry chain resolved to and `attempts` is how many times the step
|
|
326
|
+
// ran.
|
|
327
|
+
const {
|
|
328
|
+
outcome,
|
|
329
|
+
action,
|
|
330
|
+
attempts
|
|
331
|
+
} = await this.#retryRunner.run(attempt, {
|
|
332
|
+
stepId,
|
|
333
|
+
workflowId,
|
|
334
|
+
signal: scope.signal,
|
|
335
|
+
runReference: this.#retryReference(steps, stepId, invocation, scope, retryReferences)
|
|
336
|
+
});
|
|
337
|
+
state.setStepOutputs(outcome.stepId, outcome.outputs);
|
|
338
|
+
trace.push({
|
|
339
|
+
stepId: outcome.stepId,
|
|
340
|
+
successful: outcome.successful,
|
|
341
|
+
action,
|
|
342
|
+
attempts,
|
|
343
|
+
durationMs: this.#now() - stepStartedAt,
|
|
344
|
+
...(subWorkflows.length > 0 ? {
|
|
345
|
+
subWorkflows
|
|
346
|
+
} : {}),
|
|
347
|
+
...(retryReferences.length > 0 ? {
|
|
348
|
+
retryReferences
|
|
349
|
+
} : {})
|
|
350
|
+
});
|
|
351
|
+
const transition = this.#interpreter.interpret(action, outcome.successful, {
|
|
352
|
+
workflowId,
|
|
353
|
+
stepId
|
|
354
|
+
});
|
|
355
|
+
switch (transition.kind) {
|
|
356
|
+
case 'next':
|
|
357
|
+
index += 1;
|
|
358
|
+
break;
|
|
359
|
+
case 'goto':
|
|
360
|
+
index = this.#interpreter.indexOfStep(steps, transition.stepId, workflowId, {
|
|
361
|
+
reason: 'goto-target-not-found',
|
|
362
|
+
label: 'goto target'
|
|
363
|
+
});
|
|
364
|
+
break;
|
|
365
|
+
case 'transfer':
|
|
366
|
+
{
|
|
367
|
+
// a one-way transfer: this run ends here, and the target's run is
|
|
368
|
+
// nested rather than spliced into this one. Checked for cancellation
|
|
369
|
+
// before running the target — same boundary convention as every
|
|
370
|
+
// other nested call — ahead of the reference helper's own
|
|
371
|
+
// resolution, so an aborted run reports `aborted` rather than
|
|
372
|
+
// whatever resolving the target (a malformed reference, a missing
|
|
373
|
+
// source) would have thrown.
|
|
374
|
+
throwIfAborted(scope.signal, {
|
|
375
|
+
workflowId,
|
|
376
|
+
stepId,
|
|
377
|
+
callStack: nested
|
|
378
|
+
});
|
|
379
|
+
const transferredTo = await this.#runReferencedWorkflow(transition.workflowId, stepId, invocation, scope, 'goto');
|
|
380
|
+
runOutcome = {
|
|
381
|
+
kind: 'transferred',
|
|
382
|
+
transferredTo
|
|
383
|
+
};
|
|
384
|
+
break stepLoop;
|
|
385
|
+
}
|
|
386
|
+
case 'end':
|
|
387
|
+
runOutcome = {
|
|
388
|
+
kind: 'settled',
|
|
389
|
+
status: 'ended'
|
|
390
|
+
};
|
|
391
|
+
break stepLoop;
|
|
392
|
+
case 'break':
|
|
393
|
+
runOutcome = {
|
|
394
|
+
kind: 'settled',
|
|
395
|
+
status: 'failed'
|
|
396
|
+
};
|
|
397
|
+
break stepLoop;
|
|
398
|
+
default:
|
|
399
|
+
{
|
|
400
|
+
// exhaustiveness guard: a Transition kind added above without a case
|
|
401
|
+
// here would otherwise fall through silently instead of failing
|
|
402
|
+
// loudly — `transition` is provably `never` once every kind above
|
|
403
|
+
// has its own case, which TypeScript checks at compile time.
|
|
404
|
+
const unreachable = transition;
|
|
405
|
+
throw new ExecutionError(`step "${stepId}" in workflow "${workflowId}" produced an unrecognized transition ${JSON.stringify(unreachable)}`, {
|
|
406
|
+
workflowId,
|
|
407
|
+
stepId,
|
|
408
|
+
reason: 'unknown-transition-kind'
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// the closing boundary. Nothing follows the last step, so without a check
|
|
415
|
+
// here a run abandoned while that step was in flight would resolve as though
|
|
416
|
+
// the caller had waited for it — and whether it did would depend on the
|
|
417
|
+
// transport, since one honoring the signal fails the request instead.
|
|
418
|
+
// Cancellation must not be the one outcome that reads differently for the
|
|
419
|
+
// last step than for every step before it.
|
|
420
|
+
throwIfAborted(scope.signal, {
|
|
421
|
+
workflowId,
|
|
422
|
+
callStack: nested
|
|
423
|
+
});
|
|
424
|
+
return this.#result(invocation, workflow, trace, runOutcome, dependencies, startedAt);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* Assembles the run's result from how it ended, resolving the workflow's
|
|
429
|
+
* `outputs` against the final state (unless it transferred) and stamping
|
|
430
|
+
* the elapsed time. `dependencies` is reported only when the workflow
|
|
431
|
+
* actually had some.
|
|
432
|
+
*
|
|
433
|
+
* `outcome` is the single source every derived field reads from — `status`,
|
|
434
|
+
* `settledStatus`, whether `outputs` gets resolved at all, whether
|
|
435
|
+
* `transferredTo` is present — so there is no second place any of them
|
|
436
|
+
* could drift from it. A `'transferred'` run never reaches its own end, so
|
|
437
|
+
* its `outputs` declaration is never evaluated — `{}` is the only honest
|
|
438
|
+
* value; the target's own outputs live at `transferredTo.outputs`. Skipping
|
|
439
|
+
* resolution loses no validation:
|
|
440
|
+
* {@link WorkflowExecutor.#validateWorkflowOutputsShape} already checked
|
|
441
|
+
* the shape up front, independent of resolving it.
|
|
442
|
+
*/
|
|
443
|
+
#result(invocation, workflow, steps, outcome, dependencies, startedAt) {
|
|
444
|
+
const shared = {
|
|
445
|
+
workflowId: invocation.workflowId,
|
|
446
|
+
steps,
|
|
447
|
+
durationMs: this.#now() - startedAt,
|
|
448
|
+
...(dependencies.length > 0 ? {
|
|
449
|
+
dependencies
|
|
450
|
+
} : {})
|
|
451
|
+
};
|
|
452
|
+
if (outcome.kind === 'transferred') {
|
|
453
|
+
return {
|
|
454
|
+
...shared,
|
|
455
|
+
outputs: {},
|
|
456
|
+
status: 'transferred',
|
|
457
|
+
// computed once, here, rather than left for a caller to chase via
|
|
458
|
+
// `settledResult` — `transferredTo` already carries its own
|
|
459
|
+
// `settledStatus`, itself computed the same way when it was built,
|
|
460
|
+
// so this is O(1) per hop rather than a walk of the whole chain.
|
|
461
|
+
settledStatus: outcome.transferredTo.settledStatus,
|
|
462
|
+
transferredTo: outcome.transferredTo
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
return {
|
|
466
|
+
...shared,
|
|
467
|
+
outputs: this.#resolveWorkflowOutputs(workflow, invocation),
|
|
468
|
+
status: outcome.status,
|
|
469
|
+
settledStatus: outcome.status
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* Whether a nested run's transfer chain settled on `'failed'` — reading
|
|
475
|
+
* {@link WorkflowExecutionResult.settledStatus} directly, which every
|
|
476
|
+
* result already carries, rather than walking `transferredTo` itself.
|
|
477
|
+
*/
|
|
478
|
+
#settledFailed(result) {
|
|
479
|
+
return result.settledStatus === 'failed';
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/**
|
|
483
|
+
* Charges one step attempt to the run-wide budget, throwing when it is spent.
|
|
484
|
+
*
|
|
485
|
+
* This is what bounds a runaway `retry`, a runaway `goto` loop, and a runaway
|
|
486
|
+
* tree of sub-workflow calls alike. The budget spans the whole call tree, so a
|
|
487
|
+
* sub-workflow cannot start afresh; the error carries the chain in progress,
|
|
488
|
+
* because the leaf that happened to spend the last unit is rarely the workflow
|
|
489
|
+
* that is looping.
|
|
490
|
+
*/
|
|
491
|
+
#chargeBudget(scope, invocation, stepId) {
|
|
492
|
+
if (++scope.budget.spent <= this.#maxSteps) return;
|
|
493
|
+
throw new ExecutionError(`workflow "${invocation.workflowId}" exceeded its budget of ${this.#maxSteps} step attempts (a goto loop, excessive retries, or runaway sub-workflow calls)`, {
|
|
494
|
+
workflowId: invocation.workflowId,
|
|
495
|
+
stepId,
|
|
496
|
+
reason: 'step-budget',
|
|
497
|
+
path: invocation.callStack.path
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* Wraps a step-attempt thunk with the checks every attempt needs at its
|
|
503
|
+
* boundary: charge the run-wide budget, and check for cancellation both
|
|
504
|
+
* before the attempt runs and, if it throws, again on the way out — a
|
|
505
|
+
* cancellation noticed inside the attempt (StepExecutor is handed no
|
|
506
|
+
* workflow or call chain) is re-raised here with both attached, which keeps
|
|
507
|
+
* an abort surfacing mid-request as informative as one caught between two
|
|
508
|
+
* attempts.
|
|
509
|
+
*
|
|
510
|
+
* Shared by the step's own attempts and a `retry` action's `stepId`
|
|
511
|
+
* reference, which is itself one more step attempt against this same run.
|
|
512
|
+
*/
|
|
513
|
+
#chargedAttempt(runAttempt, invocation, scope, stepId) {
|
|
514
|
+
const {
|
|
515
|
+
workflowId,
|
|
516
|
+
callStack
|
|
517
|
+
} = invocation;
|
|
518
|
+
return async () => {
|
|
519
|
+
throwIfAborted(scope.signal, {
|
|
520
|
+
workflowId,
|
|
521
|
+
stepId,
|
|
522
|
+
callStack
|
|
523
|
+
});
|
|
524
|
+
this.#chargeBudget(scope, invocation, stepId);
|
|
525
|
+
try {
|
|
526
|
+
return await runAttempt();
|
|
527
|
+
} catch (error) {
|
|
528
|
+
throwIfAborted(scope.signal, {
|
|
529
|
+
workflowId,
|
|
530
|
+
stepId,
|
|
531
|
+
callStack
|
|
532
|
+
});
|
|
533
|
+
throw error;
|
|
534
|
+
}
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
/**
|
|
539
|
+
* Builds the {@link StepRetryRunContext.runReference} callback a `retry`
|
|
540
|
+
* action's `stepId` / `workflowId` reference is run through.
|
|
541
|
+
*
|
|
542
|
+
* The runner that calls this back owns *when* — once per firing, after the
|
|
543
|
+
* `retryAfter` wait, before the step is re-run; this owns *how*. A retry
|
|
544
|
+
* action naming both fields is malformed — the same mutual-exclusivity
|
|
545
|
+
* `#subWorkflowId` enforces for a step's own target — rejected before either
|
|
546
|
+
* branch below runs, rather than silently preferring one.
|
|
547
|
+
*
|
|
548
|
+
* - a `workflowId` reference runs that workflow to completion through
|
|
549
|
+
* `#run`, on the same shared budget, call stack, and cycle/depth guards
|
|
550
|
+
* as a sub-workflow step — entered via `'retry'` so a loop closed through
|
|
551
|
+
* it is named a call cycle, not a dependency one, and charged one unit
|
|
552
|
+
* for entering the reference itself, matching the "1 for the step itself
|
|
553
|
+
* plus whatever its sub-run spends" convention a sub-workflow step
|
|
554
|
+
* already follows (without it, a reference to a workflow with few or no
|
|
555
|
+
* steps of its own would cost nothing to fire). It runs with no inputs:
|
|
556
|
+
* the specification gives a retry reference no input-mapping mechanism
|
|
557
|
+
* of its own (the same gap `dependsOn` has), and `{}` is the only
|
|
558
|
+
* spec-honest fallback — see issue `#62`. Its outputs are recorded via
|
|
559
|
+
* `state.setWorkflow`, which *is* "context transfers back" — but its
|
|
560
|
+
* `inputs` are preserved rather than overwritten with `{}` when this
|
|
561
|
+
* workflowId was already recorded (by an earlier `dependsOn` prerequisite
|
|
562
|
+
* or sub-workflow step sharing the id): this call's own lack of a real
|
|
563
|
+
* inputs value must not erase one a different, better-informed caller
|
|
564
|
+
* already gave.
|
|
565
|
+
* - a `stepId` reference must name a step of the current workflow (checked
|
|
566
|
+
* lazily, at fire time, via the same lookup a `goto` target uses) and
|
|
567
|
+
* runs as one more attempt at that step, through
|
|
568
|
+
* {@link WorkflowExecutor.#stepAttempt} and
|
|
569
|
+
* {@link WorkflowExecutor.#chargedAttempt} — so a step referencing a
|
|
570
|
+
* `workflowId` itself is handled the normal way, and the reference counts
|
|
571
|
+
* against the same budget. Its outputs are recorded via
|
|
572
|
+
* `state.setStepOutputs` — that recording *is* "context transfers back".
|
|
573
|
+
*
|
|
574
|
+
* Neither kind's own `onSuccess` / `onFailure` (or, for a step reference, its
|
|
575
|
+
* `successCriteria`-derived action) is acted on: "context transfers back upon
|
|
576
|
+
* completion" makes a reference a call, not a redirect, and a failed
|
|
577
|
+
* reference does not break the retry chain — the spec speaks of the
|
|
578
|
+
* reference's *completion*, not its success, and a retry that turns out
|
|
579
|
+
* futile is bounded by `retryLimit` regardless.
|
|
580
|
+
*/
|
|
581
|
+
#retryReference(steps, stepId, invocation, scope, retryReferences) {
|
|
582
|
+
const {
|
|
583
|
+
workflowId,
|
|
584
|
+
state,
|
|
585
|
+
callStack
|
|
586
|
+
} = invocation;
|
|
587
|
+
return async action => {
|
|
588
|
+
throwIfAborted(scope.signal, {
|
|
589
|
+
workflowId,
|
|
590
|
+
stepId,
|
|
591
|
+
callStack
|
|
592
|
+
});
|
|
593
|
+
const {
|
|
594
|
+
hasStepId,
|
|
595
|
+
hasWorkflowId
|
|
596
|
+
} = actionTargets(action);
|
|
597
|
+
if (hasWorkflowId && hasStepId) {
|
|
598
|
+
throw new ExecutionError(`retry action on step "${stepId}" in workflow "${workflowId}" declares both a stepId and a workflowId (mutually exclusive)`, {
|
|
599
|
+
stepId,
|
|
600
|
+
workflowId,
|
|
601
|
+
reason: 'ambiguous-target'
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
if (hasWorkflowId) {
|
|
605
|
+
const reference = toValue(action.workflowId);
|
|
606
|
+
this.#chargeBudget(scope, invocation, stepId);
|
|
607
|
+
const result = await this.#runReferencedWorkflow(reference, stepId, invocation, scope, 'retry');
|
|
608
|
+
// the result's workflowId is the bare id the reference resolved to —
|
|
609
|
+
// the key run state is recorded under, whichever document owns it.
|
|
610
|
+
const refId = result.workflowId;
|
|
611
|
+
state.setWorkflow(refId, {
|
|
612
|
+
inputs: state.getWorkflow(refId)?.inputs ?? {},
|
|
613
|
+
outputs: result.outputs
|
|
614
|
+
});
|
|
615
|
+
retryReferences.push({
|
|
616
|
+
kind: 'workflow',
|
|
617
|
+
id: reference,
|
|
618
|
+
successful: !this.#settledFailed(result),
|
|
619
|
+
subWorkflow: result
|
|
620
|
+
});
|
|
621
|
+
return;
|
|
622
|
+
}
|
|
623
|
+
const refStepId = toValue(action.stepId);
|
|
624
|
+
const refIndex = this.#interpreter.indexOfStep(steps, refStepId, workflowId, {
|
|
625
|
+
reason: 'retry-target-not-found',
|
|
626
|
+
label: 'retry reference'
|
|
627
|
+
});
|
|
628
|
+
const refStep = steps[refIndex];
|
|
629
|
+
const refSubWorkflows = [];
|
|
630
|
+
const outcome = await this.#chargedAttempt(this.#stepAttempt(refStep, refStepId, invocation, scope, refSubWorkflows), invocation, scope, refStepId)();
|
|
631
|
+
state.setStepOutputs(outcome.stepId, outcome.outputs);
|
|
632
|
+
retryReferences.push({
|
|
633
|
+
kind: 'step',
|
|
634
|
+
id: refStepId,
|
|
635
|
+
successful: outcome.successful,
|
|
636
|
+
...(refSubWorkflows.length > 0 ? {
|
|
637
|
+
subWorkflow: refSubWorkflows[0]
|
|
638
|
+
} : {})
|
|
639
|
+
});
|
|
640
|
+
};
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
/**
|
|
644
|
+
* Runs the workflows this workflow `dependsOn`, in declaration order, before
|
|
645
|
+
* any of its own steps — the Arazzo "MUST be completed before this workflow can
|
|
646
|
+
* be processed" precondition, satisfied on demand.
|
|
647
|
+
*
|
|
648
|
+
* Each dependency's outputs are recorded into the dependent's state so
|
|
649
|
+
* `$workflows.{id}.outputs` resolves; they are not merged into the dependent's
|
|
650
|
+
* own outputs. Runs stop at the first dependency that fails — the dependent
|
|
651
|
+
* cannot be processed, so running the rest would be pointless work with live
|
|
652
|
+
* side effects.
|
|
653
|
+
*
|
|
654
|
+
* Only workflows run *as prerequisites* are remembered as satisfying later
|
|
655
|
+
* ones. A workflow that already ran in this tree as a sub-workflow **step**
|
|
656
|
+
* target does not settle a later `dependsOn` on the same id, and runs again:
|
|
657
|
+
* a step call is an explicit invocation carrying that step's parameters as
|
|
658
|
+
* inputs, whereas a prerequisite runs with whatever the caller supplied in
|
|
659
|
+
* `dependencyInputs`. Treating one as having discharged the other would
|
|
660
|
+
* silently accept a run made with different inputs than the precondition asks
|
|
661
|
+
* for.
|
|
662
|
+
*/
|
|
663
|
+
async #runDependencies(workflow, invocation, scope) {
|
|
664
|
+
const {
|
|
665
|
+
state,
|
|
666
|
+
callStack,
|
|
667
|
+
via
|
|
668
|
+
} = invocation;
|
|
669
|
+
// `runDependencies: false` is the caller vouching for the prerequisites of
|
|
670
|
+
// the workflow they named. It stops there: a sub-workflow's own `dependsOn`
|
|
671
|
+
// is an implementation detail the caller may not know exists, so skipping it
|
|
672
|
+
// on their behalf would run that workflow against state nobody claimed to
|
|
673
|
+
// have prepared — silently producing a wrong-but-`completed` result.
|
|
674
|
+
if (via === 'root' && !scope.runDependencies) return [];
|
|
675
|
+
if (!workflow.hasKey('dependsOn')) return [];
|
|
676
|
+
|
|
677
|
+
// every entry is validated before the first one runs: an authoring error
|
|
678
|
+
// found halfway down the list would otherwise be raised only after earlier
|
|
679
|
+
// prerequisites had already fired live requests.
|
|
680
|
+
const dependencyRefs = await this.#dependencyRefs(workflow, invocation);
|
|
681
|
+
const results = [];
|
|
682
|
+
for (const dependency of dependencyRefs) {
|
|
683
|
+
const qualifiedId = this.#qualifiedId(dependency.locator);
|
|
684
|
+
// a dependency already completed in this run is satisfied, not repeated: a
|
|
685
|
+
// precondition holds once met, and a diamond (two dependents sharing one
|
|
686
|
+
// dependency) must not duplicate its live side effects. Every dependent
|
|
687
|
+
// still reports and reads that one run — including the inputs the run
|
|
688
|
+
// was actually given, not whatever this dependent's own
|
|
689
|
+
// `dependencyInputs` entry would have supplied had it run it.
|
|
690
|
+
const memoized = scope.dependencyRuns.get(qualifiedId);
|
|
691
|
+
const inputs = memoized?.inputs ?? scope.dependencyInputs[dependency.inputsKey] ?? {};
|
|
692
|
+
const result = memoized?.result ?? (await this.#run(dependency.locator, inputs, scope, callStack, 'dependsOn'));
|
|
693
|
+
results.push(result);
|
|
694
|
+
// recorded whether or not it completed: a failed prerequisite still
|
|
695
|
+
// resolved (possibly partial) outputs, and the parent resolves its own
|
|
696
|
+
// outputs against this state on its way out, so `$workflows.{id}` is
|
|
697
|
+
// uniformly readable for every dependency that ran. `outputs` is the
|
|
698
|
+
// dependency's own (`{}` when it transferred) — never the settled
|
|
699
|
+
// workflow's, which would misattribute values to a declaration that
|
|
700
|
+
// never ran. Keyed by the bare id even for a cross-document dependency:
|
|
701
|
+
// `$workflows.{id}` is the only form an expression can read it back by.
|
|
702
|
+
state.setWorkflow(dependency.locator.workflowId, {
|
|
703
|
+
inputs,
|
|
704
|
+
outputs: result.outputs
|
|
705
|
+
});
|
|
706
|
+
if (this.#settledFailed(result)) return results;
|
|
707
|
+
|
|
708
|
+
// only a dependency whose chain settled non-`failed` is remembered as
|
|
709
|
+
// satisfied — a failure, transferred or not, must never let a later
|
|
710
|
+
// dependent skip running it.
|
|
711
|
+
scope.dependencyRuns.set(qualifiedId, {
|
|
712
|
+
result,
|
|
713
|
+
inputs
|
|
714
|
+
});
|
|
715
|
+
}
|
|
716
|
+
return results;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
/**
|
|
720
|
+
* The workflows a workflow declares in `dependsOn`, each resolved to its
|
|
721
|
+
* locator, with every entry checked before any of them is run: the list
|
|
722
|
+
* must be a list of strings, and each entry — a plain id or a
|
|
723
|
+
* cross-document `$sourceDescriptions.{name}.{workflowId}` reference —
|
|
724
|
+
* must name a workflow its document defines.
|
|
725
|
+
*
|
|
726
|
+
* Checking the whole list up front is the point. These are authoring errors,
|
|
727
|
+
* and validating them one at a time as they are run would let a bad entry
|
|
728
|
+
* halfway down the list throw only after the entries before it had already
|
|
729
|
+
* made live requests.
|
|
730
|
+
*
|
|
731
|
+
* `inputsKey` is the entry as written, the key
|
|
732
|
+
* {@link WorkflowExecuteOptions.dependencyInputs} addresses the dependency
|
|
733
|
+
* by.
|
|
734
|
+
*/
|
|
735
|
+
async #dependencyRefs(workflow, invocation) {
|
|
736
|
+
const {
|
|
737
|
+
workflowId,
|
|
738
|
+
document
|
|
739
|
+
} = invocation;
|
|
740
|
+
const dependsOn = workflow.dependsOn;
|
|
741
|
+
if (!isArrayElement(dependsOn)) {
|
|
742
|
+
throw new ExecutionError(`workflow "${workflowId}" has a non-list "dependsOn"`, {
|
|
743
|
+
workflowId,
|
|
744
|
+
reason: 'malformed-dependsOn'
|
|
745
|
+
});
|
|
746
|
+
}
|
|
747
|
+
const references = [...dependsOn].map((entry, index) => {
|
|
748
|
+
if (!isStringElement(entry)) {
|
|
749
|
+
throw new ExecutionError(`workflow "${workflowId}" has a non-string entry at dependsOn[${index}]`, {
|
|
750
|
+
workflowId,
|
|
751
|
+
reason: 'malformed-dependsOn'
|
|
752
|
+
});
|
|
753
|
+
}
|
|
754
|
+
return toValue(entry);
|
|
755
|
+
});
|
|
756
|
+
const refs = [];
|
|
757
|
+
for (const reference of references) {
|
|
758
|
+
const locator = await this.#workflowLocatorNormalizer.normalize(reference, document, {
|
|
759
|
+
workflowId
|
|
760
|
+
});
|
|
761
|
+
if (!locator.document.workflowIndex.has(locator.workflowId)) {
|
|
762
|
+
throw new ExecutionError(`workflow "${workflowId}" depends on "${reference}", which the Arazzo document at "${locator.document.uri}" does not define`, {
|
|
763
|
+
workflowId,
|
|
764
|
+
reason: 'workflow-not-found'
|
|
765
|
+
});
|
|
766
|
+
}
|
|
767
|
+
refs.push({
|
|
768
|
+
locator,
|
|
769
|
+
inputsKey: reference
|
|
770
|
+
});
|
|
771
|
+
}
|
|
772
|
+
return refs;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
/**
|
|
776
|
+
* Builds the thunk that runs one attempt at a step, hiding which kind of step
|
|
777
|
+
* it is from the retry loop.
|
|
778
|
+
*
|
|
779
|
+
* An operation step is delegated to {@link StepExecutor}. A step targeting a
|
|
780
|
+
* `workflowId` is a sub-workflow call this executor runs itself — the case
|
|
781
|
+
* StepExecutor refuses — mapping the step's `parameters` to the sub-workflow's
|
|
782
|
+
* inputs, recording the sub-run under `$workflows`, then resolving the step's
|
|
783
|
+
* own `outputs` and selecting its actions against that updated state. Because
|
|
784
|
+
* both reduce to a {@link StepAttemptOutcome}, `retry` on a sub-workflow step
|
|
785
|
+
* works as it does on an operation step: each attempt re-runs the sub-workflow
|
|
786
|
+
* — its steps, that is — charged against the same budget. Prerequisites it
|
|
787
|
+
* already completed stay satisfied and are not run again, since a completed
|
|
788
|
+
* `dependsOn` workflow is memoized for the whole run; a retry re-runs the
|
|
789
|
+
* work, not the preconditions.
|
|
790
|
+
*/
|
|
791
|
+
#stepAttempt(step, stepId, invocation, scope, subWorkflows) {
|
|
792
|
+
const {
|
|
793
|
+
workflowId,
|
|
794
|
+
document,
|
|
795
|
+
state,
|
|
796
|
+
callStack
|
|
797
|
+
} = invocation;
|
|
798
|
+
if (!isStringElement(step.workflowId)) {
|
|
799
|
+
return () => this.#stepExecutor.forDocument(document).execute(step, state, scope.executeOptions);
|
|
800
|
+
}
|
|
801
|
+
const reference = this.#subWorkflowReference(step, stepId, workflowId);
|
|
802
|
+
const stepScope = {
|
|
803
|
+
stepId
|
|
804
|
+
};
|
|
805
|
+
// the reference resolves to the same locator on every attempt, so it is
|
|
806
|
+
// resolved once for the whole retry chain, on the first attempt; that
|
|
807
|
+
// same first-attempt moment — before this call has written anything —
|
|
808
|
+
// is when this call's own $workflows entry is pinned (typically absent,
|
|
809
|
+
// but a legitimate value when an earlier, different step already called
|
|
810
|
+
// the same workflowId), held fixed across every attempt below so a retry
|
|
811
|
+
// never sees this call's *own* prior-attempt output. Everything else is
|
|
812
|
+
// read live per attempt.
|
|
813
|
+
let locator;
|
|
814
|
+
let priorWorkflowEntry;
|
|
815
|
+
return async () => {
|
|
816
|
+
if (locator === undefined) {
|
|
817
|
+
locator = await this.#workflowLocatorNormalizer.normalize(reference, document, {
|
|
818
|
+
workflowId,
|
|
819
|
+
stepId
|
|
820
|
+
});
|
|
821
|
+
priorWorkflowEntry = state.getWorkflow(locator.workflowId);
|
|
822
|
+
}
|
|
823
|
+
const subWorkflowId = locator.workflowId;
|
|
824
|
+
|
|
825
|
+
// the sub-workflow's inputs come from the step's parameters, mapped by
|
|
826
|
+
// bare name — per the specification, "all parameters map to workflow
|
|
827
|
+
// inputs" for such a step. That includes an inherited parameter that
|
|
828
|
+
// carries an `in` (the step's own carry none): it still arrives as an
|
|
829
|
+
// input under its name, not under the '{in}.{name}' key an operation
|
|
830
|
+
// step would deliver it by.
|
|
831
|
+
//
|
|
832
|
+
// Resolved fresh on every attempt, not once outside this thunk: a
|
|
833
|
+
// `retry` action's reference can now mutate `$steps` / `$workflows`
|
|
834
|
+
// between attempts (a repair a retried step's own parameters should
|
|
835
|
+
// see), and freezing inputs once would hide that. The one thing that
|
|
836
|
+
// must NOT drift attempt to attempt is this call's own target —
|
|
837
|
+
// `#inputContext` pins `subWorkflowId`'s entry back to
|
|
838
|
+
// `priorWorkflowEntry` so a parameter reading `$workflows.{subWorkflowId}`
|
|
839
|
+
// sees the same (usually absent) value on every attempt, not the
|
|
840
|
+
// previous attempt's own result.
|
|
841
|
+
const preContext = this.#inputContext(state, subWorkflowId, priorWorkflowEntry);
|
|
842
|
+
const inputs = this.#parameterResolver.resolve(step.parameters, expression => this.#evaluate(preContext, expression, document), stepScope);
|
|
843
|
+
const result = await this.#run(locator, inputs, scope, callStack, 'step');
|
|
844
|
+
subWorkflows.push(result);
|
|
845
|
+
state.setWorkflow(subWorkflowId, {
|
|
846
|
+
inputs,
|
|
847
|
+
outputs: result.outputs
|
|
848
|
+
});
|
|
849
|
+
|
|
850
|
+
// resolved after the sub-run is recorded, so the step's outputs can map
|
|
851
|
+
// out of `$workflows.{subWorkflowId}.outputs`. The called workflow is the
|
|
852
|
+
// step's result: `$outputs` names its outputs — the enclosing workflow's
|
|
853
|
+
// own outputs do not exist until it ends, so that reading could never
|
|
854
|
+
// resolve here — and, the step having made no HTTP call, `calledWorkflow`
|
|
855
|
+
// makes `$statusCode`, `$response` and the other HTTP-exchange roots
|
|
856
|
+
// authoring errors rather than silent misses.
|
|
857
|
+
const context = {
|
|
858
|
+
...state.toContext(),
|
|
859
|
+
outputs: result.outputs,
|
|
860
|
+
calledWorkflow: {
|
|
861
|
+
stepId,
|
|
862
|
+
workflowId: subWorkflowId
|
|
863
|
+
}
|
|
864
|
+
};
|
|
865
|
+
const outputs = this.#outputResolver.resolve(step.outputs, expression => this.#evaluate(context, expression, document), stepScope);
|
|
866
|
+
// an `end`ed sub-workflow returned to its caller with outputs, so it took
|
|
867
|
+
// the success path like a completed one; only a chain settling `failed` is
|
|
868
|
+
// a failure — a sub-workflow that transferred is judged by where its
|
|
869
|
+
// chain settled, via `#settledFailed`, not by the intermediate
|
|
870
|
+
// `'transferred'` status itself. The step's own `successCriteria` still
|
|
871
|
+
// apply on top — they are the author's assertion about this step, and
|
|
872
|
+
// dropping them because the step happens to target a workflow would
|
|
873
|
+
// silently discard it. They see the sub-run's own outputs (`{}` when it
|
|
874
|
+
// transferred) through `$outputs` and `$workflows`.
|
|
875
|
+
// the criteria and actions are the *calling* step's, so they are
|
|
876
|
+
// evaluated by the parent document's step executor — only the sub-run
|
|
877
|
+
// itself switched documents.
|
|
878
|
+
const stepExecutor = this.#stepExecutor.forDocument(document);
|
|
879
|
+
const successful = !this.#settledFailed(result) && stepExecutor.evaluateCriteria(step, context);
|
|
880
|
+
const matchedActions = stepExecutor.selectActions(step, successful, context);
|
|
881
|
+
return {
|
|
882
|
+
stepId,
|
|
883
|
+
successful,
|
|
884
|
+
outputs,
|
|
885
|
+
action: matchedActions[0],
|
|
886
|
+
matchedActions
|
|
887
|
+
};
|
|
888
|
+
};
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
/**
|
|
892
|
+
* The live run context, with `workflowId`'s own `$workflows` entry pinned to
|
|
893
|
+
* `entry` rather than whatever `state` holds right now.
|
|
894
|
+
*
|
|
895
|
+
* Used to resolve a sub-workflow step's inputs on a retry: every other path
|
|
896
|
+
* (`$steps`, every *other* `$workflows` entry, `$inputs`) must be read live,
|
|
897
|
+
* so a `retry` reference's repair between attempts is visible — but this
|
|
898
|
+
* call's own target must not, or a retried attempt would read its own
|
|
899
|
+
* previous attempt's output instead of the value it had before this step's
|
|
900
|
+
* retry chain began. `toContext` builds a fresh plain object per call, so
|
|
901
|
+
* overriding one key here never touches `state` itself.
|
|
902
|
+
*/
|
|
903
|
+
#inputContext(state, workflowId, entry) {
|
|
904
|
+
const context = state.toContext();
|
|
905
|
+
const {
|
|
906
|
+
[workflowId]: _current,
|
|
907
|
+
...workflows
|
|
908
|
+
} = context.workflows ?? {};
|
|
909
|
+
return {
|
|
910
|
+
...context,
|
|
911
|
+
workflows: entry === undefined ? workflows : {
|
|
912
|
+
...workflows,
|
|
913
|
+
[workflowId]: entry
|
|
914
|
+
}
|
|
915
|
+
};
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
/**
|
|
919
|
+
* The reference a sub-workflow step targets, as written — a plain
|
|
920
|
+
* workflowId of the current document, or a cross-document
|
|
921
|
+
* `$sourceDescriptions.{name}.{workflowId}` runtime expression.
|
|
922
|
+
*/
|
|
923
|
+
#subWorkflowReference(step, stepId, workflowId) {
|
|
924
|
+
// a step names its target once: declaring any other target alongside a
|
|
925
|
+
// workflow is malformed and has no defined resolution. StepExecutor makes
|
|
926
|
+
// the same check, but a sub-workflow step never reaches it — so both read
|
|
927
|
+
// the one list of target fields, and a target added to the specification
|
|
928
|
+
// cannot be rejected by one and silently accepted by the other.
|
|
929
|
+
const conflicting = STEP_TARGET_FIELDS.filter(field => field !== 'workflowId' && isStringElement(step[field]));
|
|
930
|
+
if (conflicting.length > 0) {
|
|
931
|
+
throw new ExecutionError(`step "${stepId}" in workflow "${workflowId}" declares a workflowId alongside ${conflicting.join(', ')} (mutually exclusive)`, {
|
|
932
|
+
stepId,
|
|
933
|
+
workflowId,
|
|
934
|
+
reason: 'ambiguous-target'
|
|
935
|
+
});
|
|
936
|
+
}
|
|
937
|
+
return toValue(step.workflowId);
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
/**
|
|
941
|
+
* Runs a workflow an action references directly — a retry's `workflowId`
|
|
942
|
+
* reference or a goto's transfer, plain id or cross-document expression —
|
|
943
|
+
* with `{}` inputs, since neither has an input-mapping mechanism of its own
|
|
944
|
+
* (issue `#62`).
|
|
945
|
+
*
|
|
946
|
+
* Shared rather than duplicated by its two callers, which otherwise repeat
|
|
947
|
+
* the identical "resolve the reference, then run with no inputs" sequence.
|
|
948
|
+
* Rejects a target its document does not define with the calling `stepId`
|
|
949
|
+
* attached, unlike {@link WorkflowExecutor.#resolveWorkflow}'s own
|
|
950
|
+
* `workflow-not-found` (reached only once `#run` itself resolves the
|
|
951
|
+
* target) — which carries no caller context at all, since it is reached
|
|
952
|
+
* generically from every kind of nested call.
|
|
953
|
+
*/
|
|
954
|
+
async #runReferencedWorkflow(reference, stepId, invocation, scope, via) {
|
|
955
|
+
const locator = await this.#workflowLocatorNormalizer.normalize(reference, invocation.document, {
|
|
956
|
+
workflowId: invocation.workflowId,
|
|
957
|
+
stepId
|
|
958
|
+
});
|
|
959
|
+
if (!locator.document.workflowIndex.has(locator.workflowId)) {
|
|
960
|
+
throw new ExecutionError(`workflow "${locator.workflowId}" not found in Arazzo document at "${locator.document.uri}"`, {
|
|
961
|
+
workflowId: locator.workflowId,
|
|
962
|
+
stepId,
|
|
963
|
+
reason: 'workflow-not-found'
|
|
964
|
+
});
|
|
965
|
+
}
|
|
966
|
+
return this.#run(locator, {}, scope, invocation.callStack, via);
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
/**
|
|
970
|
+
* Extracts and normalizes the workflow a locator names. A workflow its
|
|
971
|
+
* document does not define is the executor-level `workflow-not-found`
|
|
972
|
+
* authoring error, raised here rather than leaking the extractor's
|
|
973
|
+
* `ExtractionError`.
|
|
974
|
+
*/
|
|
975
|
+
async #resolveWorkflow(locator, scope) {
|
|
976
|
+
const {
|
|
977
|
+
document,
|
|
978
|
+
workflowId
|
|
979
|
+
} = locator;
|
|
980
|
+
const qualifiedId = this.#qualifiedId(locator);
|
|
981
|
+
const cached = scope.workflows.get(qualifiedId);
|
|
982
|
+
if (cached !== undefined) return cached;
|
|
983
|
+
if (!document.workflowIndex.has(workflowId)) {
|
|
984
|
+
throw new ExecutionError(`workflow "${workflowId}" not found in Arazzo document at "${document.uri}"`, {
|
|
985
|
+
workflowId,
|
|
986
|
+
reason: 'workflow-not-found'
|
|
987
|
+
});
|
|
988
|
+
}
|
|
989
|
+
const workflow = await this.#normalizer.normalize(this.#extractor.extract(document, workflowId), document);
|
|
990
|
+
scope.workflows.set(qualifiedId, workflow);
|
|
991
|
+
return workflow;
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
/**
|
|
995
|
+
* The run-wide identity of the workflow a locator names —
|
|
996
|
+
* `{documentURI}#{workflowId}`, the key the normalized-workflow cache and
|
|
997
|
+
* the dependency ledger share (the call stack compares the same pair,
|
|
998
|
+
* unformatted). Qualified because a bare workflowId is only unique within
|
|
999
|
+
* its own document.
|
|
1000
|
+
*/
|
|
1001
|
+
#qualifiedId(locator) {
|
|
1002
|
+
return `${locator.document.uri}#${locator.workflowId}`;
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
/**
|
|
1006
|
+
* The workflow's steps as an array in list order. An absent `steps` is a
|
|
1007
|
+
* workflow with no steps — an empty list, a completed no-op run. A present but
|
|
1008
|
+
* malformed `steps` (not a list, or holding a non-step entry) is an authoring
|
|
1009
|
+
* error and throws rather than being silently treated as empty or partial.
|
|
1010
|
+
*/
|
|
1011
|
+
#orderedSteps(workflow, workflowId) {
|
|
1012
|
+
if (!workflow.hasKey('steps')) return [];
|
|
1013
|
+
const steps = workflow.steps;
|
|
1014
|
+
if (!isArrayElement(steps)) {
|
|
1015
|
+
throw new ExecutionError(`workflow "${workflowId}" has a non-list "steps"`, {
|
|
1016
|
+
workflowId,
|
|
1017
|
+
reason: 'malformed-steps'
|
|
1018
|
+
});
|
|
1019
|
+
}
|
|
1020
|
+
return [...steps].map((step, index) => {
|
|
1021
|
+
if (!isStepElement(step)) {
|
|
1022
|
+
throw new ExecutionError(`workflow "${workflowId}" has a non-step entry at steps[${index}]`, {
|
|
1023
|
+
workflowId,
|
|
1024
|
+
reason: 'malformed-steps'
|
|
1025
|
+
});
|
|
1026
|
+
}
|
|
1027
|
+
return step;
|
|
1028
|
+
});
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
/**
|
|
1032
|
+
* Checks that the workflow's `outputs`, if present, is a map — without
|
|
1033
|
+
* resolving any of its values. The same check {@link OutputResolver.resolve}
|
|
1034
|
+
* makes on its way to actually resolving them, run here early instead: the
|
|
1035
|
+
* shape needs no run state, so this can — and, to keep a malformed `outputs`
|
|
1036
|
+
* from costing the run its live side effects, must — happen alongside
|
|
1037
|
+
* {@link WorkflowExecutor.#orderedSteps}, before any prerequisite or step of
|
|
1038
|
+
* the workflow runs.
|
|
1039
|
+
*/
|
|
1040
|
+
#validateWorkflowOutputsShape(workflow, workflowId) {
|
|
1041
|
+
if (!workflow.hasKey('outputs')) return;
|
|
1042
|
+
const outputs = workflow.outputs;
|
|
1043
|
+
if (isObjectElement(outputs)) return;
|
|
1044
|
+
throw new ResolverError('`outputs` is present but is not a map', {
|
|
1045
|
+
workflowId,
|
|
1046
|
+
target: 'outputs',
|
|
1047
|
+
reason: 'malformed-outputs'
|
|
1048
|
+
});
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
/**
|
|
1052
|
+
* Resolves the workflow's `outputs` declaration against the final run state,
|
|
1053
|
+
* mirroring how {@link StepExecutor} resolves a step's outputs.
|
|
1054
|
+
*/
|
|
1055
|
+
#resolveWorkflowOutputs(workflow, invocation) {
|
|
1056
|
+
const context = invocation.state.toContext();
|
|
1057
|
+
return this.#outputResolver.resolve(workflow.outputs, expression => this.#evaluate(context, expression, invocation.document), {
|
|
1058
|
+
workflowId: toValue(workflow.workflowId)
|
|
1059
|
+
});
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
/**
|
|
1063
|
+
* Resolves a runtime expression leniently against a context, forwarding
|
|
1064
|
+
* `$components` / `$sourceDescriptions` resolution to the given document —
|
|
1065
|
+
* the invocation's, so a foreign workflow's expressions resolve against
|
|
1066
|
+
* the document that owns it — and the registry; the workflow-scoped
|
|
1067
|
+
* counterpart of {@link StepExecutor}'s bridge.
|
|
1068
|
+
*/
|
|
1069
|
+
#evaluate(context, expression, document) {
|
|
1070
|
+
return new RuntimeExpressionEvaluator(context, {
|
|
1071
|
+
strict: false,
|
|
1072
|
+
document,
|
|
1073
|
+
registry: this.#registry
|
|
1074
|
+
}).evaluate(expression);
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
export default WorkflowExecutor;
|