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/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 };