@dudousxd/nestjs-durable-core 0.27.1 → 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 -1785
  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 -61
  107. package/dist/workflow-ctx.d.ts.map +0 -1
  108. package/dist/workflow-ctx.js +0 -451
  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
package/dist/index.d.ts CHANGED
@@ -1,20 +1,2044 @@
1
- export * from './duration';
2
- export * from './engine';
3
- export * from './entities';
4
- export * from './errors';
5
- export * from './event-accumulators';
6
- export * from './interfaces';
7
- export * from './protocol';
8
- export * from './queue';
9
- export * from './remote-step-factory';
10
- export * from './remote-workflow-executor';
11
- export * from './codec-state-store';
12
- export * from './events';
13
- export * from './metrics';
14
- export * from './scheduler';
15
- export * from './search-attributes';
16
- export * from './tokens';
17
- export * from './workflow-ref';
18
- export { InMemoryStateStore } from './testing/in-memory-state-store';
19
- export { InMemoryTransport } from './testing/in-memory-transport';
20
- //# sourceMappingURL=index.d.ts.map
1
+ import { z } from 'zod';
2
+
3
+ /**
4
+ * Parse a human duration to milliseconds. Accepts a number (already ms) or a string like
5
+ * `'500ms'`, `'30s'`, `'15m'`, `'2h'`, `'7d'`, `'7 days'`.
6
+ */
7
+ declare function parseDuration(duration: string | number): number;
8
+
9
+ /**
10
+ * A **workflow reference** is how one workflow names another to call it: either a registered workflow
11
+ * **name** (a string — the only option across runtimes, e.g. a Python workflow) or, for a same-runtime
12
+ * TypeScript workflow, its **class**. The class carries the input/output types through the call, so
13
+ * `ctx.child(ShippingWorkflow, input)` type-checks the input and returns a typed result — while a
14
+ * string stays available for the cross-runtime case.
15
+ */
16
+ /**
17
+ * The symbol the `@Workflow` decorator stamps a workflow's registered name onto, so a class ref can
18
+ * be resolved back to its name. A global-registry symbol (`Symbol.for`) so it survives duplicate
19
+ * copies of this package in a dependency tree.
20
+ */
21
+ declare const WORKFLOW_NAME_KEY: unique symbol;
22
+ /** Structural shape of a `@Workflow` class — its `run(ctx, input)` carries the input/output types. */
23
+ type WorkflowClass<TInput = unknown, TOutput = unknown> = abstract new (...args: never[]) => {
24
+ run(ctx: never, input: TInput): Promise<TOutput> | TOutput;
25
+ };
26
+ /** A workflow reference: a registered name (cross-runtime) or a workflow class (typed, same-runtime). */
27
+ type WorkflowRef<TInput = unknown, TOutput = unknown> = string | WorkflowClass<TInput, TOutput>;
28
+ /** The input type a workflow class's `run` accepts. */
29
+ type WorkflowInputOf<C> = C extends abstract new (...args: never[]) => {
30
+ run(ctx: never, input: infer I): unknown;
31
+ } ? I : unknown;
32
+ /** The output type a workflow class's `run` resolves to (Promise unwrapped). */
33
+ type WorkflowOutputOf<C> = C extends abstract new (...args: never[]) => {
34
+ run(ctx: never, input: never): infer R;
35
+ } ? Awaited<R> : unknown;
36
+ /**
37
+ * Resolve a {@link WorkflowRef} to its registered workflow name: a string is returned as-is; a
38
+ * `@Workflow` class is resolved via the name the decorator stamped on it. Throws if a class was
39
+ * never decorated (so it carries no registered name).
40
+ */
41
+ declare function workflowName(ref: WorkflowRef): string;
42
+
43
+ /**
44
+ * Core type contracts for nestjs-durable.
45
+ *
46
+ * These are intentionally framework-agnostic: `@dudousxd/nestjs-durable-core` knows only
47
+ * these interfaces, never a concrete transport, store or ORM. Adapters implement them.
48
+ */
49
+ type RunStatus =
50
+ /** Created + enqueued by `start`, not yet picked up — a worker will lease and execute it. */
51
+ 'pending' | 'running' | 'suspended' | 'completed' | 'failed' | 'cancelled'
52
+ /** Dead-letter: recovery gave up after `maxRecoveryAttempts` (a poison pill). Terminal; inspect it. */
53
+ | 'dead';
54
+ /** One execution of a workflow. The unit of durability and the unit shown in the dashboard. */
55
+ interface WorkflowRun {
56
+ id: string;
57
+ /** Registered workflow name, e.g. `checkout`. */
58
+ workflow: string;
59
+ /** Code version at start; old runs must resume on the version they began on. */
60
+ workflowVersion: string;
61
+ status: RunStatus;
62
+ /** Serialized workflow input (the args the run was started with). */
63
+ input: unknown;
64
+ /** Serialized workflow output, once `completed`. */
65
+ output?: unknown;
66
+ /** Structured error, once `failed`. */
67
+ error?: StepError | undefined;
68
+ /** When `suspended` on a durable sleep: epoch ms at which the run becomes due to resume. */
69
+ wakeAt?: number | undefined;
70
+ /** Recovery lease owner (engine instance id) while a run is being resumed. */
71
+ lockedBy?: string | undefined;
72
+ /** Recovery lease expiry (epoch ms); another instance may take over once it passes. */
73
+ lockedUntil?: number | undefined;
74
+ /** How many times crash-recovery has picked this run up — caps poison pills (see maxRecoveryAttempts). */
75
+ recoveryAttempts?: number | undefined;
76
+ /** Searchable labels: the workflow's static `@Workflow({ tags })` merged with the run's start-time tags. */
77
+ tags?: string[] | undefined;
78
+ /** Typed, queryable run data (e.g. `{ amount: 200, tier: 'pro' }`) — see {@link RunQuery.attributes}. */
79
+ searchAttributes?: SearchAttributes | undefined;
80
+ createdAt: Date;
81
+ updatedAt: Date;
82
+ }
83
+ type StepKind = 'local' | 'remote' | 'sleep' | 'signal';
84
+ /**
85
+ * The recorded result of a single step at a deterministic logical position (`seq`).
86
+ * On replay, a `completed` checkpoint means the step is NOT re-executed — its `output` is
87
+ * returned. A non-terminal checkpoint (`pending`/`running`) does not short-circuit: the step is
88
+ * re-awaited (remote) or re-run (local).
89
+ */
90
+ interface StepCheckpoint {
91
+ runId: string;
92
+ /** Deterministic logical position of the step within the run. */
93
+ seq: number;
94
+ /** Registered step name (matches the remote handler name for remote steps). */
95
+ name: string;
96
+ kind: StepKind;
97
+ /** Stable id passed to remote workers so they can dedupe a re-delivered task. */
98
+ stepId: string;
99
+ /**
100
+ * `pending` = a remote step dispatched and awaiting its worker result (the run is durably
101
+ * suspended, not held in memory); it becomes `completed`/`failed` when the result arrives.
102
+ * `running` = a local step whose body is executing in-process right now (see `trackStepStart`);
103
+ * it's overwritten by `completed`/`failed` when the body settles, and on a crash mid-body it
104
+ * simply re-runs on replay (only `completed` short-circuits).
105
+ */
106
+ status: 'pending' | 'running' | 'completed' | 'failed';
107
+ /** What the step was called with — the `ctx.call` args for a remote step (a local step has none). */
108
+ input?: unknown;
109
+ output?: unknown;
110
+ error?: StepError | undefined;
111
+ attempts: number;
112
+ /** For remote steps: which worker group ran it. */
113
+ workerGroup?: string | undefined;
114
+ /** Structured events/logs the step emitted (sub-step outcomes, debug/error lines). */
115
+ events?: StepEvent[] | undefined;
116
+ /** For sleep steps: epoch ms the sleep elapses at. */
117
+ wakeAt?: number | undefined;
118
+ /**
119
+ * When the step entered the system: for a remote step, when the engine dispatched it to the
120
+ * transport; for a local step, when it began. Queue-wait time = `startedAt − enqueuedAt`.
121
+ */
122
+ enqueuedAt: Date;
123
+ /** When processing actually began: worker pickup for a remote step, execution start for a local one. */
124
+ startedAt: Date;
125
+ finishedAt: Date;
126
+ }
127
+ /**
128
+ * A structured event a step (or its worker) emits while running — a log line and/or a sub-step
129
+ * outcome. The dashboard renders these under the step, so you can see what happened inside a step
130
+ * that the workflow treats as one unit (e.g. which of N parallel sub-processes ok/failed/skipped).
131
+ */
132
+ interface StepEvent {
133
+ /** Epoch ms. */
134
+ at: number;
135
+ level: 'debug' | 'info' | 'warn' | 'error';
136
+ message: string;
137
+ /** Stable run identity for a sub-process. Distinct invocations of the same `name` carry distinct
138
+ * ids, so their phases and log trails never collapse into one. Absent on events emitted by the
139
+ * legacy `sub()` path, which keys by `name` instead. */
140
+ subId?: string;
141
+ /** For a sub-step/sub-process within the step: its name. */
142
+ name?: string;
143
+ /** Open, consumer-defined grouping label for a sub-process (e.g. a handler/lane). The dashboard
144
+ * groups rows by it. The library never interprets it. */
145
+ group?: string;
146
+ /** For a sub-step: its terminal outcome (closed enum — drives colour + aggregation). */
147
+ status?: 'ok' | 'failed' | 'skipped';
148
+ /** Open, consumer-defined intermediate phase label for a sub-process transition. Carries no
149
+ * terminal `status`; the library timestamps and orders it but never interprets it. */
150
+ phase?: string;
151
+ /** For a log line emitted *inside* a sub-process: that owning sub-process's name, so the dashboard
152
+ * can group a step's log trail under each sub-process instead of one flat list. Set on logs (no
153
+ * `status`); a worker stamps it from the sub-process it's running.
154
+ * @deprecated Superseded by `subId` for run-distinct grouping; kept so existing workers/runs render. */
155
+ process?: string;
156
+ /** Optional structured payload. `data.durationMs` (number) overrides the derived duration. */
157
+ data?: unknown;
158
+ }
159
+ /**
160
+ * Handed to a local step's body (`ctx.step(name, (log) => …)`) so it can record what happened
161
+ * inside the step — debug/info/warn/error lines and per-sub-process outcomes. The events are
162
+ * checkpointed with the step and rendered under it in the dashboard. The remote/cross-language
163
+ * counterpart is the worker attaching the same `StepEvent[]` to its `StepResult` (see the Python
164
+ * SDK's `StepContext`), so observability is symmetric regardless of where the step ran.
165
+ */
166
+ interface StepLogger {
167
+ debug(message: string, data?: unknown): void;
168
+ info(message: string, data?: unknown): void;
169
+ warn(message: string, data?: unknown): void;
170
+ error(message: string, data?: unknown): void;
171
+ /** Record a sub-step / sub-process outcome (e.g. one of N parallel p-processes). */
172
+ sub(name: string, status: 'ok' | 'failed' | 'skipped', message?: string, data?: unknown): void;
173
+ /** Record a sub-process event. Typically pass `phase` for an intermediate transition (carrying no
174
+ * terminal status), or `status` for the terminal outcome — the type permits either; the engine
175
+ * interprets which is set. `id` is the run identity (distinct per invocation); `group` is an
176
+ * open grouping label. */
177
+ subEvent(e: {
178
+ id: string;
179
+ name: string;
180
+ group?: string | undefined;
181
+ phase?: string | undefined;
182
+ status?: 'ok' | 'failed' | 'skipped' | undefined;
183
+ message?: string | undefined;
184
+ data?: unknown;
185
+ }): void;
186
+ /**
187
+ * Ergonomic sub-process lifecycle: run `body`, timing it, and record a terminal `ok` with the
188
+ * measured `durationMs` on success — or `failed` (with the error message) if it throws, then
189
+ * re-throw. `sp.phase(label)` records an intermediate transition; `sp.skip(reason)` a terminal
190
+ * `skipped`. Logs emitted inside `body` are tagged to this sub-process so the dashboard groups
191
+ * them under it. Returns whatever `body` returns. The TS twin of the Python SDK's `sub_process`.
192
+ *
193
+ * ```ts
194
+ * const rows = await log.subProcess('fetch-data', async () => readEverything());
195
+ * await log.subProcess('export-file', () => upload(rows));
196
+ * ```
197
+ */
198
+ subProcess<T>(name: string, body: (sp: SubProcessHandle) => Promise<T> | T, opts?: {
199
+ group?: string;
200
+ id?: string;
201
+ }): Promise<T>;
202
+ }
203
+ /** The handle a {@link StepLogger.subProcess} body receives to mark phases / a non-`ok` outcome. */
204
+ interface SubProcessHandle {
205
+ /** Record an intermediate phase transition (a consumer-defined label, no terminal status). */
206
+ phase(phase: string, data?: unknown): SubProcessHandle;
207
+ /** Record a terminal `skipped` outcome (e.g. nothing to do / validation failed). */
208
+ skip(reason?: string, data?: unknown): void;
209
+ /** Record a terminal `failed` outcome explicitly (the wrapper also does this if the body throws). */
210
+ fail(reason?: string, data?: unknown): void;
211
+ }
212
+ interface StepError {
213
+ message: string;
214
+ /** Optional machine-readable code, e.g. `declined`, `timeout`. */
215
+ code?: string | undefined;
216
+ /** Whether the engine should treat this as retryable. */
217
+ retryable?: boolean | undefined;
218
+ stack?: string | undefined;
219
+ }
220
+ interface StateStore {
221
+ /**
222
+ * Provision the tables/collections this store needs, idempotently. Called on boot when the
223
+ * module's `autoSchema` is on. Optional: stores that need no setup (in-memory) omit it.
224
+ */
225
+ ensureSchema?(): Promise<void>;
226
+ createRun(run: WorkflowRun): Promise<void>;
227
+ updateRun(runId: string, patch: Partial<WorkflowRun>): Promise<void>;
228
+ getRun(runId: string): Promise<WorkflowRun | null>;
229
+ getCheckpoint(runId: string, seq: number): Promise<StepCheckpoint | null>;
230
+ /**
231
+ * Persist a checkpoint and advance the run atomically. Durable semantics depend on this
232
+ * being a single transaction; stores without transactions cannot give the strong guarantee.
233
+ */
234
+ saveCheckpoint(checkpoint: StepCheckpoint): Promise<void>;
235
+ /** Used by recovery on boot to find runs to resume (crashed, left `running`). */
236
+ listIncompleteRuns(): Promise<WorkflowRun[]>;
237
+ /** The oldest `pending` runs awaiting dispatch (FIFO, by `createdAt`), capped at `limit`. */
238
+ listPendingRuns(limit: number): Promise<WorkflowRun[]>;
239
+ /** Suspended runs whose durable timer is due (`wakeAt <= nowMs`), ready to resume. */
240
+ listDueTimers(nowMs: number): Promise<WorkflowRun[]>;
241
+ /**
242
+ * Atomically acquire the recovery lease on a run for `owner` until `leaseUntilMs`, but only if
243
+ * it is currently unlocked or its lease has expired (`<= nowMs`). Returns whether it was
244
+ * acquired — so concurrent engine instances never recover the same run twice.
245
+ */
246
+ tryLockRun(runId: string, owner: string, leaseUntilMs: number, nowMs: number): Promise<boolean>;
247
+ /** Release a run's recovery lease so another instance can pick it up (e.g. once it suspends). */
248
+ releaseRunLock(runId: string): Promise<void>;
249
+ /**
250
+ * Extend a run's lease to `leaseUntilMs`, but ONLY if `owner` still holds it — so a live worker
251
+ * heartbeating its long run keeps the lease, while a dead worker's lease still expires and gets
252
+ * reclaimed. Returns false if the lease was lost (taken over or released).
253
+ */
254
+ renewRunLock(runId: string, owner: string, leaseUntilMs: number): Promise<boolean>;
255
+ /** Record that a run is suspended waiting for an external signal `token`. */
256
+ putSignalWaiter(waiter: SignalWaiter): Promise<void>;
257
+ /** Atomically take (and remove) the run waiting on `token`, if any. */
258
+ takeSignalWaiter(token: string): Promise<SignalWaiter | null>;
259
+ /** List waiters whose `token` starts with `prefix` — used to fan out an event to its subscribers. */
260
+ listSignalWaiters(prefix: string): Promise<SignalWaiter[]>;
261
+ /**
262
+ * Buffer a signal whose waiter hasn't arrived yet, so the next `waitForSignal(token)` consumes it
263
+ * instead of it being lost (FIFO per token). Makes signals reliable regardless of timing and
264
+ * powers `signalWithStart`.
265
+ */
266
+ bufferSignal(token: string, payload: unknown): Promise<void>;
267
+ /** Take the oldest buffered signal for `token` (FIFO), or null if none is buffered. */
268
+ takeBufferedSignal(token: string): Promise<{
269
+ payload: unknown;
270
+ } | null>;
271
+ /**
272
+ * Run `work` in a SINGLE store transaction — giving it the store-native transaction handle (`raw`)
273
+ * for the caller's own DB writes plus a `saveCheckpoint` that commits IN THE SAME transaction, so a
274
+ * business write and the step's "done" checkpoint are atomic (exactly-once). Optional — only the SQL
275
+ * adapters implement it; `ctx.transaction` errors on a store without it.
276
+ */
277
+ transaction?<T>(work: (tx: StoreTransaction) => Promise<T>): Promise<T>;
278
+ listRuns(query: RunQuery): Promise<WorkflowRun[]>;
279
+ listCheckpoints(runId: string): Promise<StepCheckpoint[]>;
280
+ }
281
+ /** Typed, queryable per-run data — exact values for `eq`/`ne`, numbers/strings for range ops. */
282
+ type SearchAttributes = Record<string, string | number | boolean>;
283
+ type AttributeOp = 'eq' | 'ne' | 'gt' | 'gte' | 'lt' | 'lte';
284
+ /** One predicate over a run's {@link SearchAttributes}; a {@link RunQuery} ANDs them all. */
285
+ interface AttributeFilter {
286
+ key: string;
287
+ op: AttributeOp;
288
+ value: string | number | boolean;
289
+ }
290
+ interface RunQuery {
291
+ workflow?: string | undefined;
292
+ status?: RunStatus | undefined;
293
+ /**
294
+ * Match any of these statuses (a `status IN (...)` filter). ORed together, and ANDed with the other
295
+ * predicates. Use this instead of issuing one {@link listRuns} call per status — e.g. the singleton
296
+ * admission gate counts `running` + `suspended` in-flight runs in a single scan. If both `status` and
297
+ * `statuses` are set, both must hold (the single `status` further narrows the set). Empty array =
298
+ * matches nothing.
299
+ */
300
+ statuses?: RunStatus[] | undefined;
301
+ /** Only runs carrying this tag (exact match against {@link WorkflowRun.tags}). */
302
+ tag?: string | undefined;
303
+ /**
304
+ * Typed/range predicates over {@link WorkflowRun.searchAttributes}, ANDed together (e.g. `amount`
305
+ * >= 200 and `tier` = 'pro'). Applied in-process after the coarse filters, so pair with
306
+ * `workflow`/`status`/`tag` to bound the scan on large stores.
307
+ */
308
+ attributes?: AttributeFilter[] | undefined;
309
+ limit?: number | undefined;
310
+ offset?: number | undefined;
311
+ }
312
+ /** The transaction handle `StateStore.transaction` hands to its work callback. */
313
+ interface StoreTransaction {
314
+ /** The store-native transaction handle (TypeORM `EntityManager`, Prisma tx client, MikroORM `EntityManager`,
315
+ * Drizzle tx) — do your business DB writes on THIS so they commit atomically with the checkpoint. */
316
+ readonly raw: unknown;
317
+ /** Persist the step checkpoint inside this transaction. */
318
+ saveCheckpoint(checkpoint: StepCheckpoint): Promise<void>;
319
+ }
320
+ /** Binds an external signal `token` to the suspended run/step position waiting for it. */
321
+ interface SignalWaiter {
322
+ token: string;
323
+ runId: string;
324
+ seq: number;
325
+ }
326
+ /** A unit of work dispatched to a remote worker. This is the documented wire payload. */
327
+ interface RemoteTask {
328
+ runId: string;
329
+ seq: number;
330
+ /** Handler name the worker registered, e.g. `payments.charge-card`. */
331
+ name: string;
332
+ stepId: string;
333
+ /** Worker group expected to handle this task. */
334
+ group: string;
335
+ input: unknown;
336
+ /** W3C traceparent so the worker can continue the distributed trace. */
337
+ traceparent?: string | undefined;
338
+ /**
339
+ * Opaque context carrier (tenant / user / correlation ids) the worker re-exposes to the step
340
+ * handler, for cross-process propagation alongside the {@link traceparent}. The engine treats it
341
+ * as a pass-through object and never inspects its shape — the producer (e.g. `@dudousxd/nestjs-context`)
342
+ * owns the keys. Absent when no `context` provider is configured.
343
+ */
344
+ context?: Record<string, unknown> | undefined;
345
+ /**
346
+ * Id of the transport this task was dispatched on (when the engine runs a pool — see
347
+ * {@link NamedTransport}). A worker that consumes several transports replies via the matching one,
348
+ * so failover is symmetric without the worker choosing a transport. Absent for a single transport.
349
+ */
350
+ transport?: string | undefined;
351
+ attempt: number;
352
+ }
353
+ interface StepResult {
354
+ runId: string;
355
+ seq: number;
356
+ stepId: string;
357
+ status: 'completed' | 'failed';
358
+ output?: unknown;
359
+ error?: StepError | undefined;
360
+ /** Epoch ms when the worker began processing — lets the engine report queue-wait time. */
361
+ startedAt?: number | undefined;
362
+ /** Structured events the worker emitted while running the step (sub-step outcomes, logs). */
363
+ events?: StepEvent[] | undefined;
364
+ }
365
+ interface Heartbeat {
366
+ runId: string;
367
+ seq: number;
368
+ stepId: string;
369
+ group: string;
370
+ }
371
+ /** engine → workflow worker: advance this run one turn by replaying the function against `history`. */
372
+ interface WorkflowTask {
373
+ /** Dedupe id for this turn (a re-delivered task must be idempotent). */
374
+ taskId: string;
375
+ runId: string;
376
+ /** Registered workflow name + the version the run started on — replay must use that version. */
377
+ workflow: string;
378
+ workflowVersion: string;
379
+ input: unknown;
380
+ /** Completed durable ops so far, ordered by seq — what the worker replays its results from. */
381
+ history: HistoryEvent[];
382
+ /** Signals delivered to the run but not yet consumed, so `wait_signal` resolves on replay. */
383
+ pendingSignals?: Array<{
384
+ seq: number;
385
+ signal: string;
386
+ payload: unknown;
387
+ }>;
388
+ group: string;
389
+ /** Id of the transport this task was dispatched on (pool failover) — see {@link NamedTransport}. */
390
+ transport?: string;
391
+ traceparent?: string;
392
+ attempt: number;
393
+ }
394
+ /** One resolved durable op in a run's history — a superset of a completed {@link StepCheckpoint}. */
395
+ interface HistoryEvent {
396
+ seq: number;
397
+ kind: 'step' | 'call' | 'timer' | 'signal' | 'child';
398
+ name?: string | undefined;
399
+ /** Resolved value: a step/call output, a child run's output, a signal payload. */
400
+ output?: unknown;
401
+ /** Set when the op resolved to a failure (e.g. a failed remote step the workflow may catch). */
402
+ error?: StepError | undefined;
403
+ }
404
+ /** A decision the workflow function produced at a `seq` that was not yet in history. */
405
+ type WorkflowCommand =
406
+ /** `ctx.call(remoteStep, input)` — dispatch a remote step and await it. */
407
+ {
408
+ kind: 'call';
409
+ seq: number;
410
+ name: string;
411
+ group: string;
412
+ input: unknown;
413
+ }
414
+ /** `ctx.step(name, body)` — a LOCAL step the worker already ran this turn; the engine persists its
415
+ * result so replay returns it instead of re-running (durability for side-effectful work).
416
+ * `startedAt`/`finishedAt` (epoch ms) carry the step's real wall-clock window so the dashboard
417
+ * shows a true duration instead of 0ms, and `events` carry the sub-process/log trail the step
418
+ * emitted (so each handler's p-processes show under it). All optional for back-compat. */
419
+ | {
420
+ kind: 'recordStep';
421
+ seq: number;
422
+ name: string;
423
+ output?: unknown;
424
+ error?: StepError;
425
+ startedAt?: number;
426
+ finishedAt?: number;
427
+ events?: StepEvent[];
428
+ }
429
+ /** `ctx.sleep(ms)` — a durable timer of `ms` duration. The engine computes the absolute deadline
430
+ * (now + ms) when it applies the command, so the worker never reads the clock (determinism). */
431
+ | {
432
+ kind: 'sleep';
433
+ seq: number;
434
+ ms: number;
435
+ }
436
+ /** `ctx.wait_signal(name)` — block until a signal `name` is delivered to the run. */
437
+ | {
438
+ kind: 'waitSignal';
439
+ seq: number;
440
+ signal: string;
441
+ }
442
+ /** `ctx.start_child(workflow, input)` — start a child run with its own lifecycle. */
443
+ | {
444
+ kind: 'startChild';
445
+ seq: number;
446
+ workflow: string;
447
+ input: unknown;
448
+ };
449
+ /** workflow worker → engine: the result of replaying one turn of a remote workflow. */
450
+ interface WorkflowDecision {
451
+ taskId: string;
452
+ runId: string;
453
+ /** `continue` = produced `commands` and is blocked on an await; otherwise the run settles. */
454
+ status: 'continue' | 'completed' | 'failed';
455
+ /** New durable ops the replay produced this turn (status === 'continue'), ordered by seq. */
456
+ commands: WorkflowCommand[];
457
+ /** Final workflow output (status === 'completed'). */
458
+ output?: unknown;
459
+ /** Terminal error (status === 'failed'). */
460
+ error?: StepError;
461
+ }
462
+ /**
463
+ * workflow worker → engine: a LOCAL step's lifecycle, streamed AS IT HAPPENS (not batched into the
464
+ * turn's final {@link WorkflowDecision}). A Python `@workflow` runs its `ctx.step`s inline over one
465
+ * turn that can last minutes; without this the engine learns of the steps only when the turn ends,
466
+ * so the dashboard shows nothing mid-run. The worker emits `running` when a step's body starts and
467
+ * `completed`/`failed` when it settles; the engine checkpoints each immediately, so a step appears
468
+ * in-flight and then resolves live. The turn's final `recordStep` command re-persists the same
469
+ * checkpoint idempotently (replay history), so this is purely additive observability.
470
+ */
471
+ interface WorkflowStepEvent {
472
+ runId: string;
473
+ seq: number;
474
+ name: string;
475
+ phase: 'running' | 'completed' | 'failed';
476
+ /** Epoch ms the step body began (all phases) and settled (`completed`/`failed`). */
477
+ startedAt: number;
478
+ finishedAt?: number;
479
+ /** The replayed result / failure for the settled phases. */
480
+ output?: unknown;
481
+ error?: StepError;
482
+ /** Sub-process + log trail the step emitted so far (the handler's p-processes). */
483
+ events?: StepEvent[];
484
+ }
485
+ /**
486
+ * Advances a workflow run one turn. The engine has one per workflow: the default {@link InProcess}
487
+ * one runs a registered TS function with the in-process replay machinery; a remote one dispatches a
488
+ * {@link WorkflowTask} to a worker (Python) and awaits its {@link WorkflowDecision}. Either way the
489
+ * engine applies the returned decision — so recovery, timers, singleton and dead-letter stay engine
490
+ * concerns, identical for in-process and remote workflows.
491
+ */
492
+ interface WorkflowExecutor {
493
+ advance(run: WorkflowRun, history: HistoryEvent[], pendingSignals?: WorkflowTask['pendingSignals']): Promise<WorkflowDecision>;
494
+ }
495
+ /**
496
+ * A transport in an ordered pool, identified by `id`. The engine dispatches on the first by default
497
+ * and fails over to the next on a dispatch error; a step can pin one via `ctx.call(…, { transport })`.
498
+ * The chosen `id` is stamped on the {@link RemoteTask} so a worker replies through the matching one.
499
+ */
500
+ interface NamedTransport {
501
+ id: string;
502
+ transport: Transport;
503
+ }
504
+ /**
505
+ * Decides where a freshly-`start`ed run executes. `start` creates the run as `pending` and hands its
506
+ * id here instead of running the body inline — so the API/caller never blocks on workflow execution.
507
+ * The default in-process dispatcher runs it on this instance (a microtask); a broker-backed one
508
+ * enqueues the id for a worker pool to consume (`engine.runOne(runId)`); a no-op one leaves it
509
+ * `pending` in the store for a worker's `runPending` poll to pick up (DB-only, caller-doesn't-execute).
510
+ */
511
+ interface RunDispatcher {
512
+ dispatch(runId: string): void | Promise<void>;
513
+ }
514
+ interface Transport {
515
+ /** engine → worker */
516
+ dispatch(task: RemoteTask): Promise<void>;
517
+ /** worker → engine: a step finished (ok or error). */
518
+ onResult(handler: (result: StepResult) => Promise<void>): void;
519
+ /** worker → engine: liveness signal for an in-flight long step. */
520
+ onHeartbeat(handler: (beat: Heartbeat) => Promise<void>): void;
521
+ /** Release the transport's resources (broker workers, queues, connections) for a clean shutdown.
522
+ * Optional — an in-process transport has nothing to close. Called on `onApplicationShutdown`
523
+ * after the engine drains, so a deploy hands off instead of leaving the broker to time out. */
524
+ close?(): Promise<void>;
525
+ /** engine → workflow worker: dispatch a {@link WorkflowTask} (the polyglot-workflow path). Optional
526
+ * — only transports that carry workflow tasks (BullMQ) implement it; the {@link RemoteWorkflowExecutor}
527
+ * uses it + {@link onDecision} to advance a remote workflow over the broker. */
528
+ dispatchWorkflowTask?(task: WorkflowTask): Promise<void>;
529
+ /** workflow worker → engine: a replayed turn's {@link WorkflowDecision}. Pair with dispatchWorkflowTask. */
530
+ onDecision?(handler: (decision: WorkflowDecision) => Promise<void>): void;
531
+ /** workflow worker → engine: a LOCAL step's {@link WorkflowStepEvent}, streamed mid-turn so the
532
+ * engine can checkpoint it live. Point-to-point (a single engine instance consumes each event and
533
+ * persists it once — no cross-pod duplicate writes). Optional; only broker transports carry it. */
534
+ dispatchStepEvent?(event: WorkflowStepEvent): Promise<void>;
535
+ /** engine ← workflow worker: consume streamed {@link WorkflowStepEvent}s. Pair with dispatchStepEvent. */
536
+ onStepEvent?(handler: (event: WorkflowStepEvent) => Promise<void>): void;
537
+ /** Worker-health for a group: queue backlog + live worker heartbeats. Optional — only broker
538
+ * transports (BullMQ) that can introspect the task queue and the worker-heartbeat keys implement
539
+ * it. The engine aggregates this across its groups in {@link WorkflowEngine.workerHealth}. */
540
+ groupHealth?(group: string): Promise<GroupHealth>;
541
+ /** Distinct groups that currently have a live worker heartbeat — discovered from the heartbeat
542
+ * keyspace, so a group with workers but no engine-side registration (e.g. a local-step group)
543
+ * still surfaces. Pairs with {@link groupHealth}. */
544
+ listWorkerGroups?(): Promise<string[]>;
545
+ }
546
+ /** One worker's liveness record — a TTL'd heartbeat a worker refreshes while it's consuming. Its
547
+ * ABSENCE (the key expired) is the signal: a worker that died or stalled stops refreshing. */
548
+ interface WorkerHeartbeat {
549
+ /** The worker group this instance serves (e.g. `pipeline`, `processing-workflows`). */
550
+ group: string;
551
+ /** Stable per-process id (host + pid), so N replicas of a group each show as a distinct worker. */
552
+ instanceId: string;
553
+ /** Epoch ms of the worker's most recent heartbeat. */
554
+ lastBeatAt: number;
555
+ }
556
+ /** Per-group worker-health snapshot: how much work is queued vs. how many workers are alive to do it.
557
+ * The actionable alert state is `depth > 0 && liveWorkers.length === 0` — work piling up with no
558
+ * consumer (exactly the failure where a worker is "alive but not consuming"). */
559
+ interface GroupHealth {
560
+ group: string;
561
+ /** Outstanding jobs in the group's task queue (waiting + active + delayed + prioritized). */
562
+ depth: number;
563
+ /** Workers with a non-expired heartbeat for this group. */
564
+ liveWorkers: WorkerHeartbeat[];
565
+ }
566
+ /**
567
+ * The **control plane** — a broadcast pub/sub across ALL engine instances (every pod), separate
568
+ * from the {@link Transport}'s point-to-point work queues (`dispatch`/`onResult`). It carries what
569
+ * every instance may need regardless of who runs a given run: lifecycle events (so a dashboard-only
570
+ * pod can live-tail a run executing on a worker pod) and cancellation (so the pod actually running a
571
+ * run learns it was cancelled elsewhere). In-process implementations broadcast locally; a
572
+ * cross-process one (BullMQ) fans out over its broker (Redis pub/sub). Give the engine a
573
+ * `controlPlane` to enable cross-instance events/cancellation; omit it and the engine is local-only.
574
+ * A transport that can broadcast may implement this too and be passed as both.
575
+ */
576
+ interface ControlPlane {
577
+ publishControl(msg: ControlMessage): Promise<void>;
578
+ onControl(handler: (msg: ControlMessage) => void): void;
579
+ }
580
+ /** A message on the {@link ControlPlane}. `from` is the originating engine's `instanceId`, so a
581
+ * broker that echoes a publish back to its own subscriber (e.g. Redis pub/sub) can be deduped by
582
+ * the originator. */
583
+ type ControlMessage = {
584
+ from?: string;
585
+ } & ({
586
+ kind: 'event';
587
+ event: EngineEvent;
588
+ } | {
589
+ kind: 'cancel';
590
+ runId: string;
591
+ } | {
592
+ kind: 'enqueued';
593
+ runId: string;
594
+ });
595
+ type BackoffStrategy = 'fixed' | 'exp';
596
+ interface StepOptions {
597
+ /** Max attempts before the step (and run) fails. */
598
+ retries?: number;
599
+ /** How the delay between retries grows: `fixed` (constant) or `exp` (doubles each attempt). */
600
+ backoff?: BackoffStrategy;
601
+ /** Base delay in ms between retries. Omit (or 0) to retry with no delay. */
602
+ backoffMs?: number;
603
+ /** Upper bound on the (exponential) backoff delay. */
604
+ backoffMaxMs?: number;
605
+ /** Add random jitter (50–100% of the computed delay) to avoid thundering-herd retries. */
606
+ jitter?: boolean;
607
+ /**
608
+ * Liveness window for a **remote** step (`ctx.call`): if the worker produces no result and no
609
+ * heartbeat within this many ms, the engine presumes it dead and fails the dispatch with a
610
+ * `RemoteStepTimeout` (retryable — it re-dispatches per `retries`). Each heartbeat resets the
611
+ * window. Ignored for local steps. Omit to wait indefinitely.
612
+ */
613
+ timeoutMs?: number;
614
+ /**
615
+ * Saga compensation: if this step completes but the run later **fails**, the engine runs the
616
+ * registered `compensate` callbacks in reverse order (undo what was done). Local steps only.
617
+ * Idempotency note: a step is already deduplicated by its deterministic `stepId` (runId:seq) —
618
+ * remote workers can use it as the idempotency key, so there's no separate key option.
619
+ */
620
+ compensate?: () => Promise<void>;
621
+ }
622
+ /**
623
+ * A typed handle to a step that runs on a remote worker. The `name` string is the contract:
624
+ * the worker registers a handler under the same name. `input`/`output` validate at the boundary.
625
+ */
626
+ interface RemoteStepDef<TInput = unknown, TOutput = unknown> extends StepOptions {
627
+ name: string;
628
+ /** Worker group expected to handle this step. */
629
+ group: string;
630
+ input: z.ZodType<TInput>;
631
+ output: z.ZodType<TOutput>;
632
+ /** Branding so `ctx.call` can infer types. */
633
+ readonly __remote: true;
634
+ }
635
+ /**
636
+ * A durable webhook handle minted by {@link WorkflowCtx.webhook}. Hand `url` to a third party,
637
+ * then `await wait()` — the run suspends with zero compute until the external system POSTs the
638
+ * callback (delivered as `engine.signal(token, body)`), and resumes with the body.
639
+ */
640
+ interface DurableWebhook<TPayload = unknown> {
641
+ /** Deterministic signal token (`wh:<runId>:<seq>`) the callback delivers on — stable across replay. */
642
+ readonly token: string;
643
+ /**
644
+ * Public callback URL for `token`, built by the engine's `webhookUrl` option. Hand this to the
645
+ * third party. `undefined` when no builder is configured (use {@link DurableWebhook.token} to
646
+ * build your own).
647
+ */
648
+ readonly url?: string | undefined;
649
+ /** Suspend until the callback arrives, then resume with its payload. */
650
+ wait(): Promise<TPayload>;
651
+ }
652
+ /**
653
+ * The context handed to a workflow function. Every interaction with the outside world goes
654
+ * through it so the engine can checkpoint — the workflow body itself stays deterministic.
655
+ */
656
+ interface WorkflowCtx {
657
+ readonly runId: string;
658
+ /**
659
+ * Run a local durable step: executed once, then its result is checkpointed and replayed. The
660
+ * body receives a {@link StepLogger} to record debug/error lines and sub-process outcomes — these
661
+ * are checkpointed with the step and shown under it in the dashboard.
662
+ */
663
+ step<TOutput>(name: string, fn: (log: StepLogger) => Promise<TOutput>, options?: StepOptions): Promise<TOutput>;
664
+ /**
665
+ * **Exactly-once** durable step for DB work: runs `fn` and writes the step's checkpoint in ONE
666
+ * store transaction, so the business write and the "done" marker commit atomically — a crash can
667
+ * never leave the write done-but-not-checkpointed (which a plain `ctx.step` would re-run). `fn`
668
+ * receives the store-native transaction handle (`tx` — a TypeORM/MikroORM `EntityManager`, a Prisma
669
+ * tx client, or a Drizzle tx); do your writes on it. Needs a SQL store that supports transactions
670
+ * (the bundled SQL adapters do); throws otherwise.
671
+ */
672
+ transaction<TOutput>(name: string, fn: (tx: unknown) => Promise<TOutput>): Promise<TOutput>;
673
+ /**
674
+ * Call a durable **entity** op and await its result — the entity (`engine.registerEntity`) runs the
675
+ * op serialized per `key` over durable state. e.g. `await ctx.callEntity('cart', userId, 'add', item)`.
676
+ */
677
+ callEntity<TResult = unknown>(name: string, key: string, op: string, arg?: unknown): Promise<TResult>;
678
+ /** Send a durable entity op without awaiting a result (fire-and-forget, dispatched once). */
679
+ signalEntity(name: string, key: string, op: string, arg?: unknown): Promise<void>;
680
+ /**
681
+ * Dispatch a typed remote step and await its checkpointed result. Options:
682
+ * - `queue` — subject the dispatch to a registered flow-control queue (concurrency / rate limit).
683
+ * - `priority` — admission priority within that queue; higher is admitted first when a slot is
684
+ * contended (default 0). No effect without a `queue`.
685
+ * - `fairnessKey` — the fairness bucket for a queue with `fairness: 'key'` (e.g. a tenant id);
686
+ * the queue round-robins across distinct keys so one key can't monopolize the budget. Defaults
687
+ * to the run id when omitted. No effect without a `queue`.
688
+ * - `transport` — pin the dispatch to a named transport in the pool (else the pool's first, with
689
+ * failover to the rest). See `engine.registerQueue` / the engine's `transports` option.
690
+ */
691
+ call<TInput, TOutput>(step: RemoteStepDef<TInput, TOutput>, input: TInput, opts?: {
692
+ queue?: string;
693
+ priority?: number;
694
+ fairnessKey?: string;
695
+ transport?: string;
696
+ }): Promise<TOutput>;
697
+ /**
698
+ * Durable sleep: suspends the run for `duration` (e.g. `'30s'`, `'2h'`, `'7 days'`, or ms as a
699
+ * number) without consuming resources, resuming automatically once the timer is due — even
700
+ * across restarts.
701
+ */
702
+ sleep(duration: string | number): Promise<void>;
703
+ /**
704
+ * Durable sleep until an **absolute** time (a `Date` or epoch ms) — like {@link sleep} but for a
705
+ * fixed deadline (e.g. "resume at midnight"). Resumes automatically once the time passes, across
706
+ * restarts. The recorded wake time is fixed on the first run, so it's replay-stable.
707
+ */
708
+ sleepUntil(when: Date | number): Promise<void>;
709
+ /**
710
+ * End this run and **continue as a fresh execution** of the same workflow with `input` and a clean
711
+ * history — for long-running / looping workflows that would otherwise accumulate unbounded
712
+ * checkpoints (and slow replays). The next run gets id `<runId>~N`. Terminal: it always throws, so
713
+ * code after it never runs. Carry forward whatever state the next iteration needs in `input`.
714
+ */
715
+ continueAsNew(input?: unknown): Promise<never>;
716
+ /**
717
+ * Suspend the run until an external `engine.signal(token, payload)` arrives (e.g. a webhook or
718
+ * human approval), then resume with the payload. Waits indefinitely by default — no compute
719
+ * consumed. Pass `{ timeoutMs }` to bound the wait: if the deadline passes first the call throws
720
+ * a `SignalTimeoutError` (catch it in the workflow to branch).
721
+ */
722
+ waitForSignal<TPayload>(token: string, opts?: {
723
+ timeoutMs?: number;
724
+ }): Promise<TPayload>;
725
+ /**
726
+ * Wait for a named **event** published via `engine.publishEvent(name, payload)`, then resume with
727
+ * the payload. Unlike a signal (point-to-point by token), events are name-based pub/sub: pass an
728
+ * optional `match` (a subset of the payload that must deep-equal) so a publish fans out only to the
729
+ * runs it concerns — e.g. `ctx.waitForEvent('payment.settled', { match: { orderId } })`. `timeoutMs`
730
+ * bounds the wait (throws `SignalTimeoutError`). No compute consumed while waiting.
731
+ */
732
+ waitForEvent<TPayload>(name: string, opts?: {
733
+ match?: Record<string, unknown>;
734
+ timeoutMs?: number;
735
+ }): Promise<TPayload>;
736
+ /**
737
+ * An external task with **async completion**: run `dispatch` once (checkpointed — e.g. send to a
738
+ * queue, kick off a non-durable worker or a foreign service like a Python process), then suspend
739
+ * with zero compute until `engine.completeTask(runId, name, result)` (or `failTask`) reports back,
740
+ * and resume with the result. The durable, first-class counterpart of the hand-rolled
741
+ * "dispatch over SQS → wait for COMPLETE_PHASE → signal" pattern. `name` must be unique per run.
742
+ */
743
+ task<TResult>(name: string, dispatch: () => Promise<void>, options?: StepOptions): Promise<TResult>;
744
+ /**
745
+ * Run another registered workflow as a **tracked child** and await its result: starts it once and
746
+ * suspends — zero compute — until the child reaches a terminal state, then resumes with the child's
747
+ * output (or throws a FatalError if the child failed). `childId` defaults to a deterministic id
748
+ * derived from this run and the call position, so it's stable across replay.
749
+ *
750
+ * Pass the child's **class** (`ctx.child(ShippingWorkflow, input)`) for a typed input + result; pass
751
+ * a **string** name for a cross-runtime child (e.g. a Python workflow) where there's no class.
752
+ */
753
+ child<C extends WorkflowClass>(workflow: C, input: WorkflowInputOf<C>, childId?: string): Promise<WorkflowOutputOf<C>>;
754
+ child<TOutput>(workflow: string, input: unknown, childId?: string): Promise<TOutput>;
755
+ /**
756
+ * Start a child workflow **fire-and-forget**: dispatches it once (checkpointed, replay-safe) and
757
+ * returns its run id immediately — the parent keeps running instead of suspending. Use it to kick
758
+ * off side work (an audit log, a notification) you don't need to wait on, or to fan out: collect
759
+ * the ids, then later `await ctx.child(...)` each with the same id to join (the start is idempotent
760
+ * by id, so the child runs exactly once). Class or string ref, like {@link child}.
761
+ */
762
+ startChild<C extends WorkflowClass>(workflow: C, input: WorkflowInputOf<C>, childId?: string): Promise<string>;
763
+ startChild(workflow: string, input: unknown, childId?: string): Promise<string>;
764
+ /**
765
+ * Pause the run at this point until a human resumes it from the dashboard (or
766
+ * `engine.continue(runId)`). Records a visible `pending` checkpoint so the breakpoint shows up
767
+ * in the timeline, then suspends with zero compute — the durable equivalent of a debugger
768
+ * breakpoint. Gate it on your own config to make breakpoints opt-in per run:
769
+ * `if (cfg.breakAfterExtraction) await ctx.breakpoint('after-extraction')`.
770
+ */
771
+ breakpoint(label?: string): Promise<void>;
772
+ /**
773
+ * Mint a durable webhook: returns a handle with a deterministic `token` and (if the engine has a
774
+ * `webhookUrl` builder) a public `url`. Hand the url to a third party — inside a `ctx.step` — then
775
+ * `await handle.wait()` to suspend with zero compute until they POST the callback (the dashboard
776
+ * turns that POST into `engine.signal(token, body)`). The first-class, replay-safe version of
777
+ * "expose a callback URL and wait for it".
778
+ */
779
+ webhook<TPayload>(): DurableWebhook<TPayload>;
780
+ /**
781
+ * Publish a named, queryable value from inside the run — the latest value for `key` is readable
782
+ * externally via `engine.getEvent(runId, key)` while the run is still in flight (progress, a
783
+ * partial result, a status). Checkpointed and replay-safe (overwrites the previous value for the
784
+ * same key). The read side has no effect on the run — the durable, suspend-model counterpart of a
785
+ * Temporal query.
786
+ */
787
+ setEvent<TValue>(key: string, value: TValue): Promise<void>;
788
+ /**
789
+ * Expose a named **update point**: suspend until an external `engine.update(runId, name, arg)`
790
+ * delivers `arg`, then resume with it. The update is run-scoped (`name` need only be unique within
791
+ * the run) and gated by any validator registered via `engine.registerUpdateValidator` — a rejected
792
+ * update never reaches here. Pass `{ timeoutMs }` to bound the wait (throws `SignalTimeoutError`).
793
+ * The durable counterpart of a Temporal update handler.
794
+ */
795
+ onUpdate<TArg>(name: string, opts?: {
796
+ timeoutMs?: number;
797
+ }): Promise<TArg>;
798
+ /**
799
+ * Guard an in-place workflow change without a new version. Wrap the changed code in
800
+ * `if (await ctx.patched('my-change')) { …new… } else { …old… }`: a fresh run records a marker and
801
+ * takes the new branch (`true`); a run already recorded under the old code keeps the old branch
802
+ * (`false`), because its history has a real step where the marker would sit. The marker is
803
+ * position-transparent for old runs (it doesn't shift their recorded steps), so guarding code is
804
+ * replay-safe. Once every old run has drained, remove the guard (keep the new branch).
805
+ */
806
+ patched(id: string): Promise<boolean>;
807
+ /**
808
+ * Deterministic wall-clock (epoch ms): records the time on the first run and replays the SAME
809
+ * value afterwards. Use this instead of `Date.now()` inside a workflow — a raw `Date.now()` returns
810
+ * a different value on every replay, which silently corrupts a durable run.
811
+ */
812
+ now(): Promise<number>;
813
+ /**
814
+ * Deterministic random in `[0, 1)`: recorded once, then replayed. Use instead of `Math.random()`
815
+ * (same replay-safety reason as {@link now}).
816
+ */
817
+ random(): Promise<number>;
818
+ /** Deterministic UUID v4: recorded once, then replayed. Use instead of `crypto.randomUUID()`. */
819
+ uuid(): Promise<string>;
820
+ }
821
+ /** Result of executing or resuming a workflow run. */
822
+ interface RunResult {
823
+ runId: string;
824
+ status: RunStatus;
825
+ output?: unknown;
826
+ error?: StepError | undefined;
827
+ }
828
+ /**
829
+ * Validates an incoming `engine.update` before it is delivered to the run. Throw (or return a
830
+ * non-empty string) to reject — the run is left untouched. Return nothing/void to accept. May be
831
+ * async (e.g. a business-rule check against a DB).
832
+ */
833
+ type UpdateValidator<TArg = unknown> = (arg: TArg) => void | string | Promise<void | string>;
834
+ /** Outcome of `engine.update`: rejected by the validator, or accepted and delivered. */
835
+ type UpdateResult = {
836
+ accepted: false;
837
+ reason: string;
838
+ } | {
839
+ accepted: true;
840
+ run: RunResult | null;
841
+ };
842
+ type EngineEventType = 'run.started' | 'run.completed' | 'run.failed' | 'run.suspended' | 'step.started' | 'step.completed' | 'step.failed' | 'step.progress';
843
+ /**
844
+ * A lifecycle event emitted by the engine. The observability surfaces (dashboard, OTel, the
845
+ * Telescope integration) all subscribe to these rather than reaching into the store.
846
+ */
847
+ interface EngineEvent {
848
+ type: EngineEventType;
849
+ runId: string;
850
+ workflow?: string | undefined;
851
+ seq?: number | undefined;
852
+ name?: string | undefined;
853
+ kind?: StepKind | undefined;
854
+ output?: unknown;
855
+ error?: StepError | undefined;
856
+ /** Wall-clock duration of the unit that just finished (step or run), when known. */
857
+ durationMs?: number | undefined;
858
+ /** For a remote step: how long it waited in the queue before a worker picked it up. */
859
+ queueMs?: number | undefined;
860
+ /** The live step event carried by a `step.progress` (the single log line / sub-process outcome a
861
+ * running step just emitted). Absent on lifecycle events. */
862
+ event?: StepEvent | undefined;
863
+ at: Date;
864
+ }
865
+ type EngineListener = (event: EngineEvent) => void;
866
+ /** What a {@link StepInterceptor} is told about the local step it is wrapping. */
867
+ interface StepInvocation {
868
+ readonly runId: string;
869
+ readonly workflow: string;
870
+ /** The step name passed to `ctx.step(name, ...)` (also `'now'`/`'random'`/`'uuid'` internals). */
871
+ readonly stepName: string;
872
+ /** The step's logical position within the run. */
873
+ readonly seq: number;
874
+ /** 1-based attempt number — increments across `ctx.step` retries. */
875
+ readonly attempt: number;
876
+ }
877
+ /**
878
+ * Wraps the **real execution** of a local `ctx.step` (Template/Nest-style onion middleware). Call
879
+ * `next()` to run the step body (or the next interceptor) and return — or transform — its result;
880
+ * throw to fail the step. First-registered runs outermost. Interceptors fire only when a step
881
+ * actually executes, NOT on replay (a replayed step returns its recorded output without running),
882
+ * so they see true execution timing. Register with `engine.use`.
883
+ */
884
+ type StepInterceptor = (invocation: StepInvocation, next: () => Promise<unknown>) => Promise<unknown>;
885
+
886
+ /** One operation on a durable entity: mutate `state` in place and/or return a result. */
887
+ type EntityHandler<S = unknown> = (state: S, arg: unknown) => unknown | Promise<unknown>;
888
+ /** A durable keyed entity (a virtual object): per-key serialized handlers over durable state. */
889
+ interface EntityConfig<S = unknown> {
890
+ /** Build the initial state for a fresh key. */
891
+ initialState: () => S;
892
+ /** Operation handlers, keyed by op name. Each runs serially per key, exactly once. */
893
+ handlers: Record<string, EntityHandler<S>>;
894
+ }
895
+ /** The slice of the engine the entity subsystem needs (so it stays a standalone module). */
896
+ interface EntityHost {
897
+ register(name: string, version: string, fn: (ctx: WorkflowCtx, input: unknown) => Promise<unknown>): void;
898
+ signalWithStart(workflow: string, input: unknown, runId: string, signal: {
899
+ token: string;
900
+ payload?: unknown;
901
+ }): Promise<{
902
+ runId: string;
903
+ }>;
904
+ getEvent<T>(runId: string, key: string): Promise<T | undefined>;
905
+ signal(token: string, payload: unknown): Promise<unknown>;
906
+ }
907
+ /**
908
+ * Durable keyed **entities** (virtual objects): a keyed actor whose handlers run **serialized per
909
+ * key** over **durable state**, exactly once. Each key is one long-lived run (`entity:<name>:<key>`)
910
+ * processing ops in order. Drive it with `signal` (fire) or `ctx.callEntity` (call + await result),
911
+ * read state with `getState`. Registers its built-in runner on construction.
912
+ */
913
+ declare class Entities {
914
+ private readonly host;
915
+ private readonly configs;
916
+ constructor(host: EntityHost);
917
+ register<S>(name: string, config: EntityConfig<S>): void;
918
+ /** Deliver an op to an entity's runner, starting it on first contact. `reply` (a signal token)
919
+ * receives the handler's result, for `ctx.callEntity`. */
920
+ dispatch(name: string, key: string, op: string, arg: unknown, reply?: string): Promise<{
921
+ runId: string;
922
+ }>;
923
+ signal(name: string, key: string, op: string, arg?: unknown): Promise<void>;
924
+ getState<S = unknown>(name: string, key: string): Promise<S | undefined>;
925
+ /** The built-in runner: one long-lived run per key, processing ops serially over state. */
926
+ private run;
927
+ }
928
+
929
+ /** How `onEvent` triggers are coalesced into fewer runs (see `@Workflow({ debounce | batch })`). */
930
+ type EventBatchConfig = {
931
+ mode: 'debounce';
932
+ windowMs: number;
933
+ } | {
934
+ mode: 'batch';
935
+ maxSize: number;
936
+ windowMs: number;
937
+ };
938
+ /** The slice of the engine the accumulators need (so they stay a standalone module). */
939
+ interface AccumulatorHost {
940
+ register(name: string, version: string, fn: (ctx: WorkflowCtx, input: unknown) => Promise<unknown>): void;
941
+ signalWithStart(workflow: string, input: unknown, runId: string, signal: {
942
+ token: string;
943
+ payload?: unknown;
944
+ }): Promise<{
945
+ runId: string;
946
+ }>;
947
+ }
948
+ /**
949
+ * Built-in workflows that coalesce `onEvent` triggers. Each is a long-lived per-target accumulator
950
+ * fed by `signalWithStart`: it collects payloads then starts the target once, and `continueAsNew`s to
951
+ * re-arm for the next burst. Signal buffering keeps the re-arm race-free. Registers its runners on
952
+ * construction.
953
+ */
954
+ declare class EventAccumulators {
955
+ private readonly host;
956
+ constructor(host: AccumulatorHost);
957
+ /** Route an `onEvent` payload into its target's debounce/batch accumulator. */
958
+ route(target: string, config: EventBatchConfig, payload: unknown): Promise<{
959
+ runId: string;
960
+ }>;
961
+ /** Fire the target with the LAST payload once events have been quiet for `windowMs`. */
962
+ private debounce;
963
+ /** Fire the target with all payloads (`{ events }`) once `maxSize` or `windowMs` from the first. */
964
+ private batch;
965
+ }
966
+
967
+ /**
968
+ * Flow control for remote steps. A queue caps how much work `ctx.call(step, input, { queue })`
969
+ * admits at once — a concurrency limit and/or a fixed-window rate limit. When a call can't be
970
+ * admitted it doesn't dispatch: the run re-suspends with a near-future wake time, and the timer
971
+ * poller re-tries admission later — so flow control is durable (survives crashes) without holding
972
+ * the run in memory.
973
+ *
974
+ * Accounting is per engine instance (the DBOS `workerConcurrency` tier): correct for the common
975
+ * single-orchestrator deployment. Global, cross-instance limits would need a durable counter in the
976
+ * store — a deliberate follow-up, not built here.
977
+ *
978
+ * Beyond raw caps, the controller can admit contended slots by PRIORITY and by per-KEY FAIRNESS
979
+ * (round-robin), so a single noisy key/tenant can't monopolize the concurrency budget. This is the
980
+ * DISPATCH/admission layer — it never touches workflow replay (engine positional logic). Default
981
+ * behaviour is unchanged FIFO when neither priority nor fairness is configured/passed.
982
+ *
983
+ * Design (vs Temporal/Inngest/DBOS): Temporal task queues are FIFO with separate priority levels;
984
+ * Inngest concurrency keys + throttle give per-key fairness; DBOS queues are FIFO with a
985
+ * `workerConcurrency` cap. We fold both knobs into the same in-process admission gate: an optional
986
+ * per-call `priority` (higher first) and an optional queue-level `fairness: 'key'` (round-robin by
987
+ * the per-call `key`). Because blocked calls re-suspend durably and retry independently (there is no
988
+ * resident FIFO list to reorder), the controller remembers the set of WAITERS that asked-and-were-
989
+ * blocked, and on a later retry only admits the one that is rightful-next under the policy.
990
+ */
991
+ interface QueueConfig {
992
+ /** Queue name, referenced by `ctx.call(step, input, { queue: name })`. */
993
+ name: string;
994
+ /** Max steps in flight at once for this queue (this instance). Omit for unlimited. */
995
+ concurrency?: number;
996
+ /** Fixed-window rate limit: at most `limit` admissions per `periodMs`. Omit for unlimited. */
997
+ rateLimit?: {
998
+ limit: number;
999
+ periodMs: number;
1000
+ };
1001
+ /** Delay (ms) before a concurrency-blocked call re-checks for a free slot. Default 1000. */
1002
+ retryMs?: number;
1003
+ /**
1004
+ * Fairness policy for a contended concurrency slot. `'key'` round-robins across distinct per-call
1005
+ * keys (the least-recently-served key wins the next slot), so one key can't monopolize the budget.
1006
+ * Omit (or `'fifo'`) for plain first-come ordering. Priority always wins over fairness; fairness
1007
+ * only breaks ties within the same priority.
1008
+ */
1009
+ fairness?: 'fifo' | 'key';
1010
+ }
1011
+ /**
1012
+ * Per-call admission hints. All optional — omitting them keeps the legacy FIFO behaviour.
1013
+ * - `priority`: higher is admitted first (default 0).
1014
+ * - `key`: the fairness bucket (e.g. a tenant id) for `fairness: 'key'`.
1015
+ * - `waiterId`: a stable id for THIS call across its retries (the engine passes the stepId), so the
1016
+ * controller can track it as one waiter instead of counting each retry as a new contender.
1017
+ */
1018
+ interface AdmissionItem {
1019
+ priority?: number | undefined;
1020
+ key?: string | undefined;
1021
+ waiterId?: string | undefined;
1022
+ }
1023
+ /** Admission decision: either admitted, or blocked until `retryAt` (epoch ms). */
1024
+ type Admission = {
1025
+ ok: true;
1026
+ } | {
1027
+ ok: false;
1028
+ retryAt: number;
1029
+ };
1030
+ /** In-process admission controller for one {@link QueueConfig}. */
1031
+ declare class QueueController {
1032
+ readonly config: QueueConfig;
1033
+ private readonly clock;
1034
+ private inFlight;
1035
+ private windowStart;
1036
+ private windowCount;
1037
+ /** Registered waiters by id (only used when priority/fairness ordering is in play). */
1038
+ private readonly waiters;
1039
+ /** Monotonic counter stamping each newly-seen waiter for a stable FIFO tiebreak. */
1040
+ private waiterSeq;
1041
+ /** Per-key "last served" order for round-robin fairness — higher = more recently served. */
1042
+ private readonly keyServed;
1043
+ private servedTick;
1044
+ constructor(config: QueueConfig, clock: () => number);
1045
+ /** Whether this controller does any priority/fairness ordering (else it's a plain FIFO gate). */
1046
+ private get ordered();
1047
+ /**
1048
+ * Try to admit one unit of work. On `ok`, the caller has taken a concurrency slot and must call
1049
+ * {@link release} when the step settles. On block, `retryAt` is when admission may next succeed.
1050
+ *
1051
+ * `item` carries optional priority/fairness hints; omitting it is the legacy FIFO path. When
1052
+ * ordering is configured (or a priority is given), a blocked call REGISTERS as a waiter and is only
1053
+ * admitted on a later retry once it is the rightful next under (priority desc, fairness, FIFO).
1054
+ */
1055
+ tryAdmit(item?: AdmissionItem): Admission;
1056
+ /** Release a concurrency slot taken by a successful {@link tryAdmit}. */
1057
+ release(): void;
1058
+ /** Track (or refresh) a blocked call as a waiter; its FIFO seq is stamped only on first sight. */
1059
+ private register;
1060
+ /**
1061
+ * Is the calling item the best candidate among all registered waiters? Ordering, best first:
1062
+ * 1. higher `priority`;
1063
+ * 2. fairness: with `fairness: 'key'`, the least-recently-served key wins (round-robin);
1064
+ * 3. earliest registration (FIFO) — stable tiebreak.
1065
+ * A bare item (no waiterId) is compared on its hints directly.
1066
+ */
1067
+ private isNext;
1068
+ /** Strict "a should be admitted before b" comparison under the active policy. */
1069
+ private better;
1070
+ /** Last-served tick for a key (0 = never served, so an unseen key is admitted before a busy one). */
1071
+ private servedOf;
1072
+ }
1073
+
1074
+ type WorkflowFn = (ctx: WorkflowCtx, input: unknown) => Promise<unknown>;
1075
+ /** Options for {@link WorkflowEngine.start}. */
1076
+ interface StartOptions {
1077
+ /** Run-scoped tags, merged with the workflow's static `@Workflow({ tags })` onto the run. */
1078
+ tags?: string[] | undefined;
1079
+ /** Typed, queryable run data stamped on the run (e.g. `{ amount: 200, tier: 'pro' }`). */
1080
+ searchAttributes?: SearchAttributes | undefined;
1081
+ }
1082
+ /**
1083
+ * Serialize runs of a workflow that share a key — a durable, FIFO mutex (e.g. one pipeline per base).
1084
+ * Excess runs are admitted in creation order, `limit` at a time; the rest wait (suspended) and retry
1085
+ * admission on a timer until a slot frees. Race-free on a consistent store (admission order is the
1086
+ * same `(createdAt, id)` view for every instance).
1087
+ */
1088
+ interface SingletonConfig {
1089
+ /** Derive the serialization key from the workflow input. */
1090
+ key: (input: unknown) => string;
1091
+ /** Max concurrent runs sharing the key. Default 1 (a mutex). */
1092
+ limit?: number;
1093
+ /**
1094
+ * Max GATED (waiting-for-admission) runs allowed to queue behind the `limit` in-flight ones. When
1095
+ * set, `start` rejects with {@link SingletonQueueFullError} once in-flight + gated reaches
1096
+ * `limit + maxQueueDepth` — back-pressure against an unbounded same-key backlog. Omit for the
1097
+ * default unbounded queue. Counts `pending`/`running`/`suspended` runs sharing the key.
1098
+ */
1099
+ maxQueueDepth?: number;
1100
+ }
1101
+ interface WorkflowEngineDeps {
1102
+ store: StateStore;
1103
+ /** A single task transport. Shorthand for a one-entry `transports` pool (id `default`). */
1104
+ transport?: Transport | undefined;
1105
+ /**
1106
+ * An ordered pool of named transports. The engine dispatches on the first and fails over to the
1107
+ * next on a dispatch error; a step pins one via `ctx.call(step, input, { transport: id })`. Use
1108
+ * this instead of `transport` for failover / multi-broker setups.
1109
+ */
1110
+ transports?: NamedTransport[] | undefined;
1111
+ /**
1112
+ * Cross-instance broadcast pub/sub for lifecycle events + cancellation (see {@link ControlPlane}).
1113
+ * Separate from the task `transport`; omit for a single-instance / local-only setup. A transport
1114
+ * that can also broadcast may be passed here as well.
1115
+ */
1116
+ controlPlane?: ControlPlane | undefined;
1117
+ /** Epoch-ms clock; injectable for tests. Defaults to `Date.now`. */
1118
+ clock?: (() => number) | undefined;
1119
+ /** Unique id for this engine instance, used for recovery leases. Defaults to a random id. */
1120
+ instanceId?: string | undefined;
1121
+ /** Recovery lease duration in ms — how long this instance owns a run it picked up. Default 30s. */
1122
+ leaseMs?: number | undefined;
1123
+ /**
1124
+ * Cap how many times crash-recovery may pick up the same still-`running` run before giving up and
1125
+ * moving it to the `dead` dead-letter state (a poison pill that crashes the process every boot).
1126
+ * Omit for unlimited (the default — recovery always retries).
1127
+ */
1128
+ maxRecoveryAttempts?: number | undefined;
1129
+ /**
1130
+ * Build the public callback URL for a `ctx.webhook()` token (e.g.
1131
+ * ``(t) => `https://api.example.com/durable/webhooks/${t}` ``). Populates
1132
+ * {@link DurableWebhook.url}. Omit if you build URLs yourself from the token.
1133
+ */
1134
+ webhookUrl?: ((token: string) => string) | undefined;
1135
+ /**
1136
+ * Provide the current W3C `traceparent` to stamp on each dispatched {@link RemoteTask}, so a
1137
+ * worker (including the Python SDK) continues the distributed trace. Keep core OTel-free: supply
1138
+ * `otelTraceparent` from `@dudousxd/nestjs-durable-otel`, or your own context reader. Omit to send none.
1139
+ */
1140
+ traceparent?: (() => string | undefined) | undefined;
1141
+ /**
1142
+ * Provide an opaque context carrier (tenant / user / correlation ids) to attach to each dispatched
1143
+ * {@link RemoteTask} as its `context`, so a worker (including the Python SDK) re-exposes it to the
1144
+ * step handler — cross-process propagation alongside the {@link traceparent}. Keep core dependency-free:
1145
+ * supply this from `@dudousxd/nestjs-context` or your own request-scoped reader. The engine never
1146
+ * inspects the returned object's shape. Omit to send none.
1147
+ *
1148
+ * Re-evaluated at each (re)dispatch — including a retry or a resume that the engine drives AFTER a
1149
+ * crash/scale-down, which runs OUTSIDE the originating request scope. On such a path this provider
1150
+ * may return empty or stale values (the request-scoped tenant/user is gone). Treat the carrier as
1151
+ * best-effort correlation/propagation metadata only — do NOT treat it as an authorization boundary.
1152
+ */
1153
+ context?: (() => Record<string, unknown> | undefined) | undefined;
1154
+ /**
1155
+ * Re-hydrate the originating context around a LOCAL step body, so a `@DurableStep` reader sees the
1156
+ * tenant / user / correlation ids that were live when the run was started — even on a path the
1157
+ * engine drives outside the originating request scope (a resume after crash/scale-down, a timer).
1158
+ * Given the carrier produced by {@link context} (may be empty/undefined) and the step body `fn`, it
1159
+ * runs `fn` with that context ambiently established (e.g. inside `@dudousxd/nestjs-context`'s ALS)
1160
+ * and returns its result. Keep core dependency-free: supply this from the nestjs wiring (which owns
1161
+ * nestjs-context) or your own ALS bridge. The handler signature is unchanged — re-hydration is
1162
+ * ambient. Default: passthrough (`(_, fn) => fn()`), so behavior is byte-identical when unset.
1163
+ */
1164
+ rehydrate?: (<T>(carrier: Record<string, unknown> | undefined, fn: () => T) => T) | undefined;
1165
+ /**
1166
+ * Attempts for each saga compensation when the run fails (a transient undo — e.g. a refund API
1167
+ * hiccup — gets another try). Default 1 (no retry). Compensations must be idempotent.
1168
+ */
1169
+ compensationRetries?: number | undefined;
1170
+ /**
1171
+ * Persist a `running` checkpoint when a local step's body begins, so an in-flight step shows up
1172
+ * in the dashboard (and a fresh page load / REST query) the moment it starts — not only once it
1173
+ * completes. The `step.started` lifecycle event is emitted either way (the live SSE view always
1174
+ * sees the start); this flag only controls the extra checkpoint write. Default `true`. Set
1175
+ * `false` on hot paths with many short local steps to halve their checkpoint writes — you keep
1176
+ * the live event but lose reload-survivable in-flight visibility.
1177
+ */
1178
+ trackStepStart?: boolean | undefined;
1179
+ /**
1180
+ * Where a freshly-`start`ed run executes (see {@link RunDispatcher}). Defaults to in-process: the
1181
+ * run executes on this instance asynchronously (a microtask), so `start` returns without blocking.
1182
+ * Pass a no-op dispatcher on a caller that must NOT run workflows (e.g. an API/dashboard pod), and
1183
+ * run `runPending` on a worker pod to pick those up; or a broker-backed one for a worker pool.
1184
+ */
1185
+ runDispatcher?: RunDispatcher | undefined;
1186
+ }
1187
+ /**
1188
+ * The orchestrator. Owns workflow state and replays runs deterministically: each step's
1189
+ * result is checkpointed, so on resume a completed step returns its saved output instead of
1190
+ * re-executing. Remote steps are dispatched over the Transport; their results checkpoint the
1191
+ * same way local steps do.
1192
+ */
1193
+ declare class WorkflowEngine {
1194
+ private readonly store;
1195
+ /** Ordered transport pool (dispatch + failover). Empty = no remote steps. */
1196
+ private readonly pool;
1197
+ private readonly controlPlane?;
1198
+ private readonly clock;
1199
+ private readonly instanceId;
1200
+ private readonly leaseMs;
1201
+ private readonly maxRecoveryAttempts?;
1202
+ private readonly webhookUrl?;
1203
+ private readonly traceparent?;
1204
+ private readonly context?;
1205
+ /** Establish the originating context ambiently around a local step body (see {@link WorkflowEngineDeps.rehydrate}). Default passthrough. */
1206
+ private readonly rehydrate;
1207
+ private readonly compensationRetries;
1208
+ /** Persist a `running` checkpoint at the start of a local step body (see {@link WorkflowEngineDeps.trackStepStart}). */
1209
+ private readonly trackStepStart;
1210
+ /** Where a freshly-started run executes — in-process by default (see {@link RunDispatcher}). */
1211
+ private readonly runDispatcher;
1212
+ /** Every registered workflow, keyed by `name@version` — so old versions stay runnable. */
1213
+ private readonly workflows;
1214
+ /** The newest registered version per workflow name — used to `start` new runs. */
1215
+ private readonly latest;
1216
+ /** Event name → workflow names started when that event is published (see `onEvent`). */
1217
+ private readonly eventTriggers;
1218
+ /** Durable-entity subsystem (registers the `__entity` runner; see `registerEntity`). */
1219
+ private readonly entities;
1220
+ /** Event debounce/batch accumulators (register the `__evt_*` runners; see `accumulators.route`). */
1221
+ private readonly accumulators;
1222
+ /** In-flight remote steps awaiting a worker result, keyed by stepId. */
1223
+ private readonly pending;
1224
+ /** Per-step "reset the liveness timer" callbacks, called when a heartbeat arrives. */
1225
+ private readonly heartbeatResets;
1226
+ private readonly listeners;
1227
+ /** Step interceptors (onion middleware around real local-step execution), first = outermost. */
1228
+ private readonly interceptors;
1229
+ /** Callbacks notified (on any instance) when a run is cancelled — for cooperative cancellation. */
1230
+ private readonly cancelListeners;
1231
+ /** Callbacks notified when a run is enqueued elsewhere — for low-latency cross-pod dispatch. */
1232
+ private readonly enqueuedListeners;
1233
+ /** Notified when a run is dead-lettered (moved to `dead`) — a hook for a DLQ handler. */
1234
+ private readonly deadListeners;
1235
+ /** Validators gating `engine.update`, keyed by `<workflow>:<updateName>`. */
1236
+ private readonly updateValidators;
1237
+ /** Runs being cancelled WITH saga compensation — see `cancel({ compensate: true })`. */
1238
+ private readonly cancelRequested;
1239
+ /** Flow-control queues for remote steps, keyed by name (see {@link registerQueue}). */
1240
+ private readonly queues;
1241
+ /** Which queue a dispatched step took a slot from, by stepId — so the result can release it. */
1242
+ private readonly stepQueue;
1243
+ /** Executions currently in flight, so a graceful shutdown can wait for them to settle. */
1244
+ private readonly inflight;
1245
+ private draining;
1246
+ constructor(deps: WorkflowEngineDeps);
1247
+ /**
1248
+ * Register a **durable entity** (a virtual object): a keyed actor whose `handlers` run **serialized
1249
+ * per key** over **durable state**, exactly once. Drive it with `signalEntity` (fire) /
1250
+ * `ctx.callEntity` (call + await result) and read its state with `getEntityState`. See {@link Entities}.
1251
+ */
1252
+ registerEntity<S>(name: string, config: EntityConfig<S>): void;
1253
+ /** Send an operation to an entity (fire-and-forget). Ordered + exactly-once per key. */
1254
+ signalEntity(name: string, key: string, op: string, arg?: unknown): Promise<void>;
1255
+ /** Read an entity's current durable state (published after each op), or undefined if it has none yet. */
1256
+ getEntityState<S = unknown>(name: string, key: string): Promise<S | undefined>;
1257
+ /**
1258
+ * Be notified when a run is enqueued on ANOTHER instance (via the control plane), so a worker can
1259
+ * pick it up at once — e.g. `engine.onEnqueued((runId) => engine.runOne(runId))`. Returns an
1260
+ * unsubscribe function. Only wire this on instances that should execute runs (workers).
1261
+ */
1262
+ onEnqueued(listener: (runId: string) => void): () => void;
1263
+ /** Fire cooperative-cancellation listeners for `runId` (a worker bridge aborts in-flight work). */
1264
+ private notifyCancelled;
1265
+ /**
1266
+ * Register a workflow version. Register multiple versions of the same name to keep in-flight
1267
+ * runs working across a breaking change: old runs resume on the version they started on, new
1268
+ * runs start on the newest registered version.
1269
+ */
1270
+ register(name: string, version: string, fn: WorkflowFn, opts?: {
1271
+ tags?: string[] | undefined;
1272
+ singleton?: SingletonConfig | undefined;
1273
+ executionTimeout?: string | number | undefined;
1274
+ validateInput?: ((input: unknown) => void | Promise<void>) | undefined;
1275
+ onEvent?: string[] | undefined;
1276
+ eventBatch?: EventBatchConfig | undefined;
1277
+ }): void;
1278
+ /**
1279
+ * Register a workflow whose body runs in another SDK (e.g. Python). The engine owns the run exactly
1280
+ * as for a TS workflow — it persists checkpoints, recovers, runs timers — but advances it by handing
1281
+ * the run's history to `executor` (which dispatches a {@link WorkflowTask} to the worker) and applying
1282
+ * the {@link WorkflowDecision} the worker's replay returns. The worker never touches the store.
1283
+ */
1284
+ registerRemote(name: string, version: string, opts: {
1285
+ group: string;
1286
+ executor: WorkflowExecutor;
1287
+ tags?: string[];
1288
+ singleton?: SingletonConfig;
1289
+ executionTimeout?: string | number;
1290
+ validateInput?: (input: unknown) => void | Promise<void>;
1291
+ }): void;
1292
+ /**
1293
+ * Register a flow-control queue referenced by `ctx.call(step, input, { queue })`. Caps concurrent
1294
+ * in-flight steps and/or the admission rate; blocked calls re-suspend and retry, so the limit is
1295
+ * durable. Per engine instance (see {@link QueueConfig}). Registering the same name replaces it.
1296
+ */
1297
+ registerQueue(config: QueueConfig): void;
1298
+ /** Subscribe to lifecycle events. Returns an unsubscribe function. */
1299
+ subscribe(listener: EngineListener): () => void;
1300
+ /**
1301
+ * Register a {@link StepInterceptor} — onion middleware run around the real execution of every
1302
+ * local `ctx.step` (timing, logging, tracing, error enrichment, context propagation). First
1303
+ * registered is outermost; interceptors fire only when a step executes, never on replay. Returns
1304
+ * an unsubscribe function.
1305
+ */
1306
+ use(interceptor: StepInterceptor): () => void;
1307
+ /**
1308
+ * Fold the registered interceptors around a local step body (identity when there are none), then
1309
+ * run the whole thing inside the re-hydrated originating context. The carrier is read at execution
1310
+ * time from {@link context} — the SAME reader stamped on dispatched remote tasks — so a local step
1311
+ * sees the live tenant / user / trace ids ambiently (via the {@link rehydrate} bridge). Default
1312
+ * `rehydrate` is a passthrough, so this is byte-identical to a bare body call when unwired.
1313
+ */
1314
+ private interceptStep;
1315
+ /**
1316
+ * Be notified when a run is cancelled — on ANY instance, via the transport control plane. A
1317
+ * worker bridge can use this for cooperative cancellation: abort the in-flight work for `runId`
1318
+ * instead of finishing it just to have the result discarded. Returns an unsubscribe function.
1319
+ */
1320
+ onCancel(listener: (runId: string) => void): () => void;
1321
+ /**
1322
+ * Be notified when a run is **dead-lettered** — moved to `dead` after exceeding
1323
+ * `maxRecoveryAttempts`. The listener receives the dead run (status `dead`, with its error), so a
1324
+ * DLQ handler can do something other than just leaving it parked: alert, push to a real queue, or
1325
+ * start a dead-letter workflow (e.g. `engine.onDead((run) => engine.start('pipeline-dlq', run, ...))`).
1326
+ * Returns an unsubscribe function.
1327
+ */
1328
+ onDead(listener: (run: WorkflowRun) => void): () => void;
1329
+ private notifyDead;
1330
+ /** Emit a locally-produced lifecycle event: deliver to subscribers AND broadcast it on the
1331
+ * control plane so other instances (e.g. a dashboard pod) can live-tail this run. */
1332
+ private emit;
1333
+ /** Deliver an event to local subscribers only (no re-broadcast) — used for both locally-produced
1334
+ * events and ones received from the control plane, so an event shows up once on every instance. */
1335
+ private deliver;
1336
+ start<C extends WorkflowClass>(workflow: C, input: WorkflowInputOf<C>, runId: string, opts?: StartOptions): Promise<RunResult>;
1337
+ start<TInput>(workflow: string, input: TInput, runId: string, opts?: StartOptions): Promise<RunResult>;
1338
+ /** Read a run's current persisted state (or null if unknown). A thin pass-through to the store. */
1339
+ getRun(runId: string): Promise<WorkflowRun | null>;
1340
+ resume(runId: string): Promise<RunResult>;
1341
+ /** Track an in-flight execution so {@link drain} can wait for it. */
1342
+ private track;
1343
+ /**
1344
+ * Graceful shutdown: stop picking up new runs (recovery/timer become no-ops) and wait for
1345
+ * in-flight executions to settle, up to `timeoutMs`. Call from your app's shutdown hook so a
1346
+ * deploy hands off cleanly instead of leaving runs to the lease timeout.
1347
+ */
1348
+ drain(timeoutMs?: number): Promise<void>;
1349
+ /**
1350
+ * Cancel in-flight runs that have outlived their workflow's `executionTimeout`. Call it from the
1351
+ * timer poller alongside {@link resumeDueTimers}. A timed-out run is moved to `cancelled` with an
1352
+ * `execution_timeout` error (terminal, so a late step result can't resurrect it).
1353
+ */
1354
+ sweepTimeouts(now?: number): Promise<void>;
1355
+ /**
1356
+ * Resume every run left incomplete by a crash or deploy. Called on boot. Completed steps
1357
+ * replay from their checkpoints, so only the work that had not finished runs again.
1358
+ */
1359
+ recoverIncomplete(nowMs?: number): Promise<RunResult[]>;
1360
+ /**
1361
+ * Per-recovery bookkeeping (called once the lease is held): count the attempt, or — past
1362
+ * `maxRecoveryAttempts` — move a poison pill to the `dead` dead-letter state. Returns a terminal
1363
+ * result to skip the resume, or `undefined` to proceed.
1364
+ */
1365
+ private countRecovery;
1366
+ /**
1367
+ * Resume every suspended run whose durable timer is due. Call periodically (a poller) and on
1368
+ * boot. A run still not due re-suspends cheaply without running new work.
1369
+ */
1370
+ resumeDueTimers(nowMs?: number): Promise<RunResult[]>;
1371
+ /**
1372
+ * Lease and execute one run by id — the worker side of dispatch. Acquires the recovery lease (so
1373
+ * exactly one instance runs it), then resumes the body. Returns the result, or null if another
1374
+ * instance holds the lease or the engine is draining. The default in-process dispatcher calls this;
1375
+ * a broker-backed worker calls it per consumed run id.
1376
+ */
1377
+ runOne(runId: string): Promise<RunResult | null>;
1378
+ /**
1379
+ * Re-enqueue a run for a worker to (re-)execute — the dispatch-model **retry**. Sets it back to
1380
+ * `pending`, clears any stale lease, and dispatches; a worker resumes it (replaying its checkpoints,
1381
+ * re-attempting the failed step). Returns the enqueued state immediately — never runs the body
1382
+ * inline — or null if the run is unknown. The dashboard "retry" goes through here so it can't block
1383
+ * the HTTP request on workflow execution.
1384
+ */
1385
+ requeue(runId: string): Promise<RunResult | null>;
1386
+ /**
1387
+ * **Fix-and-replay**: re-run a run (typically a `dead`/`failed` one) with a corrected `input`, as a
1388
+ * fresh run with clean history. It's a NEW run — `newRunId` defaults to `<runId>~retry~<uuid>` — so
1389
+ * the original stays inspectable. Returns the new run's id, or null if `runId` is unknown.
1390
+ */
1391
+ retryWithInput(runId: string, input: unknown, newRunId?: string): Promise<{
1392
+ runId: string;
1393
+ } | null>;
1394
+ /**
1395
+ * Pick up and execute every `pending` run — the poll-based side of dispatch for a worker pod with
1396
+ * no broker. Runs enqueued by other pods (or by a caller using a no-op dispatcher) sit `pending`
1397
+ * in the store until polled; leasing ensures exactly one pod runs each. Call periodically alongside
1398
+ * {@link resumeDueTimers}.
1399
+ */
1400
+ runPending(nowMs?: number): Promise<RunResult[]>;
1401
+ /**
1402
+ * Resolve once `runId` reaches a settled state — terminal (completed/failed/cancelled/dead) or
1403
+ * suspended (handed off to a timer/signal/event). The async counterpart to dispatch: pair it with
1404
+ * `start` when a call site needs the outcome — `await start(...); const r = await waitForRun(id)`.
1405
+ */
1406
+ waitForRun(runId: string, opts?: {
1407
+ timeoutMs?: number;
1408
+ }): Promise<RunResult>;
1409
+ /**
1410
+ * Resume each run only if this instance can acquire its recovery lease — so when several
1411
+ * replicas recover or poll at once, each run is picked up by exactly one of them.
1412
+ */
1413
+ private resumeLeased;
1414
+ /**
1415
+ * Deliver an external signal to the run waiting on `token` and resume it with `payload`.
1416
+ * Returns the run result, or null if no run is waiting for that token.
1417
+ */
1418
+ /**
1419
+ * Publish a named event. It does two things, and returns how many runs it touched (the sum):
1420
+ * 1. **Resumes** every in-flight run waiting on it via `ctx.waitForEvent(name, { match })` whose
1421
+ * match the payload satisfies (fan-out, vs `signal`'s point-to-point token).
1422
+ * 2. **Starts** a fresh run of every workflow registered with `onEvent: [name]`, passing the
1423
+ * payload as input. Idempotent by `evt:<id>:<workflow>` — pass `opts.id` to dedupe redeliveries
1424
+ * of the same logical event (default: a fresh uuid, so each publish triggers once).
1425
+ */
1426
+ publishEvent(name: string, payload: unknown, opts?: {
1427
+ id?: string;
1428
+ }): Promise<number>;
1429
+ signal(token: string, payload: unknown): Promise<RunResult | null>;
1430
+ /**
1431
+ * Ensure a run exists for `runId`, then deliver a signal to it — atomically race-free thanks to
1432
+ * signal buffering: if the run is new (or busy / not yet waiting), the signal is buffered and
1433
+ * consumed when it reaches `waitForSignal(token)`. The canonical **durable-entity / accumulator**
1434
+ * pattern: one long-lived run per key (the `runId`) that loops on `waitForSignal`, fed events by
1435
+ * many `signalWithStart` calls. `start` is idempotent by `runId`, so concurrent callers converge on
1436
+ * one run. (Use a per-run `token`, e.g. derived from `runId`, so the signal targets this entity.)
1437
+ */
1438
+ signalWithStart(workflow: WorkflowRef, input: unknown, runId: string, signal: {
1439
+ token: string;
1440
+ payload?: unknown;
1441
+ }, opts?: StartOptions): Promise<{
1442
+ runId: string;
1443
+ }>;
1444
+ /**
1445
+ * Report the result of a `ctx.task(name, …)` back to its run (async completion). The external
1446
+ * worker that the task dispatched to calls this when done; the run resumes with `result`. Returns
1447
+ * null if no run is waiting on the task (e.g. a duplicate/late delivery) — a safe no-op.
1448
+ */
1449
+ completeTask(runId: string, name: string, result: unknown): Promise<RunResult | null>;
1450
+ /** Report that a `ctx.task` failed — the run resumes and throws a FatalError at the task. */
1451
+ failTask(runId: string, name: string, error: string): Promise<RunResult | null>;
1452
+ /**
1453
+ * Notify a parent that's waiting on `runId` as a child of its terminal outcome (the `ctx.child`
1454
+ * rendezvous). A no-op when no parent is waiting, so `execute()` can call it on every run without
1455
+ * knowing about the child feature.
1456
+ */
1457
+ private notifyParent;
1458
+ /**
1459
+ * Cancel a run (e.g. from the dashboard). Returns null if the run does not exist. Pass
1460
+ * `{ compensate: true }` to undo the saga first: the suspended run is resumed so its completed
1461
+ * steps' compensations run in reverse (visible as `compensate:<step>` events), then it's marked
1462
+ * cancelled. Without it, cancellation is immediate (no undo).
1463
+ */
1464
+ cancel(runId: string, opts?: {
1465
+ compensate?: boolean;
1466
+ }): Promise<RunResult | null>;
1467
+ /**
1468
+ * Bulk-cancel every run matching a filter — e.g. cancel all `order` runs tagged `vip`, or every run
1469
+ * whose `tier` search attribute is `free`. The filter is a {@link RunQuery} (workflow / status / tag
1470
+ * / search-attribute predicates), so it reuses the same matching the dashboard list uses. Each match
1471
+ * is run through {@link cancel}, so the same plumbing applies per run: child cascade, the optional
1472
+ * saga `compensate`, local cancel listeners, and the control-plane broadcast that tells the owning
1473
+ * worker to abort. Returns one {@link RunResult} per matched run (already-finished matches report
1474
+ * their terminal status — `cancel` is a no-op on them, never clobbering a completed/dead run).
1475
+ */
1476
+ cancelWhere(filter: Omit<RunQuery, 'limit' | 'offset'>, opts?: {
1477
+ compensate?: boolean;
1478
+ }): Promise<RunResult[]>;
1479
+ /**
1480
+ * Cascade cancellation to a run's children — both awaited (`ctx.child`, found via its live
1481
+ * `child:<id>` waiter) and fire-and-forget (`ctx.startChild`, found via its `spawn:<id>`
1482
+ * checkpoint). Recursive, so a whole subtree is cancelled; the terminal guard in `cancel` stops it
1483
+ * at finished / already-cancelled runs (no loops, no re-cancel).
1484
+ */
1485
+ private cancelChildren;
1486
+ /**
1487
+ * The ids of the runs a run spawned — both awaited (`ctx.child`) and fire-and-forget
1488
+ * (`ctx.startChild`, found via its `spawn:<id>` checkpoint). The canonical parent→children edge,
1489
+ * used for both cancellation cascades and the dashboard run-tree.
1490
+ *
1491
+ * An awaited child is discovered two ways, because the live `child:<id>` waiter only exists WHILE
1492
+ * the parent is suspended on it: the waiter resolves and is consumed the moment the child settles,
1493
+ * so a completed parent (or a completed awaited child) would otherwise drop out of the tree. The
1494
+ * `signal:child:<id>` checkpoint (the placeholder written when the parent first awaits the child,
1495
+ * overwritten as completed/failed when it settles) persists across completion, so we read both and
1496
+ * dedupe — the edge stays stable for a finished run, not just a live one.
1497
+ */
1498
+ getRunChildren(parentRunId: string): Promise<string[]>;
1499
+ /** The worker groups this engine dispatches to: every registered remote workflow's group, plus any
1500
+ * `extra` the caller declares. Local-step groups (a group consumed by in-process `@DurableStep`
1501
+ * workers, e.g. `pipeline`) aren't derivable from registrations — pass them via `extra` so a group
1502
+ * with backlog and ZERO workers is still reported (the alert case has no heartbeat to discover). */
1503
+ knownGroups(extra?: string[]): string[];
1504
+ /** Per-group worker health (queue backlog + live worker heartbeats). Covers {@link knownGroups}
1505
+ * (so a registered group with backlog and ZERO workers still reports — the alert case) UNION the
1506
+ * groups discovered from live heartbeats (so a local-step group like `pipeline`, not derivable
1507
+ * from registrations, shows once its workers beat). Empty when no transport can introspect health
1508
+ * (only the BullMQ transport implements `groupHealth`). */
1509
+ workerHealth(extra?: string[]): Promise<GroupHealth[]>;
1510
+ /**
1511
+ * Persist a streamed local-step lifecycle event from a remote workflow worker (see
1512
+ * {@link WorkflowStepEvent}). A Python `@workflow` runs its `ctx.step`s inline over one turn that
1513
+ * can last minutes; the worker streams each step's start/finish so the engine checkpoints it LIVE —
1514
+ * a step shows `running` the moment its body begins, then resolves to `completed`/`failed` with its
1515
+ * real wall-clock window and sub-process events — instead of every step appearing at once when the
1516
+ * turn ends. The turn's final `recordStep` command re-persists the same (runId, seq) checkpoint
1517
+ * idempotently, so this is purely additive observability and never changes the run's outcome.
1518
+ */
1519
+ private persistStepEvent;
1520
+ /**
1521
+ * Announce a local step's body has begun and (when `trackStepStart`) checkpoint it as `running`,
1522
+ * so it's visible in flight rather than appearing only on completion. The checkpoint is a
1523
+ * placeholder overwritten by {@link completeStep}/{@link failStep}; it never short-circuits replay
1524
+ * (only `completed` does), so a crash mid-body just re-runs the step. The `step.started` event
1525
+ * fires regardless — the live SSE view sees the start even with persistence off.
1526
+ */
1527
+ private startStep;
1528
+ /** Checkpoint a finished step and announce it — the two things that must always happen together. */
1529
+ private completeStep;
1530
+ /** Checkpoint a step that failed terminally, so the failure point is visible (not just the run). */
1531
+ private failStep;
1532
+ /**
1533
+ * Whether `run` may run now under its singleton key: it's among the `limit` oldest in-flight runs
1534
+ * (running or suspended) sharing the key, by `(createdAt, id)` order. A consistent store gives every
1535
+ * instance the same ordering, so admission is race-free + FIFO.
1536
+ */
1537
+ private admitSingleton;
1538
+ /**
1539
+ * Notify-on-release: a singleton run just settled (freeing a slot), so wake the gated waiters that
1540
+ * can now be admitted — immediately, instead of letting them sleep until their ~1s retry timer.
1541
+ * Dispatches up to `limit` of the oldest gated (`suspended` + same singleton tag) waiters by
1542
+ * `(createdAt, id)`; each re-checks admission in `runExecution` and runs only if it actually wins a
1543
+ * slot, so this never breaks the FIFO + race-free guarantees (the durable timer remains the
1544
+ * cross-instance / crash fallback — this just removes the poll latency on the happy path).
1545
+ */
1546
+ private wakeNextSingletons;
1547
+ private execute;
1548
+ /**
1549
+ * Advance a remote (cross-SDK) workflow one turn: hand its history to the executor (which dispatches
1550
+ * a workflow task to the worker and awaits its replay) and apply the decision. Mirrors
1551
+ * {@link runExecution}'s settle/suspend; the lease is held by {@link execute}. The result that lands
1552
+ * us back here (a remote step finished, a timer fired) goes through `resume` like any TS workflow.
1553
+ */
1554
+ private runRemoteExecution;
1555
+ /** The run's resolved durable ops as replay inputs: completed/failed steps + elapsed timers. */
1556
+ private remoteHistory;
1557
+ /** Apply a turn's commands: persist recorded local steps, dispatch remote calls, schedule timers.
1558
+ * Returns the earliest timer deadline to suspend on (or undefined — suspended on a result). */
1559
+ private applyCommands;
1560
+ /** The run body, lease held + renewed by {@link execute}. */
1561
+ private runExecution;
1562
+ /**
1563
+ * Run one saga compensation, retried up to `compensationRetries`, emitting a `compensate:<step>`
1564
+ * step event for its outcome so a stranded undo is visible. Never throws — a permanently-failing
1565
+ * compensation is skipped so it can't mask the original failure.
1566
+ */
1567
+ private runCompensation;
1568
+ /** The seam handed to {@link createWorkflowCtx}: the authoring API reaches durability + lifecycle
1569
+ * (checkpointing, dispatch, child start) through this, so the ctx primitives live in their own
1570
+ * module and the engine stays the orchestrator. */
1571
+ private ctxHostFor;
1572
+ /**
1573
+ * Resume a run paused at a {@link WorkflowCtx.breakpoint} (e.g. the dashboard "continue" button).
1574
+ * Finds the run's pending breakpoint checkpoint and signals it. Returns null if the run isn't
1575
+ * paused at a breakpoint.
1576
+ */
1577
+ continue(runId: string): Promise<RunResult | null>;
1578
+ /**
1579
+ * Read the latest value a run published for `key` via {@link WorkflowCtx.setEvent} — a
1580
+ * side-effect-free query of a live (or finished) run's state. Returns `undefined` if the run
1581
+ * never published that key. The suspend-model counterpart of a Temporal query.
1582
+ */
1583
+ getEvent<TValue = unknown>(runId: string, key: string): Promise<TValue | undefined>;
1584
+ /**
1585
+ * Register a validator gating `engine.update(runId, name, …)` for runs of `workflow`. The
1586
+ * validator runs BEFORE the update is delivered, so a rejection leaves the run untouched. One
1587
+ * validator per (workflow, update name); registering again replaces it.
1588
+ */
1589
+ registerUpdateValidator<TArg>(workflow: string, name: string, validate: UpdateValidator<TArg>): void;
1590
+ /**
1591
+ * Deliver a validated update to the run waiting at `ctx.onUpdate(name)`. Runs the registered
1592
+ * validator (if any) first: on rejection returns `{ accepted: false, reason }` without disturbing
1593
+ * the run; otherwise delivers `arg` and resumes, returning `{ accepted: true, run }` (`run` is null
1594
+ * if nothing was waiting — a too-early or duplicate update).
1595
+ */
1596
+ update(runId: string, name: string, arg: unknown): Promise<UpdateResult>;
1597
+ private callRemote;
1598
+ /**
1599
+ * Complete a durable remote step from its worker result and resume the run — runs on whichever
1600
+ * instance receives the result (the dispatching one may be gone), so the run is crash/scale-safe.
1601
+ * A no-op if the checkpoint isn't `pending` (a duplicate or late delivery).
1602
+ */
1603
+ private completeRemoteResult;
1604
+ /** Release the flow-control slot a dispatched step held (if any), by its stepId. */
1605
+ private releaseQueueSlot;
1606
+ /** In-memory await path for a remote step with a liveness `timeoutMs` (re-dispatch on timeout). */
1607
+ private callRemoteInMemory;
1608
+ /**
1609
+ * Await a remote result, but reject with `RemoteStepTimeout` if neither the result nor a heartbeat
1610
+ * arrives within `timeoutMs`. Each heartbeat (delivered via `transport.onHeartbeat`) rearms the
1611
+ * window, so a worker that keeps beating stays alive past `timeoutMs`.
1612
+ */
1613
+ private awaitWithHeartbeat;
1614
+ }
1615
+ /** Raised inside the workflow when a remote worker reports a step failure. */
1616
+ declare class RemoteStepError extends Error {
1617
+ readonly stepError?: StepError | undefined;
1618
+ constructor(stepError?: StepError);
1619
+ }
1620
+
1621
+ /**
1622
+ * Thrown inside a step to signal an unrecoverable failure: the engine will NOT retry it,
1623
+ * regardless of the step's `retries` setting, and fails the run immediately. Use it for
1624
+ * business errors that retrying cannot fix (e.g. a declined card, invalid input).
1625
+ */
1626
+ declare class FatalError extends Error {
1627
+ readonly code?: string | undefined;
1628
+ constructor(message: string, code?: string);
1629
+ }
1630
+ /**
1631
+ * Thrown by `start` when a singleton workflow's wait queue is full: the count of in-flight + gated
1632
+ * runs sharing the key already equals `limit + maxQueueDepth`, so admitting another would let the
1633
+ * same-key backlog grow unbounded. Back-pressure — the caller should retry later or shed load. Only
1634
+ * raised when {@link SingletonConfig.maxQueueDepth} is set (omit it for the old unbounded behavior).
1635
+ */
1636
+ declare class SingletonQueueFullError extends Error {
1637
+ readonly workflow: string;
1638
+ readonly key: string;
1639
+ readonly maxQueueDepth: number;
1640
+ constructor(workflow: string, key: string, maxQueueDepth: number);
1641
+ }
1642
+ /**
1643
+ * Internal control signal thrown to suspend a run (e.g. on a durable sleep). Not an error the
1644
+ * user should throw or catch; the engine uses it to stop execution and persist `wakeAt`.
1645
+ */
1646
+ declare class SignalTimeoutError extends Error {
1647
+ readonly token: string;
1648
+ readonly timeoutMs: number;
1649
+ constructor(token: string, timeoutMs: number);
1650
+ }
1651
+ /**
1652
+ * Thrown when a remote step produces no result and no heartbeat within its `timeoutMs` window —
1653
+ * i.e. the worker is presumed dead. Subject to the step's `retries` (it's retryable), so the engine
1654
+ * re-dispatches before giving up.
1655
+ */
1656
+ declare class RemoteStepTimeout extends Error {
1657
+ readonly stepId: string;
1658
+ readonly timeoutMs: number;
1659
+ constructor(stepId: string, timeoutMs: number);
1660
+ }
1661
+ /**
1662
+ * Thrown on resume when the workflow code no longer matches the recorded history: the step at a
1663
+ * logical position has a different name/kind than the checkpoint saved there. This means the
1664
+ * workflow definition changed (a step was added/removed/reordered) under an in-flight run without a
1665
+ * new `@Workflow` version — continuing would replay the wrong checkpoint into the wrong step and
1666
+ * silently corrupt the run, so the engine fails loudly instead. Register a new workflow version for
1667
+ * breaking changes (old runs finish on the version they started on).
1668
+ */
1669
+ declare class NonDeterminismError extends Error {
1670
+ readonly runId: string;
1671
+ readonly seq: number;
1672
+ constructor(runId: string, seq: number, expected: string, recorded: string);
1673
+ }
1674
+ declare class WorkflowSuspended extends Error {
1675
+ /** Epoch ms to auto-resume (durable sleep), or undefined when waiting on an external signal. */
1676
+ readonly wakeAt?: number | undefined;
1677
+ constructor(wakeAt?: number);
1678
+ }
1679
+ /**
1680
+ * Thrown by `ctx.continueAsNew(input)` to end the current run and hand off to a fresh execution of
1681
+ * the same workflow with a clean history — for long-running / looping workflows that would otherwise
1682
+ * accumulate unbounded checkpoints. The engine completes this run and starts the next one.
1683
+ */
1684
+ declare class ContinueAsNew extends Error {
1685
+ readonly input: unknown;
1686
+ constructor(input: unknown);
1687
+ }
1688
+
1689
+ /** Canonical step id — the stable identity of a step within a run, used for dedupe and
1690
+ * correlation. The format is part of the cross-language wire contract (Python builds the same). */
1691
+ declare function stepId(runId: string, seq: number): string;
1692
+ /** Deterministic signal token a breakpoint suspends on — derived from its logical position. Shared
1693
+ * so `ctx.breakpoint` (which records it) and `engine.continue` (which signals it) agree. */
1694
+ declare const breakpointToken: (runId: string, seq: number) => string;
1695
+ /** A remote-worker step body. The optional `log` records sub-process outcomes and debug/error
1696
+ * lines that ride back on the result — the TypeScript twin of the Python SDK's `StepContext`. */
1697
+ type StepHandler = (input: unknown, log: StepLogger) => Promise<unknown> | unknown;
1698
+ /**
1699
+ * Run `handler` for `task` and produce the wire-format {@link StepResult}. Pure (no transport,
1700
+ * no I/O beyond the handler), so every transport — and any language port — can share the exact
1701
+ * same completed / failed / no-handler contract instead of re-deriving it.
1702
+ */
1703
+ declare function runStepHandler(task: RemoteTask, handler: StepHandler | undefined): Promise<StepResult>;
1704
+
1705
+ interface RemoteStepConfig<TInput, TOutput> extends StepOptions {
1706
+ name: string;
1707
+ /** Worker group expected to handle this step. Defaults to the `name` prefix before the first dot. */
1708
+ group?: string;
1709
+ input: z.ZodType<TInput>;
1710
+ output: z.ZodType<TOutput>;
1711
+ }
1712
+ /**
1713
+ * Defines a typed handle to a step that runs on a remote worker. Call it from a workflow with
1714
+ * `ctx.call(step, input)`; a worker registers a handler under the same `name`.
1715
+ */
1716
+ declare function remoteStep<TInput, TOutput>(config: RemoteStepConfig<TInput, TOutput>): RemoteStepDef<TInput, TOutput>;
1717
+
1718
+ /**
1719
+ * A {@link WorkflowExecutor} backed by a {@link Transport}: it advances a remote workflow by
1720
+ * dispatching a {@link WorkflowTask} over the broker and awaiting the matching {@link WorkflowDecision}
1721
+ * (correlated by `taskId`). Pass one to `engine.registerRemote(name, version, { group, executor })` so
1722
+ * a workflow authored in another SDK (e.g. the Python `durable-worker`) is driven over Redis/BullMQ.
1723
+ *
1724
+ * Recovery-safe: if the engine crashes awaiting a decision, the re-drive dispatches a fresh task with
1725
+ * the same history — the worker's replay is deterministic, so it returns the same decision; a late
1726
+ * decision for the old `taskId` simply finds no waiter and is dropped.
1727
+ */
1728
+ declare class RemoteWorkflowExecutor implements WorkflowExecutor {
1729
+ private readonly transport;
1730
+ private readonly group;
1731
+ private readonly opts;
1732
+ private readonly pending;
1733
+ private subscribed;
1734
+ constructor(transport: Transport, group: string, opts?: {
1735
+ timeoutMs?: number;
1736
+ });
1737
+ private ensureSubscribed;
1738
+ advance(run: WorkflowRun, history: HistoryEvent[]): Promise<WorkflowDecision>;
1739
+ }
1740
+
1741
+ /**
1742
+ * Transforms a payload value as it crosses the store boundary — e.g. encrypt-at-rest, compress, or
1743
+ * redact PII. `decode` must be the exact inverse of `encode`. Both are synchronous (use a local
1744
+ * cipher / codec); wrap an async KMS yourself if you need one.
1745
+ */
1746
+ interface PayloadCodec {
1747
+ encode(value: unknown): unknown;
1748
+ decode(value: unknown): unknown;
1749
+ }
1750
+ /**
1751
+ * A `StateStore` decorator that runs run/step **payloads** (input + output) through a
1752
+ * {@link PayloadCodec} — encoded on write, decoded on read — so they're never stored in the clear.
1753
+ * Adapter-agnostic: wrap any store. Searchable metadata (id, status, workflow, tags, timestamps) and
1754
+ * the structured `error` are left untouched so the dashboard, queries, and recovery still work.
1755
+ *
1756
+ * ```ts
1757
+ * const store = new CodecStateStore(new TypeOrmStateStore(ds), aesCodec);
1758
+ * ```
1759
+ */
1760
+ declare class CodecStateStore implements StateStore {
1761
+ private readonly inner;
1762
+ private readonly codec;
1763
+ constructor(inner: StateStore, codec: PayloadCodec);
1764
+ private enc;
1765
+ private dec;
1766
+ private encRun;
1767
+ private decRun;
1768
+ private encCp;
1769
+ private decCp;
1770
+ ensureSchema(): Promise<void>;
1771
+ createRun(run: WorkflowRun): Promise<void>;
1772
+ updateRun(runId: string, patch: Partial<WorkflowRun>): Promise<void>;
1773
+ getRun(runId: string): Promise<WorkflowRun | null>;
1774
+ getCheckpoint(runId: string, seq: number): Promise<StepCheckpoint | null>;
1775
+ saveCheckpoint(checkpoint: StepCheckpoint): Promise<void>;
1776
+ transaction<T>(work: (tx: {
1777
+ raw: unknown;
1778
+ saveCheckpoint: (cp: StepCheckpoint) => Promise<void>;
1779
+ }) => Promise<T>): Promise<T>;
1780
+ listIncompleteRuns(): Promise<WorkflowRun[]>;
1781
+ listPendingRuns(limit: number): Promise<WorkflowRun[]>;
1782
+ listDueTimers(nowMs: number): Promise<WorkflowRun[]>;
1783
+ tryLockRun(runId: string, owner: string, leaseUntilMs: number, nowMs: number): Promise<boolean>;
1784
+ releaseRunLock(runId: string): Promise<void>;
1785
+ renewRunLock(runId: string, owner: string, leaseUntilMs: number): Promise<boolean>;
1786
+ putSignalWaiter(waiter: SignalWaiter): Promise<void>;
1787
+ takeSignalWaiter(token: string): Promise<SignalWaiter | null>;
1788
+ listSignalWaiters(prefix: string): Promise<SignalWaiter[]>;
1789
+ bufferSignal(token: string, payload: unknown): Promise<void>;
1790
+ takeBufferedSignal(token: string): Promise<{
1791
+ payload: unknown;
1792
+ } | null>;
1793
+ listRuns(query: RunQuery): Promise<WorkflowRun[]>;
1794
+ listCheckpoints(runId: string): Promise<StepCheckpoint[]>;
1795
+ }
1796
+
1797
+ /** Token prefix for every waiter on `name` — used by `listSignalWaiters` to fan an event out. */
1798
+ declare function eventPrefix(name: string): string;
1799
+ /** Build the unique waiter token for one `waitForEvent` call (name + match + run position). */
1800
+ declare function eventToken(name: string, match: Record<string, unknown> | undefined, runId: string, seq: number): string;
1801
+ /** The match criteria embedded in an event waiter token (the 3rd `:`-segment is the base64 match). */
1802
+ declare function eventMatchOf(token: string): Record<string, unknown>;
1803
+ /** Whether `payload` satisfies a waiter's `match`: every match key deep-equals the payload's value. */
1804
+ declare function eventMatches(payload: unknown, match: Record<string, unknown>): boolean;
1805
+
1806
+ interface MetricsSnapshot {
1807
+ runs: {
1808
+ started: number;
1809
+ completed: number;
1810
+ failed: number;
1811
+ suspended: number;
1812
+ };
1813
+ steps: {
1814
+ started: number;
1815
+ completed: number;
1816
+ failed: number;
1817
+ };
1818
+ /** Per-workflow run counters, keyed by workflow name. */
1819
+ byWorkflow: Record<string, {
1820
+ started: number;
1821
+ completed: number;
1822
+ failed: number;
1823
+ }>;
1824
+ /** Sum + count of recorded step durations (ms), for an average. */
1825
+ stepDuration: {
1826
+ sumMs: number;
1827
+ count: number;
1828
+ };
1829
+ }
1830
+ interface MetricsCollector {
1831
+ snapshot(): MetricsSnapshot;
1832
+ /** Prometheus text exposition of the counters — serve this from a `/metrics` endpoint. */
1833
+ prometheus(): string;
1834
+ /** Unsubscribe from the engine. */
1835
+ stop(): void;
1836
+ }
1837
+ type Subscribable = {
1838
+ subscribe(listener: (event: EngineEvent) => void): () => void;
1839
+ };
1840
+ /**
1841
+ * Subscribe to an engine's lifecycle events and accumulate Prometheus-style counters (runs + steps by
1842
+ * outcome, per-workflow run counts, step-duration sum/count). Dependency-free — call `.prometheus()`
1843
+ * from your metrics endpoint, or `.snapshot()` for the raw numbers. Counters are per process; scrape
1844
+ * each instance.
1845
+ */
1846
+ declare function collectMetrics(engine: Subscribable): MetricsCollector;
1847
+
1848
+ interface ScheduledWorkflow {
1849
+ /** Stable key identifying this schedule — part of the deterministic run id. */
1850
+ key: string;
1851
+ workflow: string;
1852
+ input?: unknown;
1853
+ /** Start one run every `everyMs`. Mutually exclusive with `cron`. */
1854
+ everyMs?: number;
1855
+ /**
1856
+ * A cron expression (5 fields `m h dom mon dow`, or 6 with leading seconds) evaluated in
1857
+ * {@link ScheduledWorkflow.timezone}. Mutually exclusive with `everyMs`. Needs the optional peer
1858
+ * dependency `cron-parser`.
1859
+ */
1860
+ cron?: string;
1861
+ /** IANA timezone the `cron` fires in (e.g. `America/Sao_Paulo`). Defaults to UTC. */
1862
+ timezone?: string;
1863
+ /** Temporarily stop firing this schedule (kept registered). Defaults to false. */
1864
+ paused?: boolean;
1865
+ /**
1866
+ * What to do when the previous window's run hasn't finished yet (fixed-interval schedules only):
1867
+ * `'allow'` (default) starts the new window anyway; `'skip'` skips it while the prior run is still
1868
+ * `running`/`suspended`, so a slow run can't pile up overlapping executions.
1869
+ */
1870
+ overlap?: 'allow' | 'skip';
1871
+ /**
1872
+ * Randomly delay the dispatch by up to this many ms BEFORE firing, to spread load when many
1873
+ * instances tick on the same boundary (avoids a thundering herd). Opt-in; absent = fire immediately.
1874
+ *
1875
+ * This is dispatch-path jitter only — it does NOT affect the run id (still the time bucket), so
1876
+ * idempotency is unchanged: two instances jittering the same window still start it exactly once.
1877
+ * (The engine forbids `Math.random` inside workflow code; the scheduler dispatch path is not
1878
+ * workflow code, so jitter here is fine.)
1879
+ */
1880
+ jitter?: number;
1881
+ /**
1882
+ * Enqueue windows that were missed while the scheduler was down, instead of silently skipping
1883
+ * them. Opt-in; absent = only the current window fires. `maxCatchup` bounds how many PRIOR windows
1884
+ * are backfilled (a long outage can't flood the system). Backfilled runs use each missed window's
1885
+ * deterministic bucket run id, so `engine.start` idempotency skips any that already ran.
1886
+ */
1887
+ backfill?: {
1888
+ maxCatchup: number;
1889
+ };
1890
+ }
1891
+ /** Injectable dispatch-path effects (kept out of the deterministic run-id logic). Defaults to real
1892
+ * `Math.random` + `setTimeout`; overridden in tests so jitter is deterministic and instant. */
1893
+ interface RunSchedulesOptions {
1894
+ random?: () => number;
1895
+ sleep?: (ms: number) => Promise<void>;
1896
+ }
1897
+ /** Deterministic run id for a fixed-interval schedule's current time window. */
1898
+ declare function scheduledRunId(key: string, everyMs: number, nowMs: number): string;
1899
+ /**
1900
+ * Epoch ms of the most recent cron fire at or before `nowMs`, evaluated in `timezone` (default UTC).
1901
+ * This is the deterministic "bucket" a cron run is keyed on, so polling repeatedly within an
1902
+ * interval resolves to the same fire time — and thus the same idempotent run id.
1903
+ */
1904
+ declare function prevCronFireMs(expr: string, nowMs: number, timezone?: string): number;
1905
+ /**
1906
+ * Start each schedule's current-window run. The run id is the time bucket (a fixed-interval index,
1907
+ * or the cron fire time) and `engine.start` is idempotent, so firing this on an interval — or
1908
+ * racing two instances on the same tick — starts **each window exactly once**. Wire it to a
1909
+ * `setInterval`, the durable timer poller, or `@nestjs/schedule`. Returns the run ids for the
1910
+ * current windows.
1911
+ */
1912
+ declare function runSchedules(engine: Pick<WorkflowEngine, 'start' | 'getRun'>, schedules: readonly ScheduledWorkflow[], nowMs: number, opts?: RunSchedulesOptions): Promise<string[]>;
1913
+
1914
+ /**
1915
+ * Does a run's search attributes satisfy EVERY filter (AND)? A missing key never matches (so `ne`
1916
+ * on an absent key is false too — the attribute simply isn't there to compare). Empty/undefined
1917
+ * filters match everything. Shared by every store so typed/range queries behave identically across
1918
+ * adapters (applied in-process after the coarse workflow/status/tag filters).
1919
+ */
1920
+ declare function matchesAttributes(attributes: SearchAttributes | undefined, filters: AttributeFilter[] | undefined): boolean;
1921
+ /**
1922
+ * Apply a query's attribute predicates then its `offset`/`limit`, in-process — for store adapters
1923
+ * that can't express typed/range predicates in SQL. Pass rows already coarse-filtered (workflow /
1924
+ * status / tag) and sorted newest-first; this filters by `attributes` and paginates. Only call it
1925
+ * when `query.attributes` is set (otherwise let the DB do `LIMIT`/`OFFSET`).
1926
+ */
1927
+ declare function applyAttributeQuery(rows: WorkflowRun[], query: RunQuery): WorkflowRun[];
1928
+ /**
1929
+ * One normalized row of the `durable_run_attributes` side-table: a single (key → value) pair of a
1930
+ * run's search attributes, split into typed columns so SQL can index/range-scan it. Exactly one of
1931
+ * `strValue`/`numValue` is set per row (booleans normalize to a string `"true"`/`"false"` so `eq`/`ne`
1932
+ * still match). NULL for the column that doesn't apply, which keeps `(key, numValue)` /
1933
+ * `(key, strValue)` indexes selective.
1934
+ */
1935
+ interface RunAttributeRow {
1936
+ runId: string;
1937
+ key: string;
1938
+ strValue: string | null;
1939
+ numValue: number | null;
1940
+ }
1941
+ /**
1942
+ * Explode a run's search attributes into normalized side-table rows (one per key). Used by SQL stores
1943
+ * to maintain `durable_run_attributes` on every create/update and by the in-memory store's index, so
1944
+ * attribute predicates can be pushed DOWN into a join/EXISTS instead of scanned in-process. Numbers go
1945
+ * to `numValue`; strings to `strValue`; booleans to `strValue` as `"true"`/`"false"` (matching how the
1946
+ * in-process `compare` treats `eq`/`ne` on booleans). Returns `[]` for a run with no attributes.
1947
+ */
1948
+ declare function normalizeAttributeRows(runId: string, attributes: SearchAttributes | undefined): RunAttributeRow[];
1949
+ /** SQL comparison operator for an {@link AttributeOp} (used to build pushdown predicates). */
1950
+ declare function sqlComparator(op: AttributeOp): string;
1951
+ /**
1952
+ * Which side-table column an attribute filter compares against: numeric operands (and range ops on
1953
+ * numbers) hit `numValue`; everything else hits `strValue` (booleans are stored as `"true"`/`"false"`).
1954
+ * Keeping this in core means every SQL adapter pushes predicates down identically.
1955
+ */
1956
+ declare function attributeColumnFor(filter: AttributeFilter): 'numValue' | 'strValue';
1957
+ /** The literal a side-table predicate compares against (booleans → `"true"`/`"false"` strings). */
1958
+ declare function attributeOperand(filter: AttributeFilter): string | number;
1959
+
1960
+ /**
1961
+ * Dependency-injection tokens. Adapters bind their implementations to these so `core` and
1962
+ * `@dudousxd/nestjs-durable` depend only on the interfaces in `./interfaces`.
1963
+ */
1964
+ declare const STATE_STORE: unique symbol;
1965
+ declare const TRANSPORT: unique symbol;
1966
+ declare const DURABLE_OPTIONS: unique symbol;
1967
+
1968
+ /**
1969
+ * A non-durable, in-process `StateStore` for tests and local development.
1970
+ * The shipped `@dudousxd/nestjs-durable-store` package re-exports an equivalent.
1971
+ */
1972
+ declare class InMemoryStateStore implements StateStore {
1973
+ private readonly runs;
1974
+ private readonly checkpoints;
1975
+ private readonly signalWaiters;
1976
+ /**
1977
+ * Normalized search-attribute side-table, the in-memory analog of the SQL stores'
1978
+ * `durable_run_attributes`: `key -> (runId -> row)`. Maintained on every create/update so an
1979
+ * attribute query can be pushed DOWN to a key-indexed candidate set instead of scanning every run
1980
+ * and comparing in-process. Mirrors {@link normalizeAttributeRows} so behavior matches the SQL path.
1981
+ */
1982
+ private readonly attributeIndex;
1983
+ /** Test/observability hook: how many candidate runs the last attribute query's index produced. */
1984
+ lastAttributeCandidates: number;
1985
+ private key;
1986
+ /** Drop then re-add a run's normalized attribute rows so the index tracks its current attributes. */
1987
+ private reindexAttributes;
1988
+ createRun(run: WorkflowRun): Promise<void>;
1989
+ updateRun(runId: string, patch: Partial<WorkflowRun>): Promise<void>;
1990
+ getRun(runId: string): Promise<WorkflowRun | null>;
1991
+ getCheckpoint(runId: string, seq: number): Promise<StepCheckpoint | null>;
1992
+ saveCheckpoint(checkpoint: StepCheckpoint): Promise<void>;
1993
+ listIncompleteRuns(): Promise<WorkflowRun[]>;
1994
+ listPendingRuns(limit: number): Promise<WorkflowRun[]>;
1995
+ listDueTimers(nowMs: number): Promise<WorkflowRun[]>;
1996
+ tryLockRun(runId: string, owner: string, leaseUntilMs: number, nowMs: number): Promise<boolean>;
1997
+ releaseRunLock(runId: string): Promise<void>;
1998
+ renewRunLock(runId: string, owner: string, leaseUntilMs: number): Promise<boolean>;
1999
+ putSignalWaiter(waiter: SignalWaiter): Promise<void>;
2000
+ takeSignalWaiter(token: string): Promise<SignalWaiter | null>;
2001
+ listSignalWaiters(prefix: string): Promise<SignalWaiter[]>;
2002
+ transaction<T>(work: (tx: {
2003
+ raw: unknown;
2004
+ saveCheckpoint: (cp: StepCheckpoint) => Promise<void>;
2005
+ }) => Promise<T>): Promise<T>;
2006
+ private readonly bufferedSignals;
2007
+ bufferSignal(token: string, payload: unknown): Promise<void>;
2008
+ takeBufferedSignal(token: string): Promise<{
2009
+ payload: unknown;
2010
+ } | null>;
2011
+ /**
2012
+ * Candidate run ids satisfying ONE attribute predicate, served from {@link attributeIndex} — the
2013
+ * pushdown step. Numeric predicates scan only `numValue` rows under the key; string/boolean ones
2014
+ * scan `strValue` rows. This is the in-memory analog of the SQL EXISTS on `durable_run_attributes`.
2015
+ */
2016
+ private candidatesForFilter;
2017
+ listRuns(query: RunQuery): Promise<WorkflowRun[]>;
2018
+ listCheckpoints(runId: string): Promise<StepCheckpoint[]>;
2019
+ }
2020
+
2021
+ /**
2022
+ * An in-process `Transport` (and `ControlPlane`) for tests and local development: registered
2023
+ * handlers stand in for remote workers, so a whole cross-app workflow runs in a single process,
2024
+ * and control messages broadcast locally. Pass the same instance as both `transport` and
2025
+ * `controlPlane`.
2026
+ */
2027
+ declare class InMemoryTransport implements Transport, ControlPlane {
2028
+ private readonly handlers;
2029
+ private resultHandler?;
2030
+ private stepEventHandler?;
2031
+ private readonly controlHandlers;
2032
+ /** Register a fake worker handler for a step name. */
2033
+ handle(name: string, fn: StepHandler): void;
2034
+ dispatch(task: RemoteTask): Promise<void>;
2035
+ onResult(handler: (result: StepResult) => Promise<void>): void;
2036
+ onHeartbeat(_handler: (beat: Heartbeat) => Promise<void>): void;
2037
+ /** Workflow step lifecycle: deliver to the engine's handler (async, mirroring a real broker). */
2038
+ dispatchStepEvent(event: WorkflowStepEvent): Promise<void>;
2039
+ onStepEvent(handler: (event: WorkflowStepEvent) => Promise<void>): void;
2040
+ publishControl(msg: ControlMessage): Promise<void>;
2041
+ onControl(handler: (msg: ControlMessage) => void): void;
2042
+ }
2043
+
2044
+ export { type AccumulatorHost, type Admission, type AdmissionItem, type AttributeFilter, type AttributeOp, type BackoffStrategy, CodecStateStore, ContinueAsNew, type ControlMessage, type ControlPlane, DURABLE_OPTIONS, type DurableWebhook, type EngineEvent, type EngineEventType, type EngineListener, Entities, type EntityConfig, type EntityHandler, type EntityHost, EventAccumulators, type EventBatchConfig, FatalError, type GroupHealth, type Heartbeat, type HistoryEvent, InMemoryStateStore, InMemoryTransport, type MetricsCollector, type MetricsSnapshot, type NamedTransport, NonDeterminismError, type PayloadCodec, type QueueConfig, QueueController, type RemoteStepConfig, type RemoteStepDef, RemoteStepError, RemoteStepTimeout, type RemoteTask, RemoteWorkflowExecutor, type RunAttributeRow, type RunDispatcher, type RunQuery, type RunResult, type RunSchedulesOptions, type RunStatus, STATE_STORE, type ScheduledWorkflow, type SearchAttributes, SignalTimeoutError, type SignalWaiter, type SingletonConfig, SingletonQueueFullError, type StartOptions, type StateStore, type StepCheckpoint, type StepError, type StepEvent, type StepHandler, type StepInterceptor, type StepInvocation, type StepKind, type StepLogger, type StepOptions, type StepResult, type StoreTransaction, type SubProcessHandle, TRANSPORT, type Transport, type UpdateResult, type UpdateValidator, WORKFLOW_NAME_KEY, type WorkerHeartbeat, type WorkflowClass, type WorkflowCommand, type WorkflowCtx, type WorkflowDecision, WorkflowEngine, type WorkflowEngineDeps, type WorkflowExecutor, type WorkflowInputOf, type WorkflowOutputOf, type WorkflowRef, type WorkflowRun, type WorkflowStepEvent, WorkflowSuspended, type WorkflowTask, applyAttributeQuery, attributeColumnFor, attributeOperand, breakpointToken, collectMetrics, eventMatchOf, eventMatches, eventPrefix, eventToken, matchesAttributes, normalizeAttributeRows, parseDuration, prevCronFireMs, remoteStep, runSchedules, runStepHandler, scheduledRunId, sqlComparator, stepId, workflowName };