octaflow 0.15.0 → 0.17.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/README.md +27 -11
- package/dist/ai/index.d.ts +2 -2
- package/dist/ai/index.js +1 -1
- package/dist/{core-ERYGlYsS.js → core-DNWPwFaE.js} +119 -37
- package/dist/{dispatcher-CnMTwfCc.d.ts → dispatcher-Cbb-KjY_.d.ts} +15 -2
- package/dist/dispatcher-pgboss/index.d.ts +59 -9
- package/dist/dispatcher-pgboss/index.js +37 -10
- package/dist/{hooks-CDRFQZbT.d.ts → hooks-BdxIcc92.d.ts} +1 -1
- package/dist/{index-D_fOYfmf.d.ts → index-BURAYZui.d.ts} +259 -13
- package/dist/index.d.ts +37 -9
- package/dist/index.js +762 -129
- package/dist/{observability-3tcmRwIw.d.ts → observability-B5KgSJfF.d.ts} +77 -6
- package/dist/store-pg/index.d.ts +2 -2
- package/dist/store-pg/index.js +64 -7
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
**Durable workflows for TypeScript that run on the Postgres you already have.**
|
|
10
10
|
|
|
11
11
|
Declare a DAG of Zod-typed steps. The engine runs each step as soon as its dependencies
|
|
12
|
-
complete, persists every transition, retries failures, and
|
|
12
|
+
complete, persists every transition, retries failures, and picks up where it left off after a
|
|
13
|
+
restart. There is
|
|
13
14
|
no workflow server to operate, no control plane, and no vendor — it's a library you import,
|
|
14
15
|
not a platform you adopt.
|
|
15
16
|
|
|
@@ -64,11 +65,12 @@ of what it did. Both models are durable — they trade off differently, and
|
|
|
64
65
|
|
|
65
66
|
A fuller pipeline than the snippet above — dynamic fan-out over two `map` steps at once, a
|
|
66
67
|
concurrency cap so only 2 of 6 images encode at a time, a flaky step retried, a sub-workflow,
|
|
67
|
-
a step suspended on an external event
|
|
68
|
-
|
|
68
|
+
a step suspended on an external event under a deadline, a durable sleep, and a branch where
|
|
69
|
+
the arm the review didn't choose is skipped but the join still fires. Then a second workflow
|
|
70
|
+
fails and its completed steps roll back in reverse.
|
|
69
71
|
|
|
70
72
|
<p align="center">
|
|
71
|
-
<img src="./docs/demo.svg" alt="Terminal trace of a publishing pipeline: parallel fan-out over locales and images with a concurrency cap, a retried step, a sub-workflow, a suspend-and-resume on an external event, a durable sleep, then a saga rollback in reverse order" width="760">
|
|
73
|
+
<img src="./docs/demo.svg" alt="Terminal trace of a publishing pipeline: parallel fan-out over locales and images with a concurrency cap, a retried step, a sub-workflow, a suspend-and-resume on an external event with a deadline armed, a durable sleep, a skipped branch converging on a join, then a saga rollback in reverse order" width="760">
|
|
72
74
|
</p>
|
|
73
75
|
|
|
74
76
|
Every line is an engine transition emitted through the [`FlowObserver`](https://octabits-io.github.io/octaflow/running/observability/) seam —
|
|
@@ -118,10 +120,12 @@ that the others have earned.
|
|
|
118
120
|
Reach for something else if:
|
|
119
121
|
|
|
120
122
|
- **Your control flow is genuinely dynamic.** A declarative DAG is fixed at definition time.
|
|
121
|
-
Flow softens this
|
|
122
|
-
[
|
|
123
|
-
|
|
124
|
-
|
|
123
|
+
Flow softens this a lot — [`when` guards and joins](https://octabits-io.github.io/octaflow/core/branching/) (if/else over a static graph),
|
|
124
|
+
[`defineMapStep`](https://octabits-io.github.io/octaflow/core/fan-out-and-map/) (runtime-sized fan-out),
|
|
125
|
+
[sub-workflows](https://octabits-io.github.io/octaflow/core/sub-workflows/), and [`waitForEvent`](https://octabits-io.github.io/octaflow/core/signals/) — but the
|
|
126
|
+
set of steps is still fixed up front. If your process is "**loop** until a human approves,
|
|
127
|
+
branching on whatever they typed," an imperative durable function will express it more
|
|
128
|
+
naturally: Flow can pick a branch, not invent a step.
|
|
125
129
|
- **You want a UI out of the box.** Flow ships a wire-safe projection
|
|
126
130
|
([`toPublicWorkflow`](https://octabits-io.github.io/octaflow/extending/http/)) and lifecycle events, not a
|
|
127
131
|
dashboard. You build it.
|
|
@@ -187,13 +191,18 @@ next to what your steps actually do.
|
|
|
187
191
|
| ⚡ | **Auto-parallelism** — dependency-free steps run concurrently; a step starts when all its deps complete |
|
|
188
192
|
| 🔁 | **Retry & timeout** — per-step `maxAttempts`, fixed/exponential backoff, wall-clock timeout |
|
|
189
193
|
| 💤 | **Durable sleep** — hold a step in the queue for N ms (survives restarts) |
|
|
194
|
+
| 🔀 | **Conditional branching** — `when` guards skip a step and its branch; `join: 'any'` converges |
|
|
195
|
+
| ⏳ | **Deadlines** — a budget on a suspended step (fail, or continue with a stand-in answer) and on a whole run |
|
|
196
|
+
| ♻️ | **Retry a failed run** — `retryWorkflow` resumes from the failure point; completed steps keep their output |
|
|
190
197
|
| 🚦 | **Concurrency & rate limiting** — per-step-type caps and token buckets via a pluggable gate |
|
|
191
198
|
| ⏰ | **Cron / scheduled starts** — fire workflows on a schedule (pg-boss) |
|
|
192
199
|
| 🔑 | **Start idempotency** — a dedup key collapses double-clicks / overlapping ticks |
|
|
193
200
|
| 🗺️ | **Dynamic fan-out / map** — spawn one child step per item of a runtime-sized list |
|
|
194
|
-
| ⏸️ | **Signals / waitForEvent** — suspend a step until an external event (`resumeStep`) |
|
|
201
|
+
| ⏸️ | **Signals / waitForEvent** — suspend a step until an external event (`resumeStep`), with an optional deadline |
|
|
195
202
|
| 🪆 | **Sub-workflows** — a step starts a child workflow and awaits its result |
|
|
196
203
|
| ↩️ | **Saga compensation** — run rollback handlers in reverse order on failure |
|
|
204
|
+
| 🚑 | **Crash recovery** — a step whose worker died is re-queued while its attempt budget lasts |
|
|
205
|
+
| 💓 | **Heartbeats** — a long step proves it is alive, so a dead one is caught in seconds, not minutes |
|
|
197
206
|
| 🔭 | **Observability** — lifecycle events (run history) + per-step spans, both pluggable |
|
|
198
207
|
| 🤖 | **AI add-on** — instrumented models, token/cost capture, quota, daily rollups |
|
|
199
208
|
| 🧱 | **Pluggable everything** — `WorkflowStore`, `Dispatcher`, `StepGate`, `FlowObserver`, hooks |
|
|
@@ -239,9 +248,13 @@ Full docs — concepts, every feature, production wiring and the API reference
|
|
|
239
248
|
| [Defining steps](https://octabits-io.github.io/octaflow/core/defining-steps/) | `defineStep` and its variants |
|
|
240
249
|
| [Retry & timeout](https://octabits-io.github.io/octaflow/core/retry-and-timeout/) | attempt budgets, backoff, and how a failure is classified |
|
|
241
250
|
| [Fan-out & map](https://octabits-io.github.io/octaflow/core/fan-out-and-map/) | one child step per item of a runtime list |
|
|
251
|
+
| [Branching](https://octabits-io.github.io/octaflow/core/branching/) | `when` guards and join rules — if/else over a static DAG |
|
|
242
252
|
| [Signals](https://octabits-io.github.io/octaflow/core/signals/) · [Sub-workflows](https://octabits-io.github.io/octaflow/core/sub-workflows/) · [Saga](https://octabits-io.github.io/octaflow/core/saga-compensation/) | suspend, nest, and roll back |
|
|
253
|
+
| [Deadlines](https://octabits-io.github.io/octaflow/core/deadlines/) | budgets for a suspended step and for a whole run |
|
|
254
|
+
| [Heartbeats](https://octabits-io.github.io/octaflow/core/heartbeats/) | liveness for long steps, and interrupting one that was cancelled |
|
|
243
255
|
| [Postgres & pg-boss](https://octabits-io.github.io/octaflow/running/postgres-and-pg-boss/) | production wiring, workers, DLQ, cron |
|
|
244
|
-
| [
|
|
256
|
+
| [Cancellation & recovery](https://octabits-io.github.io/octaflow/running/cancellation-and-recovery/) | cancelling a run, sweeping steps a crash left behind, and retrying a failed run |
|
|
257
|
+
| [Observability](https://octabits-io.github.io/octaflow/running/observability/) · [Live progress](https://octabits-io.github.io/octaflow/running/live-progress/) | lifecycle events, spans, and streaming them to a browser |
|
|
245
258
|
| [The AI add-on](https://octabits-io.github.io/octaflow/extending/ai/) | token/cost capture, quota, usage rollups |
|
|
246
259
|
| [Extending](https://octabits-io.github.io/octaflow/extending/interfaces/) | custom stores, dispatchers and gates |
|
|
247
260
|
| [API reference](https://octabits-io.github.io/octaflow/reference/api/) | every export, by entry point |
|
|
@@ -267,8 +280,11 @@ Runnable, focused examples live in [`examples/`](./examples) — see [`examples/
|
|
|
267
280
|
| 11 | `11-observability.ts` | observer events + tracer spans |
|
|
268
281
|
| 12 | `12-postgres-pgboss-production.ts` | full pg store + gate + event sink + pg-boss + cron |
|
|
269
282
|
| 13 | `13-ai-workflow.ts` | AI add-on (instrumented model + cost) |
|
|
283
|
+
| 14 | `14-live-progress.ts` | `FlowObserver` → SSE fan-out (build your own dashboard) |
|
|
284
|
+
| 15 | `15-conditional-branching.ts` | `when` guards + a `join: 'any'` convergence |
|
|
285
|
+
| 16 | `16-deadlines-and-retry.ts` | wait deadlines, run deadlines, `retryWorkflow`, heartbeats |
|
|
270
286
|
|
|
271
|
-
The in-memory examples (01–11) share a small driver, [`examples/runtime.ts`](./examples/runtime.ts),
|
|
287
|
+
The in-memory examples (01–11, 14–16) share a small driver, [`examples/runtime.ts`](./examples/runtime.ts),
|
|
272
288
|
that builds an engine over the in-memory store and an in-process queue you drain.
|
|
273
289
|
|
|
274
290
|
---
|
package/dist/ai/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { E as RetryPolicy, g as TypedWorkflow, h as TypedStepContext, m as TypedStep, nt as Result, tt as FlowErrorShape } from "../index-BURAYZui.js";
|
|
2
2
|
import { n as Logger } from "../logger-BCus3AOa.js";
|
|
3
|
-
import { i as BuildStepContextArgs, o as WorkflowHooks } from "../hooks-
|
|
3
|
+
import { i as BuildStepContextArgs, o as WorkflowHooks } from "../hooks-BdxIcc92.js";
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
import { EmbeddingModelV4, LanguageModelV4 } from "@ai-sdk/provider";
|
|
6
6
|
//#region src/ai/instrumented-model.d.ts
|
package/dist/ai/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { S as ok, b as noopLogger, f as defineStep, l as buildWorkflow, x as err } from "../core-
|
|
1
|
+
import { S as ok, b as noopLogger, f as defineStep, l as buildWorkflow, x as err } from "../core-DNWPwFaE.js";
|
|
2
2
|
import { wrapEmbeddingModel, wrapLanguageModel } from "ai";
|
|
3
3
|
//#region src/ai/instrumented-model.ts
|
|
4
4
|
function createUsageAccumulator() {
|
|
@@ -171,33 +171,61 @@ const emptyObjectSchema = z.object({});
|
|
|
171
171
|
* Validation errors are non-retryable (a schema mismatch is a programming error).
|
|
172
172
|
*/
|
|
173
173
|
function defineStep(config) {
|
|
174
|
-
const { type, workflowInputSchema, outputSchema, dependencies, handler, retry, isRetryable, timeoutMs, delayMs, waitForEvent } = config;
|
|
174
|
+
const { type, workflowInputSchema, outputSchema, dependencies, handler, retry, isRetryable, timeoutMs, delayMs, waitForEvent, join } = config;
|
|
175
175
|
const deps = dependencies ?? {};
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
176
|
+
/**
|
|
177
|
+
* Classify a thrown error. Precedence: an explicit marker on the error
|
|
178
|
+
* (following `cause`), then this step's predicate, then the default
|
|
179
|
+
* classifier. `retryableFrom` records which one answered, so the engine's
|
|
180
|
+
* `defaultRetryable` can override a guess without overriding a decision
|
|
181
|
+
* someone actually made.
|
|
182
|
+
*/
|
|
183
|
+
const toStepError = (error) => {
|
|
184
|
+
const message = error instanceof Error ? error.message : "Unknown step error";
|
|
185
|
+
const marked = explicitRetryability(error);
|
|
186
|
+
return {
|
|
187
|
+
key: "step_error",
|
|
188
|
+
message,
|
|
189
|
+
retryable: marked ?? (isRetryable ? isRetryable(error) : isRetryableError(error)),
|
|
190
|
+
retryableFrom: marked !== void 0 ? "explicit" : isRetryable ? "predicate" : "heuristic"
|
|
191
|
+
};
|
|
192
|
+
};
|
|
193
|
+
/**
|
|
194
|
+
* Phases 1–3 of the pipeline: validate the workflow input, validate each
|
|
195
|
+
* dependency's output, and assemble the typed context. Shared so the `when`
|
|
196
|
+
* guard decides on exactly the same values the handler would receive.
|
|
197
|
+
*/
|
|
198
|
+
const buildTypedContext = (ctx) => {
|
|
199
|
+
const inputResult = workflowInputSchema.safeParse(ctx.workflowInput);
|
|
200
|
+
if (!inputResult.success) return {
|
|
201
|
+
ok: false,
|
|
202
|
+
error: {
|
|
203
|
+
key: "step_error",
|
|
204
|
+
message: `[${type}] Invalid workflow input: ${inputResult.error.message}`,
|
|
205
|
+
retryable: false
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
const parsedDeps = {};
|
|
209
|
+
for (const [depKey, depStep] of Object.entries(deps)) {
|
|
210
|
+
const raw = ctx.dependencyOutputs[depKey];
|
|
211
|
+
if (join === "any" && raw === void 0) {
|
|
212
|
+
parsedDeps[depKey] = void 0;
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
const depResult = depStep.outputSchema.safeParse(raw);
|
|
216
|
+
if (!depResult.success) return {
|
|
180
217
|
ok: false,
|
|
181
218
|
error: {
|
|
182
219
|
key: "step_error",
|
|
183
|
-
message: `[${type}] Invalid
|
|
220
|
+
message: `[${type}] Invalid dependency output '${depKey}': ${depResult.error.message}`,
|
|
184
221
|
retryable: false
|
|
185
222
|
}
|
|
186
223
|
};
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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 = {
|
|
224
|
+
parsedDeps[depKey] = depResult.data;
|
|
225
|
+
}
|
|
226
|
+
return {
|
|
227
|
+
ok: true,
|
|
228
|
+
value: {
|
|
201
229
|
workflowId: ctx.workflowId,
|
|
202
230
|
stepId: ctx.stepId,
|
|
203
231
|
stepKey: ctx.stepKey,
|
|
@@ -206,9 +234,16 @@ function defineStep(config) {
|
|
|
206
234
|
stepInput: ctx.stepInput,
|
|
207
235
|
deps: parsedDeps,
|
|
208
236
|
signal: ctx.signal,
|
|
237
|
+
heartbeat: ctx.heartbeat,
|
|
209
238
|
context: ctx.context
|
|
210
|
-
}
|
|
211
|
-
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
};
|
|
242
|
+
const wrappedHandler = async (ctx) => {
|
|
243
|
+
try {
|
|
244
|
+
const built = buildTypedContext(ctx);
|
|
245
|
+
if (!built.ok) return built;
|
|
246
|
+
const output = await handler(built.value);
|
|
212
247
|
const outputResult = outputSchema.safeParse(output);
|
|
213
248
|
if (!outputResult.success) return {
|
|
214
249
|
ok: false,
|
|
@@ -223,19 +258,31 @@ function defineStep(config) {
|
|
|
223
258
|
value: outputResult.data
|
|
224
259
|
};
|
|
225
260
|
} catch (error) {
|
|
226
|
-
const message = error instanceof Error ? error.message : "Unknown step error";
|
|
227
|
-
const marked = explicitRetryability(error);
|
|
228
261
|
return {
|
|
229
262
|
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
|
-
}
|
|
263
|
+
error: toStepError(error)
|
|
236
264
|
};
|
|
237
265
|
}
|
|
238
266
|
};
|
|
267
|
+
let wrappedCondition;
|
|
268
|
+
if (config.when) {
|
|
269
|
+
const when = config.when;
|
|
270
|
+
wrappedCondition = async (ctx) => {
|
|
271
|
+
try {
|
|
272
|
+
const built = buildTypedContext(ctx);
|
|
273
|
+
if (!built.ok) return built;
|
|
274
|
+
return {
|
|
275
|
+
ok: true,
|
|
276
|
+
value: await when(built.value) === true
|
|
277
|
+
};
|
|
278
|
+
} catch (error) {
|
|
279
|
+
return {
|
|
280
|
+
ok: false,
|
|
281
|
+
error: toStepError(error)
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
};
|
|
285
|
+
}
|
|
239
286
|
let wrappedCompensate;
|
|
240
287
|
if (config.compensate) {
|
|
241
288
|
const compensate = config.compensate;
|
|
@@ -256,6 +303,7 @@ function defineStep(config) {
|
|
|
256
303
|
stepInput: ctx.stepInput,
|
|
257
304
|
deps: parsedDeps,
|
|
258
305
|
signal: ctx.signal,
|
|
306
|
+
heartbeat: ctx.heartbeat,
|
|
259
307
|
context: ctx.context,
|
|
260
308
|
output: out.success ? out.data : ctx.output
|
|
261
309
|
});
|
|
@@ -269,9 +317,14 @@ function defineStep(config) {
|
|
|
269
317
|
handler: wrappedHandler,
|
|
270
318
|
retry,
|
|
271
319
|
timeoutMs,
|
|
320
|
+
onTimeout: config.onTimeout,
|
|
321
|
+
heartbeatTimeoutMs: config.heartbeatTimeoutMs,
|
|
322
|
+
heartbeat: config.heartbeat,
|
|
272
323
|
delayMs,
|
|
273
324
|
waitForEvent,
|
|
274
|
-
compensate: wrappedCompensate
|
|
325
|
+
compensate: wrappedCompensate,
|
|
326
|
+
condition: wrappedCondition,
|
|
327
|
+
join
|
|
275
328
|
};
|
|
276
329
|
}
|
|
277
330
|
/**
|
|
@@ -281,6 +334,21 @@ function defineStep(config) {
|
|
|
281
334
|
* (validated by dependents against `outputSchema`), then the DAG advances.
|
|
282
335
|
*
|
|
283
336
|
* Use for human-in-the-loop approvals, webhooks, or any async external dependency.
|
|
337
|
+
*
|
|
338
|
+
* Give it a `timeoutMs` so the wait cannot last forever. What the expiry does is
|
|
339
|
+
* `onTimeout`'s call: `'fail'` (the default) ends the run, while `{ output }`
|
|
340
|
+
* completes the step with a stand-in answer and lets the DAG carry on — which,
|
|
341
|
+
* paired with a `when` guard downstream, is how "approve within 48 hours,
|
|
342
|
+
* otherwise escalate" is expressed:
|
|
343
|
+
*
|
|
344
|
+
* ```ts
|
|
345
|
+
* const approval = defineWaitStep({
|
|
346
|
+
* type: 'await-approval',
|
|
347
|
+
* outputSchema: z.object({ approved: z.boolean() }),
|
|
348
|
+
* timeoutMs: 48 * 60 * 60 * 1000,
|
|
349
|
+
* onTimeout: { output: { approved: false } },
|
|
350
|
+
* });
|
|
351
|
+
* ```
|
|
284
352
|
*/
|
|
285
353
|
function defineWaitStep(config) {
|
|
286
354
|
return defineStep({
|
|
@@ -289,6 +357,10 @@ function defineWaitStep(config) {
|
|
|
289
357
|
outputSchema: config.outputSchema,
|
|
290
358
|
dependencies: config.dependencies,
|
|
291
359
|
waitForEvent: true,
|
|
360
|
+
timeoutMs: config.timeoutMs,
|
|
361
|
+
onTimeout: config.onTimeout,
|
|
362
|
+
when: config.when,
|
|
363
|
+
join: config.join,
|
|
292
364
|
handler: async () => ({})
|
|
293
365
|
});
|
|
294
366
|
}
|
|
@@ -308,6 +380,7 @@ function defineMapStep(config) {
|
|
|
308
380
|
workflowInputSchema: config.workflowInputSchema,
|
|
309
381
|
outputSchema: z.object({ items: z.array(z.unknown()) }),
|
|
310
382
|
dependencies: config.dependencies,
|
|
383
|
+
when: config.when,
|
|
311
384
|
handler: async (ctx) => ({ items: await config.items(ctx) })
|
|
312
385
|
});
|
|
313
386
|
const child = defineStep({
|
|
@@ -317,6 +390,7 @@ function defineMapStep(config) {
|
|
|
317
390
|
retry: config.itemRetry,
|
|
318
391
|
isRetryable: config.itemIsRetryable,
|
|
319
392
|
timeoutMs: config.itemTimeoutMs,
|
|
393
|
+
heartbeatTimeoutMs: config.itemHeartbeatTimeoutMs,
|
|
320
394
|
handler: async (ctx) => config.each(ctx.stepInput.item, {
|
|
321
395
|
index: Number(ctx.stepInput.index ?? 0),
|
|
322
396
|
context: ctx.context,
|
|
@@ -333,7 +407,8 @@ function defineMapStep(config) {
|
|
|
333
407
|
type: childType,
|
|
334
408
|
handler: child.handler,
|
|
335
409
|
retry: child.retry,
|
|
336
|
-
timeoutMs: child.timeoutMs
|
|
410
|
+
timeoutMs: child.timeoutMs,
|
|
411
|
+
heartbeatTimeoutMs: child.heartbeatTimeoutMs
|
|
337
412
|
}
|
|
338
413
|
};
|
|
339
414
|
}
|
|
@@ -355,6 +430,7 @@ function defineSubWorkflowStep(config) {
|
|
|
355
430
|
workflowInputSchema: config.workflowInputSchema,
|
|
356
431
|
outputSchema: recordSchema,
|
|
357
432
|
dependencies: config.dependencies,
|
|
433
|
+
when: config.when,
|
|
358
434
|
handler: async (ctx) => config.input(ctx)
|
|
359
435
|
}),
|
|
360
436
|
outputSchema: config.outputSchema ?? recordSchema,
|
|
@@ -409,18 +485,24 @@ function buildWorkflow(config) {
|
|
|
409
485
|
registry.register(step.type, step.handler, {
|
|
410
486
|
retry: step.retry,
|
|
411
487
|
timeoutMs: step.timeoutMs,
|
|
488
|
+
onTimeout: step.onTimeout,
|
|
489
|
+
heartbeatTimeoutMs: step.heartbeatTimeoutMs,
|
|
490
|
+
heartbeat: step.heartbeat,
|
|
412
491
|
delayMs: step.delayMs,
|
|
413
492
|
waitForEvent: step.waitForEvent,
|
|
414
493
|
map: step.map,
|
|
415
494
|
childType: step.childType,
|
|
416
495
|
subWorkflowDefinition: step.subWorkflowDefinition,
|
|
417
|
-
compensate: step.compensate
|
|
496
|
+
compensate: step.compensate,
|
|
497
|
+
condition: step.condition,
|
|
498
|
+
join: step.join
|
|
418
499
|
});
|
|
419
500
|
if (step.childRegistration) {
|
|
420
501
|
const c = step.childRegistration;
|
|
421
502
|
registry.register(c.type, c.handler, {
|
|
422
503
|
retry: c.retry,
|
|
423
|
-
timeoutMs: c.timeoutMs
|
|
504
|
+
timeoutMs: c.timeoutMs,
|
|
505
|
+
heartbeatTimeoutMs: c.heartbeatTimeoutMs
|
|
424
506
|
});
|
|
425
507
|
}
|
|
426
508
|
if (step.subWorkflowRegister) step.subWorkflowRegister(registry);
|
|
@@ -441,7 +523,7 @@ function buildWorkflow(config) {
|
|
|
441
523
|
* The engine's records ({@link WorkflowRecord}, {@link StepRecord}) are the
|
|
442
524
|
* store's data model: they carry fields that must not leak onto a public API
|
|
443
525
|
* (`partitionKey`, `idempotencyKey`, sub-workflow linkage, `metadata`, retry
|
|
444
|
-
* `attempts
|
|
526
|
+
* `attempts`, heartbeats) and step statuses that are engine mechanics rather than display
|
|
445
527
|
* states. Serving them verbatim also couples the consumer's API contract to
|
|
446
528
|
* flow's record shape. This module owns the boundary instead:
|
|
447
529
|
*
|
|
@@ -531,8 +613,8 @@ const PUBLIC_WORKFLOW_SCHEMA = z.object({
|
|
|
531
613
|
});
|
|
532
614
|
/**
|
|
533
615
|
* Project a step record to its public view. Drops `workflowId` (redundant
|
|
534
|
-
* under the parent), `metadata`, `attempts`, and `parentStepId`;
|
|
535
|
-
* status.
|
|
616
|
+
* under the parent), `metadata`, `attempts`, `heartbeatAt`, and `parentStepId`;
|
|
617
|
+
* folds the status.
|
|
536
618
|
*/
|
|
537
619
|
function toPublicStep(step) {
|
|
538
620
|
return {
|
|
@@ -1,11 +1,24 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { L as StepId, W as WorkflowId, nt as Result, tt as FlowErrorShape } from "./index-BURAYZui.js";
|
|
2
2
|
//#region src/core/dispatcher.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* What a step job is for. A queue carries two kinds of work for a step, and
|
|
5
|
+
* they differ only in this field:
|
|
6
|
+
*
|
|
7
|
+
* - `'execute'` (default) — run the step. Every job before wait deadlines
|
|
8
|
+
* existed was this, which is why it is also what a payload without a `kind`
|
|
9
|
+
* means: an in-flight job enqueued by an older version still routes correctly.
|
|
10
|
+
* - `'timeout'` — the step's wait budget has elapsed; settle it if it is still
|
|
11
|
+
* suspended. Delivered by the same durable delay that powers a sleep step.
|
|
12
|
+
*/
|
|
13
|
+
type DispatchKind = 'execute' | 'timeout';
|
|
3
14
|
/** Payload handed to the dispatcher to schedule a single step for execution. */
|
|
4
15
|
interface DispatchStepPayload {
|
|
5
16
|
workflowId: WorkflowId;
|
|
6
17
|
stepId: StepId;
|
|
7
18
|
stepKey: string;
|
|
8
19
|
stepType: string;
|
|
20
|
+
/** Defaults to `'execute'` when absent. */
|
|
21
|
+
kind?: DispatchKind;
|
|
9
22
|
}
|
|
10
23
|
interface EnqueueOptions {
|
|
11
24
|
/**
|
|
@@ -34,4 +47,4 @@ interface Dispatcher {
|
|
|
34
47
|
enqueueStepIn?(handle: unknown, payload: DispatchStepPayload, options?: EnqueueOptions): Promise<Result<void, FlowErrorShape>>;
|
|
35
48
|
}
|
|
36
49
|
//#endregion
|
|
37
|
-
export {
|
|
50
|
+
export { EnqueueOptions as i, DispatchStepPayload as n, Dispatcher as r, DispatchKind as t };
|
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import "../index-
|
|
1
|
+
import "../index-BURAYZui.js";
|
|
2
2
|
import { n as Logger } from "../logger-BCus3AOa.js";
|
|
3
|
-
import {
|
|
3
|
+
import { r as Dispatcher } from "../dispatcher-Cbb-KjY_.js";
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
import { PgBoss } from "pg-boss";
|
|
6
6
|
//#region src/dispatcher-pgboss/payload.d.ts
|
|
7
7
|
/**
|
|
8
8
|
* The wire payload for a step job. It is the flow-core `DispatchStepPayload`
|
|
9
9
|
* plus the `partitionKey` — the worker needs the partition to reconstruct a
|
|
10
|
-
* partition-scoped engine before
|
|
10
|
+
* partition-scoped engine before handing the job to `engine.handleStepJob`.
|
|
11
11
|
*/
|
|
12
12
|
declare const WIRE_STEP_PAYLOAD_SCHEMA: z.ZodObject<{
|
|
13
13
|
partitionKey: z.ZodString;
|
|
@@ -15,6 +15,10 @@ declare const WIRE_STEP_PAYLOAD_SCHEMA: z.ZodObject<{
|
|
|
15
15
|
stepId: z.ZodNumber;
|
|
16
16
|
stepKey: z.ZodString;
|
|
17
17
|
stepType: z.ZodString;
|
|
18
|
+
kind: z.ZodDefault<z.ZodEnum<{
|
|
19
|
+
execute: "execute";
|
|
20
|
+
timeout: "timeout";
|
|
21
|
+
}>>;
|
|
18
22
|
}, z.core.$strip>;
|
|
19
23
|
type WireStepPayload = z.infer<typeof WIRE_STEP_PAYLOAD_SCHEMA>;
|
|
20
24
|
interface StepQueueConfig {
|
|
@@ -29,8 +33,15 @@ declare const DEFAULT_STEP_QUEUE_CONFIG: Required<StepQueueConfig>;
|
|
|
29
33
|
/**
|
|
30
34
|
* The wire payload for a scheduled (or ad-hoc) workflow **start**. Carries the
|
|
31
35
|
* partition plus what `engine.startWorkflow` needs; the host resolves `workflowType`
|
|
32
|
-
* to a definition.
|
|
33
|
-
*
|
|
36
|
+
* to a definition.
|
|
37
|
+
*
|
|
38
|
+
* Two mutually exclusive ways to ask for start idempotency, because a cron schedule
|
|
39
|
+
* stores its payload **once** and pg-boss redelivers that same payload on every tick:
|
|
40
|
+
*
|
|
41
|
+
* - `idempotencyKeyPrefix` — the usual choice. The start worker appends the job id,
|
|
42
|
+
* so each tick gets a distinct key while a redelivery of the *same* tick reuses it.
|
|
43
|
+
* - `idempotencyKey` — used verbatim. On a schedule this means "start exactly one
|
|
44
|
+
* workflow, ever"; every later tick returns the first one.
|
|
34
45
|
*/
|
|
35
46
|
declare const WIRE_START_PAYLOAD_SCHEMA: z.ZodObject<{
|
|
36
47
|
partitionKey: z.ZodString;
|
|
@@ -38,8 +49,22 @@ declare const WIRE_START_PAYLOAD_SCHEMA: z.ZodObject<{
|
|
|
38
49
|
input: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
39
50
|
entityRef: z.ZodOptional<z.ZodString>;
|
|
40
51
|
idempotencyKey: z.ZodOptional<z.ZodString>;
|
|
52
|
+
idempotencyKeyPrefix: z.ZodOptional<z.ZodString>;
|
|
41
53
|
}, z.core.$strip>;
|
|
42
54
|
type WireStartPayload = z.infer<typeof WIRE_START_PAYLOAD_SCHEMA>;
|
|
55
|
+
/**
|
|
56
|
+
* What a {@link WireStartPayload} becomes at delivery time: the same fields, plus the
|
|
57
|
+
* pg-boss job id and the **resolved** `idempotencyKey` to hand `engine.startWorkflow`.
|
|
58
|
+
*
|
|
59
|
+
* `idempotencyKey` is verbatim when the payload set one, `${idempotencyKeyPrefix}:${jobId}`
|
|
60
|
+
* when it set a prefix, and `undefined` when it set neither.
|
|
61
|
+
*/
|
|
62
|
+
interface StartJobContext extends WireStartPayload {
|
|
63
|
+
/** pg-boss job id — unique per cron tick, stable across that job's retries. */
|
|
64
|
+
jobId: string;
|
|
65
|
+
}
|
|
66
|
+
/** Resolve the per-delivery idempotency key. Explicit key wins over a prefix. */
|
|
67
|
+
declare function resolveStartIdempotencyKey(payload: WireStartPayload, jobId: string): string | undefined;
|
|
43
68
|
//#endregion
|
|
44
69
|
//#region src/dispatcher-pgboss/queue.d.ts
|
|
45
70
|
/**
|
|
@@ -136,7 +161,20 @@ interface ScheduleStartInput {
|
|
|
136
161
|
workflowType: string;
|
|
137
162
|
input?: Record<string, unknown>;
|
|
138
163
|
entityRef?: string;
|
|
139
|
-
/**
|
|
164
|
+
/**
|
|
165
|
+
* Per-tick start idempotency. The start worker resolves this to
|
|
166
|
+
* `${idempotencyKeyPrefix}:${jobId}`, so each cron tick gets a distinct key while a
|
|
167
|
+
* redelivery of the same tick reuses it — the usual thing you want on a schedule.
|
|
168
|
+
*/
|
|
169
|
+
idempotencyKeyPrefix?: string;
|
|
170
|
+
/**
|
|
171
|
+
* Verbatim start idempotency key.
|
|
172
|
+
*
|
|
173
|
+
* A schedule stores its payload **once**, so this same key rides every tick — and
|
|
174
|
+
* start keys never expire. On a cron schedule that means **exactly one workflow,
|
|
175
|
+
* ever**; every later tick returns the first one. That is occasionally what you
|
|
176
|
+
* want ("run this once and never again"); if it isn't, use `idempotencyKeyPrefix`.
|
|
177
|
+
*/
|
|
140
178
|
idempotencyKey?: string;
|
|
141
179
|
/** Optional IANA time zone (default UTC). */
|
|
142
180
|
tz?: string;
|
|
@@ -150,8 +188,20 @@ declare function createPgBossScheduler(deps: PgBossSchedulerDeps): {
|
|
|
150
188
|
/** List the schedules registered on this queue. */
|
|
151
189
|
list(): Promise<import("pg-boss").Schedule[]>;
|
|
152
190
|
};
|
|
153
|
-
/**
|
|
154
|
-
|
|
191
|
+
/**
|
|
192
|
+
* Invoked for each start job — typically resolves the type and calls `engine.startWorkflow`.
|
|
193
|
+
*
|
|
194
|
+
* The context's `idempotencyKey` is already resolved for **this delivery** (see
|
|
195
|
+
* {@link resolveStartIdempotencyKey}), so forwarding it is the right default:
|
|
196
|
+
*
|
|
197
|
+
* ```ts
|
|
198
|
+
* await starter.start(async (job) => {
|
|
199
|
+
* const wf = workflowsByType[job.workflowType];
|
|
200
|
+
* await engine.startWorkflow(wf.definition, job.input, { idempotencyKey: job.idempotencyKey });
|
|
201
|
+
* });
|
|
202
|
+
* ```
|
|
203
|
+
*/
|
|
204
|
+
type StartJobProcessor = (payload: StartJobContext) => Promise<void>;
|
|
155
205
|
interface PgBossStartWorkerDeps {
|
|
156
206
|
boss: PgBoss;
|
|
157
207
|
queueName: string;
|
|
@@ -167,4 +217,4 @@ declare function createPgBossStartWorker(deps: PgBossStartWorkerDeps): {
|
|
|
167
217
|
stop: () => Promise<void>;
|
|
168
218
|
};
|
|
169
219
|
//#endregion
|
|
170
|
-
export { DEFAULT_STEP_QUEUE_CONFIG, DlqProcessor, PgBossDispatcherDeps, PgBossDlqWorkerDeps, PgBossSchedulerDeps, PgBossStartWorkerDeps, PgBossStepWorkerDeps, PgBossStepWorkerOptions, ScheduleStartInput, StartJobProcessor, StepJobProcessor, StepQueueConfig, WIRE_START_PAYLOAD_SCHEMA, WIRE_STEP_PAYLOAD_SCHEMA, WireStartPayload, WireStepPayload, createPgBossDispatcher, createPgBossDlqWorker, createPgBossScheduler, createPgBossStartWorker, createPgBossStepWorker, ensureStartQueue, ensureStepQueue };
|
|
220
|
+
export { DEFAULT_STEP_QUEUE_CONFIG, DlqProcessor, PgBossDispatcherDeps, PgBossDlqWorkerDeps, PgBossSchedulerDeps, PgBossStartWorkerDeps, PgBossStepWorkerDeps, PgBossStepWorkerOptions, ScheduleStartInput, StartJobContext, StartJobProcessor, StepJobProcessor, StepQueueConfig, WIRE_START_PAYLOAD_SCHEMA, WIRE_STEP_PAYLOAD_SCHEMA, WireStartPayload, WireStepPayload, createPgBossDispatcher, createPgBossDlqWorker, createPgBossScheduler, createPgBossStartWorker, createPgBossStepWorker, ensureStartQueue, ensureStepQueue, resolveStartIdempotencyKey };
|
|
@@ -3,14 +3,20 @@ import { z } from "zod";
|
|
|
3
3
|
/**
|
|
4
4
|
* The wire payload for a step job. It is the flow-core `DispatchStepPayload`
|
|
5
5
|
* plus the `partitionKey` — the worker needs the partition to reconstruct a
|
|
6
|
-
* partition-scoped engine before
|
|
6
|
+
* partition-scoped engine before handing the job to `engine.handleStepJob`.
|
|
7
7
|
*/
|
|
8
8
|
const WIRE_STEP_PAYLOAD_SCHEMA = z.object({
|
|
9
9
|
partitionKey: z.string().min(1),
|
|
10
10
|
workflowId: z.number().int().positive(),
|
|
11
11
|
stepId: z.number().int().positive(),
|
|
12
12
|
stepKey: z.string().min(1),
|
|
13
|
-
stepType: z.string().min(1)
|
|
13
|
+
stepType: z.string().min(1),
|
|
14
|
+
/**
|
|
15
|
+
* What the job asks for: run the step, or settle it because its wait deadline
|
|
16
|
+
* elapsed. Defaulted rather than required, so a job enqueued before wait
|
|
17
|
+
* deadlines existed still parses — and still means "run the step".
|
|
18
|
+
*/
|
|
19
|
+
kind: z.enum(["execute", "timeout"]).default("execute")
|
|
14
20
|
});
|
|
15
21
|
const DEFAULT_STEP_QUEUE_CONFIG = {
|
|
16
22
|
retryLimit: 2,
|
|
@@ -20,16 +26,31 @@ const DEFAULT_STEP_QUEUE_CONFIG = {
|
|
|
20
26
|
/**
|
|
21
27
|
* The wire payload for a scheduled (or ad-hoc) workflow **start**. Carries the
|
|
22
28
|
* partition plus what `engine.startWorkflow` needs; the host resolves `workflowType`
|
|
23
|
-
* to a definition.
|
|
24
|
-
*
|
|
29
|
+
* to a definition.
|
|
30
|
+
*
|
|
31
|
+
* Two mutually exclusive ways to ask for start idempotency, because a cron schedule
|
|
32
|
+
* stores its payload **once** and pg-boss redelivers that same payload on every tick:
|
|
33
|
+
*
|
|
34
|
+
* - `idempotencyKeyPrefix` — the usual choice. The start worker appends the job id,
|
|
35
|
+
* so each tick gets a distinct key while a redelivery of the *same* tick reuses it.
|
|
36
|
+
* - `idempotencyKey` — used verbatim. On a schedule this means "start exactly one
|
|
37
|
+
* workflow, ever"; every later tick returns the first one.
|
|
25
38
|
*/
|
|
26
39
|
const WIRE_START_PAYLOAD_SCHEMA = z.object({
|
|
27
40
|
partitionKey: z.string().min(1),
|
|
28
41
|
workflowType: z.string().min(1),
|
|
29
42
|
input: z.record(z.string(), z.unknown()).default({}),
|
|
30
43
|
entityRef: z.string().optional(),
|
|
31
|
-
|
|
44
|
+
/** Verbatim dedup key — collapses *every* delivery, including future cron ticks. */
|
|
45
|
+
idempotencyKey: z.string().optional(),
|
|
46
|
+
/** Per-tick dedup: the start worker resolves this to `${prefix}:${jobId}`. */
|
|
47
|
+
idempotencyKeyPrefix: z.string().optional()
|
|
32
48
|
});
|
|
49
|
+
/** Resolve the per-delivery idempotency key. Explicit key wins over a prefix. */
|
|
50
|
+
function resolveStartIdempotencyKey(payload, jobId) {
|
|
51
|
+
if (payload.idempotencyKey !== void 0) return payload.idempotencyKey;
|
|
52
|
+
if (payload.idempotencyKeyPrefix !== void 0) return `${payload.idempotencyKeyPrefix}:${jobId}`;
|
|
53
|
+
}
|
|
33
54
|
//#endregion
|
|
34
55
|
//#region src/dispatcher-pgboss/queue.ts
|
|
35
56
|
function dlqName(queueName) {
|
|
@@ -85,8 +106,9 @@ function createPgBossDispatcher(deps) {
|
|
|
85
106
|
async function send(payload, options, handle) {
|
|
86
107
|
try {
|
|
87
108
|
const wire = {
|
|
88
|
-
|
|
89
|
-
...payload
|
|
109
|
+
kind: "execute",
|
|
110
|
+
...payload,
|
|
111
|
+
partitionKey
|
|
90
112
|
};
|
|
91
113
|
const parsed = WIRE_STEP_PAYLOAD_SCHEMA.safeParse(wire);
|
|
92
114
|
if (!parsed.success) return {
|
|
@@ -275,7 +297,8 @@ function createPgBossScheduler(deps) {
|
|
|
275
297
|
workflowType: input.workflowType,
|
|
276
298
|
input: input.input ?? {},
|
|
277
299
|
entityRef: input.entityRef,
|
|
278
|
-
idempotencyKey: input.idempotencyKey
|
|
300
|
+
idempotencyKey: input.idempotencyKey,
|
|
301
|
+
idempotencyKeyPrefix: input.idempotencyKeyPrefix
|
|
279
302
|
};
|
|
280
303
|
await boss.schedule(queueName, input.cron, payload, {
|
|
281
304
|
key: input.key,
|
|
@@ -309,7 +332,11 @@ function createPgBossStartWorker(deps) {
|
|
|
309
332
|
logger?.error("Invalid start payload", void 0, { jobId: job.id });
|
|
310
333
|
continue;
|
|
311
334
|
}
|
|
312
|
-
await process(
|
|
335
|
+
await process({
|
|
336
|
+
...parsed.data,
|
|
337
|
+
jobId: job.id,
|
|
338
|
+
idempotencyKey: resolveStartIdempotencyKey(parsed.data, job.id)
|
|
339
|
+
});
|
|
313
340
|
}
|
|
314
341
|
});
|
|
315
342
|
}
|
|
@@ -325,4 +352,4 @@ function createPgBossStartWorker(deps) {
|
|
|
325
352
|
};
|
|
326
353
|
}
|
|
327
354
|
//#endregion
|
|
328
|
-
export { DEFAULT_STEP_QUEUE_CONFIG, WIRE_START_PAYLOAD_SCHEMA, WIRE_STEP_PAYLOAD_SCHEMA, createPgBossDispatcher, createPgBossDlqWorker, createPgBossScheduler, createPgBossStartWorker, createPgBossStepWorker, ensureStartQueue, ensureStepQueue };
|
|
355
|
+
export { DEFAULT_STEP_QUEUE_CONFIG, WIRE_START_PAYLOAD_SCHEMA, WIRE_STEP_PAYLOAD_SCHEMA, createPgBossDispatcher, createPgBossDlqWorker, createPgBossScheduler, createPgBossStartWorker, createPgBossStepWorker, ensureStartQueue, ensureStepQueue, resolveStartIdempotencyKey };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { D as StartOptions, L as StepId, R as StepRecord, U as WorkflowDefinition, W as WorkflowId, nt as Result, q as WorkflowRecord, tt as FlowErrorShape } from "./index-BURAYZui.js";
|
|
2
2
|
//#region src/core/hooks.d.ts
|
|
3
3
|
interface BeforeStartArgs {
|
|
4
4
|
definition: WorkflowDefinition;
|