octaflow 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,170 @@
1
+ import "../index-D_fOYfmf.js";
2
+ import { n as Logger } from "../logger-BCus3AOa.js";
3
+ import { n as Dispatcher } from "../dispatcher-BydjyrgU.js";
4
+ import { z } from "zod";
5
+ import { PgBoss } from "pg-boss";
6
+ //#region src/dispatcher-pgboss/payload.d.ts
7
+ /**
8
+ * The wire payload for a step job. It is the flow-core `DispatchStepPayload`
9
+ * plus the `partitionKey` — the worker needs the partition to reconstruct a
10
+ * partition-scoped engine before calling `executeStep`.
11
+ */
12
+ declare const WIRE_STEP_PAYLOAD_SCHEMA: z.ZodObject<{
13
+ partitionKey: z.ZodString;
14
+ workflowId: z.ZodNumber;
15
+ stepId: z.ZodNumber;
16
+ stepKey: z.ZodString;
17
+ stepType: z.ZodString;
18
+ }, z.core.$strip>;
19
+ type WireStepPayload = z.infer<typeof WIRE_STEP_PAYLOAD_SCHEMA>;
20
+ interface StepQueueConfig {
21
+ /** Retries before a job is dead-lettered. Default 2. */
22
+ retryLimit?: number;
23
+ /** Seconds between retries. Default 30. */
24
+ retryDelay?: number;
25
+ /** Seconds before an in-flight job is considered expired. Default 600. */
26
+ expireInSeconds?: number;
27
+ }
28
+ declare const DEFAULT_STEP_QUEUE_CONFIG: Required<StepQueueConfig>;
29
+ /**
30
+ * The wire payload for a scheduled (or ad-hoc) workflow **start**. Carries the
31
+ * partition plus what `engine.startWorkflow` needs; the host resolves `workflowType`
32
+ * to a definition. `idempotencyKey` is forwarded to the start so overlapping
33
+ * cron ticks don't double-start.
34
+ */
35
+ declare const WIRE_START_PAYLOAD_SCHEMA: z.ZodObject<{
36
+ partitionKey: z.ZodString;
37
+ workflowType: z.ZodString;
38
+ input: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
39
+ entityRef: z.ZodOptional<z.ZodString>;
40
+ idempotencyKey: z.ZodOptional<z.ZodString>;
41
+ }, z.core.$strip>;
42
+ type WireStartPayload = z.infer<typeof WIRE_START_PAYLOAD_SCHEMA>;
43
+ //#endregion
44
+ //#region src/dispatcher-pgboss/queue.d.ts
45
+ /**
46
+ * Create the step queue and its dead-letter queue (idempotent). Call once at
47
+ * startup; the dispatcher also calls it lazily on first enqueue.
48
+ */
49
+ declare function ensureStepQueue(boss: PgBoss, queueName: string, config?: StepQueueConfig): Promise<void>;
50
+ interface PgBossDispatcherDeps {
51
+ boss: PgBoss;
52
+ queueName: string;
53
+ /** Partition this dispatcher is bound to — stamped into every enqueued job. */
54
+ partitionKey: string;
55
+ config?: StepQueueConfig;
56
+ }
57
+ /** A flow-core `Dispatcher` backed by pg-boss. */
58
+ declare function createPgBossDispatcher(deps: PgBossDispatcherDeps): Dispatcher;
59
+ interface PgBossStepWorkerOptions {
60
+ /** Steps fetched per poll. Default 1. */
61
+ batchSize?: number;
62
+ /** Base poll interval in seconds (pg-boss minimum 0.5). Default: pg-boss's own. */
63
+ pollingIntervalSeconds?: number;
64
+ /**
65
+ * Poll interval used while pg-boss's LISTEN/NOTIFY is active for the queue, where
66
+ * polling is only a backstop. Ignored when notify is off or unavailable.
67
+ */
68
+ notifyPollingIntervalSeconds?: number;
69
+ /**
70
+ * Keep fetching with no delay while every fetch returns a full `batchSize` batch;
71
+ * the first short fetch ends the burst. Ignored when `batchSize` is 1.
72
+ *
73
+ * **Worth enabling for throughput.** Without it a worker spends most of its time
74
+ * waiting out the poll interval: it drains a batch in milliseconds, then sleeps.
75
+ * See the performance table in the README.
76
+ */
77
+ burstWhenBatchFull?: boolean;
78
+ /** Burst while the queue's cached ready count exceeds this. Reacts on pg-boss's stats cadence. */
79
+ burstWhenReadyExceeds?: number;
80
+ /**
81
+ * How many steps from one fetched batch run at a time. Default 1 (serial).
82
+ *
83
+ * Raising this only helps once the worker is not poll-bound — pair it with
84
+ * `burstWhenBatchFull`. Each in-flight step needs a store connection, so keep
85
+ * `concurrency × workers` within your Postgres pool and `max_connections`.
86
+ */
87
+ concurrency?: number;
88
+ }
89
+ interface PgBossStepWorkerDeps {
90
+ boss: PgBoss;
91
+ queueName: string;
92
+ config?: StepQueueConfig;
93
+ logger?: Logger;
94
+ workerOptions?: PgBossStepWorkerOptions;
95
+ }
96
+ /** Handles a single step job. Reconstruct the partition-scoped engine and call
97
+ * `engine.executeStep(workflowId, stepId)`. Throw to trigger a pg-boss retry. */
98
+ type StepJobProcessor = (payload: WireStepPayload) => Promise<void>;
99
+ /** A pg-boss worker that drives `executeStep` for step jobs. */
100
+ declare function createPgBossStepWorker(deps: PgBossStepWorkerDeps): {
101
+ start: (process: StepJobProcessor) => Promise<void>;
102
+ stop: () => Promise<void>;
103
+ };
104
+ interface PgBossDlqWorkerDeps {
105
+ boss: PgBoss;
106
+ queueName: string;
107
+ logger?: Logger;
108
+ pollingIntervalSeconds?: number;
109
+ }
110
+ /** Invoked for a step job that exhausted all retries. Typically calls
111
+ * `engine.handleStepExhausted(workflowId, stepId, reason)`. */
112
+ type DlqProcessor = (payload: WireStepPayload) => Promise<void>;
113
+ /** A pg-boss worker on the step queue's dead-letter queue. */
114
+ declare function createPgBossDlqWorker(deps: PgBossDlqWorkerDeps): {
115
+ start: (onDlq: DlqProcessor) => Promise<void>;
116
+ stop: () => Promise<void>;
117
+ };
118
+ //#endregion
119
+ //#region src/dispatcher-pgboss/scheduler.d.ts
120
+ /** Create the start queue (idempotent). */
121
+ declare function ensureStartQueue(boss: PgBoss, queueName: string): Promise<void>;
122
+ interface PgBossSchedulerDeps {
123
+ boss: PgBoss;
124
+ /** Queue scheduled start jobs are sent to (and the start worker reads). */
125
+ queueName: string;
126
+ /** Partition stamped into every scheduled start payload. */
127
+ partitionKey: string;
128
+ logger?: Logger;
129
+ }
130
+ interface ScheduleStartInput {
131
+ /** Unique key for this schedule within the queue (allows many crons per queue). */
132
+ key: string;
133
+ /** Cron expression (5- or 6-field). */
134
+ cron: string;
135
+ /** Workflow type to start (the host resolves it to a definition). */
136
+ workflowType: string;
137
+ input?: Record<string, unknown>;
138
+ entityRef?: string;
139
+ /** Forwarded to the start so overlapping ticks don't double-start. */
140
+ idempotencyKey?: string;
141
+ /** Optional IANA time zone (default UTC). */
142
+ tz?: string;
143
+ }
144
+ /** Registers cron schedules that enqueue workflow-start jobs. */
145
+ declare function createPgBossScheduler(deps: PgBossSchedulerDeps): {
146
+ /** Create or replace a cron schedule that starts a workflow. */
147
+ schedule(input: ScheduleStartInput): Promise<void>;
148
+ /** Remove a schedule by key. */
149
+ unschedule(key: string): Promise<void>;
150
+ /** List the schedules registered on this queue. */
151
+ list(): Promise<import("pg-boss").Schedule[]>;
152
+ };
153
+ /** Invoked for each start job — typically resolves the type and calls `engine.startWorkflow`. */
154
+ type StartJobProcessor = (payload: WireStartPayload) => Promise<void>;
155
+ interface PgBossStartWorkerDeps {
156
+ boss: PgBoss;
157
+ queueName: string;
158
+ logger?: Logger;
159
+ workerOptions?: {
160
+ batchSize?: number;
161
+ pollingIntervalSeconds?: number;
162
+ };
163
+ }
164
+ /** A pg-boss worker on the start queue that drives a host start callback. */
165
+ declare function createPgBossStartWorker(deps: PgBossStartWorkerDeps): {
166
+ start: (process: StartJobProcessor) => Promise<void>;
167
+ stop: () => Promise<void>;
168
+ };
169
+ //#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 };
@@ -0,0 +1,301 @@
1
+ import { z } from "zod";
2
+ //#region src/dispatcher-pgboss/payload.ts
3
+ /**
4
+ * The wire payload for a step job. It is the flow-core `DispatchStepPayload`
5
+ * plus the `partitionKey` — the worker needs the partition to reconstruct a
6
+ * partition-scoped engine before calling `executeStep`.
7
+ */
8
+ const WIRE_STEP_PAYLOAD_SCHEMA = z.object({
9
+ partitionKey: z.string().min(1),
10
+ workflowId: z.number().int().positive(),
11
+ stepId: z.number().int().positive(),
12
+ stepKey: z.string().min(1),
13
+ stepType: z.string().min(1)
14
+ });
15
+ const DEFAULT_STEP_QUEUE_CONFIG = {
16
+ retryLimit: 2,
17
+ retryDelay: 30,
18
+ expireInSeconds: 600
19
+ };
20
+ /**
21
+ * The wire payload for a scheduled (or ad-hoc) workflow **start**. Carries the
22
+ * partition plus what `engine.startWorkflow` needs; the host resolves `workflowType`
23
+ * to a definition. `idempotencyKey` is forwarded to the start so overlapping
24
+ * cron ticks don't double-start.
25
+ */
26
+ const WIRE_START_PAYLOAD_SCHEMA = z.object({
27
+ partitionKey: z.string().min(1),
28
+ workflowType: z.string().min(1),
29
+ input: z.record(z.string(), z.unknown()).default({}),
30
+ entityRef: z.string().optional(),
31
+ idempotencyKey: z.string().optional()
32
+ });
33
+ //#endregion
34
+ //#region src/dispatcher-pgboss/queue.ts
35
+ function dlqName(queueName) {
36
+ return `${queueName}-dlq`;
37
+ }
38
+ /**
39
+ * Create the step queue and its dead-letter queue (idempotent). Call once at
40
+ * startup; the dispatcher also calls it lazily on first enqueue.
41
+ */
42
+ async function ensureStepQueue(boss, queueName, config = {}) {
43
+ const cfg = {
44
+ ...DEFAULT_STEP_QUEUE_CONFIG,
45
+ ...config
46
+ };
47
+ const dlq = dlqName(queueName);
48
+ const swallowExists = (error) => {
49
+ if (error instanceof Error && error.message.includes("already exists")) return;
50
+ throw error;
51
+ };
52
+ try {
53
+ await boss.createQueue(dlq, { retryLimit: 0 });
54
+ } catch (e) {
55
+ swallowExists(e);
56
+ }
57
+ try {
58
+ await boss.createQueue(queueName, {
59
+ retryLimit: cfg.retryLimit,
60
+ retryDelay: cfg.retryDelay,
61
+ expireInSeconds: cfg.expireInSeconds,
62
+ deadLetter: dlq
63
+ });
64
+ } catch (e) {
65
+ swallowExists(e);
66
+ }
67
+ }
68
+ /** A flow-core `Dispatcher` backed by pg-boss. */
69
+ function createPgBossDispatcher(deps) {
70
+ const { boss, queueName, partitionKey } = deps;
71
+ const cfg = {
72
+ ...DEFAULT_STEP_QUEUE_CONFIG,
73
+ ...deps.config
74
+ };
75
+ let ensured = false;
76
+ return { async enqueueStep(payload, options) {
77
+ try {
78
+ const wire = {
79
+ partitionKey,
80
+ ...payload
81
+ };
82
+ const parsed = WIRE_STEP_PAYLOAD_SCHEMA.safeParse(wire);
83
+ if (!parsed.success) return {
84
+ ok: false,
85
+ error: {
86
+ key: "queue_error",
87
+ message: `Invalid step payload: ${parsed.error.message}`
88
+ }
89
+ };
90
+ if (!ensured) {
91
+ await ensureStepQueue(boss, queueName, cfg);
92
+ ensured = true;
93
+ }
94
+ const startAfter = options?.startAfterSeconds;
95
+ if (!await boss.send(queueName, parsed.data, {
96
+ retryLimit: cfg.retryLimit,
97
+ retryDelay: cfg.retryDelay,
98
+ expireInSeconds: cfg.expireInSeconds,
99
+ ...startAfter != null && startAfter > 0 ? { startAfter } : {}
100
+ })) return {
101
+ ok: false,
102
+ error: {
103
+ key: "queue_error",
104
+ message: `Failed to enqueue job to ${queueName}`
105
+ }
106
+ };
107
+ return {
108
+ ok: true,
109
+ value: void 0
110
+ };
111
+ } catch (error) {
112
+ return {
113
+ ok: false,
114
+ error: {
115
+ key: "queue_error",
116
+ message: error instanceof Error ? error.message : "Unknown enqueue error"
117
+ }
118
+ };
119
+ }
120
+ } };
121
+ }
122
+ /** A pg-boss worker that drives `executeStep` for step jobs. */
123
+ function createPgBossStepWorker(deps) {
124
+ const { boss, queueName } = deps;
125
+ let workerId = null;
126
+ async function start(process) {
127
+ await ensureStepQueue(boss, queueName, deps.config);
128
+ const opts = deps.workerOptions ?? {};
129
+ const lanes = Math.max(1, opts.concurrency ?? 1);
130
+ const workOpts = {
131
+ batchSize: opts.batchSize ?? 1,
132
+ perJobResults: true,
133
+ ...opts.pollingIntervalSeconds != null && { pollingIntervalSeconds: opts.pollingIntervalSeconds },
134
+ ...opts.notifyPollingIntervalSeconds != null && { notifyPollingIntervalSeconds: opts.notifyPollingIntervalSeconds },
135
+ ...opts.burstWhenBatchFull != null && { burstWhenBatchFull: opts.burstWhenBatchFull },
136
+ ...opts.burstWhenReadyExceeds != null && { burstWhenReadyExceeds: opts.burstWhenReadyExceeds }
137
+ };
138
+ workerId = await boss.work(queueName, workOpts, async (jobs) => {
139
+ const results = [];
140
+ const pending = [...jobs];
141
+ await Promise.all(Array.from({ length: Math.min(lanes, pending.length) }, async () => {
142
+ for (;;) {
143
+ const job = pending.shift();
144
+ if (!job) break;
145
+ const parsed = WIRE_STEP_PAYLOAD_SCHEMA.safeParse(job.data);
146
+ if (!parsed.success) {
147
+ results.push({
148
+ id: job.id,
149
+ status: "deadletter",
150
+ output: { message: `Invalid step payload for job ${job.id}: ${parsed.error.message}` }
151
+ });
152
+ continue;
153
+ }
154
+ try {
155
+ await process(parsed.data);
156
+ results.push({
157
+ id: job.id,
158
+ status: "completed"
159
+ });
160
+ } catch (error) {
161
+ results.push({
162
+ id: job.id,
163
+ status: "failed",
164
+ output: { message: error instanceof Error ? error.message : String(error) }
165
+ });
166
+ }
167
+ }
168
+ }));
169
+ return results;
170
+ });
171
+ }
172
+ async function stop() {
173
+ if (workerId) {
174
+ await boss.offWork(workerId);
175
+ workerId = null;
176
+ }
177
+ }
178
+ return {
179
+ start,
180
+ stop
181
+ };
182
+ }
183
+ /** A pg-boss worker on the step queue's dead-letter queue. */
184
+ function createPgBossDlqWorker(deps) {
185
+ const { boss, queueName, logger } = deps;
186
+ const dlq = dlqName(queueName);
187
+ let workerId = null;
188
+ async function start(onDlq) {
189
+ workerId = await boss.work(dlq, { pollingIntervalSeconds: deps.pollingIntervalSeconds ?? 30 }, async (jobs) => {
190
+ for (const job of jobs) {
191
+ const parsed = WIRE_STEP_PAYLOAD_SCHEMA.safeParse(job.data);
192
+ if (!parsed.success) {
193
+ logger?.error("Invalid payload in DLQ", void 0, { jobId: job.id });
194
+ continue;
195
+ }
196
+ logger?.error("Step job dead-lettered", void 0, {
197
+ jobId: job.id,
198
+ workflowId: parsed.data.workflowId,
199
+ stepId: parsed.data.stepId,
200
+ stepKey: parsed.data.stepKey
201
+ });
202
+ try {
203
+ await onDlq(parsed.data);
204
+ } catch (error) {
205
+ logger?.error("DLQ handler failed", error instanceof Error ? error : void 0, { jobId: job.id });
206
+ }
207
+ }
208
+ });
209
+ }
210
+ async function stop() {
211
+ if (workerId) {
212
+ await boss.offWork(workerId);
213
+ workerId = null;
214
+ }
215
+ }
216
+ return {
217
+ start,
218
+ stop
219
+ };
220
+ }
221
+ //#endregion
222
+ //#region src/dispatcher-pgboss/scheduler.ts
223
+ /** Create the start queue (idempotent). */
224
+ async function ensureStartQueue(boss, queueName) {
225
+ try {
226
+ await boss.createQueue(queueName);
227
+ } catch (e) {
228
+ if (e instanceof Error && e.message.includes("already exists")) return;
229
+ throw e;
230
+ }
231
+ }
232
+ /** Registers cron schedules that enqueue workflow-start jobs. */
233
+ function createPgBossScheduler(deps) {
234
+ const { boss, queueName, partitionKey } = deps;
235
+ let ensured = false;
236
+ async function ensure() {
237
+ if (!ensured) {
238
+ await ensureStartQueue(boss, queueName);
239
+ ensured = true;
240
+ }
241
+ }
242
+ return {
243
+ /** Create or replace a cron schedule that starts a workflow. */
244
+ async schedule(input) {
245
+ await ensure();
246
+ const payload = {
247
+ partitionKey,
248
+ workflowType: input.workflowType,
249
+ input: input.input ?? {},
250
+ entityRef: input.entityRef,
251
+ idempotencyKey: input.idempotencyKey
252
+ };
253
+ await boss.schedule(queueName, input.cron, payload, {
254
+ key: input.key,
255
+ ...input.tz ? { tz: input.tz } : {}
256
+ });
257
+ },
258
+ /** Remove a schedule by key. */
259
+ async unschedule(key) {
260
+ await boss.unschedule(queueName, key);
261
+ },
262
+ /** List the schedules registered on this queue. */
263
+ async list() {
264
+ return (await boss.getSchedules()).filter((s) => s.name === queueName);
265
+ }
266
+ };
267
+ }
268
+ /** A pg-boss worker on the start queue that drives a host start callback. */
269
+ function createPgBossStartWorker(deps) {
270
+ const { boss, queueName, logger } = deps;
271
+ let workerId = null;
272
+ async function start(process) {
273
+ await ensureStartQueue(boss, queueName);
274
+ const workOpts = {
275
+ batchSize: deps.workerOptions?.batchSize ?? 1,
276
+ ...deps.workerOptions?.pollingIntervalSeconds != null && { pollingIntervalSeconds: deps.workerOptions.pollingIntervalSeconds }
277
+ };
278
+ workerId = await boss.work(queueName, workOpts, async (jobs) => {
279
+ for (const job of jobs) {
280
+ const parsed = WIRE_START_PAYLOAD_SCHEMA.safeParse(job.data);
281
+ if (!parsed.success) {
282
+ logger?.error("Invalid start payload", void 0, { jobId: job.id });
283
+ continue;
284
+ }
285
+ await process(parsed.data);
286
+ }
287
+ });
288
+ }
289
+ async function stop() {
290
+ if (workerId) {
291
+ await boss.offWork(workerId);
292
+ workerId = null;
293
+ }
294
+ }
295
+ return {
296
+ start,
297
+ stop
298
+ };
299
+ }
300
+ //#endregion
301
+ export { DEFAULT_STEP_QUEUE_CONFIG, WIRE_START_PAYLOAD_SCHEMA, WIRE_STEP_PAYLOAD_SCHEMA, createPgBossDispatcher, createPgBossDlqWorker, createPgBossScheduler, createPgBossStartWorker, createPgBossStepWorker, ensureStartQueue, ensureStepQueue };
@@ -0,0 +1,56 @@
1
+ import { B as WorkflowDefinition, E as StartOptions, F as StepId, I as StepRecord, K as FlowErrorShape, U as WorkflowRecord, V as WorkflowId, q as Result } from "./index-D_fOYfmf.js";
2
+ //#region src/core/hooks.d.ts
3
+ interface BeforeStartArgs {
4
+ definition: WorkflowDefinition;
5
+ input: Record<string, unknown>;
6
+ options?: StartOptions;
7
+ }
8
+ interface BeforeStartResult {
9
+ /** Extra metadata persisted on the workflow row (merged over `options.metadata`). */
10
+ metadata?: Record<string, unknown>;
11
+ }
12
+ interface BuildStepContextArgs {
13
+ workflowId: WorkflowId;
14
+ stepId: StepId;
15
+ stepKey: string;
16
+ partitionKey: string;
17
+ workflow: WorkflowRecord;
18
+ step: StepRecord;
19
+ }
20
+ interface AfterStepArgs<TContext> {
21
+ workflowId: WorkflowId;
22
+ stepId: StepId;
23
+ partitionKey: string;
24
+ workflow: WorkflowRecord;
25
+ step: StepRecord;
26
+ output: Record<string, unknown>;
27
+ /** The same context object that was handed to the step handler. */
28
+ context: TContext;
29
+ }
30
+ interface WorkflowCompletedArgs {
31
+ workflowId: WorkflowId;
32
+ partitionKey: string;
33
+ workflow: WorkflowRecord;
34
+ }
35
+ interface WorkflowHooks<TContext = unknown> {
36
+ /**
37
+ * Runs before a workflow is created. Return an error to reject the start
38
+ * (e.g. quota exceeded) — the engine passes the error through untouched.
39
+ */
40
+ onBeforeStart?(args: BeforeStartArgs): Promise<Result<BeforeStartResult, FlowErrorShape>>;
41
+ /**
42
+ * Produces the per-step `context` handed to the handler (and later to
43
+ * `onAfterStep`). This is where the AI add-on resolves and instruments a model.
44
+ * If omitted, handlers receive `undefined` as their context.
45
+ */
46
+ buildStepContext?(args: BuildStepContextArgs): TContext | Promise<TContext>;
47
+ /**
48
+ * Runs after a step handler succeeds, before the step is marked completed.
49
+ * The AI add-on reads token usage off `context` here and persists cost.
50
+ */
51
+ onAfterStep?(args: AfterStepArgs<TContext>): Promise<void>;
52
+ /** Runs once when a workflow reaches `completed` (e.g. daily usage rollup). */
53
+ onWorkflowCompleted?(args: WorkflowCompletedArgs): Promise<void>;
54
+ }
55
+ //#endregion
56
+ export { WorkflowCompletedArgs as a, BuildStepContextArgs as i, BeforeStartArgs as n, WorkflowHooks as o, BeforeStartResult as r, AfterStepArgs as t };