@usearazzo/runner 0.0.0-stage → 1.0.1-alpha.10

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.
Files changed (152) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/LICENSE +202 -0
  3. package/NOTICE +20 -0
  4. package/README.md +777 -2
  5. package/dist/arazzo-runner.browser.js +113254 -0
  6. package/dist/arazzo-runner.browser.min.js +1 -0
  7. package/package.json +91 -4
  8. package/src/action/ActionResolver.cjs +160 -0
  9. package/src/action/ActionResolver.mjs +150 -0
  10. package/src/assembler/OpenAPI2DocumentAssembler.cjs +111 -0
  11. package/src/assembler/OpenAPI2DocumentAssembler.mjs +104 -0
  12. package/src/assembler/OpenAPI30DocumentAssembler.cjs +110 -0
  13. package/src/assembler/OpenAPI30DocumentAssembler.mjs +103 -0
  14. package/src/assembler/OpenAPI31DocumentAssembler.cjs +111 -0
  15. package/src/assembler/OpenAPI31DocumentAssembler.mjs +104 -0
  16. package/src/assembler/OpenAPIDocumentAssembler.cjs +58 -0
  17. package/src/assembler/OpenAPIDocumentAssembler.mjs +50 -0
  18. package/src/client/HTTPClient.cjs +5 -0
  19. package/src/client/HTTPClient.mjs +1 -0
  20. package/src/client/HTTPClientFetch.cjs +16 -0
  21. package/src/client/HTTPClientFetch.mjs +10 -0
  22. package/src/client/OpenAPIOperationRequest.cjs +5 -0
  23. package/src/client/OpenAPIOperationRequest.mjs +1 -0
  24. package/src/client/OpenAPIOperationResponse.cjs +42 -0
  25. package/src/client/OpenAPIOperationResponse.mjs +36 -0
  26. package/src/client/OpenAPIOperationResponseNormalizer.cjs +48 -0
  27. package/src/client/OpenAPIOperationResponseNormalizer.mjs +40 -0
  28. package/src/client/ParameterDelivery.cjs +119 -0
  29. package/src/client/ParameterDelivery.mjs +113 -0
  30. package/src/constants.cjs +9 -0
  31. package/src/constants.mjs +3 -0
  32. package/src/criterion/CriterionEvaluator.cjs +205 -0
  33. package/src/criterion/CriterionEvaluator.mjs +196 -0
  34. package/src/criterion/JSONPathCriterionEvaluator.cjs +47 -0
  35. package/src/criterion/JSONPathCriterionEvaluator.mjs +40 -0
  36. package/src/criterion/RegexCriterionEvaluator.cjs +67 -0
  37. package/src/criterion/RegexCriterionEvaluator.mjs +60 -0
  38. package/src/criterion/SimpleCriterionEvaluator.cjs +50 -0
  39. package/src/criterion/SimpleCriterionEvaluator.mjs +43 -0
  40. package/src/criterion/XPathCriterionEvaluator.cjs +117 -0
  41. package/src/criterion/XPathCriterionEvaluator.mjs +109 -0
  42. package/src/document/APIDocument.cjs +35 -0
  43. package/src/document/APIDocument.mjs +30 -0
  44. package/src/document/ArazzoDocument.cjs +81 -0
  45. package/src/document/ArazzoDocument.mjs +74 -0
  46. package/src/document/ArazzoSourceDescriptionIndex.cjs +26 -0
  47. package/src/document/ArazzoSourceDescriptionIndex.mjs +20 -0
  48. package/src/document/ArazzoStepIndex.cjs +25 -0
  49. package/src/document/ArazzoStepIndex.mjs +19 -0
  50. package/src/document/ArazzoWorkflowIndex.cjs +17 -0
  51. package/src/document/ArazzoWorkflowIndex.mjs +11 -0
  52. package/src/document/OpenAPIDocument.cjs +28 -0
  53. package/src/document/OpenAPIDocument.mjs +21 -0
  54. package/src/document/OpenAPIOperationIndex.cjs +24 -0
  55. package/src/document/OpenAPIOperationIndex.mjs +18 -0
  56. package/src/document/ParameterIdentity.cjs +93 -0
  57. package/src/document/ParameterIdentity.mjs +88 -0
  58. package/src/document/openapi-types.cjs +5 -0
  59. package/src/document/openapi-types.mjs +1 -0
  60. package/src/errors/ArazzoRunnerError.cjs +10 -0
  61. package/src/errors/ArazzoRunnerError.mjs +5 -0
  62. package/src/errors/AssemblerError.cjs +11 -0
  63. package/src/errors/AssemblerError.mjs +4 -0
  64. package/src/errors/ClientError.cjs +11 -0
  65. package/src/errors/ClientError.mjs +4 -0
  66. package/src/errors/CriterionError.cjs +11 -0
  67. package/src/errors/CriterionError.mjs +4 -0
  68. package/src/errors/ExecutionError.cjs +11 -0
  69. package/src/errors/ExecutionError.mjs +4 -0
  70. package/src/errors/ExtractionError.cjs +11 -0
  71. package/src/errors/ExtractionError.mjs +4 -0
  72. package/src/errors/InvalidEntryDocumentError.cjs +11 -0
  73. package/src/errors/InvalidEntryDocumentError.mjs +4 -0
  74. package/src/errors/NormalizationError.cjs +11 -0
  75. package/src/errors/NormalizationError.mjs +4 -0
  76. package/src/errors/ResolverError.cjs +11 -0
  77. package/src/errors/ResolverError.mjs +4 -0
  78. package/src/errors/RuntimeExpressionError.cjs +11 -0
  79. package/src/errors/RuntimeExpressionError.mjs +4 -0
  80. package/src/errors/UnmatchedProviderError.cjs +11 -0
  81. package/src/errors/UnmatchedProviderError.mjs +4 -0
  82. package/src/executor/ArazzoWorkflowLocatorNormalizer.cjs +147 -0
  83. package/src/executor/ArazzoWorkflowLocatorNormalizer.mjs +137 -0
  84. package/src/executor/OpenAPIOperationExecutor.cjs +458 -0
  85. package/src/executor/OpenAPIOperationExecutor.mjs +447 -0
  86. package/src/executor/OpenAPIOperationLocatorNormalizer.cjs +197 -0
  87. package/src/executor/OpenAPIOperationLocatorNormalizer.mjs +189 -0
  88. package/src/executor/SourceDescriptionsLocatorNormalizer.cjs +44 -0
  89. package/src/executor/SourceDescriptionsLocatorNormalizer.mjs +38 -0
  90. package/src/executor/StepExecutor.cjs +333 -0
  91. package/src/executor/StepExecutor.mjs +326 -0
  92. package/src/executor/StepRetryRunner.cjs +226 -0
  93. package/src/executor/StepRetryRunner.mjs +215 -0
  94. package/src/executor/StepTransitionInterpreter.cjs +159 -0
  95. package/src/executor/StepTransitionInterpreter.mjs +151 -0
  96. package/src/executor/WorkflowCallStack.cjs +116 -0
  97. package/src/executor/WorkflowCallStack.mjs +106 -0
  98. package/src/executor/WorkflowExecutor.cjs +1091 -0
  99. package/src/executor/WorkflowExecutor.mjs +1077 -0
  100. package/src/executor/abort.cjs +70 -0
  101. package/src/executor/abort.mjs +60 -0
  102. package/src/expression/RuntimeExpressionContext.cjs +5 -0
  103. package/src/expression/RuntimeExpressionContext.mjs +1 -0
  104. package/src/expression/RuntimeExpressionEvaluator.cjs +337 -0
  105. package/src/expression/RuntimeExpressionEvaluator.mjs +328 -0
  106. package/src/extractor/ArazzoStepExtractor.cjs +48 -0
  107. package/src/extractor/ArazzoStepExtractor.mjs +41 -0
  108. package/src/extractor/ArazzoWorkflowExtractor.cjs +50 -0
  109. package/src/extractor/ArazzoWorkflowExtractor.mjs +43 -0
  110. package/src/extractor/OpenAPIOperationExtractor.cjs +84 -0
  111. package/src/extractor/OpenAPIOperationExtractor.mjs +77 -0
  112. package/src/index.cjs +419 -0
  113. package/src/index.mjs +73 -0
  114. package/src/normalizer/ArazzoStepNormalizer.cjs +23 -0
  115. package/src/normalizer/ArazzoStepNormalizer.mjs +17 -0
  116. package/src/normalizer/ArazzoWorkflowNormalizer.cjs +214 -0
  117. package/src/normalizer/ArazzoWorkflowNormalizer.mjs +206 -0
  118. package/src/normalizer/OpenAPI2OperationNormalizer.cjs +128 -0
  119. package/src/normalizer/OpenAPI2OperationNormalizer.mjs +120 -0
  120. package/src/normalizer/OpenAPI30OperationNormalizer.cjs +129 -0
  121. package/src/normalizer/OpenAPI30OperationNormalizer.mjs +121 -0
  122. package/src/normalizer/OpenAPI31OperationNormalizer.cjs +131 -0
  123. package/src/normalizer/OpenAPI31OperationNormalizer.mjs +123 -0
  124. package/src/normalizer/OpenAPIOperationNormalizer.cjs +58 -0
  125. package/src/normalizer/OpenAPIOperationNormalizer.mjs +50 -0
  126. package/src/registry/DocumentRegistry.cjs +160 -0
  127. package/src/registry/DocumentRegistry.mjs +151 -0
  128. package/src/registry/providers/ArazzoDocumentRegistryProvider.cjs +154 -0
  129. package/src/registry/providers/ArazzoDocumentRegistryProvider.mjs +146 -0
  130. package/src/registry/providers/DocumentRegistryProvider.cjs +31 -0
  131. package/src/registry/providers/DocumentRegistryProvider.mjs +25 -0
  132. package/src/registry/providers/OpenAPIDocumentRegistryProvider.cjs +122 -0
  133. package/src/registry/providers/OpenAPIDocumentRegistryProvider.mjs +114 -0
  134. package/src/resolver/ArazzoValueResolver.cjs +42 -0
  135. package/src/resolver/ArazzoValueResolver.mjs +37 -0
  136. package/src/resolver/OpenAPIOperationParameterResolver.cjs +117 -0
  137. package/src/resolver/OpenAPIOperationParameterResolver.mjs +110 -0
  138. package/src/resolver/OutputResolver.cjs +71 -0
  139. package/src/resolver/OutputResolver.mjs +64 -0
  140. package/src/resolver/RequestBodyResolver.cjs +227 -0
  141. package/src/resolver/RequestBodyResolver.mjs +220 -0
  142. package/src/resolver/ResolverScope.cjs +5 -0
  143. package/src/resolver/ResolverScope.mjs +1 -0
  144. package/src/resolver/StepParameterResolver.cjs +22 -0
  145. package/src/resolver/StepParameterResolver.mjs +15 -0
  146. package/src/resolver/WorkflowParameterResolver.cjs +73 -0
  147. package/src/resolver/WorkflowParameterResolver.mjs +66 -0
  148. package/src/state/WorkflowExecutionState.cjs +99 -0
  149. package/src/state/WorkflowExecutionState.mjs +93 -0
  150. package/src/vendor/swagger-client.cjs +1 -0
  151. package/src/vendor/swagger-client.mjs +1 -0
  152. 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;