@dudousxd/nestjs-durable-core 0.75.1 → 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.cjs CHANGED
@@ -3861,6 +3861,12 @@ var WorkflowEngine = class {
3861
3861
  updateValidators = /* @__PURE__ */ new Map();
3862
3862
  /** Runs being cancelled WITH saga compensation — see `cancel({ compensate: true })`. */
3863
3863
  cancelRequested = /* @__PURE__ */ new Set();
3864
+ /**
3865
+ * Seqs of the dispatched remote steps a turn suspended on, by run, while that turn executes on
3866
+ * THIS instance — so the turn can re-check them once its lease is released (see
3867
+ * {@link redriveSettledSteps}).
3868
+ */
3869
+ awaitedRemoteSteps = /* @__PURE__ */ new Map();
3864
3870
  /** Flow-control admission backend for remote steps (see {@link registerQueue}). */
3865
3871
  admission;
3866
3872
  /** Runs on THIS instance blocked on admission, by queue — woken early on a freed-slot signal. */
@@ -4335,7 +4341,18 @@ var WorkflowEngine = class {
4335
4341
  createdAt: now,
4336
4342
  updatedAt: now
4337
4343
  };
4338
- await this.store.createRun(run);
4344
+ try {
4345
+ await this.store.createRun(run);
4346
+ } catch (err) {
4347
+ const winner = await this.store.getRun(runId).catch(() => null);
4348
+ if (!winner) throw err;
4349
+ return {
4350
+ runId,
4351
+ status: winner.status,
4352
+ output: winner.output,
4353
+ error: winner.error
4354
+ };
4355
+ }
4339
4356
  await this.runDispatcher.dispatch(runId);
4340
4357
  if (this.controlPlane) {
4341
4358
  void this.controlPlane.publishControl({
@@ -5264,6 +5281,24 @@ var WorkflowEngine = class {
5264
5281
  * itself. Only re-drives for a group with no waiter left whose checkpoints are all settled, which
5265
5282
  * the next turn is then guaranteed to see — so it cannot loop.
5266
5283
  */
5284
+ /**
5285
+ * Re-drive a run that just suspended on dispatched remote steps which, by now, have a result.
5286
+ *
5287
+ * Same race as {@link redriveSettledChildren}, for `ctx.step`: a result landing while the turn
5288
+ * that dispatched it still holds the lease has its wake dropped by {@link execute}, and the turn
5289
+ * then suspends with nothing left to wake it before the step's redispatch deadline. Called after
5290
+ * the lease is released, so any later result wakes the run itself; it only re-drives for a
5291
+ * checkpoint that is already settled, which the next turn replays past — so it cannot loop.
5292
+ */
5293
+ async redriveSettledSteps(runId, seqs) {
5294
+ for (const seq of seqs) {
5295
+ const cp = await this.store.getCheckpoint(runId, seq);
5296
+ if (cp && (cp.status === "completed" || cp.status === "failed")) {
5297
+ await this.resumeImplicit(runId);
5298
+ return;
5299
+ }
5300
+ }
5301
+ }
5267
5302
  async redriveSettledChildren(runId, fan) {
5268
5303
  if (!this.store.hasSignalWaiter) return;
5269
5304
  for (const wait of fan.waits) {
@@ -6718,6 +6753,9 @@ var WorkflowEngine = class {
6718
6753
  const replay = /* @__PURE__ */ new Map();
6719
6754
  for (const cp of snapshot) replay.set(cp.seq, cp);
6720
6755
  let suspendedOnFan;
6756
+ let suspendedOnSteps = false;
6757
+ const awaitedSteps = /* @__PURE__ */ new Set();
6758
+ this.awaitedRemoteSteps.set(run.id, awaitedSteps);
6721
6759
  const ctx = createWorkflowCtx(this.ctxHostFor(replay, run.namespace), run.id, compensations, run.workflow, registered?.searchAttributesSchema);
6722
6760
  try {
6723
6761
  const output = await runInWorkflowCtx(ctx, () => fn(ctx, run.input));
@@ -6774,6 +6812,7 @@ var WorkflowEngine = class {
6774
6812
  wakeAt: err.wakeAt
6775
6813
  });
6776
6814
  suspendedOnFan = err.fan;
6815
+ suspendedOnSteps = suspended.status === "suspended";
6777
6816
  return suspended;
6778
6817
  }
6779
6818
  if (this.cancelRequested.has(run.id)) {
@@ -6793,10 +6832,16 @@ var WorkflowEngine = class {
6793
6832
  error: error2
6794
6833
  });
6795
6834
  } finally {
6835
+ if (this.awaitedRemoteSteps.get(run.id) === awaitedSteps) {
6836
+ this.awaitedRemoteSteps.delete(run.id);
6837
+ }
6796
6838
  await this.store.releaseRunLock(run.id);
6797
6839
  if (suspendedOnFan) {
6798
6840
  void this.redriveSettledChildren(run.id, suspendedOnFan).catch(() => void 0);
6799
6841
  }
6842
+ if (suspendedOnSteps && awaitedSteps.size > 0) {
6843
+ void this.redriveSettledSteps(run.id, awaitedSteps).catch(() => void 0);
6844
+ }
6800
6845
  }
6801
6846
  }
6802
6847
  /**
@@ -7216,13 +7261,16 @@ var WorkflowEngine = class {
7216
7261
  if (this.pool.size === 0) throw new Error("remote steps require a Transport");
7217
7262
  if (step.timeoutMs) return this.callRemoteInMemory(runId, seq, step, input, transport, group);
7218
7263
  if (existing?.status === "pending") {
7264
+ this.awaitedRemoteSteps.get(runId)?.add(seq);
7219
7265
  if (this.remoteRedispatchMs == null) throw new WorkflowSuspended();
7220
7266
  if (existing.wakeAt == null) {
7221
7267
  const wakeAt = this.clock() + this.remoteRedispatchMs;
7222
- await this.store.saveCheckpoint({
7268
+ if (!await this.savePendingCheckpoint({
7223
7269
  ...existing,
7224
7270
  wakeAt
7225
- });
7271
+ })) {
7272
+ return this.callRemote(runId, seq, step, input, queue, transport, void 0, admission, routing);
7273
+ }
7226
7274
  throw new WorkflowSuspended(wakeAt);
7227
7275
  }
7228
7276
  if (this.clock() < existing.wakeAt) throw new WorkflowSuspended(existing.wakeAt);
@@ -7235,7 +7283,7 @@ var WorkflowEngine = class {
7235
7283
  const reAttempt = existing.attempts + 1;
7236
7284
  const nextDeadline = this.clock() + this.remoteRedispatchMs;
7237
7285
  const reEnqueuedAt = /* @__PURE__ */ new Date();
7238
- await this.store.saveCheckpoint({
7286
+ const claimed = await this.savePendingCheckpoint({
7239
7287
  ...existing,
7240
7288
  attempts: reAttempt,
7241
7289
  wakeAt: nextDeadline,
@@ -7249,6 +7297,9 @@ var WorkflowEngine = class {
7249
7297
  lostStepEvent(this.clock(), reAttempt, "redispatched")
7250
7298
  ]
7251
7299
  });
7300
+ if (!claimed) {
7301
+ return this.callRemote(runId, seq, step, input, queue, transport, void 0, admission, routing);
7302
+ }
7252
7303
  await this.dispatchRemoteTask({
7253
7304
  runId,
7254
7305
  seq,
@@ -7336,8 +7387,15 @@ var WorkflowEngine = class {
7336
7387
  name: step.name,
7337
7388
  kind: "remote"
7338
7389
  });
7390
+ this.awaitedRemoteSteps.get(runId)?.add(seq);
7339
7391
  throw new WorkflowSuspended();
7340
7392
  }
7393
+ /** {@link StateStore.saveCheckpointIfPending}, or a plain save on a store without it. */
7394
+ async savePendingCheckpoint(checkpoint) {
7395
+ if (this.store.saveCheckpointIfPending) return this.store.saveCheckpointIfPending(checkpoint);
7396
+ await this.store.saveCheckpoint(checkpoint);
7397
+ return true;
7398
+ }
7341
7399
  /** Enqueue a remote step task to its worker group via the transport pool. Shared by the initial
7342
7400
  * dispatch, the opt-in self-heal re-dispatch (see `remoteRedispatchMs`), and the explicit
7343
7401
  * {@link redispatchPending}. Carries the current trace/context so a re-dispatch is traceable. */
@@ -7368,7 +7426,7 @@ var WorkflowEngine = class {
7368
7426
  if (run && (run.status === "cancelled" || run.status === "completed")) return;
7369
7427
  const finishedAt = /* @__PURE__ */ new Date();
7370
7428
  const startedAt = result.startedAt ? new Date(result.startedAt) : cp.startedAt;
7371
- await this.store.saveCheckpoint({
7429
+ const claimed = await this.savePendingCheckpoint({
7372
7430
  ...cp,
7373
7431
  status: result.status,
7374
7432
  output: result.status === "completed" ? result.output : cp.output,
@@ -7377,6 +7435,7 @@ var WorkflowEngine = class {
7377
7435
  startedAt,
7378
7436
  finishedAt
7379
7437
  });
7438
+ if (!claimed) return;
7380
7439
  this.emit({
7381
7440
  type: result.status === "completed" ? "step.completed" : "step.failed",
7382
7441
  runId: result.runId,
@@ -7769,6 +7828,9 @@ var CodecStateStore = class {
7769
7828
  /** Forwarded verbatim (deletes touch no payload). Bound only when the inner store has it, so the
7770
7829
  * engine's per-run fallback still kicks in through the wrapper. */
7771
7830
  deleteRuns;
7831
+ /** Encodes like {@link saveCheckpoint}. Bound only when the inner store has it, so the engine's
7832
+ * plain-save fallback still kicks in through the wrapper. */
7833
+ saveCheckpointIfPending;
7772
7834
  /** Forwarded verbatim — retention selects by status/updatedAt/scope, never by payload. Bound only
7773
7835
  * when the inner store prunes, so the retention poller still sees (and warns about) its absence. */
7774
7836
  pruneTerminalRuns;
@@ -7796,6 +7858,8 @@ var CodecStateStore = class {
7796
7858
  if (inner.tryAdmitSingleton) this.tryAdmitSingleton = inner.tryAdmitSingleton.bind(inner);
7797
7859
  const deleteRuns = inner.deleteRuns;
7798
7860
  if (deleteRuns) this.deleteRuns = (ids) => deleteRuns.call(inner, ids);
7861
+ const saveIfPending = inner.saveCheckpointIfPending;
7862
+ if (saveIfPending) this.saveCheckpointIfPending = (cp) => saveIfPending.call(inner, this.encCp(cp));
7799
7863
  const prune = inner.pruneTerminalRuns;
7800
7864
  if (prune) this.pruneTerminalRuns = (policy, now, limit) => prune.call(inner, policy, now, limit);
7801
7865
  const count = inner.countRuns;
@@ -8359,6 +8423,14 @@ var InMemoryStateStore = class {
8359
8423
  ...checkpoint
8360
8424
  });
8361
8425
  }
8426
+ async saveCheckpointIfPending(checkpoint) {
8427
+ const key = this.key(checkpoint.runId, checkpoint.seq);
8428
+ if (this.checkpoints.get(key)?.status !== "pending") return false;
8429
+ this.checkpoints.set(key, {
8430
+ ...checkpoint
8431
+ });
8432
+ return true;
8433
+ }
8362
8434
  async listIncompleteRuns(namespace) {
8363
8435
  return [
8364
8436
  ...this.runs.values()