octaflow 0.19.0 → 0.20.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 CHANGED
@@ -85,7 +85,7 @@ npx tsx scripts/demo.ts # reproduce it
85
85
  ## Contents
86
86
 
87
87
  - [How it compares](#how-it-compares)
88
- - [When not to use Flow](#when-not-to-use-flow)
88
+ - [When not to use it](#when-not-to-use-it)
89
89
  - [Performance](#performance)
90
90
  - [Features](#features)
91
91
  - [Installation](#installation)
@@ -115,7 +115,7 @@ you already run Postgres, it adds no infrastructure — the queue ([pg-boss](htt
115
115
  is Postgres too. You give up the dashboards, the polyglot SDKs, and the operational maturity
116
116
  that the others have earned.
117
117
 
118
- ## When not to use Flow
118
+ ## When not to use it
119
119
 
120
120
  Reach for something else if:
121
121
 
@@ -130,6 +130,14 @@ Reach for something else if:
130
130
  ([`toPublicWorkflow`](https://octaflow.octabits.io/extending/http/)) and lifecycle events, not a
131
131
  dashboard. You build it.
132
132
  - **You need non-TypeScript workers.** The DAG and its schemas are TypeScript values.
133
+ - **The caller is waiting.** Every step is a persisted transition, and in production a queue hop:
134
+ a step starts when a worker fetches it — on the next poll, unless `burstWhenBatchFull` is
135
+ keeping it busy — and adds engine time on top of your handler. Anything inside a
136
+ request/response — validate, transform, answer — is a function call. Put a workflow behind a
137
+ request only to *start* it, and let the page poll or subscribe for the outcome.
138
+ - **One job with retries.** A single step that must run later and try again is a queue job;
139
+ pg-boss on its own does that with less. A DAG earns its keep when steps depend on each other,
140
+ run in parallel, or must be resumed and read back.
133
141
  - **You can't run Postgres**, or you need throughput past what a Postgres-backed queue gives you.
134
142
  - **You need a support contract**, or an API frozen by a 1.0 promise. This is pre-1.0 and 0.x
135
143
  minors can break.
@@ -203,7 +211,7 @@ next to what your steps actually do.
203
211
  | ↩️ | **Saga compensation** — run rollback handlers in reverse order on failure |
204
212
  | 🚑 | **Crash recovery** — a step whose worker died is re-queued while its attempt budget lasts |
205
213
  | 💓 | **Heartbeats** — a long step proves it is alive, so a dead one is caught in seconds, not minutes |
206
- | 🔭 | **Observability** — lifecycle events (run history) + per-step spans, both pluggable |
214
+ | 🔭 | **Observability** — lifecycle events (run history), per-step spans, and `createMeterObserver` turning the events into OpenTelemetry-style metrics — all pluggable, all structural |
207
215
  | 🤖 | **AI add-on** — instrumented models, token/cost capture, quota, daily rollups |
208
216
  | 🧱 | **Pluggable everything** — `WorkflowStore`, `Dispatcher`, `StepGate`, `FlowObserver`, hooks |
209
217
 
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { $ as computeReadiness, A as StepConditionHandler, B as StepStatus, C as defineWaitStep, D as StartOptions, E as RetryPolicy, F as StepHandlerNotFoundError, G as WorkflowNotFoundError, H as WorkflowCreatedResult, I as StepHandlerRegistry, J as WorkflowStatus, K as WorkflowNotRetryableError, L as StepId, M as StepError, N as StepExecutionContext, O as StepCompensateHandler, P as StepHandler, Q as ReadinessPlan, R as StepRecord, S as defineSubWorkflowStep, T as InvalidWorkflowDefinitionError, U as WorkflowDefinition, V as WaitTimeoutPolicy, W as WorkflowId, X as JoinRule, Y as WorkflowWithSteps, Z as PlannedSkip, _ as WorkflowOutput, a as STEP_DISPLAY_STATUS, b as defineSleepStep, c as WORKFLOW_STATUS_SCHEMA, d as toPublicWorkflow, et as isTerminalStepStatus, f as StepDeps, g as TypedWorkflow, h as TypedStepContext, i as PublicWorkflowStep, it as ok, j as StepDefinition, k as StepCompensationContext, l as toDisplayStepStatus, m as TypedStep, n as PUBLIC_WORKFLOW_STEP_SCHEMA, nt as Result, o as STEP_DISPLAY_STATUS_SCHEMA, p as StepOutput, q as WorkflowRecord, r as PublicWorkflow, rt as err, s as StepDisplayStatus, t as PUBLIC_WORKFLOW_SCHEMA, tt as FlowErrorShape, u as toPublicStep, v as buildWorkflow, w as FlowError, x as defineStep, y as defineMapStep, z as StepRegistration } from "./index-BURAYZui.js";
2
2
  import { n as Logger, r as noopLogger, t as LogAttributes } from "./logger-BCus3AOa.js";
3
- import { A as WorkflowStore, C as CreateWorkflowStep, D as ListWorkflowsFilters, E as FinishWorkflowParams, O as ReopenWorkflowParams, 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 TransactionalScope, 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-B5KgSJfF.js";
3
+ import { A as ListWorkflowsFilters, C as AddChildStep, D as CreatedWorkflow, E as CreateWorkflowStep, M as TransactionalScope, N as WorkflowStore, O as FailStepParams, S as createInMemoryStepGate, T as CreateWorkflowParams, _ as InMemoryStepGateConfig, a as FlowSpan, b as StepGateDecision, c as RecordingObserver, d as createMeterObserver, f as createRecordingObserver, g as ConcurrencyRule, h as noopTracer, i as FlowObserver, j as ReopenWorkflowParams, k as FinishWorkflowParams, l as RecordingTracer, m as noopObserver, n as FlowEventType, o as FlowTracer, p as createRecordingTracer, r as FlowMeter, s as RecordedSpan, t as FlowEvent, u as combineObservers, v as RateRule, w as CompleteStepParams, x as StepGateRequest, y as StepGate } from "./observability-Bu-SXLcS.js";
4
4
  import { i as EnqueueOptions, n as DispatchStepPayload, r as Dispatcher, t as DispatchKind } from "./dispatcher-g7Ct0J6w.js";
5
5
  import { a as WorkflowCompletedArgs, i as BuildStepContextArgs, n as BeforeStartArgs, o as WorkflowHooks, r as BeforeStartResult, t as AfterStepArgs } from "./hooks-BdxIcc92.js";
6
6
  //#region src/core/registry.d.ts
@@ -162,4 +162,4 @@ export declare function createInMemoryWorkflowStore(partitionKey?: string): Work
162
162
  };
163
163
  };
164
164
  //#endregion
165
- export { AddChildStep, AfterStepArgs, BeforeStartArgs, BeforeStartResult, BuildStepContextArgs, CompleteStepParams, ConcurrencyRule, CreateWorkflowParams, CreateWorkflowStep, CreatedWorkflow, DispatchKind, DispatchStepPayload, Dispatcher, EnqueueOptions, FailStepParams, FinishWorkflowParams, FlowError, FlowErrorShape, FlowEvent, FlowEventType, FlowObserver, FlowSpan, FlowTracer, InMemoryStepGateConfig, InvalidWorkflowDefinitionError, JoinRule, ListWorkflowsFilters, LogAttributes, Logger, PUBLIC_WORKFLOW_SCHEMA, PUBLIC_WORKFLOW_STEP_SCHEMA, PlannedSkip, PublicWorkflow, PublicWorkflowStep, RateRule, ReadinessPlan, RecordedSpan, RecordingObserver, RecordingTracer, ReopenWorkflowParams, Result, RetryPolicy, STEP_DISPLAY_STATUS, STEP_DISPLAY_STATUS_SCHEMA, StartOptions, StepCompensateHandler, StepCompensationContext, StepConditionHandler, StepDefinition, StepDeps, StepDisplayStatus, StepError, StepExecutionContext, StepGate, StepGateDecision, StepGateRequest, StepHandler, StepHandlerNotFoundError, StepHandlerRegistry, StepId, StepOutput, StepRecord, StepRegistration, StepStatus, TransactionalScope, TypedStep, TypedStepContext, TypedWorkflow, WORKFLOW_STATUS_SCHEMA, WaitTimeoutPolicy, WorkflowCompletedArgs, WorkflowCreatedResult, WorkflowDefinition, WorkflowHooks, WorkflowId, WorkflowNotFoundError, WorkflowNotRetryableError, WorkflowOutput, WorkflowRecord, WorkflowStatus, WorkflowStore, WorkflowWithSteps, buildWorkflow, computeReadiness, createInMemoryStepGate, createRecordingObserver, createRecordingTracer, defineMapStep, defineSleepStep, defineStep, defineSubWorkflowStep, defineWaitStep, err, isTerminalStepStatus, noopLogger, noopObserver, noopTracer, ok, toDisplayStepStatus, toPublicStep, toPublicWorkflow };
165
+ export { AddChildStep, AfterStepArgs, BeforeStartArgs, BeforeStartResult, BuildStepContextArgs, CompleteStepParams, ConcurrencyRule, CreateWorkflowParams, CreateWorkflowStep, CreatedWorkflow, DispatchKind, DispatchStepPayload, Dispatcher, EnqueueOptions, FailStepParams, FinishWorkflowParams, FlowError, FlowErrorShape, FlowEvent, FlowEventType, FlowMeter, FlowObserver, FlowSpan, FlowTracer, InMemoryStepGateConfig, InvalidWorkflowDefinitionError, JoinRule, ListWorkflowsFilters, LogAttributes, Logger, PUBLIC_WORKFLOW_SCHEMA, PUBLIC_WORKFLOW_STEP_SCHEMA, PlannedSkip, PublicWorkflow, PublicWorkflowStep, RateRule, ReadinessPlan, RecordedSpan, RecordingObserver, RecordingTracer, ReopenWorkflowParams, Result, RetryPolicy, STEP_DISPLAY_STATUS, STEP_DISPLAY_STATUS_SCHEMA, StartOptions, StepCompensateHandler, StepCompensationContext, StepConditionHandler, StepDefinition, StepDeps, StepDisplayStatus, StepError, StepExecutionContext, StepGate, StepGateDecision, StepGateRequest, StepHandler, StepHandlerNotFoundError, StepHandlerRegistry, StepId, StepOutput, StepRecord, StepRegistration, StepStatus, TransactionalScope, TypedStep, TypedStepContext, TypedWorkflow, WORKFLOW_STATUS_SCHEMA, WaitTimeoutPolicy, WorkflowCompletedArgs, WorkflowCreatedResult, WorkflowDefinition, WorkflowHooks, WorkflowId, WorkflowNotFoundError, WorkflowNotRetryableError, WorkflowOutput, WorkflowRecord, WorkflowStatus, WorkflowStore, WorkflowWithSteps, buildWorkflow, combineObservers, computeReadiness, createInMemoryStepGate, createMeterObserver, createRecordingObserver, createRecordingTracer, defineMapStep, defineSleepStep, defineStep, defineSubWorkflowStep, defineWaitStep, err, isTerminalStepStatus, noopLogger, noopObserver, noopTracer, ok, toDisplayStepStatus, toPublicStep, toPublicWorkflow };
package/dist/index.js CHANGED
@@ -153,6 +153,85 @@ function createRecordingObserver() {
153
153
  }
154
154
  };
155
155
  }
156
+ /** Seconds-scale duration buckets, the OpenTelemetry semantic-convention set. */
157
+ const DURATION_SECONDS_BOUNDARIES = [
158
+ .005,
159
+ .01,
160
+ .025,
161
+ .05,
162
+ .075,
163
+ .1,
164
+ .25,
165
+ .5,
166
+ .75,
167
+ 1,
168
+ 2.5,
169
+ 5,
170
+ 7.5,
171
+ 10,
172
+ 30,
173
+ 60,
174
+ 300
175
+ ];
176
+ /**
177
+ * A `FlowObserver` that turns the event stream into metrics:
178
+ *
179
+ * - `octaflow.workflow.events` (counter) — every workflow transition, by
180
+ * `workflow_type` and `event`.
181
+ * - `octaflow.step.events` (counter) — every step transition, by
182
+ * `workflow_type`, `step_key`, `step_type` and `event`.
183
+ * - `octaflow.step.duration` (histogram, seconds) — one point per
184
+ * `step.completed` / `step.failed`, by `workflow_type`, `step_key` and
185
+ * `outcome`.
186
+ *
187
+ * Attributes are the definitions' names, never ids — a workflow id in a
188
+ * metric attribute is a cardinality leak. Pass it as `observer` to the engine;
189
+ * combine with a history sink through `combineObservers`.
190
+ */
191
+ function createMeterObserver(meter) {
192
+ const workflowEvents = meter.counter("octaflow.workflow.events", {
193
+ description: "Workflow lifecycle transitions.",
194
+ unit: "{event}"
195
+ });
196
+ const stepEvents = meter.counter("octaflow.step.events", {
197
+ description: "Step lifecycle transitions.",
198
+ unit: "{event}"
199
+ });
200
+ const stepDuration = meter.histogram("octaflow.step.duration", {
201
+ description: "Wall-clock duration of one step run.",
202
+ unit: "s",
203
+ boundaries: DURATION_SECONDS_BOUNDARIES
204
+ });
205
+ return { record(event) {
206
+ const workflowType = event.workflowType ?? "";
207
+ if (event.type.startsWith("workflow.")) {
208
+ workflowEvents.add(1, {
209
+ workflow_type: workflowType,
210
+ event: event.type
211
+ });
212
+ return;
213
+ }
214
+ stepEvents.add(1, {
215
+ workflow_type: workflowType,
216
+ step_key: event.stepKey ?? "",
217
+ step_type: event.stepType ?? "",
218
+ event: event.type
219
+ });
220
+ if ((event.type === "step.completed" || event.type === "step.failed") && event.durationMs !== void 0) stepDuration.record(event.durationMs / 1e3, {
221
+ workflow_type: workflowType,
222
+ step_key: event.stepKey ?? "",
223
+ outcome: event.type === "step.completed" ? "completed" : "failed"
224
+ });
225
+ } };
226
+ }
227
+ /** Fan one event out to several observers (history sink + metrics, say). Each is guarded. */
228
+ function combineObservers(...observers) {
229
+ return { record(event) {
230
+ for (const observer of observers) try {
231
+ observer.record(event);
232
+ } catch {}
233
+ } };
234
+ }
156
235
  const noopSpan = {
157
236
  setAttribute() {},
158
237
  recordError() {},
@@ -2154,4 +2233,4 @@ function createInMemoryWorkflowStore(partitionKey = "default") {
2154
2233
  };
2155
2234
  }
2156
2235
  //#endregion
2157
- export { PUBLIC_WORKFLOW_SCHEMA, PUBLIC_WORKFLOW_STEP_SCHEMA, STEP_DISPLAY_STATUS, STEP_DISPLAY_STATUS_SCHEMA, WORKFLOW_STATUS_SCHEMA, buildWorkflow, computeReadiness, createInMemoryStepGate, createInMemoryWorkflowStore, createRecordingObserver, createRecordingTracer, createStepHandlerRegistry, createWorkflowEngine, defineMapStep, defineSleepStep, defineStep, defineSubWorkflowStep, defineWaitStep, err, explicitRetryability, isRetryableError, isTerminalStepStatus, markRetryable, nonRetryableError, noopLogger, noopObserver, noopTracer, ok, retryableError, toDisplayStepStatus, toPublicStep, toPublicWorkflow };
2236
+ export { PUBLIC_WORKFLOW_SCHEMA, PUBLIC_WORKFLOW_STEP_SCHEMA, STEP_DISPLAY_STATUS, STEP_DISPLAY_STATUS_SCHEMA, WORKFLOW_STATUS_SCHEMA, buildWorkflow, combineObservers, computeReadiness, createInMemoryStepGate, createInMemoryWorkflowStore, createMeterObserver, createRecordingObserver, createRecordingTracer, createStepHandlerRegistry, createWorkflowEngine, defineMapStep, defineSleepStep, defineStep, defineSubWorkflowStep, defineWaitStep, err, explicitRetryability, isRetryableError, isTerminalStepStatus, markRetryable, nonRetryableError, noopLogger, noopObserver, noopTracer, ok, retryableError, toDisplayStepStatus, toPublicStep, toPublicWorkflow };
@@ -316,6 +316,45 @@ interface RecordingObserver extends FlowObserver {
316
316
  clear(): void;
317
317
  }
318
318
  declare function createRecordingObserver(): RecordingObserver;
319
+ /**
320
+ * The slice of a metrics API the engine's meter observer needs. Structural:
321
+ * `@octabits-io/framework/telemetry`'s meter and an OpenTelemetry `Meter`
322
+ * both fit (the SDK's `createCounter`/`createHistogram` need a two-line
323
+ * adapter, the framework's meter none).
324
+ */
325
+ interface FlowMeter {
326
+ counter(name: string, options?: {
327
+ description?: string;
328
+ unit?: string;
329
+ }): {
330
+ add(value: number, attributes?: Record<string, string | number | boolean>): void;
331
+ };
332
+ histogram(name: string, options?: {
333
+ description?: string;
334
+ unit?: string;
335
+ boundaries?: readonly number[];
336
+ }): {
337
+ record(value: number, attributes?: Record<string, string | number | boolean>): void;
338
+ };
339
+ }
340
+ /**
341
+ * A `FlowObserver` that turns the event stream into metrics:
342
+ *
343
+ * - `octaflow.workflow.events` (counter) — every workflow transition, by
344
+ * `workflow_type` and `event`.
345
+ * - `octaflow.step.events` (counter) — every step transition, by
346
+ * `workflow_type`, `step_key`, `step_type` and `event`.
347
+ * - `octaflow.step.duration` (histogram, seconds) — one point per
348
+ * `step.completed` / `step.failed`, by `workflow_type`, `step_key` and
349
+ * `outcome`.
350
+ *
351
+ * Attributes are the definitions' names, never ids — a workflow id in a
352
+ * metric attribute is a cardinality leak. Pass it as `observer` to the engine;
353
+ * combine with a history sink through `combineObservers`.
354
+ */
355
+ declare function createMeterObserver(meter: FlowMeter): FlowObserver;
356
+ /** Fan one event out to several observers (history sink + metrics, say). Each is guarded. */
357
+ declare function combineObservers(...observers: FlowObserver[]): FlowObserver;
319
358
  /** A minimal span handle. Maps 1:1 onto an OpenTelemetry span in an adapter. */
320
359
  interface FlowSpan {
321
360
  setAttribute(key: string, value: string | number | boolean): void;
@@ -342,4 +381,4 @@ interface RecordingTracer extends FlowTracer {
342
381
  }
343
382
  declare function createRecordingTracer(): RecordingTracer;
344
383
  //#endregion
345
- export { WorkflowStore as A, CreateWorkflowStep as C, ListWorkflowsFilters as D, FinishWorkflowParams as E, ReopenWorkflowParams 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, TransactionalScope 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 };
384
+ export { ListWorkflowsFilters as A, AddChildStep as C, CreatedWorkflow as D, CreateWorkflowStep as E, TransactionalScope as M, WorkflowStore as N, FailStepParams as O, createInMemoryStepGate as S, CreateWorkflowParams as T, InMemoryStepGateConfig as _, FlowSpan as a, StepGateDecision as b, RecordingObserver as c, createMeterObserver as d, createRecordingObserver as f, ConcurrencyRule as g, noopTracer as h, FlowObserver as i, ReopenWorkflowParams as j, FinishWorkflowParams as k, RecordingTracer as l, noopObserver as m, FlowEventType as n, FlowTracer as o, createRecordingTracer as p, FlowMeter as r, RecordedSpan as s, FlowEvent as t, combineObservers as u, RateRule as v, CompleteStepParams as w, StepGateRequest as x, StepGate as y };
@@ -1,5 +1,5 @@
1
1
  import { W as WorkflowId } from "../index-BURAYZui.js";
2
- import { A as WorkflowStore, g as StepGate, h as RateRule, p as ConcurrencyRule, r as FlowObserver, t as FlowEvent } from "../observability-B5KgSJfF.js";
2
+ import { N as WorkflowStore, g as ConcurrencyRule, i as FlowObserver, t as FlowEvent, v as RateRule, y as StepGate } from "../observability-Bu-SXLcS.js";
3
3
  import { Pool } from "pg";
4
4
  //#region src/store-pg/ddl.d.ts
5
5
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "octaflow",
3
- "version": "0.19.0",
3
+ "version": "0.20.1",
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",