@dudousxd/nestjs-durable-core 0.34.0 → 0.36.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. */
@@ -409,6 +417,15 @@ interface StateStore {
409
417
  * No-op if the run doesn't exist. (Token-keyed buffered signals are transient and left alone.)
410
418
  */
411
419
  deleteRun(runId: string): Promise<void>;
420
+ /**
421
+ * Hard-delete up to `limit` terminal runs that fall OUTSIDE `policy` (oldest / over the count cap
422
+ * first), cascading to their child rows exactly like {@link deleteRun}. Returns how many runs were
423
+ * deleted — call again while it returns `limit` to drain a large backlog in bounded batches.
424
+ *
425
+ * Optional: only adapters that implement a bulk prune provide it; the retention poller no-ops with a
426
+ * warning when the configured store omits it. See {@link RetentionPolicy} for the keep/prune rule.
427
+ */
428
+ pruneTerminalRuns?(policy: RetentionPolicy, nowMs: number, limit: number): Promise<number>;
412
429
  getCheckpoint(runId: string, seq: number): Promise<StepCheckpoint | null>;
413
430
  /**
414
431
  * Persist a checkpoint and advance the run atomically. Durable semantics depend on this
@@ -483,6 +500,26 @@ interface StateStore {
483
500
  */
484
501
  listCheckpointsByNamePrefix?(runId: string, prefixes: string[]): Promise<StepCheckpoint[]>;
485
502
  }
503
+ /** The terminal run statuses — a run in one of these is finished and will never change on its own.
504
+ * Only these are eligible for retention pruning (a {@link RetentionPolicy} targeting a non-terminal
505
+ * status would race the engine and delete live work). */
506
+ declare const TERMINAL_RUN_STATUSES: readonly RunStatus[];
507
+ /**
508
+ * One retention rule for hard-pruning terminal run history. Within `statuses` (terminal only), a run
509
+ * is KEPT only if it satisfies EVERY bound you set, and pruned the moment it violates any:
510
+ * - `maxAgeMs` — prune runs whose `updatedAt` (≈ the time they reached terminal) is older than
511
+ * `now - maxAgeMs`.
512
+ * - `maxCount` — keep only the `maxCount` most-recent (by `updatedAt`) runs in the status set; prune
513
+ * everything past that.
514
+ *
515
+ * Set one or both (both = the most-restrictive wins: capped at `maxCount` rows AND nothing older than
516
+ * `maxAgeMs`). Statuses not named by any policy are never pruned.
517
+ */
518
+ interface RetentionPolicy {
519
+ statuses: RunStatus[];
520
+ maxAgeMs?: number;
521
+ maxCount?: number;
522
+ }
486
523
  /** Typed, queryable per-run data — exact values for `eq`/`ne`, numbers/strings for range ops. */
487
524
  type SearchAttributes = Record<string, string | number | boolean>;
488
525
  type AttributeOp = 'eq' | 'ne' | 'gt' | 'gte' | 'lt' | 'lte';
@@ -1751,6 +1788,13 @@ declare class WorkflowEngine {
1751
1788
  /** Apply a turn's commands: persist recorded local steps, dispatch remote calls, schedule timers.
1752
1789
  * Returns the earliest timer deadline to suspend on (or undefined — suspended on a result). */
1753
1790
  private applyCommands;
1791
+ /**
1792
+ * Undo the registered saga compensations in reverse and settle the run `cancelled`. Shared by EVERY
1793
+ * exit path of a run with a compensating cancel in flight — whether the resumed replay returned,
1794
+ * suspended, hit continue-as-new, or threw — so a cancel is never lost just because the body didn't
1795
+ * stop at a suspension point. Clears the in-memory cancel flag.
1796
+ */
1797
+ private compensateAndCancel;
1754
1798
  /** The run body, lease held + renewed by {@link execute}. */
1755
1799
  private runExecution;
1756
1800
  /**
@@ -2344,4 +2388,4 @@ declare class InMemoryTransport implements Transport, ControlPlane {
2344
2388
  onControl(handler: (msg: ControlMessage) => void): void;
2345
2389
  }
2346
2390
 
2347
- 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 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, TRANSPORT, TRANSPORT_CANONICAL, type Transport, type UpdateResult, type UpdateValidator, WORKFLOW_NAME_KEY, type WorkerHeartbeat, 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 };
2391
+ 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 WorkerHeartbeat, 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 };
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. */
@@ -409,6 +417,15 @@ interface StateStore {
409
417
  * No-op if the run doesn't exist. (Token-keyed buffered signals are transient and left alone.)
410
418
  */
411
419
  deleteRun(runId: string): Promise<void>;
420
+ /**
421
+ * Hard-delete up to `limit` terminal runs that fall OUTSIDE `policy` (oldest / over the count cap
422
+ * first), cascading to their child rows exactly like {@link deleteRun}. Returns how many runs were
423
+ * deleted — call again while it returns `limit` to drain a large backlog in bounded batches.
424
+ *
425
+ * Optional: only adapters that implement a bulk prune provide it; the retention poller no-ops with a
426
+ * warning when the configured store omits it. See {@link RetentionPolicy} for the keep/prune rule.
427
+ */
428
+ pruneTerminalRuns?(policy: RetentionPolicy, nowMs: number, limit: number): Promise<number>;
412
429
  getCheckpoint(runId: string, seq: number): Promise<StepCheckpoint | null>;
413
430
  /**
414
431
  * Persist a checkpoint and advance the run atomically. Durable semantics depend on this
@@ -483,6 +500,26 @@ interface StateStore {
483
500
  */
484
501
  listCheckpointsByNamePrefix?(runId: string, prefixes: string[]): Promise<StepCheckpoint[]>;
485
502
  }
503
+ /** The terminal run statuses — a run in one of these is finished and will never change on its own.
504
+ * Only these are eligible for retention pruning (a {@link RetentionPolicy} targeting a non-terminal
505
+ * status would race the engine and delete live work). */
506
+ declare const TERMINAL_RUN_STATUSES: readonly RunStatus[];
507
+ /**
508
+ * One retention rule for hard-pruning terminal run history. Within `statuses` (terminal only), a run
509
+ * is KEPT only if it satisfies EVERY bound you set, and pruned the moment it violates any:
510
+ * - `maxAgeMs` — prune runs whose `updatedAt` (≈ the time they reached terminal) is older than
511
+ * `now - maxAgeMs`.
512
+ * - `maxCount` — keep only the `maxCount` most-recent (by `updatedAt`) runs in the status set; prune
513
+ * everything past that.
514
+ *
515
+ * Set one or both (both = the most-restrictive wins: capped at `maxCount` rows AND nothing older than
516
+ * `maxAgeMs`). Statuses not named by any policy are never pruned.
517
+ */
518
+ interface RetentionPolicy {
519
+ statuses: RunStatus[];
520
+ maxAgeMs?: number;
521
+ maxCount?: number;
522
+ }
486
523
  /** Typed, queryable per-run data — exact values for `eq`/`ne`, numbers/strings for range ops. */
487
524
  type SearchAttributes = Record<string, string | number | boolean>;
488
525
  type AttributeOp = 'eq' | 'ne' | 'gt' | 'gte' | 'lt' | 'lte';
@@ -1751,6 +1788,13 @@ declare class WorkflowEngine {
1751
1788
  /** Apply a turn's commands: persist recorded local steps, dispatch remote calls, schedule timers.
1752
1789
  * Returns the earliest timer deadline to suspend on (or undefined — suspended on a result). */
1753
1790
  private applyCommands;
1791
+ /**
1792
+ * Undo the registered saga compensations in reverse and settle the run `cancelled`. Shared by EVERY
1793
+ * exit path of a run with a compensating cancel in flight — whether the resumed replay returned,
1794
+ * suspended, hit continue-as-new, or threw — so a cancel is never lost just because the body didn't
1795
+ * stop at a suspension point. Clears the in-memory cancel flag.
1796
+ */
1797
+ private compensateAndCancel;
1754
1798
  /** The run body, lease held + renewed by {@link execute}. */
1755
1799
  private runExecution;
1756
1800
  /**
@@ -2344,4 +2388,4 @@ declare class InMemoryTransport implements Transport, ControlPlane {
2344
2388
  onControl(handler: (msg: ControlMessage) => void): void;
2345
2389
  }
2346
2390
 
2347
- 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 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, TRANSPORT, TRANSPORT_CANONICAL, type Transport, type UpdateResult, type UpdateValidator, WORKFLOW_NAME_KEY, type WorkerHeartbeat, 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 };
2391
+ 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 WorkerHeartbeat, 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 };
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
@@ -3348,6 +3413,14 @@ var RemoteStepError = class extends Error {
3348
3413
  }
3349
3414
  };
3350
3415
 
3416
+ // src/interfaces.ts
3417
+ var TERMINAL_RUN_STATUSES = [
3418
+ "completed",
3419
+ "failed",
3420
+ "cancelled",
3421
+ "dead"
3422
+ ];
3423
+
3351
3424
  // src/remote-step-factory.ts
3352
3425
  function remoteStep(config) {
3353
3426
  return {
@@ -3908,7 +3981,7 @@ var InMemoryStateStore = class {
3908
3981
  async listIncompleteRuns() {
3909
3982
  return [
3910
3983
  ...this.runs.values()
3911
- ].filter((r) => r.status === "running").map((r) => ({
3984
+ ].filter((r) => r.status === "running" || r.status === "cancelling").map((r) => ({
3912
3985
  ...r
3913
3986
  }));
3914
3987
  }
@@ -4160,6 +4233,7 @@ export {
4160
4233
  STATE_STORE_CANONICAL,
4161
4234
  SignalTimeoutError,
4162
4235
  SingletonQueueFullError,
4236
+ TERMINAL_RUN_STATUSES,
4163
4237
  TRANSPORT,
4164
4238
  TRANSPORT_CANONICAL,
4165
4239
  WORKFLOW_NAME_KEY,