octaflow 0.15.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
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
|
|
|
@@ -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.
|
|
@@ -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) {
|
|
@@ -275,7 +290,8 @@ function createPgBossScheduler(deps) {
|
|
|
275
290
|
workflowType: input.workflowType,
|
|
276
291
|
input: input.input ?? {},
|
|
277
292
|
entityRef: input.entityRef,
|
|
278
|
-
idempotencyKey: input.idempotencyKey
|
|
293
|
+
idempotencyKey: input.idempotencyKey,
|
|
294
|
+
idempotencyKeyPrefix: input.idempotencyKeyPrefix
|
|
279
295
|
};
|
|
280
296
|
await boss.schedule(queueName, input.cron, payload, {
|
|
281
297
|
key: input.key,
|
|
@@ -309,7 +325,11 @@ function createPgBossStartWorker(deps) {
|
|
|
309
325
|
logger?.error("Invalid start payload", void 0, { jobId: job.id });
|
|
310
326
|
continue;
|
|
311
327
|
}
|
|
312
|
-
await process(
|
|
328
|
+
await process({
|
|
329
|
+
...parsed.data,
|
|
330
|
+
jobId: job.id,
|
|
331
|
+
idempotencyKey: resolveStartIdempotencyKey(parsed.data, job.id)
|
|
332
|
+
});
|
|
313
333
|
}
|
|
314
334
|
});
|
|
315
335
|
}
|
|
@@ -325,4 +345,4 @@ function createPgBossStartWorker(deps) {
|
|
|
325
345
|
};
|
|
326
346
|
}
|
|
327
347
|
//#endregion
|
|
328
|
-
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/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",
|