@dudousxd/nestjs-durable-core 0.85.1 → 0.85.3

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
@@ -5704,6 +5704,22 @@ declare class WorkflowEngine {
5704
5704
  private markStarted;
5705
5705
  private singletonWaitResult;
5706
5706
  private execute;
5707
+ /**
5708
+ * Keep a wake that found `runId`'s lease held by a turn executing elsewhere, durably, so that turn
5709
+ * runs again once it ends instead of suspending on a snapshot that predates what the wake brought
5710
+ * (a signal, a child's completion). The request is a buffered signal on a token no workflow waits
5711
+ * on; the holder takes it after it releases the lease ({@link honorWakeRequests}).
5712
+ *
5713
+ * Written first and looked at second, against a holder that releases first and looks second:
5714
+ * whichever look runs last sees the other side's write. So when the run is no longer executing by
5715
+ * the time this re-reads it, the holder may have looked already, and this side takes the request
5716
+ * and wakes the run itself once the lease is free.
5717
+ */
5718
+ private requestWakeAfterTurn;
5719
+ /** After a turn that suspended released its lease: run again if a wake was kept for it. */
5720
+ private honorWakeRequests;
5721
+ /** Take every wake request kept for `runId`; true when there was one. */
5722
+ private takeWakeRequests;
5707
5723
  /**
5708
5724
  * A wake that found the run leased by another instance is dropped, and normally that is fine: the
5709
5725
  * holder's turn either sees what the wake wrote, or re-checks once it releases the lease
@@ -7102,7 +7118,11 @@ declare function scheduleRunIdAt(s: ScheduledWorkflow, nowMs: number): string;
7102
7118
  * Start each schedule's current-window run. The run id is the time bucket (a fixed-interval index,
7103
7119
  * or the cron fire time) and `engine.start` is idempotent, so firing this on an interval — or
7104
7120
  * racing two instances on the same tick — starts **each window exactly once**. Wire it to a
7105
- * `setInterval`, the durable timer poller, or `@nestjs/schedule`. Returns the run ids this call
7121
+ * `setInterval`, the durable timer poller, or `@nestjs/schedule`.
7122
+ *
7123
+ * Pass the SAME `engine` object on every call: which windows already fired, and the jittered fire
7124
+ * time of each deferred window, are remembered per engine object. A fresh wrapper per call would
7125
+ * redraw a deferred window's fire time on every tick, so it might never fire. Returns the run ids this call
7106
7126
  * started: a window that already has a run is not started or returned again, and once this process
7107
7127
  * has seen a window's run exist, later calls in that window skip it without a store read. A
7108
7128
  * jittered window is deferred to a later call, never slept on.
package/dist/index.d.ts CHANGED
@@ -5704,6 +5704,22 @@ declare class WorkflowEngine {
5704
5704
  private markStarted;
5705
5705
  private singletonWaitResult;
5706
5706
  private execute;
5707
+ /**
5708
+ * Keep a wake that found `runId`'s lease held by a turn executing elsewhere, durably, so that turn
5709
+ * runs again once it ends instead of suspending on a snapshot that predates what the wake brought
5710
+ * (a signal, a child's completion). The request is a buffered signal on a token no workflow waits
5711
+ * on; the holder takes it after it releases the lease ({@link honorWakeRequests}).
5712
+ *
5713
+ * Written first and looked at second, against a holder that releases first and looks second:
5714
+ * whichever look runs last sees the other side's write. So when the run is no longer executing by
5715
+ * the time this re-reads it, the holder may have looked already, and this side takes the request
5716
+ * and wakes the run itself once the lease is free.
5717
+ */
5718
+ private requestWakeAfterTurn;
5719
+ /** After a turn that suspended released its lease: run again if a wake was kept for it. */
5720
+ private honorWakeRequests;
5721
+ /** Take every wake request kept for `runId`; true when there was one. */
5722
+ private takeWakeRequests;
5707
5723
  /**
5708
5724
  * A wake that found the run leased by another instance is dropped, and normally that is fine: the
5709
5725
  * holder's turn either sees what the wake wrote, or re-checks once it releases the lease
@@ -7102,7 +7118,11 @@ declare function scheduleRunIdAt(s: ScheduledWorkflow, nowMs: number): string;
7102
7118
  * Start each schedule's current-window run. The run id is the time bucket (a fixed-interval index,
7103
7119
  * or the cron fire time) and `engine.start` is idempotent, so firing this on an interval — or
7104
7120
  * racing two instances on the same tick — starts **each window exactly once**. Wire it to a
7105
- * `setInterval`, the durable timer poller, or `@nestjs/schedule`. Returns the run ids this call
7121
+ * `setInterval`, the durable timer poller, or `@nestjs/schedule`.
7122
+ *
7123
+ * Pass the SAME `engine` object on every call: which windows already fired, and the jittered fire
7124
+ * time of each deferred window, are remembered per engine object. A fresh wrapper per call would
7125
+ * redraw a deferred window's fire time on every tick, so it might never fire. Returns the run ids this call
7106
7126
  * started: a window that already has a run is not started or returned again, and once this process
7107
7127
  * has seen a window's run exist, later calls in that window skip it without a store read. A
7108
7128
  * jittered window is deferred to a later call, never slept on.
package/dist/index.js CHANGED
@@ -2532,6 +2532,7 @@ async function runSchedules(engine, schedules, nowMs, opts) {
2532
2532
  if (prev && (prev.status === "running" || prev.status === "suspended")) continue;
2533
2533
  }
2534
2534
  }
2535
+ if (!(s.jitter && s.jitter > 0)) jitterFireAt.get(engine)?.delete(s.key);
2535
2536
  const firing = s.jitter && s.jitter > 0 ? jitterDue(engine, s, candidates, nowMs, random) : candidates;
2536
2537
  if (firing.length === 0) continue;
2537
2538
  for (const runId of firing) {
@@ -3823,7 +3824,6 @@ function createWorkflowCtx(host, runId, compensations, workflow = "", searchAttr
3823
3824
  }));
3824
3825
  return unwrapCompletion(buffered.payload, `child "${id}"`);
3825
3826
  }
3826
- if (!await store.getRun(id)) host.startChild(workflowName(workflow2), input, id, priority, version);
3827
3827
  if (!existing) {
3828
3828
  await writeCheckpoint(instantCheckpoint({
3829
3829
  runId,
@@ -3833,6 +3833,7 @@ function createWorkflowCtx(host, runId, compensations, workflow = "", searchAttr
3833
3833
  status: "running"
3834
3834
  }));
3835
3835
  }
3836
+ if (!await store.getRun(id)) host.startChild(workflowName(workflow2), input, id, priority, version);
3836
3837
  throw new WorkflowSuspended(void 0, fanJoin([
3837
3838
  {
3838
3839
  waiterPrefix: `child:${id}`,
@@ -4451,6 +4452,7 @@ function deferred() {
4451
4452
  __name(deferred, "deferred");
4452
4453
  var pendingStateKey = /* @__PURE__ */ __name((at) => String(new Date(at).getTime()), "pendingStateKey");
4453
4454
  var freshPendingKey = /* @__PURE__ */ __name(() => `${Date.now()}-${globalThis.crypto.randomUUID().slice(0, 8)}`, "freshPendingKey");
4455
+ var wakeRequestToken = /* @__PURE__ */ __name((runId) => `durable:wake-after-turn:${runId}`, "wakeRequestToken");
4454
4456
  var CHILD_SIGNAL_PREFIX = "signal:child:";
4455
4457
  var DELIVER_CHUNK = 50;
4456
4458
  function childCompletion(child) {
@@ -7637,6 +7639,7 @@ var WorkflowEngine = class {
7637
7639
  const lockNow = this.clock();
7638
7640
  if (run.lockedBy !== this.instanceId || (run.lockedUntil ?? 0) <= lockNow) {
7639
7641
  if (!await this.store.tryLockRun(run.id, this.instanceId, lockNow + this.leaseMs, lockNow)) {
7642
+ if (!this.leaseRetries.has(run.id)) this.inBackground(this.requestWakeAfterTurn(run.id));
7640
7643
  this.recheckAfterLease(run.id, run.lockedUntil);
7641
7644
  return {
7642
7645
  runId: run.id,
@@ -7650,12 +7653,52 @@ var WorkflowEngine = class {
7650
7653
  if (registered?.singleton && (result.status === "completed" || result.status === "failed" || result.status === "cancelled" || result.status === "dead")) {
7651
7654
  void this.singletons.wakeNext(run).catch(() => void 0);
7652
7655
  }
7656
+ if (result.status === "suspended") this.inBackground(this.honorWakeRequests(run.id));
7657
+ else if (TERMINAL_RUN_STATUSES.includes(result.status)) {
7658
+ this.inBackground(this.takeWakeRequests(run.id));
7659
+ }
7653
7660
  return result;
7654
7661
  } finally {
7655
7662
  held.stop();
7656
7663
  }
7657
7664
  }
7658
7665
  /**
7666
+ * Keep a wake that found `runId`'s lease held by a turn executing elsewhere, durably, so that turn
7667
+ * runs again once it ends instead of suspending on a snapshot that predates what the wake brought
7668
+ * (a signal, a child's completion). The request is a buffered signal on a token no workflow waits
7669
+ * on; the holder takes it after it releases the lease ({@link honorWakeRequests}).
7670
+ *
7671
+ * Written first and looked at second, against a holder that releases first and looks second:
7672
+ * whichever look runs last sees the other side's write. So when the run is no longer executing by
7673
+ * the time this re-reads it, the holder may have looked already, and this side takes the request
7674
+ * and wakes the run itself once the lease is free.
7675
+ */
7676
+ async requestWakeAfterTurn(runId) {
7677
+ await this.store.bufferSignal(wakeRequestToken(runId), null);
7678
+ const run = await this.store.getRun(runId);
7679
+ if (run?.status === "running") return;
7680
+ if (!run) {
7681
+ await this.takeWakeRequests(runId);
7682
+ return;
7683
+ }
7684
+ if (!await this.takeWakeRequests(runId)) return;
7685
+ if (TERMINAL_RUN_STATUSES.includes(run.status)) return;
7686
+ this.recheckAfterLease(runId, run.lockedUntil);
7687
+ }
7688
+ /** After a turn that suspended released its lease: run again if a wake was kept for it. */
7689
+ async honorWakeRequests(runId) {
7690
+ if (!await this.takeWakeRequests(runId)) return;
7691
+ await this.wake(runId).catch((err) => {
7692
+ if (!(err instanceof NamespaceMismatch)) throw err;
7693
+ });
7694
+ }
7695
+ /** Take every wake request kept for `runId`; true when there was one. */
7696
+ async takeWakeRequests(runId) {
7697
+ let taken = false;
7698
+ while (await this.store.takeBufferedSignal(wakeRequestToken(runId))) taken = true;
7699
+ return taken;
7700
+ }
7701
+ /**
7659
7702
  * A wake that found the run leased by another instance is dropped, and normally that is fine: the
7660
7703
  * holder's turn either sees what the wake wrote, or re-checks once it releases the lease
7661
7704
  * ({@link redriveSettledSteps}, {@link redriveSettledChildren}). Not if the holder goes down first