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