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.
@@ -0,0 +1,574 @@
1
+ import { z } from "zod";
2
+ //#region src/core/result.ts
3
+ const ok = (value) => ({
4
+ ok: true,
5
+ value
6
+ });
7
+ const err = (error) => ({
8
+ ok: false,
9
+ error
10
+ });
11
+ //#endregion
12
+ //#region src/core/logger.ts
13
+ const noopLogger = {
14
+ info() {},
15
+ warn() {},
16
+ error() {}
17
+ };
18
+ //#endregion
19
+ //#region src/core/retry.ts
20
+ /**
21
+ * Realm-safe marker for an explicit retryability decision. `Symbol.for` so a
22
+ * duplicated copy of this module still reads markers set by the other copy.
23
+ */
24
+ const RETRYABLE = Symbol.for("octaflow.retryable");
25
+ /**
26
+ * Fallback store for errors that cannot carry a property — frozen, sealed, or
27
+ * non-extensible objects, which some SDKs hand out. Keyed weakly, so marking an
28
+ * error never keeps it alive.
29
+ */
30
+ const sideTable = /* @__PURE__ */ new WeakMap();
31
+ /**
32
+ * Tag an error with an explicit retryability decision, overriding both the
33
+ * step predicate and the message heuristic. Returns the same error, so it can
34
+ * wrap a throw:
35
+ *
36
+ * ```ts
37
+ * throw markRetryable(await client.readError(), true);
38
+ * ```
39
+ */
40
+ function markRetryable(error, retryable = true) {
41
+ if (typeof error === "object" && error !== null) try {
42
+ Object.defineProperty(error, RETRYABLE, {
43
+ value: retryable,
44
+ enumerable: false,
45
+ configurable: true
46
+ });
47
+ } catch {
48
+ sideTable.set(error, retryable);
49
+ }
50
+ return error;
51
+ }
52
+ /** An error the engine will always retry (within the step's attempt budget). */
53
+ function retryableError(message, options) {
54
+ return markRetryable(new Error(message, options), true);
55
+ }
56
+ /** An error the engine will never retry, whatever its message says. */
57
+ function nonRetryableError(message, options) {
58
+ return markRetryable(new Error(message, options), false);
59
+ }
60
+ /** How deep to follow `cause` before giving up. Guards against pathological chains. */
61
+ const MAX_CAUSE_DEPTH = 8;
62
+ /**
63
+ * The explicit decision carried by an error, or `undefined` if it carries none.
64
+ * Callers deciding retryability themselves should consult this first.
65
+ *
66
+ * Follows the `cause` chain, so a marked error keeps its decision when a caller
67
+ * wraps it — `new Error('upstream failed', { cause: retryableError('busy') })`
68
+ * still reports `true`. The outermost decision wins; the walk is depth-capped and
69
+ * cycle-safe.
70
+ */
71
+ function explicitRetryability(error) {
72
+ const seen = /* @__PURE__ */ new Set();
73
+ let current = error;
74
+ for (let depth = 0; depth < MAX_CAUSE_DEPTH; depth++) {
75
+ if (typeof current !== "object" || current === null || seen.has(current)) return void 0;
76
+ seen.add(current);
77
+ const value = current[RETRYABLE];
78
+ if (typeof value === "boolean") return value;
79
+ const stored = sideTable.get(current);
80
+ if (stored !== void 0) return stored;
81
+ current = current.cause;
82
+ }
83
+ }
84
+ /**
85
+ * Transient `code` values, as set by Node's networking stack and by most clients
86
+ * built on it. Deliberately excludes permanent ones like `ENOTFOUND` (bad host)
87
+ * and `ERR_INVALID_ARG_TYPE` (a bug).
88
+ */
89
+ const RETRYABLE_CODES = /* @__PURE__ */ new Set([
90
+ "ECONNRESET",
91
+ "ECONNREFUSED",
92
+ "ECONNABORTED",
93
+ "ETIMEDOUT",
94
+ "EPIPE",
95
+ "EHOSTUNREACH",
96
+ "ENETUNREACH",
97
+ "ENETRESET",
98
+ "EAI_AGAIN",
99
+ "EBUSY",
100
+ "UND_ERR_CONNECT_TIMEOUT",
101
+ "UND_ERR_HEADERS_TIMEOUT",
102
+ "UND_ERR_BODY_TIMEOUT"
103
+ ]);
104
+ /** Transient HTTP statuses. 5xx is transient except the ones that mean "never". */
105
+ function isRetryableStatus(status) {
106
+ if (status === 408 || status === 425 || status === 429) return true;
107
+ return status >= 500 && status < 600 && status !== 501 && status !== 505;
108
+ }
109
+ /** Read a numeric HTTP status off the shapes clients actually use. */
110
+ function statusOf(error) {
111
+ for (const key of [
112
+ "status",
113
+ "statusCode",
114
+ "httpStatusCode"
115
+ ]) {
116
+ const v = error[key];
117
+ if (typeof v === "number" && Number.isFinite(v)) return v;
118
+ }
119
+ const response = error.response;
120
+ if (response && typeof response.status === "number") return response.status;
121
+ }
122
+ /**
123
+ * Default classifier for a thrown error, used when nothing more specific applies.
124
+ * Checked in order:
125
+ *
126
+ * 1. An explicit marker ({@link markRetryable}), including through `cause`.
127
+ * 2. **Structured fields** — `code` (`ECONNRESET`, `ETIMEDOUT`, …) and HTTP
128
+ * status (`status` / `statusCode` / `response.status`): 408, 425, 429, 5xx.
129
+ * 3. **The message**, matched against a small vocabulary.
130
+ *
131
+ * Steps 2–3 are a convenience, not a classifier. The message check especially will
132
+ * misjudge anything whose wording it doesn't recognise — a permanent bug reading
133
+ * `'timeout must be > 0'` looks transient. Reach for {@link retryableError} /
134
+ * {@link nonRetryableError}, `defineStep`'s `isRetryable`, or the engine's
135
+ * `defaultRetryable` when the answer matters.
136
+ */
137
+ function isRetryableError(error) {
138
+ const explicit = explicitRetryability(error);
139
+ if (explicit !== void 0) return explicit;
140
+ if (typeof error === "object" && error !== null) {
141
+ const code = error.code;
142
+ if (typeof code === "string" && RETRYABLE_CODES.has(code.toUpperCase())) return true;
143
+ const status = statusOf(error);
144
+ if (status !== void 0) return isRetryableStatus(status);
145
+ }
146
+ if (!(error instanceof Error)) return false;
147
+ const message = error.message.toLowerCase();
148
+ if (message.includes("rate limit") || message.includes("429") || message.includes("too many requests")) return true;
149
+ if (message.includes("timeout") || message.includes("econnreset") || message.includes("fetch failed")) return true;
150
+ if (message.includes("503") || message.includes("service unavailable")) return true;
151
+ return false;
152
+ }
153
+ //#endregion
154
+ //#region src/core/defineStep.ts
155
+ /**
156
+ * `z.object({})` typed as the empty-record schema. Zod infers `{}` for an empty object schema,
157
+ * but we want `Record<string, never>` for input/output-less steps — hence the bridge cast.
158
+ * Shared by `defineWaitStep`, `defineMapStep`'s child, and `defineSleepStep`.
159
+ */
160
+ const emptyObjectSchema = z.object({});
161
+ /**
162
+ * Define a type-safe workflow step with Zod validation. Wraps a typed handler
163
+ * into an untyped `StepHandler` with a 5-phase pipeline:
164
+ *
165
+ * 1. Parse `workflowInput` against `workflowInputSchema`
166
+ * 2. Parse each dependency output against its `outputSchema`
167
+ * 3. Build the typed context
168
+ * 4. Call the handler
169
+ * 5. Parse the output against `outputSchema`
170
+ *
171
+ * Validation errors are non-retryable (a schema mismatch is a programming error).
172
+ */
173
+ function defineStep(config) {
174
+ const { type, workflowInputSchema, outputSchema, dependencies, handler, retry, isRetryable, timeoutMs, delayMs, waitForEvent } = config;
175
+ const deps = dependencies ?? {};
176
+ const wrappedHandler = async (ctx) => {
177
+ try {
178
+ const inputResult = workflowInputSchema.safeParse(ctx.workflowInput);
179
+ if (!inputResult.success) return {
180
+ ok: false,
181
+ error: {
182
+ key: "step_error",
183
+ message: `[${type}] Invalid workflow input: ${inputResult.error.message}`,
184
+ retryable: false
185
+ }
186
+ };
187
+ const parsedDeps = {};
188
+ for (const [depKey, depStep] of Object.entries(deps)) {
189
+ const depResult = depStep.outputSchema.safeParse(ctx.dependencyOutputs[depKey]);
190
+ if (!depResult.success) return {
191
+ ok: false,
192
+ error: {
193
+ key: "step_error",
194
+ message: `[${type}] Invalid dependency output '${depKey}': ${depResult.error.message}`,
195
+ retryable: false
196
+ }
197
+ };
198
+ parsedDeps[depKey] = depResult.data;
199
+ }
200
+ const typedCtx = {
201
+ workflowId: ctx.workflowId,
202
+ stepId: ctx.stepId,
203
+ stepKey: ctx.stepKey,
204
+ partitionKey: ctx.partitionKey,
205
+ workflowInput: inputResult.data,
206
+ stepInput: ctx.stepInput,
207
+ deps: parsedDeps,
208
+ signal: ctx.signal,
209
+ context: ctx.context
210
+ };
211
+ const output = await handler(typedCtx);
212
+ const outputResult = outputSchema.safeParse(output);
213
+ if (!outputResult.success) return {
214
+ ok: false,
215
+ error: {
216
+ key: "step_error",
217
+ message: `[${type}] Invalid step output: ${outputResult.error.message}`,
218
+ retryable: false
219
+ }
220
+ };
221
+ return {
222
+ ok: true,
223
+ value: outputResult.data
224
+ };
225
+ } catch (error) {
226
+ const message = error instanceof Error ? error.message : "Unknown step error";
227
+ const marked = explicitRetryability(error);
228
+ return {
229
+ ok: false,
230
+ error: {
231
+ key: "step_error",
232
+ message,
233
+ retryable: marked ?? (isRetryable ? isRetryable(error) : isRetryableError(error)),
234
+ retryableFrom: marked !== void 0 ? "explicit" : isRetryable ? "predicate" : "heuristic"
235
+ }
236
+ };
237
+ }
238
+ };
239
+ let wrappedCompensate;
240
+ if (config.compensate) {
241
+ const compensate = config.compensate;
242
+ wrappedCompensate = async (ctx) => {
243
+ const input = workflowInputSchema.safeParse(ctx.workflowInput);
244
+ const parsedDeps = {};
245
+ for (const [depKey, depStep] of Object.entries(deps)) {
246
+ const r = depStep.outputSchema.safeParse(ctx.dependencyOutputs[depKey]);
247
+ parsedDeps[depKey] = r.success ? r.data : ctx.dependencyOutputs[depKey];
248
+ }
249
+ const out = outputSchema.safeParse(ctx.output);
250
+ await compensate({
251
+ workflowId: ctx.workflowId,
252
+ stepId: ctx.stepId,
253
+ stepKey: ctx.stepKey,
254
+ partitionKey: ctx.partitionKey,
255
+ workflowInput: input.success ? input.data : ctx.workflowInput,
256
+ stepInput: ctx.stepInput,
257
+ deps: parsedDeps,
258
+ signal: ctx.signal,
259
+ context: ctx.context,
260
+ output: out.success ? out.data : ctx.output
261
+ });
262
+ };
263
+ }
264
+ return {
265
+ type,
266
+ workflowInputSchema,
267
+ outputSchema,
268
+ dependencies: deps,
269
+ handler: wrappedHandler,
270
+ retry,
271
+ timeoutMs,
272
+ delayMs,
273
+ waitForEvent,
274
+ compensate: wrappedCompensate
275
+ };
276
+ }
277
+ /**
278
+ * Define a **wait-for-event** step: once its dependencies complete, it suspends
279
+ * (status `waiting`) instead of running, until `engine.resumeStep(workflowId, stepKey,
280
+ * payload)` delivers an external event. The resume payload becomes the step's output
281
+ * (validated by dependents against `outputSchema`), then the DAG advances.
282
+ *
283
+ * Use for human-in-the-loop approvals, webhooks, or any async external dependency.
284
+ */
285
+ function defineWaitStep(config) {
286
+ return defineStep({
287
+ type: config.type,
288
+ workflowInputSchema: emptyObjectSchema,
289
+ outputSchema: config.outputSchema,
290
+ dependencies: config.dependencies,
291
+ waitForEvent: true,
292
+ handler: async () => ({})
293
+ });
294
+ }
295
+ /**
296
+ * Define a **map** step: dynamic fan-out. `items(ctx)` produces a runtime-sized
297
+ * list from the step's deps/input; the engine spawns one child per item (each running
298
+ * `each(item, …)` with its own retry/timeout), suspends the parent as `mapping`, and
299
+ * completes it with `{ items: childOutputs[] }` (ordered) once all children finish. A
300
+ * failed item fails the whole map. Use for "do X for each of N images/sections/locales".
301
+ *
302
+ * The map step's output type is `{ items: TItemOutput[] }`; dependents read `deps.X.items`.
303
+ */
304
+ function defineMapStep(config) {
305
+ const childType = `${config.type}__item`;
306
+ const parent = defineStep({
307
+ type: config.type,
308
+ workflowInputSchema: config.workflowInputSchema,
309
+ outputSchema: z.object({ items: z.array(z.unknown()) }),
310
+ dependencies: config.dependencies,
311
+ handler: async (ctx) => ({ items: await config.items(ctx) })
312
+ });
313
+ const child = defineStep({
314
+ type: childType,
315
+ workflowInputSchema: emptyObjectSchema,
316
+ outputSchema: config.itemOutputSchema,
317
+ retry: config.itemRetry,
318
+ isRetryable: config.itemIsRetryable,
319
+ timeoutMs: config.itemTimeoutMs,
320
+ handler: async (ctx) => config.each(ctx.stepInput.item, {
321
+ index: Number(ctx.stepInput.index ?? 0),
322
+ context: ctx.context,
323
+ workflowId: ctx.workflowId,
324
+ partitionKey: ctx.partitionKey
325
+ })
326
+ });
327
+ return {
328
+ ...parent,
329
+ outputSchema: z.object({ items: z.array(config.itemOutputSchema) }),
330
+ map: true,
331
+ childType,
332
+ childRegistration: {
333
+ type: childType,
334
+ handler: child.handler,
335
+ retry: child.retry,
336
+ timeoutMs: child.timeoutMs
337
+ }
338
+ };
339
+ }
340
+ /**
341
+ * Define a **sub-workflow** step: once its deps complete, `input(ctx)` maps the
342
+ * parent context to the child workflow's input; the engine **starts the child workflow**
343
+ * (sharing the partition), suspends this step as `waiting`, and resumes it with the child's
344
+ * output once the child terminates. A failed/cancelled child fails this step (and cascades).
345
+ *
346
+ * Pass the built child workflow (`buildWorkflow(...)`); its step handlers are registered
347
+ * automatically alongside the parent. The step's output type is the child's output shape —
348
+ * give `outputSchema` to type it for dependents (defaults to an opaque record).
349
+ */
350
+ function defineSubWorkflowStep(config) {
351
+ const recordSchema = z.record(z.string(), z.unknown());
352
+ return {
353
+ ...defineStep({
354
+ type: config.type,
355
+ workflowInputSchema: config.workflowInputSchema,
356
+ outputSchema: recordSchema,
357
+ dependencies: config.dependencies,
358
+ handler: async (ctx) => config.input(ctx)
359
+ }),
360
+ outputSchema: config.outputSchema ?? recordSchema,
361
+ subWorkflowDefinition: config.childWorkflow.definition,
362
+ subWorkflowRegister: config.childWorkflow.register
363
+ };
364
+ }
365
+ /**
366
+ * Define a durable **sleep** step: a no-op step that, once its dependencies complete,
367
+ * is held in the queue for `sleepMs` before completing — advancing the DAG afterwards.
368
+ * The delay is durable (survives restarts) because it lives in the queue, not in memory.
369
+ *
370
+ * A sleep step is just `defineStep` with `delayMs` and an empty handler; use this for
371
+ * cooldowns, "continue in 1 hour", or spacing out downstream work.
372
+ */
373
+ function defineSleepStep(config) {
374
+ return defineStep({
375
+ type: config.type,
376
+ workflowInputSchema: emptyObjectSchema,
377
+ outputSchema: emptyObjectSchema,
378
+ dependencies: config.dependencies,
379
+ delayMs: config.sleepMs,
380
+ handler: async () => ({})
381
+ });
382
+ }
383
+ /**
384
+ * Build a typed workflow from step definitions. Derives the `WorkflowDefinition`
385
+ * DAG from each step's dependency metadata and validates at construction time
386
+ * that every dependency key references a real step.
387
+ */
388
+ function buildWorkflow(config) {
389
+ const { type, inputSchema, steps } = config;
390
+ const stepKeys = new Set(Object.keys(steps));
391
+ for (const [stepKey, step] of Object.entries(steps)) for (const depKey of Object.keys(step.dependencies)) if (!stepKeys.has(depKey)) throw new Error(`[buildWorkflow] Step '${stepKey}' depends on '${depKey}', which is not a valid step key. Valid keys: ${[...stepKeys].join(", ")}`);
392
+ const definition = {
393
+ type,
394
+ steps: Object.entries(steps).map(([key, step]) => {
395
+ const depKeys = Object.keys(step.dependencies);
396
+ return {
397
+ key,
398
+ type: step.type,
399
+ ...depKeys.length > 0 ? { dependencies: depKeys } : {}
400
+ };
401
+ })
402
+ };
403
+ return {
404
+ type,
405
+ inputSchema,
406
+ definition,
407
+ register(registry) {
408
+ for (const step of Object.values(steps)) {
409
+ registry.register(step.type, step.handler, {
410
+ retry: step.retry,
411
+ timeoutMs: step.timeoutMs,
412
+ delayMs: step.delayMs,
413
+ waitForEvent: step.waitForEvent,
414
+ map: step.map,
415
+ childType: step.childType,
416
+ subWorkflowDefinition: step.subWorkflowDefinition,
417
+ compensate: step.compensate
418
+ });
419
+ if (step.childRegistration) {
420
+ const c = step.childRegistration;
421
+ registry.register(c.type, c.handler, {
422
+ retry: c.retry,
423
+ timeoutMs: c.timeoutMs
424
+ });
425
+ }
426
+ if (step.subWorkflowRegister) step.subWorkflowRegister(registry);
427
+ }
428
+ },
429
+ start(engine, input, options) {
430
+ const parsed = inputSchema.parse(input);
431
+ return engine.startWorkflow(definition, parsed, options);
432
+ }
433
+ };
434
+ }
435
+ //#endregion
436
+ //#region src/core/public-view.ts
437
+ /**
438
+ * Public wire view of a workflow — the projection every HTTP consumer was
439
+ * hand-writing.
440
+ *
441
+ * The engine's records ({@link WorkflowRecord}, {@link StepRecord}) are the
442
+ * store's data model: they carry fields that must not leak onto a public API
443
+ * (`partitionKey`, `idempotencyKey`, sub-workflow linkage, `metadata`, retry
444
+ * `attempts`) and step statuses that are engine mechanics rather than display
445
+ * states. Serving them verbatim also couples the consumer's API contract to
446
+ * flow's record shape. This module owns the boundary instead:
447
+ *
448
+ * - {@link toPublicWorkflow} / {@link toPublicStep} — drop the internal
449
+ * fields, fold engine step statuses to the five display states.
450
+ * - {@link PUBLIC_WORKFLOW_SCHEMA} / {@link PUBLIC_WORKFLOW_STEP_SCHEMA} —
451
+ * the same shapes as Zod schemas, ready for route `response` declarations
452
+ * (OpenAPI output, response validation, typed clients).
453
+ *
454
+ * The projection is deliberately closed: anything consumer-specific rides on
455
+ * top rather than through an options bag. E.g. surfacing an `appliedAt` stamp
456
+ * kept in workflow metadata:
457
+ *
458
+ * ```ts
459
+ * const schema = PUBLIC_WORKFLOW_SCHEMA.extend({ appliedAt: z.string().nullable() });
460
+ * const view = {
461
+ * ...toPublicWorkflow(workflow),
462
+ * appliedAt: (workflow.metadata?.appliedAt as string | undefined) ?? null,
463
+ * };
464
+ * ```
465
+ */
466
+ const STEP_DISPLAY_STATUS_SCHEMA = z.enum([
467
+ "pending",
468
+ "running",
469
+ "completed",
470
+ "failed",
471
+ "skipped"
472
+ ]);
473
+ /**
474
+ * Engine step statuses folded to display states. `waiting` (suspended for an
475
+ * event), `mapping` (fan-out in flight), and `compensating` (rollback running)
476
+ * are all "still in flight" from a reader's point of view; a `compensated`
477
+ * step's work was undone, which reads as `skipped`, with the failure that
478
+ * triggered the rollback reported by the failed step itself. Exhaustive over
479
+ * {@link StepStatus} so a new engine status is a compile error here, not a
480
+ * silent hole in a consumer's API.
481
+ */
482
+ const STEP_DISPLAY_STATUS = {
483
+ pending: "pending",
484
+ running: "running",
485
+ completed: "completed",
486
+ failed: "failed",
487
+ skipped: "skipped",
488
+ waiting: "running",
489
+ mapping: "running",
490
+ compensating: "running",
491
+ compensated: "skipped"
492
+ };
493
+ /** Fold one engine step status to its display state. */
494
+ function toDisplayStepStatus(status) {
495
+ return STEP_DISPLAY_STATUS[status];
496
+ }
497
+ const WORKFLOW_STATUS_SCHEMA = z.enum([
498
+ "pending",
499
+ "running",
500
+ "completed",
501
+ "failed",
502
+ "cancelled"
503
+ ]);
504
+ const PUBLIC_WORKFLOW_STEP_SCHEMA = z.object({
505
+ id: z.number().int(),
506
+ key: z.string(),
507
+ type: z.string(),
508
+ status: STEP_DISPLAY_STATUS_SCHEMA,
509
+ dependencies: z.array(z.string()),
510
+ input: z.record(z.string(), z.unknown()).nullable(),
511
+ output: z.record(z.string(), z.unknown()).nullable(),
512
+ error: z.string().nullable(),
513
+ startedAt: z.string().nullable(),
514
+ completedAt: z.string().nullable()
515
+ });
516
+ const PUBLIC_WORKFLOW_SCHEMA = z.object({
517
+ id: z.number().int(),
518
+ type: z.string(),
519
+ status: WORKFLOW_STATUS_SCHEMA,
520
+ input: z.record(z.string(), z.unknown()),
521
+ output: z.record(z.string(), z.unknown()).nullable(),
522
+ error: z.string().nullable(),
523
+ entityRef: z.string().nullable(),
524
+ totalSteps: z.number().int(),
525
+ completedSteps: z.number().int(),
526
+ failedSteps: z.number().int(),
527
+ steps: z.array(PUBLIC_WORKFLOW_STEP_SCHEMA),
528
+ createdAt: z.string(),
529
+ startedAt: z.string().nullable(),
530
+ completedAt: z.string().nullable()
531
+ });
532
+ /**
533
+ * Project a step record to its public view. Drops `workflowId` (redundant
534
+ * under the parent), `metadata`, `attempts`, and `parentStepId`; folds the
535
+ * status.
536
+ */
537
+ function toPublicStep(step) {
538
+ return {
539
+ id: step.id,
540
+ key: step.key,
541
+ type: step.type,
542
+ status: toDisplayStepStatus(step.status),
543
+ dependencies: step.dependencies,
544
+ input: step.input,
545
+ output: step.output,
546
+ error: step.error,
547
+ startedAt: step.startedAt,
548
+ completedAt: step.completedAt
549
+ };
550
+ }
551
+ /**
552
+ * Project a workflow (with steps) to its public view. Drops `partitionKey`,
553
+ * `idempotencyKey`, `parentWorkflowId`/`parentStepId`, and `metadata`.
554
+ */
555
+ function toPublicWorkflow(workflow) {
556
+ return {
557
+ id: workflow.id,
558
+ type: workflow.type,
559
+ status: workflow.status,
560
+ input: workflow.input,
561
+ output: workflow.output,
562
+ error: workflow.error,
563
+ entityRef: workflow.entityRef,
564
+ totalSteps: workflow.totalSteps,
565
+ completedSteps: workflow.completedSteps,
566
+ failedSteps: workflow.failedSteps,
567
+ steps: workflow.steps.map(toPublicStep),
568
+ createdAt: workflow.createdAt,
569
+ startedAt: workflow.startedAt,
570
+ completedAt: workflow.completedAt
571
+ };
572
+ }
573
+ //#endregion
574
+ export { ok as S, markRetryable as _, WORKFLOW_STATUS_SCHEMA as a, noopLogger as b, toPublicWorkflow as c, defineSleepStep as d, defineStep as f, isRetryableError as g, explicitRetryability as h, STEP_DISPLAY_STATUS_SCHEMA as i, buildWorkflow as l, defineWaitStep as m, PUBLIC_WORKFLOW_STEP_SCHEMA as n, toDisplayStepStatus as o, defineSubWorkflowStep as p, STEP_DISPLAY_STATUS as r, toPublicStep as s, PUBLIC_WORKFLOW_SCHEMA as t, defineMapStep as u, nonRetryableError as v, err as x, retryableError as y };
@@ -0,0 +1,26 @@
1
+ import { F as StepId, K as FlowErrorShape, V as WorkflowId, q as Result } from "./index-D_fOYfmf.js";
2
+ //#region src/core/dispatcher.d.ts
3
+ /** Payload handed to the dispatcher to schedule a single step for execution. */
4
+ interface DispatchStepPayload {
5
+ workflowId: WorkflowId;
6
+ stepId: StepId;
7
+ stepKey: string;
8
+ stepType: string;
9
+ }
10
+ interface EnqueueOptions {
11
+ /**
12
+ * Delay before the step becomes eligible to run, in seconds. Durable — survives
13
+ * restarts. Used for retry backoff (and, later, durable sleep). Default 0.
14
+ */
15
+ startAfterSeconds?: number;
16
+ }
17
+ /**
18
+ * Schedules step execution. The default adapter is a pg-boss queue, but any
19
+ * durable-job mechanism works: the only contract is "eventually call
20
+ * `engine.executeStep(workflowId, stepId)` for this payload, with retries".
21
+ */
22
+ interface Dispatcher {
23
+ enqueueStep(payload: DispatchStepPayload, options?: EnqueueOptions): Promise<Result<void, FlowErrorShape>>;
24
+ }
25
+ //#endregion
26
+ export { Dispatcher as n, EnqueueOptions as r, DispatchStepPayload as t };