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