@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,326 @@
|
|
|
1
|
+
import { toValue } from '@speclynx/apidom-core';
|
|
2
|
+
import { isArrayElement, isStringElement } from '@speclynx/apidom-datamodel';
|
|
3
|
+
import { isCriterionElement } from '@speclynx/apidom-ns-arazzo-1';
|
|
4
|
+
import RuntimeExpressionEvaluator from "../expression/RuntimeExpressionEvaluator.mjs";
|
|
5
|
+
import CriterionEvaluator from "../criterion/CriterionEvaluator.mjs";
|
|
6
|
+
import OpenAPIOperationParameterResolver from "../resolver/OpenAPIOperationParameterResolver.mjs";
|
|
7
|
+
import RequestBodyResolver from "../resolver/RequestBodyResolver.mjs";
|
|
8
|
+
import OutputResolver from "../resolver/OutputResolver.mjs";
|
|
9
|
+
import ActionResolver from "../action/ActionResolver.mjs";
|
|
10
|
+
import OpenAPIOperationLocatorNormalizer from "./OpenAPIOperationLocatorNormalizer.mjs";
|
|
11
|
+
import ExecutionError from "../errors/ExecutionError.mjs";
|
|
12
|
+
import RuntimeExpressionError from "../errors/RuntimeExpressionError.mjs";
|
|
13
|
+
import { readAbortSignal, throwIfAborted } from "./abort.mjs";
|
|
14
|
+
/**
|
|
15
|
+
* The mutually exclusive fields by which a step names its target. A step may
|
|
16
|
+
* declare at most one. Extend this as the Arazzo Specification adds targets
|
|
17
|
+
* (e.g. `channelPath` in 1.1.0) so every exclusivity check stays complete —
|
|
18
|
+
* both this module's and the workflow executor's, which makes the same check
|
|
19
|
+
* for a step it intercepts before {@link StepExecutor.execute}.
|
|
20
|
+
* @internal
|
|
21
|
+
*/
|
|
22
|
+
export const STEP_TARGET_FIELDS = ['operationId', 'operationPath', 'workflowId'];
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* A read-only source of the runtime expression context for a step.
|
|
26
|
+
*
|
|
27
|
+
* The step executor only reads accumulated run state — it produces the
|
|
28
|
+
* pre-request context (`toContext()`) and the post-request context
|
|
29
|
+
* (`toContext(request, response)`) — and never mutates it, so it depends on
|
|
30
|
+
* this narrow interface rather than the full execution state.
|
|
31
|
+
* @public
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Options for the StepExecutor.
|
|
36
|
+
* @public
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The outcome of executing a single step.
|
|
41
|
+
* @public
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Executes a single Arazzo step that invokes an OpenAPI operation.
|
|
46
|
+
*
|
|
47
|
+
* The pipeline locates the step's operation (`operationId` / `operationPath`),
|
|
48
|
+
* resolves the step's parameters and request body against the pre-request
|
|
49
|
+
* context, delegates to {@link OpenAPIOperationExecutor} to run the located
|
|
50
|
+
* operation, then evaluates `successCriteria`, resolves `outputs`, and selects
|
|
51
|
+
* the `onSuccess`/`onFailure` action against the post-request context.
|
|
52
|
+
*
|
|
53
|
+
* It reads run state through a {@link ContextSource} and mutates nothing — the
|
|
54
|
+
* caller records the returned outputs and interprets the returned action. A step
|
|
55
|
+
* targeting a `workflowId` (a sub-workflow) is not an operation step and throws
|
|
56
|
+
* {@link ExecutionError}; running sub-workflows is the workflow executor's
|
|
57
|
+
* concern. A received response with unmet criteria is a normal
|
|
58
|
+
* `successful: false` outcome; malformed input (e.g. an invalid criterion or
|
|
59
|
+
* expression) throws, as does a run the caller has cancelled through an
|
|
60
|
+
* `AbortSignal` in the execute options.
|
|
61
|
+
* @public
|
|
62
|
+
*/
|
|
63
|
+
class StepExecutor {
|
|
64
|
+
#document;
|
|
65
|
+
#registry;
|
|
66
|
+
#locatorNormalizer;
|
|
67
|
+
#operationExecutor;
|
|
68
|
+
#parameterResolver = new OpenAPIOperationParameterResolver();
|
|
69
|
+
#requestBodyResolver = new RequestBodyResolver();
|
|
70
|
+
#outputResolver = new OutputResolver();
|
|
71
|
+
#criterionEvaluator = new CriterionEvaluator();
|
|
72
|
+
#actionResolver = new ActionResolver();
|
|
73
|
+
constructor(options) {
|
|
74
|
+
this.#document = options.document;
|
|
75
|
+
this.#registry = options.registry;
|
|
76
|
+
this.#locatorNormalizer = new OpenAPIOperationLocatorNormalizer(options.registry);
|
|
77
|
+
this.#operationExecutor = options.operationExecutor;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* A step executor bound to the given document, sharing this one's registry
|
|
82
|
+
* and operation executor — this instance itself when the document is
|
|
83
|
+
* already the one it is bound to.
|
|
84
|
+
*
|
|
85
|
+
* A step executor's identity includes the document its steps belong to: a
|
|
86
|
+
* plain `operationId` resolves against *that* document's source
|
|
87
|
+
* descriptions, and `$components` / `$sourceDescriptions` expressions
|
|
88
|
+
* resolve against it too. A cross-document workflow run therefore derives
|
|
89
|
+
* a sibling per foreign document rather than re-pointing this one.
|
|
90
|
+
*/
|
|
91
|
+
forDocument(document) {
|
|
92
|
+
if (document === this.#document) return this;
|
|
93
|
+
return new StepExecutor({
|
|
94
|
+
document,
|
|
95
|
+
registry: this.#registry,
|
|
96
|
+
operationExecutor: this.#operationExecutor
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Executes the step against the given run state, returning its outcome.
|
|
102
|
+
*
|
|
103
|
+
* `executeOptions` is an opaque bag passed through to the operation executor
|
|
104
|
+
* (e.g. a `server` to run the step against, or an `AbortSignal`). The
|
|
105
|
+
* Arazzo-derived options (`operationId`, `parameters`, `requestBody`) always
|
|
106
|
+
* take precedence over it.
|
|
107
|
+
*
|
|
108
|
+
* A `signal` in that bag is honored here and not only at the transport: an
|
|
109
|
+
* already-aborted signal throws `reason: 'aborted'` instead of issuing a
|
|
110
|
+
* request nobody is waiting for, whose wire-level cancellation would surface
|
|
111
|
+
* as a transport {@link ClientError} — a request that failed on its own terms
|
|
112
|
+
* — rather than as the withdrawal it is.
|
|
113
|
+
*/
|
|
114
|
+
async execute(step, state, executeOptions = {}) {
|
|
115
|
+
const stepId = toValue(step.stepId);
|
|
116
|
+
|
|
117
|
+
// a step's target fields are mutually exclusive; declaring more than one is
|
|
118
|
+
// malformed and has no defined resolution.
|
|
119
|
+
const declaredTargets = STEP_TARGET_FIELDS.filter(field => isStringElement(step[field]));
|
|
120
|
+
if (declaredTargets.length > 1) {
|
|
121
|
+
throw new ExecutionError(`Step "${stepId}" declares more than one of ${STEP_TARGET_FIELDS.join(', ')} (mutually exclusive)`, {
|
|
122
|
+
stepId,
|
|
123
|
+
reason: 'ambiguous-target'
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
if (isStringElement(step.workflowId)) {
|
|
127
|
+
throw new ExecutionError(`Step "${stepId}" targets a workflowId; use the workflow executor`, {
|
|
128
|
+
stepId,
|
|
129
|
+
reason: 'workflow-step'
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// locate which operation the step invokes — reduced to a (document, JSON
|
|
134
|
+
// Pointer) locator that the operation executor extracts, normalizes,
|
|
135
|
+
// assembles into a standalone doc, and runs.
|
|
136
|
+
const locator = await this.#locateOperation(step, stepId);
|
|
137
|
+
|
|
138
|
+
// resolve parameters and request body against the pre-request context.
|
|
139
|
+
// keyed by '{in}.{name}' rather than bare name, so two parameters that
|
|
140
|
+
// legally differ only in their location each reach their own place.
|
|
141
|
+
const scope = {
|
|
142
|
+
stepId
|
|
143
|
+
};
|
|
144
|
+
const preContext = state.toContext();
|
|
145
|
+
const parameters = this.#parameterResolver.resolve(step.parameters, expression => this.#evaluate(preContext, expression), scope);
|
|
146
|
+
const requestBody = this.#requestBodyResolver.resolve(step.requestBody, expression => this.#evaluate(preContext, expression), scope);
|
|
147
|
+
|
|
148
|
+
// checked here rather than on entry: locating the operation and resolving
|
|
149
|
+
// the expressions above are awaited, so a run cancelled during them would
|
|
150
|
+
// still reach this line — and this is the last moment before the step has a
|
|
151
|
+
// live side effect. The signal stays in the bag, so a request already in
|
|
152
|
+
// flight is aborted by the transport as before.
|
|
153
|
+
const signal = readAbortSignal(executeOptions);
|
|
154
|
+
throwIfAborted(signal, {
|
|
155
|
+
stepId
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
// the Arazzo-derived parameters and request body are spread after the opaque
|
|
159
|
+
// executeOptions bag so they take precedence over it. the operation executor
|
|
160
|
+
// then forces the operation target on top of this before executing.
|
|
161
|
+
let response;
|
|
162
|
+
try {
|
|
163
|
+
response = await this.#operationExecutor.execute(locator, {
|
|
164
|
+
...executeOptions,
|
|
165
|
+
parameters,
|
|
166
|
+
...this.#requestBodyOptions(requestBody)
|
|
167
|
+
});
|
|
168
|
+
} catch (error) {
|
|
169
|
+
// a transport that honors the signal rejects the request it was told to
|
|
170
|
+
// drop, which the operation executor wraps as a `ClientError` — the shape
|
|
171
|
+
// reserved for a request that failed on its own terms. Cancelled while
|
|
172
|
+
// cancelled, it did not: reporting it as the abort keeps one error shape
|
|
173
|
+
// whether the run was withdrawn between two steps or mid-flight, and keeps
|
|
174
|
+
// the whole feature from reading differently per transport.
|
|
175
|
+
throwIfAborted(signal, {
|
|
176
|
+
stepId
|
|
177
|
+
});
|
|
178
|
+
throw error;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// evaluate criteria, outputs, and the next action against the post-request
|
|
182
|
+
// context (with $request / $url / $method and $response / $statusCode).
|
|
183
|
+
const postContext = state.toContext(this.#requestContext(response), this.#responseContext(response));
|
|
184
|
+
const successful = this.evaluateCriteria(step, postContext);
|
|
185
|
+
const outputs = this.#outputResolver.resolve(step.outputs, expression => this.#evaluate(postContext, expression), scope);
|
|
186
|
+
const matchedActions = this.selectActions(step, successful, postContext);
|
|
187
|
+
return {
|
|
188
|
+
stepId,
|
|
189
|
+
response,
|
|
190
|
+
successful,
|
|
191
|
+
outputs,
|
|
192
|
+
action: matchedActions[0],
|
|
193
|
+
matchedActions
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* The request-body execute options for the step, as a typed partial to spread
|
|
199
|
+
* into the client options.
|
|
200
|
+
*
|
|
201
|
+
* Empty when the step has no request body; the `requestContentType` key is
|
|
202
|
+
* present only when a content type was resolved, so it is omitted rather than
|
|
203
|
+
* spread as `undefined`.
|
|
204
|
+
*/
|
|
205
|
+
#requestBodyOptions(requestBody) {
|
|
206
|
+
if (requestBody === undefined) return {};
|
|
207
|
+
return requestBody.contentType === undefined ? {
|
|
208
|
+
requestBody: requestBody.payload
|
|
209
|
+
} : {
|
|
210
|
+
requestBody: requestBody.payload,
|
|
211
|
+
requestContentType: requestBody.contentType
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
async #locateOperation(step, stepId) {
|
|
215
|
+
if (isStringElement(step.operationId)) {
|
|
216
|
+
return this.#locatorNormalizer.normalizeOperationId(toValue(step.operationId), this.#document);
|
|
217
|
+
}
|
|
218
|
+
if (isStringElement(step.operationPath)) {
|
|
219
|
+
return this.#locatorNormalizer.normalizeOperationPath(toValue(step.operationPath), this.#document);
|
|
220
|
+
}
|
|
221
|
+
throw new ExecutionError(`Step "${stepId}" has no operationId or operationPath`, {
|
|
222
|
+
stepId,
|
|
223
|
+
reason: 'missing-operation'
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Evaluates the step's `successCriteria`; all criteria must pass. A step with
|
|
229
|
+
* no criteria succeeds on a received response.
|
|
230
|
+
*
|
|
231
|
+
* Public for the same reason as {@link StepExecutor.selectActions}: a step
|
|
232
|
+
* targeting a `workflowId` is run by the workflow executor and never reaches
|
|
233
|
+
* {@link StepExecutor.execute}, but its criteria are still the author's
|
|
234
|
+
* assertion about the step and must be evaluated somewhere.
|
|
235
|
+
*/
|
|
236
|
+
evaluateCriteria(step, context) {
|
|
237
|
+
const criteria = step.successCriteria;
|
|
238
|
+
// no (or non-array) successCriteria: the step succeeds on a received response.
|
|
239
|
+
if (!isArrayElement(criteria)) return true;
|
|
240
|
+
for (const criterion of criteria) {
|
|
241
|
+
if (!isCriterionElement(criterion)) continue;
|
|
242
|
+
const met = this.#criterionEvaluator.evaluate(criterion, expression => this.#evaluate(context, expression));
|
|
243
|
+
if (!met) return false;
|
|
244
|
+
}
|
|
245
|
+
return true;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Selects every matching `onSuccess` / `onFailure` action for the outcome, in
|
|
250
|
+
* list order, gating each action's criteria against the context.
|
|
251
|
+
*
|
|
252
|
+
* The step's own list is the whole story here: a step that declares none of
|
|
253
|
+
* its own already carries the workflow-level defaults, inherited by
|
|
254
|
+
* {@link ArazzoWorkflowNormalizer} — the same place a step's inherited
|
|
255
|
+
* `parameters` come from — so there is no fallback left to apply at selection
|
|
256
|
+
* time.
|
|
257
|
+
*
|
|
258
|
+
* The full matching list (not just the first) is returned so the caller can
|
|
259
|
+
* honor "retryLimit exhausted prior to subsequent failure actions"; the common
|
|
260
|
+
* caller simply takes the first.
|
|
261
|
+
*
|
|
262
|
+
* Public because selecting a step's actions is meaningful beyond running one:
|
|
263
|
+
* the workflow executor selects them for a step targeting a `workflowId`, which
|
|
264
|
+
* it runs itself and which therefore never reaches
|
|
265
|
+
* {@link StepExecutor.execute}. Both callers reach the criteria evaluator and
|
|
266
|
+
* the expression context through this one method rather than wiring their own.
|
|
267
|
+
*/
|
|
268
|
+
selectActions(step, successful, context) {
|
|
269
|
+
const isCriterionMet = criterion => {
|
|
270
|
+
try {
|
|
271
|
+
return this.#criterionEvaluator.evaluate(criterion, expression => this.#evaluate(context, expression));
|
|
272
|
+
} catch (error) {
|
|
273
|
+
// an action's criteria decide whether it applies to this outcome, and
|
|
274
|
+
// actions are shared — inherited from the workflow, reused from
|
|
275
|
+
// components. A condition on an HTTP exchange cannot apply to a step
|
|
276
|
+
// that calls a workflow, so the action does not match; the run does not
|
|
277
|
+
// abort over a default written for the operation steps.
|
|
278
|
+
if (error instanceof RuntimeExpressionError && error.reason === 'unavailable-expression') {
|
|
279
|
+
return false;
|
|
280
|
+
}
|
|
281
|
+
throw error;
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
const scope = {
|
|
285
|
+
stepId: toValue(step.stepId)
|
|
286
|
+
};
|
|
287
|
+
return successful ? this.#actionResolver.resolveOnSuccess(step.onSuccess, isCriterionMet, scope) : this.#actionResolver.resolveOnFailure(step.onFailure, isCriterionMet, scope);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Resolves a runtime expression leniently against a context, forwarding
|
|
292
|
+
* `$components` / `$sourceDescriptions` resolution to the document and
|
|
293
|
+
* registry.
|
|
294
|
+
*/
|
|
295
|
+
#evaluate(context, expression) {
|
|
296
|
+
return new RuntimeExpressionEvaluator(context, {
|
|
297
|
+
strict: false,
|
|
298
|
+
document: this.#document,
|
|
299
|
+
registry: this.#registry
|
|
300
|
+
}).evaluate(expression);
|
|
301
|
+
}
|
|
302
|
+
#responseContext(response) {
|
|
303
|
+
return {
|
|
304
|
+
statusCode: response.status,
|
|
305
|
+
header: response.headers,
|
|
306
|
+
body: response.body
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* The request context (`$url`, `$method`, `$request.header.*`,
|
|
312
|
+
* `$request.body`) from the request the client actually sent, or `undefined`
|
|
313
|
+
* when the client did not report one.
|
|
314
|
+
*/
|
|
315
|
+
#requestContext(response) {
|
|
316
|
+
const request = response.request;
|
|
317
|
+
if (request === undefined) return undefined;
|
|
318
|
+
return {
|
|
319
|
+
url: request.url,
|
|
320
|
+
method: request.method,
|
|
321
|
+
header: request.headers,
|
|
322
|
+
body: request.body
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
export default StepExecutor;
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
var _interopRequireDefault = require("@babel/runtime-corejs3/helpers/interopRequireDefault").default;
|
|
4
|
+
Object.defineProperty(exports, "__esModule", {
|
|
5
|
+
value: true
|
|
6
|
+
});
|
|
7
|
+
exports.default = void 0;
|
|
8
|
+
var _apidomCore = require("@speclynx/apidom-core");
|
|
9
|
+
var _apidomDatamodel = require("@speclynx/apidom-datamodel");
|
|
10
|
+
var _apidomNsArazzo = require("@speclynx/apidom-ns-arazzo-1");
|
|
11
|
+
var _ExecutionError = _interopRequireDefault(require("../errors/ExecutionError.cjs"));
|
|
12
|
+
/**
|
|
13
|
+
* What one attempt at a step produced.
|
|
14
|
+
*
|
|
15
|
+
* Both kinds of step reduce to this — an operation step (whose richer
|
|
16
|
+
* `StepExecutionResult` satisfies this shape) and a sub-workflow step
|
|
17
|
+
* (synthesized from the sub-run's outcome) — so the retry loop drives them
|
|
18
|
+
* identically and knows nothing about either.
|
|
19
|
+
* @internal
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The terminal outcome of a step once its retries are settled.
|
|
24
|
+
* @internal
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Options for the StepRetryRunner.
|
|
29
|
+
* @internal
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Which step is being run — carried for diagnostics, plus the run's cancellation
|
|
34
|
+
* so the wait between attempts can be cut short.
|
|
35
|
+
* @internal
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Runs a step until its `retry` failure actions are settled.
|
|
40
|
+
*
|
|
41
|
+
* On success, or an immediate non-retry failure, the step runs once. On a failure
|
|
42
|
+
* whose first matching action is a `retry`, the step is re-run up to that
|
|
43
|
+
* action's `retryLimit` (default 1), waiting `retryAfter` seconds between
|
|
44
|
+
* attempts. Per Arazzo 1.0.1 — "retryLimit MUST be exhausted prior to executing
|
|
45
|
+
* subsequent failure actions" — an exhausted retry falls through to the *next*
|
|
46
|
+
* matching failure action of the latest attempt, which may itself be another
|
|
47
|
+
* `retry` with its own independent budget, or a terminal `end` / `goto`. Each
|
|
48
|
+
* attempt re-runs the step and re-selects against the fresh outcome, so an action
|
|
49
|
+
* whose criteria stop matching stops being chosen.
|
|
50
|
+
*
|
|
51
|
+
* It is agnostic about what a step *is*: the caller supplies a thunk that runs one
|
|
52
|
+
* attempt and returns its {@link StepAttemptOutcome}, which is what lets an
|
|
53
|
+
* operation step and a sub-workflow step retry through identical machinery. That
|
|
54
|
+
* thunk is also where a caller puts anything it wants done per attempt — charging
|
|
55
|
+
* a run budget, say — so this class needs no notion of one; a thunk that throws
|
|
56
|
+
* ends the step there, without further attempts. One consequence is worth
|
|
57
|
+
* knowing: a `retryAfter` delay is awaited before the next attempt begins, so a
|
|
58
|
+
* caller whose per-attempt check is about to fail (a spent budget, say) still
|
|
59
|
+
* pays that delay before its error surfaces. Keeping the seam this narrow is
|
|
60
|
+
* worth one bounded wait on a run that was already doomed.
|
|
61
|
+
*
|
|
62
|
+
* A `retry` action's `stepId` / `workflowId` reference works the same way: this
|
|
63
|
+
* class only decides *when* one fires (once per firing, after the `retryAfter`
|
|
64
|
+
* wait, before the step is re-run) — it hands the action to
|
|
65
|
+
* {@link StepRetryRunContext.runReference} and knows nothing about *what* the
|
|
66
|
+
* reference is or how it runs. A caller that omits that callback gets the
|
|
67
|
+
* standalone rejection instead of a silently ignored reference.
|
|
68
|
+
*
|
|
69
|
+
* Cancellation is the one thing it will not wait out: the run's `AbortSignal`
|
|
70
|
+
* reaches the timer, which returns early, and the next attempt then trips the
|
|
71
|
+
* caller's own check. Turning an abort into an error stays the caller's job, as
|
|
72
|
+
* with the budget; the signal is here only because a wait already in progress
|
|
73
|
+
* cannot be cut short from inside the thunk.
|
|
74
|
+
* @internal
|
|
75
|
+
*/
|
|
76
|
+
class StepRetryRunner {
|
|
77
|
+
static #DEFAULT_RETRY_LIMIT = 1;
|
|
78
|
+
/**
|
|
79
|
+
* A real timer that stops waiting as soon as the run is cancelled, and leaves
|
|
80
|
+
* no pending timeout behind either way — a `retryAfter` of minutes would
|
|
81
|
+
* otherwise hold the event loop open long after the caller walked away.
|
|
82
|
+
*
|
|
83
|
+
* It resolves on abort rather than rejecting: turning a cancellation into an
|
|
84
|
+
* error is the caller's per-attempt check, which reports it uniformly for a
|
|
85
|
+
* run cancelled mid-wait and one cancelled mid-request.
|
|
86
|
+
*/
|
|
87
|
+
static #DEFAULT_SLEEP = (ms, signal) => new Promise(resolve => {
|
|
88
|
+
if (signal?.aborted === true) {
|
|
89
|
+
resolve();
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
const onAbort = () => {
|
|
93
|
+
clearTimeout(timer);
|
|
94
|
+
resolve();
|
|
95
|
+
};
|
|
96
|
+
// unsubscribed when the wait ends normally, so a signal outliving many
|
|
97
|
+
// retries does not accumulate a listener per wait.
|
|
98
|
+
const timer = setTimeout(() => {
|
|
99
|
+
signal?.removeEventListener('abort', onAbort);
|
|
100
|
+
resolve();
|
|
101
|
+
}, ms);
|
|
102
|
+
signal?.addEventListener('abort', onAbort, {
|
|
103
|
+
once: true
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
#sleep;
|
|
107
|
+
constructor(options = {}) {
|
|
108
|
+
this.#sleep = options.sleep ?? StepRetryRunner.#DEFAULT_SLEEP;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Runs `attempt` until the step succeeds, a terminal failure action is reached,
|
|
113
|
+
* or its retries are exhausted.
|
|
114
|
+
*/
|
|
115
|
+
async run(attempt, context) {
|
|
116
|
+
// attempts already spent on each retry action, keyed by the action element.
|
|
117
|
+
// The element is the correct identity here: re-running the step returns the
|
|
118
|
+
// same element instances (the workflow is normalized once and `onFailure`'s
|
|
119
|
+
// getter returns its stored children — nothing re-refracts per call), and
|
|
120
|
+
// keying by the element survives the criteria re-filtering that varies
|
|
121
|
+
// `matchedActions` between attempts (position within that filtered list is
|
|
122
|
+
// NOT stable when a response change drops an earlier action). So each retry
|
|
123
|
+
// in a chain keeps its own independent budget across re-runs.
|
|
124
|
+
const retriesSpent = new Map();
|
|
125
|
+
let attempts = 0;
|
|
126
|
+
for (;;) {
|
|
127
|
+
const outcome = await attempt();
|
|
128
|
+
attempts += 1;
|
|
129
|
+
if (outcome.successful) {
|
|
130
|
+
return {
|
|
131
|
+
outcome,
|
|
132
|
+
action: outcome.action,
|
|
133
|
+
attempts
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// walk the matching failure actions: fire the first retry that still has
|
|
138
|
+
// budget, else fall through to the first terminal (non-retry) action.
|
|
139
|
+
let fired = false;
|
|
140
|
+
let terminal;
|
|
141
|
+
for (const action of outcome.matchedActions) {
|
|
142
|
+
if (!this.#isRetry(action)) {
|
|
143
|
+
terminal = action;
|
|
144
|
+
break;
|
|
145
|
+
}
|
|
146
|
+
const spent = retriesSpent.get(action) ?? 0;
|
|
147
|
+
if (spent >= this.#retryLimit(action)) continue; // exhausted — try the next action
|
|
148
|
+
|
|
149
|
+
const carriesReference = this.#carriesReference(action);
|
|
150
|
+
// narrowed once here rather than asserted at the call site below: by
|
|
151
|
+
// the time `runReference` is invoked, its absence would already have
|
|
152
|
+
// thrown on the line above, so the type reflects that instead of
|
|
153
|
+
// relying on a non-null assertion to say so.
|
|
154
|
+
const runReference = carriesReference ? context.runReference : undefined;
|
|
155
|
+
if (carriesReference && runReference === undefined) this.#rejectReference(context);
|
|
156
|
+
retriesSpent.set(action, spent + 1);
|
|
157
|
+
// only sleep for a real, positive delay — skip the event-loop yield of
|
|
158
|
+
// sleep(0) for immediate retries, and never hand a custom sleep a
|
|
159
|
+
// zero/negative/NaN value.
|
|
160
|
+
const delayMs = this.#retryAfterMs(action);
|
|
161
|
+
if (delayMs > 0) await this.#sleep(delayMs, context.signal);
|
|
162
|
+
if (runReference !== undefined) await runReference(action);
|
|
163
|
+
fired = true;
|
|
164
|
+
break;
|
|
165
|
+
}
|
|
166
|
+
if (fired) continue; // re-run the step
|
|
167
|
+
return {
|
|
168
|
+
outcome,
|
|
169
|
+
action: terminal,
|
|
170
|
+
attempts
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Whether a matched action is a `retry` failure action (narrowing it to
|
|
177
|
+
* {@link FailureActionElement} so `retryLimit` / `retryAfter` are accessible).
|
|
178
|
+
*/
|
|
179
|
+
#isRetry(action) {
|
|
180
|
+
return (0, _apidomNsArazzo.isFailureActionElement)(action) && (0, _apidomCore.toValue)(action.type) === 'retry';
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* A `retry` action's attempt budget — its `retryLimit`, or the default of 1
|
|
185
|
+
* when unset or non-numeric.
|
|
186
|
+
*/
|
|
187
|
+
#retryLimit(action) {
|
|
188
|
+
return (0, _apidomDatamodel.isNumberElement)(action.retryLimit) ? (0, _apidomCore.toValue)(action.retryLimit) : StepRetryRunner.#DEFAULT_RETRY_LIMIT;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* The delay before a retry, in milliseconds — the action's `retryAfter`
|
|
193
|
+
* (seconds) converted. `0` when unset, non-numeric, or not a finite positive
|
|
194
|
+
* value (a negative or `NaN` `retryAfter` means "no wait", never a bad value
|
|
195
|
+
* handed to `sleep`).
|
|
196
|
+
*/
|
|
197
|
+
#retryAfterMs(action) {
|
|
198
|
+
if (!(0, _apidomDatamodel.isNumberElement)(action.retryAfter)) return 0;
|
|
199
|
+
const ms = (0, _apidomCore.toValue)(action.retryAfter) * 1000;
|
|
200
|
+
return Number.isFinite(ms) && ms > 0 ? ms : 0;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Whether a `retry` action carries a `stepId` / `workflowId` reference to
|
|
205
|
+
* execute before retrying ("the reference is executed and the context is
|
|
206
|
+
* returned, after which the current step is retried").
|
|
207
|
+
*/
|
|
208
|
+
#carriesReference(action) {
|
|
209
|
+
return (0, _apidomDatamodel.isStringElement)(action.stepId) || (0, _apidomDatamodel.isStringElement)(action.workflowId);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Rejects a referenced `retry` action when the caller supplied no
|
|
214
|
+
* {@link StepRetryRunContext.runReference}, so an unsupported caller
|
|
215
|
+
* (or a step retried directly against this runner in isolation) fails
|
|
216
|
+
* loudly rather than silently ignoring the reference.
|
|
217
|
+
*/
|
|
218
|
+
#rejectReference(context) {
|
|
219
|
+
throw new _ExecutionError.default(`retry action on step "${context.stepId}" in workflow "${context.workflowId}" carries a stepId/workflowId reference, but no runReference was given to run it`, {
|
|
220
|
+
stepId: context.stepId,
|
|
221
|
+
workflowId: context.workflowId,
|
|
222
|
+
reason: 'retry-reference-unsupported'
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
var _default = exports.default = StepRetryRunner;
|