@dudousxd/nestjs-durable-core 0.44.0 → 0.45.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.cts CHANGED
@@ -802,17 +802,17 @@ interface WorkflowExecutor {
802
802
  */
803
803
  advance?(run: WorkflowRun, history: HistoryEvent[], pendingSignals?: WorkflowTask['pendingSignals']): Promise<WorkflowDecision>;
804
804
  /**
805
- * DISPATCH-AND-SUSPEND (broker-backed, multi-instance safe): dispatch the turn to a worker and
806
- * return the dispatched `taskId` WITHOUT awaiting the decision. The engine then suspends the run,
807
- * recording the `taskId` on it; the worker's {@link WorkflowDecision} comes back over the
808
- * transport's {@link Transport.onDecision} and is applied DURABLY by whatever engine instance
809
- * receives it ({@link WorkflowEngine.completeRemoteDecision} looks the run up by `decision.runId`).
810
- * No in-memory await, so the decision is never lost to a point-to-point broker handing it to a
811
- * non-dispatching instance. Provide this OR {@link advance} — exactly one.
812
- */
813
- dispatch?(run: WorkflowRun, history: HistoryEvent[], pendingSignals?: WorkflowTask['pendingSignals']): Promise<{
814
- taskId: string;
815
- }>;
805
+ * DISPATCH-AND-SUSPEND (broker-backed, multi-instance safe): enqueue the turn to a worker under the
806
+ * ENGINE-SUPPLIED `taskId`, WITHOUT awaiting the decision. The engine generates `taskId`, records it
807
+ * on the run as the awaited marker, AND releases the run's lease — all BEFORE calling this — so the
808
+ * worker's {@link WorkflowDecision} (delivered over {@link Transport.onDecision} and applied DURABLY
809
+ * by {@link WorkflowEngine.completeRemoteDecision}, which looks the run up by `decision.runId`) can
810
+ * never arrive ahead of its marker or contend with a still-held lease and be dropped. This method
811
+ * therefore ONLY enqueues — it owns no taskId generation and returns nothing. No in-memory await, so
812
+ * the decision is never lost to a point-to-point broker handing it to a non-dispatching instance.
813
+ * Provide this OR {@link advance} — exactly one.
814
+ */
815
+ dispatch?(run: WorkflowRun, history: HistoryEvent[], taskId: string, pendingSignals?: WorkflowTask['pendingSignals']): Promise<void>;
816
816
  }
817
817
  /**
818
818
  * A transport in an ordered pool, identified by `id`. The engine dispatches on the first by default
@@ -840,6 +840,13 @@ interface Transport {
840
840
  onResult(handler: (result: StepResult) => Promise<void>): void;
841
841
  /** worker → engine: liveness signal for an in-flight long step. */
842
842
  onHeartbeat(handler: (beat: Heartbeat) => Promise<void>): void;
843
+ /**
844
+ * Partition this transport's queues/keys by `namespace` (matching the engine's store namespace), so
845
+ * multiple logical deployments can share one broker. Idempotent. An explicit namespace passed to the
846
+ * transport's constructor TAKES PRECEDENCE over this. No-op when `namespace === "default"`. Optional —
847
+ * transports that don't partition simply omit it.
848
+ */
849
+ useNamespace?(namespace: string): void;
843
850
  /** Release the transport's resources (broker workers, queues, connections) for a clean shutdown.
844
851
  * Optional — an in-process transport has nothing to close. Called on `onApplicationShutdown`
845
852
  * after the engine drains, so a deploy hands off instead of leaving the broker to time out. */
@@ -2244,15 +2251,16 @@ declare function remoteStep<TInput, TOutput>(config: RemoteStepConfig<TInput, TO
2244
2251
 
2245
2252
  /**
2246
2253
  * A {@link WorkflowExecutor} backed by a {@link Transport}: it advances a remote workflow by
2247
- * DISPATCHING a {@link WorkflowTask} over the broker and returning the dispatched `taskId` WITHOUT
2254
+ * DISPATCHING a {@link WorkflowTask} over the broker under the ENGINE-SUPPLIED `taskId`, WITHOUT
2248
2255
  * awaiting the decision. Pass one to `engine.registerRemote(name, version, { group, executor })` so a
2249
2256
  * workflow authored in another SDK (e.g. the Python `durable-worker`) is driven over Redis/BullMQ.
2250
2257
  *
2251
2258
  * Multi-instance safe by construction: the executor holds NO in-memory state correlating a dispatched
2252
- * turn to its decision. The engine suspends the run recording the dispatched `taskId`, and the
2253
- * worker's {@link import('./interfaces').WorkflowDecision} is delivered over the transport's
2254
- * `onDecision` and applied DURABLY (by run id) on whatever engine instance consumes it — even if that
2255
- * is not the instance that dispatched the turn. A point-to-point broker can therefore hand the
2259
+ * turn to its decision. The engine records the `taskId` on the run as the awaited marker AND releases
2260
+ * the run's lease BEFORE calling `dispatch`, so the worker's {@link import('./interfaces').WorkflowDecision},
2261
+ * delivered over the transport's `onDecision` and applied DURABLY (by run id) on whatever engine
2262
+ * instance consumes it — even one that did not dispatch the turn — can never arrive ahead of its marker
2263
+ * or contend with a still-held lease and be dropped. A point-to-point broker can therefore hand the
2256
2264
  * decision to any instance without it being dropped (the bug this design fixes).
2257
2265
  *
2258
2266
  * Recovery-safe: if the engine crashes (or a decision is genuinely lost because the worker died), the
@@ -2268,12 +2276,10 @@ declare class RemoteWorkflowExecutor implements WorkflowExecutor {
2268
2276
  timeoutMs?: number;
2269
2277
  });
2270
2278
  /**
2271
- * Dispatch one workflow turn and return its `taskId`. Does NOT await the decision — the engine
2272
- * suspends the run on the returned `taskId` and applies the decision durably via `onDecision`.
2279
+ * Enqueue one workflow turn under the engine-supplied `taskId`. Does NOT await the decision — the
2280
+ * engine has already suspended the run on `taskId` and applies the decision durably via `onDecision`.
2273
2281
  */
2274
- dispatch(run: WorkflowRun, history: HistoryEvent[], pendingSignals?: WorkflowTask['pendingSignals']): Promise<{
2275
- taskId: string;
2276
- }>;
2282
+ dispatch(run: WorkflowRun, history: HistoryEvent[], taskId: string, pendingSignals?: WorkflowTask['pendingSignals']): Promise<void>;
2277
2283
  }
2278
2284
 
2279
2285
  /**
package/dist/index.d.ts CHANGED
@@ -802,17 +802,17 @@ interface WorkflowExecutor {
802
802
  */
803
803
  advance?(run: WorkflowRun, history: HistoryEvent[], pendingSignals?: WorkflowTask['pendingSignals']): Promise<WorkflowDecision>;
804
804
  /**
805
- * DISPATCH-AND-SUSPEND (broker-backed, multi-instance safe): dispatch the turn to a worker and
806
- * return the dispatched `taskId` WITHOUT awaiting the decision. The engine then suspends the run,
807
- * recording the `taskId` on it; the worker's {@link WorkflowDecision} comes back over the
808
- * transport's {@link Transport.onDecision} and is applied DURABLY by whatever engine instance
809
- * receives it ({@link WorkflowEngine.completeRemoteDecision} looks the run up by `decision.runId`).
810
- * No in-memory await, so the decision is never lost to a point-to-point broker handing it to a
811
- * non-dispatching instance. Provide this OR {@link advance} — exactly one.
812
- */
813
- dispatch?(run: WorkflowRun, history: HistoryEvent[], pendingSignals?: WorkflowTask['pendingSignals']): Promise<{
814
- taskId: string;
815
- }>;
805
+ * DISPATCH-AND-SUSPEND (broker-backed, multi-instance safe): enqueue the turn to a worker under the
806
+ * ENGINE-SUPPLIED `taskId`, WITHOUT awaiting the decision. The engine generates `taskId`, records it
807
+ * on the run as the awaited marker, AND releases the run's lease — all BEFORE calling this — so the
808
+ * worker's {@link WorkflowDecision} (delivered over {@link Transport.onDecision} and applied DURABLY
809
+ * by {@link WorkflowEngine.completeRemoteDecision}, which looks the run up by `decision.runId`) can
810
+ * never arrive ahead of its marker or contend with a still-held lease and be dropped. This method
811
+ * therefore ONLY enqueues — it owns no taskId generation and returns nothing. No in-memory await, so
812
+ * the decision is never lost to a point-to-point broker handing it to a non-dispatching instance.
813
+ * Provide this OR {@link advance} — exactly one.
814
+ */
815
+ dispatch?(run: WorkflowRun, history: HistoryEvent[], taskId: string, pendingSignals?: WorkflowTask['pendingSignals']): Promise<void>;
816
816
  }
817
817
  /**
818
818
  * A transport in an ordered pool, identified by `id`. The engine dispatches on the first by default
@@ -840,6 +840,13 @@ interface Transport {
840
840
  onResult(handler: (result: StepResult) => Promise<void>): void;
841
841
  /** worker → engine: liveness signal for an in-flight long step. */
842
842
  onHeartbeat(handler: (beat: Heartbeat) => Promise<void>): void;
843
+ /**
844
+ * Partition this transport's queues/keys by `namespace` (matching the engine's store namespace), so
845
+ * multiple logical deployments can share one broker. Idempotent. An explicit namespace passed to the
846
+ * transport's constructor TAKES PRECEDENCE over this. No-op when `namespace === "default"`. Optional —
847
+ * transports that don't partition simply omit it.
848
+ */
849
+ useNamespace?(namespace: string): void;
843
850
  /** Release the transport's resources (broker workers, queues, connections) for a clean shutdown.
844
851
  * Optional — an in-process transport has nothing to close. Called on `onApplicationShutdown`
845
852
  * after the engine drains, so a deploy hands off instead of leaving the broker to time out. */
@@ -2244,15 +2251,16 @@ declare function remoteStep<TInput, TOutput>(config: RemoteStepConfig<TInput, TO
2244
2251
 
2245
2252
  /**
2246
2253
  * A {@link WorkflowExecutor} backed by a {@link Transport}: it advances a remote workflow by
2247
- * DISPATCHING a {@link WorkflowTask} over the broker and returning the dispatched `taskId` WITHOUT
2254
+ * DISPATCHING a {@link WorkflowTask} over the broker under the ENGINE-SUPPLIED `taskId`, WITHOUT
2248
2255
  * awaiting the decision. Pass one to `engine.registerRemote(name, version, { group, executor })` so a
2249
2256
  * workflow authored in another SDK (e.g. the Python `durable-worker`) is driven over Redis/BullMQ.
2250
2257
  *
2251
2258
  * Multi-instance safe by construction: the executor holds NO in-memory state correlating a dispatched
2252
- * turn to its decision. The engine suspends the run recording the dispatched `taskId`, and the
2253
- * worker's {@link import('./interfaces').WorkflowDecision} is delivered over the transport's
2254
- * `onDecision` and applied DURABLY (by run id) on whatever engine instance consumes it — even if that
2255
- * is not the instance that dispatched the turn. A point-to-point broker can therefore hand the
2259
+ * turn to its decision. The engine records the `taskId` on the run as the awaited marker AND releases
2260
+ * the run's lease BEFORE calling `dispatch`, so the worker's {@link import('./interfaces').WorkflowDecision},
2261
+ * delivered over the transport's `onDecision` and applied DURABLY (by run id) on whatever engine
2262
+ * instance consumes it — even one that did not dispatch the turn — can never arrive ahead of its marker
2263
+ * or contend with a still-held lease and be dropped. A point-to-point broker can therefore hand the
2256
2264
  * decision to any instance without it being dropped (the bug this design fixes).
2257
2265
  *
2258
2266
  * Recovery-safe: if the engine crashes (or a decision is genuinely lost because the worker died), the
@@ -2268,12 +2276,10 @@ declare class RemoteWorkflowExecutor implements WorkflowExecutor {
2268
2276
  timeoutMs?: number;
2269
2277
  });
2270
2278
  /**
2271
- * Dispatch one workflow turn and return its `taskId`. Does NOT await the decision — the engine
2272
- * suspends the run on the returned `taskId` and applies the decision durably via `onDecision`.
2279
+ * Enqueue one workflow turn under the engine-supplied `taskId`. Does NOT await the decision — the
2280
+ * engine has already suspended the run on `taskId` and applies the decision durably via `onDecision`.
2273
2281
  */
2274
- dispatch(run: WorkflowRun, history: HistoryEvent[], pendingSignals?: WorkflowTask['pendingSignals']): Promise<{
2275
- taskId: string;
2276
- }>;
2282
+ dispatch(run: WorkflowRun, history: HistoryEvent[], taskId: string, pendingSignals?: WorkflowTask['pendingSignals']): Promise<void>;
2277
2283
  }
2278
2284
 
2279
2285
  /**
package/dist/index.js CHANGED
@@ -701,7 +701,6 @@ function eventMatches(payload, match) {
701
701
  __name(eventMatches, "eventMatches");
702
702
 
703
703
  // src/remote-workflow-executor.ts
704
- var taskCounter = 0;
705
704
  var RemoteWorkflowExecutor = class {
706
705
  static {
707
706
  __name(this, "RemoteWorkflowExecutor");
@@ -713,15 +712,13 @@ var RemoteWorkflowExecutor = class {
713
712
  this.group = group;
714
713
  }
715
714
  /**
716
- * Dispatch one workflow turn and return its `taskId`. Does NOT await the decision — the engine
717
- * suspends the run on the returned `taskId` and applies the decision durably via `onDecision`.
715
+ * Enqueue one workflow turn under the engine-supplied `taskId`. Does NOT await the decision — the
716
+ * engine has already suspended the run on `taskId` and applies the decision durably via `onDecision`.
718
717
  */
719
- async dispatch(run, history, pendingSignals) {
718
+ async dispatch(run, history, taskId, pendingSignals) {
720
719
  if (!this.transport.dispatchWorkflowTask) {
721
720
  throw new Error("transport does not support workflow tasks (dispatchWorkflowTask)");
722
721
  }
723
- taskCounter += 1;
724
- const taskId = `${run.id}:wf:${taskCounter}`;
725
722
  const task = {
726
723
  taskId,
727
724
  runId: run.id,
@@ -737,9 +734,6 @@ var RemoteWorkflowExecutor = class {
737
734
  attempt: 1
738
735
  };
739
736
  await this.transport.dispatchWorkflowTask(task);
740
- return {
741
- taskId
742
- };
743
737
  }
744
738
  };
745
739
 
@@ -919,6 +913,17 @@ var TransportPool = class {
919
913
  ...groups
920
914
  ];
921
915
  }
916
+ /**
917
+ * Propagate the engine's `namespace` to every transport that partitions by it — so the same
918
+ * namespace that scopes the store also scopes each transport's queues/keys. A transport that
919
+ * doesn't partition (no `useNamespace`) is skipped. Idempotent; a transport given an explicit
920
+ * namespace at construction ignores this (see {@link Transport.useNamespace}).
921
+ */
922
+ useNamespace(namespace) {
923
+ for (const { transport } of this.transports) {
924
+ transport.useNamespace?.(namespace);
925
+ }
926
+ }
922
927
  /** Pinned `preferId` first, then the rest (failover order). */
923
928
  ordered(preferId) {
924
929
  if (!preferId) return this.transports;
@@ -1586,6 +1591,7 @@ var WorkflowEngine = class {
1586
1591
  this.admission.onFreed?.((queue) => this.wakeQueueWaiters(queue));
1587
1592
  this.instanceId = deps.instanceId ?? globalThis.crypto.randomUUID();
1588
1593
  this.namespace = deps.namespace ?? "default";
1594
+ this.pool.useNamespace(this.namespace);
1589
1595
  this.leaseMs = deps.leaseMs ?? 3e4;
1590
1596
  this.maxRecoveryAttempts = deps.maxRecoveryAttempts;
1591
1597
  this.remoteAdvanceSilenceMs = deps.remoteAdvanceSilenceMs;
@@ -2928,19 +2934,9 @@ var WorkflowEngine = class {
2928
2934
  }
2929
2935
  const history = await this.remoteHistory(run.id);
2930
2936
  if (remote.executor.dispatch) {
2931
- const { taskId } = await remote.executor.dispatch(run, history);
2937
+ const taskId = `${run.id}:wf:${globalThis.crypto.randomUUID().slice(0, 8)}`;
2932
2938
  const silenceMs = this.remoteAdvanceSilenceMs;
2933
2939
  const wakeAt = silenceMs != null ? this.clock() + silenceMs : void 0;
2934
- const fresh = await this.store.getRun(run.id);
2935
- if (fresh && (fresh.status === "cancelled" || fresh.status === "completed" || fresh.status === "dead")) {
2936
- await this.store.releaseRunLock(run.id);
2937
- return {
2938
- runId: run.id,
2939
- status: fresh.status,
2940
- output: fresh.output,
2941
- error: fresh.error
2942
- };
2943
- }
2944
2940
  await this.store.updateRun(run.id, {
2945
2941
  status: "suspended",
2946
2942
  awaitingDecisionTaskId: taskId,
@@ -2953,6 +2949,7 @@ var WorkflowEngine = class {
2953
2949
  workflow: run.workflow
2954
2950
  });
2955
2951
  await this.store.releaseRunLock(run.id);
2952
+ await remote.executor.dispatch(run, history, taskId);
2956
2953
  return {
2957
2954
  runId: run.id,
2958
2955
  status: "suspended"