octaflow 0.16.0 → 0.17.1
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 +44 -31
- 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 +7 -3
- package/dist/dispatcher-pgboss/index.js +11 -4
- 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 +13 -13
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://github.com/octabits-io/octaflow/actions/workflows/ci.yml)
|
|
4
4
|
[](https://www.npmjs.com/package/octaflow)
|
|
5
|
-
[](https://octaflow.octabits.io/)
|
|
6
6
|
[](./LICENSE)
|
|
7
7
|
[](https://www.npmjs.com/package/octaflow)
|
|
8
8
|
|
|
@@ -14,12 +14,12 @@ restart. There is
|
|
|
14
14
|
no workflow server to operate, no control plane, and no vendor — it's a library you import,
|
|
15
15
|
not a platform you adopt.
|
|
16
16
|
|
|
17
|
-
### 📖 [Read the docs →](https://octabits
|
|
17
|
+
### 📖 [Read the docs →](https://octaflow.octabits.io/)
|
|
18
18
|
|
|
19
|
-
[Quick start](https://octabits
|
|
20
|
-
[Concepts](https://octabits
|
|
21
|
-
[Postgres & pg-boss](https://octabits
|
|
22
|
-
[API reference](https://octabits
|
|
19
|
+
[Quick start](https://octaflow.octabits.io/start/quick-start/) ·
|
|
20
|
+
[Concepts](https://octaflow.octabits.io/core/concepts/) ·
|
|
21
|
+
[Postgres & pg-boss](https://octaflow.octabits.io/running/postgres-and-pg-boss/) ·
|
|
22
|
+
[API reference](https://octaflow.octabits.io/reference/api/)
|
|
23
23
|
|
|
24
24
|
```ts
|
|
25
25
|
const wf = buildWorkflow({
|
|
@@ -65,14 +65,15 @@ of what it did. Both models are durable — they trade off differently, and
|
|
|
65
65
|
|
|
66
66
|
A fuller pipeline than the snippet above — dynamic fan-out over two `map` steps at once, a
|
|
67
67
|
concurrency cap so only 2 of 6 images encode at a time, a flaky step retried, a sub-workflow,
|
|
68
|
-
a step suspended on an external event
|
|
69
|
-
|
|
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.
|
|
70
71
|
|
|
71
72
|
<p align="center">
|
|
72
|
-
<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">
|
|
73
74
|
</p>
|
|
74
75
|
|
|
75
|
-
Every line is an engine transition emitted through the [`FlowObserver`](https://octabits
|
|
76
|
+
Every line is an engine transition emitted through the [`FlowObserver`](https://octaflow.octabits.io/running/observability/) seam —
|
|
76
77
|
the same one you'd point at OpenTelemetry or an events table — not a `console.log` in a handler.
|
|
77
78
|
|
|
78
79
|
```bash
|
|
@@ -105,7 +106,7 @@ workflow is expressed**.
|
|
|
105
106
|
| **Model** | declarative — a static DAG value | imperative workflow code | imperative step functions | imperative tasks | job queue; flows are **trees** |
|
|
106
107
|
| **Fan-in / diamond deps** | yes | yes | yes | yes | no — a job can't be shared by two branches |
|
|
107
108
|
| **Inspect before running** | yes — the DAG is a value | no — the graph is the execution trace | no | no | yes — the flow tree is data |
|
|
108
|
-
| **Web dashboard** | **none** — [build one](https://octabits
|
|
109
|
+
| **Web dashboard** | **none** — [build one](https://octaflow.octabits.io/running/live-progress/) | yes | yes | yes | via third-party UIs |
|
|
109
110
|
| **Languages** | TypeScript | polyglot SDKs | TS, Python, Go, Kotlin | TS | Node (+ ports) |
|
|
110
111
|
| **Maturity** | **pre-1.0** | mature | mature | mature | mature, widely deployed |
|
|
111
112
|
|
|
@@ -119,12 +120,14 @@ that the others have earned.
|
|
|
119
120
|
Reach for something else if:
|
|
120
121
|
|
|
121
122
|
- **Your control flow is genuinely dynamic.** A declarative DAG is fixed at definition time.
|
|
122
|
-
Flow softens this
|
|
123
|
-
[
|
|
124
|
-
|
|
125
|
-
|
|
123
|
+
Flow softens this a lot — [`when` guards and joins](https://octaflow.octabits.io/core/branching/) (if/else over a static graph),
|
|
124
|
+
[`defineMapStep`](https://octaflow.octabits.io/core/fan-out-and-map/) (runtime-sized fan-out),
|
|
125
|
+
[sub-workflows](https://octaflow.octabits.io/core/sub-workflows/), and [`waitForEvent`](https://octaflow.octabits.io/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.
|
|
126
129
|
- **You want a UI out of the box.** Flow ships a wire-safe projection
|
|
127
|
-
([`toPublicWorkflow`](https://octabits
|
|
130
|
+
([`toPublicWorkflow`](https://octaflow.octabits.io/extending/http/)) and lifecycle events, not a
|
|
128
131
|
dashboard. You build it.
|
|
129
132
|
- **You need non-TypeScript workers.** The DAG and its schemas are TypeScript values.
|
|
130
133
|
- **You can't run Postgres**, or you need throughput past what a Postgres-backed queue gives you.
|
|
@@ -188,13 +191,18 @@ next to what your steps actually do.
|
|
|
188
191
|
| ⚡ | **Auto-parallelism** — dependency-free steps run concurrently; a step starts when all its deps complete |
|
|
189
192
|
| 🔁 | **Retry & timeout** — per-step `maxAttempts`, fixed/exponential backoff, wall-clock timeout |
|
|
190
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 |
|
|
191
197
|
| 🚦 | **Concurrency & rate limiting** — per-step-type caps and token buckets via a pluggable gate |
|
|
192
198
|
| ⏰ | **Cron / scheduled starts** — fire workflows on a schedule (pg-boss) |
|
|
193
199
|
| 🔑 | **Start idempotency** — a dedup key collapses double-clicks / overlapping ticks |
|
|
194
200
|
| 🗺️ | **Dynamic fan-out / map** — spawn one child step per item of a runtime-sized list |
|
|
195
|
-
| ⏸️ | **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 |
|
|
196
202
|
| 🪆 | **Sub-workflows** — a step starts a child workflow and awaits its result |
|
|
197
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 |
|
|
198
206
|
| 🔭 | **Observability** — lifecycle events (run history) + per-step spans, both pluggable |
|
|
199
207
|
| 🤖 | **AI add-on** — instrumented models, token/cost capture, quota, daily rollups |
|
|
200
208
|
| 🧱 | **Pluggable everything** — `WorkflowStore`, `Dispatcher`, `StepGate`, `FlowObserver`, hooks |
|
|
@@ -231,22 +239,25 @@ pnpm add ai @ai-sdk/provider
|
|
|
231
239
|
## Documentation
|
|
232
240
|
|
|
233
241
|
Full docs — concepts, every feature, production wiring and the API reference — live at
|
|
234
|
-
**[octabits
|
|
242
|
+
**[octaflow.octabits.io](https://octaflow.octabits.io/)**.
|
|
235
243
|
|
|
236
244
|
| | |
|
|
237
245
|
|---|---|
|
|
238
|
-
| [Quick start](https://octabits
|
|
239
|
-
| [Concepts](https://octabits
|
|
240
|
-
| [Defining steps](https://octabits
|
|
241
|
-
| [Retry & timeout](https://octabits
|
|
242
|
-
| [Fan-out & map](https://octabits
|
|
243
|
-
| [
|
|
244
|
-
| [
|
|
245
|
-
| [
|
|
246
|
-
| [
|
|
247
|
-
| [
|
|
248
|
-
| [
|
|
249
|
-
| [
|
|
246
|
+
| [Quick start](https://octaflow.octabits.io/start/quick-start/) | a runnable workflow in one file, no database |
|
|
247
|
+
| [Concepts](https://octaflow.octabits.io/core/concepts/) | step, workflow, registry, store, dispatcher, engine, partition |
|
|
248
|
+
| [Defining steps](https://octaflow.octabits.io/core/defining-steps/) | `defineStep` and its variants |
|
|
249
|
+
| [Retry & timeout](https://octaflow.octabits.io/core/retry-and-timeout/) | attempt budgets, backoff, and how a failure is classified |
|
|
250
|
+
| [Fan-out & map](https://octaflow.octabits.io/core/fan-out-and-map/) | one child step per item of a runtime list |
|
|
251
|
+
| [Branching](https://octaflow.octabits.io/core/branching/) | `when` guards and join rules — if/else over a static DAG |
|
|
252
|
+
| [Signals](https://octaflow.octabits.io/core/signals/) · [Sub-workflows](https://octaflow.octabits.io/core/sub-workflows/) · [Saga](https://octaflow.octabits.io/core/saga-compensation/) | suspend, nest, and roll back |
|
|
253
|
+
| [Deadlines](https://octaflow.octabits.io/core/deadlines/) | budgets for a suspended step and for a whole run |
|
|
254
|
+
| [Heartbeats](https://octaflow.octabits.io/core/heartbeats/) | liveness for long steps, and interrupting one that was cancelled |
|
|
255
|
+
| [Postgres & pg-boss](https://octaflow.octabits.io/running/postgres-and-pg-boss/) | production wiring, workers, DLQ, cron |
|
|
256
|
+
| [Cancellation & recovery](https://octaflow.octabits.io/running/cancellation-and-recovery/) | cancelling a run, sweeping steps a crash left behind, and retrying a failed run |
|
|
257
|
+
| [Observability](https://octaflow.octabits.io/running/observability/) · [Live progress](https://octaflow.octabits.io/running/live-progress/) | lifecycle events, spans, and streaming them to a browser |
|
|
258
|
+
| [The AI add-on](https://octaflow.octabits.io/extending/ai/) | token/cost capture, quota, usage rollups |
|
|
259
|
+
| [Extending](https://octaflow.octabits.io/extending/interfaces/) | custom stores, dispatchers and gates |
|
|
260
|
+
| [API reference](https://octaflow.octabits.io/reference/api/) | every export, by entry point |
|
|
250
261
|
|
|
251
262
|
---
|
|
252
263
|
|
|
@@ -270,8 +281,10 @@ Runnable, focused examples live in [`examples/`](./examples) — see [`examples/
|
|
|
270
281
|
| 12 | `12-postgres-pgboss-production.ts` | full pg store + gate + event sink + pg-boss + cron |
|
|
271
282
|
| 13 | `13-ai-workflow.ts` | AI add-on (instrumented model + cost) |
|
|
272
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 |
|
|
273
286
|
|
|
274
|
-
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),
|
|
275
288
|
that builds an engine over the in-memory store and an in-process queue you drain.
|
|
276
289
|
|
|
277
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 {
|
|
@@ -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,
|
|
@@ -100,8 +106,9 @@ function createPgBossDispatcher(deps) {
|
|
|
100
106
|
async function send(payload, options, handle) {
|
|
101
107
|
try {
|
|
102
108
|
const wire = {
|
|
103
|
-
|
|
104
|
-
...payload
|
|
109
|
+
kind: "execute",
|
|
110
|
+
...payload,
|
|
111
|
+
partitionKey
|
|
105
112
|
};
|
|
106
113
|
const parsed = WIRE_STEP_PAYLOAD_SCHEMA.safeParse(wire);
|
|
107
114
|
if (!parsed.success) return {
|
|
@@ -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;
|