@dudousxd/nestjs-durable-core 0.75.2 → 0.76.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
@@ -969,6 +969,16 @@ interface StateStore {
969
969
  * being a single transaction; stores without transactions cannot give the strong guarantee.
970
970
  */
971
971
  saveCheckpoint(checkpoint: StepCheckpoint): Promise<void>;
972
+ /**
973
+ * Overwrite the checkpoint at `(runId, seq)` with `checkpoint`, but only while the stored row is
974
+ * still `pending`; resolves whether it wrote. The engine writes a dispatched step's row from two
975
+ * places at once (a turn stamping its redispatch deadline, and the worker's result settling it),
976
+ * so a blind {@link saveCheckpoint} from a row read a moment earlier can put a `completed` step
977
+ * back to `pending`. This is the compare-and-set that prevents it.
978
+ *
979
+ * Optional: a store that omits it falls back to {@link saveCheckpoint}, which keeps that race.
980
+ */
981
+ saveCheckpointIfPending?(checkpoint: StepCheckpoint): Promise<boolean>;
972
982
  /** Used by recovery on boot to find runs to resume (crashed, left `running`). */
973
983
  listIncompleteRuns(namespace?: string): Promise<WorkflowRun[]>;
974
984
  /** The oldest `pending` runs awaiting dispatch (FIFO, by `createdAt`), capped at `limit`. */
@@ -3796,6 +3806,12 @@ declare class WorkflowEngine {
3796
3806
  * {@link redriveSettledSteps}).
3797
3807
  */
3798
3808
  private readonly awaitedRemoteSteps;
3809
+ /**
3810
+ * When THIS instance settled a remote step from its result, by `runId:seq`. That result's resume
3811
+ * runs here, and `execute` lets it into a lease this instance holds, so its wake is never the
3812
+ * dropped one {@link redriveSettledSteps} recovers. Pruned by age (see {@link noteSettledHere}).
3813
+ */
3814
+ private readonly settledHere;
3799
3815
  /** Flow-control admission backend for remote steps (see {@link registerQueue}). */
3800
3816
  private readonly admission;
3801
3817
  /** Runs on THIS instance blocked on admission, by queue — woken early on a freed-slot signal. */
@@ -4833,6 +4849,10 @@ declare class WorkflowEngine {
4833
4849
  * capable+compatible worker has appeared, else re-parks with a fresh `wakeAt` (design §7.5). */
4834
4850
  private dueBlockedRuns;
4835
4851
  private callRemote;
4852
+ /** Remember a step settled here, forgetting entries older than a lease so the map stays small. */
4853
+ private noteSettledHere;
4854
+ /** {@link StateStore.saveCheckpointIfPending}, or a plain save on a store without it. */
4855
+ private savePendingCheckpoint;
4836
4856
  /** Enqueue a remote step task to its worker group via the transport pool. Shared by the initial
4837
4857
  * dispatch, the opt-in self-heal re-dispatch (see `remoteRedispatchMs`), and the explicit
4838
4858
  * {@link redispatchPending}. Carries the current trace/context so a re-dispatch is traceable. */
@@ -5508,6 +5528,9 @@ declare class CodecStateStore implements StateStore {
5508
5528
  /** Forwarded verbatim (deletes touch no payload). Bound only when the inner store has it, so the
5509
5529
  * engine's per-run fallback still kicks in through the wrapper. */
5510
5530
  readonly deleteRuns?: (runIds: string[]) => Promise<void>;
5531
+ /** Encodes like {@link saveCheckpoint}. Bound only when the inner store has it, so the engine's
5532
+ * plain-save fallback still kicks in through the wrapper. */
5533
+ readonly saveCheckpointIfPending?: (checkpoint: StepCheckpoint) => Promise<boolean>;
5511
5534
  /** Forwarded verbatim — retention selects by status/updatedAt/scope, never by payload. Bound only
5512
5535
  * when the inner store prunes, so the retention poller still sees (and warns about) its absence. */
5513
5536
  readonly pruneTerminalRuns?: (policy: RetentionPolicy, nowMs: number, limit: number) => Promise<number>;
@@ -5977,6 +6000,7 @@ declare class InMemoryStateStore implements StateStore {
5977
6000
  pruneTerminalRuns(policy: RetentionPolicy, nowMs: number, limit: number): Promise<number>;
5978
6001
  getCheckpoint(runId: string, seq: number): Promise<StepCheckpoint | null>;
5979
6002
  saveCheckpoint(checkpoint: StepCheckpoint): Promise<void>;
6003
+ saveCheckpointIfPending(checkpoint: StepCheckpoint): Promise<boolean>;
5980
6004
  listIncompleteRuns(namespace?: string): Promise<WorkflowRun[]>;
5981
6005
  listPendingRuns(limit: number, namespace?: string): Promise<WorkflowRun[]>;
5982
6006
  listDueTimers(nowMs: number, namespace?: string): Promise<WorkflowRun[]>;
package/dist/index.d.ts CHANGED
@@ -969,6 +969,16 @@ interface StateStore {
969
969
  * being a single transaction; stores without transactions cannot give the strong guarantee.
970
970
  */
971
971
  saveCheckpoint(checkpoint: StepCheckpoint): Promise<void>;
972
+ /**
973
+ * Overwrite the checkpoint at `(runId, seq)` with `checkpoint`, but only while the stored row is
974
+ * still `pending`; resolves whether it wrote. The engine writes a dispatched step's row from two
975
+ * places at once (a turn stamping its redispatch deadline, and the worker's result settling it),
976
+ * so a blind {@link saveCheckpoint} from a row read a moment earlier can put a `completed` step
977
+ * back to `pending`. This is the compare-and-set that prevents it.
978
+ *
979
+ * Optional: a store that omits it falls back to {@link saveCheckpoint}, which keeps that race.
980
+ */
981
+ saveCheckpointIfPending?(checkpoint: StepCheckpoint): Promise<boolean>;
972
982
  /** Used by recovery on boot to find runs to resume (crashed, left `running`). */
973
983
  listIncompleteRuns(namespace?: string): Promise<WorkflowRun[]>;
974
984
  /** The oldest `pending` runs awaiting dispatch (FIFO, by `createdAt`), capped at `limit`. */
@@ -3796,6 +3806,12 @@ declare class WorkflowEngine {
3796
3806
  * {@link redriveSettledSteps}).
3797
3807
  */
3798
3808
  private readonly awaitedRemoteSteps;
3809
+ /**
3810
+ * When THIS instance settled a remote step from its result, by `runId:seq`. That result's resume
3811
+ * runs here, and `execute` lets it into a lease this instance holds, so its wake is never the
3812
+ * dropped one {@link redriveSettledSteps} recovers. Pruned by age (see {@link noteSettledHere}).
3813
+ */
3814
+ private readonly settledHere;
3799
3815
  /** Flow-control admission backend for remote steps (see {@link registerQueue}). */
3800
3816
  private readonly admission;
3801
3817
  /** Runs on THIS instance blocked on admission, by queue — woken early on a freed-slot signal. */
@@ -4833,6 +4849,10 @@ declare class WorkflowEngine {
4833
4849
  * capable+compatible worker has appeared, else re-parks with a fresh `wakeAt` (design §7.5). */
4834
4850
  private dueBlockedRuns;
4835
4851
  private callRemote;
4852
+ /** Remember a step settled here, forgetting entries older than a lease so the map stays small. */
4853
+ private noteSettledHere;
4854
+ /** {@link StateStore.saveCheckpointIfPending}, or a plain save on a store without it. */
4855
+ private savePendingCheckpoint;
4836
4856
  /** Enqueue a remote step task to its worker group via the transport pool. Shared by the initial
4837
4857
  * dispatch, the opt-in self-heal re-dispatch (see `remoteRedispatchMs`), and the explicit
4838
4858
  * {@link redispatchPending}. Carries the current trace/context so a re-dispatch is traceable. */
@@ -5508,6 +5528,9 @@ declare class CodecStateStore implements StateStore {
5508
5528
  /** Forwarded verbatim (deletes touch no payload). Bound only when the inner store has it, so the
5509
5529
  * engine's per-run fallback still kicks in through the wrapper. */
5510
5530
  readonly deleteRuns?: (runIds: string[]) => Promise<void>;
5531
+ /** Encodes like {@link saveCheckpoint}. Bound only when the inner store has it, so the engine's
5532
+ * plain-save fallback still kicks in through the wrapper. */
5533
+ readonly saveCheckpointIfPending?: (checkpoint: StepCheckpoint) => Promise<boolean>;
5511
5534
  /** Forwarded verbatim — retention selects by status/updatedAt/scope, never by payload. Bound only
5512
5535
  * when the inner store prunes, so the retention poller still sees (and warns about) its absence. */
5513
5536
  readonly pruneTerminalRuns?: (policy: RetentionPolicy, nowMs: number, limit: number) => Promise<number>;
@@ -5977,6 +6000,7 @@ declare class InMemoryStateStore implements StateStore {
5977
6000
  pruneTerminalRuns(policy: RetentionPolicy, nowMs: number, limit: number): Promise<number>;
5978
6001
  getCheckpoint(runId: string, seq: number): Promise<StepCheckpoint | null>;
5979
6002
  saveCheckpoint(checkpoint: StepCheckpoint): Promise<void>;
6003
+ saveCheckpointIfPending(checkpoint: StepCheckpoint): Promise<boolean>;
5980
6004
  listIncompleteRuns(namespace?: string): Promise<WorkflowRun[]>;
5981
6005
  listPendingRuns(limit: number, namespace?: string): Promise<WorkflowRun[]>;
5982
6006
  listDueTimers(nowMs: number, namespace?: string): Promise<WorkflowRun[]>;
package/dist/index.js CHANGED
@@ -3704,6 +3704,12 @@ var WorkflowEngine = class {
3704
3704
  * {@link redriveSettledSteps}).
3705
3705
  */
3706
3706
  awaitedRemoteSteps = /* @__PURE__ */ new Map();
3707
+ /**
3708
+ * When THIS instance settled a remote step from its result, by `runId:seq`. That result's resume
3709
+ * runs here, and `execute` lets it into a lease this instance holds, so its wake is never the
3710
+ * dropped one {@link redriveSettledSteps} recovers. Pruned by age (see {@link noteSettledHere}).
3711
+ */
3712
+ settledHere = /* @__PURE__ */ new Map();
3707
3713
  /** Flow-control admission backend for remote steps (see {@link registerQueue}). */
3708
3714
  admission;
3709
3715
  /** Runs on THIS instance blocked on admission, by queue — woken early on a freed-slot signal. */
@@ -4178,7 +4184,18 @@ var WorkflowEngine = class {
4178
4184
  createdAt: now,
4179
4185
  updatedAt: now
4180
4186
  };
4181
- await this.store.createRun(run);
4187
+ try {
4188
+ await this.store.createRun(run);
4189
+ } catch (err) {
4190
+ const winner = await this.store.getRun(runId).catch(() => null);
4191
+ if (!winner) throw err;
4192
+ return {
4193
+ runId,
4194
+ status: winner.status,
4195
+ output: winner.output,
4196
+ error: winner.error
4197
+ };
4198
+ }
4182
4199
  await this.runDispatcher.dispatch(runId);
4183
4200
  if (this.controlPlane) {
4184
4201
  void this.controlPlane.publishControl({
@@ -5120,6 +5137,16 @@ var WorkflowEngine = class {
5120
5137
  for (const seq of seqs) {
5121
5138
  const cp = await this.store.getCheckpoint(runId, seq);
5122
5139
  if (cp && (cp.status === "completed" || cp.status === "failed")) {
5140
+ if (this.settledHere.has(`${runId}:${seq}`)) return;
5141
+ const nowMs = this.clock();
5142
+ if (!await this.store.tryLockRun(runId, this.instanceId, nowMs + this.leaseMs, nowMs)) {
5143
+ return;
5144
+ }
5145
+ const run = await this.store.getRun(runId);
5146
+ if (run?.status !== "suspended") {
5147
+ await this.store.releaseRunLock(runId);
5148
+ return;
5149
+ }
5123
5150
  await this.resumeImplicit(runId);
5124
5151
  return;
5125
5152
  }
@@ -7091,10 +7118,12 @@ var WorkflowEngine = class {
7091
7118
  if (this.remoteRedispatchMs == null) throw new WorkflowSuspended();
7092
7119
  if (existing.wakeAt == null) {
7093
7120
  const wakeAt = this.clock() + this.remoteRedispatchMs;
7094
- await this.store.saveCheckpoint({
7121
+ if (!await this.savePendingCheckpoint({
7095
7122
  ...existing,
7096
7123
  wakeAt
7097
- });
7124
+ })) {
7125
+ return this.callRemote(runId, seq, step, input, queue, transport, void 0, admission, routing);
7126
+ }
7098
7127
  throw new WorkflowSuspended(wakeAt);
7099
7128
  }
7100
7129
  if (this.clock() < existing.wakeAt) throw new WorkflowSuspended(existing.wakeAt);
@@ -7107,7 +7136,7 @@ var WorkflowEngine = class {
7107
7136
  const reAttempt = existing.attempts + 1;
7108
7137
  const nextDeadline = this.clock() + this.remoteRedispatchMs;
7109
7138
  const reEnqueuedAt = /* @__PURE__ */ new Date();
7110
- await this.store.saveCheckpoint({
7139
+ const claimed = await this.savePendingCheckpoint({
7111
7140
  ...existing,
7112
7141
  attempts: reAttempt,
7113
7142
  wakeAt: nextDeadline,
@@ -7121,6 +7150,9 @@ var WorkflowEngine = class {
7121
7150
  lostStepEvent(this.clock(), reAttempt, "redispatched")
7122
7151
  ]
7123
7152
  });
7153
+ if (!claimed) {
7154
+ return this.callRemote(runId, seq, step, input, queue, transport, void 0, admission, routing);
7155
+ }
7124
7156
  await this.dispatchRemoteTask({
7125
7157
  runId,
7126
7158
  seq,
@@ -7211,6 +7243,21 @@ var WorkflowEngine = class {
7211
7243
  this.awaitedRemoteSteps.get(runId)?.add(seq);
7212
7244
  throw new WorkflowSuspended();
7213
7245
  }
7246
+ /** Remember a step settled here, forgetting entries older than a lease so the map stays small. */
7247
+ noteSettledHere(key) {
7248
+ const now = this.clock();
7249
+ this.settledHere.set(key, now);
7250
+ if (this.settledHere.size < 1024) return;
7251
+ for (const [k, at] of this.settledHere) {
7252
+ if (now - at > this.leaseMs) this.settledHere.delete(k);
7253
+ }
7254
+ }
7255
+ /** {@link StateStore.saveCheckpointIfPending}, or a plain save on a store without it. */
7256
+ async savePendingCheckpoint(checkpoint) {
7257
+ if (this.store.saveCheckpointIfPending) return this.store.saveCheckpointIfPending(checkpoint);
7258
+ await this.store.saveCheckpoint(checkpoint);
7259
+ return true;
7260
+ }
7214
7261
  /** Enqueue a remote step task to its worker group via the transport pool. Shared by the initial
7215
7262
  * dispatch, the opt-in self-heal re-dispatch (see `remoteRedispatchMs`), and the explicit
7216
7263
  * {@link redispatchPending}. Carries the current trace/context so a re-dispatch is traceable. */
@@ -7241,7 +7288,7 @@ var WorkflowEngine = class {
7241
7288
  if (run && (run.status === "cancelled" || run.status === "completed")) return;
7242
7289
  const finishedAt = /* @__PURE__ */ new Date();
7243
7290
  const startedAt = result.startedAt ? new Date(result.startedAt) : cp.startedAt;
7244
- await this.store.saveCheckpoint({
7291
+ const claimed = await this.savePendingCheckpoint({
7245
7292
  ...cp,
7246
7293
  status: result.status,
7247
7294
  output: result.status === "completed" ? result.output : cp.output,
@@ -7250,6 +7297,8 @@ var WorkflowEngine = class {
7250
7297
  startedAt,
7251
7298
  finishedAt
7252
7299
  });
7300
+ if (!claimed) return;
7301
+ this.noteSettledHere(`${result.runId}:${result.seq}`);
7253
7302
  this.emit({
7254
7303
  type: result.status === "completed" ? "step.completed" : "step.failed",
7255
7304
  runId: result.runId,
@@ -7642,6 +7691,9 @@ var CodecStateStore = class {
7642
7691
  /** Forwarded verbatim (deletes touch no payload). Bound only when the inner store has it, so the
7643
7692
  * engine's per-run fallback still kicks in through the wrapper. */
7644
7693
  deleteRuns;
7694
+ /** Encodes like {@link saveCheckpoint}. Bound only when the inner store has it, so the engine's
7695
+ * plain-save fallback still kicks in through the wrapper. */
7696
+ saveCheckpointIfPending;
7645
7697
  /** Forwarded verbatim — retention selects by status/updatedAt/scope, never by payload. Bound only
7646
7698
  * when the inner store prunes, so the retention poller still sees (and warns about) its absence. */
7647
7699
  pruneTerminalRuns;
@@ -7669,6 +7721,8 @@ var CodecStateStore = class {
7669
7721
  if (inner.tryAdmitSingleton) this.tryAdmitSingleton = inner.tryAdmitSingleton.bind(inner);
7670
7722
  const deleteRuns = inner.deleteRuns;
7671
7723
  if (deleteRuns) this.deleteRuns = (ids) => deleteRuns.call(inner, ids);
7724
+ const saveIfPending = inner.saveCheckpointIfPending;
7725
+ if (saveIfPending) this.saveCheckpointIfPending = (cp) => saveIfPending.call(inner, this.encCp(cp));
7672
7726
  const prune = inner.pruneTerminalRuns;
7673
7727
  if (prune) this.pruneTerminalRuns = (policy, now, limit) => prune.call(inner, policy, now, limit);
7674
7728
  const count = inner.countRuns;
@@ -8232,6 +8286,14 @@ var InMemoryStateStore = class {
8232
8286
  ...checkpoint
8233
8287
  });
8234
8288
  }
8289
+ async saveCheckpointIfPending(checkpoint) {
8290
+ const key = this.key(checkpoint.runId, checkpoint.seq);
8291
+ if (this.checkpoints.get(key)?.status !== "pending") return false;
8292
+ this.checkpoints.set(key, {
8293
+ ...checkpoint
8294
+ });
8295
+ return true;
8296
+ }
8235
8297
  async listIncompleteRuns(namespace) {
8236
8298
  return [
8237
8299
  ...this.runs.values()