@dudousxd/nestjs-durable-core 0.34.0 → 0.35.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
@@ -209,7 +209,15 @@ declare function workflowName(ref: WorkflowRef): string;
209
209
  */
210
210
  type RunStatus =
211
211
  /** Created + enqueued by `start`, not yet picked up — a worker will lease and execute it. */
212
- 'pending' | 'running' | 'suspended' | 'completed' | 'failed' | 'cancelled'
212
+ 'pending' | 'running' | 'suspended'
213
+ /**
214
+ * Compensating cancel in progress: `cancel(runId, { compensate: true })` has been requested and the
215
+ * saga undo is running in the background. NON-TERMINAL — the run is still in-flight (counted by the
216
+ * admission gate, re-driven by recovery if the engine crashes mid-compensation), and flips to
217
+ * `cancelled` once compensation completes. A non-compensating cancel skips this and goes straight to
218
+ * `cancelled`.
219
+ */
220
+ | 'cancelling' | 'completed' | 'failed' | 'cancelled'
213
221
  /** Dead-letter: recovery gave up after `maxRecoveryAttempts` (a poison pill). Terminal; inspect it. */
214
222
  | 'dead';
215
223
  /** One execution of a workflow. The unit of durability and the unit shown in the dashboard. */
@@ -1751,6 +1759,13 @@ declare class WorkflowEngine {
1751
1759
  /** Apply a turn's commands: persist recorded local steps, dispatch remote calls, schedule timers.
1752
1760
  * Returns the earliest timer deadline to suspend on (or undefined — suspended on a result). */
1753
1761
  private applyCommands;
1762
+ /**
1763
+ * Undo the registered saga compensations in reverse and settle the run `cancelled`. Shared by EVERY
1764
+ * exit path of a run with a compensating cancel in flight — whether the resumed replay returned,
1765
+ * suspended, hit continue-as-new, or threw — so a cancel is never lost just because the body didn't
1766
+ * stop at a suspension point. Clears the in-memory cancel flag.
1767
+ */
1768
+ private compensateAndCancel;
1754
1769
  /** The run body, lease held + renewed by {@link execute}. */
1755
1770
  private runExecution;
1756
1771
  /**
package/dist/index.d.ts CHANGED
@@ -209,7 +209,15 @@ declare function workflowName(ref: WorkflowRef): string;
209
209
  */
210
210
  type RunStatus =
211
211
  /** Created + enqueued by `start`, not yet picked up — a worker will lease and execute it. */
212
- 'pending' | 'running' | 'suspended' | 'completed' | 'failed' | 'cancelled'
212
+ 'pending' | 'running' | 'suspended'
213
+ /**
214
+ * Compensating cancel in progress: `cancel(runId, { compensate: true })` has been requested and the
215
+ * saga undo is running in the background. NON-TERMINAL — the run is still in-flight (counted by the
216
+ * admission gate, re-driven by recovery if the engine crashes mid-compensation), and flips to
217
+ * `cancelled` once compensation completes. A non-compensating cancel skips this and goes straight to
218
+ * `cancelled`.
219
+ */
220
+ | 'cancelling' | 'completed' | 'failed' | 'cancelled'
213
221
  /** Dead-letter: recovery gave up after `maxRecoveryAttempts` (a poison pill). Terminal; inspect it. */
214
222
  | 'dead';
215
223
  /** One execution of a workflow. The unit of durability and the unit shown in the dashboard. */
@@ -1751,6 +1759,13 @@ declare class WorkflowEngine {
1751
1759
  /** Apply a turn's commands: persist recorded local steps, dispatch remote calls, schedule timers.
1752
1760
  * Returns the earliest timer deadline to suspend on (or undefined — suspended on a result). */
1753
1761
  private applyCommands;
1762
+ /**
1763
+ * Undo the registered saga compensations in reverse and settle the run `cancelled`. Shared by EVERY
1764
+ * exit path of a run with a compensating cancel in flight — whether the resumed replay returned,
1765
+ * suspended, hit continue-as-new, or threw — so a cancel is never lost just because the body didn't
1766
+ * stop at a suspension point. Clears the in-memory cancel flag.
1767
+ */
1768
+ private compensateAndCancel;
1754
1769
  /** The run body, lease held + renewed by {@link execute}. */
1755
1770
  private runExecution;
1756
1771
  /**
package/dist/index.js CHANGED
@@ -733,7 +733,8 @@ var SingletonGate = class {
733
733
  statuses: [
734
734
  "pending",
735
735
  "running",
736
- "suspended"
736
+ "suspended",
737
+ "cancelling"
737
738
  ]
738
739
  });
739
740
  if (queued.length >= cap) {
@@ -751,7 +752,8 @@ var SingletonGate = class {
751
752
  workflow: run.workflow,
752
753
  statuses: [
753
754
  "running",
754
- "suspended"
755
+ "suspended",
756
+ "cancelling"
755
757
  ]
756
758
  })).sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime() || a.id.localeCompare(b.id));
757
759
  const idx = inflight.findIndex((r) => r.id === run.id);
@@ -1900,14 +1902,16 @@ var WorkflowEngine = class {
1900
1902
  continue;
1901
1903
  }
1902
1904
  await this.store.releaseRunLock(run.id);
1903
- await this.store.updateRun(run.id, {
1904
- status: "pending",
1905
- updatedAt: /* @__PURE__ */ new Date()
1906
- });
1905
+ if (run.status !== "cancelling") {
1906
+ await this.store.updateRun(run.id, {
1907
+ status: "pending",
1908
+ updatedAt: /* @__PURE__ */ new Date()
1909
+ });
1910
+ }
1907
1911
  await this.runDispatcher.dispatch(run.id);
1908
1912
  results.push({
1909
1913
  runId: run.id,
1910
- status: "pending"
1914
+ status: run.status === "cancelling" ? "cancelling" : "pending"
1911
1915
  });
1912
1916
  }
1913
1917
  return results;
@@ -2026,7 +2030,7 @@ var WorkflowEngine = class {
2026
2030
  * `start` when a call site needs the outcome — `await start(...); const r = await waitForRun(id)`.
2027
2031
  */
2028
2032
  waitForRun(runId, opts) {
2029
- const isSettled = /* @__PURE__ */ __name((s) => s !== "pending" && s !== "running", "isSettled");
2033
+ const isSettled = /* @__PURE__ */ __name((s) => s !== "pending" && s !== "running" && s !== "cancelling", "isSettled");
2030
2034
  const toResult = /* @__PURE__ */ __name((run) => ({
2031
2035
  runId,
2032
2036
  status: run.status,
@@ -2190,8 +2194,19 @@ var WorkflowEngine = class {
2190
2194
  error: run.error
2191
2195
  };
2192
2196
  }
2197
+ if (run.status === "cancelling") {
2198
+ return {
2199
+ runId,
2200
+ status: "cancelling"
2201
+ };
2202
+ }
2193
2203
  if (opts?.compensate && (run.status === "suspended" || run.status === "running")) {
2194
2204
  this.cancelRequested.add(runId);
2205
+ await this.store.updateRun(runId, {
2206
+ status: "cancelling",
2207
+ updatedAt: /* @__PURE__ */ new Date()
2208
+ });
2209
+ run.status = "cancelling";
2195
2210
  if (this.controlPlane) {
2196
2211
  void this.controlPlane.publishControl({
2197
2212
  kind: "cancel",
@@ -2205,7 +2220,7 @@ var WorkflowEngine = class {
2205
2220
  await this.cancelChildren(runId, opts);
2206
2221
  return {
2207
2222
  runId,
2208
- status: run.status
2223
+ status: "cancelling"
2209
2224
  };
2210
2225
  }
2211
2226
  const error = {
@@ -2596,6 +2611,36 @@ var WorkflowEngine = class {
2596
2611
  };
2597
2612
  }
2598
2613
  async runRemoteExecution(run, registered) {
2614
+ if (run.status === "cancelling") {
2615
+ const error = {
2616
+ message: "cancelled"
2617
+ };
2618
+ await this.store.updateRun(run.id, {
2619
+ status: "cancelled",
2620
+ error,
2621
+ updatedAt: /* @__PURE__ */ new Date()
2622
+ });
2623
+ this.emit({
2624
+ type: "run.failed",
2625
+ runId: run.id,
2626
+ workflow: run.workflow,
2627
+ error
2628
+ });
2629
+ this.notifyCancelled(run.id);
2630
+ if (this.controlPlane) {
2631
+ void this.controlPlane.publishControl({
2632
+ kind: "cancel",
2633
+ runId: run.id,
2634
+ from: this.instanceId
2635
+ }).catch(() => void 0);
2636
+ }
2637
+ await this.store.releaseRunLock(run.id);
2638
+ return {
2639
+ runId: run.id,
2640
+ status: "cancelled",
2641
+ error
2642
+ };
2643
+ }
2599
2644
  const remote = registered.remote;
2600
2645
  if (run.status === "pending") {
2601
2646
  await this.store.updateRun(run.id, {
@@ -2689,6 +2734,15 @@ var WorkflowEngine = class {
2689
2734
  status: "cancelled"
2690
2735
  };
2691
2736
  }
2737
+ const fresh = await this.store.getRun(run.id);
2738
+ if (fresh && (fresh.status === "cancelled" || fresh.status === "completed" || fresh.status === "dead")) {
2739
+ return {
2740
+ runId: run.id,
2741
+ status: fresh.status,
2742
+ output: fresh.output,
2743
+ error: fresh.error
2744
+ };
2745
+ }
2692
2746
  const wakeAt = await this.applyCommands(run, decision.commands);
2693
2747
  return this.settleRun(run, {
2694
2748
  kind: "suspended",
@@ -2856,8 +2910,41 @@ var WorkflowEngine = class {
2856
2910
  }
2857
2911
  return wakeAt;
2858
2912
  }
2913
+ /**
2914
+ * Undo the registered saga compensations in reverse and settle the run `cancelled`. Shared by EVERY
2915
+ * exit path of a run with a compensating cancel in flight — whether the resumed replay returned,
2916
+ * suspended, hit continue-as-new, or threw — so a cancel is never lost just because the body didn't
2917
+ * stop at a suspension point. Clears the in-memory cancel flag.
2918
+ */
2919
+ async compensateAndCancel(run, compensations) {
2920
+ this.cancelRequested.delete(run.id);
2921
+ for (let i = compensations.length - 1; i >= 0; i -= 1) {
2922
+ const comp = compensations[i];
2923
+ if (comp) await this.runCompensation(run, comp);
2924
+ }
2925
+ const error = {
2926
+ message: "cancelled"
2927
+ };
2928
+ await this.store.updateRun(run.id, {
2929
+ status: "cancelled",
2930
+ error,
2931
+ updatedAt: /* @__PURE__ */ new Date()
2932
+ });
2933
+ this.emit({
2934
+ type: "run.failed",
2935
+ runId: run.id,
2936
+ workflow: run.workflow,
2937
+ error
2938
+ });
2939
+ return {
2940
+ runId: run.id,
2941
+ status: "cancelled",
2942
+ error
2943
+ };
2944
+ }
2859
2945
  /** The run body, lease held + renewed by {@link execute}. */
2860
2946
  async runExecution(run, fn, registered) {
2947
+ if (run.status === "cancelling") this.cancelRequested.add(run.id);
2861
2948
  if (run.status === "pending") {
2862
2949
  await this.store.updateRun(run.id, {
2863
2950
  status: "running",
@@ -2870,7 +2957,7 @@ var WorkflowEngine = class {
2870
2957
  workflow: run.workflow
2871
2958
  });
2872
2959
  }
2873
- if (registered?.singleton && !await this.singletons.admit(run, registered.singleton)) {
2960
+ if (run.status !== "cancelling" && registered?.singleton && !await this.singletons.admit(run, registered.singleton)) {
2874
2961
  const wakeAt = this.singletons.retryWakeAt();
2875
2962
  await this.store.updateRun(run.id, {
2876
2963
  status: "suspended",
@@ -2895,12 +2982,14 @@ var WorkflowEngine = class {
2895
2982
  const ctx = createWorkflowCtx(this.ctxHostFor(replay), run.id, compensations, run.workflow);
2896
2983
  try {
2897
2984
  const output = await fn(ctx, run.input);
2985
+ if (this.cancelRequested.has(run.id)) return this.compensateAndCancel(run, compensations);
2898
2986
  return this.settleRun(run, {
2899
2987
  kind: "completed",
2900
2988
  output
2901
2989
  });
2902
2990
  } catch (err) {
2903
2991
  if (err instanceof ContinueAsNew) {
2992
+ if (this.cancelRequested.has(run.id)) return this.compensateAndCancel(run, compensations);
2904
2993
  await this.store.updateRun(run.id, {
2905
2994
  status: "completed",
2906
2995
  output: void 0,
@@ -2924,37 +3013,13 @@ var WorkflowEngine = class {
2924
3013
  };
2925
3014
  }
2926
3015
  if (err instanceof WorkflowSuspended) {
2927
- if (this.cancelRequested.has(run.id)) {
2928
- this.cancelRequested.delete(run.id);
2929
- for (let i = compensations.length - 1; i >= 0; i -= 1) {
2930
- const comp = compensations[i];
2931
- if (comp) await this.runCompensation(run, comp);
2932
- }
2933
- const error2 = {
2934
- message: "cancelled"
2935
- };
2936
- await this.store.updateRun(run.id, {
2937
- status: "cancelled",
2938
- error: error2,
2939
- updatedAt: /* @__PURE__ */ new Date()
2940
- });
2941
- this.emit({
2942
- type: "run.failed",
2943
- runId: run.id,
2944
- workflow: run.workflow,
2945
- error: error2
2946
- });
2947
- return {
2948
- runId: run.id,
2949
- status: "cancelled",
2950
- error: error2
2951
- };
2952
- }
3016
+ if (this.cancelRequested.has(run.id)) return this.compensateAndCancel(run, compensations);
2953
3017
  return this.settleRun(run, {
2954
3018
  kind: "suspended",
2955
3019
  wakeAt: err.wakeAt
2956
3020
  });
2957
3021
  }
3022
+ if (this.cancelRequested.has(run.id)) return this.compensateAndCancel(run, compensations);
2958
3023
  const error = {
2959
3024
  message: err instanceof Error ? err.message : String(err),
2960
3025
  stack: err instanceof Error ? err.stack : void 0
@@ -3908,7 +3973,7 @@ var InMemoryStateStore = class {
3908
3973
  async listIncompleteRuns() {
3909
3974
  return [
3910
3975
  ...this.runs.values()
3911
- ].filter((r) => r.status === "running").map((r) => ({
3976
+ ].filter((r) => r.status === "running" || r.status === "cancelling").map((r) => ({
3912
3977
  ...r
3913
3978
  }));
3914
3979
  }