octaflow 0.14.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/README.md +288 -0
- package/dist/ai/index.d.ts +397 -0
- package/dist/ai/index.js +466 -0
- package/dist/core-ERYGlYsS.js +574 -0
- package/dist/dispatcher-BydjyrgU.d.ts +26 -0
- package/dist/dispatcher-pgboss/index.d.ts +170 -0
- package/dist/dispatcher-pgboss/index.js +301 -0
- package/dist/hooks-CDRFQZbT.d.ts +56 -0
- package/dist/index-D_fOYfmf.d.ts +580 -0
- package/dist/index.d.ts +137 -0
- package/dist/index.js +1426 -0
- package/dist/logger-BCus3AOa.d.ts +10 -0
- package/dist/observability-BsrvbhDr.d.ts +249 -0
- package/dist/store-pg/index.d.ts +195 -0
- package/dist/store-pg/index.js +671 -0
- package/package.json +116 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1426 @@
|
|
|
1
|
+
import { S as ok, _ as markRetryable, a as WORKFLOW_STATUS_SCHEMA, b as noopLogger, c as toPublicWorkflow, d as defineSleepStep, f as defineStep, g as isRetryableError, h as explicitRetryability, i as STEP_DISPLAY_STATUS_SCHEMA, l as buildWorkflow, m as defineWaitStep, n as PUBLIC_WORKFLOW_STEP_SCHEMA, o as toDisplayStepStatus, p as defineSubWorkflowStep, r as STEP_DISPLAY_STATUS, s as toPublicStep, t as PUBLIC_WORKFLOW_SCHEMA, u as defineMapStep, v as nonRetryableError, x as err, y as retryableError } from "./core-ERYGlYsS.js";
|
|
2
|
+
//#region src/core/gate.ts
|
|
3
|
+
/**
|
|
4
|
+
* A per-process {@link StepGate}: per-stepType concurrency caps (held slots) plus
|
|
5
|
+
* per-stepType token-bucket rate limits (throughput). Both are optional and checked
|
|
6
|
+
* per step type; a step type with no rule is always admitted.
|
|
7
|
+
*
|
|
8
|
+
* Concurrency is reserved first, then the rate token — so a rate denial does not waste
|
|
9
|
+
* a concurrency slot. Good for single-worker deployments; swap in a shared-store gate
|
|
10
|
+
* for multi-worker concurrency control.
|
|
11
|
+
*/
|
|
12
|
+
function createInMemoryStepGate(config = {}) {
|
|
13
|
+
const now = config.now ?? (() => Date.now());
|
|
14
|
+
const concurrencyRetry = Math.max(1, config.concurrencyRetrySeconds ?? 1);
|
|
15
|
+
const running = /* @__PURE__ */ new Map();
|
|
16
|
+
const buckets = /* @__PURE__ */ new Map();
|
|
17
|
+
function rateCheck(stepType) {
|
|
18
|
+
const rule = config.rateLimit?.[stepType];
|
|
19
|
+
if (!rule) return { ok: true };
|
|
20
|
+
const capacity = rule.burst ?? Math.max(1, Math.ceil(rule.perSecond));
|
|
21
|
+
const tNow = now();
|
|
22
|
+
let b = buckets.get(stepType);
|
|
23
|
+
if (!b) {
|
|
24
|
+
b = {
|
|
25
|
+
tokens: capacity,
|
|
26
|
+
updatedMs: tNow
|
|
27
|
+
};
|
|
28
|
+
buckets.set(stepType, b);
|
|
29
|
+
}
|
|
30
|
+
const elapsedSec = Math.max(0, (tNow - b.updatedMs) / 1e3);
|
|
31
|
+
b.tokens = Math.min(capacity, b.tokens + elapsedSec * rule.perSecond);
|
|
32
|
+
b.updatedMs = tNow;
|
|
33
|
+
if (b.tokens >= 1) {
|
|
34
|
+
b.tokens -= 1;
|
|
35
|
+
return { ok: true };
|
|
36
|
+
}
|
|
37
|
+
const needed = 1 - b.tokens;
|
|
38
|
+
return {
|
|
39
|
+
ok: false,
|
|
40
|
+
retryAfterSeconds: Math.max(1, Math.ceil(needed / rule.perSecond))
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
return { async acquire(req) {
|
|
44
|
+
let release = () => {};
|
|
45
|
+
const cRule = config.concurrency?.[req.stepType];
|
|
46
|
+
if (cRule) {
|
|
47
|
+
const current = running.get(req.stepType) ?? 0;
|
|
48
|
+
if (current >= cRule.maxConcurrent) return {
|
|
49
|
+
admitted: false,
|
|
50
|
+
retryAfterSeconds: concurrencyRetry
|
|
51
|
+
};
|
|
52
|
+
running.set(req.stepType, current + 1);
|
|
53
|
+
let released = false;
|
|
54
|
+
release = () => {
|
|
55
|
+
if (released) return;
|
|
56
|
+
released = true;
|
|
57
|
+
running.set(req.stepType, Math.max(0, (running.get(req.stepType) ?? 1) - 1));
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
const rate = rateCheck(req.stepType);
|
|
61
|
+
if (!rate.ok) {
|
|
62
|
+
release();
|
|
63
|
+
return {
|
|
64
|
+
admitted: false,
|
|
65
|
+
retryAfterSeconds: rate.retryAfterSeconds
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
return {
|
|
69
|
+
admitted: true,
|
|
70
|
+
release
|
|
71
|
+
};
|
|
72
|
+
} };
|
|
73
|
+
}
|
|
74
|
+
//#endregion
|
|
75
|
+
//#region src/core/observability.ts
|
|
76
|
+
const noopObserver = { record() {} };
|
|
77
|
+
function createRecordingObserver() {
|
|
78
|
+
const events = [];
|
|
79
|
+
return {
|
|
80
|
+
events,
|
|
81
|
+
record(event) {
|
|
82
|
+
events.push(event);
|
|
83
|
+
},
|
|
84
|
+
clear() {
|
|
85
|
+
events.length = 0;
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
const noopSpan = {
|
|
90
|
+
setAttribute() {},
|
|
91
|
+
recordError() {},
|
|
92
|
+
end() {}
|
|
93
|
+
};
|
|
94
|
+
const noopTracer = { startSpan: () => noopSpan };
|
|
95
|
+
function createRecordingTracer() {
|
|
96
|
+
const spans = [];
|
|
97
|
+
return {
|
|
98
|
+
spans,
|
|
99
|
+
startSpan(name, attributes = {}) {
|
|
100
|
+
const rec = {
|
|
101
|
+
name,
|
|
102
|
+
attributes: { ...attributes },
|
|
103
|
+
ended: false
|
|
104
|
+
};
|
|
105
|
+
spans.push(rec);
|
|
106
|
+
return {
|
|
107
|
+
setAttribute(key, value) {
|
|
108
|
+
rec.attributes[key] = value;
|
|
109
|
+
},
|
|
110
|
+
recordError(error) {
|
|
111
|
+
rec.error = error.message;
|
|
112
|
+
},
|
|
113
|
+
end() {
|
|
114
|
+
rec.ended = true;
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
//#endregion
|
|
121
|
+
//#region src/core/registry.ts
|
|
122
|
+
/**
|
|
123
|
+
* Creates an in-memory step handler registry.
|
|
124
|
+
* Populate it at application startup (e.g. by registering every workflow's steps).
|
|
125
|
+
* Each registration carries the handler plus its optional retry/timeout policy.
|
|
126
|
+
*/
|
|
127
|
+
function createStepHandlerRegistry() {
|
|
128
|
+
const entries = /* @__PURE__ */ new Map();
|
|
129
|
+
return {
|
|
130
|
+
register(type, handler, options) {
|
|
131
|
+
entries.set(type, {
|
|
132
|
+
handler,
|
|
133
|
+
retry: options?.retry,
|
|
134
|
+
timeoutMs: options?.timeoutMs,
|
|
135
|
+
delayMs: options?.delayMs,
|
|
136
|
+
waitForEvent: options?.waitForEvent,
|
|
137
|
+
map: options?.map,
|
|
138
|
+
childType: options?.childType,
|
|
139
|
+
subWorkflowDefinition: options?.subWorkflowDefinition,
|
|
140
|
+
compensate: options?.compensate
|
|
141
|
+
});
|
|
142
|
+
},
|
|
143
|
+
get(type) {
|
|
144
|
+
return entries.get(type)?.handler;
|
|
145
|
+
},
|
|
146
|
+
getRegistration(type) {
|
|
147
|
+
return entries.get(type);
|
|
148
|
+
},
|
|
149
|
+
has(type) {
|
|
150
|
+
return entries.has(type);
|
|
151
|
+
},
|
|
152
|
+
types() {
|
|
153
|
+
return Array.from(entries.keys());
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
//#endregion
|
|
158
|
+
//#region src/core/engine.ts
|
|
159
|
+
/**
|
|
160
|
+
* Run a handler under an optional wall-clock timeout. On expiry the abort signal
|
|
161
|
+
* is fired (cooperative handlers stop) and a retryable timeout error is returned —
|
|
162
|
+
* the engine then applies the step's retry policy.
|
|
163
|
+
*/
|
|
164
|
+
async function runWithTimeout(handler, ctx, timeoutMs, abort) {
|
|
165
|
+
if (!timeoutMs || timeoutMs <= 0) return handler(ctx);
|
|
166
|
+
let timer;
|
|
167
|
+
const timeout = new Promise((resolve) => {
|
|
168
|
+
timer = setTimeout(() => {
|
|
169
|
+
abort.abort();
|
|
170
|
+
resolve({
|
|
171
|
+
ok: false,
|
|
172
|
+
error: {
|
|
173
|
+
key: "step_error",
|
|
174
|
+
message: `Step timed out after ${timeoutMs}ms`,
|
|
175
|
+
retryable: true,
|
|
176
|
+
retryableFrom: "explicit"
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
}, timeoutMs);
|
|
180
|
+
});
|
|
181
|
+
try {
|
|
182
|
+
return await Promise.race([handler(ctx), timeout]);
|
|
183
|
+
} finally {
|
|
184
|
+
if (timer) clearTimeout(timer);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
/** Backoff delay (in whole seconds) before the next attempt, given the just-failed attempt number. */
|
|
188
|
+
function backoffDelaySeconds(retry, failedAttemptNo) {
|
|
189
|
+
const initialMs = retry?.initialDelayMs ?? 1e3;
|
|
190
|
+
const maxMs = retry?.maxDelayMs ?? 6e4;
|
|
191
|
+
const baseMs = retry?.backoff === "exponential" ? initialMs * 2 ** (failedAttemptNo - 1) : initialMs;
|
|
192
|
+
return Math.max(0, Math.ceil(Math.min(baseMs, maxMs) / 1e3));
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Reverse topological order: a step appears before its dependencies, so saga
|
|
196
|
+
* compensation undoes effects in the opposite order they were produced. Pure / deterministic.
|
|
197
|
+
*/
|
|
198
|
+
function reverseTopologicalOrder(steps) {
|
|
199
|
+
const inSet = new Set(steps.map((s) => s.key));
|
|
200
|
+
const placed = /* @__PURE__ */ new Set();
|
|
201
|
+
const topo = [];
|
|
202
|
+
let guard = 0;
|
|
203
|
+
while (placed.size < steps.length && guard++ <= steps.length) for (const s of steps) {
|
|
204
|
+
if (placed.has(s.key)) continue;
|
|
205
|
+
if ((s.dependencies ?? []).filter((d) => inSet.has(d)).every((d) => placed.has(d))) {
|
|
206
|
+
topo.push(s);
|
|
207
|
+
placed.add(s.key);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
for (const s of steps) if (!placed.has(s.key)) topo.push(s);
|
|
211
|
+
return topo.reverse();
|
|
212
|
+
}
|
|
213
|
+
function createWorkflowEngine(deps) {
|
|
214
|
+
const { store, dispatcher, registry, partitionKey } = deps;
|
|
215
|
+
const gate = deps.gate;
|
|
216
|
+
const logger = deps.logger ?? noopLogger;
|
|
217
|
+
const hooks = deps.hooks ?? {};
|
|
218
|
+
const observer = deps.observer ?? noopObserver;
|
|
219
|
+
const tracer = deps.tracer ?? noopTracer;
|
|
220
|
+
const now = deps.now ?? (() => /* @__PURE__ */ new Date());
|
|
221
|
+
const stepExpirySeconds = deps.config?.stepExpirySeconds ?? 600;
|
|
222
|
+
const stuckStepBufferSeconds = deps.config?.stuckStepBufferSeconds ?? 300;
|
|
223
|
+
const defaultRetryable = deps.config?.defaultRetryable;
|
|
224
|
+
const nowIso = () => now().toISOString();
|
|
225
|
+
/** Emit a run-history / metrics event. Guarded — a faulty observer never breaks a run. */
|
|
226
|
+
function emit(event) {
|
|
227
|
+
try {
|
|
228
|
+
observer.record({
|
|
229
|
+
...event,
|
|
230
|
+
partitionKey,
|
|
231
|
+
at: nowIso()
|
|
232
|
+
});
|
|
233
|
+
} catch (e) {
|
|
234
|
+
logger.error("Observer threw", e instanceof Error ? e : new Error(String(e)), { workflowId: event.workflowId });
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
/** Durable start delay for a ready step, from its registered `delayMs`. */
|
|
238
|
+
function readyStepDelay(stepType) {
|
|
239
|
+
const delayMs = registry.getRegistration(stepType)?.delayMs;
|
|
240
|
+
if (!delayMs || delayMs <= 0) return void 0;
|
|
241
|
+
return { startAfterSeconds: Math.ceil(delayMs / 1e3) };
|
|
242
|
+
}
|
|
243
|
+
/** Whether a step type suspends on readiness instead of dispatching. */
|
|
244
|
+
function isWaitStep(stepType) {
|
|
245
|
+
return registry.getRegistration(stepType)?.waitForEvent === true;
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Make a newly-ready step runnable: a `waitForEvent` step suspends (status `waiting`)
|
|
249
|
+
* and awaits `resumeStep`; everything else is enqueued (with any durable delay).
|
|
250
|
+
* Returns whether the step was enqueued.
|
|
251
|
+
*/
|
|
252
|
+
async function dispatchReadyStep(workflowId, stepId, stepKey, stepType) {
|
|
253
|
+
if (isWaitStep(stepType)) {
|
|
254
|
+
await store.markStepWaiting(stepId);
|
|
255
|
+
logger.info("Step is waiting for an event", {
|
|
256
|
+
workflowId,
|
|
257
|
+
stepId,
|
|
258
|
+
stepKey
|
|
259
|
+
});
|
|
260
|
+
emit({
|
|
261
|
+
type: "step.waiting",
|
|
262
|
+
workflowId,
|
|
263
|
+
stepId,
|
|
264
|
+
stepKey,
|
|
265
|
+
stepType
|
|
266
|
+
});
|
|
267
|
+
return false;
|
|
268
|
+
}
|
|
269
|
+
const result = await dispatcher.enqueueStep({
|
|
270
|
+
workflowId,
|
|
271
|
+
stepId,
|
|
272
|
+
stepKey,
|
|
273
|
+
stepType
|
|
274
|
+
}, readyStepDelay(stepType));
|
|
275
|
+
if (!result.ok) {
|
|
276
|
+
logger.error("Failed to enqueue step", new Error(result.error.message), {
|
|
277
|
+
workflowId,
|
|
278
|
+
stepKey
|
|
279
|
+
});
|
|
280
|
+
return false;
|
|
281
|
+
}
|
|
282
|
+
logger.info("Enqueued step", {
|
|
283
|
+
workflowId,
|
|
284
|
+
stepKey
|
|
285
|
+
});
|
|
286
|
+
return true;
|
|
287
|
+
}
|
|
288
|
+
function validateDefinition(definition) {
|
|
289
|
+
const { steps } = definition;
|
|
290
|
+
if (steps.length === 0) return {
|
|
291
|
+
ok: false,
|
|
292
|
+
error: {
|
|
293
|
+
key: "invalid_workflow_definition",
|
|
294
|
+
message: "Workflow must have at least one step"
|
|
295
|
+
}
|
|
296
|
+
};
|
|
297
|
+
const keys = /* @__PURE__ */ new Set();
|
|
298
|
+
for (const step of steps) {
|
|
299
|
+
if (keys.has(step.key)) return {
|
|
300
|
+
ok: false,
|
|
301
|
+
error: {
|
|
302
|
+
key: "invalid_workflow_definition",
|
|
303
|
+
message: `Duplicate step key: ${step.key}`
|
|
304
|
+
}
|
|
305
|
+
};
|
|
306
|
+
keys.add(step.key);
|
|
307
|
+
}
|
|
308
|
+
for (const step of steps) for (const dep of step.dependencies ?? []) {
|
|
309
|
+
if (!keys.has(dep)) return {
|
|
310
|
+
ok: false,
|
|
311
|
+
error: {
|
|
312
|
+
key: "invalid_workflow_definition",
|
|
313
|
+
message: `Step '${step.key}' depends on unknown step '${dep}'`
|
|
314
|
+
}
|
|
315
|
+
};
|
|
316
|
+
if (dep === step.key) return {
|
|
317
|
+
ok: false,
|
|
318
|
+
error: {
|
|
319
|
+
key: "invalid_workflow_definition",
|
|
320
|
+
message: `Step '${step.key}' cannot depend on itself`
|
|
321
|
+
}
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
const inDegree = /* @__PURE__ */ new Map();
|
|
325
|
+
const adjacency = /* @__PURE__ */ new Map();
|
|
326
|
+
for (const step of steps) {
|
|
327
|
+
inDegree.set(step.key, (step.dependencies ?? []).length);
|
|
328
|
+
adjacency.set(step.key, []);
|
|
329
|
+
}
|
|
330
|
+
for (const step of steps) for (const dep of step.dependencies ?? []) adjacency.get(dep).push(step.key);
|
|
331
|
+
const queue = [];
|
|
332
|
+
for (const [key, deg] of inDegree) if (deg === 0) queue.push(key);
|
|
333
|
+
let visited = 0;
|
|
334
|
+
while (queue.length > 0) {
|
|
335
|
+
const current = queue.shift();
|
|
336
|
+
visited++;
|
|
337
|
+
for (const neighbor of adjacency.get(current) ?? []) {
|
|
338
|
+
const newDeg = inDegree.get(neighbor) - 1;
|
|
339
|
+
inDegree.set(neighbor, newDeg);
|
|
340
|
+
if (newDeg === 0) queue.push(neighbor);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
if (visited !== steps.length) return {
|
|
344
|
+
ok: false,
|
|
345
|
+
error: {
|
|
346
|
+
key: "invalid_workflow_definition",
|
|
347
|
+
message: "Workflow contains circular dependencies"
|
|
348
|
+
}
|
|
349
|
+
};
|
|
350
|
+
for (const step of steps) if (!registry.has(step.type)) return {
|
|
351
|
+
ok: false,
|
|
352
|
+
error: {
|
|
353
|
+
key: "step_handler_not_found",
|
|
354
|
+
message: `No handler registered for step type '${step.type}'`,
|
|
355
|
+
stepType: step.type
|
|
356
|
+
}
|
|
357
|
+
};
|
|
358
|
+
return {
|
|
359
|
+
ok: true,
|
|
360
|
+
value: void 0
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
async function startWorkflow(definition, input, options) {
|
|
364
|
+
const validation = validateDefinition(definition);
|
|
365
|
+
if (!validation.ok) return validation;
|
|
366
|
+
let metadata = options?.metadata;
|
|
367
|
+
if (hooks.onBeforeStart) {
|
|
368
|
+
const guard = await hooks.onBeforeStart({
|
|
369
|
+
definition,
|
|
370
|
+
input,
|
|
371
|
+
options
|
|
372
|
+
});
|
|
373
|
+
if (!guard.ok) return guard;
|
|
374
|
+
if (guard.value.metadata) metadata = {
|
|
375
|
+
...metadata ?? {},
|
|
376
|
+
...guard.value.metadata
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
const { steps } = definition;
|
|
380
|
+
const created = await store.createWorkflow({
|
|
381
|
+
type: definition.type,
|
|
382
|
+
input,
|
|
383
|
+
entityRef: options?.entityRef,
|
|
384
|
+
metadata,
|
|
385
|
+
idempotencyKey: options?.idempotencyKey,
|
|
386
|
+
parentWorkflowId: options?.parentWorkflowId,
|
|
387
|
+
parentStepId: options?.parentStepId,
|
|
388
|
+
startedAt: nowIso(),
|
|
389
|
+
steps: steps.map((s) => ({
|
|
390
|
+
key: s.key,
|
|
391
|
+
type: s.type,
|
|
392
|
+
dependencies: s.dependencies ?? [],
|
|
393
|
+
input: s.input ?? null
|
|
394
|
+
}))
|
|
395
|
+
});
|
|
396
|
+
const workflowId = created.workflowId;
|
|
397
|
+
if (created.alreadyExisted) {
|
|
398
|
+
logger.info("Idempotent start: returning existing workflow", {
|
|
399
|
+
workflowId,
|
|
400
|
+
idempotencyKey: options?.idempotencyKey
|
|
401
|
+
});
|
|
402
|
+
return {
|
|
403
|
+
ok: true,
|
|
404
|
+
value: {
|
|
405
|
+
workflowId,
|
|
406
|
+
totalSteps: created.steps.length,
|
|
407
|
+
enqueuedSteps: []
|
|
408
|
+
}
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
emit({
|
|
412
|
+
type: "workflow.started",
|
|
413
|
+
workflowId,
|
|
414
|
+
workflowType: definition.type
|
|
415
|
+
});
|
|
416
|
+
const readySteps = steps.filter((s) => !s.dependencies || s.dependencies.length === 0);
|
|
417
|
+
const enqueuedStepKeys = [];
|
|
418
|
+
for (const readyStep of readySteps) {
|
|
419
|
+
const dbStep = created.steps.find((s) => s.key === readyStep.key);
|
|
420
|
+
if (!dbStep) continue;
|
|
421
|
+
if (await dispatchReadyStep(workflowId, dbStep.id, readyStep.key, readyStep.type)) enqueuedStepKeys.push(readyStep.key);
|
|
422
|
+
}
|
|
423
|
+
logger.info("Workflow started", {
|
|
424
|
+
workflowId,
|
|
425
|
+
type: definition.type,
|
|
426
|
+
totalSteps: steps.length,
|
|
427
|
+
enqueuedSteps: enqueuedStepKeys
|
|
428
|
+
});
|
|
429
|
+
return {
|
|
430
|
+
ok: true,
|
|
431
|
+
value: {
|
|
432
|
+
workflowId,
|
|
433
|
+
totalSteps: steps.length,
|
|
434
|
+
enqueuedSteps: enqueuedStepKeys
|
|
435
|
+
}
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
/** Type-safe start: duck-types on `{ inputSchema, definition }`. */
|
|
439
|
+
function start(workflow, input, options) {
|
|
440
|
+
return startWorkflow(workflow.definition, workflow.inputSchema.parse(input), options);
|
|
441
|
+
}
|
|
442
|
+
async function executeStep(workflowId, stepId) {
|
|
443
|
+
const workflow = await store.getWorkflow(workflowId);
|
|
444
|
+
if (!workflow) return {
|
|
445
|
+
ok: false,
|
|
446
|
+
error: {
|
|
447
|
+
key: "workflow_not_found",
|
|
448
|
+
message: `Workflow ${workflowId} not found`
|
|
449
|
+
}
|
|
450
|
+
};
|
|
451
|
+
if (workflow.status === "cancelled" || workflow.status === "failed") {
|
|
452
|
+
logger.info("Skipping step for non-active workflow", {
|
|
453
|
+
workflowId,
|
|
454
|
+
stepId,
|
|
455
|
+
status: workflow.status
|
|
456
|
+
});
|
|
457
|
+
return {
|
|
458
|
+
ok: true,
|
|
459
|
+
value: void 0
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
const step = await store.getStep(stepId);
|
|
463
|
+
if (!step || step.workflowId !== workflowId) return {
|
|
464
|
+
ok: false,
|
|
465
|
+
error: {
|
|
466
|
+
key: "workflow_not_found",
|
|
467
|
+
message: `Step ${stepId} not found`
|
|
468
|
+
}
|
|
469
|
+
};
|
|
470
|
+
if (step.status !== "pending") {
|
|
471
|
+
logger.info("Skipping already-processed step", {
|
|
472
|
+
workflowId,
|
|
473
|
+
stepId,
|
|
474
|
+
stepKey: step.key,
|
|
475
|
+
status: step.status
|
|
476
|
+
});
|
|
477
|
+
return {
|
|
478
|
+
ok: true,
|
|
479
|
+
value: void 0
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
let releaseSlot;
|
|
483
|
+
if (gate) {
|
|
484
|
+
const decision = await gate.acquire({
|
|
485
|
+
partitionKey,
|
|
486
|
+
workflowId,
|
|
487
|
+
stepId,
|
|
488
|
+
stepKey: step.key,
|
|
489
|
+
stepType: step.type
|
|
490
|
+
});
|
|
491
|
+
if (!decision.admitted) {
|
|
492
|
+
logger.info("Step deferred by gate", {
|
|
493
|
+
workflowId,
|
|
494
|
+
stepId,
|
|
495
|
+
stepKey: step.key,
|
|
496
|
+
retryAfterSeconds: decision.retryAfterSeconds
|
|
497
|
+
});
|
|
498
|
+
const re = await dispatcher.enqueueStep({
|
|
499
|
+
workflowId,
|
|
500
|
+
stepId,
|
|
501
|
+
stepKey: step.key,
|
|
502
|
+
stepType: step.type
|
|
503
|
+
}, { startAfterSeconds: decision.retryAfterSeconds });
|
|
504
|
+
if (!re.ok) logger.error("Failed to re-enqueue gated step", new Error(re.error.message), {
|
|
505
|
+
workflowId,
|
|
506
|
+
stepId
|
|
507
|
+
});
|
|
508
|
+
return {
|
|
509
|
+
ok: true,
|
|
510
|
+
value: void 0
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
releaseSlot = decision.release;
|
|
514
|
+
}
|
|
515
|
+
const startMs = now().getTime();
|
|
516
|
+
try {
|
|
517
|
+
if (!await store.markStepRunning(stepId, nowIso())) {
|
|
518
|
+
logger.info("Lost the step claim to a concurrent worker", {
|
|
519
|
+
workflowId,
|
|
520
|
+
stepId,
|
|
521
|
+
stepKey: step.key
|
|
522
|
+
});
|
|
523
|
+
return {
|
|
524
|
+
ok: true,
|
|
525
|
+
value: void 0
|
|
526
|
+
};
|
|
527
|
+
}
|
|
528
|
+
const dependencyOutputs = {};
|
|
529
|
+
const stepDeps = step.dependencies ?? [];
|
|
530
|
+
if (stepDeps.length > 0) {
|
|
531
|
+
const allSteps = await store.listSteps(workflowId);
|
|
532
|
+
for (const depKey of stepDeps) {
|
|
533
|
+
const depStep = allSteps.find((s) => s.key === depKey);
|
|
534
|
+
if (!depStep || depStep.status !== "completed") {
|
|
535
|
+
logger.error("Step dependency not completed", /* @__PURE__ */ new Error(`Dependency '${depKey}' is ${depStep?.status ?? "missing"}`), {
|
|
536
|
+
workflowId,
|
|
537
|
+
stepId
|
|
538
|
+
});
|
|
539
|
+
return {
|
|
540
|
+
ok: false,
|
|
541
|
+
error: {
|
|
542
|
+
key: "step_error",
|
|
543
|
+
message: `Dependency '${depKey}' is not completed`
|
|
544
|
+
}
|
|
545
|
+
};
|
|
546
|
+
}
|
|
547
|
+
dependencyOutputs[depKey] = depStep.output ?? void 0;
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
const registration = registry.getRegistration(step.type);
|
|
551
|
+
if (!registration) {
|
|
552
|
+
await markStepFailed(stepId, workflowId, `No handler for step type '${step.type}'`);
|
|
553
|
+
return {
|
|
554
|
+
ok: false,
|
|
555
|
+
error: {
|
|
556
|
+
key: "step_handler_not_found",
|
|
557
|
+
message: `No handler for '${step.type}'`,
|
|
558
|
+
stepType: step.type
|
|
559
|
+
}
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
const { handler, retry, timeoutMs } = registration;
|
|
563
|
+
const maxAttempts = retry?.maxAttempts ?? 1;
|
|
564
|
+
const attemptNo = step.attempts + 1;
|
|
565
|
+
emit({
|
|
566
|
+
type: "step.started",
|
|
567
|
+
workflowId,
|
|
568
|
+
workflowType: workflow.type,
|
|
569
|
+
stepId,
|
|
570
|
+
stepKey: step.key,
|
|
571
|
+
stepType: step.type,
|
|
572
|
+
attempt: attemptNo
|
|
573
|
+
});
|
|
574
|
+
const span = tracer.startSpan("flow.step", {
|
|
575
|
+
"flow.workflow_id": workflowId,
|
|
576
|
+
"flow.workflow_type": workflow.type,
|
|
577
|
+
"flow.step_key": step.key,
|
|
578
|
+
"flow.step_type": step.type,
|
|
579
|
+
"flow.attempt": attemptNo,
|
|
580
|
+
"flow.partition": partitionKey
|
|
581
|
+
});
|
|
582
|
+
try {
|
|
583
|
+
const context = hooks.buildStepContext ? await hooks.buildStepContext({
|
|
584
|
+
workflowId,
|
|
585
|
+
stepId,
|
|
586
|
+
stepKey: step.key,
|
|
587
|
+
partitionKey,
|
|
588
|
+
workflow,
|
|
589
|
+
step
|
|
590
|
+
}) : void 0;
|
|
591
|
+
const abort = new AbortController();
|
|
592
|
+
const handlerResult = await runWithTimeout(handler, {
|
|
593
|
+
workflowId,
|
|
594
|
+
stepId,
|
|
595
|
+
stepKey: step.key,
|
|
596
|
+
partitionKey,
|
|
597
|
+
workflowInput: workflow.input ?? {},
|
|
598
|
+
stepInput: step.input ?? {},
|
|
599
|
+
dependencyOutputs,
|
|
600
|
+
signal: abort.signal,
|
|
601
|
+
context
|
|
602
|
+
}, timeoutMs, abort);
|
|
603
|
+
if (handlerResult.ok) {
|
|
604
|
+
if (registration.map) {
|
|
605
|
+
emit({
|
|
606
|
+
type: "step.mapping",
|
|
607
|
+
workflowId,
|
|
608
|
+
workflowType: workflow.type,
|
|
609
|
+
stepId,
|
|
610
|
+
stepKey: step.key,
|
|
611
|
+
stepType: step.type,
|
|
612
|
+
attempt: attemptNo
|
|
613
|
+
});
|
|
614
|
+
await spawnMapChildren(workflowId, step, registration.childType ?? `${step.type}__item`, handlerResult.value);
|
|
615
|
+
return {
|
|
616
|
+
ok: true,
|
|
617
|
+
value: void 0
|
|
618
|
+
};
|
|
619
|
+
}
|
|
620
|
+
if (registration.subWorkflowDefinition) {
|
|
621
|
+
await startSubWorkflow(workflowId, step, registration.subWorkflowDefinition, handlerResult.value);
|
|
622
|
+
return {
|
|
623
|
+
ok: true,
|
|
624
|
+
value: void 0
|
|
625
|
+
};
|
|
626
|
+
}
|
|
627
|
+
if (hooks.onAfterStep) await hooks.onAfterStep({
|
|
628
|
+
workflowId,
|
|
629
|
+
stepId,
|
|
630
|
+
partitionKey,
|
|
631
|
+
workflow,
|
|
632
|
+
step,
|
|
633
|
+
output: handlerResult.value,
|
|
634
|
+
context
|
|
635
|
+
});
|
|
636
|
+
emit({
|
|
637
|
+
type: "step.completed",
|
|
638
|
+
workflowId,
|
|
639
|
+
workflowType: workflow.type,
|
|
640
|
+
stepId,
|
|
641
|
+
stepKey: step.key,
|
|
642
|
+
stepType: step.type,
|
|
643
|
+
attempt: attemptNo,
|
|
644
|
+
durationMs: now().getTime() - startMs
|
|
645
|
+
});
|
|
646
|
+
if (step.parentStepId != null) await onChildCompleted(workflowId, step, handlerResult.value);
|
|
647
|
+
else await onStepCompleted(workflowId, stepId, handlerResult.value);
|
|
648
|
+
return {
|
|
649
|
+
ok: true,
|
|
650
|
+
value: void 0
|
|
651
|
+
};
|
|
652
|
+
}
|
|
653
|
+
if ((handlerResult.error.retryableFrom === "heuristic" && defaultRetryable !== void 0 ? defaultRetryable : handlerResult.error.retryable) && attemptNo < maxAttempts) {
|
|
654
|
+
const delaySeconds = backoffDelaySeconds(retry, attemptNo);
|
|
655
|
+
logger.info("Retrying step", {
|
|
656
|
+
workflowId,
|
|
657
|
+
stepId,
|
|
658
|
+
stepKey: step.key,
|
|
659
|
+
attempt: attemptNo,
|
|
660
|
+
maxAttempts,
|
|
661
|
+
delaySeconds,
|
|
662
|
+
reason: handlerResult.error.message
|
|
663
|
+
});
|
|
664
|
+
emit({
|
|
665
|
+
type: "step.retrying",
|
|
666
|
+
workflowId,
|
|
667
|
+
workflowType: workflow.type,
|
|
668
|
+
stepId,
|
|
669
|
+
stepKey: step.key,
|
|
670
|
+
stepType: step.type,
|
|
671
|
+
attempt: attemptNo,
|
|
672
|
+
durationMs: now().getTime() - startMs,
|
|
673
|
+
error: handlerResult.error.message
|
|
674
|
+
});
|
|
675
|
+
await store.markStepPending(stepId);
|
|
676
|
+
const re = await dispatcher.enqueueStep({
|
|
677
|
+
workflowId,
|
|
678
|
+
stepId,
|
|
679
|
+
stepKey: step.key,
|
|
680
|
+
stepType: step.type
|
|
681
|
+
}, { startAfterSeconds: delaySeconds });
|
|
682
|
+
if (!re.ok) await failStepTerminal(workflowId, step, `${handlerResult.error.message} (retry enqueue failed: ${re.error.message})`);
|
|
683
|
+
return {
|
|
684
|
+
ok: true,
|
|
685
|
+
value: void 0
|
|
686
|
+
};
|
|
687
|
+
}
|
|
688
|
+
span.recordError(new Error(handlerResult.error.message));
|
|
689
|
+
emit({
|
|
690
|
+
type: "step.failed",
|
|
691
|
+
workflowId,
|
|
692
|
+
workflowType: workflow.type,
|
|
693
|
+
stepId,
|
|
694
|
+
stepKey: step.key,
|
|
695
|
+
stepType: step.type,
|
|
696
|
+
attempt: attemptNo,
|
|
697
|
+
durationMs: now().getTime() - startMs,
|
|
698
|
+
error: handlerResult.error.message
|
|
699
|
+
});
|
|
700
|
+
await failStepTerminal(workflowId, step, handlerResult.error.message);
|
|
701
|
+
return {
|
|
702
|
+
ok: true,
|
|
703
|
+
value: void 0
|
|
704
|
+
};
|
|
705
|
+
} catch (error) {
|
|
706
|
+
const message = error instanceof Error ? error.message : "Unknown step execution error";
|
|
707
|
+
logger.error("Step execution threw", error instanceof Error ? error : new Error(message), {
|
|
708
|
+
workflowId,
|
|
709
|
+
stepId
|
|
710
|
+
});
|
|
711
|
+
span.recordError(error instanceof Error ? error : new Error(message));
|
|
712
|
+
emit({
|
|
713
|
+
type: "step.failed",
|
|
714
|
+
workflowId,
|
|
715
|
+
workflowType: workflow.type,
|
|
716
|
+
stepId,
|
|
717
|
+
stepKey: step.key,
|
|
718
|
+
stepType: step.type,
|
|
719
|
+
attempt: attemptNo,
|
|
720
|
+
durationMs: now().getTime() - startMs,
|
|
721
|
+
error: message
|
|
722
|
+
});
|
|
723
|
+
await failStepTerminal(workflowId, step, message);
|
|
724
|
+
throw error;
|
|
725
|
+
} finally {
|
|
726
|
+
span.end();
|
|
727
|
+
}
|
|
728
|
+
} finally {
|
|
729
|
+
if (releaseSlot) await releaseSlot();
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
/**
|
|
733
|
+
* Deliver an external event to a `waiting` step: completes it with `payload`
|
|
734
|
+
* as its output and advances the DAG. Idempotent — resuming a non-waiting step (e.g. a
|
|
735
|
+
* re-delivered event) is a logged no-op. The host correlates the event to `stepKey`.
|
|
736
|
+
*/
|
|
737
|
+
async function resumeStep(workflowId, stepKey, payload = {}) {
|
|
738
|
+
const workflow = await store.getWorkflow(workflowId);
|
|
739
|
+
if (!workflow) return {
|
|
740
|
+
ok: false,
|
|
741
|
+
error: {
|
|
742
|
+
key: "workflow_not_found",
|
|
743
|
+
message: `Workflow ${workflowId} not found`
|
|
744
|
+
}
|
|
745
|
+
};
|
|
746
|
+
if (workflow.status !== "running" && workflow.status !== "pending") {
|
|
747
|
+
logger.info("Ignoring resume for non-active workflow", {
|
|
748
|
+
workflowId,
|
|
749
|
+
stepKey,
|
|
750
|
+
status: workflow.status
|
|
751
|
+
});
|
|
752
|
+
return {
|
|
753
|
+
ok: true,
|
|
754
|
+
value: void 0
|
|
755
|
+
};
|
|
756
|
+
}
|
|
757
|
+
const step = (await store.listSteps(workflowId)).find((s) => s.key === stepKey);
|
|
758
|
+
if (!step) return {
|
|
759
|
+
ok: false,
|
|
760
|
+
error: {
|
|
761
|
+
key: "workflow_not_found",
|
|
762
|
+
message: `Step '${stepKey}' not found in workflow ${workflowId}`
|
|
763
|
+
}
|
|
764
|
+
};
|
|
765
|
+
if (step.status !== "waiting") {
|
|
766
|
+
logger.info("Ignoring resume for non-waiting step", {
|
|
767
|
+
workflowId,
|
|
768
|
+
stepKey,
|
|
769
|
+
status: step.status
|
|
770
|
+
});
|
|
771
|
+
return {
|
|
772
|
+
ok: true,
|
|
773
|
+
value: void 0
|
|
774
|
+
};
|
|
775
|
+
}
|
|
776
|
+
emit({
|
|
777
|
+
type: "step.resumed",
|
|
778
|
+
workflowId,
|
|
779
|
+
stepId: step.id,
|
|
780
|
+
stepKey,
|
|
781
|
+
stepType: step.type
|
|
782
|
+
});
|
|
783
|
+
await onStepCompleted(workflowId, step.id, payload);
|
|
784
|
+
logger.info("Resumed waiting step", {
|
|
785
|
+
workflowId,
|
|
786
|
+
stepKey
|
|
787
|
+
});
|
|
788
|
+
return {
|
|
789
|
+
ok: true,
|
|
790
|
+
value: void 0
|
|
791
|
+
};
|
|
792
|
+
}
|
|
793
|
+
async function onStepCompleted(workflowId, stepId, output) {
|
|
794
|
+
const completedAt = nowIso();
|
|
795
|
+
await store.completeStep({
|
|
796
|
+
workflowId,
|
|
797
|
+
stepId,
|
|
798
|
+
output,
|
|
799
|
+
completedAt
|
|
800
|
+
});
|
|
801
|
+
const keyedSteps = (await store.listSteps(workflowId)).filter((s) => s.parentStepId == null);
|
|
802
|
+
const completedKeys = new Set(keyedSteps.filter((s) => s.status === "completed").map((s) => s.key));
|
|
803
|
+
const newlyReady = keyedSteps.filter((s) => {
|
|
804
|
+
if (s.status !== "pending") return false;
|
|
805
|
+
return (s.dependencies ?? []).every((dep) => completedKeys.has(dep));
|
|
806
|
+
});
|
|
807
|
+
for (const readyStep of newlyReady) await dispatchReadyStep(workflowId, readyStep.id, readyStep.key, readyStep.type);
|
|
808
|
+
if (keyedSteps.some((s) => s.id !== stepId && s.status === "failed")) {
|
|
809
|
+
await checkWorkflowFailure(workflowId);
|
|
810
|
+
return;
|
|
811
|
+
}
|
|
812
|
+
if (keyedSteps.every((s) => s.id === stepId || s.status === "completed" || s.status === "skipped") && newlyReady.length === 0) {
|
|
813
|
+
const aggregatedOutput = {};
|
|
814
|
+
for (const s of keyedSteps) if (s.status === "completed" || s.id === stepId) aggregatedOutput[s.key] = s.id === stepId ? output : s.output ?? null;
|
|
815
|
+
await store.finishWorkflow({
|
|
816
|
+
workflowId,
|
|
817
|
+
status: "completed",
|
|
818
|
+
output: aggregatedOutput,
|
|
819
|
+
completedAt
|
|
820
|
+
});
|
|
821
|
+
logger.info("Workflow completed", { workflowId });
|
|
822
|
+
emit({
|
|
823
|
+
type: "workflow.completed",
|
|
824
|
+
workflowId
|
|
825
|
+
});
|
|
826
|
+
if (hooks.onWorkflowCompleted) {
|
|
827
|
+
const finalWorkflow = await store.getWorkflow(workflowId);
|
|
828
|
+
if (finalWorkflow) Promise.resolve(hooks.onWorkflowCompleted({
|
|
829
|
+
workflowId,
|
|
830
|
+
partitionKey,
|
|
831
|
+
workflow: finalWorkflow
|
|
832
|
+
})).catch((e) => logger.error("onWorkflowCompleted hook failed", e instanceof Error ? e : new Error(String(e)), { workflowId }));
|
|
833
|
+
}
|
|
834
|
+
await bridgeSubWorkflow(workflowId);
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
/** A map parent produced its item list — spawn one child per item and suspend it. */
|
|
838
|
+
async function spawnMapChildren(workflowId, parentStep, childType, itemsOutput) {
|
|
839
|
+
const items = Array.isArray(itemsOutput.items) ? itemsOutput.items : [];
|
|
840
|
+
if (items.length === 0) {
|
|
841
|
+
await onStepCompleted(workflowId, parentStep.id, { items: [] });
|
|
842
|
+
return;
|
|
843
|
+
}
|
|
844
|
+
const children = items.map((item, index) => ({
|
|
845
|
+
key: `${parentStep.key}#${index}`,
|
|
846
|
+
type: childType,
|
|
847
|
+
input: {
|
|
848
|
+
item,
|
|
849
|
+
index
|
|
850
|
+
}
|
|
851
|
+
}));
|
|
852
|
+
const created = await store.addChildSteps(workflowId, parentStep.id, children);
|
|
853
|
+
await store.markStepMapping(parentStep.id);
|
|
854
|
+
for (const child of created) {
|
|
855
|
+
const result = await dispatcher.enqueueStep({
|
|
856
|
+
workflowId,
|
|
857
|
+
stepId: child.id,
|
|
858
|
+
stepKey: child.key,
|
|
859
|
+
stepType: child.type
|
|
860
|
+
}, readyStepDelay(child.type));
|
|
861
|
+
if (!result.ok) logger.error("Failed to enqueue map child", new Error(result.error.message), {
|
|
862
|
+
workflowId,
|
|
863
|
+
stepKey: child.key
|
|
864
|
+
});
|
|
865
|
+
}
|
|
866
|
+
logger.info("Map step spawned children", {
|
|
867
|
+
workflowId,
|
|
868
|
+
parentKey: parentStep.key,
|
|
869
|
+
count: created.length
|
|
870
|
+
});
|
|
871
|
+
}
|
|
872
|
+
/** A map child completed — complete the parent (with aggregated outputs) once all siblings finish. */
|
|
873
|
+
async function onChildCompleted(workflowId, childStep, output) {
|
|
874
|
+
await store.completeStep({
|
|
875
|
+
workflowId,
|
|
876
|
+
stepId: childStep.id,
|
|
877
|
+
output,
|
|
878
|
+
completedAt: nowIso()
|
|
879
|
+
});
|
|
880
|
+
const parentId = childStep.parentStepId;
|
|
881
|
+
const children = await store.listChildSteps(parentId);
|
|
882
|
+
if (children.some((c) => c.status === "failed")) {
|
|
883
|
+
await checkWorkflowFailure(workflowId);
|
|
884
|
+
return;
|
|
885
|
+
}
|
|
886
|
+
if (children.every((c) => c.status === "completed")) await onStepCompleted(workflowId, parentId, { items: children.map((c) => c.output ?? null) });
|
|
887
|
+
}
|
|
888
|
+
/** Fail a map parent and skip its still-pending children (a sibling item errored). */
|
|
889
|
+
async function failMapParent(workflowId, parentStepId, message) {
|
|
890
|
+
const parent = await store.getStep(parentStepId);
|
|
891
|
+
if (!parent || parent.status === "failed" || parent.status === "completed") return;
|
|
892
|
+
await markStepFailed(parentStepId, workflowId, message);
|
|
893
|
+
for (const child of await store.listChildSteps(parentStepId)) if (child.status === "pending") await store.skipStep(child.id, "Skipped: sibling map item failed");
|
|
894
|
+
}
|
|
895
|
+
/** Terminal failure of a step — fails the parent map too if the step is a map child. */
|
|
896
|
+
async function failStepTerminal(workflowId, step, message) {
|
|
897
|
+
await markStepFailed(step.id, workflowId, message);
|
|
898
|
+
if (step.parentStepId != null) await failMapParent(workflowId, step.parentStepId, `Map item failed: ${message}`);
|
|
899
|
+
await checkWorkflowFailure(workflowId);
|
|
900
|
+
}
|
|
901
|
+
/** A sub-workflow step ran — start the child workflow and suspend the parent step. */
|
|
902
|
+
async function startSubWorkflow(workflowId, parentStep, childDefinition, childInput) {
|
|
903
|
+
await store.markStepWaiting(parentStep.id);
|
|
904
|
+
emit({
|
|
905
|
+
type: "step.waiting",
|
|
906
|
+
workflowId,
|
|
907
|
+
stepId: parentStep.id,
|
|
908
|
+
stepKey: parentStep.key,
|
|
909
|
+
stepType: parentStep.type
|
|
910
|
+
});
|
|
911
|
+
const started = await startWorkflow(childDefinition, childInput, {
|
|
912
|
+
parentWorkflowId: workflowId,
|
|
913
|
+
parentStepId: parentStep.id
|
|
914
|
+
});
|
|
915
|
+
if (!started.ok) {
|
|
916
|
+
logger.error("Failed to start sub-workflow", new Error(started.error.message), {
|
|
917
|
+
workflowId,
|
|
918
|
+
stepKey: parentStep.key
|
|
919
|
+
});
|
|
920
|
+
await failStepTerminal(workflowId, parentStep, `Sub-workflow start failed: ${started.error.message}`);
|
|
921
|
+
return;
|
|
922
|
+
}
|
|
923
|
+
logger.info("Sub-workflow started", {
|
|
924
|
+
workflowId,
|
|
925
|
+
parentKey: parentStep.key,
|
|
926
|
+
childWorkflowId: started.value.workflowId
|
|
927
|
+
});
|
|
928
|
+
}
|
|
929
|
+
/**
|
|
930
|
+
* A workflow reached a terminal state — if it's a sub-workflow child, settle its parent
|
|
931
|
+
* step: complete it with the child's output, or fail it (and cascade) on failure/cancel.
|
|
932
|
+
* Idempotent — a no-op unless the parent step is still `waiting`.
|
|
933
|
+
*/
|
|
934
|
+
async function bridgeSubWorkflow(childWorkflowId) {
|
|
935
|
+
const child = await store.getWorkflow(childWorkflowId);
|
|
936
|
+
if (!child || child.parentWorkflowId == null || child.parentStepId == null) return;
|
|
937
|
+
const parentStep = await store.getStep(child.parentStepId);
|
|
938
|
+
if (!parentStep || parentStep.status !== "waiting") return;
|
|
939
|
+
if (child.status === "completed") await onStepCompleted(child.parentWorkflowId, child.parentStepId, child.output ?? {});
|
|
940
|
+
else {
|
|
941
|
+
await markStepFailed(child.parentStepId, child.parentWorkflowId, `Sub-workflow ${child.status}${child.error ? `: ${child.error}` : ""}`);
|
|
942
|
+
await checkWorkflowFailure(child.parentWorkflowId);
|
|
943
|
+
}
|
|
944
|
+
logger.info("Bridged sub-workflow to parent step", {
|
|
945
|
+
childWorkflowId,
|
|
946
|
+
parentWorkflowId: child.parentWorkflowId,
|
|
947
|
+
status: child.status
|
|
948
|
+
});
|
|
949
|
+
}
|
|
950
|
+
/**
|
|
951
|
+
* Run each completed step's `compensate` handler in reverse dependency order to undo side
|
|
952
|
+
* effects after the workflow failed. Best-effort: one attempt per step, throws are logged and
|
|
953
|
+
* surfaced on the step (status `compensated` with the error). Steps without a handler are left.
|
|
954
|
+
*/
|
|
955
|
+
async function compensateWorkflow(workflowId) {
|
|
956
|
+
const workflow = await store.getWorkflow(workflowId);
|
|
957
|
+
if (!workflow) return;
|
|
958
|
+
const steps = await store.listSteps(workflowId);
|
|
959
|
+
const compensatable = steps.filter((s) => s.status === "completed" && registry.getRegistration(s.type)?.compensate);
|
|
960
|
+
if (compensatable.length === 0) return;
|
|
961
|
+
const byKey = new Map(steps.map((s) => [s.key, s]));
|
|
962
|
+
for (const step of reverseTopologicalOrder(compensatable)) {
|
|
963
|
+
const compensate = registry.getRegistration(step.type)?.compensate;
|
|
964
|
+
if (!compensate) continue;
|
|
965
|
+
await store.markStepCompensating(step.id);
|
|
966
|
+
emit({
|
|
967
|
+
type: "step.compensating",
|
|
968
|
+
workflowId,
|
|
969
|
+
workflowType: workflow.type,
|
|
970
|
+
stepId: step.id,
|
|
971
|
+
stepKey: step.key,
|
|
972
|
+
stepType: step.type
|
|
973
|
+
});
|
|
974
|
+
try {
|
|
975
|
+
const dependencyOutputs = {};
|
|
976
|
+
for (const depKey of step.dependencies ?? []) dependencyOutputs[depKey] = byKey.get(depKey)?.output ?? void 0;
|
|
977
|
+
const context = hooks.buildStepContext ? await hooks.buildStepContext({
|
|
978
|
+
workflowId,
|
|
979
|
+
stepId: step.id,
|
|
980
|
+
stepKey: step.key,
|
|
981
|
+
partitionKey,
|
|
982
|
+
workflow,
|
|
983
|
+
step
|
|
984
|
+
}) : void 0;
|
|
985
|
+
await compensate({
|
|
986
|
+
workflowId,
|
|
987
|
+
stepId: step.id,
|
|
988
|
+
stepKey: step.key,
|
|
989
|
+
partitionKey,
|
|
990
|
+
workflowInput: workflow.input ?? {},
|
|
991
|
+
stepInput: step.input ?? {},
|
|
992
|
+
dependencyOutputs,
|
|
993
|
+
context,
|
|
994
|
+
output: step.output ?? {}
|
|
995
|
+
});
|
|
996
|
+
await store.markStepCompensated(step.id);
|
|
997
|
+
logger.info("Compensated step", {
|
|
998
|
+
workflowId,
|
|
999
|
+
stepKey: step.key
|
|
1000
|
+
});
|
|
1001
|
+
emit({
|
|
1002
|
+
type: "step.compensated",
|
|
1003
|
+
workflowId,
|
|
1004
|
+
workflowType: workflow.type,
|
|
1005
|
+
stepId: step.id,
|
|
1006
|
+
stepKey: step.key,
|
|
1007
|
+
stepType: step.type
|
|
1008
|
+
});
|
|
1009
|
+
} catch (error) {
|
|
1010
|
+
const message = error instanceof Error ? error.message : "Unknown compensation error";
|
|
1011
|
+
logger.error("Compensation failed", error instanceof Error ? error : new Error(message), {
|
|
1012
|
+
workflowId,
|
|
1013
|
+
stepKey: step.key
|
|
1014
|
+
});
|
|
1015
|
+
await store.markStepCompensated(step.id, `Compensation failed: ${message}`);
|
|
1016
|
+
emit({
|
|
1017
|
+
type: "step.compensated",
|
|
1018
|
+
workflowId,
|
|
1019
|
+
workflowType: workflow.type,
|
|
1020
|
+
stepId: step.id,
|
|
1021
|
+
stepKey: step.key,
|
|
1022
|
+
stepType: step.type,
|
|
1023
|
+
error: message
|
|
1024
|
+
});
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
async function markStepFailed(stepId, workflowId, errorMessage) {
|
|
1029
|
+
await store.failStep({
|
|
1030
|
+
workflowId,
|
|
1031
|
+
stepId,
|
|
1032
|
+
error: errorMessage,
|
|
1033
|
+
completedAt: nowIso()
|
|
1034
|
+
});
|
|
1035
|
+
}
|
|
1036
|
+
async function checkWorkflowFailure(workflowId) {
|
|
1037
|
+
for (;;) {
|
|
1038
|
+
const steps = await store.listSteps(workflowId);
|
|
1039
|
+
const blockedKeys = new Set(steps.filter((s) => s.status === "failed" || s.status === "skipped").map((s) => s.key));
|
|
1040
|
+
const toSkip = steps.filter((s) => s.status === "pending" && (s.dependencies ?? []).some((dep) => blockedKeys.has(dep)));
|
|
1041
|
+
if (toSkip.length === 0) break;
|
|
1042
|
+
for (const s of toSkip) {
|
|
1043
|
+
await store.skipStep(s.id, "Skipped due to failed dependency");
|
|
1044
|
+
emit({
|
|
1045
|
+
type: "step.skipped",
|
|
1046
|
+
workflowId,
|
|
1047
|
+
stepId: s.id,
|
|
1048
|
+
stepKey: s.key,
|
|
1049
|
+
stepType: s.type
|
|
1050
|
+
});
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
const refreshed = await store.listSteps(workflowId);
|
|
1054
|
+
if (refreshed.every((s) => [
|
|
1055
|
+
"completed",
|
|
1056
|
+
"failed",
|
|
1057
|
+
"skipped"
|
|
1058
|
+
].includes(s.status))) {
|
|
1059
|
+
const firstFailed = refreshed.find((s) => s.status === "failed");
|
|
1060
|
+
await store.finishWorkflow({
|
|
1061
|
+
workflowId,
|
|
1062
|
+
status: "failed",
|
|
1063
|
+
error: firstFailed?.error ?? "One or more steps failed",
|
|
1064
|
+
completedAt: nowIso()
|
|
1065
|
+
});
|
|
1066
|
+
logger.info("Workflow failed", { workflowId });
|
|
1067
|
+
emit({
|
|
1068
|
+
type: "workflow.failed",
|
|
1069
|
+
workflowId,
|
|
1070
|
+
error: firstFailed?.error ?? "One or more steps failed"
|
|
1071
|
+
});
|
|
1072
|
+
await compensateWorkflow(workflowId);
|
|
1073
|
+
await bridgeSubWorkflow(workflowId);
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
async function cancelWorkflow(workflowId) {
|
|
1077
|
+
const workflow = await store.getWorkflow(workflowId);
|
|
1078
|
+
if (!workflow) return {
|
|
1079
|
+
ok: false,
|
|
1080
|
+
error: {
|
|
1081
|
+
key: "workflow_not_found",
|
|
1082
|
+
message: `Workflow ${workflowId} not found`
|
|
1083
|
+
}
|
|
1084
|
+
};
|
|
1085
|
+
if (workflow.status !== "pending" && workflow.status !== "running") return {
|
|
1086
|
+
ok: true,
|
|
1087
|
+
value: void 0
|
|
1088
|
+
};
|
|
1089
|
+
await store.skipPendingSteps(workflowId, "Workflow cancelled");
|
|
1090
|
+
await store.finishWorkflow({
|
|
1091
|
+
workflowId,
|
|
1092
|
+
status: "cancelled",
|
|
1093
|
+
completedAt: nowIso()
|
|
1094
|
+
});
|
|
1095
|
+
logger.info("Workflow cancelled", { workflowId });
|
|
1096
|
+
emit({
|
|
1097
|
+
type: "workflow.cancelled",
|
|
1098
|
+
workflowId
|
|
1099
|
+
});
|
|
1100
|
+
await bridgeSubWorkflow(workflowId);
|
|
1101
|
+
return {
|
|
1102
|
+
ok: true,
|
|
1103
|
+
value: void 0
|
|
1104
|
+
};
|
|
1105
|
+
}
|
|
1106
|
+
async function getWorkflowStatus(workflowId) {
|
|
1107
|
+
const workflow = await store.getWorkflow(workflowId);
|
|
1108
|
+
if (!workflow) return {
|
|
1109
|
+
ok: false,
|
|
1110
|
+
error: {
|
|
1111
|
+
key: "workflow_not_found",
|
|
1112
|
+
message: `Workflow ${workflowId} not found`
|
|
1113
|
+
}
|
|
1114
|
+
};
|
|
1115
|
+
const steps = await store.listSteps(workflowId);
|
|
1116
|
+
return {
|
|
1117
|
+
ok: true,
|
|
1118
|
+
value: {
|
|
1119
|
+
...workflow,
|
|
1120
|
+
steps
|
|
1121
|
+
}
|
|
1122
|
+
};
|
|
1123
|
+
}
|
|
1124
|
+
async function listWorkflows(filters) {
|
|
1125
|
+
return {
|
|
1126
|
+
ok: true,
|
|
1127
|
+
value: await store.listWorkflows({
|
|
1128
|
+
...filters,
|
|
1129
|
+
limit: filters?.limit ?? 50
|
|
1130
|
+
})
|
|
1131
|
+
};
|
|
1132
|
+
}
|
|
1133
|
+
async function handleStepExhausted(workflowId, stepId, errorMessage) {
|
|
1134
|
+
const step = await store.getStep(stepId);
|
|
1135
|
+
if (!step || step.workflowId !== workflowId || [
|
|
1136
|
+
"completed",
|
|
1137
|
+
"failed",
|
|
1138
|
+
"skipped"
|
|
1139
|
+
].includes(step.status)) return;
|
|
1140
|
+
await markStepFailed(stepId, workflowId, errorMessage);
|
|
1141
|
+
await checkWorkflowFailure(workflowId);
|
|
1142
|
+
}
|
|
1143
|
+
/**
|
|
1144
|
+
* Recover workflows whose step is stuck in `running` past the dispatcher expiry
|
|
1145
|
+
* (worker crashed between marking `running` and completing). Marks such steps
|
|
1146
|
+
* failed and cascades the workflow to `failed`.
|
|
1147
|
+
*/
|
|
1148
|
+
async function recoverStuckWorkflows() {
|
|
1149
|
+
const stuckThresholdSeconds = stepExpirySeconds + stuckStepBufferSeconds;
|
|
1150
|
+
const cutoff = (/* @__PURE__ */ new Date(now().getTime() - stuckThresholdSeconds * 1e3)).toISOString();
|
|
1151
|
+
const runningWorkflows = await store.listRunningWorkflows();
|
|
1152
|
+
if (runningWorkflows.length === 0) return {
|
|
1153
|
+
recoveredSteps: 0,
|
|
1154
|
+
recoveredWorkflows: 0
|
|
1155
|
+
};
|
|
1156
|
+
let recoveredSteps = 0;
|
|
1157
|
+
const affectedWorkflowIds = /* @__PURE__ */ new Set();
|
|
1158
|
+
for (const workflow of runningWorkflows) {
|
|
1159
|
+
const stuckSteps = await store.findStuckSteps(workflow.id, cutoff);
|
|
1160
|
+
for (const step of stuckSteps) {
|
|
1161
|
+
logger.warn("Recovering stuck workflow step", {
|
|
1162
|
+
workflowId: workflow.id,
|
|
1163
|
+
stepId: step.id,
|
|
1164
|
+
stepKey: step.key,
|
|
1165
|
+
startedAt: step.startedAt
|
|
1166
|
+
});
|
|
1167
|
+
await markStepFailed(step.id, workflow.id, `Step exceeded ${stuckThresholdSeconds}s without completion (worker likely crashed)`);
|
|
1168
|
+
recoveredSteps++;
|
|
1169
|
+
affectedWorkflowIds.add(workflow.id);
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
for (const workflowId of affectedWorkflowIds) await checkWorkflowFailure(workflowId);
|
|
1173
|
+
return {
|
|
1174
|
+
recoveredSteps,
|
|
1175
|
+
recoveredWorkflows: affectedWorkflowIds.size
|
|
1176
|
+
};
|
|
1177
|
+
}
|
|
1178
|
+
return {
|
|
1179
|
+
validateDefinition,
|
|
1180
|
+
startWorkflow,
|
|
1181
|
+
start,
|
|
1182
|
+
executeStep,
|
|
1183
|
+
resumeStep,
|
|
1184
|
+
handleStepExhausted,
|
|
1185
|
+
recoverStuckWorkflows,
|
|
1186
|
+
cancelWorkflow,
|
|
1187
|
+
getWorkflowStatus,
|
|
1188
|
+
listWorkflows
|
|
1189
|
+
};
|
|
1190
|
+
}
|
|
1191
|
+
//#endregion
|
|
1192
|
+
//#region src/core/in-memory-store.ts
|
|
1193
|
+
/**
|
|
1194
|
+
* Reference `WorkflowStore` backed by plain Maps. Intended for tests and single-
|
|
1195
|
+
* process experimentation — NOT durable. Partition-scoped at construction to
|
|
1196
|
+
* mirror real adapters (the partition value is recorded but isolation is trivial
|
|
1197
|
+
* since each instance owns its own maps).
|
|
1198
|
+
*/
|
|
1199
|
+
function createInMemoryWorkflowStore(partitionKey = "default") {
|
|
1200
|
+
const workflows = /* @__PURE__ */ new Map();
|
|
1201
|
+
const steps = /* @__PURE__ */ new Map();
|
|
1202
|
+
const byIdempotency = /* @__PURE__ */ new Map();
|
|
1203
|
+
let workflowSeq = 0;
|
|
1204
|
+
let stepSeq = 0;
|
|
1205
|
+
const clone = (v) => v == null ? v : JSON.parse(JSON.stringify(v));
|
|
1206
|
+
function stepsOf(workflowId) {
|
|
1207
|
+
return Array.from(steps.values()).filter((s) => s.workflowId === workflowId);
|
|
1208
|
+
}
|
|
1209
|
+
async function createWorkflow(params) {
|
|
1210
|
+
if (params.idempotencyKey != null) {
|
|
1211
|
+
const existingId = byIdempotency.get(params.idempotencyKey);
|
|
1212
|
+
if (existingId != null) return {
|
|
1213
|
+
workflowId: existingId,
|
|
1214
|
+
steps: stepsOf(existingId).map(clone),
|
|
1215
|
+
alreadyExisted: true
|
|
1216
|
+
};
|
|
1217
|
+
}
|
|
1218
|
+
const id = ++workflowSeq;
|
|
1219
|
+
const record = {
|
|
1220
|
+
id,
|
|
1221
|
+
type: params.type,
|
|
1222
|
+
status: "running",
|
|
1223
|
+
partitionKey,
|
|
1224
|
+
input: clone(params.input),
|
|
1225
|
+
output: null,
|
|
1226
|
+
error: null,
|
|
1227
|
+
entityRef: params.entityRef ?? null,
|
|
1228
|
+
idempotencyKey: params.idempotencyKey ?? null,
|
|
1229
|
+
parentWorkflowId: params.parentWorkflowId ?? null,
|
|
1230
|
+
parentStepId: params.parentStepId ?? null,
|
|
1231
|
+
totalSteps: params.steps.length,
|
|
1232
|
+
completedSteps: 0,
|
|
1233
|
+
failedSteps: 0,
|
|
1234
|
+
metadata: params.metadata ? clone(params.metadata) : null,
|
|
1235
|
+
createdAt: params.startedAt,
|
|
1236
|
+
startedAt: params.startedAt,
|
|
1237
|
+
completedAt: null
|
|
1238
|
+
};
|
|
1239
|
+
workflows.set(id, record);
|
|
1240
|
+
if (params.idempotencyKey != null) byIdempotency.set(params.idempotencyKey, id);
|
|
1241
|
+
return {
|
|
1242
|
+
workflowId: id,
|
|
1243
|
+
steps: params.steps.map((s) => {
|
|
1244
|
+
const stepId = ++stepSeq;
|
|
1245
|
+
const stepRecord = {
|
|
1246
|
+
id: stepId,
|
|
1247
|
+
workflowId: id,
|
|
1248
|
+
key: s.key,
|
|
1249
|
+
type: s.type,
|
|
1250
|
+
status: "pending",
|
|
1251
|
+
dependencies: [...s.dependencies],
|
|
1252
|
+
input: s.input ? clone(s.input) : null,
|
|
1253
|
+
output: null,
|
|
1254
|
+
error: null,
|
|
1255
|
+
metadata: null,
|
|
1256
|
+
attempts: 0,
|
|
1257
|
+
parentStepId: null,
|
|
1258
|
+
startedAt: null,
|
|
1259
|
+
completedAt: null
|
|
1260
|
+
};
|
|
1261
|
+
steps.set(stepId, stepRecord);
|
|
1262
|
+
return clone(stepRecord);
|
|
1263
|
+
}),
|
|
1264
|
+
alreadyExisted: false
|
|
1265
|
+
};
|
|
1266
|
+
}
|
|
1267
|
+
async function getWorkflow(workflowId) {
|
|
1268
|
+
const w = workflows.get(workflowId);
|
|
1269
|
+
return w ? clone(w) : null;
|
|
1270
|
+
}
|
|
1271
|
+
async function getStep(stepId) {
|
|
1272
|
+
const s = steps.get(stepId);
|
|
1273
|
+
return s ? clone(s) : null;
|
|
1274
|
+
}
|
|
1275
|
+
async function listSteps(workflowId) {
|
|
1276
|
+
return stepsOf(workflowId).map(clone);
|
|
1277
|
+
}
|
|
1278
|
+
async function markStepRunning(stepId, startedAt) {
|
|
1279
|
+
const s = steps.get(stepId);
|
|
1280
|
+
if (!s || s.status !== "pending") return false;
|
|
1281
|
+
s.status = "running";
|
|
1282
|
+
s.startedAt = startedAt;
|
|
1283
|
+
s.attempts += 1;
|
|
1284
|
+
return true;
|
|
1285
|
+
}
|
|
1286
|
+
async function markStepPending(stepId) {
|
|
1287
|
+
const s = steps.get(stepId);
|
|
1288
|
+
if (s) s.status = "pending";
|
|
1289
|
+
}
|
|
1290
|
+
async function markStepWaiting(stepId) {
|
|
1291
|
+
const s = steps.get(stepId);
|
|
1292
|
+
if (s) s.status = "waiting";
|
|
1293
|
+
}
|
|
1294
|
+
async function markStepMapping(stepId) {
|
|
1295
|
+
const s = steps.get(stepId);
|
|
1296
|
+
if (s) s.status = "mapping";
|
|
1297
|
+
}
|
|
1298
|
+
async function markStepCompensating(stepId) {
|
|
1299
|
+
const s = steps.get(stepId);
|
|
1300
|
+
if (s) s.status = "compensating";
|
|
1301
|
+
}
|
|
1302
|
+
async function markStepCompensated(stepId, error) {
|
|
1303
|
+
const s = steps.get(stepId);
|
|
1304
|
+
if (s) {
|
|
1305
|
+
s.status = "compensated";
|
|
1306
|
+
if (error != null) s.error = error;
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
async function addChildSteps(workflowId, parentStepId, children) {
|
|
1310
|
+
const created = children.map((c) => {
|
|
1311
|
+
const stepId = ++stepSeq;
|
|
1312
|
+
const stepRecord = {
|
|
1313
|
+
id: stepId,
|
|
1314
|
+
workflowId,
|
|
1315
|
+
key: c.key,
|
|
1316
|
+
type: c.type,
|
|
1317
|
+
status: "pending",
|
|
1318
|
+
dependencies: [],
|
|
1319
|
+
input: c.input ? clone(c.input) : null,
|
|
1320
|
+
output: null,
|
|
1321
|
+
error: null,
|
|
1322
|
+
metadata: null,
|
|
1323
|
+
attempts: 0,
|
|
1324
|
+
parentStepId,
|
|
1325
|
+
startedAt: null,
|
|
1326
|
+
completedAt: null
|
|
1327
|
+
};
|
|
1328
|
+
steps.set(stepId, stepRecord);
|
|
1329
|
+
return clone(stepRecord);
|
|
1330
|
+
});
|
|
1331
|
+
const w = workflows.get(workflowId);
|
|
1332
|
+
if (w) w.totalSteps += created.length;
|
|
1333
|
+
return created;
|
|
1334
|
+
}
|
|
1335
|
+
async function listChildSteps(parentStepId) {
|
|
1336
|
+
return Array.from(steps.values()).filter((s) => s.parentStepId === parentStepId).sort((a, b) => a.id - b.id).map(clone);
|
|
1337
|
+
}
|
|
1338
|
+
async function completeStep(params) {
|
|
1339
|
+
const s = steps.get(params.stepId);
|
|
1340
|
+
if (s) {
|
|
1341
|
+
s.status = "completed";
|
|
1342
|
+
s.output = clone(params.output);
|
|
1343
|
+
s.completedAt = params.completedAt;
|
|
1344
|
+
}
|
|
1345
|
+
const w = workflows.get(params.workflowId);
|
|
1346
|
+
if (w) w.completedSteps += 1;
|
|
1347
|
+
}
|
|
1348
|
+
async function failStep(params) {
|
|
1349
|
+
const s = steps.get(params.stepId);
|
|
1350
|
+
if (s) {
|
|
1351
|
+
s.status = "failed";
|
|
1352
|
+
s.error = params.error;
|
|
1353
|
+
s.completedAt = params.completedAt;
|
|
1354
|
+
}
|
|
1355
|
+
const w = workflows.get(params.workflowId);
|
|
1356
|
+
if (w) w.failedSteps += 1;
|
|
1357
|
+
}
|
|
1358
|
+
async function skipStep(stepId, reason) {
|
|
1359
|
+
const s = steps.get(stepId);
|
|
1360
|
+
if (s) {
|
|
1361
|
+
s.status = "skipped";
|
|
1362
|
+
s.error = reason;
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
async function skipPendingSteps(workflowId, reason) {
|
|
1366
|
+
for (const s of stepsOf(workflowId)) if (s.status === "pending" || s.status === "waiting") {
|
|
1367
|
+
s.status = "skipped";
|
|
1368
|
+
s.error = reason;
|
|
1369
|
+
}
|
|
1370
|
+
}
|
|
1371
|
+
async function finishWorkflow(params) {
|
|
1372
|
+
const w = workflows.get(params.workflowId);
|
|
1373
|
+
if (!w) return;
|
|
1374
|
+
w.status = params.status;
|
|
1375
|
+
w.completedAt = params.completedAt;
|
|
1376
|
+
if (params.output !== void 0) w.output = clone(params.output);
|
|
1377
|
+
if (params.error !== void 0) w.error = params.error;
|
|
1378
|
+
}
|
|
1379
|
+
async function listWorkflows(filters) {
|
|
1380
|
+
let result = Array.from(workflows.values());
|
|
1381
|
+
if (filters.status) result = result.filter((w) => w.status === filters.status);
|
|
1382
|
+
if (filters.type) result = result.filter((w) => w.type === filters.type);
|
|
1383
|
+
if (filters.entityRef) result = result.filter((w) => w.entityRef === filters.entityRef);
|
|
1384
|
+
result = result.sort((a, b) => b.id - a.id).slice(0, filters.limit ?? 50);
|
|
1385
|
+
return result.map((w) => ({
|
|
1386
|
+
...clone(w),
|
|
1387
|
+
steps: stepsOf(w.id).map(clone)
|
|
1388
|
+
}));
|
|
1389
|
+
}
|
|
1390
|
+
async function listRunningWorkflows() {
|
|
1391
|
+
return Array.from(workflows.values()).filter((w) => w.status === "running").map(clone);
|
|
1392
|
+
}
|
|
1393
|
+
async function findStuckSteps(workflowId, cutoff) {
|
|
1394
|
+
return stepsOf(workflowId).filter((s) => s.status === "running" && s.startedAt != null && s.startedAt < cutoff).map(clone);
|
|
1395
|
+
}
|
|
1396
|
+
return {
|
|
1397
|
+
createWorkflow,
|
|
1398
|
+
getWorkflow,
|
|
1399
|
+
getStep,
|
|
1400
|
+
listSteps,
|
|
1401
|
+
markStepRunning,
|
|
1402
|
+
markStepPending,
|
|
1403
|
+
markStepWaiting,
|
|
1404
|
+
markStepMapping,
|
|
1405
|
+
markStepCompensating,
|
|
1406
|
+
markStepCompensated,
|
|
1407
|
+
addChildSteps,
|
|
1408
|
+
listChildSteps,
|
|
1409
|
+
completeStep,
|
|
1410
|
+
failStep,
|
|
1411
|
+
skipStep,
|
|
1412
|
+
skipPendingSteps,
|
|
1413
|
+
finishWorkflow,
|
|
1414
|
+
listWorkflows,
|
|
1415
|
+
listRunningWorkflows,
|
|
1416
|
+
findStuckSteps,
|
|
1417
|
+
_dump() {
|
|
1418
|
+
return {
|
|
1419
|
+
workflows: Array.from(workflows.values()).map(clone),
|
|
1420
|
+
steps: Array.from(steps.values()).map(clone)
|
|
1421
|
+
};
|
|
1422
|
+
}
|
|
1423
|
+
};
|
|
1424
|
+
}
|
|
1425
|
+
//#endregion
|
|
1426
|
+
export { PUBLIC_WORKFLOW_SCHEMA, PUBLIC_WORKFLOW_STEP_SCHEMA, STEP_DISPLAY_STATUS, STEP_DISPLAY_STATUS_SCHEMA, WORKFLOW_STATUS_SCHEMA, buildWorkflow, createInMemoryStepGate, createInMemoryWorkflowStore, createRecordingObserver, createRecordingTracer, createStepHandlerRegistry, createWorkflowEngine, defineMapStep, defineSleepStep, defineStep, defineSubWorkflowStep, defineWaitStep, err, explicitRetryability, isRetryableError, markRetryable, nonRetryableError, noopLogger, noopObserver, noopTracer, ok, retryableError, toDisplayStepStatus, toPublicStep, toPublicWorkflow };
|