@smithers-orchestrator/scheduler 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/package.json +190 -0
- package/src/ApprovalResolution.ts +7 -0
- package/src/CachePolicy.ts +8 -0
- package/src/ContinuationRequest.ts +3 -0
- package/src/ContinueAsNewTransition.ts +9 -0
- package/src/EngineDecision.ts +19 -0
- package/src/PlanNode.ts +32 -0
- package/src/RalphMeta.ts +7 -0
- package/src/RalphState.ts +4 -0
- package/src/RalphStateMap.ts +3 -0
- package/src/ReadonlyTaskStateMap.ts +3 -0
- package/src/RenderContext.ts +14 -0
- package/src/RetryPolicy.ts +6 -0
- package/src/RetryWaitMap.ts +1 -0
- package/src/RunResult.ts +15 -0
- package/src/ScheduleResult.ts +15 -0
- package/src/ScheduleSnapshot.ts +8 -0
- package/src/Scheduler.js +28 -0
- package/src/SchedulerLive.js +8 -0
- package/src/SmithersWorkflowOptions.ts +43 -0
- package/src/TaskFailure.ts +5 -0
- package/src/TaskOutput.ts +9 -0
- package/src/TaskRecord.ts +10 -0
- package/src/TaskState.ts +10 -0
- package/src/TaskStateMap.ts +3 -0
- package/src/TokenUsage.ts +9 -0
- package/src/WaitReason.ts +8 -0
- package/src/WorkflowSession.js +10 -0
- package/src/WorkflowSessionLive.js +6 -0
- package/src/WorkflowSessionOptions.ts +10 -0
- package/src/WorkflowSessionService.ts +52 -0
- package/src/buildPlanTree.js +273 -0
- package/src/buildStateKey.js +8 -0
- package/src/cloneTaskStateMap.js +10 -0
- package/src/computeRetryDelayMs.js +14 -0
- package/src/index.d.ts +437 -0
- package/src/index.js +53 -0
- package/src/isTerminalState.js +15 -0
- package/src/makeWorkflowSession.js +723 -0
- package/src/nowMs.js +6 -0
- package/src/parseStateKey.js +15 -0
- package/src/retryPolicyToSchedule.js +26 -0
- package/src/retryScheduleDelayMs.js +23 -0
- package/src/scheduleTasks.js +330 -0
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
import { SmithersError } from "@smithers-orchestrator/errors/SmithersError";
|
|
2
|
+
/** @typedef {import("./PlanNode.ts").PlanNode} PlanNode */
|
|
3
|
+
/** @typedef {import("./RalphMeta.ts").RalphMeta} RalphMeta */
|
|
4
|
+
/** @typedef {import("./RalphStateMap.ts").RalphStateMap} RalphStateMap */
|
|
5
|
+
/** @typedef {import("@smithers-orchestrator/graph").XmlNode} XmlNode */
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @param {string} prefix
|
|
9
|
+
* @param {readonly number[]} path
|
|
10
|
+
* @returns {string}
|
|
11
|
+
*/
|
|
12
|
+
function stablePathId(prefix, path) {
|
|
13
|
+
if (path.length === 0)
|
|
14
|
+
return `${prefix}:root`;
|
|
15
|
+
return `${prefix}:${path.join(".")}`;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* @param {unknown} explicitId
|
|
19
|
+
* @param {string} prefix
|
|
20
|
+
* @param {readonly number[]} path
|
|
21
|
+
* @returns {string}
|
|
22
|
+
*/
|
|
23
|
+
function resolveStableId(explicitId, prefix, path) {
|
|
24
|
+
if (typeof explicitId === "string" && explicitId.trim().length > 0) {
|
|
25
|
+
return explicitId;
|
|
26
|
+
}
|
|
27
|
+
return stablePathId(prefix, path);
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* @param {string | undefined} value
|
|
31
|
+
* @returns {boolean}
|
|
32
|
+
*/
|
|
33
|
+
function parseBool(value) {
|
|
34
|
+
if (!value)
|
|
35
|
+
return false;
|
|
36
|
+
return value === "true" || value === "1";
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* @param {string | undefined} value
|
|
40
|
+
* @param {number} fallback
|
|
41
|
+
* @returns {number}
|
|
42
|
+
*/
|
|
43
|
+
function parseNum(value, fallback) {
|
|
44
|
+
const parsed = value ? Number(value) : NaN;
|
|
45
|
+
return !Number.isNaN(parsed) ? parsed : fallback;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* @param {readonly { readonly ralphId: string; readonly iteration: number }[]} loopStack
|
|
49
|
+
* @returns {string}
|
|
50
|
+
*/
|
|
51
|
+
function buildLoopScope(loopStack) {
|
|
52
|
+
if (loopStack.length === 0)
|
|
53
|
+
return "";
|
|
54
|
+
return `@@${loopStack.map((entry) => `${entry.ralphId}=${entry.iteration}`).join(",")}`;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* @param {XmlNode | null} xml
|
|
58
|
+
* @param {RalphStateMap} [ralphState]
|
|
59
|
+
* @returns {{ readonly plan: PlanNode | null; readonly ralphs: readonly RalphMeta[]; }}
|
|
60
|
+
*/
|
|
61
|
+
export function buildPlanTree(xml, ralphState) {
|
|
62
|
+
if (!xml)
|
|
63
|
+
return { plan: null, ralphs: [] };
|
|
64
|
+
const ralphs = [];
|
|
65
|
+
const seenRalph = new Set();
|
|
66
|
+
/**
|
|
67
|
+
* @param {XmlNode} node
|
|
68
|
+
* @param {{ readonly path: readonly number[]; readonly parentIsRalph: boolean; readonly loopStack: readonly { readonly ralphId: string; readonly iteration: number }[]; }} ctx
|
|
69
|
+
* @returns {PlanNode | null}
|
|
70
|
+
*/
|
|
71
|
+
function walk(node, ctx) {
|
|
72
|
+
if (node.kind === "text")
|
|
73
|
+
return null;
|
|
74
|
+
const tag = node.tag;
|
|
75
|
+
if (ctx.parentIsRalph && tag === "smithers:ralph") {
|
|
76
|
+
throw new SmithersError("NESTED_LOOP", "Nested <Ralph> is not supported.");
|
|
77
|
+
}
|
|
78
|
+
let loopStack = ctx.loopStack;
|
|
79
|
+
let scopedRalphId;
|
|
80
|
+
if (tag === "smithers:ralph") {
|
|
81
|
+
const logicalId = resolveStableId(node.props.id, "ralph", ctx.path);
|
|
82
|
+
const scope = buildLoopScope(loopStack);
|
|
83
|
+
scopedRalphId = logicalId + scope;
|
|
84
|
+
const currentIter = ralphState?.get(scopedRalphId)?.iteration ?? 0;
|
|
85
|
+
loopStack = [...loopStack, { ralphId: logicalId, iteration: currentIter }];
|
|
86
|
+
}
|
|
87
|
+
if (tag === "smithers:saga") {
|
|
88
|
+
const id = resolveStableId(node.props.id, "saga", ctx.path);
|
|
89
|
+
const onFailure = node.props.onFailure ??
|
|
90
|
+
"compensate";
|
|
91
|
+
const actionChildren = [];
|
|
92
|
+
const compensationChildren = [];
|
|
93
|
+
let specialIndex = 0;
|
|
94
|
+
for (const child of node.children) {
|
|
95
|
+
const nextPath = child.kind === "element" ? [...ctx.path, specialIndex++] : ctx.path;
|
|
96
|
+
if (child.kind !== "element")
|
|
97
|
+
continue;
|
|
98
|
+
if (child.tag === "smithers:saga-actions") {
|
|
99
|
+
let nestedIndex = 0;
|
|
100
|
+
for (const nested of child.children) {
|
|
101
|
+
const nestedPath = nested.kind === "element" ? [...nextPath, nestedIndex++] : nextPath;
|
|
102
|
+
const built = walk(nested, {
|
|
103
|
+
path: nestedPath,
|
|
104
|
+
parentIsRalph: false,
|
|
105
|
+
loopStack,
|
|
106
|
+
});
|
|
107
|
+
if (built)
|
|
108
|
+
actionChildren.push(built);
|
|
109
|
+
}
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
if (child.tag === "smithers:saga-compensations") {
|
|
113
|
+
let nestedIndex = 0;
|
|
114
|
+
for (const nested of child.children) {
|
|
115
|
+
const nestedPath = nested.kind === "element" ? [...nextPath, nestedIndex++] : nextPath;
|
|
116
|
+
const built = walk(nested, {
|
|
117
|
+
path: nestedPath,
|
|
118
|
+
parentIsRalph: false,
|
|
119
|
+
loopStack,
|
|
120
|
+
});
|
|
121
|
+
if (built)
|
|
122
|
+
compensationChildren.push(built);
|
|
123
|
+
}
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
const built = walk(child, {
|
|
127
|
+
path: nextPath,
|
|
128
|
+
parentIsRalph: false,
|
|
129
|
+
loopStack,
|
|
130
|
+
});
|
|
131
|
+
if (built)
|
|
132
|
+
actionChildren.push(built);
|
|
133
|
+
}
|
|
134
|
+
return {
|
|
135
|
+
kind: "saga",
|
|
136
|
+
id,
|
|
137
|
+
actionChildren,
|
|
138
|
+
compensationChildren,
|
|
139
|
+
onFailure,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
if (tag === "smithers:try-catch-finally") {
|
|
143
|
+
const id = resolveStableId(node.props.id, "tcf", ctx.path);
|
|
144
|
+
const tryChildren = [];
|
|
145
|
+
const catchChildren = [];
|
|
146
|
+
const finallyChildren = [];
|
|
147
|
+
let specialIndex = 0;
|
|
148
|
+
for (const child of node.children) {
|
|
149
|
+
const nextPath = child.kind === "element" ? [...ctx.path, specialIndex++] : ctx.path;
|
|
150
|
+
if (child.kind !== "element")
|
|
151
|
+
continue;
|
|
152
|
+
const target = child.tag === "smithers:tcf-catch"
|
|
153
|
+
? catchChildren
|
|
154
|
+
: child.tag === "smithers:tcf-finally"
|
|
155
|
+
? finallyChildren
|
|
156
|
+
: tryChildren;
|
|
157
|
+
if (child.tag === "smithers:tcf-try" ||
|
|
158
|
+
child.tag === "smithers:tcf-catch" ||
|
|
159
|
+
child.tag === "smithers:tcf-finally") {
|
|
160
|
+
let nestedIndex = 0;
|
|
161
|
+
for (const nested of child.children) {
|
|
162
|
+
const nestedPath = nested.kind === "element" ? [...nextPath, nestedIndex++] : nextPath;
|
|
163
|
+
const built = walk(nested, {
|
|
164
|
+
path: nestedPath,
|
|
165
|
+
parentIsRalph: false,
|
|
166
|
+
loopStack,
|
|
167
|
+
});
|
|
168
|
+
if (built)
|
|
169
|
+
target.push(built);
|
|
170
|
+
}
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
const built = walk(child, {
|
|
174
|
+
path: nextPath,
|
|
175
|
+
parentIsRalph: false,
|
|
176
|
+
loopStack,
|
|
177
|
+
});
|
|
178
|
+
if (built)
|
|
179
|
+
tryChildren.push(built);
|
|
180
|
+
}
|
|
181
|
+
return {
|
|
182
|
+
kind: "try-catch-finally",
|
|
183
|
+
id,
|
|
184
|
+
tryChildren,
|
|
185
|
+
catchChildren,
|
|
186
|
+
finallyChildren,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
const children = [];
|
|
190
|
+
let elementIndex = 0;
|
|
191
|
+
const isRalph = tag === "smithers:ralph";
|
|
192
|
+
for (const child of node.children) {
|
|
193
|
+
const nextPath = child.kind === "element" ? [...ctx.path, elementIndex++] : ctx.path;
|
|
194
|
+
const built = walk(child, {
|
|
195
|
+
path: nextPath,
|
|
196
|
+
parentIsRalph: isRalph,
|
|
197
|
+
loopStack,
|
|
198
|
+
});
|
|
199
|
+
if (built)
|
|
200
|
+
children.push(built);
|
|
201
|
+
}
|
|
202
|
+
if (tag === "smithers:task") {
|
|
203
|
+
const logicalId = node.props.id;
|
|
204
|
+
if (!logicalId)
|
|
205
|
+
return null;
|
|
206
|
+
const ancestorScope = loopStack.length > 1 ? buildLoopScope(loopStack.slice(0, -1)) : "";
|
|
207
|
+
return { kind: "task", nodeId: logicalId + ancestorScope };
|
|
208
|
+
}
|
|
209
|
+
if (tag === "smithers:workflow" || tag === "smithers:sequence") {
|
|
210
|
+
return { kind: "sequence", children };
|
|
211
|
+
}
|
|
212
|
+
if (tag === "smithers:parallel" || tag === "smithers:merge-queue") {
|
|
213
|
+
return { kind: "parallel", children };
|
|
214
|
+
}
|
|
215
|
+
if (tag === "smithers:worktree") {
|
|
216
|
+
return { kind: "group", children };
|
|
217
|
+
}
|
|
218
|
+
if (tag === "smithers:subflow") {
|
|
219
|
+
const mode = node.props.mode ?? "childRun";
|
|
220
|
+
if (mode === "inline") {
|
|
221
|
+
return { kind: "sequence", children };
|
|
222
|
+
}
|
|
223
|
+
const logicalId = node.props.id;
|
|
224
|
+
if (!logicalId)
|
|
225
|
+
return null;
|
|
226
|
+
const ancestorScope = loopStack.length > 1 ? buildLoopScope(loopStack.slice(0, -1)) : "";
|
|
227
|
+
return { kind: "task", nodeId: logicalId + ancestorScope };
|
|
228
|
+
}
|
|
229
|
+
if (tag === "smithers:sandbox" ||
|
|
230
|
+
tag === "smithers:wait-for-event" ||
|
|
231
|
+
tag === "smithers:timer") {
|
|
232
|
+
const logicalId = node.props.id;
|
|
233
|
+
if (!logicalId)
|
|
234
|
+
return null;
|
|
235
|
+
const ancestorScope = loopStack.length > 1 ? buildLoopScope(loopStack.slice(0, -1)) : "";
|
|
236
|
+
return { kind: "task", nodeId: logicalId + ancestorScope };
|
|
237
|
+
}
|
|
238
|
+
if (tag === "smithers:continue-as-new") {
|
|
239
|
+
return { kind: "continue-as-new", stateJson: node.props.stateJson };
|
|
240
|
+
}
|
|
241
|
+
if (tag === "smithers:ralph") {
|
|
242
|
+
const id = scopedRalphId;
|
|
243
|
+
if (seenRalph.has(id)) {
|
|
244
|
+
throw new SmithersError("DUPLICATE_ID", `Duplicate Ralph id detected: ${id}`, { kind: "ralph", id });
|
|
245
|
+
}
|
|
246
|
+
seenRalph.add(id);
|
|
247
|
+
const parsedContinueAsNewEvery = Math.floor(parseNum(node.props.continueAsNewEvery, 0));
|
|
248
|
+
const continueAsNewEvery = Number.isFinite(parsedContinueAsNewEvery) && parsedContinueAsNewEvery > 0
|
|
249
|
+
? parsedContinueAsNewEvery
|
|
250
|
+
: undefined;
|
|
251
|
+
const meta = {
|
|
252
|
+
id,
|
|
253
|
+
until: parseBool(node.props.until),
|
|
254
|
+
maxIterations: parseNum(node.props.maxIterations, 5),
|
|
255
|
+
onMaxReached: node.props.onMaxReached ?? "return-last",
|
|
256
|
+
continueAsNewEvery,
|
|
257
|
+
};
|
|
258
|
+
ralphs.push(meta);
|
|
259
|
+
return {
|
|
260
|
+
kind: "ralph",
|
|
261
|
+
id,
|
|
262
|
+
children,
|
|
263
|
+
until: meta.until,
|
|
264
|
+
maxIterations: meta.maxIterations,
|
|
265
|
+
onMaxReached: meta.onMaxReached,
|
|
266
|
+
continueAsNewEvery,
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
return { kind: "group", children };
|
|
270
|
+
}
|
|
271
|
+
const plan = walk(xml, { path: [], parentIsRalph: false, loopStack: [] });
|
|
272
|
+
return { plan, ralphs };
|
|
273
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
|
|
2
|
+
/** @typedef {import("./ReadonlyTaskStateMap.ts").ReadonlyTaskStateMap} ReadonlyTaskStateMap */
|
|
3
|
+
/** @typedef {import("./TaskStateMap.ts").TaskStateMap} TaskStateMap */
|
|
4
|
+
/**
|
|
5
|
+
* @param {ReadonlyTaskStateMap} states
|
|
6
|
+
* @returns {TaskStateMap}
|
|
7
|
+
*/
|
|
8
|
+
export function cloneTaskStateMap(states) {
|
|
9
|
+
return new Map(states);
|
|
10
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { retryPolicyToSchedule } from "./retryPolicyToSchedule.js";
|
|
2
|
+
import { retryScheduleDelayMs } from "./retryScheduleDelayMs.js";
|
|
3
|
+
/** @typedef {import("./RetryPolicy.ts").RetryPolicy} RetryPolicy */
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @param {RetryPolicy | undefined} policy
|
|
7
|
+
* @param {number} attempt
|
|
8
|
+
* @returns {number}
|
|
9
|
+
*/
|
|
10
|
+
export function computeRetryDelayMs(policy, attempt) {
|
|
11
|
+
if (!policy)
|
|
12
|
+
return 0;
|
|
13
|
+
return retryScheduleDelayMs(retryPolicyToSchedule(policy), attempt);
|
|
14
|
+
}
|