octaflow 0.14.0 → 0.16.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 +6 -3
- package/dist/{dispatcher-BydjyrgU.d.ts → dispatcher-CnMTwfCc.d.ts} +11 -0
- package/dist/dispatcher-pgboss/index.d.ts +53 -7
- package/dist/dispatcher-pgboss/index.js +56 -9
- package/dist/index.d.ts +3 -3
- package/dist/index.js +116 -37
- package/dist/{observability-BsrvbhDr.d.ts → observability-3tcmRwIw.d.ts} +26 -1
- package/dist/store-pg/index.d.ts +1 -1
- package/dist/store-pg/index.js +20 -0
- package/package.json +1 -1
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
|
|
|
@@ -104,7 +105,7 @@ workflow is expressed**.
|
|
|
104
105
|
| **Model** | declarative — a static DAG value | imperative workflow code | imperative step functions | imperative tasks | job queue; flows are **trees** |
|
|
105
106
|
| **Fan-in / diamond deps** | yes | yes | yes | yes | no — a job can't be shared by two branches |
|
|
106
107
|
| **Inspect before running** | yes — the DAG is a value | no — the graph is the execution trace | no | no | yes — the flow tree is data |
|
|
107
|
-
| **Web dashboard** | **none** — build one
|
|
108
|
+
| **Web dashboard** | **none** — [build one](https://octabits-io.github.io/octaflow/running/live-progress/) | yes | yes | yes | via third-party UIs |
|
|
108
109
|
| **Languages** | TypeScript | polyglot SDKs | TS, Python, Go, Kotlin | TS | Node (+ ports) |
|
|
109
110
|
| **Maturity** | **pre-1.0** | mature | mature | mature | mature, widely deployed |
|
|
110
111
|
|
|
@@ -241,7 +242,8 @@ Full docs — concepts, every feature, production wiring and the API reference
|
|
|
241
242
|
| [Fan-out & map](https://octabits-io.github.io/octaflow/core/fan-out-and-map/) | one child step per item of a runtime list |
|
|
242
243
|
| [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 |
|
|
243
244
|
| [Postgres & pg-boss](https://octabits-io.github.io/octaflow/running/postgres-and-pg-boss/) | production wiring, workers, DLQ, cron |
|
|
244
|
-
| [
|
|
245
|
+
| [Cancellation & recovery](https://octabits-io.github.io/octaflow/running/cancellation-and-recovery/) | cancelling a run, and sweeping steps a crash left behind |
|
|
246
|
+
| [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
247
|
| [The AI add-on](https://octabits-io.github.io/octaflow/extending/ai/) | token/cost capture, quota, usage rollups |
|
|
246
248
|
| [Extending](https://octabits-io.github.io/octaflow/extending/interfaces/) | custom stores, dispatchers and gates |
|
|
247
249
|
| [API reference](https://octabits-io.github.io/octaflow/reference/api/) | every export, by entry point |
|
|
@@ -267,6 +269,7 @@ Runnable, focused examples live in [`examples/`](./examples) — see [`examples/
|
|
|
267
269
|
| 11 | `11-observability.ts` | observer events + tracer spans |
|
|
268
270
|
| 12 | `12-postgres-pgboss-production.ts` | full pg store + gate + event sink + pg-boss + cron |
|
|
269
271
|
| 13 | `13-ai-workflow.ts` | AI add-on (instrumented model + cost) |
|
|
272
|
+
| 14 | `14-live-progress.ts` | `FlowObserver` → SSE fan-out (build your own dashboard) |
|
|
270
273
|
|
|
271
274
|
The in-memory examples (01–11) share a small driver, [`examples/runtime.ts`](./examples/runtime.ts),
|
|
272
275
|
that builds an engine over the in-memory store and an in-process queue you drain.
|
|
@@ -21,6 +21,17 @@ interface EnqueueOptions {
|
|
|
21
21
|
*/
|
|
22
22
|
interface Dispatcher {
|
|
23
23
|
enqueueStep(payload: DispatchStepPayload, options?: EnqueueOptions): Promise<Result<void, FlowErrorShape>>;
|
|
24
|
+
/**
|
|
25
|
+
* **Optional capability.** Enqueue inside the caller's store transaction, so
|
|
26
|
+
* the job and the state change that produced it commit together.
|
|
27
|
+
*
|
|
28
|
+
* `handle` comes from `WorkflowStore.runInTransaction` and is opaque to the
|
|
29
|
+
* engine — the store and dispatcher agree on its meaning. Implement this only
|
|
30
|
+
* when the queue lives in the same database as the store (pg-boss on the same
|
|
31
|
+
* Postgres does; SQS or a separate Redis cannot). Without it the engine writes
|
|
32
|
+
* state and then enqueues, which is at-least-once with a crash window.
|
|
33
|
+
*/
|
|
34
|
+
enqueueStepIn?(handle: unknown, payload: DispatchStepPayload, options?: EnqueueOptions): Promise<Result<void, FlowErrorShape>>;
|
|
24
35
|
}
|
|
25
36
|
//#endregion
|
|
26
37
|
export { Dispatcher as n, EnqueueOptions as r, DispatchStepPayload as t };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import "../index-D_fOYfmf.js";
|
|
2
2
|
import { n as Logger } from "../logger-BCus3AOa.js";
|
|
3
|
-
import { n as Dispatcher } from "../dispatcher-
|
|
3
|
+
import { n as Dispatcher } from "../dispatcher-CnMTwfCc.js";
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
import { PgBoss } from "pg-boss";
|
|
6
6
|
//#region src/dispatcher-pgboss/payload.d.ts
|
|
@@ -29,8 +29,15 @@ declare const DEFAULT_STEP_QUEUE_CONFIG: Required<StepQueueConfig>;
|
|
|
29
29
|
/**
|
|
30
30
|
* The wire payload for a scheduled (or ad-hoc) workflow **start**. Carries the
|
|
31
31
|
* partition plus what `engine.startWorkflow` needs; the host resolves `workflowType`
|
|
32
|
-
* to a definition.
|
|
33
|
-
*
|
|
32
|
+
* to a definition.
|
|
33
|
+
*
|
|
34
|
+
* Two mutually exclusive ways to ask for start idempotency, because a cron schedule
|
|
35
|
+
* stores its payload **once** and pg-boss redelivers that same payload on every tick:
|
|
36
|
+
*
|
|
37
|
+
* - `idempotencyKeyPrefix` — the usual choice. The start worker appends the job id,
|
|
38
|
+
* so each tick gets a distinct key while a redelivery of the *same* tick reuses it.
|
|
39
|
+
* - `idempotencyKey` — used verbatim. On a schedule this means "start exactly one
|
|
40
|
+
* workflow, ever"; every later tick returns the first one.
|
|
34
41
|
*/
|
|
35
42
|
declare const WIRE_START_PAYLOAD_SCHEMA: z.ZodObject<{
|
|
36
43
|
partitionKey: z.ZodString;
|
|
@@ -38,8 +45,22 @@ declare const WIRE_START_PAYLOAD_SCHEMA: z.ZodObject<{
|
|
|
38
45
|
input: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
39
46
|
entityRef: z.ZodOptional<z.ZodString>;
|
|
40
47
|
idempotencyKey: z.ZodOptional<z.ZodString>;
|
|
48
|
+
idempotencyKeyPrefix: z.ZodOptional<z.ZodString>;
|
|
41
49
|
}, z.core.$strip>;
|
|
42
50
|
type WireStartPayload = z.infer<typeof WIRE_START_PAYLOAD_SCHEMA>;
|
|
51
|
+
/**
|
|
52
|
+
* What a {@link WireStartPayload} becomes at delivery time: the same fields, plus the
|
|
53
|
+
* pg-boss job id and the **resolved** `idempotencyKey` to hand `engine.startWorkflow`.
|
|
54
|
+
*
|
|
55
|
+
* `idempotencyKey` is verbatim when the payload set one, `${idempotencyKeyPrefix}:${jobId}`
|
|
56
|
+
* when it set a prefix, and `undefined` when it set neither.
|
|
57
|
+
*/
|
|
58
|
+
interface StartJobContext extends WireStartPayload {
|
|
59
|
+
/** pg-boss job id — unique per cron tick, stable across that job's retries. */
|
|
60
|
+
jobId: string;
|
|
61
|
+
}
|
|
62
|
+
/** Resolve the per-delivery idempotency key. Explicit key wins over a prefix. */
|
|
63
|
+
declare function resolveStartIdempotencyKey(payload: WireStartPayload, jobId: string): string | undefined;
|
|
43
64
|
//#endregion
|
|
44
65
|
//#region src/dispatcher-pgboss/queue.d.ts
|
|
45
66
|
/**
|
|
@@ -136,7 +157,20 @@ interface ScheduleStartInput {
|
|
|
136
157
|
workflowType: string;
|
|
137
158
|
input?: Record<string, unknown>;
|
|
138
159
|
entityRef?: string;
|
|
139
|
-
/**
|
|
160
|
+
/**
|
|
161
|
+
* Per-tick start idempotency. The start worker resolves this to
|
|
162
|
+
* `${idempotencyKeyPrefix}:${jobId}`, so each cron tick gets a distinct key while a
|
|
163
|
+
* redelivery of the same tick reuses it — the usual thing you want on a schedule.
|
|
164
|
+
*/
|
|
165
|
+
idempotencyKeyPrefix?: string;
|
|
166
|
+
/**
|
|
167
|
+
* Verbatim start idempotency key.
|
|
168
|
+
*
|
|
169
|
+
* A schedule stores its payload **once**, so this same key rides every tick — and
|
|
170
|
+
* start keys never expire. On a cron schedule that means **exactly one workflow,
|
|
171
|
+
* ever**; every later tick returns the first one. That is occasionally what you
|
|
172
|
+
* want ("run this once and never again"); if it isn't, use `idempotencyKeyPrefix`.
|
|
173
|
+
*/
|
|
140
174
|
idempotencyKey?: string;
|
|
141
175
|
/** Optional IANA time zone (default UTC). */
|
|
142
176
|
tz?: string;
|
|
@@ -150,8 +184,20 @@ declare function createPgBossScheduler(deps: PgBossSchedulerDeps): {
|
|
|
150
184
|
/** List the schedules registered on this queue. */
|
|
151
185
|
list(): Promise<import("pg-boss").Schedule[]>;
|
|
152
186
|
};
|
|
153
|
-
/**
|
|
154
|
-
|
|
187
|
+
/**
|
|
188
|
+
* Invoked for each start job — typically resolves the type and calls `engine.startWorkflow`.
|
|
189
|
+
*
|
|
190
|
+
* The context's `idempotencyKey` is already resolved for **this delivery** (see
|
|
191
|
+
* {@link resolveStartIdempotencyKey}), so forwarding it is the right default:
|
|
192
|
+
*
|
|
193
|
+
* ```ts
|
|
194
|
+
* await starter.start(async (job) => {
|
|
195
|
+
* const wf = workflowsByType[job.workflowType];
|
|
196
|
+
* await engine.startWorkflow(wf.definition, job.input, { idempotencyKey: job.idempotencyKey });
|
|
197
|
+
* });
|
|
198
|
+
* ```
|
|
199
|
+
*/
|
|
200
|
+
type StartJobProcessor = (payload: StartJobContext) => Promise<void>;
|
|
155
201
|
interface PgBossStartWorkerDeps {
|
|
156
202
|
boss: PgBoss;
|
|
157
203
|
queueName: string;
|
|
@@ -167,4 +213,4 @@ declare function createPgBossStartWorker(deps: PgBossStartWorkerDeps): {
|
|
|
167
213
|
stop: () => Promise<void>;
|
|
168
214
|
};
|
|
169
215
|
//#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 };
|
|
216
|
+
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 };
|
|
@@ -20,16 +20,31 @@ const DEFAULT_STEP_QUEUE_CONFIG = {
|
|
|
20
20
|
/**
|
|
21
21
|
* The wire payload for a scheduled (or ad-hoc) workflow **start**. Carries the
|
|
22
22
|
* partition plus what `engine.startWorkflow` needs; the host resolves `workflowType`
|
|
23
|
-
* to a definition.
|
|
24
|
-
*
|
|
23
|
+
* to a definition.
|
|
24
|
+
*
|
|
25
|
+
* Two mutually exclusive ways to ask for start idempotency, because a cron schedule
|
|
26
|
+
* stores its payload **once** and pg-boss redelivers that same payload on every tick:
|
|
27
|
+
*
|
|
28
|
+
* - `idempotencyKeyPrefix` — the usual choice. The start worker appends the job id,
|
|
29
|
+
* so each tick gets a distinct key while a redelivery of the *same* tick reuses it.
|
|
30
|
+
* - `idempotencyKey` — used verbatim. On a schedule this means "start exactly one
|
|
31
|
+
* workflow, ever"; every later tick returns the first one.
|
|
25
32
|
*/
|
|
26
33
|
const WIRE_START_PAYLOAD_SCHEMA = z.object({
|
|
27
34
|
partitionKey: z.string().min(1),
|
|
28
35
|
workflowType: z.string().min(1),
|
|
29
36
|
input: z.record(z.string(), z.unknown()).default({}),
|
|
30
37
|
entityRef: z.string().optional(),
|
|
31
|
-
|
|
38
|
+
/** Verbatim dedup key — collapses *every* delivery, including future cron ticks. */
|
|
39
|
+
idempotencyKey: z.string().optional(),
|
|
40
|
+
/** Per-tick dedup: the start worker resolves this to `${prefix}:${jobId}`. */
|
|
41
|
+
idempotencyKeyPrefix: z.string().optional()
|
|
32
42
|
});
|
|
43
|
+
/** Resolve the per-delivery idempotency key. Explicit key wins over a prefix. */
|
|
44
|
+
function resolveStartIdempotencyKey(payload, jobId) {
|
|
45
|
+
if (payload.idempotencyKey !== void 0) return payload.idempotencyKey;
|
|
46
|
+
if (payload.idempotencyKeyPrefix !== void 0) return `${payload.idempotencyKeyPrefix}:${jobId}`;
|
|
47
|
+
}
|
|
33
48
|
//#endregion
|
|
34
49
|
//#region src/dispatcher-pgboss/queue.ts
|
|
35
50
|
function dlqName(queueName) {
|
|
@@ -73,7 +88,16 @@ function createPgBossDispatcher(deps) {
|
|
|
73
88
|
...deps.config
|
|
74
89
|
};
|
|
75
90
|
let ensured = false;
|
|
76
|
-
|
|
91
|
+
/**
|
|
92
|
+
* Adapt flow's `SqlExecutor` to the shape pg-boss wants for a caller-supplied
|
|
93
|
+
* connection. The two are the same idea under different names.
|
|
94
|
+
*/
|
|
95
|
+
function asPgBossDb(handle) {
|
|
96
|
+
const exec = handle;
|
|
97
|
+
if (!exec || typeof exec.query !== "function") return void 0;
|
|
98
|
+
return { executeSql: (text, values) => exec.query(text, values) };
|
|
99
|
+
}
|
|
100
|
+
async function send(payload, options, handle) {
|
|
77
101
|
try {
|
|
78
102
|
const wire = {
|
|
79
103
|
partitionKey,
|
|
@@ -92,11 +116,20 @@ function createPgBossDispatcher(deps) {
|
|
|
92
116
|
ensured = true;
|
|
93
117
|
}
|
|
94
118
|
const startAfter = options?.startAfterSeconds;
|
|
119
|
+
const db = handle === void 0 ? void 0 : asPgBossDb(handle);
|
|
120
|
+
if (handle !== void 0 && !db) return {
|
|
121
|
+
ok: false,
|
|
122
|
+
error: {
|
|
123
|
+
key: "queue_error",
|
|
124
|
+
message: "Transaction handle is not a SqlExecutor — is the store store-pg?"
|
|
125
|
+
}
|
|
126
|
+
};
|
|
95
127
|
if (!await boss.send(queueName, parsed.data, {
|
|
96
128
|
retryLimit: cfg.retryLimit,
|
|
97
129
|
retryDelay: cfg.retryDelay,
|
|
98
130
|
expireInSeconds: cfg.expireInSeconds,
|
|
99
|
-
...startAfter != null && startAfter > 0 ? { startAfter } : {}
|
|
131
|
+
...startAfter != null && startAfter > 0 ? { startAfter } : {},
|
|
132
|
+
...db ? { db } : {}
|
|
100
133
|
})) return {
|
|
101
134
|
ok: false,
|
|
102
135
|
error: {
|
|
@@ -117,7 +150,16 @@ function createPgBossDispatcher(deps) {
|
|
|
117
150
|
}
|
|
118
151
|
};
|
|
119
152
|
}
|
|
120
|
-
}
|
|
153
|
+
}
|
|
154
|
+
return {
|
|
155
|
+
enqueueStep: (payload, options) => send(payload, options),
|
|
156
|
+
/**
|
|
157
|
+
* Enqueue on the store's open transaction, so the job and the state change
|
|
158
|
+
* that produced it commit together. `handle` is the transaction-bound
|
|
159
|
+
* `SqlExecutor` that `createWorkflowStore.runInTransaction` hands out.
|
|
160
|
+
*/
|
|
161
|
+
enqueueStepIn: (handle, payload, options) => send(payload, options, handle)
|
|
162
|
+
};
|
|
121
163
|
}
|
|
122
164
|
/** A pg-boss worker that drives `executeStep` for step jobs. */
|
|
123
165
|
function createPgBossStepWorker(deps) {
|
|
@@ -248,7 +290,8 @@ function createPgBossScheduler(deps) {
|
|
|
248
290
|
workflowType: input.workflowType,
|
|
249
291
|
input: input.input ?? {},
|
|
250
292
|
entityRef: input.entityRef,
|
|
251
|
-
idempotencyKey: input.idempotencyKey
|
|
293
|
+
idempotencyKey: input.idempotencyKey,
|
|
294
|
+
idempotencyKeyPrefix: input.idempotencyKeyPrefix
|
|
252
295
|
};
|
|
253
296
|
await boss.schedule(queueName, input.cron, payload, {
|
|
254
297
|
key: input.key,
|
|
@@ -282,7 +325,11 @@ function createPgBossStartWorker(deps) {
|
|
|
282
325
|
logger?.error("Invalid start payload", void 0, { jobId: job.id });
|
|
283
326
|
continue;
|
|
284
327
|
}
|
|
285
|
-
await process(
|
|
328
|
+
await process({
|
|
329
|
+
...parsed.data,
|
|
330
|
+
jobId: job.id,
|
|
331
|
+
idempotencyKey: resolveStartIdempotencyKey(parsed.data, job.id)
|
|
332
|
+
});
|
|
286
333
|
}
|
|
287
334
|
});
|
|
288
335
|
}
|
|
@@ -298,4 +345,4 @@ function createPgBossStartWorker(deps) {
|
|
|
298
345
|
};
|
|
299
346
|
}
|
|
300
347
|
//#endregion
|
|
301
|
-
export { DEFAULT_STEP_QUEUE_CONFIG, WIRE_START_PAYLOAD_SCHEMA, WIRE_STEP_PAYLOAD_SCHEMA, createPgBossDispatcher, createPgBossDlqWorker, createPgBossScheduler, createPgBossStartWorker, createPgBossStepWorker, ensureStartQueue, ensureStepQueue };
|
|
348
|
+
export { DEFAULT_STEP_QUEUE_CONFIG, WIRE_START_PAYLOAD_SCHEMA, WIRE_STEP_PAYLOAD_SCHEMA, createPgBossDispatcher, createPgBossDlqWorker, createPgBossScheduler, createPgBossStartWorker, createPgBossStepWorker, ensureStartQueue, ensureStepQueue, resolveStartIdempotencyKey };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { A as StepError, B as WorkflowDefinition, C as FlowError, D as StepCompensateHandler, E as StartOptions, F as StepId, G as WorkflowWithSteps, H as WorkflowNotFoundError, I as StepRecord, J as err, K as FlowErrorShape, L as StepRegistration, M as StepHandler, N as StepHandlerNotFoundError, O as StepCompensationContext, P as StepHandlerRegistry, R as StepStatus, S as defineWaitStep, T as RetryPolicy, U as WorkflowRecord, V as WorkflowId, W as WorkflowStatus, Y as ok, _ as buildWorkflow, a as STEP_DISPLAY_STATUS, b as defineStep, c as WORKFLOW_STATUS_SCHEMA, d as toPublicWorkflow, f as StepOutput, g as WorkflowOutput, h as TypedWorkflow, i as PublicWorkflowStep, j as StepExecutionContext, k as StepDefinition, l as toDisplayStepStatus, m as TypedStepContext, n as PUBLIC_WORKFLOW_STEP_SCHEMA, o as STEP_DISPLAY_STATUS_SCHEMA, p as TypedStep, q as Result, r as PublicWorkflow, s as StepDisplayStatus, t as PUBLIC_WORKFLOW_SCHEMA, u as toPublicStep, v as defineMapStep, w as InvalidWorkflowDefinitionError, x as defineSubWorkflowStep, y as defineSleepStep, z as WorkflowCreatedResult } from "./index-D_fOYfmf.js";
|
|
2
2
|
import { n as Logger, r as noopLogger, t as LogAttributes } from "./logger-BCus3AOa.js";
|
|
3
|
-
import { C as CreateWorkflowStep, D as ListWorkflowsFilters, E as FinishWorkflowParams, O as
|
|
4
|
-
import { n as Dispatcher, r as EnqueueOptions, t as DispatchStepPayload } from "./dispatcher-
|
|
3
|
+
import { C as CreateWorkflowStep, D as ListWorkflowsFilters, E as FinishWorkflowParams, O as TransactionalScope, S as CreateWorkflowParams, T as FailStepParams, _ as StepGateDecision, a as FlowTracer, b as AddChildStep, c as RecordingTracer, d as noopObserver, f as noopTracer, g as StepGate, h as RateRule, i as FlowSpan, k as WorkflowStore, l as createRecordingObserver, m as InMemoryStepGateConfig, n as FlowEventType, o as RecordedSpan, p as ConcurrencyRule, r as FlowObserver, s as RecordingObserver, t as FlowEvent, u as createRecordingTracer, v as StepGateRequest, w as CreatedWorkflow, x as CompleteStepParams, y as createInMemoryStepGate } from "./observability-3tcmRwIw.js";
|
|
4
|
+
import { n as Dispatcher, r as EnqueueOptions, t as DispatchStepPayload } from "./dispatcher-CnMTwfCc.js";
|
|
5
5
|
import { a as WorkflowCompletedArgs, i as BuildStepContextArgs, n as BeforeStartArgs, o as WorkflowHooks, r as BeforeStartResult, t as AfterStepArgs } from "./hooks-CDRFQZbT.js";
|
|
6
6
|
//#region src/core/registry.d.ts
|
|
7
7
|
/**
|
|
@@ -134,4 +134,4 @@ declare function createInMemoryWorkflowStore(partitionKey?: string): WorkflowSto
|
|
|
134
134
|
};
|
|
135
135
|
};
|
|
136
136
|
//#endregion
|
|
137
|
-
export { AddChildStep, AfterStepArgs, BeforeStartArgs, BeforeStartResult, BuildStepContextArgs, CompleteStepParams, ConcurrencyRule, CreateWorkflowParams, CreateWorkflowStep, CreatedWorkflow, DispatchStepPayload, Dispatcher, EnqueueOptions, FailStepParams, FinishWorkflowParams, FlowError, FlowErrorShape, FlowEvent, FlowEventType, FlowObserver, FlowSpan, FlowTracer, InMemoryStepGateConfig, InvalidWorkflowDefinitionError, ListWorkflowsFilters, LogAttributes, Logger, PUBLIC_WORKFLOW_SCHEMA, PUBLIC_WORKFLOW_STEP_SCHEMA, PublicWorkflow, PublicWorkflowStep, RateRule, RecordedSpan, RecordingObserver, RecordingTracer, Result, RetryPolicy, STEP_DISPLAY_STATUS, STEP_DISPLAY_STATUS_SCHEMA, StartOptions, StepCompensateHandler, StepCompensationContext, StepDefinition, StepDisplayStatus, StepError, StepExecutionContext, StepGate, StepGateDecision, StepGateRequest, StepHandler, StepHandlerNotFoundError, StepHandlerRegistry, StepId, StepOutput, StepRecord, StepRegistration, StepStatus, TypedStep, TypedStepContext, TypedWorkflow, WORKFLOW_STATUS_SCHEMA, WorkflowCompletedArgs, WorkflowCreatedResult, WorkflowDefinition, WorkflowEngine, WorkflowEngineConfig, WorkflowEngineDeps, WorkflowHooks, WorkflowId, WorkflowNotFoundError, WorkflowOutput, WorkflowRecord, WorkflowStatus, WorkflowStore, WorkflowWithSteps, buildWorkflow, createInMemoryStepGate, createInMemoryWorkflowStore, createRecordingObserver, createRecordingTracer, createStepHandlerRegistry, createWorkflowEngine, defineMapStep, defineSleepStep, defineStep, defineSubWorkflowStep, defineWaitStep, err, explicitRetryability, isRetryableError, markRetryable, nonRetryableError, noopLogger, noopObserver, noopTracer, ok, retryableError, toDisplayStepStatus, toPublicStep, toPublicWorkflow };
|
|
137
|
+
export { AddChildStep, AfterStepArgs, BeforeStartArgs, BeforeStartResult, BuildStepContextArgs, CompleteStepParams, ConcurrencyRule, CreateWorkflowParams, CreateWorkflowStep, CreatedWorkflow, DispatchStepPayload, Dispatcher, EnqueueOptions, FailStepParams, FinishWorkflowParams, FlowError, FlowErrorShape, FlowEvent, FlowEventType, FlowObserver, FlowSpan, FlowTracer, InMemoryStepGateConfig, InvalidWorkflowDefinitionError, ListWorkflowsFilters, LogAttributes, Logger, PUBLIC_WORKFLOW_SCHEMA, PUBLIC_WORKFLOW_STEP_SCHEMA, PublicWorkflow, PublicWorkflowStep, RateRule, RecordedSpan, RecordingObserver, RecordingTracer, Result, RetryPolicy, STEP_DISPLAY_STATUS, STEP_DISPLAY_STATUS_SCHEMA, StartOptions, StepCompensateHandler, StepCompensationContext, StepDefinition, StepDisplayStatus, StepError, StepExecutionContext, StepGate, StepGateDecision, StepGateRequest, StepHandler, StepHandlerNotFoundError, StepHandlerRegistry, StepId, StepOutput, StepRecord, StepRegistration, StepStatus, TransactionalScope, TypedStep, TypedStepContext, TypedWorkflow, WORKFLOW_STATUS_SCHEMA, WorkflowCompletedArgs, WorkflowCreatedResult, WorkflowDefinition, WorkflowEngine, WorkflowEngineConfig, WorkflowEngineDeps, WorkflowHooks, WorkflowId, WorkflowNotFoundError, WorkflowOutput, WorkflowRecord, WorkflowStatus, WorkflowStore, WorkflowWithSteps, buildWorkflow, createInMemoryStepGate, createInMemoryWorkflowStore, createRecordingObserver, createRecordingTracer, createStepHandlerRegistry, createWorkflowEngine, defineMapStep, defineSleepStep, defineStep, defineSubWorkflowStep, defineWaitStep, err, explicitRetryability, isRetryableError, markRetryable, nonRetryableError, noopLogger, noopObserver, noopTracer, ok, retryableError, toDisplayStepStatus, toPublicStep, toPublicWorkflow };
|
package/dist/index.js
CHANGED
|
@@ -221,6 +221,26 @@ function createWorkflowEngine(deps) {
|
|
|
221
221
|
const stepExpirySeconds = deps.config?.stepExpirySeconds ?? 600;
|
|
222
222
|
const stuckStepBufferSeconds = deps.config?.stuckStepBufferSeconds ?? 300;
|
|
223
223
|
const defaultRetryable = deps.config?.defaultRetryable;
|
|
224
|
+
/**
|
|
225
|
+
* Both halves must opt in: the store has to offer a transaction and the
|
|
226
|
+
* dispatcher has to be able to enqueue inside it. When they do, a state change
|
|
227
|
+
* and the dispatches it unlocks commit atomically — there is no window in
|
|
228
|
+
* which a step is `pending` with no job behind it. When either cannot, the
|
|
229
|
+
* engine writes and then enqueues, exactly as before.
|
|
230
|
+
*/
|
|
231
|
+
const atomicDispatch = typeof store.runInTransaction === "function" && typeof dispatcher.enqueueStepIn === "function";
|
|
232
|
+
/**
|
|
233
|
+
* Run `fn` atomically when both halves support it, otherwise run it directly.
|
|
234
|
+
* `fn` receives the scope to write through — pass it to `dispatchReadyStep`.
|
|
235
|
+
*
|
|
236
|
+
* Only writes and enqueues belong in here. Anything that can run user code
|
|
237
|
+
* (saga compensation, hooks) must stay outside, or a rollback handler making a
|
|
238
|
+
* network call would hold a database transaction open while it does.
|
|
239
|
+
*/
|
|
240
|
+
async function withDispatchScope(fn) {
|
|
241
|
+
if (!atomicDispatch) return fn(void 0);
|
|
242
|
+
return store.runInTransaction((scope) => fn(scope));
|
|
243
|
+
}
|
|
224
244
|
const nowIso = () => now().toISOString();
|
|
225
245
|
/** Emit a run-history / metrics event. Guarded — a faulty observer never breaks a run. */
|
|
226
246
|
function emit(event) {
|
|
@@ -249,9 +269,10 @@ function createWorkflowEngine(deps) {
|
|
|
249
269
|
* and awaits `resumeStep`; everything else is enqueued (with any durable delay).
|
|
250
270
|
* Returns whether the step was enqueued.
|
|
251
271
|
*/
|
|
252
|
-
async function dispatchReadyStep(workflowId, stepId, stepKey, stepType) {
|
|
272
|
+
async function dispatchReadyStep(workflowId, stepId, stepKey, stepType, scope) {
|
|
273
|
+
const st = scope?.store ?? store;
|
|
253
274
|
if (isWaitStep(stepType)) {
|
|
254
|
-
await
|
|
275
|
+
await st.markStepWaiting(stepId);
|
|
255
276
|
logger.info("Step is waiting for an event", {
|
|
256
277
|
workflowId,
|
|
257
278
|
stepId,
|
|
@@ -266,12 +287,14 @@ function createWorkflowEngine(deps) {
|
|
|
266
287
|
});
|
|
267
288
|
return false;
|
|
268
289
|
}
|
|
269
|
-
const
|
|
290
|
+
const payload = {
|
|
270
291
|
workflowId,
|
|
271
292
|
stepId,
|
|
272
293
|
stepKey,
|
|
273
294
|
stepType
|
|
274
|
-
}
|
|
295
|
+
};
|
|
296
|
+
const delay = readyStepDelay(stepType);
|
|
297
|
+
const result = scope ? await dispatcher.enqueueStepIn(scope.handle, payload, delay) : await dispatcher.enqueueStep(payload, delay);
|
|
275
298
|
if (!result.ok) {
|
|
276
299
|
logger.error("Failed to enqueue step", new Error(result.error.message), {
|
|
277
300
|
workflowId,
|
|
@@ -377,21 +400,38 @@ function createWorkflowEngine(deps) {
|
|
|
377
400
|
};
|
|
378
401
|
}
|
|
379
402
|
const { steps } = definition;
|
|
380
|
-
const created = await
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
403
|
+
const { created, enqueuedStepKeys } = await withDispatchScope(async (scope) => {
|
|
404
|
+
const created = await (scope?.store ?? store).createWorkflow({
|
|
405
|
+
type: definition.type,
|
|
406
|
+
input,
|
|
407
|
+
entityRef: options?.entityRef,
|
|
408
|
+
metadata,
|
|
409
|
+
idempotencyKey: options?.idempotencyKey,
|
|
410
|
+
parentWorkflowId: options?.parentWorkflowId,
|
|
411
|
+
parentStepId: options?.parentStepId,
|
|
412
|
+
startedAt: nowIso(),
|
|
413
|
+
steps: steps.map((s) => ({
|
|
414
|
+
key: s.key,
|
|
415
|
+
type: s.type,
|
|
416
|
+
dependencies: s.dependencies ?? [],
|
|
417
|
+
input: s.input ?? null
|
|
418
|
+
}))
|
|
419
|
+
});
|
|
420
|
+
if (created.alreadyExisted) return {
|
|
421
|
+
created,
|
|
422
|
+
enqueuedStepKeys: []
|
|
423
|
+
};
|
|
424
|
+
const readySteps = steps.filter((s) => !s.dependencies || s.dependencies.length === 0);
|
|
425
|
+
const keys = [];
|
|
426
|
+
for (const readyStep of readySteps) {
|
|
427
|
+
const dbStep = created.steps.find((s) => s.key === readyStep.key);
|
|
428
|
+
if (!dbStep) continue;
|
|
429
|
+
if (await dispatchReadyStep(created.workflowId, dbStep.id, readyStep.key, readyStep.type, scope)) keys.push(readyStep.key);
|
|
430
|
+
}
|
|
431
|
+
return {
|
|
432
|
+
created,
|
|
433
|
+
enqueuedStepKeys: keys
|
|
434
|
+
};
|
|
395
435
|
});
|
|
396
436
|
const workflowId = created.workflowId;
|
|
397
437
|
if (created.alreadyExisted) {
|
|
@@ -413,13 +453,6 @@ function createWorkflowEngine(deps) {
|
|
|
413
453
|
workflowId,
|
|
414
454
|
workflowType: definition.type
|
|
415
455
|
});
|
|
416
|
-
const readySteps = steps.filter((s) => !s.dependencies || s.dependencies.length === 0);
|
|
417
|
-
const enqueuedStepKeys = [];
|
|
418
|
-
for (const readyStep of readySteps) {
|
|
419
|
-
const dbStep = created.steps.find((s) => s.key === readyStep.key);
|
|
420
|
-
if (!dbStep) continue;
|
|
421
|
-
if (await dispatchReadyStep(workflowId, dbStep.id, readyStep.key, readyStep.type)) enqueuedStepKeys.push(readyStep.key);
|
|
422
|
-
}
|
|
423
456
|
logger.info("Workflow started", {
|
|
424
457
|
workflowId,
|
|
425
458
|
type: definition.type,
|
|
@@ -468,6 +501,20 @@ function createWorkflowEngine(deps) {
|
|
|
468
501
|
}
|
|
469
502
|
};
|
|
470
503
|
if (step.status !== "pending") {
|
|
504
|
+
const workflowLive = workflow.status === "running" || workflow.status === "pending";
|
|
505
|
+
if (step.status === "completed" && workflowLive) {
|
|
506
|
+
logger.info("Re-driving advance for a redelivered completed step", {
|
|
507
|
+
workflowId,
|
|
508
|
+
stepId,
|
|
509
|
+
stepKey: step.key
|
|
510
|
+
});
|
|
511
|
+
if (step.parentStepId != null) await advanceAfterChildCompleted(workflowId, step);
|
|
512
|
+
else await advanceAfterStepCompleted(workflowId, stepId, step.output ?? {});
|
|
513
|
+
return {
|
|
514
|
+
ok: true,
|
|
515
|
+
value: void 0
|
|
516
|
+
};
|
|
517
|
+
}
|
|
471
518
|
logger.info("Skipping already-processed step", {
|
|
472
519
|
workflowId,
|
|
473
520
|
stepId,
|
|
@@ -791,32 +838,60 @@ function createWorkflowEngine(deps) {
|
|
|
791
838
|
};
|
|
792
839
|
}
|
|
793
840
|
async function onStepCompleted(workflowId, stepId, output) {
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
841
|
+
await finalizeAfterStepCompleted(workflowId, stepId, output, await withDispatchScope(async (scope) => {
|
|
842
|
+
await (scope?.store ?? store).completeStep({
|
|
843
|
+
workflowId,
|
|
844
|
+
stepId,
|
|
845
|
+
output,
|
|
846
|
+
completedAt: nowIso()
|
|
847
|
+
});
|
|
848
|
+
return dispatchNewlyReady(workflowId, stepId, scope);
|
|
849
|
+
}));
|
|
850
|
+
}
|
|
851
|
+
/**
|
|
852
|
+
* The readiness half of a completion, without the write. Split out so a
|
|
853
|
+
* redelivered job for an already-completed step can re-drive it — see the
|
|
854
|
+
* recovery branch in {@link executeStep}. Idempotent: dispatching a step that
|
|
855
|
+
* is already queued is harmless, because claiming it is atomic.
|
|
856
|
+
*/
|
|
857
|
+
async function advanceAfterStepCompleted(workflowId, stepId, output) {
|
|
858
|
+
await finalizeAfterStepCompleted(workflowId, stepId, output, await withDispatchScope((scope) => dispatchNewlyReady(workflowId, stepId, scope)));
|
|
859
|
+
}
|
|
860
|
+
/**
|
|
861
|
+
* Transactional half: read readiness and dispatch whatever just became ready.
|
|
862
|
+
* Contains only store writes and enqueues — never user code.
|
|
863
|
+
*/
|
|
864
|
+
async function dispatchNewlyReady(workflowId, stepId, scope) {
|
|
865
|
+
const keyedSteps = (await (scope?.store ?? store).listSteps(workflowId)).filter((s) => s.parentStepId == null);
|
|
802
866
|
const completedKeys = new Set(keyedSteps.filter((s) => s.status === "completed").map((s) => s.key));
|
|
803
867
|
const newlyReady = keyedSteps.filter((s) => {
|
|
804
868
|
if (s.status !== "pending") return false;
|
|
805
869
|
return (s.dependencies ?? []).every((dep) => completedKeys.has(dep));
|
|
806
870
|
});
|
|
807
|
-
for (const readyStep of newlyReady) await dispatchReadyStep(workflowId, readyStep.id, readyStep.key, readyStep.type);
|
|
871
|
+
for (const readyStep of newlyReady) await dispatchReadyStep(workflowId, readyStep.id, readyStep.key, readyStep.type, scope);
|
|
872
|
+
return {
|
|
873
|
+
keyedSteps,
|
|
874
|
+
newlyReadyCount: newlyReady.length
|
|
875
|
+
};
|
|
876
|
+
}
|
|
877
|
+
/**
|
|
878
|
+
* Non-transactional half: decide whether the workflow is now failed or done.
|
|
879
|
+
* Deliberately outside any transaction — the failure path runs saga
|
|
880
|
+
* compensation, i.e. user handlers that may do network I/O.
|
|
881
|
+
*/
|
|
882
|
+
async function finalizeAfterStepCompleted(workflowId, stepId, output, { keyedSteps, newlyReadyCount }) {
|
|
808
883
|
if (keyedSteps.some((s) => s.id !== stepId && s.status === "failed")) {
|
|
809
884
|
await checkWorkflowFailure(workflowId);
|
|
810
885
|
return;
|
|
811
886
|
}
|
|
812
|
-
if (keyedSteps.every((s) => s.id === stepId || s.status === "completed" || s.status === "skipped") &&
|
|
887
|
+
if (keyedSteps.every((s) => s.id === stepId || s.status === "completed" || s.status === "skipped") && newlyReadyCount === 0) {
|
|
813
888
|
const aggregatedOutput = {};
|
|
814
889
|
for (const s of keyedSteps) if (s.status === "completed" || s.id === stepId) aggregatedOutput[s.key] = s.id === stepId ? output : s.output ?? null;
|
|
815
890
|
await store.finishWorkflow({
|
|
816
891
|
workflowId,
|
|
817
892
|
status: "completed",
|
|
818
893
|
output: aggregatedOutput,
|
|
819
|
-
completedAt
|
|
894
|
+
completedAt: nowIso()
|
|
820
895
|
});
|
|
821
896
|
logger.info("Workflow completed", { workflowId });
|
|
822
897
|
emit({
|
|
@@ -877,6 +952,10 @@ function createWorkflowEngine(deps) {
|
|
|
877
952
|
output,
|
|
878
953
|
completedAt: nowIso()
|
|
879
954
|
});
|
|
955
|
+
await advanceAfterChildCompleted(workflowId, childStep);
|
|
956
|
+
}
|
|
957
|
+
/** The aggregation half of a map child's completion, without the write. */
|
|
958
|
+
async function advanceAfterChildCompleted(workflowId, childStep) {
|
|
880
959
|
const parentId = childStep.parentStepId;
|
|
881
960
|
const children = await store.listChildSteps(parentId);
|
|
882
961
|
if (children.some((c) => c.status === "failed")) {
|
|
@@ -61,6 +61,18 @@ interface ListWorkflowsFilters {
|
|
|
61
61
|
entityRef?: string;
|
|
62
62
|
limit?: number;
|
|
63
63
|
}
|
|
64
|
+
/**
|
|
65
|
+
* A store bound to an open transaction, plus an opaque handle a capable
|
|
66
|
+
* {@link Dispatcher} can join so a job and the state change that produced it
|
|
67
|
+
* commit together. Core never inspects `handle` — it belongs to the adapter
|
|
68
|
+
* pair (e.g. store-pg hands dispatcher-pgboss its transaction-bound executor).
|
|
69
|
+
*/
|
|
70
|
+
interface TransactionalScope {
|
|
71
|
+
/** A store whose writes go through the open transaction. */
|
|
72
|
+
store: WorkflowStore;
|
|
73
|
+
/** Opaque; pass to `Dispatcher.enqueueStepIn`. */
|
|
74
|
+
handle: unknown;
|
|
75
|
+
}
|
|
64
76
|
/**
|
|
65
77
|
* Persistence boundary for the engine. Implementations are **partition-scoped at
|
|
66
78
|
* construction** (e.g. bound to one tenant) — exactly like the engine — so methods
|
|
@@ -72,6 +84,19 @@ interface ListWorkflowsFilters {
|
|
|
72
84
|
* are computed from `listSteps`.
|
|
73
85
|
*/
|
|
74
86
|
interface WorkflowStore {
|
|
87
|
+
/**
|
|
88
|
+
* **Optional capability.** Run `fn` inside one store transaction.
|
|
89
|
+
*
|
|
90
|
+
* When the dispatcher also implements `enqueueStepIn`, the engine uses both so
|
|
91
|
+
* that a state change and the dispatches it unlocks commit atomically —
|
|
92
|
+
* closing the window where a crash leaves a step `pending` with no job behind
|
|
93
|
+
* it. A store that cannot offer this simply omits it, and the engine falls
|
|
94
|
+
* back to writing then enqueueing.
|
|
95
|
+
*
|
|
96
|
+
* Implementations MUST run every operation on `scope.store` inside the same
|
|
97
|
+
* transaction, and MUST roll back if `fn` throws.
|
|
98
|
+
*/
|
|
99
|
+
runInTransaction?<T>(fn: (scope: TransactionalScope) => Promise<T>): Promise<T>;
|
|
75
100
|
/** Create the workflow + all step rows atomically (transactional). */
|
|
76
101
|
createWorkflow(params: CreateWorkflowParams): Promise<CreatedWorkflow>;
|
|
77
102
|
getWorkflow(workflowId: WorkflowId): Promise<WorkflowRecord | null>;
|
|
@@ -246,4 +271,4 @@ interface RecordingTracer extends FlowTracer {
|
|
|
246
271
|
}
|
|
247
272
|
declare function createRecordingTracer(): RecordingTracer;
|
|
248
273
|
//#endregion
|
|
249
|
-
export { CreateWorkflowStep as C, ListWorkflowsFilters as D, FinishWorkflowParams as E,
|
|
274
|
+
export { CreateWorkflowStep as C, ListWorkflowsFilters as D, FinishWorkflowParams as E, TransactionalScope as O, CreateWorkflowParams as S, FailStepParams as T, StepGateDecision as _, FlowTracer as a, AddChildStep as b, RecordingTracer as c, noopObserver as d, noopTracer as f, StepGate as g, RateRule as h, FlowSpan as i, WorkflowStore as k, createRecordingObserver as l, InMemoryStepGateConfig as m, FlowEventType as n, RecordedSpan as o, ConcurrencyRule as p, FlowObserver as r, RecordingObserver as s, FlowEvent as t, createRecordingTracer as u, StepGateRequest as v, CreatedWorkflow as w, CompleteStepParams as x, createInMemoryStepGate as y };
|
package/dist/store-pg/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { V as WorkflowId } from "../index-D_fOYfmf.js";
|
|
2
|
-
import {
|
|
2
|
+
import { g as StepGate, h as RateRule, k as WorkflowStore, p as ConcurrencyRule, r as FlowObserver, t as FlowEvent } from "../observability-3tcmRwIw.js";
|
|
3
3
|
import { Pool } from "pg";
|
|
4
4
|
//#region src/store-pg/ddl.d.ts
|
|
5
5
|
/**
|
package/dist/store-pg/index.js
CHANGED
|
@@ -386,7 +386,27 @@ function createWorkflowStore(deps) {
|
|
|
386
386
|
cutoff
|
|
387
387
|
])).rows.map(mapStep);
|
|
388
388
|
}
|
|
389
|
+
/**
|
|
390
|
+
* Optional transaction capability (see `WorkflowStore.runInTransaction`). Hands
|
|
391
|
+
* back a store bound to the open transaction plus the transaction-bound
|
|
392
|
+
* executor as the opaque handle — `createPgBossDispatcher` knows how to enqueue
|
|
393
|
+
* through it, so a job and the state change that produced it commit together.
|
|
394
|
+
*
|
|
395
|
+
* Recursing is not supported (adapters never nest transactions); a nested call
|
|
396
|
+
* would reuse the same connection via `poolExecutor`, so it is simply the same
|
|
397
|
+
* transaction rather than a new one.
|
|
398
|
+
*/
|
|
399
|
+
async function runInTransaction(fn) {
|
|
400
|
+
return exec.transaction((tx) => fn({
|
|
401
|
+
store: createWorkflowStore({
|
|
402
|
+
...deps,
|
|
403
|
+
exec: tx
|
|
404
|
+
}),
|
|
405
|
+
handle: tx
|
|
406
|
+
}));
|
|
407
|
+
}
|
|
389
408
|
return {
|
|
409
|
+
runInTransaction,
|
|
390
410
|
createWorkflow,
|
|
391
411
|
getWorkflow,
|
|
392
412
|
getStep,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "octaflow",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.0",
|
|
4
4
|
"description": "Durable DAG workflow engine: Zod-typed steps over Postgres + pg-boss, with an optional AI add-on (token/cost/quota). Layered single package.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|