@dudousxd/nestjs-durable 0.26.0 → 0.28.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -1,5 +1,6 @@
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';
1
+ import { StepError, WorkflowRef, SingletonConfig, StateStore, Transport, NamedTransport, ControlPlane, ScheduledWorkflow, RetentionPolicy, QueueConfig, AdmissionBackend, WorkflowClass, WorkflowInputOf, StartOptions, RunResult, RunGateway, WorkflowEngine, EntityHandler, RunDetail, RunQuery, WorkflowRun, EngineEvent, RunRequest, RunReply, StepInvocation, TenantEvent } from '@dudousxd/nestjs-durable-core';
2
+ import { z } from 'zod';
3
+ import { ConcurrencyOption, StartRunDeps, RunRedisWorkerOptions, RunningWorker, DurableWorkerRuntime } from '@dudousxd/durable-worker';
3
4
  import { DynamicModule, InjectionToken, OnModuleInit, OnApplicationBootstrap, OnApplicationShutdown, Provider } from '@nestjs/common';
4
5
  import { DiscoveryService, MetadataScanner } from '@nestjs/core';
5
6
 
@@ -105,7 +106,7 @@ interface WorkflowOptions {
105
106
  * Start a fresh run of this workflow whenever any of these events is published via
106
107
  * `publishEvent(name, payload)` — the payload becomes the run's input. e.g.
107
108
  * `onEvent: ['user.registered', 'user.invited']`. The same subscription can also be declared with
108
- * the `@OnEvent(...)` class decorator; the two are merged.
109
+ * the `@OnDurableEvent(...)` class decorator; the two are merged.
109
110
  */
110
111
  onEvent?: string[];
111
112
  /**
@@ -131,14 +132,71 @@ declare function Workflow(options: WorkflowOptions): ClassDecorator;
131
132
  declare function getWorkflowMeta(target: Function): WorkflowMeta | undefined;
132
133
  declare const DURABLE_STEP_METADATA: unique symbol;
133
134
  interface DurableStepMeta {
135
+ /** The resolved routing name — derived (`Class.method`) or explicit, always present. */
134
136
  name: string;
137
+ /** Opt-in runtime input schema, validated when the handler is served (see `scanSteps`). Absent on
138
+ * a bare `@Step()` — compile-time types from the method signature only, no runtime check. */
139
+ input?: z.ZodType | undefined;
140
+ /** Opt-in runtime output schema, validated before the handler's result is handed back. */
141
+ output?: z.ZodType | undefined;
135
142
  }
136
143
  /**
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.
144
+ * `@Step({ name?, input?, output?, retries?, backoff?, backoffMs?, backoffMaxMs?, jitter?,
145
+ * timeoutMs? })` — the object call form. See {@link Step}.
140
146
  */
141
- declare function Step(name: string): MethodDecorator;
147
+ interface StepOptions {
148
+ /** Explicit routing name, overriding the derived `Class.method`. */
149
+ name?: string;
150
+ /** Runtime input schema, validated at the serve boundary (opt-in — a bare `@Step()` skips it). */
151
+ input?: z.ZodType;
152
+ /** Runtime output schema, validated before the result is returned (opt-in). */
153
+ output?: z.ZodType;
154
+ /**
155
+ * Def-level durable-dispatch policy stamped under `DURABLE_STEP_CONFIG` and read by `ctx.step` at
156
+ * the dispatch boundary (see `stepConfigOf`) — a per-call `ctx.step(ref, input, opts)` overrides
157
+ * these field-by-field. Max attempts before the step (and run) fails.
158
+ */
159
+ retries?: number;
160
+ /** How the delay between retries grows: `fixed` (constant) or `exp` (doubles each attempt). */
161
+ backoff?: 'fixed' | 'exp';
162
+ /** Base delay in ms between retries. Omit (or 0) to retry with no delay. */
163
+ backoffMs?: number;
164
+ /** Upper bound on the (exponential) backoff delay. */
165
+ backoffMaxMs?: number;
166
+ /** Add random jitter (50–100% of the computed delay) to avoid thundering-herd retries. */
167
+ jitter?: boolean;
168
+ /**
169
+ * Liveness window for a dispatched step: no result/heartbeat within this many ms presumes the
170
+ * worker dead and fails the dispatch with a `RemoteStepTimeout` (retryable — re-dispatches per
171
+ * `retries`). Omit to wait indefinitely.
172
+ */
173
+ timeoutMs?: number;
174
+ }
175
+ /**
176
+ * Marks a provider method as a durable step handler. An in-process transport (e.g. the
177
+ * event-emitter transport) or a co-located/thin worker runtime routes a dispatched task to it BY
178
+ * NAME — `ctx.step(this.svc.method, input)` reads that same name off the method reference (stamped
179
+ * via the shared, cross-package `DURABLE_STEP_NAME` symbol from `@dudousxd/nestjs-durable-core`), so
180
+ * there's no separately-declared def linking a call site to this handler. The method's single
181
+ * argument is the step input (plus an optional `StepLogger` second arg); its return value is the
182
+ * step output.
183
+ *
184
+ * Three call forms:
185
+ * - `@Step()` — bare: the routing name is DERIVED from the method as `` `${ClassName}.${method}` ``
186
+ * (e.g. `ExtractionService.runExtractionPage`) — refactor-safe, no magic string.
187
+ * - `@Step('custom:name')` — explicit name override (stable across refactors, or a cross-runtime
188
+ * contract with a non-JS worker that has no `@Step` of its own).
189
+ * - `@Step({ name?, input?, output?, retries?, backoff?, backoffMs?, backoffMaxMs?, jitter?,
190
+ * timeoutMs? })` — optional name override, opt-in RUNTIME zod schemas, and a def-level
191
+ * durable-retry/liveness-timeout policy. `input` validates before the method runs; `output`
192
+ * validates its return value before it's handed back (see `scanSteps`). `retries`/`backoff`/
193
+ * `backoffMs`/`backoffMaxMs`/`jitter`/`timeoutMs` are the policy `ctx.step` reads off this method
194
+ * reference (via `stepConfigOf`) to build the dispatched `StepDef`'s durable retry/backoff and
195
+ * remote-liveness timeout — a per-call `ctx.step(ref, input, opts)` overrides them field-by-field.
196
+ * A bare `@Step()` carries none of this and skips validation/retry/timeout — compile-time types
197
+ * from the method signature are the only check, and the step dispatches with no retry/timeout.
198
+ */
199
+ declare function Step(nameOrOptions?: string | StepOptions): MethodDecorator;
142
200
  /**
143
201
  * @deprecated Use `@Step` instead. `@DurableStep` is a back-compat alias of {@link Step} and
144
202
  * writes the same `DURABLE_STEP_METADATA`, so discovery/registrars treat them identically.
@@ -176,190 +234,17 @@ declare const ON_EVENT_METADATA: unique symbol;
176
234
  /**
177
235
  * Subscribe a `@Workflow` class to one or more events: when any of them is published via
178
236
  * `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
- /** Key prefix namespacing the durable queues. Defaults to `durable` (matches the transport). */
198
- prefix?: string;
199
- /** Stable id for this worker process in heartbeats/control. Defaults to a per-host/pid id. */
200
- instanceId?: string;
201
- /**
202
- * How many tasks each group's consumer runs concurrently (BullMQ Worker concurrency). Defaults to 1.
203
- * Applies to every group unless overridden per-group by {@link concurrencyByGroup}. Raise it so a
204
- * fanned-out batch (e.g. the N remote steps of a `gather`) runs in parallel instead of serially.
205
- *
206
- * Pass `'adaptive'` (or `{ mode:'adaptive', ... }`) to let each consumer self-tune its concurrency
207
- * (latency gradient + RAM brake + backpressure) and publish a live status on its heartbeat.
208
- */
209
- concurrency?: ConcurrencyOption;
210
- /** Per-group concurrency override, keyed by group name. Falls back to {@link concurrency} (then 1).
211
- * Each value may itself be `'adaptive'` / `{ mode:'adaptive', ... }`. */
212
- concurrencyByGroup?: Record<string, ConcurrencyOption>;
213
- }
214
- interface DurableWorkerModuleAsyncOptions {
215
- useFactory: (...args: never[]) => DurableWorkerModuleOptions | Promise<DurableWorkerModuleOptions>;
216
- inject?: InjectionToken[];
217
- }
218
- /** The resolved {@link DurableWorkerModuleOptions} (canonical token). */
219
- declare const DURABLE_WORKER_OPTIONS: unique symbol;
220
- /**
221
- * The `runRedisWorker` function the module uses to start each group's BullMQ consumer. Defaults to
222
- * the real one from `@dudousxd/durable-worker`; tests `overrideProvider` it with a fake so no real
223
- * Redis is needed.
224
- */
225
- declare const RUN_REDIS_WORKER: unique symbol;
226
- /** The list of started {@link RunningWorker} handles (one per group), closed on shutdown. */
227
- declare const DURABLE_WORKER_RUNNERS: unique symbol;
228
- /** The signature of `runRedisWorker` — injected behind {@link RUN_REDIS_WORKER}. */
229
- type RunRedisWorkerFn = (opts: RunRedisWorkerOptions) => Promise<RunningWorker>;
230
- /**
231
- * Discovers every provider carrying `@Workflow` metadata and registers its `run(ctx, input)` on the
232
- * thin {@link DurableWorkerRuntime}. Mirrors the engine-side `WorkflowRegistrar`, but registers on
233
- * the runner-core runtime instead of a `WorkflowEngine` — the runtime drives `run` with the thin
234
- * `WorkflowContext` (which `implements WorkflowCtx`), so the body runs unchanged. NO store/engine.
235
- */
236
- declare class ThinWorkflowRegistrar implements OnModuleInit {
237
- private readonly discovery;
238
- private readonly runtime;
239
- constructor(discovery: DiscoveryService, runtime: DurableWorkerRuntime);
240
- onModuleInit(): void;
241
- }
242
- /**
243
- * Discovers every `@Step` method and registers it as a step handler on the thin
244
- * {@link DurableWorkerRuntime}. Mirrors the engine-side `DurableStepRegistrar`, but always registers
245
- * on the runtime (a thin worker IS the consumer — there is no in-process-vs-queue branch).
246
- */
247
- declare class ThinStepRegistrar implements OnModuleInit {
248
- private readonly discovery;
249
- private readonly metadataScanner;
250
- private readonly runtime;
251
- constructor(discovery: DiscoveryService, metadataScanner: MetadataScanner, runtime: DurableWorkerRuntime);
252
- onModuleInit(): void;
253
- }
254
- /**
255
- * Starts one BullMQ consumer per configured group on bootstrap (after both registrars have run, so
256
- * every handler is registered before any task is consumed) and closes them on shutdown. The only
257
- * runtime side effect of a thin worker: register handlers, run the consumer. No engine, no store,
258
- * no recovery, no timers, no dispatch.
259
- *
260
- * For the shutdown close to fire, enable Nest's shutdown hooks: `app.enableShutdownHooks()`.
261
- */
262
- declare class ThinWorkerBootstrap implements OnApplicationBootstrap, OnApplicationShutdown {
263
- private readonly runtime;
264
- private readonly options;
265
- private readonly runRedisWorker;
266
- private readonly runnersSink;
267
- private readonly runners;
268
- constructor(runtime: DurableWorkerRuntime, options: DurableWorkerModuleOptions, runRedisWorker: RunRedisWorkerFn, runnersSink: RunningWorker[]);
269
- onApplicationBootstrap(): Promise<void>;
270
- onApplicationShutdown(): Promise<void>;
271
- }
272
- /**
273
- * A NestJS dynamic module that turns an app into a **PURE durable worker**: it discovers
274
- * `@Workflow`/`@Step` providers, registers them on the thin {@link DurableWorkerRuntime}, and
275
- * runs one BullMQ consumer per group via `runRedisWorker`. It is **control-plane-less** — it binds
276
- * NO `WorkflowEngine`, NO store/ORM, NO dashboard, NO timer poller, NO recovery, and NO run dispatch.
277
- * Use it for a worker pod that only executes work an engine elsewhere coordinates.
237
+ * Equivalent to `@Workflow({ onEvent })` — use whichever reads better; multiple
238
+ * `@OnDurableEvent(...)` decorators and the option are all merged.
278
239
  *
279
- * Contrast with `DurableModule` (`worker: true`), which is a full control plane WITH a store: this
280
- * module is the store-less half. A workflow body runs identically on either (conformance-tested).
240
+ * Named "durable" on purpose: `@nestjs/event-emitter` exports an `@OnEvent` decorator, and in an
241
+ * app using both libs an auto-import picking the wrong one fails silently in either direction.
281
242
  */
282
- declare class DurableWorkerModule {
283
- static forRoot(options: DurableWorkerModuleOptions): DynamicModule;
284
- static forRootAsync(options: DurableWorkerModuleAsyncOptions): DynamicModule;
285
- private static build;
286
- }
287
-
288
- /**
289
- * Opt-in config for an **in-app worker**: the same NestJS process runs the engine AND serves its own
290
- * discovered `@Workflow`/`@Step` on one default `group`. The engine registers each `@Workflow`
291
- * GROUP-SERVED (its turns are dispatched to `group` over the transport via a {@link RemoteWorkflowExecutor}
292
- * instead of run inline), and a co-located {@link DurableWorkerRuntime} consumes `group` and replays the
293
- * very same TS bodies. This is the uniform-dispatch "one app, both roles" shape — every turn pays a
294
- * transport round-trip even though the worker is the same process. Requires a transport that carries
295
- * workflow tasks (BullMQ); an in-process-only transport cannot dispatch a {@link WorkflowExecutor}.
296
- */
297
- interface DurableInAppWorkerOptions {
298
- /** The default group this app dispatches its `@Workflow` turns to AND consumes as a worker. */
299
- group: string;
300
- /** ioredis connection (string or options) for the co-located worker consumer (`runRedisWorker`). */
301
- connection: string | Record<string, unknown>;
302
- /** Key prefix namespacing the durable queues. Defaults to `durable` (matches the transport). */
303
- prefix?: string;
304
- /** Stable id for this worker process in heartbeats/control. Defaults to a per-host/pid id. */
305
- instanceId?: string;
306
- /**
307
- * How many tasks the co-located worker runs concurrently from its group's queue (BullMQ Worker
308
- * concurrency). Defaults to 1. Raise it so a fanned-out batch (e.g. the N remote steps of a
309
- * `gather`) runs in parallel. Per process; total parallelism is `concurrency × replicas`.
310
- *
311
- * Pass `'adaptive'` (or `{ mode:'adaptive', ... }`) to let the worker self-tune its concurrency
312
- * (latency gradient + RAM brake + backpressure) and publish a live status on its heartbeat.
313
- */
314
- concurrency?: ConcurrencyOption;
315
- }
316
- /** The resolved {@link DurableInAppWorkerOptions} or `null` when the app didn't opt in. */
317
- declare const IN_APP_WORKER_OPTIONS: unique symbol;
318
- /**
319
- * The group-served binding the {@link import('./workflow.registrar').WorkflowRegistrar} applies to every
320
- * discovered `@Workflow`: `{ group, executor }` when the app opted into an in-app worker, else `null`
321
- * (workflows register inline — the unchanged default). The executor is a {@link RemoteWorkflowExecutor}
322
- * over the engine's own transport, so a group-served turn is dispatched to `group` and the co-located
323
- * worker (below) advances it.
324
- */
325
- declare const IN_APP_WORKER_BINDING: unique symbol;
326
- /** The co-located worker's {@link DurableWorkerRuntime} (the consumer half). */
327
- declare const IN_APP_WORKER_RUNTIME: unique symbol;
328
- /** `runRedisWorker`, injected so tests can substitute a fake (no real Redis). Defaults to the real one. */
329
- declare const IN_APP_RUN_REDIS_WORKER: unique symbol;
330
- /** Started {@link RunningWorker} handles for the in-app group(s), closed on shutdown. */
331
- declare const IN_APP_WORKER_RUNNERS: unique symbol;
332
- /** The group-served binding shape resolved behind {@link IN_APP_WORKER_BINDING}. */
333
- interface InAppWorkerBinding {
334
- group: string;
335
- executor: WorkflowExecutor;
336
- }
337
- /**
338
- * The consumer half of the in-app worker: on init it registers every discovered `@Workflow`/`@Step`
339
- * on a {@link DurableWorkerRuntime} (the SAME bodies the engine registered group-served), and on bootstrap
340
- * it starts one `runRedisWorker` consumer on the configured group, closing it on shutdown. A no-op when
341
- * the app didn't opt in. Mirrors the thin {@link import('./durable-worker.module').DurableWorkerModule},
342
- * but co-located with a full engine.
343
- */
344
- declare class InAppWorkerBootstrap implements OnModuleInit, OnApplicationBootstrap, OnApplicationShutdown {
345
- private readonly discovery;
346
- private readonly metadataScanner;
347
- private readonly options;
348
- private readonly runtime;
349
- private readonly runRedisWorker;
350
- private readonly runnersSink;
351
- private readonly runners;
352
- constructor(discovery: DiscoveryService, metadataScanner: MetadataScanner, options: DurableInAppWorkerOptions | null, runtime: DurableWorkerRuntime, runRedisWorker: RunRedisWorkerFn, runnersSink: RunningWorker[]);
353
- onModuleInit(): void;
354
- onApplicationBootstrap(): Promise<void>;
355
- onApplicationShutdown(): Promise<void>;
356
- }
357
- /**
358
- * The providers that stand up an in-app worker, added to {@link import('./durable.module').DurableModule}.
359
- * All are inert when `inAppWorker` is unset (the binding resolves to `null`, the bootstrap no-ops), so a
360
- * plain `DurableModule` is byte-for-byte unchanged.
361
- */
362
- declare function inAppWorkerProviders(): Provider[];
243
+ declare function OnDurableEvent(...events: string[]): ClassDecorator;
244
+ /** @deprecated Renamed to `OnDurableEvent` — this alias clashes with `@nestjs/event-emitter`'s `@OnEvent` and will be removed in the next minor. */
245
+ declare const OnEvent: typeof OnDurableEvent;
246
+ /** All events a workflow class subscribes to — the union of `@Workflow({ onEvent })` and `@OnDurableEvent`. */
247
+ declare function getOnEvents(meta: WorkflowMeta, target: object): string[];
363
248
 
364
249
  /**
365
250
  * Retention config for the {@link RetentionPoller}. One or more {@link RetentionPolicy policies}
@@ -387,12 +272,32 @@ interface DurableRetentionOptions {
387
272
  /** Max runs hard-deleted per batch (per policy, looped until drained). Defaults to 1000. */
388
273
  batchSize?: number;
389
274
  }
275
+ /**
276
+ * Options for `DurableModule.forRoot`/`forRootAsync`. The **role is inferred** from which of `store`/
277
+ * `connection` are set:
278
+ *
279
+ * - `{ store, transport }` — **operator**: a real `WorkflowEngine` + `StoreRunGateway` + drivers/
280
+ * timer/retention/registrars, executing registered bodies INLINE. Driven by {@link drive} (default
281
+ * `true`).
282
+ * - `{ connection }` (no `store`) — **thin worker**: `WorkflowEngine` resolves to a store-less
283
+ * `DurableStartClient`; `RUN_GATEWAY` is a `ProxyRunGateway` when `transport` is also given, else
284
+ * every method rejects with a clear error. No store/timer/retention/entity — just discovered
285
+ * `@Workflow`/`@Step` handlers served over ONE `runRedisWorker` consumer.
286
+ * - `{ store, transport, connection }` — an **operator that also runs a co-located worker**: every
287
+ * `@Workflow` is registered GROUP-SERVED (dispatched over the transport, PER WORKFLOW NAME, instead
288
+ * of inline) and a co-located consumer (one `runRedisWorker` call) replays the same bodies.
289
+ *
290
+ * `forRoot` throws when neither `store` nor `connection` is set, and when `store` is set without a
291
+ * `transport`.
292
+ */
390
293
  interface DurableModuleOptions {
391
- store: StateStore;
294
+ /** State store — set this to play the **operator** role (see the interface doc for the full role
295
+ * matrix). Omit for a store-less **thin worker** (`connection` only). */
296
+ store?: StateStore;
392
297
  transport?: Transport;
393
298
  /**
394
299
  * An ordered pool of named transports for failover / multi-broker setups. The engine dispatches on
395
- * the first and fails over to the next; a step pins one via `ctx.call(step, input, { transport })`.
300
+ * the first and fails over to the next; a step pins one via `ctx.step(handler, input, { transport })`.
396
301
  * Use instead of `transport`.
397
302
  */
398
303
  transports?: NamedTransport[];
@@ -401,29 +306,34 @@ interface DurableModuleOptions {
401
306
  * transport when it can broadcast (event-emitter, BullMQ); set explicitly to use a dedicated one.
402
307
  */
403
308
  controlPlane?: ControlPlane;
404
- /** Interval (ms) for the durable-timer poller. `0` disables it. Defaults to 1000. */
309
+ /** Interval (ms) for the durable-timer poller. `0` disables it. Defaults to 1000. Operator only. */
405
310
  timerPollMs?: number;
406
311
  /**
407
312
  * Auto-create the durable tables on boot via `store.ensureSchema()`. Defaults to true. Turn
408
313
  * off in production and call the store adapter's `ensure*DurableSchema()` from a migration.
314
+ * Operator only.
409
315
  */
410
316
  autoSchema?: boolean;
411
317
  /**
412
- * Worker-pool partition for this instance (forwarded to the engine). Default `'default'`. Set a
413
- * distinct value to share ONE state store across non-interchangeable pools — e.g. a developer's
414
- * local instance vs the deployed cluster. See `WorkflowEngineDeps.namespace`.
318
+ * Worker-pool namespace for this instance (forwarded to the engine). The poll paths
319
+ * (`runPending`/`recoverIncomplete`/`resumeDueTimers`/`sweepTimeouts`) only act on runs in this
320
+ * namespace. Set distinct values to safely share ONE state store across non-interchangeable
321
+ * pools — e.g. a developer's local instance vs the deployed cluster. **Omit it to make this
322
+ * instance an OPERATOR** — an unset namespace drives/recovers/resumes runs of EVERY namespace and
323
+ * leaves the transport on its bare prefix. See `WorkflowEngineDeps.namespace`. Not to be confused
324
+ * with {@link partition} (the co-located/thin-worker QUEUE routing suffix).
415
325
  */
416
326
  namespace?: string;
417
327
  /**
418
328
  * Multi-instance recovery lease, in ms — how long an instance owns a run it picked up before
419
- * another may take over. Defaults to 30000. Set above your longest synchronous run.
329
+ * another may take over. Defaults to 30000. Set above your longest synchronous run. Operator only.
420
330
  */
421
331
  leaseMs?: number;
422
- /** Unique id for this instance (for leases). Defaults to a random id. */
332
+ /** Unique id for this instance (for leases, and the co-located/thin-worker consumer's heartbeats). */
423
333
  instanceId?: string;
424
334
  /**
425
335
  * Cap recovery attempts before a still-`running` run is moved to the `dead` dead-letter state
426
- * (a poison pill that crashes the process every boot). Omit for unlimited.
336
+ * (a poison pill that crashes the process every boot). Omit for unlimited. Operator only.
427
337
  */
428
338
  maxRecoveryAttempts?: number;
429
339
  /**
@@ -432,6 +342,7 @@ interface DurableModuleOptions {
432
342
  * and lets recovery re-drive; each heartbeat rearms the window so a slow-but-alive worker is never
433
343
  * re-driven. Pair with a worker SDK that emits run-scoped heartbeats (`@dudousxd/durable-worker` ≥ the
434
344
  * release that ships them, and the Python `durable-worker`). Omit for the prior unbounded await.
345
+ * Operator only.
435
346
  */
436
347
  remoteAdvanceSilenceMs?: number;
437
348
  /**
@@ -441,54 +352,59 @@ interface DurableModuleOptions {
441
352
  * it can alert, compensate, or queue for review. Resolution per dead run: the workflow's inline
442
353
  * `@DeadLetter()` method → its `@Workflow({ deadLetterWorkflow })` reference → this default. Omit
443
354
  * everything to just leave dead runs parked (inspectable + retriable from the dashboard). Accepts a
444
- * workflow class (refactor-safe) or a name (cross-runtime).
355
+ * workflow class (refactor-safe) or a name (cross-runtime). Operator only.
445
356
  */
446
357
  deadLetterWorkflow?: WorkflowRef;
447
358
  /**
448
- * Whether this instance plays the **worker** role: register `@Step` handlers (consume the
449
- * transport), recover incomplete runs on boot, and poll due timers. Defaults to `true`. Set
450
- * `false` for a **dashboard/dispatch-only** instance (e.g. an API pod) that mounts the control
451
- * plane and reads the store but must not process or recover workflows — leave that to the workers.
359
+ * Whether an **operator** instance actively DRIVES runs — polls pending, recovers crashed
360
+ * (`recoverIncomplete`), resumes due timers, sweeps timeouts, prunes retention, and consumes local
361
+ * steps — as opposed to a read-only/dashboard replica. Defaults to `true`. Set `false` for a
362
+ * **dashboard/dispatch-only** instance (e.g. an API pod) that mounts the store/dashboard but must
363
+ * not process or recover workflows — leave that to another driving instance. `drive: false` also
364
+ * installs the engine's no-op run dispatcher, so a freshly `start()`ed run stays enqueue-only until
365
+ * a driving instance's poll picks it up. Ignored (irrelevant) for a thin worker (no `store`).
452
366
  */
453
- worker?: boolean;
454
- /** Max ms to wait for in-flight runs on shutdown before exiting. Defaults to 10000. */
367
+ drive?: boolean;
368
+ /** Max ms to wait for in-flight runs on shutdown before exiting. Defaults to 10000. Operator only. */
455
369
  shutdownTimeoutMs?: number;
456
370
  /**
457
371
  * Recurring workflows to start on a schedule (fixed interval or cron). The timer poller fires
458
- * them each tick on **worker** instances only; `engine.start` is idempotent by the schedule's
372
+ * them each tick on **driving** instances only; `engine.start` is idempotent by the schedule's
459
373
  * time-bucket run id, so racing instances start each window exactly once. Cron schedules need the
460
- * optional `cron-parser` peer dependency.
374
+ * optional `cron-parser` peer dependency. Operator only.
461
375
  */
462
376
  schedules?: ScheduledWorkflow[];
463
377
  /**
464
378
  * Hard-prune terminal run history on an interval so `durable_workflow_runs` (and its child tables)
465
379
  * stays bounded — without it, completed/failed/cancelled runs accumulate forever and the timer
466
- * poller's per-tick status scans get linearly slower. Worker instances only. Omit to keep all
380
+ * poller's per-tick status scans get linearly slower. Driving instances only. Omit to keep all
467
381
  * history (the default). Requires a store adapter that implements `pruneTerminalRuns` (the
468
382
  * MikroORM adapter does); other adapters no-op with a warning. See {@link DurableRetentionOptions}.
383
+ * Operator only.
469
384
  */
470
385
  retention?: DurableRetentionOptions;
471
386
  /**
472
387
  * Build the public callback URL for a `ctx.webhook()` token, e.g.
473
388
  * ``(t) => `https://api.example.com/durable/api/webhooks/${t}` ``. Populates `DurableWebhook.url`
474
389
  * so a step can hand the URL to a third party. The dashboard's `POST webhooks/:token` receives the
475
- * callback. Omit to build URLs yourself from the token.
390
+ * callback. Omit to build URLs yourself from the token. Operator only.
476
391
  */
477
392
  webhookUrl?: (token: string) => string;
478
393
  /**
479
394
  * Flow-control queues for remote steps, registered on the engine at startup. Reference one from a
480
- * workflow with `ctx.call(step, input, { queue: name })` to cap its concurrency / admission rate.
395
+ * workflow with `ctx.step(handler, input, { queue: name })` to cap its concurrency / admission rate.
396
+ * Operator only.
481
397
  */
482
398
  queues?: QueueConfig[];
483
399
  /**
484
400
  * Admission backend for the flow-control `queues`. Defaults to in-process (per-instance) caps. Pass
485
401
  * a `RedisAdmissionBackend` (from `@dudousxd/nestjs-durable-admission-redis`) to make concurrency /
486
- * rate-limit / priority ordering GLOBAL across every engine replica.
402
+ * rate-limit / priority ordering GLOBAL across every engine replica. Operator only.
487
403
  */
488
404
  admission?: AdmissionBackend;
489
405
  /**
490
406
  * Provide the current W3C `traceparent` to stamp on dispatched remote tasks, so workers continue
491
- * the distributed trace. Pass `otelTraceparent` from `@dudousxd/nestjs-durable-otel`.
407
+ * the distributed trace. Pass `otelTraceparent` from `@dudousxd/nestjs-durable-otel`. Operator only.
492
408
  */
493
409
  traceparent?: () => string | undefined;
494
410
  /**
@@ -505,20 +421,63 @@ interface DurableModuleOptions {
505
421
  * Re-evaluated at each (re)dispatch — including a retry or a crash/scale-down resume that the engine
506
422
  * drives OUTSIDE the originating request scope, where this reader may return empty or stale values.
507
423
  * Treat the carrier as best-effort correlation/propagation metadata only — do NOT treat it as an
508
- * authorization boundary.
424
+ * authorization boundary. Operator only.
509
425
  */
510
426
  context?: () => Record<string, unknown> | undefined;
511
- /** Attempts for each saga compensation when a run fails. Default 1 (no retry). Idempotent undos. */
427
+ /** Attempts for each saga compensation when a run fails. Default 1 (no retry). Idempotent undos.
428
+ * Operator only. */
512
429
  compensationRetries?: number;
513
430
  /**
514
- * Opt into an **in-app worker** (uniform dispatch): the same process runs the engine AND serves its
515
- * own discovered `@Workflow`/`@Step` on one default `group`. Each `@Workflow` is registered
516
- * GROUP-SERVED — its turns are dispatched to `group` over the transport and replayed by a co-located
517
- * worker consumer — instead of run inline. Requires a workflow-task transport (BullMQ). Omit (the
518
- * default) to keep every `@Workflow` on the inline fast path with zero dispatch round-trips. See
519
- * {@link DurableInAppWorkerOptions}.
431
+ * Opt into tenant read scoping: when `true` AND {@link namespace} is set, the module confines the
432
+ * store's reads to that namespace (a tenant-boundary view) instead of the operator view that sees
433
+ * all namespaces. Default `false` — the control plane (e.g. flip's `/ctrl` operator screens) stays
434
+ * unscoped. Requires a store that exposes the `withScope` capability (the MikroORM adapter does);
435
+ * a pre-built store without it is used as-is (construct it already-scoped instead). Operator only.
436
+ */
437
+ scopeReads?: boolean;
438
+ /**
439
+ * ioredis connection (string or options) for a **thin worker** (`connection` only) or the
440
+ * **co-located worker** consumer (`store` + `connection`). Set this to play a worker role (see the
441
+ * interface doc for the full role matrix). Omit to keep every `@Workflow` on the operator's inline
442
+ * fast path with zero dispatch round-trips.
443
+ */
444
+ connection?: string | Record<string, unknown>;
445
+ /**
446
+ * The isolation partition a worker role serves — a thin worker's or co-located worker's queue
447
+ * subscription, AND (for a co-located worker) the suffix each `@Workflow`'s dispatch token carries.
448
+ * Each handler's queue token is `tenantGroup(sanitizeQueueToken(name), partition)`
449
+ * (`@dudousxd/nestjs-durable-core`), so `undefined`, `''`, or `'default'` stays byte-identical to
450
+ * the bare (sanitized) name (single-tenant deployment unchanged), and any other partition serves
451
+ * `<name>@<partition>` — matching the queue name an operator's convention dispatch routes that
452
+ * tenant's runs to (`tenantGroup(run.workflow, run.namespace)` on the engine side). Not to be
453
+ * confused with {@link namespace} (the operator's own poll-scoping axis). Ignored for a plain
454
+ * operator (no `connection`).
455
+ */
456
+ partition?: string;
457
+ /** Key prefix namespacing the durable queues for a worker role's consumer. Defaults to `durable`
458
+ * (matches the transport). Ignored for a plain operator (no `connection`). */
459
+ prefix?: string;
460
+ /**
461
+ * How many tasks a worker role's co-located/thin consumer runs concurrently PER SUBSCRIBED QUEUE
462
+ * (BullMQ Worker concurrency — the same limit is applied to every per-name queue the single
463
+ * `runRedisWorker` call starts). Defaults to 1. Raise it so a fanned-out batch (e.g. the N remote
464
+ * steps of a `gather`) runs in parallel instead of serially.
465
+ *
466
+ * Pass `'adaptive'` (or `{ mode:'adaptive', ... }`) to let the consumer self-tune its concurrency
467
+ * (latency gradient + RAM brake + backpressure) and publish a live status on its heartbeat. Ignored
468
+ * for a plain operator (no `connection`).
469
+ */
470
+ concurrency?: ConcurrencyOption;
471
+ /**
472
+ * Per-handler concurrency override, keyed by workflow/step NAME. Falls back to {@link concurrency}
473
+ * (then 1). NOT YET WIRED THROUGH: `runRedisWorker` (`@dudousxd/durable-worker`) currently applies
474
+ * one {@link concurrency} limit uniformly across every per-name queue it subscribes to from a
475
+ * single call — reserved here for when per-name concurrency lands there.
520
476
  */
521
- inAppWorker?: DurableInAppWorkerOptions;
477
+ concurrencyByHandler?: Record<string, ConcurrencyOption>;
478
+ /** Timeout for a thin worker's `RunGateway` round-trip over `transport` before it rejects. Defaults
479
+ * to 10_000ms inside `ProxyRunGateway`. Ignored for an operator (bound to `StoreRunGateway`). */
480
+ runGatewayTimeoutMs?: number;
522
481
  }
523
482
  interface DurableModuleAsyncOptions {
524
483
  useFactory: (...args: never[]) => DurableModuleOptions | Promise<DurableModuleOptions>;
@@ -530,6 +489,118 @@ declare class DurableModule {
530
489
  private static build;
531
490
  }
532
491
 
492
+ /**
493
+ * The **store-less `engine.start` facade** for a tenant worker. Provided under the `WorkflowEngine`
494
+ * DI token by {@link import('./durable.module').DurableModule} (`forRoot({ connection })`, no
495
+ * `store`), so tenant code calls `engine.start(...)` UNCHANGED — it has no idea it is a tenant.
496
+ * Instead of touching a DB, `start` publishes a `StartRunMessage` on the SHARED `durable-start-run`
497
+ * queue (Option B: tenant rides as message DATA, never as wire segmentation). The operator (control
498
+ * plane, `namespace: undefined`) consumes it, stamps the run's namespace from `tenant`, and routes
499
+ * the run's task to `<workflow>@<tenant>` — the partition THIS tenant's worker serves.
500
+ *
501
+ * `cancel`/`deleteRun` need the store/driver a tenant does not have; they throw. No wire message
502
+ * exists for them (the operator owns cancellation/retention).
503
+ */
504
+ declare class DurableStartClient {
505
+ private readonly options;
506
+ private readonly deps?;
507
+ private readonly tenant;
508
+ constructor(options: DurableModuleOptions, deps?: StartRunDeps | undefined);
509
+ start<C extends WorkflowClass>(workflow: C, input: WorkflowInputOf<C>, runId?: string, opts?: StartOptions): Promise<RunResult>;
510
+ start(workflow: string, input: unknown, runId?: string, opts?: StartOptions): Promise<RunResult>;
511
+ cancel(_runId: string): Promise<void>;
512
+ deleteRun(_runId: string): Promise<void>;
513
+ resume(_runId: string): Promise<void>;
514
+ waitForRun(_runId: string, _opts?: {
515
+ timeoutMs?: number;
516
+ }): Promise<void>;
517
+ signal(_token: string, _payload: unknown): Promise<void>;
518
+ signalWithStart(_workflow: string, _input: unknown, _runId: string, _signal: {
519
+ token: string;
520
+ payload?: unknown;
521
+ }, _opts?: StartOptions): Promise<void>;
522
+ publishEvent(_name: string, _payload: unknown, _opts?: {
523
+ id?: string;
524
+ }): Promise<void>;
525
+ }
526
+
527
+ /**
528
+ * The `runRedisWorker` function the module uses to start each partition's BullMQ consumer.
529
+ * Defaults to the real one from `@dudousxd/durable-worker`; tests `overrideProvider` it with a
530
+ * fake so no real Redis is needed.
531
+ */
532
+ declare const RUN_REDIS_WORKER: unique symbol;
533
+ /** The list of started {@link RunningWorker} handles (the single handle {@link ThinWorkerBootstrap}
534
+ * starts), closed on shutdown. */
535
+ declare const DURABLE_WORKER_RUNNERS: unique symbol;
536
+ /** The signature of `runRedisWorker` — injected behind {@link RUN_REDIS_WORKER}. */
537
+ type RunRedisWorkerFn = (opts: RunRedisWorkerOptions) => Promise<RunningWorker>;
538
+ /**
539
+ * Discovers every provider carrying `@Workflow` metadata and registers its `run(ctx, input)` on the
540
+ * thin {@link DurableWorkerRuntime}. Mirrors the engine-side `WorkflowRegistrar`, but registers on
541
+ * the runner-core runtime instead of a `WorkflowEngine` — the runtime drives `run` with the thin
542
+ * `WorkflowContext` (which `implements WorkflowCtx`), so the body runs unchanged. NO store/engine.
543
+ * Inert (skips registration) outside the pure thin-worker role — see {@link isPureThinWorker}.
544
+ */
545
+ declare class ThinWorkflowRegistrar implements OnModuleInit {
546
+ private readonly discovery;
547
+ private readonly runtime;
548
+ private readonly options;
549
+ constructor(discovery: DiscoveryService, runtime: DurableWorkerRuntime, options: DurableModuleOptions);
550
+ onModuleInit(): void;
551
+ }
552
+ /**
553
+ * Discovers every `@Step` method and registers it as a step handler on the thin
554
+ * {@link DurableWorkerRuntime}. Mirrors the engine-side `DurableStepRegistrar`, but always registers
555
+ * on the runtime (a thin worker IS the consumer — there is no in-process-vs-queue branch). Inert
556
+ * outside the pure thin-worker role — see {@link isPureThinWorker}.
557
+ */
558
+ declare class ThinStepRegistrar implements OnModuleInit {
559
+ private readonly discovery;
560
+ private readonly metadataScanner;
561
+ private readonly runtime;
562
+ private readonly options;
563
+ constructor(discovery: DiscoveryService, metadataScanner: MetadataScanner, runtime: DurableWorkerRuntime, options: DurableModuleOptions);
564
+ onModuleInit(): void;
565
+ }
566
+ /**
567
+ * Starts ONE `runRedisWorker` call on bootstrap — after both registrars have run, so every handler
568
+ * is registered before any task is consumed — and closes it on shutdown. The runner (`@dudousxd/
569
+ * durable-worker`) derives its subscription from `runtime.registeredNames()` (one queue per
570
+ * registered `@Workflow`/`@Step` name), not a hand-declared group list. Inert outside the pure
571
+ * thin-worker role — see {@link isPureThinWorker}: an operator (`store` set) never starts this
572
+ * consumer, and `store + connection` starts its OWN co-located consumer instead (`in-app-worker.ts`).
573
+ *
574
+ * For the shutdown close to fire, enable Nest's shutdown hooks: `app.enableShutdownHooks()`.
575
+ */
576
+ declare class ThinWorkerBootstrap implements OnApplicationBootstrap, OnApplicationShutdown {
577
+ private readonly runtime;
578
+ private readonly options;
579
+ private readonly runRedisWorker;
580
+ private readonly runnersSink;
581
+ private readonly runners;
582
+ constructor(runtime: DurableWorkerRuntime, options: DurableModuleOptions, runRedisWorker: RunRedisWorkerFn, runnersSink: RunningWorker[]);
583
+ onApplicationBootstrap(): Promise<void>;
584
+ onApplicationShutdown(): Promise<void>;
585
+ }
586
+ /**
587
+ * The thin-worker slice of {@link import('./durable.module').DurableModule}'s unified provider set —
588
+ * active for the pure thin-worker role (`connection` set, `store` unset): registers discovered
589
+ * `@Workflow`/`@Step` on a store-less {@link DurableWorkerRuntime} and starts ONE `runRedisWorker`
590
+ * consumer, one queue PER REGISTERED NAME. Always present in the provider list (so a single
591
+ * `DurableModule.forRootAsync` can serve any role once its options resolve); every class/factory here
592
+ * is inert (see {@link isPureThinWorker}) outside that role.
593
+ */
594
+ declare function thinWorkerProviders(): Provider[];
595
+ /**
596
+ * The no-transport fallback bound to `RUN_GATEWAY` for a thin worker (`connection` set, `store`
597
+ * unset) that didn't also pass a `transport` — mirrors `DurableStartClient`'s tenant-error idiom
598
+ * (`tenantUnsupported`): every method rejects with a clear, named error instead of a cryptic
599
+ * `this.gateway.X is not a function`. `subscribe` is synchronous on the `RunGateway` port, so it
600
+ * throws synchronously (same message) rather than returning a rejected promise.
601
+ */
602
+ declare function unavailableRunGateway(): RunGateway;
603
+
533
604
  declare const ENTITY_METADATA: unique symbol;
534
605
  declare const ENTITY_ON_METADATA: unique symbol;
535
606
  /**
@@ -571,6 +642,135 @@ declare class EntityService {
571
642
  getState<S = unknown>(name: string, key: string): Promise<S | undefined>;
572
643
  }
573
644
 
645
+ /**
646
+ * The **co-located in-app worker** (uniform dispatch): active whenever an app supplies BOTH `store`
647
+ * AND `connection` (`DurableModule.forRoot({ store, transport, connection, partition? })`) — the same
648
+ * process runs the engine AND serves its own discovered `@Workflow`/`@Step`. The engine registers each
649
+ * `@Workflow` GROUP-SERVED — its turns are dispatched, PER WORKFLOW NAME, to
650
+ * `tenantGroup(sanitizeQueueToken(name), partition)` over the transport via a per-workflow
651
+ * `RemoteWorkflowExecutor` instead of run inline — and a co-located {@link DurableWorkerRuntime}
652
+ * subscribes one queue per discovered name (via `runRedisWorker`) and replays the very same TS bodies.
653
+ * This is the uniform-dispatch "one app, both roles" shape — every turn pays a transport round-trip
654
+ * even though the worker is the same process. Requires a transport that carries workflow tasks
655
+ * (BullMQ); an in-process-only transport cannot dispatch a `WorkflowExecutor`.
656
+ *
657
+ * Distinct from the PURE thin-worker role (`connection` set, `store` unset — see
658
+ * `durable-worker.module.ts`'s `ThinWorkflowRegistrar`/`ThinStepRegistrar`/`ThinWorkerBootstrap`),
659
+ * which has no engine/store of its own at all.
660
+ */
661
+ declare const IN_APP_WORKER_BINDING: unique symbol;
662
+ /** The co-located worker's {@link DurableWorkerRuntime} (the consumer half). */
663
+ declare const IN_APP_WORKER_RUNTIME: unique symbol;
664
+ /** `runRedisWorker`, injected so tests can substitute a fake (no real Redis). Defaults to the real one. */
665
+ declare const IN_APP_RUN_REDIS_WORKER: unique symbol;
666
+ /** Started {@link RunningWorker} handles for the in-app worker, closed on shutdown. */
667
+ declare const IN_APP_WORKER_RUNNERS: unique symbol;
668
+ /** The group-served binding shape resolved behind {@link IN_APP_WORKER_BINDING}. */
669
+ interface InAppWorkerBinding {
670
+ transport: Transport;
671
+ partition?: string;
672
+ }
673
+ /**
674
+ * The consumer half of the in-app worker: on init it registers every discovered `@Workflow`/`@Step`
675
+ * on a {@link DurableWorkerRuntime} (the SAME bodies the engine registered group-served), and on
676
+ * bootstrap it starts one `runRedisWorker` call that subscribes one queue per discovered name,
677
+ * suffixed by the configured partition, closing it on shutdown. A no-op outside the co-located role
678
+ * (see {@link isCoLocatedWorker}). Mirrors the thin {@link
679
+ * import('./durable-worker.module').ThinWorkerBootstrap}, but co-located with a full engine.
680
+ */
681
+ declare class InAppWorkerBootstrap implements OnModuleInit, OnApplicationBootstrap, OnApplicationShutdown {
682
+ private readonly discovery;
683
+ private readonly metadataScanner;
684
+ private readonly options;
685
+ private readonly runtime;
686
+ private readonly runRedisWorker;
687
+ private readonly runnersSink;
688
+ private readonly runners;
689
+ constructor(discovery: DiscoveryService, metadataScanner: MetadataScanner, options: DurableModuleOptions, runtime: DurableWorkerRuntime, runRedisWorker: RunRedisWorkerFn, runnersSink: RunningWorker[]);
690
+ onModuleInit(): void;
691
+ onApplicationBootstrap(): Promise<void>;
692
+ onApplicationShutdown(): Promise<void>;
693
+ }
694
+ /**
695
+ * The providers that stand up the co-located in-app worker, added to {@link
696
+ * import('./durable.module').DurableModule}'s unified provider set. All are inert outside the
697
+ * co-located role (`store` + `connection` both set — see {@link isCoLocatedWorker}), so a plain
698
+ * operator (`store` only) or pure thin worker (`connection` only) is unaffected.
699
+ */
700
+ declare function inAppWorkerProviders(): Provider[];
701
+
702
+ /**
703
+ * Tenant-side `RunGateway` — round-trips every verb as a `RunRequest`/`RunReply` pair over the
704
+ * transport, correlated by a minted `requestId`, and bridges `subscribe` onto the transport's
705
+ * per-tenant event stream. Bound under `RUN_GATEWAY` by `DurableModule`'s thin-worker role
706
+ * (`connection` set, no `store`) when the app supplies a `transport` (see `unavailableRunGateway`
707
+ * for the no-transport fallback). The counterpart to the operator-side `StoreRunGateway`: a tenant
708
+ * worker never touches a store/driver directly, only this proxy.
709
+ */
710
+ declare class ProxyRunGateway implements RunGateway {
711
+ private readonly transport;
712
+ private readonly tenant;
713
+ private readonly timeoutMs;
714
+ private readonly pending;
715
+ constructor(transport: Transport, tenant: string, timeoutMs?: number);
716
+ private handleReply;
717
+ private request;
718
+ getRunDetail(runId: string): Promise<RunDetail | null>;
719
+ listRuns(query: RunQuery): Promise<WorkflowRun[]>;
720
+ cancel(runId: string, opts?: {
721
+ compensate?: boolean;
722
+ }): Promise<RunResult | null>;
723
+ retry(runId: string): Promise<RunResult | null>;
724
+ continue(runId: string): Promise<RunResult | null>;
725
+ retryWithInput(runId: string, input: unknown): Promise<{
726
+ runId: string;
727
+ } | null>;
728
+ subscribe(runId: string, onEvent: (event: EngineEvent) => void): () => void;
729
+ }
730
+
731
+ /**
732
+ * True for the **operator** role — `store` is set (with or without a co-located `connection`). See
733
+ * {@link DurableModuleOptions} for the full role matrix. Kept in its own module (rather than
734
+ * `durable.module.ts`) so the poller/registrar classes can import it as a plain VALUE without a
735
+ * circular value dependency on the module that also imports them as providers.
736
+ */
737
+ declare function isOperatorRole(options: DurableModuleOptions): boolean;
738
+ /**
739
+ * True when an operator instance actively DRIVES runs — polls pending, recovers crashed, resumes due
740
+ * timers, sweeps timeouts, prunes retention, consumes local steps — as opposed to a read-only/
741
+ * dashboard replica (`drive: false`) that mounts the store/dashboard but leaves driving to another
742
+ * instance. Meaningless (`false`) outside the operator role. Defaults to `true` for an operator.
743
+ */
744
+ declare function isDrivingOperator(options: DurableModuleOptions): boolean;
745
+
746
+ /** The narrow slice of `Transport` the responder needs — a tenant's read/control request in, a
747
+ * correlated reply out. Both are OPTIONAL on the full `Transport` interface (only broker
748
+ * transports carry the run-request/reply protocol); the caller capability-checks before wiring
749
+ * this up (see `durable.module.ts`). */
750
+ interface RunRequestTransport {
751
+ onRunRequest(handler: (msg: RunRequest) => Promise<void>): void;
752
+ publishRunReply(reply: RunReply): Promise<void>;
753
+ }
754
+ /**
755
+ * Operator-side consumer of a tenant's {@link RunRequest}s: answers each one against a
756
+ * `RunGateway`, enforcing the tenant boundary before touching the run. For every runId-bearing
757
+ * verb it loads the run via `getRunDetail` FIRST and compares `run.namespace` to the requesting
758
+ * `msg.tenant` — a mismatch short-circuits into a `cross-tenant` error reply WITHOUT calling the
759
+ * verb, so a tenant can never read or act on another tenant's run. `listRuns` is scoped by
760
+ * overwriting the query's `namespace` with the requester's tenant, ignoring whatever the client
761
+ * sent. This is the security boundary of the tenant run gateway — do not weaken it.
762
+ */
763
+ declare class RunRequestResponder {
764
+ private readonly transport;
765
+ private readonly gateway;
766
+ constructor(transport: RunRequestTransport, gateway: RunGateway);
767
+ /** Register the consumer on the transport. Each request is answered independently; a handler
768
+ * failure never throws back into the transport (errors are captured into an error reply). */
769
+ start(): void;
770
+ private handle;
771
+ private callVerb;
772
+ }
773
+
574
774
  declare const STEP_INTERCEPTOR_METADATA: unique symbol;
575
775
  /**
576
776
  * The shape a `@StepInterceptor()` provider must implement: `intercept(invocation, next)` wraps the
@@ -589,6 +789,57 @@ interface DurableStepInterceptor {
589
789
  declare function StepInterceptor(): ClassDecorator;
590
790
  declare function isStepInterceptor(target: Function): boolean;
591
791
 
792
+ /**
793
+ * Store-backed `RunGateway` — the operator-side implementation, bound to {@link RUN_GATEWAY} on
794
+ * worker/drive instances. Reuses `DashboardService`'s six read/control method bodies verbatim
795
+ * (`dashboard.service.ts:76-172`), so a consumer that only needs the bounded `RunGateway` surface
796
+ * (the `RunRequestResponder`, a thin controller) doesn't have to depend on the dashboard package.
797
+ */
798
+ declare class StoreRunGateway implements RunGateway {
799
+ private readonly store;
800
+ private readonly engine;
801
+ constructor(store: StateStore, engine: WorkflowEngine);
802
+ getRunDetail(runId: string): Promise<RunDetail | null>;
803
+ listRuns(query: RunQuery): Promise<WorkflowRun[]>;
804
+ cancel(runId: string, opts?: {
805
+ compensate?: boolean;
806
+ }): Promise<RunResult | null>;
807
+ /** Re-enqueue (dispatch model) instead of resuming inline — a worker picks the run up and replays it. */
808
+ retry(runId: string): Promise<RunResult | null>;
809
+ continue(runId: string): Promise<RunResult | null>;
810
+ retryWithInput(runId: string, input: unknown): Promise<{
811
+ runId: string;
812
+ } | null>;
813
+ subscribe(runId: string, onEvent: (event: EngineEvent) => void): () => void;
814
+ }
815
+
816
+ /** The narrow slice of `StateStore` the republisher needs — the `step.*`-event namespace fallback
817
+ * (see {@link TenantEventRepublisher.namespaceFor}). Mirrors the `RunRequestTransport` narrowing
818
+ * convention (`run-request-responder.ts`): depend on the smallest surface, not the whole store. */
819
+ type RunLookupStore = Pick<StateStore, 'getRun'>;
820
+ /**
821
+ * Re-publishes engine lifecycle events onto a run's per-tenant channel via `publish` (bound from
822
+ * `Transport.publishTenantEvent`), so a store-less tenant worker can live-tail its OWN runs.
823
+ *
824
+ * The run's namespace is read straight off `event.namespace` (stamped by `engine.ts`'s `emit()` on
825
+ * every `run.*` lifecycle event, where the run is already in hand); a `step.*` event doesn't carry
826
+ * it, so those fall back to `store.getRun`. EVERY run's resolved namespace is memoized in
827
+ * `runNamespaces` — including a bare/`default` run — so the store is read at most ONCE per run id
828
+ * regardless of how many `step.*` events that run emits; a `null` cache entry means "resolved, not
829
+ * a tenant" (distinct from "not yet resolved", i.e. absent from the map). A run's entry is deleted
830
+ * the moment its terminal event is handled, so the cache stays bounded to in-flight runs (tenant and
831
+ * default alike) — the same order of magnitude it was already paying for tenant runs alone, in
832
+ * exchange for turning a per-event store read into a per-run one.
833
+ */
834
+ declare class TenantEventRepublisher {
835
+ private readonly store;
836
+ private readonly publish;
837
+ private readonly runNamespaces;
838
+ constructor(store: RunLookupStore, publish: (event: TenantEvent) => Promise<void>);
839
+ handle(event: EngineEvent): Promise<void>;
840
+ private namespaceFor;
841
+ }
842
+
592
843
  /**
593
844
  * Cross-lib injection token for the current-request context accessor, owned by
594
845
  * `@dudousxd/nestjs-context`. We do NOT import nestjs-context (it is an OPTIONAL
@@ -601,6 +852,14 @@ declare function isStepInterceptor(target: Function): boolean;
601
852
  * byte-identical with nestjs-context's export.
602
853
  */
603
854
  declare const CONTEXT_ACCESSOR: unique symbol;
855
+ /**
856
+ * Injection token for the bound `RunGateway` — the read/control surface a consumer (a controller,
857
+ * or the `RunRequestResponder`) uses instead of reaching for `WorkflowEngine`/`StateStore`
858
+ * directly. `DurableModule` binds it to `StoreRunGateway` on worker/drive (operator) instances.
859
+ * `Symbol.for` so re-resolving this module (e.g. across a dynamic-module boundary) still hits the
860
+ * same registration.
861
+ */
862
+ declare const RUN_GATEWAY: unique symbol;
604
863
 
605
864
  /** Public entry point for starting and resuming workflow runs. */
606
865
  declare class WorkflowService {
@@ -647,7 +906,7 @@ declare class WorkflowService {
647
906
  }>;
648
907
  /**
649
908
  * Publish a named event. Resumes runs waiting on it via `ctx.waitForEvent(name, { match })` and
650
- * starts a fresh run of every workflow subscribed via `@Workflow({ onEvent })` / `@OnEvent` (the
909
+ * starts a fresh run of every workflow subscribed via `@Workflow({ onEvent })` / `@OnDurableEvent` (the
651
910
  * payload becomes its input). Pass `opts.id` to dedupe redeliveries. Returns how many runs it
652
911
  * touched (resumed + started).
653
912
  */
@@ -656,4 +915,4 @@ declare class WorkflowService {
656
915
  }): Promise<number>;
657
916
  }
658
917
 
659
- export { CONTEXT_ACCESSOR, type ContextAccessor, type ContextStore, DEAD_LETTER_METADATA, DURABLE_STEP_METADATA, DURABLE_WORKER_OPTIONS, DURABLE_WORKER_RUNNERS, DeadLetter, 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 };
918
+ export { CONTEXT_ACCESSOR, type ContextAccessor, type ContextStore, DEAD_LETTER_METADATA, DURABLE_STEP_METADATA, DURABLE_WORKER_RUNNERS, DeadLetter, DurableModule, type DurableModuleAsyncOptions, type DurableModuleOptions, type DurableRetentionOptions, DurableStartClient, DurableStep, type DurableStepInterceptor, type DurableStepMeta, ENTITY_METADATA, ENTITY_ON_METADATA, Entity, EntityService, IN_APP_RUN_REDIS_WORKER, IN_APP_WORKER_BINDING, IN_APP_WORKER_RUNNERS, IN_APP_WORKER_RUNTIME, type InAppWorkerBinding, InAppWorkerBootstrap, ON_EVENT_METADATA, On, OnDurableEvent, OnEvent, ProxyRunGateway, RUN_GATEWAY, RUN_REDIS_WORKER, type RunLookupStore, type RunRedisWorkerFn, RunRequestResponder, type RunRequestTransport, STEP_INTERCEPTOR_METADATA, Step, StepInterceptor, type StepOptions, StoreRunGateway, TenantEventRepublisher, ThinStepRegistrar, ThinWorkerBootstrap, ThinWorkflowRegistrar, type UserRef, WORKFLOW_METADATA, Workflow, type WorkflowMeta, type WorkflowOptions, WorkflowService, entityConfigFor, getDurableStepMeta, getEntityMeta, getOnEvents, getWorkflowMeta, inAppWorkerProviders, isDeadLetterHandler, isDrivingOperator, isOperatorRole, isStepInterceptor, thinWorkerProviders, unavailableRunGateway };