@dudousxd/nestjs-durable-core 0.27.0 → 0.28.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.
Files changed (113) hide show
  1. package/LICENSE +21 -0
  2. package/dist/index.cjs +3923 -0
  3. package/dist/index.cjs.map +1 -0
  4. package/dist/index.d.cts +2044 -0
  5. package/dist/index.d.ts +2044 -20
  6. package/dist/index.js +3857 -38
  7. package/dist/index.js.map +1 -1
  8. package/package.json +19 -5
  9. package/dist/backoff.d.ts +0 -5
  10. package/dist/backoff.d.ts.map +0 -1
  11. package/dist/backoff.js +0 -14
  12. package/dist/backoff.js.map +0 -1
  13. package/dist/checkpoints.d.ts +0 -17
  14. package/dist/checkpoints.d.ts.map +0 -1
  15. package/dist/checkpoints.js +0 -28
  16. package/dist/checkpoints.js.map +0 -1
  17. package/dist/codec-state-store.d.ts +0 -57
  18. package/dist/codec-state-store.d.ts.map +0 -1
  19. package/dist/codec-state-store.js +0 -111
  20. package/dist/codec-state-store.js.map +0 -1
  21. package/dist/completion.d.ts +0 -15
  22. package/dist/completion.d.ts.map +0 -1
  23. package/dist/completion.js +0 -13
  24. package/dist/completion.js.map +0 -1
  25. package/dist/duration.d.ts +0 -6
  26. package/dist/duration.d.ts.map +0 -1
  27. package/dist/duration.js +0 -42
  28. package/dist/duration.js.map +0 -1
  29. package/dist/engine.d.ts +0 -525
  30. package/dist/engine.d.ts.map +0 -1
  31. package/dist/engine.js +0 -1773
  32. package/dist/engine.js.map +0 -1
  33. package/dist/entities.d.ts +0 -44
  34. package/dist/entities.d.ts.map +0 -1
  35. package/dist/entities.js +0 -68
  36. package/dist/entities.js.map +0 -1
  37. package/dist/errors.d.ts +0 -56
  38. package/dist/errors.d.ts.map +0 -1
  39. package/dist/errors.js +0 -93
  40. package/dist/errors.js.map +0 -1
  41. package/dist/event-accumulators.d.ts +0 -39
  42. package/dist/event-accumulators.d.ts.map +0 -1
  43. package/dist/event-accumulators.js +0 -74
  44. package/dist/event-accumulators.js.map +0 -1
  45. package/dist/events.d.ts +0 -9
  46. package/dist/events.d.ts.map +0 -1
  47. package/dist/events.js +0 -41
  48. package/dist/events.js.map +0 -1
  49. package/dist/index.d.ts.map +0 -1
  50. package/dist/interfaces.d.ts +0 -830
  51. package/dist/interfaces.d.ts.map +0 -1
  52. package/dist/interfaces.js +0 -3
  53. package/dist/interfaces.js.map +0 -1
  54. package/dist/metrics.d.ts +0 -44
  55. package/dist/metrics.d.ts.map +0 -1
  56. package/dist/metrics.js +0 -76
  57. package/dist/metrics.js.map +0 -1
  58. package/dist/protocol.d.ts +0 -17
  59. package/dist/protocol.d.ts.map +0 -1
  60. package/dist/protocol.js +0 -53
  61. package/dist/protocol.js.map +0 -1
  62. package/dist/queue.d.ts +0 -48
  63. package/dist/queue.d.ts.map +0 -1
  64. package/dist/queue.js +0 -45
  65. package/dist/queue.js.map +0 -1
  66. package/dist/remote-step-factory.d.ts +0 -15
  67. package/dist/remote-step-factory.d.ts.map +0 -1
  68. package/dist/remote-step-factory.js +0 -15
  69. package/dist/remote-step-factory.js.map +0 -1
  70. package/dist/remote-workflow-executor.d.ts +0 -24
  71. package/dist/remote-workflow-executor.d.ts.map +0 -1
  72. package/dist/remote-workflow-executor.js +0 -73
  73. package/dist/remote-workflow-executor.js.map +0 -1
  74. package/dist/scheduler.d.ts +0 -42
  75. package/dist/scheduler.d.ts.map +0 -1
  76. package/dist/scheduler.js +0 -70
  77. package/dist/scheduler.js.map +0 -1
  78. package/dist/search-attributes.d.ts +0 -17
  79. package/dist/search-attributes.d.ts.map +0 -1
  80. package/dist/search-attributes.js +0 -47
  81. package/dist/search-attributes.js.map +0 -1
  82. package/dist/step-logger.d.ts +0 -9
  83. package/dist/step-logger.d.ts.map +0 -1
  84. package/dist/step-logger.js +0 -92
  85. package/dist/step-logger.js.map +0 -1
  86. package/dist/test-helpers.d.ts +0 -11
  87. package/dist/test-helpers.d.ts.map +0 -1
  88. package/dist/test-helpers.js +0 -15
  89. package/dist/test-helpers.js.map +0 -1
  90. package/dist/testing/in-memory-state-store.d.ts +0 -37
  91. package/dist/testing/in-memory-state-store.d.ts.map +0 -1
  92. package/dist/testing/in-memory-state-store.js +0 -134
  93. package/dist/testing/in-memory-state-store.js.map +0 -1
  94. package/dist/testing/in-memory-transport.d.ts +0 -25
  95. package/dist/testing/in-memory-transport.d.ts.map +0 -1
  96. package/dist/testing/in-memory-transport.js +0 -53
  97. package/dist/testing/in-memory-transport.js.map +0 -1
  98. package/dist/tokens.d.ts +0 -8
  99. package/dist/tokens.d.ts.map +0 -1
  100. package/dist/tokens.js +0 -11
  101. package/dist/tokens.js.map +0 -1
  102. package/dist/transport-pool.d.ts +0 -30
  103. package/dist/transport-pool.d.ts.map +0 -1
  104. package/dist/transport-pool.js +0 -86
  105. package/dist/transport-pool.js.map +0 -1
  106. package/dist/workflow-ctx.d.ts +0 -52
  107. package/dist/workflow-ctx.d.ts.map +0 -1
  108. package/dist/workflow-ctx.js +0 -432
  109. package/dist/workflow-ctx.js.map +0 -1
  110. package/dist/workflow-ref.d.ts +0 -34
  111. package/dist/workflow-ref.d.ts.map +0 -1
  112. package/dist/workflow-ref.js +0 -32
  113. package/dist/workflow-ref.js.map +0 -1
@@ -1,830 +0,0 @@
1
- import type { z } from 'zod';
2
- import type { WorkflowClass, WorkflowInputOf, WorkflowOutputOf } from './workflow-ref';
3
- /**
4
- * Core type contracts for nestjs-durable.
5
- *
6
- * These are intentionally framework-agnostic: `@dudousxd/nestjs-durable-core` knows only
7
- * these interfaces, never a concrete transport, store or ORM. Adapters implement them.
8
- */
9
- export type RunStatus =
10
- /** Created + enqueued by `start`, not yet picked up — a worker will lease and execute it. */
11
- 'pending' | 'running' | 'suspended' | 'completed' | 'failed' | 'cancelled'
12
- /** Dead-letter: recovery gave up after `maxRecoveryAttempts` (a poison pill). Terminal; inspect it. */
13
- | 'dead';
14
- /** One execution of a workflow. The unit of durability and the unit shown in the dashboard. */
15
- export interface WorkflowRun {
16
- id: string;
17
- /** Registered workflow name, e.g. `checkout`. */
18
- workflow: string;
19
- /** Code version at start; old runs must resume on the version they began on. */
20
- workflowVersion: string;
21
- status: RunStatus;
22
- /** Serialized workflow input (the args the run was started with). */
23
- input: unknown;
24
- /** Serialized workflow output, once `completed`. */
25
- output?: unknown;
26
- /** Structured error, once `failed`. */
27
- error?: StepError;
28
- /** When `suspended` on a durable sleep: epoch ms at which the run becomes due to resume. */
29
- wakeAt?: number;
30
- /** Recovery lease owner (engine instance id) while a run is being resumed. */
31
- lockedBy?: string;
32
- /** Recovery lease expiry (epoch ms); another instance may take over once it passes. */
33
- lockedUntil?: number;
34
- /** How many times crash-recovery has picked this run up — caps poison pills (see maxRecoveryAttempts). */
35
- recoveryAttempts?: number;
36
- /** Searchable labels: the workflow's static `@Workflow({ tags })` merged with the run's start-time tags. */
37
- tags?: string[];
38
- /** Typed, queryable run data (e.g. `{ amount: 200, tier: 'pro' }`) — see {@link RunQuery.attributes}. */
39
- searchAttributes?: SearchAttributes;
40
- createdAt: Date;
41
- updatedAt: Date;
42
- }
43
- export type StepKind = 'local' | 'remote' | 'sleep' | 'signal';
44
- /**
45
- * The recorded result of a single step at a deterministic logical position (`seq`).
46
- * On replay, a `completed` checkpoint means the step is NOT re-executed — its `output` is
47
- * returned. A non-terminal checkpoint (`pending`/`running`) does not short-circuit: the step is
48
- * re-awaited (remote) or re-run (local).
49
- */
50
- export interface StepCheckpoint {
51
- runId: string;
52
- /** Deterministic logical position of the step within the run. */
53
- seq: number;
54
- /** Registered step name (matches the remote handler name for remote steps). */
55
- name: string;
56
- kind: StepKind;
57
- /** Stable id passed to remote workers so they can dedupe a re-delivered task. */
58
- stepId: string;
59
- /**
60
- * `pending` = a remote step dispatched and awaiting its worker result (the run is durably
61
- * suspended, not held in memory); it becomes `completed`/`failed` when the result arrives.
62
- * `running` = a local step whose body is executing in-process right now (see `trackStepStart`);
63
- * it's overwritten by `completed`/`failed` when the body settles, and on a crash mid-body it
64
- * simply re-runs on replay (only `completed` short-circuits).
65
- */
66
- status: 'pending' | 'running' | 'completed' | 'failed';
67
- /** What the step was called with — the `ctx.call` args for a remote step (a local step has none). */
68
- input?: unknown;
69
- output?: unknown;
70
- error?: StepError;
71
- attempts: number;
72
- /** For remote steps: which worker group ran it. */
73
- workerGroup?: string;
74
- /** Structured events/logs the step emitted (sub-step outcomes, debug/error lines). */
75
- events?: StepEvent[];
76
- /** For sleep steps: epoch ms the sleep elapses at. */
77
- wakeAt?: number;
78
- /**
79
- * When the step entered the system: for a remote step, when the engine dispatched it to the
80
- * transport; for a local step, when it began. Queue-wait time = `startedAt − enqueuedAt`.
81
- */
82
- enqueuedAt: Date;
83
- /** When processing actually began: worker pickup for a remote step, execution start for a local one. */
84
- startedAt: Date;
85
- finishedAt: Date;
86
- }
87
- /**
88
- * A structured event a step (or its worker) emits while running — a log line and/or a sub-step
89
- * outcome. The dashboard renders these under the step, so you can see what happened inside a step
90
- * that the workflow treats as one unit (e.g. which of N parallel sub-processes ok/failed/skipped).
91
- */
92
- export interface StepEvent {
93
- /** Epoch ms. */
94
- at: number;
95
- level: 'debug' | 'info' | 'warn' | 'error';
96
- message: string;
97
- /** Stable run identity for a sub-process. Distinct invocations of the same `name` carry distinct
98
- * ids, so their phases and log trails never collapse into one. Absent on events emitted by the
99
- * legacy `sub()` path, which keys by `name` instead. */
100
- subId?: string;
101
- /** For a sub-step/sub-process within the step: its name. */
102
- name?: string;
103
- /** Open, consumer-defined grouping label for a sub-process (e.g. a handler/lane). The dashboard
104
- * groups rows by it. The library never interprets it. */
105
- group?: string;
106
- /** For a sub-step: its terminal outcome (closed enum — drives colour + aggregation). */
107
- status?: 'ok' | 'failed' | 'skipped';
108
- /** Open, consumer-defined intermediate phase label for a sub-process transition. Carries no
109
- * terminal `status`; the library timestamps and orders it but never interprets it. */
110
- phase?: string;
111
- /** For a log line emitted *inside* a sub-process: that owning sub-process's name, so the dashboard
112
- * can group a step's log trail under each sub-process instead of one flat list. Set on logs (no
113
- * `status`); a worker stamps it from the sub-process it's running.
114
- * @deprecated Superseded by `subId` for run-distinct grouping; kept so existing workers/runs render. */
115
- process?: string;
116
- /** Optional structured payload. `data.durationMs` (number) overrides the derived duration. */
117
- data?: unknown;
118
- }
119
- /**
120
- * Handed to a local step's body (`ctx.step(name, (log) => …)`) so it can record what happened
121
- * inside the step — debug/info/warn/error lines and per-sub-process outcomes. The events are
122
- * checkpointed with the step and rendered under it in the dashboard. The remote/cross-language
123
- * counterpart is the worker attaching the same `StepEvent[]` to its `StepResult` (see the Python
124
- * SDK's `StepContext`), so observability is symmetric regardless of where the step ran.
125
- */
126
- export interface StepLogger {
127
- debug(message: string, data?: unknown): void;
128
- info(message: string, data?: unknown): void;
129
- warn(message: string, data?: unknown): void;
130
- error(message: string, data?: unknown): void;
131
- /** Record a sub-step / sub-process outcome (e.g. one of N parallel p-processes). */
132
- sub(name: string, status: 'ok' | 'failed' | 'skipped', message?: string, data?: unknown): void;
133
- /** Record a sub-process event. Typically pass `phase` for an intermediate transition (carrying no
134
- * terminal status), or `status` for the terminal outcome — the type permits either; the engine
135
- * interprets which is set. `id` is the run identity (distinct per invocation); `group` is an
136
- * open grouping label. */
137
- subEvent(e: {
138
- id: string;
139
- name: string;
140
- group?: string;
141
- phase?: string;
142
- status?: 'ok' | 'failed' | 'skipped';
143
- message?: string;
144
- data?: unknown;
145
- }): void;
146
- /**
147
- * Ergonomic sub-process lifecycle: run `body`, timing it, and record a terminal `ok` with the
148
- * measured `durationMs` on success — or `failed` (with the error message) if it throws, then
149
- * re-throw. `sp.phase(label)` records an intermediate transition; `sp.skip(reason)` a terminal
150
- * `skipped`. Logs emitted inside `body` are tagged to this sub-process so the dashboard groups
151
- * them under it. Returns whatever `body` returns. The TS twin of the Python SDK's `sub_process`.
152
- *
153
- * ```ts
154
- * const rows = await log.subProcess('fetch-data', async () => readEverything());
155
- * await log.subProcess('export-file', () => upload(rows));
156
- * ```
157
- */
158
- subProcess<T>(name: string, body: (sp: SubProcessHandle) => Promise<T> | T, opts?: {
159
- group?: string;
160
- id?: string;
161
- }): Promise<T>;
162
- }
163
- /** The handle a {@link StepLogger.subProcess} body receives to mark phases / a non-`ok` outcome. */
164
- export interface SubProcessHandle {
165
- /** Record an intermediate phase transition (a consumer-defined label, no terminal status). */
166
- phase(phase: string, data?: unknown): SubProcessHandle;
167
- /** Record a terminal `skipped` outcome (e.g. nothing to do / validation failed). */
168
- skip(reason?: string, data?: unknown): void;
169
- /** Record a terminal `failed` outcome explicitly (the wrapper also does this if the body throws). */
170
- fail(reason?: string, data?: unknown): void;
171
- }
172
- export interface StepError {
173
- message: string;
174
- /** Optional machine-readable code, e.g. `declined`, `timeout`. */
175
- code?: string;
176
- /** Whether the engine should treat this as retryable. */
177
- retryable?: boolean;
178
- stack?: string;
179
- }
180
- export interface StateStore {
181
- /**
182
- * Provision the tables/collections this store needs, idempotently. Called on boot when the
183
- * module's `autoSchema` is on. Optional: stores that need no setup (in-memory) omit it.
184
- */
185
- ensureSchema?(): Promise<void>;
186
- createRun(run: WorkflowRun): Promise<void>;
187
- updateRun(runId: string, patch: Partial<WorkflowRun>): Promise<void>;
188
- getRun(runId: string): Promise<WorkflowRun | null>;
189
- getCheckpoint(runId: string, seq: number): Promise<StepCheckpoint | null>;
190
- /**
191
- * Persist a checkpoint and advance the run atomically. Durable semantics depend on this
192
- * being a single transaction; stores without transactions cannot give the strong guarantee.
193
- */
194
- saveCheckpoint(checkpoint: StepCheckpoint): Promise<void>;
195
- /** Used by recovery on boot to find runs to resume (crashed, left `running`). */
196
- listIncompleteRuns(): Promise<WorkflowRun[]>;
197
- /** The oldest `pending` runs awaiting dispatch (FIFO, by `createdAt`), capped at `limit`. */
198
- listPendingRuns(limit: number): Promise<WorkflowRun[]>;
199
- /** Suspended runs whose durable timer is due (`wakeAt <= nowMs`), ready to resume. */
200
- listDueTimers(nowMs: number): Promise<WorkflowRun[]>;
201
- /**
202
- * Atomically acquire the recovery lease on a run for `owner` until `leaseUntilMs`, but only if
203
- * it is currently unlocked or its lease has expired (`<= nowMs`). Returns whether it was
204
- * acquired — so concurrent engine instances never recover the same run twice.
205
- */
206
- tryLockRun(runId: string, owner: string, leaseUntilMs: number, nowMs: number): Promise<boolean>;
207
- /** Release a run's recovery lease so another instance can pick it up (e.g. once it suspends). */
208
- releaseRunLock(runId: string): Promise<void>;
209
- /**
210
- * Extend a run's lease to `leaseUntilMs`, but ONLY if `owner` still holds it — so a live worker
211
- * heartbeating its long run keeps the lease, while a dead worker's lease still expires and gets
212
- * reclaimed. Returns false if the lease was lost (taken over or released).
213
- */
214
- renewRunLock(runId: string, owner: string, leaseUntilMs: number): Promise<boolean>;
215
- /** Record that a run is suspended waiting for an external signal `token`. */
216
- putSignalWaiter(waiter: SignalWaiter): Promise<void>;
217
- /** Atomically take (and remove) the run waiting on `token`, if any. */
218
- takeSignalWaiter(token: string): Promise<SignalWaiter | null>;
219
- /** List waiters whose `token` starts with `prefix` — used to fan out an event to its subscribers. */
220
- listSignalWaiters(prefix: string): Promise<SignalWaiter[]>;
221
- /**
222
- * Buffer a signal whose waiter hasn't arrived yet, so the next `waitForSignal(token)` consumes it
223
- * instead of it being lost (FIFO per token). Makes signals reliable regardless of timing and
224
- * powers `signalWithStart`.
225
- */
226
- bufferSignal(token: string, payload: unknown): Promise<void>;
227
- /** Take the oldest buffered signal for `token` (FIFO), or null if none is buffered. */
228
- takeBufferedSignal(token: string): Promise<{
229
- payload: unknown;
230
- } | null>;
231
- /**
232
- * Run `work` in a SINGLE store transaction — giving it the store-native transaction handle (`raw`)
233
- * for the caller's own DB writes plus a `saveCheckpoint` that commits IN THE SAME transaction, so a
234
- * business write and the step's "done" checkpoint are atomic (exactly-once). Optional — only the SQL
235
- * adapters implement it; `ctx.transaction` errors on a store without it.
236
- */
237
- transaction?<T>(work: (tx: StoreTransaction) => Promise<T>): Promise<T>;
238
- listRuns(query: RunQuery): Promise<WorkflowRun[]>;
239
- listCheckpoints(runId: string): Promise<StepCheckpoint[]>;
240
- }
241
- /** Typed, queryable per-run data — exact values for `eq`/`ne`, numbers/strings for range ops. */
242
- export type SearchAttributes = Record<string, string | number | boolean>;
243
- export type AttributeOp = 'eq' | 'ne' | 'gt' | 'gte' | 'lt' | 'lte';
244
- /** One predicate over a run's {@link SearchAttributes}; a {@link RunQuery} ANDs them all. */
245
- export interface AttributeFilter {
246
- key: string;
247
- op: AttributeOp;
248
- value: string | number | boolean;
249
- }
250
- export interface RunQuery {
251
- workflow?: string;
252
- status?: RunStatus;
253
- /** Only runs carrying this tag (exact match against {@link WorkflowRun.tags}). */
254
- tag?: string;
255
- /**
256
- * Typed/range predicates over {@link WorkflowRun.searchAttributes}, ANDed together (e.g. `amount`
257
- * >= 200 and `tier` = 'pro'). Applied in-process after the coarse filters, so pair with
258
- * `workflow`/`status`/`tag` to bound the scan on large stores.
259
- */
260
- attributes?: AttributeFilter[];
261
- limit?: number;
262
- offset?: number;
263
- }
264
- /** The transaction handle `StateStore.transaction` hands to its work callback. */
265
- export interface StoreTransaction {
266
- /** The store-native transaction handle (TypeORM `EntityManager`, Prisma tx client, MikroORM `EntityManager`,
267
- * Drizzle tx) — do your business DB writes on THIS so they commit atomically with the checkpoint. */
268
- readonly raw: unknown;
269
- /** Persist the step checkpoint inside this transaction. */
270
- saveCheckpoint(checkpoint: StepCheckpoint): Promise<void>;
271
- }
272
- /** Binds an external signal `token` to the suspended run/step position waiting for it. */
273
- export interface SignalWaiter {
274
- token: string;
275
- runId: string;
276
- seq: number;
277
- }
278
- /** A unit of work dispatched to a remote worker. This is the documented wire payload. */
279
- export interface RemoteTask {
280
- runId: string;
281
- seq: number;
282
- /** Handler name the worker registered, e.g. `payments.charge-card`. */
283
- name: string;
284
- stepId: string;
285
- /** Worker group expected to handle this task. */
286
- group: string;
287
- input: unknown;
288
- /** W3C traceparent so the worker can continue the distributed trace. */
289
- traceparent?: string;
290
- /**
291
- * Opaque context carrier (tenant / user / correlation ids) the worker re-exposes to the step
292
- * handler, for cross-process propagation alongside the {@link traceparent}. The engine treats it
293
- * as a pass-through object and never inspects its shape — the producer (e.g. `@dudousxd/nestjs-context`)
294
- * owns the keys. Absent when no `context` provider is configured.
295
- */
296
- context?: Record<string, unknown>;
297
- /**
298
- * Id of the transport this task was dispatched on (when the engine runs a pool — see
299
- * {@link NamedTransport}). A worker that consumes several transports replies via the matching one,
300
- * so failover is symmetric without the worker choosing a transport. Absent for a single transport.
301
- */
302
- transport?: string;
303
- attempt: number;
304
- }
305
- export interface StepResult {
306
- runId: string;
307
- seq: number;
308
- stepId: string;
309
- status: 'completed' | 'failed';
310
- output?: unknown;
311
- error?: StepError;
312
- /** Epoch ms when the worker began processing — lets the engine report queue-wait time. */
313
- startedAt?: number;
314
- /** Structured events the worker emitted while running the step (sub-step outcomes, logs). */
315
- events?: StepEvent[];
316
- }
317
- export interface Heartbeat {
318
- runId: string;
319
- seq: number;
320
- stepId: string;
321
- group: string;
322
- }
323
- /** engine → workflow worker: advance this run one turn by replaying the function against `history`. */
324
- export interface WorkflowTask {
325
- /** Dedupe id for this turn (a re-delivered task must be idempotent). */
326
- taskId: string;
327
- runId: string;
328
- /** Registered workflow name + the version the run started on — replay must use that version. */
329
- workflow: string;
330
- workflowVersion: string;
331
- input: unknown;
332
- /** Completed durable ops so far, ordered by seq — what the worker replays its results from. */
333
- history: HistoryEvent[];
334
- /** Signals delivered to the run but not yet consumed, so `wait_signal` resolves on replay. */
335
- pendingSignals?: Array<{
336
- seq: number;
337
- signal: string;
338
- payload: unknown;
339
- }>;
340
- group: string;
341
- /** Id of the transport this task was dispatched on (pool failover) — see {@link NamedTransport}. */
342
- transport?: string;
343
- traceparent?: string;
344
- attempt: number;
345
- }
346
- /** One resolved durable op in a run's history — a superset of a completed {@link StepCheckpoint}. */
347
- export interface HistoryEvent {
348
- seq: number;
349
- kind: 'step' | 'call' | 'timer' | 'signal' | 'child';
350
- name?: string;
351
- /** Resolved value: a step/call output, a child run's output, a signal payload. */
352
- output?: unknown;
353
- /** Set when the op resolved to a failure (e.g. a failed remote step the workflow may catch). */
354
- error?: StepError;
355
- }
356
- /** A decision the workflow function produced at a `seq` that was not yet in history. */
357
- export type WorkflowCommand =
358
- /** `ctx.call(remoteStep, input)` — dispatch a remote step and await it. */
359
- {
360
- kind: 'call';
361
- seq: number;
362
- name: string;
363
- group: string;
364
- input: unknown;
365
- }
366
- /** `ctx.step(name, body)` — a LOCAL step the worker already ran this turn; the engine persists its
367
- * result so replay returns it instead of re-running (durability for side-effectful work).
368
- * `startedAt`/`finishedAt` (epoch ms) carry the step's real wall-clock window so the dashboard
369
- * shows a true duration instead of 0ms, and `events` carry the sub-process/log trail the step
370
- * emitted (so each handler's p-processes show under it). All optional for back-compat. */
371
- | {
372
- kind: 'recordStep';
373
- seq: number;
374
- name: string;
375
- output?: unknown;
376
- error?: StepError;
377
- startedAt?: number;
378
- finishedAt?: number;
379
- events?: StepEvent[];
380
- }
381
- /** `ctx.sleep(ms)` — a durable timer of `ms` duration. The engine computes the absolute deadline
382
- * (now + ms) when it applies the command, so the worker never reads the clock (determinism). */
383
- | {
384
- kind: 'sleep';
385
- seq: number;
386
- ms: number;
387
- }
388
- /** `ctx.wait_signal(name)` — block until a signal `name` is delivered to the run. */
389
- | {
390
- kind: 'waitSignal';
391
- seq: number;
392
- signal: string;
393
- }
394
- /** `ctx.start_child(workflow, input)` — start a child run with its own lifecycle. */
395
- | {
396
- kind: 'startChild';
397
- seq: number;
398
- workflow: string;
399
- input: unknown;
400
- };
401
- /** workflow worker → engine: the result of replaying one turn of a remote workflow. */
402
- export interface WorkflowDecision {
403
- taskId: string;
404
- runId: string;
405
- /** `continue` = produced `commands` and is blocked on an await; otherwise the run settles. */
406
- status: 'continue' | 'completed' | 'failed';
407
- /** New durable ops the replay produced this turn (status === 'continue'), ordered by seq. */
408
- commands: WorkflowCommand[];
409
- /** Final workflow output (status === 'completed'). */
410
- output?: unknown;
411
- /** Terminal error (status === 'failed'). */
412
- error?: StepError;
413
- }
414
- /**
415
- * workflow worker → engine: a LOCAL step's lifecycle, streamed AS IT HAPPENS (not batched into the
416
- * turn's final {@link WorkflowDecision}). A Python `@workflow` runs its `ctx.step`s inline over one
417
- * turn that can last minutes; without this the engine learns of the steps only when the turn ends,
418
- * so the dashboard shows nothing mid-run. The worker emits `running` when a step's body starts and
419
- * `completed`/`failed` when it settles; the engine checkpoints each immediately, so a step appears
420
- * in-flight and then resolves live. The turn's final `recordStep` command re-persists the same
421
- * checkpoint idempotently (replay history), so this is purely additive observability.
422
- */
423
- export interface WorkflowStepEvent {
424
- runId: string;
425
- seq: number;
426
- name: string;
427
- phase: 'running' | 'completed' | 'failed';
428
- /** Epoch ms the step body began (all phases) and settled (`completed`/`failed`). */
429
- startedAt: number;
430
- finishedAt?: number;
431
- /** The replayed result / failure for the settled phases. */
432
- output?: unknown;
433
- error?: StepError;
434
- /** Sub-process + log trail the step emitted so far (the handler's p-processes). */
435
- events?: StepEvent[];
436
- }
437
- /**
438
- * Advances a workflow run one turn. The engine has one per workflow: the default {@link InProcess}
439
- * one runs a registered TS function with the in-process replay machinery; a remote one dispatches a
440
- * {@link WorkflowTask} to a worker (Python) and awaits its {@link WorkflowDecision}. Either way the
441
- * engine applies the returned decision — so recovery, timers, singleton and dead-letter stay engine
442
- * concerns, identical for in-process and remote workflows.
443
- */
444
- export interface WorkflowExecutor {
445
- advance(run: WorkflowRun, history: HistoryEvent[], pendingSignals?: WorkflowTask['pendingSignals']): Promise<WorkflowDecision>;
446
- }
447
- /**
448
- * A transport in an ordered pool, identified by `id`. The engine dispatches on the first by default
449
- * and fails over to the next on a dispatch error; a step can pin one via `ctx.call(…, { transport })`.
450
- * The chosen `id` is stamped on the {@link RemoteTask} so a worker replies through the matching one.
451
- */
452
- export interface NamedTransport {
453
- id: string;
454
- transport: Transport;
455
- }
456
- /**
457
- * Decides where a freshly-`start`ed run executes. `start` creates the run as `pending` and hands its
458
- * id here instead of running the body inline — so the API/caller never blocks on workflow execution.
459
- * The default in-process dispatcher runs it on this instance (a microtask); a broker-backed one
460
- * enqueues the id for a worker pool to consume (`engine.runOne(runId)`); a no-op one leaves it
461
- * `pending` in the store for a worker's `runPending` poll to pick up (DB-only, caller-doesn't-execute).
462
- */
463
- export interface RunDispatcher {
464
- dispatch(runId: string): void | Promise<void>;
465
- }
466
- export interface Transport {
467
- /** engine → worker */
468
- dispatch(task: RemoteTask): Promise<void>;
469
- /** worker → engine: a step finished (ok or error). */
470
- onResult(handler: (result: StepResult) => Promise<void>): void;
471
- /** worker → engine: liveness signal for an in-flight long step. */
472
- onHeartbeat(handler: (beat: Heartbeat) => Promise<void>): void;
473
- /** Release the transport's resources (broker workers, queues, connections) for a clean shutdown.
474
- * Optional — an in-process transport has nothing to close. Called on `onApplicationShutdown`
475
- * after the engine drains, so a deploy hands off instead of leaving the broker to time out. */
476
- close?(): Promise<void>;
477
- /** engine → workflow worker: dispatch a {@link WorkflowTask} (the polyglot-workflow path). Optional
478
- * — only transports that carry workflow tasks (BullMQ) implement it; the {@link RemoteWorkflowExecutor}
479
- * uses it + {@link onDecision} to advance a remote workflow over the broker. */
480
- dispatchWorkflowTask?(task: WorkflowTask): Promise<void>;
481
- /** workflow worker → engine: a replayed turn's {@link WorkflowDecision}. Pair with dispatchWorkflowTask. */
482
- onDecision?(handler: (decision: WorkflowDecision) => Promise<void>): void;
483
- /** workflow worker → engine: a LOCAL step's {@link WorkflowStepEvent}, streamed mid-turn so the
484
- * engine can checkpoint it live. Point-to-point (a single engine instance consumes each event and
485
- * persists it once — no cross-pod duplicate writes). Optional; only broker transports carry it. */
486
- dispatchStepEvent?(event: WorkflowStepEvent): Promise<void>;
487
- /** engine ← workflow worker: consume streamed {@link WorkflowStepEvent}s. Pair with dispatchStepEvent. */
488
- onStepEvent?(handler: (event: WorkflowStepEvent) => Promise<void>): void;
489
- /** Worker-health for a group: queue backlog + live worker heartbeats. Optional — only broker
490
- * transports (BullMQ) that can introspect the task queue and the worker-heartbeat keys implement
491
- * it. The engine aggregates this across its groups in {@link WorkflowEngine.workerHealth}. */
492
- groupHealth?(group: string): Promise<GroupHealth>;
493
- /** Distinct groups that currently have a live worker heartbeat — discovered from the heartbeat
494
- * keyspace, so a group with workers but no engine-side registration (e.g. a local-step group)
495
- * still surfaces. Pairs with {@link groupHealth}. */
496
- listWorkerGroups?(): Promise<string[]>;
497
- }
498
- /** One worker's liveness record — a TTL'd heartbeat a worker refreshes while it's consuming. Its
499
- * ABSENCE (the key expired) is the signal: a worker that died or stalled stops refreshing. */
500
- export interface WorkerHeartbeat {
501
- /** The worker group this instance serves (e.g. `pipeline`, `processing-workflows`). */
502
- group: string;
503
- /** Stable per-process id (host + pid), so N replicas of a group each show as a distinct worker. */
504
- instanceId: string;
505
- /** Epoch ms of the worker's most recent heartbeat. */
506
- lastBeatAt: number;
507
- }
508
- /** Per-group worker-health snapshot: how much work is queued vs. how many workers are alive to do it.
509
- * The actionable alert state is `depth > 0 && liveWorkers.length === 0` — work piling up with no
510
- * consumer (exactly the failure where a worker is "alive but not consuming"). */
511
- export interface GroupHealth {
512
- group: string;
513
- /** Outstanding jobs in the group's task queue (waiting + active + delayed + prioritized). */
514
- depth: number;
515
- /** Workers with a non-expired heartbeat for this group. */
516
- liveWorkers: WorkerHeartbeat[];
517
- }
518
- /**
519
- * The **control plane** — a broadcast pub/sub across ALL engine instances (every pod), separate
520
- * from the {@link Transport}'s point-to-point work queues (`dispatch`/`onResult`). It carries what
521
- * every instance may need regardless of who runs a given run: lifecycle events (so a dashboard-only
522
- * pod can live-tail a run executing on a worker pod) and cancellation (so the pod actually running a
523
- * run learns it was cancelled elsewhere). In-process implementations broadcast locally; a
524
- * cross-process one (BullMQ) fans out over its broker (Redis pub/sub). Give the engine a
525
- * `controlPlane` to enable cross-instance events/cancellation; omit it and the engine is local-only.
526
- * A transport that can broadcast may implement this too and be passed as both.
527
- */
528
- export interface ControlPlane {
529
- publishControl(msg: ControlMessage): Promise<void>;
530
- onControl(handler: (msg: ControlMessage) => void): void;
531
- }
532
- /** A message on the {@link ControlPlane}. `from` is the originating engine's `instanceId`, so a
533
- * broker that echoes a publish back to its own subscriber (e.g. Redis pub/sub) can be deduped by
534
- * the originator. */
535
- export type ControlMessage = {
536
- from?: string;
537
- } & ({
538
- kind: 'event';
539
- event: EngineEvent;
540
- } | {
541
- kind: 'cancel';
542
- runId: string;
543
- } | {
544
- kind: 'enqueued';
545
- runId: string;
546
- });
547
- export type BackoffStrategy = 'fixed' | 'exp';
548
- export interface StepOptions {
549
- /** Max attempts before the step (and run) fails. */
550
- retries?: number;
551
- /** How the delay between retries grows: `fixed` (constant) or `exp` (doubles each attempt). */
552
- backoff?: BackoffStrategy;
553
- /** Base delay in ms between retries. Omit (or 0) to retry with no delay. */
554
- backoffMs?: number;
555
- /** Upper bound on the (exponential) backoff delay. */
556
- backoffMaxMs?: number;
557
- /** Add random jitter (50–100% of the computed delay) to avoid thundering-herd retries. */
558
- jitter?: boolean;
559
- /**
560
- * Liveness window for a **remote** step (`ctx.call`): if the worker produces no result and no
561
- * heartbeat within this many ms, the engine presumes it dead and fails the dispatch with a
562
- * `RemoteStepTimeout` (retryable — it re-dispatches per `retries`). Each heartbeat resets the
563
- * window. Ignored for local steps. Omit to wait indefinitely.
564
- */
565
- timeoutMs?: number;
566
- /**
567
- * Saga compensation: if this step completes but the run later **fails**, the engine runs the
568
- * registered `compensate` callbacks in reverse order (undo what was done). Local steps only.
569
- * Idempotency note: a step is already deduplicated by its deterministic `stepId` (runId:seq) —
570
- * remote workers can use it as the idempotency key, so there's no separate key option.
571
- */
572
- compensate?: () => Promise<void>;
573
- }
574
- /**
575
- * A typed handle to a step that runs on a remote worker. The `name` string is the contract:
576
- * the worker registers a handler under the same name. `input`/`output` validate at the boundary.
577
- */
578
- export interface RemoteStepDef<TInput = unknown, TOutput = unknown> extends StepOptions {
579
- name: string;
580
- /** Worker group expected to handle this step. */
581
- group: string;
582
- input: z.ZodType<TInput>;
583
- output: z.ZodType<TOutput>;
584
- /** Branding so `ctx.call` can infer types. */
585
- readonly __remote: true;
586
- }
587
- /**
588
- * A durable webhook handle minted by {@link WorkflowCtx.webhook}. Hand `url` to a third party,
589
- * then `await wait()` — the run suspends with zero compute until the external system POSTs the
590
- * callback (delivered as `engine.signal(token, body)`), and resumes with the body.
591
- */
592
- export interface DurableWebhook<TPayload = unknown> {
593
- /** Deterministic signal token (`wh:<runId>:<seq>`) the callback delivers on — stable across replay. */
594
- readonly token: string;
595
- /**
596
- * Public callback URL for `token`, built by the engine's `webhookUrl` option. Hand this to the
597
- * third party. `undefined` when no builder is configured (use {@link DurableWebhook.token} to
598
- * build your own).
599
- */
600
- readonly url?: string;
601
- /** Suspend until the callback arrives, then resume with its payload. */
602
- wait(): Promise<TPayload>;
603
- }
604
- /**
605
- * The context handed to a workflow function. Every interaction with the outside world goes
606
- * through it so the engine can checkpoint — the workflow body itself stays deterministic.
607
- */
608
- export interface WorkflowCtx {
609
- readonly runId: string;
610
- /**
611
- * Run a local durable step: executed once, then its result is checkpointed and replayed. The
612
- * body receives a {@link StepLogger} to record debug/error lines and sub-process outcomes — these
613
- * are checkpointed with the step and shown under it in the dashboard.
614
- */
615
- step<TOutput>(name: string, fn: (log: StepLogger) => Promise<TOutput>, options?: StepOptions): Promise<TOutput>;
616
- /**
617
- * **Exactly-once** durable step for DB work: runs `fn` and writes the step's checkpoint in ONE
618
- * store transaction, so the business write and the "done" marker commit atomically — a crash can
619
- * never leave the write done-but-not-checkpointed (which a plain `ctx.step` would re-run). `fn`
620
- * receives the store-native transaction handle (`tx` — a TypeORM/MikroORM `EntityManager`, a Prisma
621
- * tx client, or a Drizzle tx); do your writes on it. Needs a SQL store that supports transactions
622
- * (the bundled SQL adapters do); throws otherwise.
623
- */
624
- transaction<TOutput>(name: string, fn: (tx: unknown) => Promise<TOutput>): Promise<TOutput>;
625
- /**
626
- * Call a durable **entity** op and await its result — the entity (`engine.registerEntity`) runs the
627
- * op serialized per `key` over durable state. e.g. `await ctx.callEntity('cart', userId, 'add', item)`.
628
- */
629
- callEntity<TResult = unknown>(name: string, key: string, op: string, arg?: unknown): Promise<TResult>;
630
- /** Send a durable entity op without awaiting a result (fire-and-forget, dispatched once). */
631
- signalEntity(name: string, key: string, op: string, arg?: unknown): Promise<void>;
632
- /**
633
- * Dispatch a typed remote step and await its checkpointed result. Options:
634
- * - `queue` — subject the dispatch to a registered flow-control queue (concurrency / rate limit).
635
- * - `transport` — pin the dispatch to a named transport in the pool (else the pool's first, with
636
- * failover to the rest). See `engine.registerQueue` / the engine's `transports` option.
637
- */
638
- call<TInput, TOutput>(step: RemoteStepDef<TInput, TOutput>, input: TInput, opts?: {
639
- queue?: string;
640
- transport?: string;
641
- }): Promise<TOutput>;
642
- /**
643
- * Durable sleep: suspends the run for `duration` (e.g. `'30s'`, `'2h'`, `'7 days'`, or ms as a
644
- * number) without consuming resources, resuming automatically once the timer is due — even
645
- * across restarts.
646
- */
647
- sleep(duration: string | number): Promise<void>;
648
- /**
649
- * Durable sleep until an **absolute** time (a `Date` or epoch ms) — like {@link sleep} but for a
650
- * fixed deadline (e.g. "resume at midnight"). Resumes automatically once the time passes, across
651
- * restarts. The recorded wake time is fixed on the first run, so it's replay-stable.
652
- */
653
- sleepUntil(when: Date | number): Promise<void>;
654
- /**
655
- * End this run and **continue as a fresh execution** of the same workflow with `input` and a clean
656
- * history — for long-running / looping workflows that would otherwise accumulate unbounded
657
- * checkpoints (and slow replays). The next run gets id `<runId>~N`. Terminal: it always throws, so
658
- * code after it never runs. Carry forward whatever state the next iteration needs in `input`.
659
- */
660
- continueAsNew(input?: unknown): Promise<never>;
661
- /**
662
- * Suspend the run until an external `engine.signal(token, payload)` arrives (e.g. a webhook or
663
- * human approval), then resume with the payload. Waits indefinitely by default — no compute
664
- * consumed. Pass `{ timeoutMs }` to bound the wait: if the deadline passes first the call throws
665
- * a `SignalTimeoutError` (catch it in the workflow to branch).
666
- */
667
- waitForSignal<TPayload>(token: string, opts?: {
668
- timeoutMs?: number;
669
- }): Promise<TPayload>;
670
- /**
671
- * Wait for a named **event** published via `engine.publishEvent(name, payload)`, then resume with
672
- * the payload. Unlike a signal (point-to-point by token), events are name-based pub/sub: pass an
673
- * optional `match` (a subset of the payload that must deep-equal) so a publish fans out only to the
674
- * runs it concerns — e.g. `ctx.waitForEvent('payment.settled', { match: { orderId } })`. `timeoutMs`
675
- * bounds the wait (throws `SignalTimeoutError`). No compute consumed while waiting.
676
- */
677
- waitForEvent<TPayload>(name: string, opts?: {
678
- match?: Record<string, unknown>;
679
- timeoutMs?: number;
680
- }): Promise<TPayload>;
681
- /**
682
- * An external task with **async completion**: run `dispatch` once (checkpointed — e.g. send to a
683
- * queue, kick off a non-durable worker or a foreign service like a Python process), then suspend
684
- * with zero compute until `engine.completeTask(runId, name, result)` (or `failTask`) reports back,
685
- * and resume with the result. The durable, first-class counterpart of the hand-rolled
686
- * "dispatch over SQS → wait for COMPLETE_PHASE → signal" pattern. `name` must be unique per run.
687
- */
688
- task<TResult>(name: string, dispatch: () => Promise<void>, options?: StepOptions): Promise<TResult>;
689
- /**
690
- * Run another registered workflow as a **tracked child** and await its result: starts it once and
691
- * suspends — zero compute — until the child reaches a terminal state, then resumes with the child's
692
- * output (or throws a FatalError if the child failed). `childId` defaults to a deterministic id
693
- * derived from this run and the call position, so it's stable across replay.
694
- *
695
- * Pass the child's **class** (`ctx.child(ShippingWorkflow, input)`) for a typed input + result; pass
696
- * a **string** name for a cross-runtime child (e.g. a Python workflow) where there's no class.
697
- */
698
- child<C extends WorkflowClass>(workflow: C, input: WorkflowInputOf<C>, childId?: string): Promise<WorkflowOutputOf<C>>;
699
- child<TOutput>(workflow: string, input: unknown, childId?: string): Promise<TOutput>;
700
- /**
701
- * Start a child workflow **fire-and-forget**: dispatches it once (checkpointed, replay-safe) and
702
- * returns its run id immediately — the parent keeps running instead of suspending. Use it to kick
703
- * off side work (an audit log, a notification) you don't need to wait on, or to fan out: collect
704
- * the ids, then later `await ctx.child(...)` each with the same id to join (the start is idempotent
705
- * by id, so the child runs exactly once). Class or string ref, like {@link child}.
706
- */
707
- startChild<C extends WorkflowClass>(workflow: C, input: WorkflowInputOf<C>, childId?: string): Promise<string>;
708
- startChild(workflow: string, input: unknown, childId?: string): Promise<string>;
709
- /**
710
- * Pause the run at this point until a human resumes it from the dashboard (or
711
- * `engine.continue(runId)`). Records a visible `pending` checkpoint so the breakpoint shows up
712
- * in the timeline, then suspends with zero compute — the durable equivalent of a debugger
713
- * breakpoint. Gate it on your own config to make breakpoints opt-in per run:
714
- * `if (cfg.breakAfterExtraction) await ctx.breakpoint('after-extraction')`.
715
- */
716
- breakpoint(label?: string): Promise<void>;
717
- /**
718
- * Mint a durable webhook: returns a handle with a deterministic `token` and (if the engine has a
719
- * `webhookUrl` builder) a public `url`. Hand the url to a third party — inside a `ctx.step` — then
720
- * `await handle.wait()` to suspend with zero compute until they POST the callback (the dashboard
721
- * turns that POST into `engine.signal(token, body)`). The first-class, replay-safe version of
722
- * "expose a callback URL and wait for it".
723
- */
724
- webhook<TPayload>(): DurableWebhook<TPayload>;
725
- /**
726
- * Publish a named, queryable value from inside the run — the latest value for `key` is readable
727
- * externally via `engine.getEvent(runId, key)` while the run is still in flight (progress, a
728
- * partial result, a status). Checkpointed and replay-safe (overwrites the previous value for the
729
- * same key). The read side has no effect on the run — the durable, suspend-model counterpart of a
730
- * Temporal query.
731
- */
732
- setEvent<TValue>(key: string, value: TValue): Promise<void>;
733
- /**
734
- * Expose a named **update point**: suspend until an external `engine.update(runId, name, arg)`
735
- * delivers `arg`, then resume with it. The update is run-scoped (`name` need only be unique within
736
- * the run) and gated by any validator registered via `engine.registerUpdateValidator` — a rejected
737
- * update never reaches here. Pass `{ timeoutMs }` to bound the wait (throws `SignalTimeoutError`).
738
- * The durable counterpart of a Temporal update handler.
739
- */
740
- onUpdate<TArg>(name: string, opts?: {
741
- timeoutMs?: number;
742
- }): Promise<TArg>;
743
- /**
744
- * Guard an in-place workflow change without a new version. Wrap the changed code in
745
- * `if (await ctx.patched('my-change')) { …new… } else { …old… }`: a fresh run records a marker and
746
- * takes the new branch (`true`); a run already recorded under the old code keeps the old branch
747
- * (`false`), because its history has a real step where the marker would sit. The marker is
748
- * position-transparent for old runs (it doesn't shift their recorded steps), so guarding code is
749
- * replay-safe. Once every old run has drained, remove the guard (keep the new branch).
750
- */
751
- patched(id: string): Promise<boolean>;
752
- /**
753
- * Deterministic wall-clock (epoch ms): records the time on the first run and replays the SAME
754
- * value afterwards. Use this instead of `Date.now()` inside a workflow — a raw `Date.now()` returns
755
- * a different value on every replay, which silently corrupts a durable run.
756
- */
757
- now(): Promise<number>;
758
- /**
759
- * Deterministic random in `[0, 1)`: recorded once, then replayed. Use instead of `Math.random()`
760
- * (same replay-safety reason as {@link now}).
761
- */
762
- random(): Promise<number>;
763
- /** Deterministic UUID v4: recorded once, then replayed. Use instead of `crypto.randomUUID()`. */
764
- uuid(): Promise<string>;
765
- }
766
- /** Result of executing or resuming a workflow run. */
767
- export interface RunResult {
768
- runId: string;
769
- status: RunStatus;
770
- output?: unknown;
771
- error?: StepError;
772
- }
773
- /**
774
- * Validates an incoming `engine.update` before it is delivered to the run. Throw (or return a
775
- * non-empty string) to reject — the run is left untouched. Return nothing/void to accept. May be
776
- * async (e.g. a business-rule check against a DB).
777
- */
778
- export type UpdateValidator<TArg = unknown> = (arg: TArg) => void | string | Promise<void | string>;
779
- /** Outcome of `engine.update`: rejected by the validator, or accepted and delivered. */
780
- export type UpdateResult = {
781
- accepted: false;
782
- reason: string;
783
- } | {
784
- accepted: true;
785
- run: RunResult | null;
786
- };
787
- export type EngineEventType = 'run.started' | 'run.completed' | 'run.failed' | 'run.suspended' | 'step.started' | 'step.completed' | 'step.failed' | 'step.progress';
788
- /**
789
- * A lifecycle event emitted by the engine. The observability surfaces (dashboard, OTel, the
790
- * Telescope integration) all subscribe to these rather than reaching into the store.
791
- */
792
- export interface EngineEvent {
793
- type: EngineEventType;
794
- runId: string;
795
- workflow?: string;
796
- seq?: number;
797
- name?: string;
798
- kind?: StepKind;
799
- output?: unknown;
800
- error?: StepError;
801
- /** Wall-clock duration of the unit that just finished (step or run), when known. */
802
- durationMs?: number;
803
- /** For a remote step: how long it waited in the queue before a worker picked it up. */
804
- queueMs?: number;
805
- /** The live step event carried by a `step.progress` (the single log line / sub-process outcome a
806
- * running step just emitted). Absent on lifecycle events. */
807
- event?: StepEvent;
808
- at: Date;
809
- }
810
- export type EngineListener = (event: EngineEvent) => void;
811
- /** What a {@link StepInterceptor} is told about the local step it is wrapping. */
812
- export interface StepInvocation {
813
- readonly runId: string;
814
- readonly workflow: string;
815
- /** The step name passed to `ctx.step(name, ...)` (also `'now'`/`'random'`/`'uuid'` internals). */
816
- readonly stepName: string;
817
- /** The step's logical position within the run. */
818
- readonly seq: number;
819
- /** 1-based attempt number — increments across `ctx.step` retries. */
820
- readonly attempt: number;
821
- }
822
- /**
823
- * Wraps the **real execution** of a local `ctx.step` (Template/Nest-style onion middleware). Call
824
- * `next()` to run the step body (or the next interceptor) and return — or transform — its result;
825
- * throw to fail the step. First-registered runs outermost. Interceptors fire only when a step
826
- * actually executes, NOT on replay (a replayed step returns its recorded output without running),
827
- * so they see true execution timing. Register with `engine.use`.
828
- */
829
- export type StepInterceptor = (invocation: StepInvocation, next: () => Promise<unknown>) => Promise<unknown>;
830
- //# sourceMappingURL=interfaces.d.ts.map