@dudousxd/nestjs-durable-core 0.75.0 → 0.75.2

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
@@ -3790,6 +3790,12 @@ declare class WorkflowEngine {
3790
3790
  private readonly updateValidators;
3791
3791
  /** Runs being cancelled WITH saga compensation — see `cancel({ compensate: true })`. */
3792
3792
  private readonly cancelRequested;
3793
+ /**
3794
+ * Seqs of the dispatched remote steps a turn suspended on, by run, while that turn executes on
3795
+ * THIS instance — so the turn can re-check them once its lease is released (see
3796
+ * {@link redriveSettledSteps}).
3797
+ */
3798
+ private readonly awaitedRemoteSteps;
3793
3799
  /** Flow-control admission backend for remote steps (see {@link registerQueue}). */
3794
3800
  private readonly admission;
3795
3801
  /** Runs on THIS instance blocked on admission, by queue — woken early on a freed-slot signal. */
@@ -3801,6 +3807,8 @@ declare class WorkflowEngine {
3801
3807
  private draining;
3802
3808
  /** Wait already spent on this shutdown before `drain()` (see `beginDrain`). */
3803
3809
  private drainSpentMs;
3810
+ /** Re-arms the deadline of every `drain()` currently waiting, when more wait is reported. */
3811
+ private readonly drainRearms;
3804
3812
  constructor(deps: WorkflowEngineDeps);
3805
3813
  /**
3806
3814
  * Register a **durable entity** (a virtual object): a keyed actor whose `handlers` run **serialized
@@ -4298,6 +4306,16 @@ declare class WorkflowEngine {
4298
4306
  * itself. Only re-drives for a group with no waiter left whose checkpoints are all settled, which
4299
4307
  * the next turn is then guaranteed to see — so it cannot loop.
4300
4308
  */
4309
+ /**
4310
+ * Re-drive a run that just suspended on dispatched remote steps which, by now, have a result.
4311
+ *
4312
+ * Same race as {@link redriveSettledChildren}, for `ctx.step`: a result landing while the turn
4313
+ * that dispatched it still holds the lease has its wake dropped by {@link execute}, and the turn
4314
+ * then suspends with nothing left to wake it before the step's redispatch deadline. Called after
4315
+ * the lease is released, so any later result wakes the run itself; it only re-drives for a
4316
+ * checkpoint that is already settled, which the next turn replays past — so it cannot loop.
4317
+ */
4318
+ private redriveSettledSteps;
4301
4319
  private redriveSettledChildren;
4302
4320
  /**
4303
4321
  * Deliver an external signal to the run waiting on `token`, resume it with `payload`, and return
package/dist/index.d.ts CHANGED
@@ -3790,6 +3790,12 @@ declare class WorkflowEngine {
3790
3790
  private readonly updateValidators;
3791
3791
  /** Runs being cancelled WITH saga compensation — see `cancel({ compensate: true })`. */
3792
3792
  private readonly cancelRequested;
3793
+ /**
3794
+ * Seqs of the dispatched remote steps a turn suspended on, by run, while that turn executes on
3795
+ * THIS instance — so the turn can re-check them once its lease is released (see
3796
+ * {@link redriveSettledSteps}).
3797
+ */
3798
+ private readonly awaitedRemoteSteps;
3793
3799
  /** Flow-control admission backend for remote steps (see {@link registerQueue}). */
3794
3800
  private readonly admission;
3795
3801
  /** Runs on THIS instance blocked on admission, by queue — woken early on a freed-slot signal. */
@@ -3801,6 +3807,8 @@ declare class WorkflowEngine {
3801
3807
  private draining;
3802
3808
  /** Wait already spent on this shutdown before `drain()` (see `beginDrain`). */
3803
3809
  private drainSpentMs;
3810
+ /** Re-arms the deadline of every `drain()` currently waiting, when more wait is reported. */
3811
+ private readonly drainRearms;
3804
3812
  constructor(deps: WorkflowEngineDeps);
3805
3813
  /**
3806
3814
  * Register a **durable entity** (a virtual object): a keyed actor whose `handlers` run **serialized
@@ -4298,6 +4306,16 @@ declare class WorkflowEngine {
4298
4306
  * itself. Only re-drives for a group with no waiter left whose checkpoints are all settled, which
4299
4307
  * the next turn is then guaranteed to see — so it cannot loop.
4300
4308
  */
4309
+ /**
4310
+ * Re-drive a run that just suspended on dispatched remote steps which, by now, have a result.
4311
+ *
4312
+ * Same race as {@link redriveSettledChildren}, for `ctx.step`: a result landing while the turn
4313
+ * that dispatched it still holds the lease has its wake dropped by {@link execute}, and the turn
4314
+ * then suspends with nothing left to wake it before the step's redispatch deadline. Called after
4315
+ * the lease is released, so any later result wakes the run itself; it only re-drives for a
4316
+ * checkpoint that is already settled, which the next turn replays past — so it cannot loop.
4317
+ */
4318
+ private redriveSettledSteps;
4301
4319
  private redriveSettledChildren;
4302
4320
  /**
4303
4321
  * Deliver an external signal to the run waiting on `token`, resume it with `payload`, and return
package/dist/index.js CHANGED
@@ -3698,6 +3698,12 @@ var WorkflowEngine = class {
3698
3698
  updateValidators = /* @__PURE__ */ new Map();
3699
3699
  /** Runs being cancelled WITH saga compensation — see `cancel({ compensate: true })`. */
3700
3700
  cancelRequested = /* @__PURE__ */ new Set();
3701
+ /**
3702
+ * Seqs of the dispatched remote steps a turn suspended on, by run, while that turn executes on
3703
+ * THIS instance — so the turn can re-check them once its lease is released (see
3704
+ * {@link redriveSettledSteps}).
3705
+ */
3706
+ awaitedRemoteSteps = /* @__PURE__ */ new Map();
3701
3707
  /** Flow-control admission backend for remote steps (see {@link registerQueue}). */
3702
3708
  admission;
3703
3709
  /** Runs on THIS instance blocked on admission, by queue — woken early on a freed-slot signal. */
@@ -3709,6 +3715,8 @@ var WorkflowEngine = class {
3709
3715
  draining = false;
3710
3716
  /** Wait already spent on this shutdown before `drain()` (see `beginDrain`). */
3711
3717
  drainSpentMs = 0;
3718
+ /** Re-arms the deadline of every `drain()` currently waiting, when more wait is reported. */
3719
+ drainRearms = /* @__PURE__ */ new Set();
3712
3720
  constructor(deps) {
3713
3721
  this.store = deps.store;
3714
3722
  this.pool = new TransportPool(deps.transports ?? (deps.transport ? [
@@ -4394,7 +4402,10 @@ var WorkflowEngine = class {
4394
4402
  */
4395
4403
  beginDrain(spentMs = 0) {
4396
4404
  this.draining = true;
4397
- this.drainSpentMs += Math.max(0, spentMs);
4405
+ if (Number.isFinite(spentMs) && spentMs > 0) {
4406
+ this.drainSpentMs += spentMs;
4407
+ for (const rearm of this.drainRearms) rearm();
4408
+ }
4398
4409
  }
4399
4410
  /**
4400
4411
  * Graceful shutdown: stop picking up new runs (recovery/timer become no-ops) and wait for
@@ -4407,18 +4418,32 @@ var WorkflowEngine = class {
4407
4418
  */
4408
4419
  async drain(timeoutMs = 1e4) {
4409
4420
  this.beginDrain();
4410
- const budget = Math.max(0, timeoutMs - this.drainSpentMs);
4411
4421
  if (this.inflight.size === 0) return;
4422
+ const start = Date.now();
4423
+ let rearm = /* @__PURE__ */ __name(() => {
4424
+ }, "rearm");
4425
+ let timeout;
4412
4426
  const timer = new Promise((resolve) => {
4413
- const t = setTimeout(resolve, budget);
4414
- t.unref?.();
4427
+ rearm = /* @__PURE__ */ __name(() => {
4428
+ clearTimeout(timeout);
4429
+ const remaining = Math.max(0, start + timeoutMs - this.drainSpentMs - Date.now());
4430
+ timeout = setTimeout(resolve, remaining);
4431
+ timeout.unref?.();
4432
+ }, "rearm");
4433
+ rearm();
4415
4434
  });
4416
- await Promise.race([
4417
- Promise.allSettled([
4418
- ...this.inflight
4419
- ]),
4420
- timer
4421
- ]);
4435
+ this.drainRearms.add(rearm);
4436
+ try {
4437
+ await Promise.race([
4438
+ Promise.allSettled([
4439
+ ...this.inflight
4440
+ ]),
4441
+ timer
4442
+ ]);
4443
+ } finally {
4444
+ this.drainRearms.delete(rearm);
4445
+ clearTimeout(timeout);
4446
+ }
4422
4447
  }
4423
4448
  /**
4424
4449
  * Cancel in-flight runs that have outlived their workflow's `executionTimeout`. Call it from the
@@ -5082,6 +5107,24 @@ var WorkflowEngine = class {
5082
5107
  * itself. Only re-drives for a group with no waiter left whose checkpoints are all settled, which
5083
5108
  * the next turn is then guaranteed to see — so it cannot loop.
5084
5109
  */
5110
+ /**
5111
+ * Re-drive a run that just suspended on dispatched remote steps which, by now, have a result.
5112
+ *
5113
+ * Same race as {@link redriveSettledChildren}, for `ctx.step`: a result landing while the turn
5114
+ * that dispatched it still holds the lease has its wake dropped by {@link execute}, and the turn
5115
+ * then suspends with nothing left to wake it before the step's redispatch deadline. Called after
5116
+ * the lease is released, so any later result wakes the run itself; it only re-drives for a
5117
+ * checkpoint that is already settled, which the next turn replays past — so it cannot loop.
5118
+ */
5119
+ async redriveSettledSteps(runId, seqs) {
5120
+ for (const seq of seqs) {
5121
+ const cp = await this.store.getCheckpoint(runId, seq);
5122
+ if (cp && (cp.status === "completed" || cp.status === "failed")) {
5123
+ await this.resumeImplicit(runId);
5124
+ return;
5125
+ }
5126
+ }
5127
+ }
5085
5128
  async redriveSettledChildren(runId, fan) {
5086
5129
  if (!this.store.hasSignalWaiter) return;
5087
5130
  for (const wait of fan.waits) {
@@ -6536,6 +6579,9 @@ var WorkflowEngine = class {
6536
6579
  const replay = /* @__PURE__ */ new Map();
6537
6580
  for (const cp of snapshot) replay.set(cp.seq, cp);
6538
6581
  let suspendedOnFan;
6582
+ let suspendedOnSteps = false;
6583
+ const awaitedSteps = /* @__PURE__ */ new Set();
6584
+ this.awaitedRemoteSteps.set(run.id, awaitedSteps);
6539
6585
  const ctx = createWorkflowCtx(this.ctxHostFor(replay, run.namespace), run.id, compensations, run.workflow, registered?.searchAttributesSchema);
6540
6586
  try {
6541
6587
  const output = await runInWorkflowCtx(ctx, () => fn(ctx, run.input));
@@ -6592,6 +6638,7 @@ var WorkflowEngine = class {
6592
6638
  wakeAt: err.wakeAt
6593
6639
  });
6594
6640
  suspendedOnFan = err.fan;
6641
+ suspendedOnSteps = suspended.status === "suspended";
6595
6642
  return suspended;
6596
6643
  }
6597
6644
  if (this.cancelRequested.has(run.id)) {
@@ -6611,10 +6658,16 @@ var WorkflowEngine = class {
6611
6658
  error: error2
6612
6659
  });
6613
6660
  } finally {
6661
+ if (this.awaitedRemoteSteps.get(run.id) === awaitedSteps) {
6662
+ this.awaitedRemoteSteps.delete(run.id);
6663
+ }
6614
6664
  await this.store.releaseRunLock(run.id);
6615
6665
  if (suspendedOnFan) {
6616
6666
  void this.redriveSettledChildren(run.id, suspendedOnFan).catch(() => void 0);
6617
6667
  }
6668
+ if (suspendedOnSteps && awaitedSteps.size > 0) {
6669
+ void this.redriveSettledSteps(run.id, awaitedSteps).catch(() => void 0);
6670
+ }
6618
6671
  }
6619
6672
  }
6620
6673
  /**
@@ -7034,6 +7087,7 @@ var WorkflowEngine = class {
7034
7087
  if (this.pool.size === 0) throw new Error("remote steps require a Transport");
7035
7088
  if (step.timeoutMs) return this.callRemoteInMemory(runId, seq, step, input, transport, group);
7036
7089
  if (existing?.status === "pending") {
7090
+ this.awaitedRemoteSteps.get(runId)?.add(seq);
7037
7091
  if (this.remoteRedispatchMs == null) throw new WorkflowSuspended();
7038
7092
  if (existing.wakeAt == null) {
7039
7093
  const wakeAt = this.clock() + this.remoteRedispatchMs;
@@ -7154,6 +7208,7 @@ var WorkflowEngine = class {
7154
7208
  name: step.name,
7155
7209
  kind: "remote"
7156
7210
  });
7211
+ this.awaitedRemoteSteps.get(runId)?.add(seq);
7157
7212
  throw new WorkflowSuspended();
7158
7213
  }
7159
7214
  /** Enqueue a remote step task to its worker group via the transport pool. Shared by the initial