@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.
Files changed (45) hide show
  1. package/LICENSE +21 -0
  2. package/package.json +190 -0
  3. package/src/ApprovalResolution.ts +7 -0
  4. package/src/CachePolicy.ts +8 -0
  5. package/src/ContinuationRequest.ts +3 -0
  6. package/src/ContinueAsNewTransition.ts +9 -0
  7. package/src/EngineDecision.ts +19 -0
  8. package/src/PlanNode.ts +32 -0
  9. package/src/RalphMeta.ts +7 -0
  10. package/src/RalphState.ts +4 -0
  11. package/src/RalphStateMap.ts +3 -0
  12. package/src/ReadonlyTaskStateMap.ts +3 -0
  13. package/src/RenderContext.ts +14 -0
  14. package/src/RetryPolicy.ts +6 -0
  15. package/src/RetryWaitMap.ts +1 -0
  16. package/src/RunResult.ts +15 -0
  17. package/src/ScheduleResult.ts +15 -0
  18. package/src/ScheduleSnapshot.ts +8 -0
  19. package/src/Scheduler.js +28 -0
  20. package/src/SchedulerLive.js +8 -0
  21. package/src/SmithersWorkflowOptions.ts +43 -0
  22. package/src/TaskFailure.ts +5 -0
  23. package/src/TaskOutput.ts +9 -0
  24. package/src/TaskRecord.ts +10 -0
  25. package/src/TaskState.ts +10 -0
  26. package/src/TaskStateMap.ts +3 -0
  27. package/src/TokenUsage.ts +9 -0
  28. package/src/WaitReason.ts +8 -0
  29. package/src/WorkflowSession.js +10 -0
  30. package/src/WorkflowSessionLive.js +6 -0
  31. package/src/WorkflowSessionOptions.ts +10 -0
  32. package/src/WorkflowSessionService.ts +52 -0
  33. package/src/buildPlanTree.js +273 -0
  34. package/src/buildStateKey.js +8 -0
  35. package/src/cloneTaskStateMap.js +10 -0
  36. package/src/computeRetryDelayMs.js +14 -0
  37. package/src/index.d.ts +437 -0
  38. package/src/index.js +53 -0
  39. package/src/isTerminalState.js +15 -0
  40. package/src/makeWorkflowSession.js +723 -0
  41. package/src/nowMs.js +6 -0
  42. package/src/parseStateKey.js +15 -0
  43. package/src/retryPolicyToSchedule.js +26 -0
  44. package/src/retryScheduleDelayMs.js +23 -0
  45. 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,8 @@
1
+ /**
2
+ * @param {string} nodeId
3
+ * @param {number} iteration
4
+ * @returns {string}
5
+ */
6
+ export function buildStateKey(nodeId, iteration) {
7
+ return `${nodeId}::${iteration}`;
8
+ }
@@ -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
+ }