@dudousxd/nestjs-durable 0.0.0-beta-20260701163515

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,718 @@
1
+ import { StepError, WorkflowRef, SingletonConfig, WorkflowExecutor, StateStore, Transport, NamedTransport, ControlPlane, ScheduledWorkflow, RetentionPolicy, QueueConfig, AdmissionBackend, WorkflowEngine, EntityHandler, StepInvocation, WorkflowClass, WorkflowInputOf, StartOptions, RunResult } from '@dudousxd/nestjs-durable-core';
2
+ import { ConcurrencyOption, RunRedisWorkerOptions, RunningWorker, DurableWorkerRuntime } from '@dudousxd/durable-worker';
3
+ import { DynamicModule, InjectionToken, OnModuleInit, OnApplicationBootstrap, OnApplicationShutdown, Provider } from '@nestjs/common';
4
+ import { DiscoveryService, MetadataScanner } from '@nestjs/core';
5
+
6
+ /**
7
+ * Local, structural mirror of `@dudousxd/nestjs-context`'s public accessor
8
+ * (`packages/core/src/accessor.ts`).
9
+ *
10
+ * We deliberately do NOT import nestjs-context (it is an OPTIONAL peer). Instead
11
+ * we declare the same shape here and inject it via the shared
12
+ * {@link CONTEXT_ACCESSOR} token with `@Optional()`. Any object that structurally
13
+ * satisfies this interface — including nestjs-context's real accessor — works.
14
+ *
15
+ * Kept byte-aligned with nestjs-context's `ContextAccessor` (and the identical
16
+ * mirror in `@dudousxd/nestjs-authz`): `traceId()` / `tenantId()` are REQUIRED
17
+ * (the real accessor always provides them) and `get()` is included, so the
18
+ * structural match stays exact when DurableModule auto-feeds the carrier.
19
+ */
20
+ interface UserRef {
21
+ type: string;
22
+ id: string | number;
23
+ }
24
+ /** Opaque shape of the context store. durable never reads it; mirrors the upstream surface. */
25
+ type ContextStore = Record<string, unknown>;
26
+ interface ContextAccessor {
27
+ /** Trace id for the current request, or `undefined` when unavailable. */
28
+ traceId(): string | undefined;
29
+ /** Current tenant id, or `undefined` when no multi-tenant context is populated. */
30
+ tenantId(): string | undefined;
31
+ /** Reference to the current user, or `undefined` when unauthenticated. */
32
+ userRef(): UserRef | undefined;
33
+ /** The raw context store for the current request, or `undefined`. */
34
+ get(): ContextStore | undefined;
35
+ }
36
+
37
+ declare const WORKFLOW_METADATA: unique symbol;
38
+ interface WorkflowMeta {
39
+ name: string;
40
+ version: string;
41
+ /** The workflow this workflow's dead runs route to (a name or a class). See `WorkflowOptions`. */
42
+ deadLetterWorkflow?: WorkflowRef | undefined;
43
+ /** Static searchable labels stamped on every run of this workflow. See `WorkflowOptions`. */
44
+ tags?: string[] | undefined;
45
+ /** Per-key serialization (a durable mutex). See `WorkflowOptions`. */
46
+ singleton?: SingletonConfig | undefined;
47
+ /** Max wall-clock lifetime before a run is cancelled (e.g. `'2h'`). See `WorkflowOptions`. */
48
+ executionTimeout?: string | number | undefined;
49
+ /** class-validator DTO validated at start. See `WorkflowOptions`. */
50
+ inputSchema?: (new (...args: any[]) => object) | undefined;
51
+ /** Custom input validator (throws on invalid). See `WorkflowOptions`. */
52
+ validateInput?: ((input: unknown) => void | Promise<void>) | undefined;
53
+ /** Event names that start a fresh run of this workflow. See `WorkflowOptions`. */
54
+ onEvent?: string[] | undefined;
55
+ /** Debounce `onEvent` triggers — fire once it's quiet for this long. See `WorkflowOptions`. */
56
+ debounce?: string | number | undefined;
57
+ /** Batch `onEvent` triggers — fire on size or window. See `WorkflowOptions`. */
58
+ batch?: {
59
+ maxSize: number;
60
+ within: string | number;
61
+ } | undefined;
62
+ }
63
+ interface WorkflowOptions {
64
+ name: string;
65
+ version?: string;
66
+ /**
67
+ * Route this workflow's dead-lettered runs to another **registered** workflow — by class
68
+ * (`deadLetterWorkflow: CheckoutDlqWorkflow`, refactor-safe) or by name for a cross-runtime
69
+ * handler. For a handler co-located on the same class, prefer an inline `@DeadLetter()` method
70
+ * instead — it takes precedence, and declaring both is a boot-time error. The handler receives a
71
+ * {@link DeadLetter} payload, idempotent by a `dlq:<runId>` id.
72
+ */
73
+ deadLetterWorkflow?: WorkflowRef;
74
+ /**
75
+ * Static labels stamped on **every run** of this workflow (e.g. `tags: ['etl', 'critical']`) and
76
+ * merged with any per-run tags passed to `start`. Searchable/filterable in the dashboard.
77
+ */
78
+ tags?: string[];
79
+ /**
80
+ * Serialize runs of this workflow that share a key — a durable FIFO mutex. e.g.
81
+ * `singleton: { key: (input) => `base:${input.baseId}` }` runs at most one pipeline per base at a
82
+ * time; same-key runs queue (suspended) and admit in creation order as slots free. `limit` (default
83
+ * 1) raises the concurrency.
84
+ */
85
+ singleton?: SingletonConfig;
86
+ /**
87
+ * Max wall-clock lifetime for a run of this workflow (e.g. `'2h'`, `'7 days'`, or ms). A run that
88
+ * outlives it is moved to `cancelled` (`execution_timeout`) by the timer poller — a backstop for
89
+ * runs that get stuck or loop forever. Omit for no limit.
90
+ */
91
+ executionTimeout?: string | number;
92
+ /**
93
+ * Validate the workflow input at `start` against a **class-validator DTO** (the same
94
+ * `plainToInstance` + `validate` NestJS runs in controllers) — invalid input is rejected before any
95
+ * run is created. Needs the optional peers `class-validator` + `class-transformer`.
96
+ * `@Workflow({ inputSchema: CheckoutInput })`.
97
+ */
98
+ inputSchema?: new (...args: any[]) => object;
99
+ /**
100
+ * Custom input validator (throws on invalid) — an escape hatch for zod/yup/etc. instead of
101
+ * `inputSchema`. Takes precedence over `inputSchema` if both are set.
102
+ */
103
+ validateInput?: (input: unknown) => void | Promise<void>;
104
+ /**
105
+ * Start a fresh run of this workflow whenever any of these events is published via
106
+ * `publishEvent(name, payload)` — the payload becomes the run's input. e.g.
107
+ * `onEvent: ['user.registered', 'user.invited']`. The same subscription can also be declared with
108
+ * the `@OnEvent(...)` class decorator; the two are merged.
109
+ */
110
+ onEvent?: string[];
111
+ /**
112
+ * Coalesce `onEvent` triggers by **debouncing** — start one run only once events have been quiet
113
+ * for this long (resets on each event), with the LAST payload. e.g. `debounce: '30s'`.
114
+ */
115
+ debounce?: string | number;
116
+ /**
117
+ * Coalesce `onEvent` triggers by **batching** — start one run with all payloads (`{ events: [...] }`)
118
+ * once `maxSize` is reached or `within` elapses from the first event. e.g.
119
+ * `batch: { maxSize: 100, within: '10s' }`.
120
+ */
121
+ batch?: {
122
+ maxSize: number;
123
+ within: string | number;
124
+ };
125
+ }
126
+ /**
127
+ * Marks a provider class as a durable workflow. Its `run(ctx, input)` method becomes the
128
+ * workflow function the engine executes and replays.
129
+ */
130
+ declare function Workflow(options: WorkflowOptions): ClassDecorator;
131
+ declare function getWorkflowMeta(target: Function): WorkflowMeta | undefined;
132
+ declare const DURABLE_STEP_METADATA: unique symbol;
133
+ interface DurableStepMeta {
134
+ name: string;
135
+ }
136
+ /**
137
+ * Marks a provider method as the handler for a remote step `name`. An in-process transport
138
+ * (e.g. the event-emitter transport) routes dispatched tasks to it. The method's single
139
+ * argument is the step input; its return value is the step output.
140
+ */
141
+ declare function Step(name: string): MethodDecorator;
142
+ /**
143
+ * @deprecated Use `@Step` instead. `@DurableStep` is a back-compat alias of {@link Step} and
144
+ * writes the same `DURABLE_STEP_METADATA`, so discovery/registrars treat them identically.
145
+ */
146
+ declare const DurableStep: typeof Step;
147
+ declare function getDurableStepMeta(method: Function): DurableStepMeta | undefined;
148
+ declare const DEAD_LETTER_METADATA: unique symbol;
149
+ /**
150
+ * The payload a dead-letter handler receives: the dead run's id, its workflow, the input it was
151
+ * started with (typed via `TInput`), and the failure that killed it.
152
+ */
153
+ interface DeadLetter<TInput = unknown> {
154
+ /** Id of the run that was dead-lettered (inspectable + retriable in the dashboard). */
155
+ deadRunId: string;
156
+ /** Name of the workflow whose run died. */
157
+ workflow: string;
158
+ /** The original input the dead run was started with. */
159
+ input: TInput;
160
+ /** The structured error that moved the run to `dead`, when known. */
161
+ error?: StepError;
162
+ }
163
+ /**
164
+ * Marks a method on a `@Workflow` class as that workflow's **inline dead-letter handler**. When a
165
+ * run of the workflow is moved to `dead` (exceeded `maxRecoveryAttempts`), this method runs — as a
166
+ * durable workflow itself, auto-registered as `<workflow>.dlq` with a `dlq:<runId>` id — receiving a
167
+ * {@link DeadLetter} payload. It shares the class's injected dependencies, so the handler lives in
168
+ * the same file as the workflow it protects.
169
+ *
170
+ * Takes precedence over `@Workflow({ deadLetterWorkflow })` and the module-level `deadLetterWorkflow`
171
+ * default. The method signature is `(ctx, dead)` — the same `ctx` a workflow `run` gets.
172
+ */
173
+ declare function DeadLetter(): MethodDecorator;
174
+ declare function isDeadLetterHandler(method: Function): boolean;
175
+ declare const ON_EVENT_METADATA: unique symbol;
176
+ /**
177
+ * Subscribe a `@Workflow` class to one or more events: when any of them is published via
178
+ * `publishEvent(name, payload)`, a fresh run of the workflow starts with the payload as input.
179
+ * Equivalent to `@Workflow({ onEvent })` — use whichever reads better; multiple `@OnEvent(...)`
180
+ * decorators and the option are all merged.
181
+ */
182
+ declare function OnEvent(...events: string[]): ClassDecorator;
183
+ /** All events a workflow class subscribes to — the union of `@Workflow({ onEvent })` and `@OnEvent`. */
184
+ declare function getOnEvents(meta: WorkflowMeta, target: object): string[];
185
+
186
+ /**
187
+ * Options for a **store-less thin worker** process: where to consume (`connection`/`prefix`) and
188
+ * which worker `groups` this process serves. There is deliberately NO `store`, `transport`, engine,
189
+ * dashboard, recovery, or timer config — a thin worker only registers `@Workflow`/`@Step`
190
+ * handlers and runs the BullMQ consumer (see {@link DurableWorkerModule}).
191
+ */
192
+ interface DurableWorkerModuleOptions {
193
+ /** ioredis connection (string or options), as `runRedisWorker` / `BullMQTransport` accept. */
194
+ connection: string | Record<string, unknown>;
195
+ /** The worker groups this process serves — one BullMQ consumer is started per group. */
196
+ groups: string[];
197
+ /**
198
+ * The tenant this worker process serves. Each of {@link groups} is suffixed via
199
+ * `tenantGroup(group, tenant)` (`@dudousxd/nestjs-durable-core`) before the consumer starts, so
200
+ * `undefined`, `''`, or `'default'` stays byte-identical to the bare group (production
201
+ * unchanged), and any other tenant serves `<group>@<tenant>` — matching the group name an
202
+ * OPERATOR control plane's `remoteByConvention` routes that tenant's runs to
203
+ * (`tenantGroup(run.workflow, run.namespace)` on the engine side).
204
+ */
205
+ tenant?: string;
206
+ /** Key prefix namespacing the durable queues. Defaults to `durable` (matches the transport). */
207
+ prefix?: string;
208
+ /** Stable id for this worker process in heartbeats/control. Defaults to a per-host/pid id. */
209
+ instanceId?: string;
210
+ /**
211
+ * How many tasks each group's consumer runs concurrently (BullMQ Worker concurrency). Defaults to 1.
212
+ * Applies to every group unless overridden per-group by {@link concurrencyByGroup}. Raise it so a
213
+ * fanned-out batch (e.g. the N remote steps of a `gather`) runs in parallel instead of serially.
214
+ *
215
+ * Pass `'adaptive'` (or `{ mode:'adaptive', ... }`) to let each consumer self-tune its concurrency
216
+ * (latency gradient + RAM brake + backpressure) and publish a live status on its heartbeat.
217
+ */
218
+ concurrency?: ConcurrencyOption;
219
+ /** Per-group concurrency override, keyed by group name. Falls back to {@link concurrency} (then 1).
220
+ * Each value may itself be `'adaptive'` / `{ mode:'adaptive', ... }`. */
221
+ concurrencyByGroup?: Record<string, ConcurrencyOption>;
222
+ }
223
+ interface DurableWorkerModuleAsyncOptions {
224
+ useFactory: (...args: never[]) => DurableWorkerModuleOptions | Promise<DurableWorkerModuleOptions>;
225
+ inject?: InjectionToken[];
226
+ }
227
+ /** The resolved {@link DurableWorkerModuleOptions} (canonical token). */
228
+ declare const DURABLE_WORKER_OPTIONS: unique symbol;
229
+ /**
230
+ * The `runRedisWorker` function the module uses to start each group's BullMQ consumer. Defaults to
231
+ * the real one from `@dudousxd/durable-worker`; tests `overrideProvider` it with a fake so no real
232
+ * Redis is needed.
233
+ */
234
+ declare const RUN_REDIS_WORKER: unique symbol;
235
+ /** The list of started {@link RunningWorker} handles (one per group), closed on shutdown. */
236
+ declare const DURABLE_WORKER_RUNNERS: unique symbol;
237
+ /** The signature of `runRedisWorker` — injected behind {@link RUN_REDIS_WORKER}. */
238
+ type RunRedisWorkerFn = (opts: RunRedisWorkerOptions) => Promise<RunningWorker>;
239
+ /**
240
+ * Discovers every provider carrying `@Workflow` metadata and registers its `run(ctx, input)` on the
241
+ * thin {@link DurableWorkerRuntime}. Mirrors the engine-side `WorkflowRegistrar`, but registers on
242
+ * the runner-core runtime instead of a `WorkflowEngine` — the runtime drives `run` with the thin
243
+ * `WorkflowContext` (which `implements WorkflowCtx`), so the body runs unchanged. NO store/engine.
244
+ */
245
+ declare class ThinWorkflowRegistrar implements OnModuleInit {
246
+ private readonly discovery;
247
+ private readonly runtime;
248
+ constructor(discovery: DiscoveryService, runtime: DurableWorkerRuntime);
249
+ onModuleInit(): void;
250
+ }
251
+ /**
252
+ * Discovers every `@Step` method and registers it as a step handler on the thin
253
+ * {@link DurableWorkerRuntime}. Mirrors the engine-side `DurableStepRegistrar`, but always registers
254
+ * on the runtime (a thin worker IS the consumer — there is no in-process-vs-queue branch).
255
+ */
256
+ declare class ThinStepRegistrar implements OnModuleInit {
257
+ private readonly discovery;
258
+ private readonly metadataScanner;
259
+ private readonly runtime;
260
+ constructor(discovery: DiscoveryService, metadataScanner: MetadataScanner, runtime: DurableWorkerRuntime);
261
+ onModuleInit(): void;
262
+ }
263
+ /**
264
+ * Starts one BullMQ consumer per configured group on bootstrap (after both registrars have run, so
265
+ * every handler is registered before any task is consumed) and closes them on shutdown. The only
266
+ * runtime side effect of a thin worker: register handlers, run the consumer. No engine, no store,
267
+ * no recovery, no timers, no dispatch.
268
+ *
269
+ * For the shutdown close to fire, enable Nest's shutdown hooks: `app.enableShutdownHooks()`.
270
+ */
271
+ declare class ThinWorkerBootstrap implements OnApplicationBootstrap, OnApplicationShutdown {
272
+ private readonly runtime;
273
+ private readonly options;
274
+ private readonly runRedisWorker;
275
+ private readonly runnersSink;
276
+ private readonly runners;
277
+ constructor(runtime: DurableWorkerRuntime, options: DurableWorkerModuleOptions, runRedisWorker: RunRedisWorkerFn, runnersSink: RunningWorker[]);
278
+ onApplicationBootstrap(): Promise<void>;
279
+ onApplicationShutdown(): Promise<void>;
280
+ }
281
+ /**
282
+ * A NestJS dynamic module that turns an app into a **PURE durable worker**: it discovers
283
+ * `@Workflow`/`@Step` providers, registers them on the thin {@link DurableWorkerRuntime}, and
284
+ * runs one BullMQ consumer per group via `runRedisWorker`. It is **control-plane-less** — it binds
285
+ * NO `WorkflowEngine`, NO store/ORM, NO dashboard, NO timer poller, NO recovery, and NO run dispatch.
286
+ * Use it for a worker pod that only executes work an engine elsewhere coordinates.
287
+ *
288
+ * Contrast with `DurableModule` (`worker: true`), which is a full control plane WITH a store: this
289
+ * module is the store-less half. A workflow body runs identically on either (conformance-tested).
290
+ */
291
+ declare class DurableWorkerModule {
292
+ static forRoot(options: DurableWorkerModuleOptions): DynamicModule;
293
+ static forRootAsync(options: DurableWorkerModuleAsyncOptions): DynamicModule;
294
+ private static build;
295
+ }
296
+
297
+ /**
298
+ * Opt-in config for an **in-app worker**: the same NestJS process runs the engine AND serves its own
299
+ * discovered `@Workflow`/`@Step` on one default `group`. The engine registers each `@Workflow`
300
+ * GROUP-SERVED (its turns are dispatched to `group` over the transport via a {@link RemoteWorkflowExecutor}
301
+ * instead of run inline), and a co-located {@link DurableWorkerRuntime} consumes `group` and replays the
302
+ * very same TS bodies. This is the uniform-dispatch "one app, both roles" shape — every turn pays a
303
+ * transport round-trip even though the worker is the same process. Requires a transport that carries
304
+ * workflow tasks (BullMQ); an in-process-only transport cannot dispatch a {@link WorkflowExecutor}.
305
+ */
306
+ interface DurableInAppWorkerOptions {
307
+ /** The default group this app dispatches its `@Workflow` turns to AND consumes as a worker. */
308
+ group: string;
309
+ /** ioredis connection (string or options) for the co-located worker consumer (`runRedisWorker`). */
310
+ connection: string | Record<string, unknown>;
311
+ /** Key prefix namespacing the durable queues. Defaults to `durable` (matches the transport). */
312
+ prefix?: string;
313
+ /** Stable id for this worker process in heartbeats/control. Defaults to a per-host/pid id. */
314
+ instanceId?: string;
315
+ /**
316
+ * How many tasks the co-located worker runs concurrently from its group's queue (BullMQ Worker
317
+ * concurrency). Defaults to 1. Raise it so a fanned-out batch (e.g. the N remote steps of a
318
+ * `gather`) runs in parallel. Per process; total parallelism is `concurrency × replicas`.
319
+ *
320
+ * Pass `'adaptive'` (or `{ mode:'adaptive', ... }`) to let the worker self-tune its concurrency
321
+ * (latency gradient + RAM brake + backpressure) and publish a live status on its heartbeat.
322
+ */
323
+ concurrency?: ConcurrencyOption;
324
+ }
325
+ /** The resolved {@link DurableInAppWorkerOptions} or `null` when the app didn't opt in. */
326
+ declare const IN_APP_WORKER_OPTIONS: unique symbol;
327
+ /**
328
+ * The group-served binding the {@link import('./workflow.registrar').WorkflowRegistrar} applies to every
329
+ * discovered `@Workflow`: `{ group, executor }` when the app opted into an in-app worker, else `null`
330
+ * (workflows register inline — the unchanged default). The executor is a {@link RemoteWorkflowExecutor}
331
+ * over the engine's own transport, so a group-served turn is dispatched to `group` and the co-located
332
+ * worker (below) advances it.
333
+ */
334
+ declare const IN_APP_WORKER_BINDING: unique symbol;
335
+ /** The co-located worker's {@link DurableWorkerRuntime} (the consumer half). */
336
+ declare const IN_APP_WORKER_RUNTIME: unique symbol;
337
+ /** `runRedisWorker`, injected so tests can substitute a fake (no real Redis). Defaults to the real one. */
338
+ declare const IN_APP_RUN_REDIS_WORKER: unique symbol;
339
+ /** Started {@link RunningWorker} handles for the in-app group(s), closed on shutdown. */
340
+ declare const IN_APP_WORKER_RUNNERS: unique symbol;
341
+ /** The group-served binding shape resolved behind {@link IN_APP_WORKER_BINDING}. */
342
+ interface InAppWorkerBinding {
343
+ group: string;
344
+ executor: WorkflowExecutor;
345
+ }
346
+ /**
347
+ * The consumer half of the in-app worker: on init it registers every discovered `@Workflow`/`@Step`
348
+ * on a {@link DurableWorkerRuntime} (the SAME bodies the engine registered group-served), and on bootstrap
349
+ * it starts one `runRedisWorker` consumer on the configured group, closing it on shutdown. A no-op when
350
+ * the app didn't opt in. Mirrors the thin {@link import('./durable-worker.module').DurableWorkerModule},
351
+ * but co-located with a full engine.
352
+ */
353
+ declare class InAppWorkerBootstrap implements OnModuleInit, OnApplicationBootstrap, OnApplicationShutdown {
354
+ private readonly discovery;
355
+ private readonly metadataScanner;
356
+ private readonly options;
357
+ private readonly runtime;
358
+ private readonly runRedisWorker;
359
+ private readonly runnersSink;
360
+ private readonly runners;
361
+ constructor(discovery: DiscoveryService, metadataScanner: MetadataScanner, options: DurableInAppWorkerOptions | null, runtime: DurableWorkerRuntime, runRedisWorker: RunRedisWorkerFn, runnersSink: RunningWorker[]);
362
+ onModuleInit(): void;
363
+ onApplicationBootstrap(): Promise<void>;
364
+ onApplicationShutdown(): Promise<void>;
365
+ }
366
+ /**
367
+ * The providers that stand up an in-app worker, added to {@link import('./durable.module').DurableModule}.
368
+ * All are inert when `inAppWorker` is unset (the binding resolves to `null`, the bootstrap no-ops), so a
369
+ * plain `DurableModule` is byte-for-byte unchanged.
370
+ */
371
+ declare function inAppWorkerProviders(): Provider[];
372
+
373
+ /**
374
+ * Retention config for the {@link RetentionPoller}. One or more {@link RetentionPolicy policies}
375
+ * (status sets must be disjoint — validated at boot), swept together on a shared interval.
376
+ *
377
+ * ```ts
378
+ * retention: {
379
+ * sweepInterval: '1m',
380
+ * batchSize: 1_000,
381
+ * policies: [
382
+ * { statuses: ['completed', 'cancelled'], maxAge: '14d', maxCount: 200 },
383
+ * { statuses: ['failed'], maxAge: '90d' }, // keep failures longer for debugging
384
+ * ],
385
+ * }
386
+ * ```
387
+ */
388
+ interface DurableRetentionOptions {
389
+ /** The retention rules, one per (disjoint) status group. */
390
+ policies: RetentionPolicy[];
391
+ /**
392
+ * How often to run the prune sweep. A number is milliseconds; a string is an `ms`-style duration
393
+ * (`'1m'`, `'5m'` — `'m'` is minutes). `0` runs it once on boot only. Defaults to 60000 (1 minute).
394
+ */
395
+ sweepInterval?: number | string;
396
+ /** Max runs hard-deleted per batch (per policy, looped until drained). Defaults to 1000. */
397
+ batchSize?: number;
398
+ }
399
+ interface DurableModuleOptions {
400
+ store: StateStore;
401
+ transport?: Transport;
402
+ /**
403
+ * An ordered pool of named transports for failover / multi-broker setups. The engine dispatches on
404
+ * the first and fails over to the next; a step pins one via `ctx.call(step, input, { transport })`.
405
+ * Use instead of `transport`.
406
+ */
407
+ transports?: NamedTransport[];
408
+ /**
409
+ * Cross-instance broadcast pub/sub (lifecycle events + cancellation). Defaults to the (first)
410
+ * transport when it can broadcast (event-emitter, BullMQ); set explicitly to use a dedicated one.
411
+ */
412
+ controlPlane?: ControlPlane;
413
+ /** Interval (ms) for the durable-timer poller. `0` disables it. Defaults to 1000. */
414
+ timerPollMs?: number;
415
+ /**
416
+ * Auto-create the durable tables on boot via `store.ensureSchema()`. Defaults to true. Turn
417
+ * off in production and call the store adapter's `ensure*DurableSchema()` from a migration.
418
+ */
419
+ autoSchema?: boolean;
420
+ /**
421
+ * Worker-pool partition for this instance (forwarded to the engine). The poll paths
422
+ * (`runPending`/`recoverIncomplete`/`resumeDueTimers`/`sweepTimeouts`) only act on runs in this
423
+ * namespace. Set distinct values to safely share ONE state store across non-interchangeable
424
+ * pools — e.g. a developer's local instance vs the deployed cluster. **Omit it to make this
425
+ * instance an OPERATOR** — an unset namespace drives/recovers/resumes runs of EVERY namespace and
426
+ * leaves the transport on its bare prefix. See `WorkflowEngineDeps.namespace`.
427
+ */
428
+ namespace?: string;
429
+ /**
430
+ * Multi-instance recovery lease, in ms — how long an instance owns a run it picked up before
431
+ * another may take over. Defaults to 30000. Set above your longest synchronous run.
432
+ */
433
+ leaseMs?: number;
434
+ /** Unique id for this instance (for leases). Defaults to a random id. */
435
+ instanceId?: string;
436
+ /**
437
+ * Cap recovery attempts before a still-`running` run is moved to the `dead` dead-letter state
438
+ * (a poison pill that crashes the process every boot). Omit for unlimited.
439
+ */
440
+ maxRecoveryAttempts?: number;
441
+ /**
442
+ * Opt-in liveness deadline (ms) for a remote (polyglot) workflow `advance`. If the worker neither
443
+ * returns a decision nor sends a run-scoped heartbeat within this window, the engine presumes it dead
444
+ * and lets recovery re-drive; each heartbeat rearms the window so a slow-but-alive worker is never
445
+ * re-driven. Pair with a worker SDK that emits run-scoped heartbeats (`@dudousxd/durable-worker` ≥ the
446
+ * release that ships them, and the Python `durable-worker`). Omit for the prior unbounded await.
447
+ */
448
+ remoteAdvanceSilenceMs?: number;
449
+ /**
450
+ * The **default** workflow to route dead-lettered runs to, for workflows that don't declare their
451
+ * own. When a run is moved to `dead` (exceeded `maxRecoveryAttempts`), the started handler gets a
452
+ * `DeadLetter` payload `{ deadRunId, workflow, input, error }` (idempotent by a `dlq:<runId>` id) —
453
+ * it can alert, compensate, or queue for review. Resolution per dead run: the workflow's inline
454
+ * `@DeadLetter()` method → its `@Workflow({ deadLetterWorkflow })` reference → this default. Omit
455
+ * everything to just leave dead runs parked (inspectable + retriable from the dashboard). Accepts a
456
+ * workflow class (refactor-safe) or a name (cross-runtime).
457
+ */
458
+ deadLetterWorkflow?: WorkflowRef;
459
+ /**
460
+ * Whether this instance plays the **worker** role: register `@Step` handlers (consume the
461
+ * transport), recover incomplete runs on boot, and poll due timers. Defaults to `true`. Set
462
+ * `false` for a **dashboard/dispatch-only** instance (e.g. an API pod) that mounts the control
463
+ * plane and reads the store but must not process or recover workflows — leave that to the workers.
464
+ */
465
+ worker?: boolean;
466
+ /**
467
+ * Internal axis, orthogonal to {@link worker}: whether this instance DRIVES runs — polls
468
+ * pending, recovers crashed (`recoverIncomplete`), resumes due timers, sweeps timeouts — even
469
+ * when it does not itself execute `@Workflow`/`@Step` bodies. Defaults to `worker !== false`
470
+ * (back-compat: `worker:true` drives, and a plain `worker:false` API/dashboard pod does not).
471
+ * `DurableControlPlaneModule` sets this `true` alongside `worker:false`: an OPERATOR control
472
+ * plane that drives every run but dispatches each one to a remote tenant worker group (see
473
+ * {@link remoteByConvention}) instead of running it in-process. Most apps never set this
474
+ * directly — use `DurableModule` (a worker) or `DurableControlPlaneModule` (a driving,
475
+ * non-executing control plane) instead.
476
+ */
477
+ drive?: boolean;
478
+ /** Max ms to wait for in-flight runs on shutdown before exiting. Defaults to 10000. */
479
+ shutdownTimeoutMs?: number;
480
+ /**
481
+ * Recurring workflows to start on a schedule (fixed interval or cron). The timer poller fires
482
+ * them each tick on **worker** instances only; `engine.start` is idempotent by the schedule's
483
+ * time-bucket run id, so racing instances start each window exactly once. Cron schedules need the
484
+ * optional `cron-parser` peer dependency.
485
+ */
486
+ schedules?: ScheduledWorkflow[];
487
+ /**
488
+ * Hard-prune terminal run history on an interval so `durable_workflow_runs` (and its child tables)
489
+ * stays bounded — without it, completed/failed/cancelled runs accumulate forever and the timer
490
+ * poller's per-tick status scans get linearly slower. Worker instances only. Omit to keep all
491
+ * history (the default). Requires a store adapter that implements `pruneTerminalRuns` (the
492
+ * MikroORM adapter does); other adapters no-op with a warning. See {@link DurableRetentionOptions}.
493
+ */
494
+ retention?: DurableRetentionOptions;
495
+ /**
496
+ * Build the public callback URL for a `ctx.webhook()` token, e.g.
497
+ * ``(t) => `https://api.example.com/durable/api/webhooks/${t}` ``. Populates `DurableWebhook.url`
498
+ * so a step can hand the URL to a third party. The dashboard's `POST webhooks/:token` receives the
499
+ * callback. Omit to build URLs yourself from the token.
500
+ */
501
+ webhookUrl?: (token: string) => string;
502
+ /**
503
+ * Flow-control queues for remote steps, registered on the engine at startup. Reference one from a
504
+ * workflow with `ctx.call(step, input, { queue: name })` to cap its concurrency / admission rate.
505
+ */
506
+ queues?: QueueConfig[];
507
+ /**
508
+ * Admission backend for the flow-control `queues`. Defaults to in-process (per-instance) caps. Pass
509
+ * a `RedisAdmissionBackend` (from `@dudousxd/nestjs-durable-admission-redis`) to make concurrency /
510
+ * rate-limit / priority ordering GLOBAL across every engine replica.
511
+ */
512
+ admission?: AdmissionBackend;
513
+ /**
514
+ * Provide the current W3C `traceparent` to stamp on dispatched remote tasks, so workers continue
515
+ * the distributed trace. Pass `otelTraceparent` from `@dudousxd/nestjs-durable-otel`.
516
+ */
517
+ traceparent?: () => string | undefined;
518
+ /**
519
+ * Provide an opaque context carrier (tenant / user / correlation ids) to stamp on dispatched remote
520
+ * tasks, so workers re-expose it to the step handler alongside the `traceparent`. The engine never
521
+ * inspects its shape.
522
+ *
523
+ * **Auto-feed**: if you omit this AND `@dudousxd/nestjs-context` is installed (its accessor is bound
524
+ * to the shared `CONTEXT_ACCESSOR` token), DurableModule defaults this to a reader that builds
525
+ * `{ traceId, tenantId, userRef }` from the accessor — so a workflow dispatched within a request
526
+ * automatically carries the originating context across process boundaries. Pass your own reader to
527
+ * override the auto-feed; with neither, the carrier is omitted (unchanged behavior).
528
+ *
529
+ * Re-evaluated at each (re)dispatch — including a retry or a crash/scale-down resume that the engine
530
+ * drives OUTSIDE the originating request scope, where this reader may return empty or stale values.
531
+ * Treat the carrier as best-effort correlation/propagation metadata only — do NOT treat it as an
532
+ * authorization boundary.
533
+ */
534
+ context?: () => Record<string, unknown> | undefined;
535
+ /** Attempts for each saga compensation when a run fails. Default 1 (no retry). Idempotent undos. */
536
+ compensationRetries?: number;
537
+ /**
538
+ * Route an **unregistered** workflow whose name matches a live worker group to that group over the
539
+ * primary transport — no per-workflow `engine.remote()` registration needed. Default `false`. Set
540
+ * `true` on a control plane that dispatches to convention-named polyglot workers (see
541
+ * `WorkflowEngineDeps.remoteByConvention`).
542
+ */
543
+ remoteByConvention?: boolean;
544
+ /**
545
+ * Opt into tenant read scoping: when `true` AND {@link namespace} is set, the module confines the
546
+ * store's reads to that namespace (a tenant-boundary view) instead of the operator view that sees
547
+ * all namespaces. Default `false` — the control plane (e.g. flip's `/ctrl` operator screens) stays
548
+ * unscoped. Requires a store that exposes the `withScope` capability (the MikroORM adapter does);
549
+ * a pre-built store without it is used as-is (construct it already-scoped instead). See
550
+ * {@link DurableModuleOptions.store}.
551
+ */
552
+ scopeReads?: boolean;
553
+ /**
554
+ * Opt into an **in-app worker** (uniform dispatch): the same process runs the engine AND serves its
555
+ * own discovered `@Workflow`/`@Step` on one default `group`. Each `@Workflow` is registered
556
+ * GROUP-SERVED — its turns are dispatched to `group` over the transport and replayed by a co-located
557
+ * worker consumer — instead of run inline. Requires a workflow-task transport (BullMQ). Omit (the
558
+ * default) to keep every `@Workflow` on the inline fast path with zero dispatch round-trips. See
559
+ * {@link DurableInAppWorkerOptions}.
560
+ */
561
+ inAppWorker?: DurableInAppWorkerOptions;
562
+ }
563
+ interface DurableModuleAsyncOptions {
564
+ useFactory: (...args: never[]) => DurableModuleOptions | Promise<DurableModuleOptions>;
565
+ inject?: InjectionToken[];
566
+ }
567
+ declare class DurableModule {
568
+ static forRoot(options: DurableModuleOptions): DynamicModule;
569
+ static forRootAsync(options: DurableModuleAsyncOptions): DynamicModule;
570
+ private static build;
571
+ }
572
+ /**
573
+ * The **control-plane** packaging of {@link DurableModule}: an intention-revealing alias that always
574
+ * forces `worker: false, drive: true`. A control-plane instance mounts the engine, the dashboard, and
575
+ * the store, and DRIVES every run — polls pending, recovers crashed, resumes due timers, sweeps
576
+ * timeouts — but never itself executes a `@Workflow`/`@Step` body: pair it with
577
+ * `remoteByConvention: true` (or per-workflow `engine.remote()`) so each driven run dispatches to a
578
+ * remote tenant worker group instead of running in-process. A started run no longer sits orphaned
579
+ * `pending` forever waiting for a worker's own poll — this instance drives it itself.
580
+ *
581
+ * Pair it with tenant workers packaged via `DurableWorkerModule` (which run `@Step`/`@Workflow`
582
+ * handlers over the transport, hold no engine/store, and can `startRun` back over the protocol). Use
583
+ * `DurableModule` directly when one instance should be BOTH dispatcher and worker.
584
+ *
585
+ * Any `worker`/`drive` passed in options is ignored — this factory hard-sets `worker: false, drive: true`.
586
+ */
587
+ declare class DurableControlPlaneModule {
588
+ static forRoot(options: DurableModuleOptions): DynamicModule;
589
+ static forRootAsync(options: DurableModuleAsyncOptions): DynamicModule;
590
+ }
591
+
592
+ declare const ENTITY_METADATA: unique symbol;
593
+ declare const ENTITY_ON_METADATA: unique symbol;
594
+ /**
595
+ * Marks an `@Injectable()` class as a **durable entity** (a virtual object): its `@On(op)` methods run
596
+ * serialized per key over the instance's fields as durable state. e.g.
597
+ *
598
+ * ```ts
599
+ * @Entity({ name: 'cart' }) @Injectable()
600
+ * class Cart { items: Item[] = []; @On('add') add(i: Item) { this.items.push(i); } @On('list') list() { return this.items; } }
601
+ * ```
602
+ *
603
+ * The class must be **constructible with no arguments** (a fresh instance is the initial state per key)
604
+ * — keep entities pure state, no DI. Drive them with `EntityService` or `ctx.signalEntity`/`callEntity`.
605
+ */
606
+ declare function Entity(options: {
607
+ name: string;
608
+ }): ClassDecorator;
609
+ /** Marks an entity method as the handler for operation `op`. */
610
+ declare function On(op: string): MethodDecorator;
611
+ declare function getEntityMeta(target: Function): {
612
+ name: string;
613
+ } | undefined;
614
+ /**
615
+ * Build the engine `EntityConfig` for a discovered `@Entity` class: a fresh instance per key, and
616
+ * handlers that rehydrate the class prototype onto the (serialized) state before dispatching the op,
617
+ * so methods work after replay.
618
+ */
619
+ declare function entityConfigFor(ctor: Function): {
620
+ initialState: () => object;
621
+ handlers: Record<string, EntityHandler>;
622
+ };
623
+ /** Inject this to drive durable entities from outside a workflow. */
624
+ declare class EntityService {
625
+ private readonly engine;
626
+ constructor(engine: WorkflowEngine);
627
+ /** Send an operation to an entity (fire-and-forget; ordered + exactly-once per key). */
628
+ signal(name: string, key: string, op: string, arg?: unknown): Promise<void>;
629
+ /** Read an entity's current durable state (or undefined if it has none yet). */
630
+ getState<S = unknown>(name: string, key: string): Promise<S | undefined>;
631
+ }
632
+
633
+ declare const STEP_INTERCEPTOR_METADATA: unique symbol;
634
+ /**
635
+ * The shape a `@StepInterceptor()` provider must implement: `intercept(invocation, next)` wraps the
636
+ * real execution of every local `ctx.step` (call `next()` to run the step body / next interceptor,
637
+ * and return — or transform — its result). The engine-level {@link StepInterceptor} primitive, with
638
+ * NestJS dependency injection.
639
+ */
640
+ interface DurableStepInterceptor {
641
+ intercept(invocation: StepInvocation, next: () => Promise<unknown>): Promise<unknown>;
642
+ }
643
+ /**
644
+ * Marks an `@Injectable()` class as a durable step interceptor. The module discovers it on boot and
645
+ * registers its `intercept` method with the engine (so it can inject loggers/tracers/etc.). First
646
+ * declared is outermost. Interceptors fire only when a step actually executes, never on replay.
647
+ */
648
+ declare function StepInterceptor(): ClassDecorator;
649
+ declare function isStepInterceptor(target: Function): boolean;
650
+
651
+ /**
652
+ * Cross-lib injection token for the current-request context accessor, owned by
653
+ * `@dudousxd/nestjs-context`. We do NOT import nestjs-context (it is an OPTIONAL
654
+ * peer dependency) — instead we share its well-known token by value so DI
655
+ * resolves the same provider when nestjs-context is installed and present.
656
+ *
657
+ * `Symbol.for(key)` uses the global symbol registry, so this resolves to the
658
+ * SAME symbol instance as nestjs-context's `tokens.ts` (and the identical token
659
+ * declared by `@dudousxd/nestjs-authz`) without any import. The key MUST stay
660
+ * byte-identical with nestjs-context's export.
661
+ */
662
+ declare const CONTEXT_ACCESSOR: unique symbol;
663
+
664
+ /** Public entry point for starting and resuming workflow runs. */
665
+ declare class WorkflowService {
666
+ private readonly engine;
667
+ constructor(engine: WorkflowEngine);
668
+ /**
669
+ * Enqueue a workflow run — it creates the run (`pending`) and returns `{ runId, status: 'pending' }`
670
+ * immediately; a worker executes the body (so the caller never blocks on workflow logic). Use
671
+ * {@link waitForRun} when you need the outcome. Pass the workflow's **class**
672
+ * (`start(CheckoutWorkflow, input)`) for a typed input + refactor-safety, or a **name** string for a
673
+ * cross-runtime workflow. `runId` defaults to a random id; pass your own to make the start idempotent
674
+ * (a redelivery returns the existing run). `opts.tags` are merged with the workflow's static
675
+ * `@Workflow({ tags })`; `opts.searchAttributes` stamp typed, queryable run data.
676
+ */
677
+ start<C extends WorkflowClass>(workflow: C, input: WorkflowInputOf<C>, runId?: string, opts?: StartOptions): Promise<RunResult>;
678
+ start(workflow: string, input: unknown, runId?: string, opts?: StartOptions): Promise<RunResult>;
679
+ resume(runId: string): Promise<RunResult>;
680
+ /**
681
+ * Resolve once a run settles — terminal (completed/failed/cancelled/dead) or suspended. `start`
682
+ * only enqueues (a worker runs the body), so pair them when a request needs the outcome:
683
+ * `const { runId } = await svc.start(...); const result = await svc.waitForRun(runId)`.
684
+ */
685
+ waitForRun(runId: string, opts?: {
686
+ timeoutMs?: number;
687
+ }): Promise<RunResult>;
688
+ /** Deliver an external signal (e.g. from a webhook) to the run waiting on `token`. */
689
+ signal(token: string, payload: unknown): Promise<RunResult | null>;
690
+ /**
691
+ * Ensure a run exists for `runId`, then deliver a signal to it — race-free (the signal is buffered
692
+ * until the run reaches its `waitForSignal`). The durable-entity / accumulator pattern: one
693
+ * long-lived run per key fed events by many calls. See {@link WorkflowEngine.signalWithStart}.
694
+ */
695
+ signalWithStart<C extends WorkflowClass>(workflow: C, input: WorkflowInputOf<C>, runId: string, signal: {
696
+ token: string;
697
+ payload?: unknown;
698
+ }, opts?: StartOptions): Promise<{
699
+ runId: string;
700
+ }>;
701
+ signalWithStart(workflow: string, input: unknown, runId: string, signal: {
702
+ token: string;
703
+ payload?: unknown;
704
+ }, opts?: StartOptions): Promise<{
705
+ runId: string;
706
+ }>;
707
+ /**
708
+ * Publish a named event. Resumes runs waiting on it via `ctx.waitForEvent(name, { match })` and
709
+ * starts a fresh run of every workflow subscribed via `@Workflow({ onEvent })` / `@OnEvent` (the
710
+ * payload becomes its input). Pass `opts.id` to dedupe redeliveries. Returns how many runs it
711
+ * touched (resumed + started).
712
+ */
713
+ publishEvent(name: string, payload: unknown, opts?: {
714
+ id?: string;
715
+ }): Promise<number>;
716
+ }
717
+
718
+ export { CONTEXT_ACCESSOR, type ContextAccessor, type ContextStore, DEAD_LETTER_METADATA, DURABLE_STEP_METADATA, DURABLE_WORKER_OPTIONS, DURABLE_WORKER_RUNNERS, DeadLetter, DurableControlPlaneModule, type DurableInAppWorkerOptions, DurableModule, type DurableModuleAsyncOptions, type DurableModuleOptions, type DurableRetentionOptions, DurableStep, type DurableStepInterceptor, type DurableStepMeta, DurableWorkerModule, type DurableWorkerModuleAsyncOptions, type DurableWorkerModuleOptions, ENTITY_METADATA, ENTITY_ON_METADATA, Entity, EntityService, IN_APP_RUN_REDIS_WORKER, IN_APP_WORKER_BINDING, IN_APP_WORKER_OPTIONS, IN_APP_WORKER_RUNNERS, IN_APP_WORKER_RUNTIME, type InAppWorkerBinding, InAppWorkerBootstrap, ON_EVENT_METADATA, On, OnEvent, RUN_REDIS_WORKER, type RunRedisWorkerFn, STEP_INTERCEPTOR_METADATA, Step, StepInterceptor, ThinStepRegistrar, ThinWorkerBootstrap, ThinWorkflowRegistrar, type UserRef, WORKFLOW_METADATA, Workflow, type WorkflowMeta, type WorkflowOptions, WorkflowService, entityConfigFor, getDurableStepMeta, getEntityMeta, getOnEvents, getWorkflowMeta, inAppWorkerProviders, isDeadLetterHandler, isStepInterceptor };