@dudousxd/nestjs-durable-core 0.75.2 → 0.76.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
@@ -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`. */
@@ -4833,6 +4843,8 @@ declare class WorkflowEngine {
4833
4843
  * capable+compatible worker has appeared, else re-parks with a fresh `wakeAt` (design §7.5). */
4834
4844
  private dueBlockedRuns;
4835
4845
  private callRemote;
4846
+ /** {@link StateStore.saveCheckpointIfPending}, or a plain save on a store without it. */
4847
+ private savePendingCheckpoint;
4836
4848
  /** Enqueue a remote step task to its worker group via the transport pool. Shared by the initial
4837
4849
  * dispatch, the opt-in self-heal re-dispatch (see `remoteRedispatchMs`), and the explicit
4838
4850
  * {@link redispatchPending}. Carries the current trace/context so a re-dispatch is traceable. */
@@ -5508,6 +5520,9 @@ declare class CodecStateStore implements StateStore {
5508
5520
  /** Forwarded verbatim (deletes touch no payload). Bound only when the inner store has it, so the
5509
5521
  * engine's per-run fallback still kicks in through the wrapper. */
5510
5522
  readonly deleteRuns?: (runIds: string[]) => Promise<void>;
5523
+ /** Encodes like {@link saveCheckpoint}. Bound only when the inner store has it, so the engine's
5524
+ * plain-save fallback still kicks in through the wrapper. */
5525
+ readonly saveCheckpointIfPending?: (checkpoint: StepCheckpoint) => Promise<boolean>;
5511
5526
  /** Forwarded verbatim — retention selects by status/updatedAt/scope, never by payload. Bound only
5512
5527
  * when the inner store prunes, so the retention poller still sees (and warns about) its absence. */
5513
5528
  readonly pruneTerminalRuns?: (policy: RetentionPolicy, nowMs: number, limit: number) => Promise<number>;
@@ -5977,6 +5992,7 @@ declare class InMemoryStateStore implements StateStore {
5977
5992
  pruneTerminalRuns(policy: RetentionPolicy, nowMs: number, limit: number): Promise<number>;
5978
5993
  getCheckpoint(runId: string, seq: number): Promise<StepCheckpoint | null>;
5979
5994
  saveCheckpoint(checkpoint: StepCheckpoint): Promise<void>;
5995
+ saveCheckpointIfPending(checkpoint: StepCheckpoint): Promise<boolean>;
5980
5996
  listIncompleteRuns(namespace?: string): Promise<WorkflowRun[]>;
5981
5997
  listPendingRuns(limit: number, namespace?: string): Promise<WorkflowRun[]>;
5982
5998
  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`. */
@@ -4833,6 +4843,8 @@ declare class WorkflowEngine {
4833
4843
  * capable+compatible worker has appeared, else re-parks with a fresh `wakeAt` (design §7.5). */
4834
4844
  private dueBlockedRuns;
4835
4845
  private callRemote;
4846
+ /** {@link StateStore.saveCheckpointIfPending}, or a plain save on a store without it. */
4847
+ private savePendingCheckpoint;
4836
4848
  /** Enqueue a remote step task to its worker group via the transport pool. Shared by the initial
4837
4849
  * dispatch, the opt-in self-heal re-dispatch (see `remoteRedispatchMs`), and the explicit
4838
4850
  * {@link redispatchPending}. Carries the current trace/context so a re-dispatch is traceable. */
@@ -5508,6 +5520,9 @@ declare class CodecStateStore implements StateStore {
5508
5520
  /** Forwarded verbatim (deletes touch no payload). Bound only when the inner store has it, so the
5509
5521
  * engine's per-run fallback still kicks in through the wrapper. */
5510
5522
  readonly deleteRuns?: (runIds: string[]) => Promise<void>;
5523
+ /** Encodes like {@link saveCheckpoint}. Bound only when the inner store has it, so the engine's
5524
+ * plain-save fallback still kicks in through the wrapper. */
5525
+ readonly saveCheckpointIfPending?: (checkpoint: StepCheckpoint) => Promise<boolean>;
5511
5526
  /** Forwarded verbatim — retention selects by status/updatedAt/scope, never by payload. Bound only
5512
5527
  * when the inner store prunes, so the retention poller still sees (and warns about) its absence. */
5513
5528
  readonly pruneTerminalRuns?: (policy: RetentionPolicy, nowMs: number, limit: number) => Promise<number>;
@@ -5977,6 +5992,7 @@ declare class InMemoryStateStore implements StateStore {
5977
5992
  pruneTerminalRuns(policy: RetentionPolicy, nowMs: number, limit: number): Promise<number>;
5978
5993
  getCheckpoint(runId: string, seq: number): Promise<StepCheckpoint | null>;
5979
5994
  saveCheckpoint(checkpoint: StepCheckpoint): Promise<void>;
5995
+ saveCheckpointIfPending(checkpoint: StepCheckpoint): Promise<boolean>;
5980
5996
  listIncompleteRuns(namespace?: string): Promise<WorkflowRun[]>;
5981
5997
  listPendingRuns(limit: number, namespace?: string): Promise<WorkflowRun[]>;
5982
5998
  listDueTimers(nowMs: number, namespace?: string): Promise<WorkflowRun[]>;
package/dist/index.js CHANGED
@@ -4178,7 +4178,18 @@ var WorkflowEngine = class {
4178
4178
  createdAt: now,
4179
4179
  updatedAt: now
4180
4180
  };
4181
- await this.store.createRun(run);
4181
+ try {
4182
+ await this.store.createRun(run);
4183
+ } catch (err) {
4184
+ const winner = await this.store.getRun(runId).catch(() => null);
4185
+ if (!winner) throw err;
4186
+ return {
4187
+ runId,
4188
+ status: winner.status,
4189
+ output: winner.output,
4190
+ error: winner.error
4191
+ };
4192
+ }
4182
4193
  await this.runDispatcher.dispatch(runId);
4183
4194
  if (this.controlPlane) {
4184
4195
  void this.controlPlane.publishControl({
@@ -7091,10 +7102,12 @@ var WorkflowEngine = class {
7091
7102
  if (this.remoteRedispatchMs == null) throw new WorkflowSuspended();
7092
7103
  if (existing.wakeAt == null) {
7093
7104
  const wakeAt = this.clock() + this.remoteRedispatchMs;
7094
- await this.store.saveCheckpoint({
7105
+ if (!await this.savePendingCheckpoint({
7095
7106
  ...existing,
7096
7107
  wakeAt
7097
- });
7108
+ })) {
7109
+ return this.callRemote(runId, seq, step, input, queue, transport, void 0, admission, routing);
7110
+ }
7098
7111
  throw new WorkflowSuspended(wakeAt);
7099
7112
  }
7100
7113
  if (this.clock() < existing.wakeAt) throw new WorkflowSuspended(existing.wakeAt);
@@ -7107,7 +7120,7 @@ var WorkflowEngine = class {
7107
7120
  const reAttempt = existing.attempts + 1;
7108
7121
  const nextDeadline = this.clock() + this.remoteRedispatchMs;
7109
7122
  const reEnqueuedAt = /* @__PURE__ */ new Date();
7110
- await this.store.saveCheckpoint({
7123
+ const claimed = await this.savePendingCheckpoint({
7111
7124
  ...existing,
7112
7125
  attempts: reAttempt,
7113
7126
  wakeAt: nextDeadline,
@@ -7121,6 +7134,9 @@ var WorkflowEngine = class {
7121
7134
  lostStepEvent(this.clock(), reAttempt, "redispatched")
7122
7135
  ]
7123
7136
  });
7137
+ if (!claimed) {
7138
+ return this.callRemote(runId, seq, step, input, queue, transport, void 0, admission, routing);
7139
+ }
7124
7140
  await this.dispatchRemoteTask({
7125
7141
  runId,
7126
7142
  seq,
@@ -7211,6 +7227,12 @@ var WorkflowEngine = class {
7211
7227
  this.awaitedRemoteSteps.get(runId)?.add(seq);
7212
7228
  throw new WorkflowSuspended();
7213
7229
  }
7230
+ /** {@link StateStore.saveCheckpointIfPending}, or a plain save on a store without it. */
7231
+ async savePendingCheckpoint(checkpoint) {
7232
+ if (this.store.saveCheckpointIfPending) return this.store.saveCheckpointIfPending(checkpoint);
7233
+ await this.store.saveCheckpoint(checkpoint);
7234
+ return true;
7235
+ }
7214
7236
  /** Enqueue a remote step task to its worker group via the transport pool. Shared by the initial
7215
7237
  * dispatch, the opt-in self-heal re-dispatch (see `remoteRedispatchMs`), and the explicit
7216
7238
  * {@link redispatchPending}. Carries the current trace/context so a re-dispatch is traceable. */
@@ -7241,7 +7263,7 @@ var WorkflowEngine = class {
7241
7263
  if (run && (run.status === "cancelled" || run.status === "completed")) return;
7242
7264
  const finishedAt = /* @__PURE__ */ new Date();
7243
7265
  const startedAt = result.startedAt ? new Date(result.startedAt) : cp.startedAt;
7244
- await this.store.saveCheckpoint({
7266
+ const claimed = await this.savePendingCheckpoint({
7245
7267
  ...cp,
7246
7268
  status: result.status,
7247
7269
  output: result.status === "completed" ? result.output : cp.output,
@@ -7250,6 +7272,7 @@ var WorkflowEngine = class {
7250
7272
  startedAt,
7251
7273
  finishedAt
7252
7274
  });
7275
+ if (!claimed) return;
7253
7276
  this.emit({
7254
7277
  type: result.status === "completed" ? "step.completed" : "step.failed",
7255
7278
  runId: result.runId,
@@ -7642,6 +7665,9 @@ var CodecStateStore = class {
7642
7665
  /** Forwarded verbatim (deletes touch no payload). Bound only when the inner store has it, so the
7643
7666
  * engine's per-run fallback still kicks in through the wrapper. */
7644
7667
  deleteRuns;
7668
+ /** Encodes like {@link saveCheckpoint}. Bound only when the inner store has it, so the engine's
7669
+ * plain-save fallback still kicks in through the wrapper. */
7670
+ saveCheckpointIfPending;
7645
7671
  /** Forwarded verbatim — retention selects by status/updatedAt/scope, never by payload. Bound only
7646
7672
  * when the inner store prunes, so the retention poller still sees (and warns about) its absence. */
7647
7673
  pruneTerminalRuns;
@@ -7669,6 +7695,8 @@ var CodecStateStore = class {
7669
7695
  if (inner.tryAdmitSingleton) this.tryAdmitSingleton = inner.tryAdmitSingleton.bind(inner);
7670
7696
  const deleteRuns = inner.deleteRuns;
7671
7697
  if (deleteRuns) this.deleteRuns = (ids) => deleteRuns.call(inner, ids);
7698
+ const saveIfPending = inner.saveCheckpointIfPending;
7699
+ if (saveIfPending) this.saveCheckpointIfPending = (cp) => saveIfPending.call(inner, this.encCp(cp));
7672
7700
  const prune = inner.pruneTerminalRuns;
7673
7701
  if (prune) this.pruneTerminalRuns = (policy, now, limit) => prune.call(inner, policy, now, limit);
7674
7702
  const count = inner.countRuns;
@@ -8232,6 +8260,14 @@ var InMemoryStateStore = class {
8232
8260
  ...checkpoint
8233
8261
  });
8234
8262
  }
8263
+ async saveCheckpointIfPending(checkpoint) {
8264
+ const key = this.key(checkpoint.runId, checkpoint.seq);
8265
+ if (this.checkpoints.get(key)?.status !== "pending") return false;
8266
+ this.checkpoints.set(key, {
8267
+ ...checkpoint
8268
+ });
8269
+ return true;
8270
+ }
8235
8271
  async listIncompleteRuns(namespace) {
8236
8272
  return [
8237
8273
  ...this.runs.values()