@dudousxd/nestjs-durable 0.26.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.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
 
@@ -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.
@@ -183,184 +241,6 @@ declare function OnEvent(...events: string[]): ClassDecorator;
183
241
  /** All events a workflow class subscribes to — the union of `@Workflow({ onEvent })` and `@OnEvent`. */
184
242
  declare function getOnEvents(meta: WorkflowMeta, target: object): string[];
185
243
 
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.
278
- *
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).
281
- */
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[];
363
-
364
244
  /**
365
245
  * Retention config for the {@link RetentionPoller}. One or more {@link RetentionPolicy policies}
366
246
  * (status sets must be disjoint — validated at boot), swept together on a shared interval.
@@ -387,12 +267,32 @@ interface DurableRetentionOptions {
387
267
  /** Max runs hard-deleted per batch (per policy, looped until drained). Defaults to 1000. */
388
268
  batchSize?: number;
389
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
+ */
390
288
  interface DurableModuleOptions {
391
- 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;
392
292
  transport?: Transport;
393
293
  /**
394
294
  * 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 })`.
295
+ * the first and fails over to the next; a step pins one via `ctx.step(handler, input, { transport })`.
396
296
  * Use instead of `transport`.
397
297
  */
398
298
  transports?: NamedTransport[];
@@ -401,29 +301,34 @@ interface DurableModuleOptions {
401
301
  * transport when it can broadcast (event-emitter, BullMQ); set explicitly to use a dedicated one.
402
302
  */
403
303
  controlPlane?: ControlPlane;
404
- /** 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. */
405
305
  timerPollMs?: number;
406
306
  /**
407
307
  * Auto-create the durable tables on boot via `store.ensureSchema()`. Defaults to true. Turn
408
308
  * off in production and call the store adapter's `ensure*DurableSchema()` from a migration.
309
+ * Operator only.
409
310
  */
410
311
  autoSchema?: boolean;
411
312
  /**
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`.
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).
415
320
  */
416
321
  namespace?: string;
417
322
  /**
418
323
  * 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.
324
+ * another may take over. Defaults to 30000. Set above your longest synchronous run. Operator only.
420
325
  */
421
326
  leaseMs?: number;
422
- /** 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). */
423
328
  instanceId?: string;
424
329
  /**
425
330
  * 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.
331
+ * (a poison pill that crashes the process every boot). Omit for unlimited. Operator only.
427
332
  */
428
333
  maxRecoveryAttempts?: number;
429
334
  /**
@@ -432,6 +337,7 @@ interface DurableModuleOptions {
432
337
  * and lets recovery re-drive; each heartbeat rearms the window so a slow-but-alive worker is never
433
338
  * re-driven. Pair with a worker SDK that emits run-scoped heartbeats (`@dudousxd/durable-worker` ≥ the
434
339
  * release that ships them, and the Python `durable-worker`). Omit for the prior unbounded await.
340
+ * Operator only.
435
341
  */
436
342
  remoteAdvanceSilenceMs?: number;
437
343
  /**
@@ -441,54 +347,59 @@ interface DurableModuleOptions {
441
347
  * it can alert, compensate, or queue for review. Resolution per dead run: the workflow's inline
442
348
  * `@DeadLetter()` method → its `@Workflow({ deadLetterWorkflow })` reference → this default. Omit
443
349
  * everything to just leave dead runs parked (inspectable + retriable from the dashboard). Accepts a
444
- * workflow class (refactor-safe) or a name (cross-runtime).
350
+ * workflow class (refactor-safe) or a name (cross-runtime). Operator only.
445
351
  */
446
352
  deadLetterWorkflow?: WorkflowRef;
447
353
  /**
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.
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`).
452
361
  */
453
- worker?: boolean;
454
- /** 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. */
455
364
  shutdownTimeoutMs?: number;
456
365
  /**
457
366
  * 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
367
+ * them each tick on **driving** instances only; `engine.start` is idempotent by the schedule's
459
368
  * time-bucket run id, so racing instances start each window exactly once. Cron schedules need the
460
- * optional `cron-parser` peer dependency.
369
+ * optional `cron-parser` peer dependency. Operator only.
461
370
  */
462
371
  schedules?: ScheduledWorkflow[];
463
372
  /**
464
373
  * Hard-prune terminal run history on an interval so `durable_workflow_runs` (and its child tables)
465
374
  * 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
375
+ * poller's per-tick status scans get linearly slower. Driving instances only. Omit to keep all
467
376
  * history (the default). Requires a store adapter that implements `pruneTerminalRuns` (the
468
377
  * MikroORM adapter does); other adapters no-op with a warning. See {@link DurableRetentionOptions}.
378
+ * Operator only.
469
379
  */
470
380
  retention?: DurableRetentionOptions;
471
381
  /**
472
382
  * Build the public callback URL for a `ctx.webhook()` token, e.g.
473
383
  * ``(t) => `https://api.example.com/durable/api/webhooks/${t}` ``. Populates `DurableWebhook.url`
474
384
  * 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.
385
+ * callback. Omit to build URLs yourself from the token. Operator only.
476
386
  */
477
387
  webhookUrl?: (token: string) => string;
478
388
  /**
479
389
  * 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.
390
+ * workflow with `ctx.step(handler, input, { queue: name })` to cap its concurrency / admission rate.
391
+ * Operator only.
481
392
  */
482
393
  queues?: QueueConfig[];
483
394
  /**
484
395
  * Admission backend for the flow-control `queues`. Defaults to in-process (per-instance) caps. Pass
485
396
  * a `RedisAdmissionBackend` (from `@dudousxd/nestjs-durable-admission-redis`) to make concurrency /
486
- * rate-limit / priority ordering GLOBAL across every engine replica.
397
+ * rate-limit / priority ordering GLOBAL across every engine replica. Operator only.
487
398
  */
488
399
  admission?: AdmissionBackend;
489
400
  /**
490
401
  * 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`.
402
+ * the distributed trace. Pass `otelTraceparent` from `@dudousxd/nestjs-durable-otel`. Operator only.
492
403
  */
493
404
  traceparent?: () => string | undefined;
494
405
  /**
@@ -505,20 +416,63 @@ interface DurableModuleOptions {
505
416
  * Re-evaluated at each (re)dispatch — including a retry or a crash/scale-down resume that the engine
506
417
  * drives OUTSIDE the originating request scope, where this reader may return empty or stale values.
507
418
  * Treat the carrier as best-effort correlation/propagation metadata only — do NOT treat it as an
508
- * authorization boundary.
419
+ * authorization boundary. Operator only.
509
420
  */
510
421
  context?: () => Record<string, unknown> | undefined;
511
- /** 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. */
512
424
  compensationRetries?: number;
513
425
  /**
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}.
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.
520
471
  */
521
- 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;
522
476
  }
523
477
  interface DurableModuleAsyncOptions {
524
478
  useFactory: (...args: never[]) => DurableModuleOptions | Promise<DurableModuleOptions>;
@@ -530,6 +484,118 @@ declare class DurableModule {
530
484
  private static build;
531
485
  }
532
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
+
533
599
  declare const ENTITY_METADATA: unique symbol;
534
600
  declare const ENTITY_ON_METADATA: unique symbol;
535
601
  /**
@@ -571,6 +637,135 @@ declare class EntityService {
571
637
  getState<S = unknown>(name: string, key: string): Promise<S | undefined>;
572
638
  }
573
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
+
574
769
  declare const STEP_INTERCEPTOR_METADATA: unique symbol;
575
770
  /**
576
771
  * The shape a `@StepInterceptor()` provider must implement: `intercept(invocation, next)` wraps the
@@ -589,6 +784,57 @@ interface DurableStepInterceptor {
589
784
  declare function StepInterceptor(): ClassDecorator;
590
785
  declare function isStepInterceptor(target: Function): boolean;
591
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
+
592
838
  /**
593
839
  * Cross-lib injection token for the current-request context accessor, owned by
594
840
  * `@dudousxd/nestjs-context`. We do NOT import nestjs-context (it is an OPTIONAL
@@ -601,6 +847,14 @@ declare function isStepInterceptor(target: Function): boolean;
601
847
  * byte-identical with nestjs-context's export.
602
848
  */
603
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;
604
858
 
605
859
  /** Public entry point for starting and resuming workflow runs. */
606
860
  declare class WorkflowService {
@@ -656,4 +910,4 @@ declare class WorkflowService {
656
910
  }): Promise<number>;
657
911
  }
658
912
 
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 };
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 };