@dudousxd/nestjs-durable-core 0.44.0 → 0.44.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.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
@@ -2244,15 +2244,16 @@ declare function remoteStep<TInput, TOutput>(config: RemoteStepConfig<TInput, TO
2244
2244
 
2245
2245
  /**
2246
2246
  * 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
2247
+ * DISPATCHING a {@link WorkflowTask} over the broker under the ENGINE-SUPPLIED `taskId`, WITHOUT
2248
2248
  * awaiting the decision. Pass one to `engine.registerRemote(name, version, { group, executor })` so a
2249
2249
  * workflow authored in another SDK (e.g. the Python `durable-worker`) is driven over Redis/BullMQ.
2250
2250
  *
2251
2251
  * 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
2252
+ * turn to its decision. The engine records the `taskId` on the run as the awaited marker AND releases
2253
+ * the run's lease BEFORE calling `dispatch`, so the worker's {@link import('./interfaces').WorkflowDecision},
2254
+ * delivered over the transport's `onDecision` and applied DURABLY (by run id) on whatever engine
2255
+ * instance consumes it — even one that did not dispatch the turn — can never arrive ahead of its marker
2256
+ * or contend with a still-held lease and be dropped. A point-to-point broker can therefore hand the
2256
2257
  * decision to any instance without it being dropped (the bug this design fixes).
2257
2258
  *
2258
2259
  * Recovery-safe: if the engine crashes (or a decision is genuinely lost because the worker died), the
@@ -2268,12 +2269,10 @@ declare class RemoteWorkflowExecutor implements WorkflowExecutor {
2268
2269
  timeoutMs?: number;
2269
2270
  });
2270
2271
  /**
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`.
2272
+ * Enqueue one workflow turn under the engine-supplied `taskId`. Does NOT await the decision — the
2273
+ * engine has already suspended the run on `taskId` and applies the decision durably via `onDecision`.
2273
2274
  */
2274
- dispatch(run: WorkflowRun, history: HistoryEvent[], pendingSignals?: WorkflowTask['pendingSignals']): Promise<{
2275
- taskId: string;
2276
- }>;
2275
+ dispatch(run: WorkflowRun, history: HistoryEvent[], taskId: string, pendingSignals?: WorkflowTask['pendingSignals']): Promise<void>;
2277
2276
  }
2278
2277
 
2279
2278
  /**
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
@@ -2244,15 +2244,16 @@ declare function remoteStep<TInput, TOutput>(config: RemoteStepConfig<TInput, TO
2244
2244
 
2245
2245
  /**
2246
2246
  * 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
2247
+ * DISPATCHING a {@link WorkflowTask} over the broker under the ENGINE-SUPPLIED `taskId`, WITHOUT
2248
2248
  * awaiting the decision. Pass one to `engine.registerRemote(name, version, { group, executor })` so a
2249
2249
  * workflow authored in another SDK (e.g. the Python `durable-worker`) is driven over Redis/BullMQ.
2250
2250
  *
2251
2251
  * 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
2252
+ * turn to its decision. The engine records the `taskId` on the run as the awaited marker AND releases
2253
+ * the run's lease BEFORE calling `dispatch`, so the worker's {@link import('./interfaces').WorkflowDecision},
2254
+ * delivered over the transport's `onDecision` and applied DURABLY (by run id) on whatever engine
2255
+ * instance consumes it — even one that did not dispatch the turn — can never arrive ahead of its marker
2256
+ * or contend with a still-held lease and be dropped. A point-to-point broker can therefore hand the
2256
2257
  * decision to any instance without it being dropped (the bug this design fixes).
2257
2258
  *
2258
2259
  * Recovery-safe: if the engine crashes (or a decision is genuinely lost because the worker died), the
@@ -2268,12 +2269,10 @@ declare class RemoteWorkflowExecutor implements WorkflowExecutor {
2268
2269
  timeoutMs?: number;
2269
2270
  });
2270
2271
  /**
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`.
2272
+ * Enqueue one workflow turn under the engine-supplied `taskId`. Does NOT await the decision — the
2273
+ * engine has already suspended the run on `taskId` and applies the decision durably via `onDecision`.
2273
2274
  */
2274
- dispatch(run: WorkflowRun, history: HistoryEvent[], pendingSignals?: WorkflowTask['pendingSignals']): Promise<{
2275
- taskId: string;
2276
- }>;
2275
+ dispatch(run: WorkflowRun, history: HistoryEvent[], taskId: string, pendingSignals?: WorkflowTask['pendingSignals']): Promise<void>;
2277
2276
  }
2278
2277
 
2279
2278
  /**
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
 
@@ -2928,19 +2922,9 @@ var WorkflowEngine = class {
2928
2922
  }
2929
2923
  const history = await this.remoteHistory(run.id);
2930
2924
  if (remote.executor.dispatch) {
2931
- const { taskId } = await remote.executor.dispatch(run, history);
2925
+ const taskId = `${run.id}:wf:${globalThis.crypto.randomUUID().slice(0, 8)}`;
2932
2926
  const silenceMs = this.remoteAdvanceSilenceMs;
2933
2927
  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
2928
  await this.store.updateRun(run.id, {
2945
2929
  status: "suspended",
2946
2930
  awaitingDecisionTaskId: taskId,
@@ -2953,6 +2937,7 @@ var WorkflowEngine = class {
2953
2937
  workflow: run.workflow
2954
2938
  });
2955
2939
  await this.store.releaseRunLock(run.id);
2940
+ await remote.executor.dispatch(run, history, taskId);
2956
2941
  return {
2957
2942
  runId: run.id,
2958
2943
  status: "suspended"