@usearazzo/runner 0.0.0-stage → 1.0.1-alpha.11
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/CHANGELOG.md +37 -0
- package/LICENSE +202 -0
- package/NOTICE +20 -0
- package/README.md +163 -2
- package/dist/arazzo-runner.browser.js +113245 -0
- package/dist/arazzo-runner.browser.min.js +1 -0
- package/package.json +95 -4
- package/src/ArazzoRunner.cjs +122 -0
- package/src/ArazzoRunner.mjs +113 -0
- 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 +458 -0
- package/src/executor/OpenAPIOperationExecutor.mjs +447 -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 +426 -0
- package/src/index.mjs +75 -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 +2410 -0
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import { toValue } from '@speclynx/apidom-core';
|
|
2
|
+
import { isNumberElement, isStringElement } from '@speclynx/apidom-datamodel';
|
|
3
|
+
import { isFailureActionElement } from '@speclynx/apidom-ns-arazzo-1';
|
|
4
|
+
import ExecutionError from "../errors/ExecutionError.mjs";
|
|
5
|
+
/**
|
|
6
|
+
* What one attempt at a step produced.
|
|
7
|
+
*
|
|
8
|
+
* Both kinds of step reduce to this — an operation step (whose richer
|
|
9
|
+
* `StepExecutionResult` satisfies this shape) and a sub-workflow step
|
|
10
|
+
* (synthesized from the sub-run's outcome) — so the retry loop drives them
|
|
11
|
+
* identically and knows nothing about either.
|
|
12
|
+
* @internal
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* The terminal outcome of a step once its retries are settled.
|
|
16
|
+
* @internal
|
|
17
|
+
*/
|
|
18
|
+
/**
|
|
19
|
+
* Options for the StepRetryRunner.
|
|
20
|
+
* @internal
|
|
21
|
+
*/
|
|
22
|
+
/**
|
|
23
|
+
* Which step is being run — carried for diagnostics, plus the run's cancellation
|
|
24
|
+
* so the wait between attempts can be cut short.
|
|
25
|
+
* @internal
|
|
26
|
+
*/
|
|
27
|
+
/**
|
|
28
|
+
* Runs a step until its `retry` failure actions are settled.
|
|
29
|
+
*
|
|
30
|
+
* On success, or an immediate non-retry failure, the step runs once. On a failure
|
|
31
|
+
* whose first matching action is a `retry`, the step is re-run up to that
|
|
32
|
+
* action's `retryLimit` (default 1), waiting `retryAfter` seconds between
|
|
33
|
+
* attempts. Per Arazzo 1.0.1 — "retryLimit MUST be exhausted prior to executing
|
|
34
|
+
* subsequent failure actions" — an exhausted retry falls through to the *next*
|
|
35
|
+
* matching failure action of the latest attempt, which may itself be another
|
|
36
|
+
* `retry` with its own independent budget, or a terminal `end` / `goto`. Each
|
|
37
|
+
* attempt re-runs the step and re-selects against the fresh outcome, so an action
|
|
38
|
+
* whose criteria stop matching stops being chosen.
|
|
39
|
+
*
|
|
40
|
+
* It is agnostic about what a step *is*: the caller supplies a thunk that runs one
|
|
41
|
+
* attempt and returns its {@link StepAttemptOutcome}, which is what lets an
|
|
42
|
+
* operation step and a sub-workflow step retry through identical machinery. That
|
|
43
|
+
* thunk is also where a caller puts anything it wants done per attempt — charging
|
|
44
|
+
* a run budget, say — so this class needs no notion of one; a thunk that throws
|
|
45
|
+
* ends the step there, without further attempts. One consequence is worth
|
|
46
|
+
* knowing: a `retryAfter` delay is awaited before the next attempt begins, so a
|
|
47
|
+
* caller whose per-attempt check is about to fail (a spent budget, say) still
|
|
48
|
+
* pays that delay before its error surfaces. Keeping the seam this narrow is
|
|
49
|
+
* worth one bounded wait on a run that was already doomed.
|
|
50
|
+
*
|
|
51
|
+
* A `retry` action's `stepId` / `workflowId` reference works the same way: this
|
|
52
|
+
* class only decides *when* one fires (once per firing, after the `retryAfter`
|
|
53
|
+
* wait, before the step is re-run) — it hands the action to
|
|
54
|
+
* {@link StepRetryRunContext.runReference} and knows nothing about *what* the
|
|
55
|
+
* reference is or how it runs. A caller that omits that callback gets the
|
|
56
|
+
* standalone rejection instead of a silently ignored reference.
|
|
57
|
+
*
|
|
58
|
+
* Cancellation is the one thing it will not wait out: the run's `AbortSignal`
|
|
59
|
+
* reaches the timer, which returns early, and the next attempt then trips the
|
|
60
|
+
* caller's own check. Turning an abort into an error stays the caller's job, as
|
|
61
|
+
* with the budget; the signal is here only because a wait already in progress
|
|
62
|
+
* cannot be cut short from inside the thunk.
|
|
63
|
+
* @internal
|
|
64
|
+
*/
|
|
65
|
+
class StepRetryRunner {
|
|
66
|
+
static #DEFAULT_RETRY_LIMIT = 1;
|
|
67
|
+
/**
|
|
68
|
+
* A real timer that stops waiting as soon as the run is cancelled, and leaves
|
|
69
|
+
* no pending timeout behind either way — a `retryAfter` of minutes would
|
|
70
|
+
* otherwise hold the event loop open long after the caller walked away.
|
|
71
|
+
*
|
|
72
|
+
* It resolves on abort rather than rejecting: turning a cancellation into an
|
|
73
|
+
* error is the caller's per-attempt check, which reports it uniformly for a
|
|
74
|
+
* run cancelled mid-wait and one cancelled mid-request.
|
|
75
|
+
*/
|
|
76
|
+
static #DEFAULT_SLEEP = (ms, signal) => new Promise(resolve => {
|
|
77
|
+
if (signal?.aborted === true) {
|
|
78
|
+
resolve();
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
const onAbort = () => {
|
|
82
|
+
clearTimeout(timer);
|
|
83
|
+
resolve();
|
|
84
|
+
};
|
|
85
|
+
// unsubscribed when the wait ends normally, so a signal outliving many
|
|
86
|
+
// retries does not accumulate a listener per wait.
|
|
87
|
+
const timer = setTimeout(() => {
|
|
88
|
+
signal?.removeEventListener('abort', onAbort);
|
|
89
|
+
resolve();
|
|
90
|
+
}, ms);
|
|
91
|
+
signal?.addEventListener('abort', onAbort, {
|
|
92
|
+
once: true
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
#sleep;
|
|
96
|
+
constructor(options = {}) {
|
|
97
|
+
this.#sleep = options.sleep ?? StepRetryRunner.#DEFAULT_SLEEP;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Runs `attempt` until the step succeeds, a terminal failure action is reached,
|
|
102
|
+
* or its retries are exhausted.
|
|
103
|
+
*/
|
|
104
|
+
async run(attempt, context) {
|
|
105
|
+
// attempts already spent on each retry action, keyed by the action element.
|
|
106
|
+
// The element is the correct identity here: re-running the step returns the
|
|
107
|
+
// same element instances (the workflow is normalized once and `onFailure`'s
|
|
108
|
+
// getter returns its stored children — nothing re-refracts per call), and
|
|
109
|
+
// keying by the element survives the criteria re-filtering that varies
|
|
110
|
+
// `matchedActions` between attempts (position within that filtered list is
|
|
111
|
+
// NOT stable when a response change drops an earlier action). So each retry
|
|
112
|
+
// in a chain keeps its own independent budget across re-runs.
|
|
113
|
+
const retriesSpent = new Map();
|
|
114
|
+
let attempts = 0;
|
|
115
|
+
for (;;) {
|
|
116
|
+
const outcome = await attempt();
|
|
117
|
+
attempts += 1;
|
|
118
|
+
if (outcome.successful) {
|
|
119
|
+
return {
|
|
120
|
+
outcome,
|
|
121
|
+
action: outcome.action,
|
|
122
|
+
attempts
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// walk the matching failure actions: fire the first retry that still has
|
|
127
|
+
// budget, else fall through to the first terminal (non-retry) action.
|
|
128
|
+
let fired = false;
|
|
129
|
+
let terminal;
|
|
130
|
+
for (const action of outcome.matchedActions) {
|
|
131
|
+
if (!this.#isRetry(action)) {
|
|
132
|
+
terminal = action;
|
|
133
|
+
break;
|
|
134
|
+
}
|
|
135
|
+
const spent = retriesSpent.get(action) ?? 0;
|
|
136
|
+
if (spent >= this.#retryLimit(action)) continue; // exhausted — try the next action
|
|
137
|
+
|
|
138
|
+
const carriesReference = this.#carriesReference(action);
|
|
139
|
+
// narrowed once here rather than asserted at the call site below: by
|
|
140
|
+
// the time `runReference` is invoked, its absence would already have
|
|
141
|
+
// thrown on the line above, so the type reflects that instead of
|
|
142
|
+
// relying on a non-null assertion to say so.
|
|
143
|
+
const runReference = carriesReference ? context.runReference : undefined;
|
|
144
|
+
if (carriesReference && runReference === undefined) this.#rejectReference(context);
|
|
145
|
+
retriesSpent.set(action, spent + 1);
|
|
146
|
+
// only sleep for a real, positive delay — skip the event-loop yield of
|
|
147
|
+
// sleep(0) for immediate retries, and never hand a custom sleep a
|
|
148
|
+
// zero/negative/NaN value.
|
|
149
|
+
const delayMs = this.#retryAfterMs(action);
|
|
150
|
+
if (delayMs > 0) await this.#sleep(delayMs, context.signal);
|
|
151
|
+
if (runReference !== undefined) await runReference(action);
|
|
152
|
+
fired = true;
|
|
153
|
+
break;
|
|
154
|
+
}
|
|
155
|
+
if (fired) continue; // re-run the step
|
|
156
|
+
return {
|
|
157
|
+
outcome,
|
|
158
|
+
action: terminal,
|
|
159
|
+
attempts
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Whether a matched action is a `retry` failure action (narrowing it to
|
|
166
|
+
* {@link FailureActionElement} so `retryLimit` / `retryAfter` are accessible).
|
|
167
|
+
*/
|
|
168
|
+
#isRetry(action) {
|
|
169
|
+
return isFailureActionElement(action) && toValue(action.type) === 'retry';
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* A `retry` action's attempt budget — its `retryLimit`, or the default of 1
|
|
174
|
+
* when unset or non-numeric.
|
|
175
|
+
*/
|
|
176
|
+
#retryLimit(action) {
|
|
177
|
+
return isNumberElement(action.retryLimit) ? toValue(action.retryLimit) : StepRetryRunner.#DEFAULT_RETRY_LIMIT;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* The delay before a retry, in milliseconds — the action's `retryAfter`
|
|
182
|
+
* (seconds) converted. `0` when unset, non-numeric, or not a finite positive
|
|
183
|
+
* value (a negative or `NaN` `retryAfter` means "no wait", never a bad value
|
|
184
|
+
* handed to `sleep`).
|
|
185
|
+
*/
|
|
186
|
+
#retryAfterMs(action) {
|
|
187
|
+
if (!isNumberElement(action.retryAfter)) return 0;
|
|
188
|
+
const ms = toValue(action.retryAfter) * 1000;
|
|
189
|
+
return Number.isFinite(ms) && ms > 0 ? ms : 0;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Whether a `retry` action carries a `stepId` / `workflowId` reference to
|
|
194
|
+
* execute before retrying ("the reference is executed and the context is
|
|
195
|
+
* returned, after which the current step is retried").
|
|
196
|
+
*/
|
|
197
|
+
#carriesReference(action) {
|
|
198
|
+
return isStringElement(action.stepId) || isStringElement(action.workflowId);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Rejects a referenced `retry` action when the caller supplied no
|
|
203
|
+
* {@link StepRetryRunContext.runReference}, so an unsupported caller
|
|
204
|
+
* (or a step retried directly against this runner in isolation) fails
|
|
205
|
+
* loudly rather than silently ignoring the reference.
|
|
206
|
+
*/
|
|
207
|
+
#rejectReference(context) {
|
|
208
|
+
throw new ExecutionError(`retry action on step "${context.stepId}" in workflow "${context.workflowId}" carries a stepId/workflowId reference, but no runReference was given to run it`, {
|
|
209
|
+
stepId: context.stepId,
|
|
210
|
+
workflowId: context.workflowId,
|
|
211
|
+
reason: 'retry-reference-unsupported'
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
export default StepRetryRunner;
|
|
@@ -0,0 +1,159 @@
|
|
|
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.actionTargets = actionTargets;
|
|
8
|
+
exports.default = void 0;
|
|
9
|
+
var _apidomCore = require("@speclynx/apidom-core");
|
|
10
|
+
var _apidomDatamodel = require("@speclynx/apidom-datamodel");
|
|
11
|
+
var _ExecutionError = _interopRequireDefault(require("../errors/ExecutionError.cjs"));
|
|
12
|
+
/**
|
|
13
|
+
* Which of an action's mutually exclusive `stepId` / `workflowId` fields are
|
|
14
|
+
* present, ahead of a caller deciding what that means.
|
|
15
|
+
*
|
|
16
|
+
* Shared rather than duplicated by `goto`'s own check here and a `retry`
|
|
17
|
+
* action's reference check in `WorkflowExecutor` — both Success/Failure
|
|
18
|
+
* Action Object fields carry the identical "mutually exclusive" constraint,
|
|
19
|
+
* so both need the identical two `isStringElement` reads before diverging on
|
|
20
|
+
* their own wording and branch handling.
|
|
21
|
+
* @internal
|
|
22
|
+
*/
|
|
23
|
+
function actionTargets(action) {
|
|
24
|
+
return {
|
|
25
|
+
hasStepId: (0, _apidomDatamodel.isStringElement)(action.stepId),
|
|
26
|
+
hasWorkflowId: (0, _apidomDatamodel.isStringElement)(action.workflowId)
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* What a workflow does after a step, once the step's action has been
|
|
32
|
+
* interpreted.
|
|
33
|
+
*
|
|
34
|
+
* `next` runs the following step (the success default, or a matched `goto` that
|
|
35
|
+
* targets the next step); `goto` jumps to a step by id within the current
|
|
36
|
+
* workflow; `transfer` hands control one-way to another workflow, which the
|
|
37
|
+
* caller's run then ends on; `end` stops the run with `status: ended`; `break`
|
|
38
|
+
* stops with `status: failed` (the failure default).
|
|
39
|
+
* @internal
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Which step's outcome is being interpreted — carried for diagnostics only.
|
|
44
|
+
* @internal
|
|
45
|
+
*/
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Turns the action a step resolved to into what the workflow does next.
|
|
49
|
+
*
|
|
50
|
+
* This is the Arazzo 1.0.1 Success/Failure Action Object semantics in one place,
|
|
51
|
+
* and nothing else: it holds no state, performs no I/O, and does not act on the
|
|
52
|
+
* transition it returns — moving through the step list stays the workflow
|
|
53
|
+
* executor's business.
|
|
54
|
+
*
|
|
55
|
+
* - **no matching action** — the path default: "the next sequential step shall be
|
|
56
|
+
* executed" on success, "break and return" on failure.
|
|
57
|
+
* - **`end`** — "the workflow ends, and context returns to the caller with
|
|
58
|
+
* applicable outputs".
|
|
59
|
+
* - **`goto` a `stepId`** — jumps within the current workflow, which is where the
|
|
60
|
+
* specification requires the target to be.
|
|
61
|
+
* - **`goto` a `workflowId`** — a one-way transfer of control to another
|
|
62
|
+
* workflow: the `stepId`/`workflowId` field descriptions condition "context
|
|
63
|
+
* transfers back upon completion" on `retry` specifically, so for `goto` it
|
|
64
|
+
* does not — the current run ends here, and running the target is the
|
|
65
|
+
* executor's job, since running workflows is orchestration rather than
|
|
66
|
+
* policy. A `goto` naming both `stepId` and `workflowId` is malformed — the
|
|
67
|
+
* spec marks them mutually exclusive — and is rejected before either is
|
|
68
|
+
* read.
|
|
69
|
+
* - **anything else** — malformed input rather than a defined control flow.
|
|
70
|
+
* `retry` is the one action that never arrives here: it is settled by
|
|
71
|
+
* {@link StepRetryRunner}, which yields only the terminal action a retry chain
|
|
72
|
+
* resolves to.
|
|
73
|
+
* @internal
|
|
74
|
+
*/
|
|
75
|
+
class StepTransitionInterpreter {
|
|
76
|
+
/**
|
|
77
|
+
* Interprets a step's terminal action into the workflow's next transition.
|
|
78
|
+
*/
|
|
79
|
+
interpret(action, successful, context) {
|
|
80
|
+
if (action === undefined) {
|
|
81
|
+
// path default: next step on success, break-and-return on failure.
|
|
82
|
+
return successful ? {
|
|
83
|
+
kind: 'next'
|
|
84
|
+
} : {
|
|
85
|
+
kind: 'break'
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
const {
|
|
89
|
+
workflowId,
|
|
90
|
+
stepId
|
|
91
|
+
} = context;
|
|
92
|
+
const type = (0, _apidomCore.toValue)(action.type);
|
|
93
|
+
if (type === 'end') {
|
|
94
|
+
return {
|
|
95
|
+
kind: 'end'
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
if (type === 'goto') {
|
|
99
|
+
const {
|
|
100
|
+
hasStepId,
|
|
101
|
+
hasWorkflowId
|
|
102
|
+
} = actionTargets(action);
|
|
103
|
+
if (hasStepId && hasWorkflowId) {
|
|
104
|
+
throw new _ExecutionError.default(`goto action on step "${stepId}" in workflow "${workflowId}" declares both a stepId and a workflowId (mutually exclusive)`, {
|
|
105
|
+
stepId,
|
|
106
|
+
workflowId,
|
|
107
|
+
reason: 'ambiguous-target'
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
if (hasStepId) {
|
|
111
|
+
return {
|
|
112
|
+
kind: 'goto',
|
|
113
|
+
stepId: (0, _apidomCore.toValue)(action.stepId)
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
if (hasWorkflowId) {
|
|
117
|
+
return {
|
|
118
|
+
kind: 'transfer',
|
|
119
|
+
workflowId: (0, _apidomCore.toValue)(action.workflowId)
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
throw new _ExecutionError.default(`goto action on step "${stepId}" in workflow "${workflowId}" has neither stepId nor workflowId`, {
|
|
123
|
+
stepId,
|
|
124
|
+
workflowId,
|
|
125
|
+
reason: 'goto-target-missing'
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
throw new _ExecutionError.default(`action on step "${stepId}" in workflow "${workflowId}" has unsupported type "${type}"`, {
|
|
129
|
+
stepId,
|
|
130
|
+
workflowId,
|
|
131
|
+
reason: 'unknown-action-type'
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* The index of a step within the workflow's steps, by `stepId`; a target that
|
|
137
|
+
* names no step in this workflow is an authoring error.
|
|
138
|
+
*
|
|
139
|
+
* A general lookup against the workflow's step list, shared rather than
|
|
140
|
+
* duplicated by its two callers — a `goto` target and a `retry` action's
|
|
141
|
+
* `stepId` reference — which report a miss the same way except for the
|
|
142
|
+
* `reason` and wording that name which one it was. `descriptor` is
|
|
143
|
+
* mandatory, not defaulted to the `goto` wording: an optional default would
|
|
144
|
+
* let a future third caller silently inherit goto's diagnostics by
|
|
145
|
+
* forgetting to pass one.
|
|
146
|
+
*/
|
|
147
|
+
indexOfStep(steps, stepId, workflowId, descriptor) {
|
|
148
|
+
const index = steps.findIndex(step => (0, _apidomCore.toValue)(step.stepId) === stepId);
|
|
149
|
+
if (index === -1) {
|
|
150
|
+
throw new _ExecutionError.default(`${descriptor.label} step "${stepId}" not found in workflow "${workflowId}"`, {
|
|
151
|
+
stepId,
|
|
152
|
+
workflowId,
|
|
153
|
+
reason: descriptor.reason
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
return index;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
var _default = exports.default = StepTransitionInterpreter;
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { toValue } from '@speclynx/apidom-core';
|
|
2
|
+
import { isStringElement } from '@speclynx/apidom-datamodel';
|
|
3
|
+
import ExecutionError from "../errors/ExecutionError.mjs";
|
|
4
|
+
/**
|
|
5
|
+
* Which of an action's mutually exclusive `stepId` / `workflowId` fields are
|
|
6
|
+
* present, ahead of a caller deciding what that means.
|
|
7
|
+
*
|
|
8
|
+
* Shared rather than duplicated by `goto`'s own check here and a `retry`
|
|
9
|
+
* action's reference check in `WorkflowExecutor` — both Success/Failure
|
|
10
|
+
* Action Object fields carry the identical "mutually exclusive" constraint,
|
|
11
|
+
* so both need the identical two `isStringElement` reads before diverging on
|
|
12
|
+
* their own wording and branch handling.
|
|
13
|
+
* @internal
|
|
14
|
+
*/
|
|
15
|
+
export function actionTargets(action) {
|
|
16
|
+
return {
|
|
17
|
+
hasStepId: isStringElement(action.stepId),
|
|
18
|
+
hasWorkflowId: isStringElement(action.workflowId)
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* What a workflow does after a step, once the step's action has been
|
|
24
|
+
* interpreted.
|
|
25
|
+
*
|
|
26
|
+
* `next` runs the following step (the success default, or a matched `goto` that
|
|
27
|
+
* targets the next step); `goto` jumps to a step by id within the current
|
|
28
|
+
* workflow; `transfer` hands control one-way to another workflow, which the
|
|
29
|
+
* caller's run then ends on; `end` stops the run with `status: ended`; `break`
|
|
30
|
+
* stops with `status: failed` (the failure default).
|
|
31
|
+
* @internal
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Which step's outcome is being interpreted — carried for diagnostics only.
|
|
36
|
+
* @internal
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Turns the action a step resolved to into what the workflow does next.
|
|
41
|
+
*
|
|
42
|
+
* This is the Arazzo 1.0.1 Success/Failure Action Object semantics in one place,
|
|
43
|
+
* and nothing else: it holds no state, performs no I/O, and does not act on the
|
|
44
|
+
* transition it returns — moving through the step list stays the workflow
|
|
45
|
+
* executor's business.
|
|
46
|
+
*
|
|
47
|
+
* - **no matching action** — the path default: "the next sequential step shall be
|
|
48
|
+
* executed" on success, "break and return" on failure.
|
|
49
|
+
* - **`end`** — "the workflow ends, and context returns to the caller with
|
|
50
|
+
* applicable outputs".
|
|
51
|
+
* - **`goto` a `stepId`** — jumps within the current workflow, which is where the
|
|
52
|
+
* specification requires the target to be.
|
|
53
|
+
* - **`goto` a `workflowId`** — a one-way transfer of control to another
|
|
54
|
+
* workflow: the `stepId`/`workflowId` field descriptions condition "context
|
|
55
|
+
* transfers back upon completion" on `retry` specifically, so for `goto` it
|
|
56
|
+
* does not — the current run ends here, and running the target is the
|
|
57
|
+
* executor's job, since running workflows is orchestration rather than
|
|
58
|
+
* policy. A `goto` naming both `stepId` and `workflowId` is malformed — the
|
|
59
|
+
* spec marks them mutually exclusive — and is rejected before either is
|
|
60
|
+
* read.
|
|
61
|
+
* - **anything else** — malformed input rather than a defined control flow.
|
|
62
|
+
* `retry` is the one action that never arrives here: it is settled by
|
|
63
|
+
* {@link StepRetryRunner}, which yields only the terminal action a retry chain
|
|
64
|
+
* resolves to.
|
|
65
|
+
* @internal
|
|
66
|
+
*/
|
|
67
|
+
class StepTransitionInterpreter {
|
|
68
|
+
/**
|
|
69
|
+
* Interprets a step's terminal action into the workflow's next transition.
|
|
70
|
+
*/
|
|
71
|
+
interpret(action, successful, context) {
|
|
72
|
+
if (action === undefined) {
|
|
73
|
+
// path default: next step on success, break-and-return on failure.
|
|
74
|
+
return successful ? {
|
|
75
|
+
kind: 'next'
|
|
76
|
+
} : {
|
|
77
|
+
kind: 'break'
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
const {
|
|
81
|
+
workflowId,
|
|
82
|
+
stepId
|
|
83
|
+
} = context;
|
|
84
|
+
const type = toValue(action.type);
|
|
85
|
+
if (type === 'end') {
|
|
86
|
+
return {
|
|
87
|
+
kind: 'end'
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
if (type === 'goto') {
|
|
91
|
+
const {
|
|
92
|
+
hasStepId,
|
|
93
|
+
hasWorkflowId
|
|
94
|
+
} = actionTargets(action);
|
|
95
|
+
if (hasStepId && hasWorkflowId) {
|
|
96
|
+
throw new ExecutionError(`goto action on step "${stepId}" in workflow "${workflowId}" declares both a stepId and a workflowId (mutually exclusive)`, {
|
|
97
|
+
stepId,
|
|
98
|
+
workflowId,
|
|
99
|
+
reason: 'ambiguous-target'
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
if (hasStepId) {
|
|
103
|
+
return {
|
|
104
|
+
kind: 'goto',
|
|
105
|
+
stepId: toValue(action.stepId)
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
if (hasWorkflowId) {
|
|
109
|
+
return {
|
|
110
|
+
kind: 'transfer',
|
|
111
|
+
workflowId: toValue(action.workflowId)
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
throw new ExecutionError(`goto action on step "${stepId}" in workflow "${workflowId}" has neither stepId nor workflowId`, {
|
|
115
|
+
stepId,
|
|
116
|
+
workflowId,
|
|
117
|
+
reason: 'goto-target-missing'
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
throw new ExecutionError(`action on step "${stepId}" in workflow "${workflowId}" has unsupported type "${type}"`, {
|
|
121
|
+
stepId,
|
|
122
|
+
workflowId,
|
|
123
|
+
reason: 'unknown-action-type'
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* The index of a step within the workflow's steps, by `stepId`; a target that
|
|
129
|
+
* names no step in this workflow is an authoring error.
|
|
130
|
+
*
|
|
131
|
+
* A general lookup against the workflow's step list, shared rather than
|
|
132
|
+
* duplicated by its two callers — a `goto` target and a `retry` action's
|
|
133
|
+
* `stepId` reference — which report a miss the same way except for the
|
|
134
|
+
* `reason` and wording that name which one it was. `descriptor` is
|
|
135
|
+
* mandatory, not defaulted to the `goto` wording: an optional default would
|
|
136
|
+
* let a future third caller silently inherit goto's diagnostics by
|
|
137
|
+
* forgetting to pass one.
|
|
138
|
+
*/
|
|
139
|
+
indexOfStep(steps, stepId, workflowId, descriptor) {
|
|
140
|
+
const index = steps.findIndex(step => toValue(step.stepId) === stepId);
|
|
141
|
+
if (index === -1) {
|
|
142
|
+
throw new ExecutionError(`${descriptor.label} step "${stepId}" not found in workflow "${workflowId}"`, {
|
|
143
|
+
stepId,
|
|
144
|
+
workflowId,
|
|
145
|
+
reason: descriptor.reason
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
return index;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
export default StepTransitionInterpreter;
|
|
@@ -0,0 +1,116 @@
|
|
|
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 _ExecutionError = _interopRequireDefault(require("../errors/ExecutionError.cjs"));
|
|
9
|
+
/**
|
|
10
|
+
* How a workflow invocation was entered.
|
|
11
|
+
*
|
|
12
|
+
* Recorded per call so a detected loop can be named for the mechanism that formed
|
|
13
|
+
* it: a repeat reached purely through `dependsOn` edges is a dependency cycle,
|
|
14
|
+
* one involving a sub-workflow step call — a `retry` action's `workflowId`
|
|
15
|
+
* reference — or a `goto`'s one-way transfer to a `workflowId` — is a call
|
|
16
|
+
* cycle. `retry` and `goto` are grouped with `step` for that classification, not
|
|
17
|
+
* `dependsOn`: each is a call the current step makes, not a stated precondition.
|
|
18
|
+
* @internal
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* One workflow invocation in progress. Its identity is the (documentURI,
|
|
23
|
+
* workflowId) pair: two documents may each define a workflow of one id, and
|
|
24
|
+
* those are different workflows — comparing bare ids would call their meeting
|
|
25
|
+
* a cycle.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Options for the WorkflowCallStack.
|
|
30
|
+
* @internal
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* The chain of workflow invocations in progress, as an immutable value.
|
|
35
|
+
*
|
|
36
|
+
* {@link WorkflowCallStack.enter} returns a *new* stack with the given workflow
|
|
37
|
+
* pushed, or throws if entering it cannot be legitimate. Because entering yields
|
|
38
|
+
* a new value rather than mutating this one, leaving is implicit: an unwinding
|
|
39
|
+
* throw cannot leave a stale entry behind, and sibling calls each see only their
|
|
40
|
+
* own chain — so a diamond (a workflow reached twice on separate paths, having
|
|
41
|
+
* completed and unwound in between) is not a cycle, while a genuine re-entry is.
|
|
42
|
+
*
|
|
43
|
+
* Two distinct failures are guarded, in this order:
|
|
44
|
+
*
|
|
45
|
+
* - a **cycle** — the workflow is already in progress on this chain, so it can
|
|
46
|
+
* never terminate. Checked *first* deliberately: a cycle would eventually trip
|
|
47
|
+
* the depth ceiling too, but only after exhausting the whole budget of frames
|
|
48
|
+
* and then reporting the wrong cause.
|
|
49
|
+
* - **excessive depth** — distinct workflows nested past `maxDepth`, which is
|
|
50
|
+
* legitimate nesting that simply has to be bounded.
|
|
51
|
+
*
|
|
52
|
+
* Both carry the offending `path`, since the workflow the error names is rarely
|
|
53
|
+
* the one whose authoring needs looking at.
|
|
54
|
+
* @internal
|
|
55
|
+
*/
|
|
56
|
+
class WorkflowCallStack {
|
|
57
|
+
#calls;
|
|
58
|
+
#maxDepth;
|
|
59
|
+
#entryDocumentURI;
|
|
60
|
+
constructor(options, calls = []) {
|
|
61
|
+
this.#maxDepth = options.maxDepth;
|
|
62
|
+
this.#entryDocumentURI = options.entryDocumentURI;
|
|
63
|
+
this.#calls = calls;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* The workflows in progress, outermost first — bare ids for entry-document
|
|
68
|
+
* frames, `{documentURI}#{workflowId}` for foreign ones.
|
|
69
|
+
*/
|
|
70
|
+
get path() {
|
|
71
|
+
return this.#calls.map(call => this.#display(call.workflowId, call.documentURI));
|
|
72
|
+
}
|
|
73
|
+
#display(workflowId, documentURI) {
|
|
74
|
+
return documentURI === this.#entryDocumentURI ? workflowId : `${documentURI}#${workflowId}`;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Returns the stack with the workflow pushed — identified by its bare id
|
|
79
|
+
* together with the canonical URI of the document that owns it — or throws
|
|
80
|
+
* when entering it would form a cycle or exceed the nesting ceiling.
|
|
81
|
+
*/
|
|
82
|
+
enter(workflowId, via, documentURI) {
|
|
83
|
+
const repeated = this.#calls.findIndex(call => call.workflowId === workflowId && call.documentURI === documentURI);
|
|
84
|
+
if (repeated !== -1) {
|
|
85
|
+
const path = [...this.path, this.#display(workflowId, documentURI)];
|
|
86
|
+
// name the loop for the mechanism that formed it: every edge closing the
|
|
87
|
+
// cycle being a `dependsOn` edge makes it a dependency cycle, while any
|
|
88
|
+
// sub-workflow step call in the loop makes it a call cycle. One stack
|
|
89
|
+
// detects both, so a loop crossing the two mechanisms cannot slip between
|
|
90
|
+
// them.
|
|
91
|
+
const closingEdges = [...this.#calls.slice(repeated + 1).map(call => call.via), via];
|
|
92
|
+
const reason = closingEdges.every(edge => edge === 'dependsOn') ? 'dependsOn-cycle' : 'workflow-cycle';
|
|
93
|
+
throw new _ExecutionError.default(`workflow "${workflowId}" is already in progress; its call chain forms a cycle (${path.join(' -> ')})`, {
|
|
94
|
+
workflowId,
|
|
95
|
+
reason,
|
|
96
|
+
path
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
if (this.#calls.length >= this.#maxDepth) {
|
|
100
|
+
throw new _ExecutionError.default(`workflow "${workflowId}" nests deeper than the limit of ${this.#maxDepth} workflows`, {
|
|
101
|
+
workflowId,
|
|
102
|
+
reason: 'workflow-depth',
|
|
103
|
+
path: [...this.path, this.#display(workflowId, documentURI)]
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
return new WorkflowCallStack({
|
|
107
|
+
maxDepth: this.#maxDepth,
|
|
108
|
+
entryDocumentURI: this.#entryDocumentURI
|
|
109
|
+
}, [...this.#calls, {
|
|
110
|
+
workflowId,
|
|
111
|
+
documentURI,
|
|
112
|
+
via
|
|
113
|
+
}]);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
var _default = exports.default = WorkflowCallStack;
|