@dudousxd/nestjs-durable-core 0.45.0 → 0.46.1

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,7 +1,7 @@
1
1
  import { z } from 'zod';
2
2
 
3
3
  /**
4
- * Flow control for remote steps. A queue caps how much work `ctx.call(step, input, { queue })`
4
+ * Flow control for dispatched steps. A queue caps how much work `ctx.step(step, input, { queue })`
5
5
  * admits at once — a concurrency limit and/or a fixed-window rate limit. When a call can't be
6
6
  * admitted it doesn't dispatch: the run re-suspends with a near-future wake time, and the timer
7
7
  * poller re-tries admission later — so flow control is durable (survives crashes) without holding
@@ -25,7 +25,7 @@ import { z } from 'zod';
25
25
  * blocked, and on a later retry only admits the one that is rightful-next under the policy.
26
26
  */
27
27
  interface QueueConfig {
28
- /** Queue name, referenced by `ctx.call(step, input, { queue: name })`. */
28
+ /** Queue name, referenced by `ctx.step(step, input, { queue: name })`. */
29
29
  name: string;
30
30
  /** Max steps in flight at once for this queue (this instance). Omit for unlimited. */
31
31
  concurrency?: number;
@@ -114,7 +114,7 @@ declare class QueueController {
114
114
  }
115
115
 
116
116
  /**
117
- * Pluggable backend for the remote-step flow-control gate (`ctx.call(step, input, { queue })`). The
117
+ * Pluggable backend for the step flow-control gate (`ctx.step(step, input, { queue })`). The
118
118
  * engine asks it whether a queued call may be admitted now (concurrency / rate / ordering) and tells
119
119
  * it when an admitted slot is released.
120
120
  *
@@ -167,6 +167,69 @@ declare class InMemoryAdmissionBackend implements AdmissionBackend {
167
167
  */
168
168
  declare function parseDuration(duration: string | number): number;
169
169
 
170
+ /**
171
+ * Cross-package step-name stamp. The `@Step` decorator (`@dudousxd/nestjs-durable`) writes a
172
+ * method's derived routing name under this key; core's `ctx.step` reads it (via {@link stepNameOf})
173
+ * to route a method-reference call to the same handler a worker registers by name.
174
+ *
175
+ * `Symbol.for` (the GLOBAL registry), NOT a plain `Symbol()` — so a duplicate copy of this module
176
+ * (pnpm peer-dependency multiplexing, or a dual ESM/CJS load) still reads the SAME key. A plain
177
+ * `Symbol()` would mint a distinct token per copy, so a decorator running against one core instance
178
+ * could stamp a name core's `ctx.step` (a different instance) can never read back. Mirrors the
179
+ * `STATE_STORE` token fix in `./tokens.ts`.
180
+ */
181
+ declare const DURABLE_STEP_NAME: unique symbol;
182
+ /**
183
+ * Cross-package step-DISPATCH-POLICY stamp. `@Step({ retries, backoff, backoffMs, backoffMaxMs,
184
+ * jitter, timeoutMs })` (`@dudousxd/nestjs-durable`) writes the def-level durable-retry/liveness
185
+ * policy under this key; core's `ctx.step` reads it (via {@link stepConfigOf}) and merges it with any
186
+ * per-call {@link StepDispatchOpts} override to build the dispatched {@link StepDef}. Same
187
+ * `Symbol.for` rationale as {@link DURABLE_STEP_NAME} — a duplicate copy of this module (pnpm
188
+ * peer-dependency multiplexing, dual ESM/CJS) must still read back the SAME key.
189
+ */
190
+ declare const DURABLE_STEP_CONFIG: unique symbol;
191
+ /**
192
+ * The def-level durable-dispatch policy a `@Step(...)` can stamp on a method: retry/backoff and the
193
+ * remote-liveness `timeoutMs` — the dispatch-relevant subset of the engine's `StepOptions`. Read off
194
+ * a handler reference via {@link stepConfigOf}; a per-call {@link StepDispatchOpts} passed to
195
+ * `ctx.step(ref, input, opts)` overrides these field-by-field.
196
+ */
197
+ interface StepConfig {
198
+ /** Max attempts before the step (and run) fails. */
199
+ retries?: number | undefined;
200
+ /** How the delay between retries grows: `fixed` (constant) or `exp` (doubles each attempt). */
201
+ backoff?: 'fixed' | 'exp' | undefined;
202
+ /** Base delay in ms between retries. Omit (or 0) to retry with no delay. */
203
+ backoffMs?: number | undefined;
204
+ /** Upper bound on the (exponential) backoff delay. */
205
+ backoffMaxMs?: number | undefined;
206
+ /** Add random jitter (50–100% of the computed delay) to avoid thundering-herd retries. */
207
+ jitter?: boolean | undefined;
208
+ /** Liveness window for the dispatched step: no result/heartbeat within this many ms presumes the
209
+ * worker dead and fails the dispatch with a `RemoteStepTimeout` (retryable — re-dispatches per
210
+ * `retries`). Omit to wait indefinitely. */
211
+ timeoutMs?: number | undefined;
212
+ }
213
+ /**
214
+ * A method carrying its `@Step`-stamped routing name (and, optionally, its dispatch policy).
215
+ * `ctx.step(ref, input)` reads the name via {@link stepNameOf} — the reference itself is never
216
+ * invoked directly by the caller's process (the worker serving that name re-resolves the real
217
+ * handler from DI), so an unbound `this` on `ref` is irrelevant; `ref` is purely a typed,
218
+ * refactor-safe handle onto the stamped name (and policy).
219
+ */
220
+ type StepRef<TInput = unknown, TOutput = unknown> = ((input: TInput) => Promise<TOutput> | TOutput) & {
221
+ [DURABLE_STEP_NAME]?: string | undefined;
222
+ [DURABLE_STEP_CONFIG]?: StepConfig | undefined;
223
+ };
224
+ /** Read the `@Step`-stamped routing name off a function ref. `undefined` for anything unstamped
225
+ * (including a plain string — {@link stepNameOf} only reads function refs; a caller passing a
226
+ * string already has the routing name and dispatches it directly). */
227
+ declare function stepNameOf(ref: unknown): string | undefined;
228
+ /** Read the `@Step`-stamped dispatch policy off a function ref. `undefined` for anything unstamped
229
+ * (including a plain string — a cross-runtime string-name call has no def-level policy to read; pass
230
+ * it via {@link StepDispatchOpts} instead). */
231
+ declare function stepConfigOf(ref: unknown): StepConfig | undefined;
232
+
170
233
  /**
171
234
  * A **workflow reference** is how one workflow names another to call it: either a registered workflow
172
235
  * **name** (a string — the only option across runtimes, e.g. a Python workflow) or, for a same-runtime
@@ -297,7 +360,7 @@ interface StepCheckpoint {
297
360
  * simply re-runs on replay (only `completed` short-circuits).
298
361
  */
299
362
  status: 'pending' | 'running' | 'completed' | 'failed';
300
- /** What the step was called with — the `ctx.call` args for a remote step (a local step has none). */
363
+ /** What the step was called with — the `ctx.step` (dispatched) args for a remote step (a local step has none). */
301
364
  input?: unknown;
302
365
  output?: unknown;
303
366
  error?: StepError | undefined;
@@ -594,6 +657,85 @@ interface SignalWaiter {
594
657
  */
595
658
  parallelGroup?: string | undefined;
596
659
  }
660
+ /**
661
+ * A DB-less tenant worker → control plane request to start a run. Published by a worker that has
662
+ * no direct DB access (the hosted-control-plane tenant model) onto the
663
+ * `<effectivePrefix>-start-run` queue; the control plane consumes it and turns it into a durable
664
+ * run (P4 of the tenants plan).
665
+ *
666
+ * The `tenant` identifies the namespace that owns the run (maps to the engine's `namespace`); it
667
+ * is separate from the wire-level key prefix so a single transport can serve multiple tenants.
668
+ */
669
+ interface StartRunMessage {
670
+ tenant: string;
671
+ /** Registered workflow name. */
672
+ workflow: string;
673
+ input: unknown;
674
+ /** Optional caller-supplied run id (idempotency key). The engine generates one if absent. */
675
+ runId?: string | undefined;
676
+ /** Tags to stamp on the run (merged with the workflow's static @Workflow tags). */
677
+ tags?: string[] | undefined;
678
+ /** Typed, queryable run data to stamp on the run (same as {@link StartOptions.searchAttributes}). */
679
+ searchAttributes?: SearchAttributes | undefined;
680
+ }
681
+ /**
682
+ * A tenant worker → control plane read/control request over the shared transport. Enqueued on
683
+ * `<effectivePrefix>-run-request`; the control plane's `onRunRequest` consumer answers it, scoped to
684
+ * `tenant`, and publishes a {@link RunReply} on `<effectivePrefix>-run-reply` correlated by `requestId`.
685
+ */
686
+ interface RunRequest {
687
+ /** Correlation id minted by the tenant; the matching {@link RunReply} carries it back. */
688
+ requestId: string;
689
+ /** The requesting tenant — the operator scopes the run's namespace to this. */
690
+ tenant: string;
691
+ body: RunRequestKind;
692
+ }
693
+ /** The discriminated verb + args of a {@link RunRequest}. Mirrors the tenant-facing `RunGateway`. */
694
+ type RunRequestKind = {
695
+ kind: 'getRunDetail';
696
+ runId: string;
697
+ } | {
698
+ kind: 'listRuns';
699
+ query: RunQuery;
700
+ } | {
701
+ kind: 'cancel';
702
+ runId: string;
703
+ opts?: {
704
+ compensate?: boolean;
705
+ };
706
+ } | {
707
+ kind: 'retry';
708
+ runId: string;
709
+ } | {
710
+ kind: 'continue';
711
+ runId: string;
712
+ } | {
713
+ kind: 'retryWithInput';
714
+ runId: string;
715
+ input: unknown;
716
+ };
717
+ /** The control plane's answer to a {@link RunRequest}, correlated by `requestId`. */
718
+ interface RunReply {
719
+ requestId: string;
720
+ result: RunReplyResult;
721
+ }
722
+ /** Success carries the verb's payload (JSON-serialised); failure carries a re-throwable error. */
723
+ type RunReplyResult = {
724
+ ok: true;
725
+ data: unknown;
726
+ } | {
727
+ ok: false;
728
+ error: {
729
+ message: string;
730
+ code?: string;
731
+ };
732
+ };
733
+ /** A lifecycle event re-published to a single tenant's channel (`<effectivePrefix>-tenant-events-<tenant>`)
734
+ * so a store-less tenant can live-tail ITS OWN runs. Scoped by the run's namespace at publish time. */
735
+ interface TenantEvent {
736
+ tenant: string;
737
+ event: EngineEvent;
738
+ }
597
739
  /** A unit of work dispatched to a remote worker. This is the documented wire payload. */
598
740
  interface RemoteTask {
599
741
  runId: string;
@@ -622,7 +764,7 @@ interface RemoteTask {
622
764
  /**
623
765
  * Admission priority carried through to the broker (BullMQ job `priority`) so a transport that
624
766
  * supports priority ordering lets an urgent task jump ahead of already-enqueued lower-priority
625
- * ones at the worker. Mirrors the per-call `priority` from `ctx.call(..., { priority })`. Higher
767
+ * ones at the worker. Mirrors the per-call `priority` from `ctx.step(..., { priority })`. Higher
626
768
  * wins; absent means default/unprioritised. Transports without priority support ignore it.
627
769
  */
628
770
  priority?: number | undefined;
@@ -693,10 +835,10 @@ interface HistoryEvent {
693
835
  }
694
836
  /** A decision the workflow function produced at a `seq` that was not yet in history. */
695
837
  type WorkflowCommand =
696
- /** `ctx.call(remoteStep, input)` — dispatch a remote step and await it. A worker's
697
- * `ctx.gather_calls([...])` fan-out tags every dispatched call with the same `parallelGroup` so the
698
- * dashboard renders the remote steps as one parallel fan (parity with the gathered `recordStep` /
699
- * `startChild` tags). Undefined for a lone sequential `ctx.call`. */
838
+ /** `ctx.step(handlerOrName, input)` — dispatch a step (by typed handler reference or by name) and
839
+ * await it. A worker's `ctx.gather_calls([...])` fan-out tags every dispatched call with the same
840
+ * `parallelGroup` so the dashboard renders the dispatched steps as one parallel fan (parity with
841
+ * the gathered `recordStep` / `startChild` tags). Undefined for a lone sequential `ctx.step`. */
700
842
  {
701
843
  kind: 'call';
702
844
  seq: number;
@@ -816,7 +958,7 @@ interface WorkflowExecutor {
816
958
  }
817
959
  /**
818
960
  * A transport in an ordered pool, identified by `id`. The engine dispatches on the first by default
819
- * and fails over to the next on a dispatch error; a step can pin one via `ctx.call(…, { transport })`.
961
+ * and fails over to the next on a dispatch error; a step can pin one via `ctx.step(…, { transport })`.
820
962
  * The chosen `id` is stamped on the {@link RemoteTask} so a worker replies through the matching one.
821
963
  */
822
964
  interface NamedTransport {
@@ -871,6 +1013,35 @@ interface Transport {
871
1013
  * keyspace, so a group with workers but no engine-side registration (e.g. a local-step group)
872
1014
  * still surfaces. Pairs with {@link groupHealth}. */
873
1015
  listWorkerGroups?(): Promise<string[]>;
1016
+ /**
1017
+ * DB-less tenant worker → control plane: publish a {@link StartRunMessage} requesting a new run.
1018
+ * Optional — only transports that carry the hosted-control-plane protocol (P4) implement this.
1019
+ * The message is enqueued on `<effectivePrefix>-start-run`; the control plane's
1020
+ * `onStartRun` consumer turns it into a durable run.
1021
+ */
1022
+ dispatchStartRun?(msg: StartRunMessage): Promise<void>;
1023
+ /**
1024
+ * control plane ← tenant worker: consume {@link StartRunMessage}s and start runs. Pair with
1025
+ * {@link dispatchStartRun}. Optional — only control-plane-side transports implement this.
1026
+ */
1027
+ onStartRun?(handler: (msg: StartRunMessage) => Promise<void>): void;
1028
+ /**
1029
+ * Tenant worker → control plane: publish a {@link RunRequest} (read/control) on
1030
+ * `<effectivePrefix>-run-request`. Optional — only broker transports carry it.
1031
+ */
1032
+ dispatchRunRequest?(msg: RunRequest): Promise<void>;
1033
+ /** control plane ← tenant worker: consume {@link RunRequest}s. Pair with {@link dispatchRunRequest}. */
1034
+ onRunRequest?(handler: (msg: RunRequest) => Promise<void>): void;
1035
+ /** control plane → tenant worker: publish a correlated {@link RunReply} on `<effectivePrefix>-run-reply`
1036
+ * (pub/sub; every tenant subscribes and filters by `requestId`). */
1037
+ publishRunReply?(reply: RunReply): Promise<void>;
1038
+ /** tenant worker ← control plane: consume {@link RunReply}s (filter by `requestId` client-side). */
1039
+ onRunReply?(handler: (reply: RunReply) => void): void;
1040
+ /** control plane → tenant worker: re-publish a lifecycle {@link TenantEvent} on the run's per-tenant
1041
+ * channel `<effectivePrefix>-tenant-events-<tenant>`. */
1042
+ publishTenantEvent?(evt: TenantEvent): Promise<void>;
1043
+ /** tenant worker ← control plane: subscribe to THIS tenant's event channel. Returns an unsubscribe fn. */
1044
+ onTenantEvent?(tenant: string, handler: (evt: TenantEvent) => void): () => void;
874
1045
  }
875
1046
  /** How a worker decides its concurrency, carried in {@link WorkerStatus} so a dashboard can tell a
876
1047
  * fixed knob from a self-regulating one (and, for the latter, where the controller currently sits). */
@@ -1004,10 +1175,10 @@ interface StepOptions {
1004
1175
  /** Add random jitter (50–100% of the computed delay) to avoid thundering-herd retries. */
1005
1176
  jitter?: boolean;
1006
1177
  /**
1007
- * Liveness window for a **remote** step (`ctx.call`): if the worker produces no result and no
1178
+ * Liveness window for a dispatched step (`ctx.step`): if the worker produces no result and no
1008
1179
  * heartbeat within this many ms, the engine presumes it dead and fails the dispatch with a
1009
1180
  * `RemoteStepTimeout` (retryable — it re-dispatches per `retries`). Each heartbeat resets the
1010
- * window. Ignored for local steps. Omit to wait indefinitely.
1181
+ * window. Omit to wait indefinitely.
1011
1182
  */
1012
1183
  timeoutMs?: number;
1013
1184
  /**
@@ -1019,17 +1190,22 @@ interface StepOptions {
1019
1190
  compensate?: () => Promise<void>;
1020
1191
  }
1021
1192
  /**
1022
- * A typed handle to a step that runs on a remote worker. The `name` string is the contract:
1023
- * the worker registers a handler under the same name. `input`/`output` validate at the boundary.
1193
+ * The structural carrier a dispatched `ctx.step` call resolves to and hands the engine: `name` is
1194
+ * the routing contract (the worker registers a handler under the same name, and routing is BY that
1195
+ * name — a worker subscribes per registered handler name, not a hand-declared group). There is no
1196
+ * public factory for this anymore — a `ctx.step(ref, input)` call builds one internally from the
1197
+ * `@Step`-stamped name (see {@link StepRef}/`stepNameOf`); a `ctx.step(name, input)` call builds one
1198
+ * from the literal string. `input`/`output` are optional runtime zod schemas an authoring layer MAY
1199
+ * attach (e.g. `@Step({ input, output })`) for validation at the dispatch boundary — a bare `@Step()`
1200
+ * carries neither, and the engine skips validation when they're absent.
1024
1201
  */
1025
- interface RemoteStepDef<TInput = unknown, TOutput = unknown> extends StepOptions {
1202
+ interface StepDef<TInput = unknown, TOutput = unknown> extends StepOptions {
1026
1203
  name: string;
1027
- /** Worker group expected to handle this step. */
1028
- group: string;
1029
- input: z.ZodType<TInput>;
1030
- output: z.ZodType<TOutput>;
1031
- /** Branding so `ctx.call` can infer types. */
1032
- readonly __remote: true;
1204
+ /** Optional isolation partition; routing is by `name`. Suffixes the routing token as
1205
+ * `<name>@<partition>` (via {@link tenantGroup}) — omit to route by the bare (sanitized) `name`. */
1206
+ partition?: string | undefined;
1207
+ input?: z.ZodType<TInput> | undefined;
1208
+ output?: z.ZodType<TOutput> | undefined;
1033
1209
  }
1034
1210
  /**
1035
1211
  * A durable webhook handle minted by {@link WorkflowCtx.webhook}. Hand `url` to a third party,
@@ -1048,6 +1224,45 @@ interface DurableWebhook<TPayload = unknown> {
1048
1224
  /** Suspend until the callback arrives, then resume with its payload. */
1049
1225
  wait(): Promise<TPayload>;
1050
1226
  }
1227
+ /**
1228
+ * Options for a dispatched {@link WorkflowCtx.step} call. `retries`/`backoff`/`backoffMs`/
1229
+ * `backoffMaxMs`/`jitter`/`timeoutMs` are a PER-CALL override of the `@Step`-declared
1230
+ * {@link StepConfig} (see `stepConfigOf`) — the effective policy `ctx.step` builds into the dispatched
1231
+ * {@link StepDef} is `{ ...stepConfigOf(ref), ...opts }`, so a call-site value wins field-by-field. The
1232
+ * string (cross-runtime) form of `ctx.step` has no stamped `@Step` to read, so it uses these fields
1233
+ * as-is.
1234
+ */
1235
+ interface StepDispatchOpts {
1236
+ /** Subject the dispatch to a registered flow-control queue (concurrency / rate limit). */
1237
+ queue?: string;
1238
+ /** Admission priority within `queue`; higher is admitted first when a slot is contended
1239
+ * (default 0). No effect without a `queue`. */
1240
+ priority?: number;
1241
+ /** The fairness bucket for a queue with `fairness: 'key'` (e.g. a tenant id) — the queue
1242
+ * round-robins across distinct keys so one key can't monopolize the budget. Defaults to the run
1243
+ * id when omitted. No effect without a `queue`. */
1244
+ fairnessKey?: string;
1245
+ /** Pin the dispatch to a named transport in the pool (else the pool's first, with failover to the
1246
+ * rest). See `engine.registerQueue` / the engine's `transports` option. */
1247
+ transport?: string;
1248
+ /** Max attempts before the step (and run) fails. Overrides the `@Step`-declared value. */
1249
+ retries?: number;
1250
+ /** How the delay between retries grows: `fixed` (constant) or `exp` (doubles each attempt).
1251
+ * Overrides the `@Step`-declared value. */
1252
+ backoff?: BackoffStrategy;
1253
+ /** Base delay in ms between retries. Omit (or 0) to retry with no delay. Overrides the
1254
+ * `@Step`-declared value. */
1255
+ backoffMs?: number;
1256
+ /** Upper bound on the (exponential) backoff delay. Overrides the `@Step`-declared value. */
1257
+ backoffMaxMs?: number;
1258
+ /** Add random jitter (50–100% of the computed delay) to avoid thundering-herd retries. Overrides
1259
+ * the `@Step`-declared value. */
1260
+ jitter?: boolean;
1261
+ /** Liveness window for this dispatched step: no result/heartbeat within this many ms presumes the
1262
+ * worker dead and fails the dispatch with a `RemoteStepTimeout` (retryable — re-dispatches per
1263
+ * `retries`). Omit to wait indefinitely. Overrides the `@Step`-declared value. */
1264
+ timeoutMs?: number;
1265
+ }
1051
1266
  /**
1052
1267
  * The context handed to a workflow function. Every interaction with the outside world goes
1053
1268
  * through it so the engine can checkpoint — the workflow body itself stays deterministic.
@@ -1055,11 +1270,25 @@ interface DurableWebhook<TPayload = unknown> {
1055
1270
  interface WorkflowCtx {
1056
1271
  readonly runId: string;
1057
1272
  /**
1058
- * Run a local durable step: executed once, then its result is checkpointed and replayed. The
1059
- * body receives a {@link StepLogger} to record debug/error lines and sub-process outcomes — these
1060
- * are checkpointed with the step and shown under it in the dashboard.
1273
+ * Run a durable step — always dispatched, always engine-scheduled: the ONE step primitive (no
1274
+ * local/remote placement choice). Pass the step's method **reference** (a `@Step`-decorated
1275
+ * method, typed by its own signature — refactor-safe, autocompleted):
1276
+ *
1277
+ * ```ts
1278
+ * const r = await ctx.step(this.extraction.runExtractionPage, { page, key });
1279
+ * ```
1280
+ *
1281
+ * or its **name** for a cross-runtime handler (no JS reference to import, e.g. a Python `@step`):
1282
+ *
1283
+ * ```ts
1284
+ * const out = await ctx.step<ProcResult>('processing:proc', input);
1285
+ * ```
1286
+ *
1287
+ * Both forms dispatch identically — a step runs on whatever worker serves that name and the run
1288
+ * suspends (zero compute) until the result lands, then resumes with it (durable, replay-safe).
1061
1289
  */
1062
- step<TOutput>(name: string, fn: (log: StepLogger) => Promise<TOutput>, options?: StepOptions): Promise<TOutput>;
1290
+ step<TInput, TOutput>(handler: StepRef<TInput, TOutput>, input: TInput, opts?: StepDispatchOpts): Promise<TOutput>;
1291
+ step<TOutput = unknown>(name: string, input: unknown, opts?: StepDispatchOpts): Promise<TOutput>;
1063
1292
  /**
1064
1293
  * **Exactly-once** durable step for DB work: runs `fn` and writes the step's checkpoint in ONE
1065
1294
  * store transaction, so the business write and the "done" marker commit atomically — a crash can
@@ -1076,23 +1305,6 @@ interface WorkflowCtx {
1076
1305
  callEntity<TResult = unknown>(name: string, key: string, op: string, arg?: unknown): Promise<TResult>;
1077
1306
  /** Send a durable entity op without awaiting a result (fire-and-forget, dispatched once). */
1078
1307
  signalEntity(name: string, key: string, op: string, arg?: unknown): Promise<void>;
1079
- /**
1080
- * Dispatch a typed remote step and await its checkpointed result. Options:
1081
- * - `queue` — subject the dispatch to a registered flow-control queue (concurrency / rate limit).
1082
- * - `priority` — admission priority within that queue; higher is admitted first when a slot is
1083
- * contended (default 0). No effect without a `queue`.
1084
- * - `fairnessKey` — the fairness bucket for a queue with `fairness: 'key'` (e.g. a tenant id);
1085
- * the queue round-robins across distinct keys so one key can't monopolize the budget. Defaults
1086
- * to the run id when omitted. No effect without a `queue`.
1087
- * - `transport` — pin the dispatch to a named transport in the pool (else the pool's first, with
1088
- * failover to the rest). See `engine.registerQueue` / the engine's `transports` option.
1089
- */
1090
- call<TInput, TOutput>(step: RemoteStepDef<TInput, TOutput>, input: TInput, opts?: {
1091
- queue?: string;
1092
- priority?: number;
1093
- fairnessKey?: string;
1094
- transport?: string;
1095
- }): Promise<TOutput>;
1096
1308
  /**
1097
1309
  * Durable sleep: suspends the run for `duration` (e.g. `'30s'`, `'2h'`, `'7 days'`, or ms as a
1098
1310
  * number) without consuming resources, resuming automatically once the timer is due — even
@@ -1222,18 +1434,25 @@ interface WorkflowCtx {
1222
1434
  */
1223
1435
  patched(id: string): Promise<boolean>;
1224
1436
  /**
1225
- * Deterministic wall-clock (epoch ms): records the time on the first run and replays the SAME
1226
- * value afterwards. Use this instead of `Date.now()` inside a workflow — a raw `Date.now()` returns
1227
- * a different value on every replay, which silently corrupts a durable run.
1437
+ * Deterministic wall-clock, epoch **milliseconds** (like `Date.now()`): records the time on the
1438
+ * first run and replays the SAME value afterwards. Use this instead of `Date.now()`/`new Date()`
1439
+ * inside a workflow — a raw clock read returns a different value on every replay, which silently
1440
+ * corrupts a durable run. Returns a number so it composes in arithmetic (deadlines, elapsed);
1441
+ * for an ISO string do `new Date(await ctx.now()).toISOString()`. Every step is dispatched now
1442
+ * (see {@link step}), so a trivial timestamp capture gets this lightweight deterministic helper
1443
+ * instead of a full `@Step` + worker round-trip.
1228
1444
  */
1229
1445
  now(): Promise<number>;
1230
1446
  /**
1231
- * Deterministic random in `[0, 1)`: recorded once, then replayed. Use instead of `Math.random()`
1232
- * (same replay-safety reason as {@link now}).
1447
+ * **Deterministic capture.** Run `fn` once, checkpoint its result, and on replay return the SAME
1448
+ * value WITHOUT re-running `fn` — the durable way to bring a non-deterministic value into a workflow
1449
+ * where you control the generator: `ctx.sideEffect(() => uuidv7())`, `() => ulid()`,
1450
+ * `() => Math.random()`, a config/env read. Prefer a {@link step} for real work with side effects
1451
+ * (a DB write, an API call): `fn` here runs only once and MUST be effectively pure (it produces a
1452
+ * value; it is not re-executed on replay), like Temporal's `sideEffect`. For a plain timestamp use
1453
+ * {@link now}.
1233
1454
  */
1234
- random(): Promise<number>;
1235
- /** Deterministic UUID v4: recorded once, then replayed. Use instead of `crypto.randomUUID()`. */
1236
- uuid(): Promise<string>;
1455
+ sideEffect<TValue>(fn: () => TValue | Promise<TValue>): Promise<TValue>;
1237
1456
  /**
1238
1457
  * Merge `attrs` into THIS run's {@link WorkflowRun.searchAttributes} — the indexed metadata the
1239
1458
  * dashboard and {@link RunQuery} filter on. Shallow merge: keys you don't pass are kept. Durable +
@@ -1274,6 +1493,10 @@ interface EngineEvent {
1274
1493
  type: EngineEventType;
1275
1494
  runId: string;
1276
1495
  workflow?: string | undefined;
1496
+ /** The run's worker-pool partition (see {@link WorkflowRun.namespace}), stamped on the `run.*`
1497
+ * lifecycle events (where the emitting call site already has the run in hand) so a tenant-event
1498
+ * re-publisher can scope without a per-event store read. Absent on `step.*` events. */
1499
+ namespace?: string | undefined;
1277
1500
  seq?: number | undefined;
1278
1501
  name?: string | undefined;
1279
1502
  kind?: StepKind | undefined;
@@ -1309,6 +1532,17 @@ interface StepInvocation {
1309
1532
  */
1310
1533
  type StepInterceptor = (invocation: StepInvocation, next: () => Promise<unknown>) => Promise<unknown>;
1311
1534
 
1535
+ /**
1536
+ * {@link WorkflowCtx} plus the internal in-process local-step primitive — used ONLY by the library's
1537
+ * own built-in workflows (the durable-entity runner) that need a checkpointed, in-process step the
1538
+ * public `ctx.step` (always-dispatched, no in-process placement) no longer provides. Every
1539
+ * `createWorkflowCtx` result actually carries `localStep` at runtime; this type just names the wider
1540
+ * shape for the internal consumers that need it, without widening the public {@link WorkflowCtx}.
1541
+ */
1542
+ interface InternalWorkflowCtx extends WorkflowCtx {
1543
+ localStep<TOutput>(name: string, fn: (log: StepLogger) => Promise<TOutput>, options?: StepOptions): Promise<TOutput>;
1544
+ }
1545
+
1312
1546
  /** One operation on a durable entity: mutate `state` in place and/or return a result. */
1313
1547
  type EntityHandler<S = unknown> = (state: S, arg: unknown) => unknown | Promise<unknown>;
1314
1548
  /** A durable keyed entity (a virtual object): per-key serialized handlers over durable state. */
@@ -1320,7 +1554,7 @@ interface EntityConfig<S = unknown> {
1320
1554
  }
1321
1555
  /** The slice of the engine the entity subsystem needs (so it stays a standalone module). */
1322
1556
  interface EntityHost {
1323
- register(name: string, version: string, fn: (ctx: WorkflowCtx, input: unknown) => Promise<unknown>): void;
1557
+ register(name: string, version: string, fn: (ctx: InternalWorkflowCtx, input: unknown) => Promise<unknown>): void;
1324
1558
  signalWithStart(workflow: string, input: unknown, runId: string, signal: {
1325
1559
  token: string;
1326
1560
  payload?: unknown;
@@ -1390,7 +1624,7 @@ declare class EventAccumulators {
1390
1624
  private batch;
1391
1625
  }
1392
1626
 
1393
- type WorkflowFn = (ctx: WorkflowCtx, input: unknown) => Promise<unknown>;
1627
+ type WorkflowFn = (ctx: InternalWorkflowCtx, input: unknown) => Promise<unknown>;
1394
1628
  /** Options for {@link WorkflowEngine.start}. */
1395
1629
  interface StartOptions {
1396
1630
  /** Run-scoped tags, merged with the workflow's static `@Workflow({ tags })` onto the run. */
@@ -1402,6 +1636,12 @@ interface StartOptions {
1402
1636
  * the engine dispatches to advance it. Higher wins; absent = unprioritised. See {@link WorkflowRun.priority}.
1403
1637
  */
1404
1638
  priority?: number | undefined;
1639
+ /**
1640
+ * Override the namespace stamped on the run. Defaults to the engine's own `namespace`. Set by the
1641
+ * control plane when it starts a run on behalf of a tenant (a {@link StartRunMessage}'s `tenant`
1642
+ * maps here), so one DB-less-tenant-facing engine can create runs across many namespaces.
1643
+ */
1644
+ namespace?: string | undefined;
1405
1645
  }
1406
1646
  /**
1407
1647
  * Serialize runs of a workflow that share a key — a durable, FIFO mutex (e.g. one pipeline per base).
@@ -1428,7 +1668,7 @@ interface WorkflowEngineDeps {
1428
1668
  transport?: Transport | undefined;
1429
1669
  /**
1430
1670
  * An ordered pool of named transports. The engine dispatches on the first and fails over to the
1431
- * next on a dispatch error; a step pins one via `ctx.call(step, input, { transport: id })`. Use
1671
+ * next on a dispatch error; a step pins one via `ctx.step(step, input, { transport: id })`. Use
1432
1672
  * this instead of `transport` for failover / multi-broker setups.
1433
1673
  */
1434
1674
  transports?: NamedTransport[] | undefined;
@@ -1441,7 +1681,7 @@ interface WorkflowEngineDeps {
1441
1681
  /** Epoch-ms clock; injectable for tests. Defaults to `Date.now`. */
1442
1682
  clock?: (() => number) | undefined;
1443
1683
  /**
1444
- * Flow-control admission backend for `ctx.call(step, input, { queue })`. Defaults to an in-process
1684
+ * Flow-control admission backend for `ctx.step(step, input, { queue })`. Defaults to an in-process
1445
1685
  * {@link InMemoryAdmissionBackend} (per-instance caps). Inject a store/Redis-backed backend to make
1446
1686
  * concurrency / rate-limit / ordering GLOBAL across engine replicas.
1447
1687
  */
@@ -1451,8 +1691,11 @@ interface WorkflowEngineDeps {
1451
1691
  /**
1452
1692
  * Worker-pool partition for this engine. Stamped on every run it creates; the poll paths
1453
1693
  * (`runPending`/`recoverIncomplete`/`resumeDueTimers`/`sweepTimeouts`) only act on runs in this
1454
- * namespace. Default `'default'` — byte-identical to a single-pool deployment. Set distinct values
1455
- * to safely share ONE state store across non-interchangeable pools (e.g. local dev vs a cluster).
1694
+ * namespace. Set distinct values to safely share ONE state store across non-interchangeable pools
1695
+ * (e.g. local dev vs a cluster). **Omit it to make this engine an OPERATOR** — an unset namespace
1696
+ * drives/recovers/resumes runs of EVERY namespace (the poll filters no-op, the resume/decision
1697
+ * guards fall through) and leaves the transport on its bare prefix. This is the hosted control
1698
+ * plane: "ver tudo = ausência de namespace".
1456
1699
  */
1457
1700
  namespace?: string | undefined;
1458
1701
  /** Recovery lease duration in ms — how long this instance owns a run it picked up. Default 30s. */
@@ -1695,7 +1938,7 @@ declare class WorkflowEngine {
1695
1938
  validateInput?: (input: unknown) => void | Promise<void>;
1696
1939
  }): void;
1697
1940
  /**
1698
- * Register a flow-control queue referenced by `ctx.call(step, input, { queue })`. Caps concurrent
1941
+ * Register a flow-control queue referenced by `ctx.step(step, input, { queue })`. Caps concurrent
1699
1942
  * in-flight steps and/or the admission rate; blocked calls re-suspend and retry, so the limit is
1700
1943
  * durable. Per engine instance (see {@link QueueConfig}). Registering the same name replaces it.
1701
1944
  */
@@ -1767,6 +2010,17 @@ declare class WorkflowEngine {
1767
2010
  * its identity stays its own.
1768
2011
  */
1769
2012
  private synthesizeRemoteChild;
2013
+ /**
2014
+ * Attempt to route an unregistered workflow to the live worker group of the same name: if the
2015
+ * transport reports a live heartbeat for group `run.workflow`, synthesize a throwaway
2016
+ * {@link RegisteredWorkflow} that dispatches to that group over the primary transport. Returns
2017
+ * `undefined` when the group is not live. Always attempted — there is no opt-in flag; an explicit
2018
+ * `register`/`registerRemote` still takes precedence (this only runs when nothing is registered).
2019
+ *
2020
+ * Recomputed per call (never memoized into `this.workflows`) so it stays correct if group
2021
+ * liveness changes between calls, exactly as {@link synthesizeRemoteChild} does.
2022
+ */
2023
+ private resolveRemoteByConvention;
1770
2024
  /**
1771
2025
  * Walk the parent chain of `childRunId` to the nearest ancestor registered as REMOTE, returning its
1772
2026
  * registration (or `undefined` if there is none). The parent of an awaited child is found via its
@@ -2236,25 +2490,17 @@ type StepHandler = (input: unknown, log: StepLogger) => Promise<unknown> | unkno
2236
2490
  */
2237
2491
  declare function runStepHandler(task: RemoteTask, handler: StepHandler | undefined): Promise<StepResult>;
2238
2492
 
2239
- interface RemoteStepConfig<TInput, TOutput> extends StepOptions {
2240
- name: string;
2241
- /** Worker group expected to handle this step. Defaults to the `name` prefix before the first dot. */
2242
- group?: string;
2243
- input: z.ZodType<TInput>;
2244
- output: z.ZodType<TOutput>;
2245
- }
2246
- /**
2247
- * Defines a typed handle to a step that runs on a remote worker. Call it from a workflow with
2248
- * `ctx.call(step, input)`; a worker registers a handler under the same `name`.
2249
- */
2250
- declare function remoteStep<TInput, TOutput>(config: RemoteStepConfig<TInput, TOutput>): RemoteStepDef<TInput, TOutput>;
2251
-
2252
2493
  /**
2253
2494
  * A {@link WorkflowExecutor} backed by a {@link Transport}: it advances a remote workflow by
2254
2495
  * DISPATCHING a {@link WorkflowTask} over the broker under the ENGINE-SUPPLIED `taskId`, WITHOUT
2255
2496
  * awaiting the decision. Pass one to `engine.registerRemote(name, version, { group, executor })` so a
2256
2497
  * workflow authored in another SDK (e.g. the Python `durable-worker`) is driven over Redis/BullMQ.
2257
2498
  *
2499
+ * Routes by NAME, not a hand-declared group: the dispatched task's routing token is
2500
+ * `tenantGroup(sanitizeQueueToken(name), partition)` — the same formula a {@link import('./interfaces').StepDef}
2501
+ * uses — so `partition` is an optional isolation suffix, not a separate identity. Omit it to route by
2502
+ * the bare (sanitized) `name`.
2503
+ *
2258
2504
  * Multi-instance safe by construction: the executor holds NO in-memory state correlating a dispatched
2259
2505
  * turn to its decision. The engine records the `taskId` on the run as the awaited marker AND releases
2260
2506
  * the run's lease BEFORE calling `dispatch`, so the worker's {@link import('./interfaces').WorkflowDecision},
@@ -2271,8 +2517,9 @@ declare function remoteStep<TInput, TOutput>(config: RemoteStepConfig<TInput, TO
2271
2517
  */
2272
2518
  declare class RemoteWorkflowExecutor implements WorkflowExecutor {
2273
2519
  private readonly transport;
2274
- private readonly group;
2275
- constructor(transport: Transport, group: string, _opts?: {
2520
+ private readonly name;
2521
+ private readonly partition?;
2522
+ constructor(transport: Transport, name: string, partition?: string | undefined, _opts?: {
2276
2523
  timeoutMs?: number;
2277
2524
  });
2278
2525
  /**
@@ -2282,6 +2529,35 @@ declare class RemoteWorkflowExecutor implements WorkflowExecutor {
2282
2529
  dispatch(run: WorkflowRun, history: HistoryEvent[], taskId: string, pendingSignals?: WorkflowTask['pendingSignals']): Promise<void>;
2283
2530
  }
2284
2531
 
2532
+ /** A run + its timeline + child ids — the detail view. Mirrors the dashboard's `RunDetail`. */
2533
+ interface RunDetail {
2534
+ run: WorkflowRun;
2535
+ /** Steps in execution order (local + remote). */
2536
+ timeline: StepCheckpoint[];
2537
+ /** Ids of runs this run spawned (parent→children tree). */
2538
+ children: string[];
2539
+ }
2540
+ /**
2541
+ * The bounded read/control/stream surface a consumer (e.g. a controller) needs, satisfied by BOTH
2542
+ * topologies: the control plane binds a store-backed impl (reuses `DashboardService`); a tenant binds
2543
+ * a `ProxyRunGateway` that round-trips over the transport. Deliberately smaller than the full dashboard
2544
+ * (no metrics/bulk/workerHealth/update/signal) — those stay control-plane-only.
2545
+ */
2546
+ interface RunGateway {
2547
+ getRunDetail(runId: string): Promise<RunDetail | null>;
2548
+ listRuns(query: RunQuery): Promise<WorkflowRun[]>;
2549
+ cancel(runId: string, opts?: {
2550
+ compensate?: boolean;
2551
+ }): Promise<RunResult | null>;
2552
+ retry(runId: string): Promise<RunResult | null>;
2553
+ continue(runId: string): Promise<RunResult | null>;
2554
+ retryWithInput(runId: string, input: unknown): Promise<{
2555
+ runId: string;
2556
+ } | null>;
2557
+ /** Live lifecycle events for one run; returns an unsubscribe fn. Framework-agnostic (no rxjs). */
2558
+ subscribe(runId: string, onEvent: (event: EngineEvent) => void): () => void;
2559
+ }
2560
+
2285
2561
  /**
2286
2562
  * Transforms a payload value as it crosses the store boundary — e.g. encrypt-at-rest, compress, or
2287
2563
  * redact PII. `decode` must be the exact inverse of `encode`. Both are synchronous (use a local
@@ -2512,6 +2788,23 @@ declare function attributeColumnFor(filter: AttributeFilter): 'numValue' | 'strV
2512
2788
  /** The literal a side-table predicate compares against (booleans → `"true"`/`"false"` strings). */
2513
2789
  declare function attributeOperand(filter: AttributeFilter): string | number;
2514
2790
 
2791
+ /**
2792
+ * Compute the worker group a tenant's run dispatches to. Mirrors the transport's
2793
+ * default-is-bare rule (see {@link TransportPool.useNamespace}'s `'default'` no-op): a
2794
+ * `tenant` of `undefined`, `''`, or `'default'` is the bare `baseGroup`, so a single-tenant
2795
+ * deployment (or the `'default'` tenant of a multi-tenant one) stays byte-identical to today.
2796
+ * Any other tenant suffixes the group as `<baseGroup>@<tenant>`, so an operator control plane
2797
+ * (namespace: undefined) can route each tenant's run to its own worker pool.
2798
+ */
2799
+ declare function tenantGroup(baseGroup: string, tenant: string | undefined): string;
2800
+ /**
2801
+ * Sanitize a handler/workflow name for use as a BullMQ queue token: BullMQ forbids `:` in queue
2802
+ * names (it uses the colon internally as a key separator), so every `:` is replaced with `-`. `.`
2803
+ * is legal in a BullMQ queue name and is left as-is — step/workflow names commonly use it as a
2804
+ * namespacing separator (e.g. `payments.charge-card`).
2805
+ */
2806
+ declare function sanitizeQueueToken(name: string): string;
2807
+
2515
2808
  /**
2516
2809
  * Dependency-injection tokens. Adapters bind their implementations to these so `core` and
2517
2810
  * `@dudousxd/nestjs-durable` depend only on the interfaces in `./interfaces`.
@@ -2642,4 +2935,4 @@ declare class InMemoryTransport implements Transport, ControlPlane {
2642
2935
  onControl(handler: (msg: ControlMessage) => void): void;
2643
2936
  }
2644
2937
 
2645
- export { type AccumulatorHost, type Admission, type AdmissionBackend, type AdmissionItem, type AttributeFilter, type AttributeOp, type BackoffStrategy, type ChildCallOptions, CodecStateStore, ContinueAsNew, type ControlMessage, type ControlPlane, DURABLE_OPTIONS, DURABLE_OPTIONS_CANONICAL, type DurableWebhook, type EngineEvent, type EngineEventType, type EngineListener, Entities, type EntityConfig, type EntityHandler, type EntityHost, EventAccumulators, type EventBatchConfig, FatalError, GatherError, type GroupHealth, type Heartbeat, type HistoryEvent, InMemoryAdmissionBackend, InMemoryStateStore, InMemoryTransport, type MetricsCollector, type MetricsSnapshot, type NamedTransport, NonDeterminismError, type PayloadCodec, type QueueConfig, QueueController, type RemoteStepConfig, type RemoteStepDef, RemoteStepError, RemoteStepTimeout, type RemoteTask, RemoteWorkflowExecutor, RemoteWorkflowTimeout, type RetentionPolicy, type RunAttributeRow, type RunDispatcher, type RunQuery, type RunResult, type RunSchedulesOptions, type RunStatus, STATE_STORE, STATE_STORE_CANONICAL, type ScheduledWorkflow, type SearchAttributes, SignalTimeoutError, type SignalWaiter, type SingletonConfig, SingletonQueueFullError, type StartOptions, type StateStore, type StepCheckpoint, type StepError, type StepEvent, type StepHandler, type StepInterceptor, type StepInvocation, type StepKind, type StepLogger, type StepOptions, type StepResult, type StoreTransaction, type SubProcessHandle, TERMINAL_RUN_STATUSES, TRANSPORT, TRANSPORT_CANONICAL, type Transport, type UpdateResult, type UpdateValidator, WORKFLOW_NAME_KEY, type WorkerAdjust, type WorkerConcurrencyStatus, type WorkerHeartbeat, type WorkerStatus, type WorkflowClass, type WorkflowCommand, type WorkflowCtx, type WorkflowDecision, WorkflowEngine, type WorkflowEngineDeps, type WorkflowExecutor, type WorkflowInputOf, type WorkflowOutputOf, type WorkflowRef, type WorkflowRun, type WorkflowStepEvent, WorkflowSuspended, type WorkflowTask, applyAttributeQuery, attributeColumnFor, attributeOperand, breakpointToken, collectMetrics, createStepLogger, eventMatchOf, eventMatches, eventPrefix, eventToken, matchesAttributes, normalizeAttributeRows, parseDuration, prevCronFireMs, remoteStep, runSchedules, runStepHandler, scheduledRunId, sqlComparator, stepId, workflowName };
2938
+ export { type AccumulatorHost, type Admission, type AdmissionBackend, type AdmissionItem, type AttributeFilter, type AttributeOp, type BackoffStrategy, type ChildCallOptions, CodecStateStore, ContinueAsNew, type ControlMessage, type ControlPlane, DURABLE_OPTIONS, DURABLE_OPTIONS_CANONICAL, DURABLE_STEP_CONFIG, DURABLE_STEP_NAME, type DurableWebhook, type EngineEvent, type EngineEventType, type EngineListener, Entities, type EntityConfig, type EntityHandler, type EntityHost, EventAccumulators, type EventBatchConfig, FatalError, GatherError, type GroupHealth, type Heartbeat, type HistoryEvent, InMemoryAdmissionBackend, InMemoryStateStore, InMemoryTransport, type MetricsCollector, type MetricsSnapshot, type NamedTransport, NonDeterminismError, type PayloadCodec, type QueueConfig, QueueController, RemoteStepError, RemoteStepTimeout, type RemoteTask, RemoteWorkflowExecutor, RemoteWorkflowTimeout, type RetentionPolicy, type RunAttributeRow, type RunDetail, type RunDispatcher, type RunGateway, type RunQuery, type RunReply, type RunReplyResult, type RunRequest, type RunRequestKind, type RunResult, type RunSchedulesOptions, type RunStatus, STATE_STORE, STATE_STORE_CANONICAL, type ScheduledWorkflow, type SearchAttributes, SignalTimeoutError, type SignalWaiter, type SingletonConfig, SingletonQueueFullError, type StartOptions, type StartRunMessage, type StateStore, type StepCheckpoint, type StepConfig, type StepDef, type StepDispatchOpts, type StepError, type StepEvent, type StepHandler, type StepInterceptor, type StepInvocation, type StepKind, type StepLogger, type StepOptions, type StepRef, type StepResult, type StoreTransaction, type SubProcessHandle, TERMINAL_RUN_STATUSES, TRANSPORT, TRANSPORT_CANONICAL, type TenantEvent, type Transport, type UpdateResult, type UpdateValidator, WORKFLOW_NAME_KEY, type WorkerAdjust, type WorkerConcurrencyStatus, type WorkerHeartbeat, type WorkerStatus, type WorkflowClass, type WorkflowCommand, type WorkflowCtx, type WorkflowDecision, WorkflowEngine, type WorkflowEngineDeps, type WorkflowExecutor, type WorkflowInputOf, type WorkflowOutputOf, type WorkflowRef, type WorkflowRun, type WorkflowStepEvent, WorkflowSuspended, type WorkflowTask, applyAttributeQuery, attributeColumnFor, attributeOperand, breakpointToken, collectMetrics, createStepLogger, eventMatchOf, eventMatches, eventPrefix, eventToken, matchesAttributes, normalizeAttributeRows, parseDuration, prevCronFireMs, runSchedules, runStepHandler, sanitizeQueueToken, scheduledRunId, sqlComparator, stepConfigOf, stepId, stepNameOf, tenantGroup, workflowName };