@dudousxd/nestjs-durable-core 0.80.1 → 0.81.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.js CHANGED
@@ -928,6 +928,9 @@ async function assertConcurrency(store, workflow, quota) {
928
928
  __name(assertConcurrency, "assertConcurrency");
929
929
 
930
930
  // src/entities.ts
931
+ var NUDGE = {
932
+ nudge: true
933
+ };
931
934
  var ENTITY_WORKFLOW = "__entity";
932
935
  var entityRunId = /* @__PURE__ */ __name((name, key) => `entity:${name}:${key}`, "entityRunId");
933
936
  var Entities = class {
@@ -935,9 +938,11 @@ var Entities = class {
935
938
  __name(this, "Entities");
936
939
  }
937
940
  host;
941
+ workflowHost;
938
942
  configs = /* @__PURE__ */ new Map();
939
- constructor(host) {
943
+ constructor(host, workflowHost) {
940
944
  this.host = host;
945
+ this.workflowHost = workflowHost;
941
946
  host.register(ENTITY_WORKFLOW, "1", (ctx, input) => this.run(ctx, input));
942
947
  }
943
948
  register(name, config) {
@@ -959,6 +964,52 @@ var Entities = class {
959
964
  }
960
965
  });
961
966
  }
967
+ /**
968
+ * The durable half of a send from INSIDE a workflow (`ctx.callEntity` / `ctx.signalEntity`): write
969
+ * the op to the entity's signal buffer and return once it is stored, so the caller can checkpoint
970
+ * after it. The op carries `id`, the caller's stable `<runId>:<seq>`, so if the caller dies between
971
+ * this write and its checkpoint and sends again on replay, the entity skips the second copy. The
972
+ * entity's run is also ensured here, awaited (`start` only enqueues, it never runs the body
973
+ * inline), so an op sent on first contact can't be stranded in a buffer no run will ever read if
974
+ * the process dies right after. The wake-up ({@link kick}) is separate and may be lost: a busy
975
+ * entity reads the buffer at its next wait, and a parked one is paired with it by the engine's
976
+ * `reconcileMs` sweep. Returns `false` on a host without a workflow host.
977
+ */
978
+ async enqueue(name, key, op, arg, id, reply) {
979
+ if (!this.workflowHost) return false;
980
+ const message = {
981
+ op,
982
+ arg,
983
+ id,
984
+ ...reply ? {
985
+ reply
986
+ } : {}
987
+ };
988
+ const runId = entityRunId(name, key);
989
+ await this.workflowHost.bufferSignal(runId, message);
990
+ await this.workflowHost.ensureStarted(ENTITY_WORKFLOW, {
991
+ name,
992
+ key
993
+ }, runId);
994
+ return true;
995
+ }
996
+ /**
997
+ * Make the entity act on what {@link enqueue} buffered: wake its run if it is parked on its wait
998
+ * (the run itself was ensured by `enqueue`; ensured again here, idempotently, for a caller replay
999
+ * that only re-kicks). A run that is busy needs nothing: its next wait reads the buffer before
1000
+ * suspending. The wake carries a no-op `nudge`, never the op itself, so the op stays in
1001
+ * the buffer in arrival order and a lost or repeated kick costs at most one empty iteration.
1002
+ */
1003
+ async kick(name, key) {
1004
+ const host = this.workflowHost;
1005
+ if (!host) return;
1006
+ const runId = entityRunId(name, key);
1007
+ await host.ensureStarted(ENTITY_WORKFLOW, {
1008
+ name,
1009
+ key
1010
+ }, runId);
1011
+ await host.wakeWaiter(runId, NUDGE);
1012
+ }
962
1013
  async signal(name, key, op, arg) {
963
1014
  await this.dispatch(name, key, op, arg);
964
1015
  }
@@ -972,11 +1023,18 @@ var Entities = class {
972
1023
  if (!config) throw new FatalError(`entity "${name}" is not registered`);
973
1024
  const token = entityRunId(name, key);
974
1025
  let state = config.initialState();
1026
+ const seen = /* @__PURE__ */ new Set();
975
1027
  for (let i = 0; ; i += 1) {
976
1028
  const msg = await ctx.waitForSignal(token);
1029
+ if (msg.nudge) continue;
1030
+ if (msg.id !== void 0) {
1031
+ if (seen.has(msg.id)) continue;
1032
+ seen.add(msg.id);
1033
+ }
1034
+ const op = msg.op;
977
1035
  const out = await ctx.localStep(`op:${i}`, async () => {
978
- const handler = config.handlers[msg.op];
979
- if (!handler) throw new FatalError(`entity "${name}" has no handler for op "${msg.op}"`);
1036
+ const handler = config.handlers[op];
1037
+ if (!handler) throw new FatalError(`entity "${name}" has no handler for op "${op}"`);
980
1038
  const result = await handler(state, msg.arg);
981
1039
  return {
982
1040
  result,
@@ -986,8 +1044,9 @@ var Entities = class {
986
1044
  state = out.state;
987
1045
  await ctx.setEvent("state", state);
988
1046
  if (msg.reply) {
1047
+ const reply = msg.reply;
989
1048
  await ctx.localStep(`reply:${i}`, async () => {
990
- await this.host.signal(msg.reply, out.result);
1049
+ await this.host.signal(reply, out.result);
991
1050
  });
992
1051
  }
993
1052
  }
@@ -1911,6 +1970,10 @@ function checkpointNameKind(name) {
1911
1970
  return "step";
1912
1971
  }
1913
1972
  __name(checkpointNameKind, "checkpointNameKind");
1973
+ function poolNamespace(namespace) {
1974
+ return namespace === "" ? "default" : namespace;
1975
+ }
1976
+ __name(poolNamespace, "poolNamespace");
1914
1977
  var TERMINAL_RUN_STATUSES = [
1915
1978
  "completed",
1916
1979
  "failed",
@@ -2042,6 +2105,19 @@ var WEBHOOK_PREFIX = "wh:";
2042
2105
  var CHILD_PREFIX = "child:";
2043
2106
  var BREAKPOINT_PREFIX = "bp:";
2044
2107
  var EVENT_PREFIX = EVENT_TOKEN_PREFIX;
2108
+ function parseWebhookToken(token) {
2109
+ if (!token.startsWith(WEBHOOK_PREFIX)) return null;
2110
+ const rest = token.slice(WEBHOOK_PREFIX.length);
2111
+ const at = rest.lastIndexOf(":");
2112
+ if (at <= 0) return null;
2113
+ const seqText = rest.slice(at + 1);
2114
+ if (!/^\d+$/.test(seqText)) return null;
2115
+ return {
2116
+ runId: rest.slice(0, at),
2117
+ seq: Number(seqText)
2118
+ };
2119
+ }
2120
+ __name(parseWebhookToken, "parseWebhookToken");
2045
2121
  function classifyWaiterToken(token) {
2046
2122
  if (token.startsWith(WEBHOOK_PREFIX)) return {
2047
2123
  on: "webhook",
@@ -2579,6 +2655,11 @@ __name(errorMessage, "errorMessage");
2579
2655
 
2580
2656
  // src/singleton-admission.ts
2581
2657
  var SINGLETON_ADMITTED_TAG = "durable:singleton:admitted";
2658
+ var SINGLETON_TRACKED_TAG = "durable:singleton:tracked";
2659
+ var SINGLETON_RESERVED_TAGS = [
2660
+ SINGLETON_ADMITTED_TAG,
2661
+ SINGLETON_TRACKED_TAG
2662
+ ];
2582
2663
  var SINGLETON_ACTIVE_STATUSES = [
2583
2664
  "pending",
2584
2665
  "running",
@@ -2586,6 +2667,33 @@ var SINGLETON_ACTIVE_STATUSES = [
2586
2667
  "blocked",
2587
2668
  "cancelling"
2588
2669
  ];
2670
+ var LEGACY_SCAN_STATUSES = [
2671
+ "running",
2672
+ "suspended",
2673
+ "cancelling"
2674
+ ];
2675
+ var byAdmissionOrder = /* @__PURE__ */ __name((a, b) => a.createdAt.getTime() - b.createdAt.getTime() || a.id.localeCompare(b.id), "byAdmissionOrder");
2676
+ function withoutSingletonMarkers(tags) {
2677
+ return tags?.filter((tag) => !SINGLETON_RESERVED_TAGS.includes(tag));
2678
+ }
2679
+ __name(withoutSingletonMarkers, "withoutSingletonMarkers");
2680
+ function withSingletonTracked(tags) {
2681
+ const current = tags ?? [];
2682
+ if (current.includes(SINGLETON_ADMITTED_TAG) || current.includes(SINGLETON_TRACKED_TAG)) {
2683
+ return current;
2684
+ }
2685
+ return [
2686
+ ...current,
2687
+ SINGLETON_TRACKED_TAG
2688
+ ];
2689
+ }
2690
+ __name(withSingletonTracked, "withSingletonTracked");
2691
+ function legacySingletonHolders(active, runId, limit) {
2692
+ const scanned = active.filter((run) => LEGACY_SCAN_STATUSES.includes(run.status)).sort(byAdmissionOrder);
2693
+ const window = new Set(scanned.slice(0, limit).map((run) => run.id));
2694
+ return scanned.filter((run) => run.id !== runId && !run.tags?.includes(SINGLETON_ADMITTED_TAG) && (!run.tags?.includes(SINGLETON_TRACKED_TAG) || window.has(run.id)));
2695
+ }
2696
+ __name(legacySingletonHolders, "legacySingletonHolders");
2589
2697
  function singletonAdmissionTags(runs, runId, tag, workflow, limit) {
2590
2698
  if (!Number.isInteger(limit) || limit < 1) throw new Error("singleton limit must be a positive integer");
2591
2699
  const active = runs.filter((run) => run.workflow === workflow && SINGLETON_ACTIVE_STATUSES.includes(run.status) && run.tags?.includes(tag));
@@ -2594,12 +2702,13 @@ function singletonAdmissionTags(runs, runId, tag, workflow, limit) {
2594
2702
  if (candidate.tags?.includes(SINGLETON_ADMITTED_TAG)) return candidate.tags;
2595
2703
  if (candidate.status === "cancelling") return null;
2596
2704
  const holders = active.filter((run) => run.tags?.includes(SINGLETON_ADMITTED_TAG));
2597
- const remaining = limit - holders.length;
2705
+ const legacy = new Set(legacySingletonHolders(active, runId, limit).map((run) => run.id));
2706
+ const remaining = limit - holders.length - legacy.size;
2598
2707
  if (remaining <= 0) return null;
2599
- const waiters = active.filter((run) => !run.tags?.includes(SINGLETON_ADMITTED_TAG)).sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime() || a.id.localeCompare(b.id));
2708
+ const waiters = active.filter((run) => !run.tags?.includes(SINGLETON_ADMITTED_TAG) && !legacy.has(run.id)).sort(byAdmissionOrder);
2600
2709
  if (!waiters.slice(0, remaining).some((run) => run.id === runId)) return null;
2601
2710
  return [
2602
- ...candidate.tags ?? [],
2711
+ ...(candidate.tags ?? []).filter((t) => t !== SINGLETON_TRACKED_TAG),
2603
2712
  SINGLETON_ADMITTED_TAG
2604
2713
  ];
2605
2714
  }
@@ -2609,7 +2718,7 @@ async function clearTerminalSingletonAdmission(runId, patch, getRun) {
2609
2718
  const tags = "tags" in patch ? patch.tags : (await getRun(runId))?.tags;
2610
2719
  return {
2611
2720
  ...patch,
2612
- tags: tags?.filter((tag) => tag !== SINGLETON_ADMITTED_TAG)
2721
+ tags: withoutSingletonMarkers(tags)
2613
2722
  };
2614
2723
  }
2615
2724
  __name(clearTerminalSingletonAdmission, "clearTerminalSingletonAdmission");
@@ -2620,6 +2729,7 @@ function singletonAdmissionDenialPatch(runs, runId, tag, workflow, retryWakeAt)
2620
2729
  return {
2621
2730
  status: "suspended",
2622
2731
  wakeAt: retryWakeAt,
2732
+ tags: withSingletonTracked(candidate.tags),
2623
2733
  updatedAt: /* @__PURE__ */ new Date()
2624
2734
  };
2625
2735
  }
@@ -2672,7 +2782,7 @@ var SingletonGate = class {
2672
2782
  const admitted = await this.deps.store.tryAdmitSingleton(run.id, this.tag(cfg, run.input), run.workflow, cfg.limit ?? 1, this.retryWakeAt());
2673
2783
  if (admitted && !run.tags?.includes(SINGLETON_ADMITTED_TAG)) {
2674
2784
  run.tags = [
2675
- ...run.tags ?? [],
2785
+ ...(run.tags ?? []).filter((tag) => tag !== SINGLETON_TRACKED_TAG),
2676
2786
  SINGLETON_ADMITTED_TAG
2677
2787
  ];
2678
2788
  }
@@ -3567,30 +3677,82 @@ function createWorkflowCtx(host, runId, compensations, workflow = "", searchAttr
3567
3677
  }));
3568
3678
  return id;
3569
3679
  }, "startChild");
3680
+ const sendEntityOp = /* @__PURE__ */ __name(async (name, key, op, arg, seq, reply) => await host.enqueueEntityOp?.(name, key, op, arg, `${runId}:${seq}`, reply) ?? false, "sendEntityOp");
3570
3681
  const callEntity = /* @__PURE__ */ __name(async (name, key, op, arg) => {
3571
3682
  const current = pos.next();
3572
- const existing = await readCheckpoint(current);
3683
+ let existing = await readCheckpoint(current);
3573
3684
  if (existing && existing.status === "completed") return existing.output;
3574
3685
  const reply = `entityreply:${runId}:${current}`;
3686
+ const intentName = `signal:${reply}`;
3687
+ if (existing && existing.name !== intentName) {
3688
+ throw new NonDeterminismError(runId, current, intentName, existing.name);
3689
+ }
3690
+ if (existing && replay?.has(current)) {
3691
+ existing = await store.getCheckpoint(runId, current) ?? existing;
3692
+ if (existing.status === "completed") return existing.output;
3693
+ }
3694
+ const markSent = /* @__PURE__ */ __name(() => store.saveCheckpointIfPending?.(instantCheckpoint({
3695
+ runId,
3696
+ seq: current,
3697
+ name: intentName,
3698
+ kind: "signal",
3699
+ status: "pending",
3700
+ output: {
3701
+ sent: true
3702
+ }
3703
+ })) ?? Promise.resolve(false), "markSent");
3704
+ if (existing) {
3705
+ const buffered = await consumeBuffered(reply, current);
3706
+ if (buffered) {
3707
+ await store.removeSignalWaiter({
3708
+ token: reply,
3709
+ runId,
3710
+ seq: current
3711
+ });
3712
+ return buffered.value;
3713
+ }
3714
+ if (existing.output?.sent !== true) {
3715
+ if (await sendEntityOp(name, key, op, arg, current, reply)) await markSent();
3716
+ }
3717
+ host.kickEntity?.(name, key);
3718
+ throw new WorkflowSuspended();
3719
+ }
3575
3720
  await store.putSignalWaiter({
3576
3721
  token: reply,
3577
3722
  runId,
3578
3723
  seq: current
3579
3724
  });
3580
- host.signalEntity?.(name, key, op, arg, reply);
3725
+ if (!host.enqueueEntityOp) {
3726
+ host.signalEntity?.(name, key, op, arg, reply);
3727
+ throw new WorkflowSuspended();
3728
+ }
3729
+ await writeCheckpoint(instantCheckpoint({
3730
+ runId,
3731
+ seq: current,
3732
+ name: intentName,
3733
+ kind: "signal",
3734
+ status: "pending",
3735
+ output: {
3736
+ sent: false
3737
+ }
3738
+ }));
3739
+ if (await sendEntityOp(name, key, op, arg, current, reply)) await markSent();
3740
+ host.kickEntity?.(name, key);
3581
3741
  throw new WorkflowSuspended();
3582
3742
  }, "callEntity");
3583
3743
  const signalEntity = /* @__PURE__ */ __name(async (name, key, op, arg) => {
3584
3744
  const current = pos.next();
3585
3745
  const existing = await readCheckpoint(current);
3586
3746
  if (existing && existing.status === "completed") return;
3587
- host.signalEntity?.(name, key, op, arg);
3747
+ const durable = await sendEntityOp(name, key, op, arg, current);
3748
+ if (!durable) host.signalEntity?.(name, key, op, arg);
3588
3749
  await writeCheckpoint(instantCheckpoint({
3589
3750
  runId,
3590
3751
  seq: current,
3591
3752
  name: `entitysig:${name}:${key}`,
3592
3753
  kind: "local"
3593
3754
  }));
3755
+ if (durable) host.kickEntity?.(name, key);
3594
3756
  }, "signalEntity");
3595
3757
  const breakpoint = /* @__PURE__ */ __name(async (label) => {
3596
3758
  const current = pos.next();
@@ -3658,17 +3820,15 @@ function createWorkflowCtx(host, runId, compensations, workflow = "", searchAttr
3658
3820
  const deadlineSeq = timeoutMs == null ? -1 : pos.next();
3659
3821
  const existing = await readCheckpoint(current);
3660
3822
  if (existing && existing.status === "completed") return existing.output;
3661
- if (timeoutMs == null) {
3662
- await store.putSignalWaiter({
3823
+ const buffered = await consumeBuffered(token, current);
3824
+ if (buffered) return buffered.value;
3825
+ const deadline = timeoutMs == null ? void 0 : await stampDeadline(deadlineSeq, token, timeoutMs);
3826
+ if (deadline !== void 0 && host.clock() >= deadline) {
3827
+ await store.removeSignalWaiter({
3663
3828
  token,
3664
3829
  runId,
3665
3830
  seq: current
3666
- });
3667
- throw new WorkflowSuspended();
3668
- }
3669
- const deadline = await stampDeadline(deadlineSeq, token, timeoutMs);
3670
- if (host.clock() >= deadline) {
3671
- await store.takeSignalWaiter(token).catch(() => void 0);
3831
+ }).catch(() => void 0);
3672
3832
  throw new SignalTimeoutError(token, timeoutMs);
3673
3833
  }
3674
3834
  await store.putSignalWaiter({
@@ -3676,6 +3836,15 @@ function createWorkflowCtx(host, runId, compensations, workflow = "", searchAttr
3676
3836
  runId,
3677
3837
  seq: current
3678
3838
  });
3839
+ const lateBuffered = await consumeBuffered(token, current);
3840
+ if (lateBuffered) {
3841
+ await store.removeSignalWaiter({
3842
+ token,
3843
+ runId,
3844
+ seq: current
3845
+ });
3846
+ return lateBuffered.value;
3847
+ }
3679
3848
  throw new WorkflowSuspended(deadline);
3680
3849
  }, "wait");
3681
3850
  return {
@@ -3867,6 +4036,12 @@ var SETTLE_ELIGIBLE_STATUSES = [
3867
4036
  "failed",
3868
4037
  "cancelling"
3869
4038
  ];
4039
+ var TIMEOUT_SWEPT_STATUSES = [
4040
+ "pending",
4041
+ "running",
4042
+ "suspended",
4043
+ "blocked"
4044
+ ];
3870
4045
  var SUSPEND_ELIGIBLE_STATUSES = SETTLE_ELIGIBLE_STATUSES.filter((status) => status !== "cancelling");
3871
4046
  var RECOVERY_BATCH = 500;
3872
4047
  var NamespaceMismatch = class NamespaceMismatch2 extends Error {
@@ -3889,6 +4064,12 @@ var WorkflowEngine = class {
3889
4064
  instanceId;
3890
4065
  namespace;
3891
4066
  leaseMs;
4067
+ /**
4068
+ * Leases held by this `instanceId` that expire strictly before this epoch ms were taken by a
4069
+ * PREVIOUS process with the same id (every lease this one takes or renews expires at
4070
+ * `clock() + leaseMs`, so at or after it). See {@link orphanedRuns}.
4071
+ */
4072
+ ownLeaseCutoffMs;
3892
4073
  maxRecoveryAttempts;
3893
4074
  remoteAdvanceSilenceMs;
3894
4075
  reconcileMs;
@@ -4010,8 +4191,9 @@ var WorkflowEngine = class {
4010
4191
  this.admission = deps.admission ?? new InMemoryAdmissionBackend(this.clock);
4011
4192
  this.admission.onFreed?.((queue) => this.wakeQueueWaiters(queue));
4012
4193
  this.instanceId = deps.instanceId ?? globalThis.crypto.randomUUID();
4013
- this.namespace = deps.namespace;
4194
+ this.namespace = poolNamespace(deps.namespace);
4014
4195
  this.leaseMs = deps.leaseMs ?? 3e4;
4196
+ this.ownLeaseCutoffMs = this.clock() + this.leaseMs;
4015
4197
  this.maxRecoveryAttempts = deps.maxRecoveryAttempts;
4016
4198
  this.remoteAdvanceSilenceMs = deps.remoteAdvanceSilenceMs;
4017
4199
  this.reconcileMs = deps.reconcileMs === void 0 ? 3e5 : deps.reconcileMs || void 0;
@@ -4131,7 +4313,11 @@ var WorkflowEngine = class {
4131
4313
  }
4132
4314
  });
4133
4315
  this.accumulators = new EventAccumulators(this);
4134
- this.entities = new Entities(this);
4316
+ this.entities = new Entities(this, {
4317
+ bufferSignal: /* @__PURE__ */ __name((token, payload) => this.store.bufferSignal(token, payload), "bufferSignal"),
4318
+ wakeWaiter: /* @__PURE__ */ __name((token, payload) => this.wakeSignalWaiter(token, payload), "wakeWaiter"),
4319
+ ensureStarted: /* @__PURE__ */ __name((workflow, input, runId) => this.start(workflow, input, runId), "ensureStarted")
4320
+ });
4135
4321
  }
4136
4322
  /**
4137
4323
  * Register a **durable entity** (a virtual object): a keyed actor whose `handlers` run **serialized
@@ -4407,7 +4593,7 @@ var WorkflowEngine = class {
4407
4593
  // tenant-suffixed group the run will dispatch to — an operator starting a tenant's run via the
4408
4594
  // `onStartRun` wire (opts.namespace = tenant) must find the live `<workflow>@<tenant>` group,
4409
4595
  // not the bare group. Without this the start-run entry path can't reach a suffixed-only worker.
4410
- namespace: opts?.namespace ?? this.namespace,
4596
+ namespace: poolNamespace(opts?.namespace) ?? this.namespace,
4411
4597
  input,
4412
4598
  createdAt: /* @__PURE__ */ new Date(),
4413
4599
  updatedAt: /* @__PURE__ */ new Date()
@@ -4444,7 +4630,7 @@ var WorkflowEngine = class {
4444
4630
  VERSION_UNDECLARED_TAG
4445
4631
  ] : []
4446
4632
  ];
4447
- const tags = mergeTags(registered.tags, stampedTags)?.filter((tag) => tag !== SINGLETON_ADMITTED_TAG);
4633
+ const tags = mergeTags(registered.tags, stampedTags)?.filter((tag) => !SINGLETON_RESERVED_TAGS.includes(tag));
4448
4634
  if (registered.singleton) {
4449
4635
  await this.singletons.assertCapacity(name, registered.singleton, input);
4450
4636
  }
@@ -4458,7 +4644,7 @@ var WorkflowEngine = class {
4458
4644
  ...delayed ? {
4459
4645
  wakeAt: startAtMs
4460
4646
  } : {},
4461
- namespace: opts?.namespace ?? this.namespace,
4647
+ namespace: poolNamespace(opts?.namespace) ?? this.namespace,
4462
4648
  // DERIVED from the registration, exactly like `namespace` is derived from this engine — there is
4463
4649
  // deliberately no `opts.origin`, so a caller cannot label its run as another package's work.
4464
4650
  // `undefined` when the registration carried none (remote/convention/synthesized child, or a
@@ -4640,12 +4826,12 @@ var WorkflowEngine = class {
4640
4826
  error: run.error
4641
4827
  };
4642
4828
  }
4643
- if (this.namespace !== void 0 && run.namespace !== void 0 && run.namespace !== this.namespace) {
4829
+ if (this.namespace !== void 0 && run.namespace !== void 0 && poolNamespace(run.namespace) !== this.namespace) {
4644
4830
  await this.store.releaseRunLock(runId);
4645
4831
  throw new NamespaceMismatch();
4646
4832
  }
4647
- if (run.status === "failed" && run.tags?.includes(SINGLETON_ADMITTED_TAG)) {
4648
- run.tags = run.tags.filter((tag) => tag !== SINGLETON_ADMITTED_TAG);
4833
+ if (run.status === "failed" && (run.tags?.includes(SINGLETON_ADMITTED_TAG) || run.tags?.includes(SINGLETON_TRACKED_TAG))) {
4834
+ run.tags = withoutSingletonMarkers(run.tags);
4649
4835
  await this.store.updateRun(run.id, {
4650
4836
  tags: run.tags
4651
4837
  });
@@ -4864,6 +5050,15 @@ var WorkflowEngine = class {
4864
5050
  * `execution_timeout` error (terminal, so a late step result can't resurrect it), AND its children
4865
5051
  * are cascaded exactly as an explicit {@link cancel} cascades them.
4866
5052
  *
5053
+ * WHICH RUNS. Every run not yet finished, counted from `createdAt`: `pending` and `blocked` as well
5054
+ * as `running` and `suspended`. A run nobody picks up (no worker on its queue) or nobody can run (no
5055
+ * worker with its capabilities) is as stuck as one that hangs mid-body, and the cap is the backstop
5056
+ * for exactly that — leaving them out meant a run that never started lived forever while one that
5057
+ * started and hung was cut off. Cancelling one a worker is picking up at that moment is safe: the
5058
+ * worker's settle only writes over a live status ({@link SETTLE_ELIGIBLE_STATUSES}), so it can't
5059
+ * bring the run back. `cancelling` is left alone on purpose — a compensating cancel's saga undo is
5060
+ * running, it ends `cancelled` by itself, and cutting it short would skip the undo.
5061
+ *
4867
5062
  * WHY THE TERMINAL WRITE IS DIRECT, not a call to {@link cancel}. Two reasons, both still standing:
4868
5063
  * the run must land with the `execution_timeout` error `cancel` knows nothing about (`cancel` writes
4869
5064
  * a plain `cancelled`, and the code is what a dashboard/alert distinguishes a timeout by), and this
@@ -4876,8 +5071,8 @@ var WorkflowEngine = class {
4876
5071
  * children kept running with the parent gone and nothing pointing at them — a silent orphan you
4877
5072
  * could only find by reading the runs table by hand. {@link cancelChildren} is the SAME recursive
4878
5073
  * walk `cancel` uses, so children of children go too, and it inherits that path's two guards:
4879
- * `cancel` returns early on a run that is already `completed`/`cancelled`/`dead` (a child that beat
4880
- * the sweep to the finish is never clobbered, and a re-cancel is a no-op), and it writes the
5074
+ * `cancel` returns early on a run that is already terminal (a child that beat the sweep to the
5075
+ * finish is never clobbered, and a re-cancel is a no-op), and it writes the
4881
5076
  * parent's terminal status BEFORE recursing, so a cyclic parent→child graph terminates on the
4882
5077
  * second visit instead of looping. Two pollers sweeping the same run concurrently therefore write
4883
5078
  * the same terminal state twice and cascade into already-cancelled children — idempotent, not
@@ -4893,18 +5088,11 @@ var WorkflowEngine = class {
4893
5088
  if (this.draining) return;
4894
5089
  if (reg.executionTimeoutMs == null) continue;
4895
5090
  const deadline = now - reg.executionTimeoutMs;
4896
- const inflight = [
4897
- ...await this.store.listRuns({
4898
- workflow: reg.name,
4899
- status: "running",
4900
- namespace: this.namespace
4901
- }),
4902
- ...await this.store.listRuns({
4903
- workflow: reg.name,
4904
- status: "suspended",
4905
- namespace: this.namespace
4906
- })
4907
- ];
5091
+ const inflight = await this.store.listRuns({
5092
+ workflow: reg.name,
5093
+ statuses: TIMEOUT_SWEPT_STATUSES,
5094
+ namespace: this.namespace
5095
+ });
4908
5096
  for (const run of inflight) {
4909
5097
  if (this.draining) return;
4910
5098
  if (run.createdAt.getTime() > deadline) continue;
@@ -4937,48 +5125,103 @@ var WorkflowEngine = class {
4937
5125
  const results = [];
4938
5126
  for (const listed of await this.orphanedRuns(nowMs)) {
4939
5127
  if (this.draining) break;
4940
- const acquired = await this.store.tryLockRun(listed.id, this.instanceId, nowMs + this.leaseMs, nowMs);
4941
- if (!acquired) continue;
4942
- const run = await this.store.getRun(listed.id);
4943
- if (!run || run.status !== "running" && run.status !== "cancelling") {
4944
- await this.store.releaseRunLock(listed.id);
4945
- continue;
4946
- }
4947
- if (run.status === "running") {
4948
- const dispatched = await this.inFlightRemoteStep(run.id);
4949
- if (dispatched) {
4950
- await this.store.updateRun(run.id, {
4951
- status: "suspended",
4952
- wakeAt: this.reconcileWakeAt(dispatched.wakeAt ?? void 0),
4953
- updatedAt: /* @__PURE__ */ new Date()
4954
- });
4955
- await this.store.releaseRunLock(run.id);
4956
- results.push({
4957
- runId: run.id,
4958
- status: "suspended"
4959
- });
4960
- continue;
4961
- }
4962
- }
4963
- const settled = await this.countRecovery(run);
4964
- if (settled) {
4965
- results.push(settled);
4966
- continue;
4967
- }
4968
- await this.store.releaseRunLock(run.id);
4969
- if (run.status !== "cancelling") {
5128
+ const outcome = this.isOwnLiveLease(listed, nowMs) ? await this.reclaimOwnLease(listed, nowMs) : await this.recoverOrphan(listed, nowMs);
5129
+ if (!outcome) continue;
5130
+ if (outcome.dispatch) await this.runDispatcher.dispatch(outcome.result.runId);
5131
+ results.push(outcome.result);
5132
+ }
5133
+ return results;
5134
+ }
5135
+ /** A run whose still-live lease is held by this very `instanceId` (see {@link reclaimOwnLease}). */
5136
+ isOwnLiveLease(run, nowMs) {
5137
+ return run.lockedBy === this.instanceId && (run.lockedUntil ?? 0) > nowMs;
5138
+ }
5139
+ /**
5140
+ * Recover a run whose lease is still live but held by THIS `instanceId` and predates this process
5141
+ * (listed by {@link orphanedRuns}): the previous process with this stable id died without a
5142
+ * graceful shutdown (OOM, SIGKILL), so nothing is renewing that lease and waiting for it to expire
5143
+ * only delays the run. The lease is taken back with `renewRunLock` (a compare-and-set on the
5144
+ * owner; `tryLockRun` refuses a live lease), so the lease alone cannot fence a live execution in
5145
+ * THIS process from its own recovery: the in-process run gate does. A run with a
5146
+ * gate is executing (or about to) here and is skipped; otherwise a gate is held while the run is
5147
+ * recovered, so a resume landing meanwhile queues behind the recovery instead of running beside it.
5148
+ * The gate is released before the re-dispatch, which may resume the run on this instance.
5149
+ */
5150
+ async reclaimOwnLease(listed, nowMs) {
5151
+ if (this.runGates.has(listed.id)) return void 0;
5152
+ let release;
5153
+ const tail = new Promise((resolve) => {
5154
+ release = resolve;
5155
+ });
5156
+ const gate = {
5157
+ tail
5158
+ };
5159
+ this.runGates.set(listed.id, gate);
5160
+ this.closeGateAfter(listed.id, gate, tail);
5161
+ let outcome;
5162
+ try {
5163
+ const fresh = await this.store.getRun(listed.id);
5164
+ const liveHere = fresh?.lockedBy === this.instanceId && (fresh.lockedUntil ?? 0) > nowMs && (fresh.lockedUntil ?? 0) >= this.ownLeaseCutoffMs;
5165
+ if (fresh && !liveHere) outcome = await this.recoverOrphan(listed, nowMs, true);
5166
+ } finally {
5167
+ release(outcome?.result ?? {
5168
+ runId: listed.id,
5169
+ status: listed.status
5170
+ });
5171
+ }
5172
+ return outcome;
5173
+ }
5174
+ /**
5175
+ * One run of a {@link recoverIncomplete} pass: take its lease and decide on it. Resolves the
5176
+ * pass's result for it and whether it must be re-dispatched, or `undefined` when it was not ours.
5177
+ * `ownLease`: the run's live lease is this instance id's own, left by a previous process (see
5178
+ * {@link reclaimOwnLease}), so it is taken back by owner rather than acquired as a free one.
5179
+ */
5180
+ async recoverOrphan(listed, nowMs, ownLease = false) {
5181
+ const acquired = ownLease ? await this.store.renewRunLock(listed.id, this.instanceId, nowMs + this.leaseMs) : await this.store.tryLockRun(listed.id, this.instanceId, nowMs + this.leaseMs, nowMs);
5182
+ if (!acquired) return void 0;
5183
+ const run = await this.store.getRun(listed.id);
5184
+ if (!run || run.status !== "running" && run.status !== "cancelling") {
5185
+ await this.store.releaseRunLock(listed.id);
5186
+ return void 0;
5187
+ }
5188
+ if (run.status === "running") {
5189
+ const dispatched = await this.inFlightRemoteStep(run.id);
5190
+ if (dispatched) {
4970
5191
  await this.store.updateRun(run.id, {
4971
- status: "pending",
5192
+ status: "suspended",
5193
+ wakeAt: this.reconcileWakeAt(dispatched.wakeAt ?? void 0),
4972
5194
  updatedAt: /* @__PURE__ */ new Date()
4973
5195
  });
5196
+ await this.store.releaseRunLock(run.id);
5197
+ return {
5198
+ result: {
5199
+ runId: run.id,
5200
+ status: "suspended"
5201
+ },
5202
+ dispatch: false
5203
+ };
4974
5204
  }
4975
- await this.runDispatcher.dispatch(run.id);
4976
- results.push({
4977
- runId: run.id,
4978
- status: run.status === "cancelling" ? "cancelling" : "pending"
5205
+ }
5206
+ const settled = await this.countRecovery(run);
5207
+ if (settled) return {
5208
+ result: settled,
5209
+ dispatch: false
5210
+ };
5211
+ await this.store.releaseRunLock(run.id);
5212
+ if (run.status !== "cancelling") {
5213
+ await this.store.updateRun(run.id, {
5214
+ status: "pending",
5215
+ updatedAt: /* @__PURE__ */ new Date()
4979
5216
  });
4980
5217
  }
4981
- return results;
5218
+ return {
5219
+ result: {
5220
+ runId: run.id,
5221
+ status: run.status === "cancelling" ? "cancelling" : "pending"
5222
+ },
5223
+ dispatch: true
5224
+ };
4982
5225
  }
4983
5226
  /**
4984
5227
  * The runs {@link recoverIncomplete} should try to reclaim: incomplete runs whose lease is free or
@@ -4988,12 +5231,20 @@ var WorkflowEngine = class {
4988
5231
  * ({@link StateStore.listOrphanedRuns}, bounded by {@link RECOVERY_BATCH}); else the full list is
4989
5232
  * filtered here, which still spares the probes. Shuffled, like the due-timer pass, so instances
4990
5233
  * recovering at once spread over the batch instead of contending for the same leases.
5234
+ *
5235
+ * Also listed: runs still leased by THIS `instanceId` with a lease taken before this process
5236
+ * booted ({@link ownLeaseCutoffMs}). With a stable `instanceId` (pinned to the pod identity), that
5237
+ * is the previous process, killed without a graceful shutdown; see {@link reclaimOwnLease}.
4991
5238
  */
4992
5239
  async orphanedRuns(nowMs) {
5240
+ const ownedBy = {
5241
+ owner: this.instanceId,
5242
+ lockedBefore: this.ownLeaseCutoffMs
5243
+ };
4993
5244
  if (this.store.listOrphanedRuns) {
4994
- return shuffle(await this.store.listOrphanedRuns(nowMs, RECOVERY_BATCH, this.namespace));
5245
+ return shuffle(await this.store.listOrphanedRuns(nowMs, RECOVERY_BATCH, this.namespace, ownedBy));
4995
5246
  }
4996
- return shuffle((await this.store.listIncompleteRuns(this.namespace)).filter((run) => run.lockedUntil == null || run.lockedUntil <= nowMs).sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime()).slice(0, RECOVERY_BATCH));
5247
+ return shuffle((await this.store.listIncompleteRuns(this.namespace)).filter((run) => run.lockedUntil == null || run.lockedUntil <= nowMs || run.lockedBy === ownedBy.owner && run.lockedUntil < ownedBy.lockedBefore).sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime()).slice(0, RECOVERY_BATCH));
4997
5248
  }
4998
5249
  /**
4999
5250
  * The remote step this run dispatched and is still awaiting a result for, if any — the signature
@@ -5036,6 +5287,7 @@ var WorkflowEngine = class {
5036
5287
  error: error2,
5037
5288
  recoveryAttempts: attempts
5038
5289
  });
5290
+ this.notifyParent(run.id, failedCompletion(error2));
5039
5291
  return {
5040
5292
  runId: run.id,
5041
5293
  status: "dead",
@@ -5242,7 +5494,7 @@ var WorkflowEngine = class {
5242
5494
  error: void 0,
5243
5495
  ...resurrecting ? {
5244
5496
  recoveryAttempts: 0,
5245
- tags: run.tags?.filter((tag) => tag !== SINGLETON_ADMITTED_TAG)
5497
+ tags: withoutSingletonMarkers(run.tags)
5246
5498
  } : {},
5247
5499
  updatedAt: /* @__PURE__ */ new Date()
5248
5500
  });
@@ -5654,6 +5906,16 @@ var WorkflowEngine = class {
5654
5906
  return this.deliverSignal(lateWaiter, reclaimed.payload);
5655
5907
  }
5656
5908
  /**
5909
+ * Wake the run parked on `token` with `payload`, if there is one; otherwise do nothing — unlike
5910
+ * {@link signal}, it never buffers. For a wake-up whose real payload is ALREADY buffered on the
5911
+ * token (an entity op, see `Entities.kick`): a run that isn't parked reads that buffer at its next
5912
+ * wait, so there is nothing to keep.
5913
+ */
5914
+ async wakeSignalWaiter(token, payload) {
5915
+ const waiter = await this.store.takeSignalWaiter(token);
5916
+ return waiter ? this.deliverSignal(waiter, payload) : null;
5917
+ }
5918
+ /**
5657
5919
  * Ensure a run exists for `runId`, then deliver a signal to it — atomically race-free thanks to
5658
5920
  * signal buffering: if the run is new (or busy / not yet waiting), the signal is buffered and
5659
5921
  * consumed when it reaches `waitForSignal(token)`. The canonical **durable-entity / accumulator**
@@ -6249,6 +6511,25 @@ var WorkflowEngine = class {
6249
6511
  });
6250
6512
  }
6251
6513
  /** Admission persists the wait atomically; never overwrite a cancellation after it returns. */
6514
+ /**
6515
+ * A run's first execution: `pending` -> `running`. A singleton run not yet admitted is stamped
6516
+ * {@link SINGLETON_TRACKED_TAG} in the same write, BEFORE it competes for a slot, so a run this
6517
+ * replica is about to admit or deny is never mistaken for a holder admitted by a replica older
6518
+ * than 0.73 (see `legacySingletonHolders`).
6519
+ */
6520
+ async markStarted(run, singleton) {
6521
+ const tags = singleton ? withSingletonTracked(run.tags) : run.tags;
6522
+ const retag = tags !== run.tags;
6523
+ await this.store.updateRun(run.id, {
6524
+ status: "running",
6525
+ ...retag ? {
6526
+ tags
6527
+ } : {},
6528
+ updatedAt: /* @__PURE__ */ new Date()
6529
+ });
6530
+ run.status = "running";
6531
+ if (retag) run.tags = tags;
6532
+ }
6252
6533
  async singletonWaitResult(run) {
6253
6534
  const current = await this.store.getRun(run.id);
6254
6535
  await this.store.releaseRunLock(run.id);
@@ -6622,11 +6903,7 @@ ${run.workflowVersion}`;
6622
6903
  }
6623
6904
  const remote = registered.remote;
6624
6905
  if (run.status === "pending") {
6625
- await this.store.updateRun(run.id, {
6626
- status: "running",
6627
- updatedAt: /* @__PURE__ */ new Date()
6628
- });
6629
- run.status = "running";
6906
+ await this.markStarted(run, registered?.singleton !== void 0);
6630
6907
  this.emit({
6631
6908
  type: "run.started",
6632
6909
  runId: run.id,
@@ -7242,11 +7519,7 @@ ${run.workflowVersion}`;
7242
7519
  async runExecutionTurn(run, fn, registered) {
7243
7520
  if (run.status === "cancelling") this.cancelRequested.add(run.id);
7244
7521
  if (run.status === "pending") {
7245
- await this.store.updateRun(run.id, {
7246
- status: "running",
7247
- updatedAt: /* @__PURE__ */ new Date()
7248
- });
7249
- run.status = "running";
7522
+ await this.markStarted(run, registered?.singleton !== void 0);
7250
7523
  this.emit({
7251
7524
  type: "run.started",
7252
7525
  runId: run.id,
@@ -7622,6 +7895,17 @@ ${run.workflowVersion}`;
7622
7895
  signalEntity: /* @__PURE__ */ __name((name, key, op, arg, reply) => {
7623
7896
  queueMicrotask(() => void this.entities.dispatch(name, key, op, arg, reply).catch(() => void 0));
7624
7897
  }, "signalEntity"),
7898
+ // The durable half of a workflow-side entity send: awaited inside the turn (a store write
7899
+ // only, never an inline entity turn), so the ctx can checkpoint after it. See Entities.enqueue.
7900
+ enqueueEntityOp: /* @__PURE__ */ __name((name, key, op, arg, id, reply) => this.entities.enqueue(name, key, op, arg, id, reply), "enqueueEntityOp"),
7901
+ // Start/wake the entity for what enqueueEntityOp buffered — deferred like signalEntity above,
7902
+ // so it never re-enters a still-executing turn. A failed wake loses nothing: the op stays
7903
+ // buffered for the entity's next wait (and recovery's buffered-entity wake).
7904
+ kickEntity: /* @__PURE__ */ __name((name, key) => {
7905
+ queueMicrotask(() => void this.entities.kick(name, key).catch((err) => {
7906
+ console.warn(`[nestjs-durable] waking entity "${name}" key "${key}" failed (its op stays buffered for the next wake):`, err);
7907
+ }));
7908
+ }, "kickEntity"),
7625
7909
  interceptStep: /* @__PURE__ */ __name((invocation, body) => this.interceptStep(invocation, body), "interceptStep")
7626
7910
  };
7627
7911
  }
@@ -7763,7 +8047,7 @@ ${run.workflowVersion}`;
7763
8047
  "blocked"
7764
8048
  ]
7765
8049
  });
7766
- return blocked.filter((r) => (this.namespace === void 0 || r.namespace === void 0 || r.namespace === this.namespace) && r.wakeAt !== void 0 && r.wakeAt <= nowMs);
8050
+ return blocked.filter((r) => (this.namespace === void 0 || r.namespace === void 0 || poolNamespace(r.namespace) === this.namespace) && r.wakeAt !== void 0 && r.wakeAt <= nowMs);
7767
8051
  }
7768
8052
  async callRemote(runId, seq, step, input, queue, transport, replay, admission, routing) {
7769
8053
  const group = stepGroup(step, routing?.namespace);
@@ -8302,7 +8586,7 @@ ${run.workflowVersion}`;
8302
8586
  const run = await this.store.getRun(decision.runId);
8303
8587
  if (!run || run.awaitingDecisionTaskId !== decision.taskId) return;
8304
8588
  if (run.status === "cancelled" || run.status === "completed" || run.status === "dead") return;
8305
- if (this.namespace !== void 0 && run.namespace !== void 0 && run.namespace !== this.namespace) {
8589
+ if (this.namespace !== void 0 && run.namespace !== void 0 && poolNamespace(run.namespace) !== this.namespace) {
8306
8590
  return;
8307
8591
  }
8308
8592
  const nowMs = this.clock();
@@ -9142,7 +9426,7 @@ var CodecStateStore = class {
9142
9426
  const deleteRuns = inner.deleteRuns;
9143
9427
  if (deleteRuns) this.deleteRuns = (ids) => deleteRuns.call(inner, ids);
9144
9428
  const orphans = inner.listOrphanedRuns;
9145
- if (orphans) this.listOrphanedRuns = async (nowMs, limit, namespace) => (await orphans.call(inner, nowMs, limit, namespace)).map((r) => this.decRun(r));
9429
+ if (orphans) this.listOrphanedRuns = async (nowMs, limit, namespace, ownedBy) => (await orphans.call(inner, nowMs, limit, namespace, ownedBy)).map((r) => this.decRun(r));
9146
9430
  const updateIf = inner.updateRunIf;
9147
9431
  if (updateIf) this.updateRunIf = (runId, expected, patch) => updateIf.call(inner, runId, expected, this.encRunPatch(patch));
9148
9432
  const saveIfPending = inner.saveCheckpointIfPending;
@@ -9663,7 +9947,7 @@ var InMemoryStateStore = class {
9663
9947
  async createRun(run) {
9664
9948
  this.runs.set(run.id, {
9665
9949
  ...run,
9666
- namespace: run.namespace ?? "default"
9950
+ namespace: run.namespace || "default"
9667
9951
  });
9668
9952
  this.reindexAttributes(run.id, run.searchAttributes);
9669
9953
  }
@@ -9678,7 +9962,7 @@ var InMemoryStateStore = class {
9678
9962
  applyRunPatch(runId, inputPatch) {
9679
9963
  const patch = inputPatch.status && TERMINAL_RUN_STATUSES.includes(inputPatch.status) ? {
9680
9964
  ...inputPatch,
9681
- tags: ("tags" in inputPatch ? inputPatch.tags : this.runs.get(runId)?.tags)?.filter((tag) => tag !== SINGLETON_ADMITTED_TAG)
9965
+ tags: withoutSingletonMarkers("tags" in inputPatch ? inputPatch.tags : this.runs.get(runId)?.tags)
9682
9966
  } : inputPatch;
9683
9967
  const existing = this.runs.get(runId);
9684
9968
  if (!existing) throw new Error(`run ${runId} not found`);
@@ -9766,10 +10050,10 @@ var InMemoryStateStore = class {
9766
10050
  return true;
9767
10051
  });
9768
10052
  }
9769
- async listOrphanedRuns(nowMs, limit, namespace) {
10053
+ async listOrphanedRuns(nowMs, limit, namespace, ownedBy) {
9770
10054
  return [
9771
10055
  ...this.runs.values()
9772
- ].filter((r) => (r.status === "running" || r.status === "cancelling") && (r.lockedUntil == null || r.lockedUntil <= nowMs) && (namespace === void 0 || r.namespace === namespace)).sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime()).slice(0, Math.max(0, limit)).map((r) => ({
10056
+ ].filter((r) => (r.status === "running" || r.status === "cancelling") && (r.lockedUntil == null || r.lockedUntil <= nowMs || ownedBy !== void 0 && r.lockedBy === ownedBy.owner && r.lockedUntil < ownedBy.lockedBefore) && (namespace === void 0 || r.namespace === namespace)).sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime()).slice(0, Math.max(0, limit)).map((r) => ({
9773
10057
  ...r
9774
10058
  }));
9775
10059
  }
@@ -10161,6 +10445,7 @@ export {
10161
10445
  DURABLE_STEP_NAME,
10162
10446
  DurableWorkflow,
10163
10447
  ENGINE_MINTED_TAG_PREFIXES,
10448
+ ENTITY_WORKFLOW,
10164
10449
  EVENT_BUFFER_SCAN_LIMIT,
10165
10450
  EVENT_TOKEN_PREFIX,
10166
10451
  Entities,
@@ -10186,6 +10471,8 @@ export {
10186
10471
  RunGateway,
10187
10472
  SINGLETON_ACTIVE_STATUSES,
10188
10473
  SINGLETON_ADMITTED_TAG,
10474
+ SINGLETON_RESERVED_TAGS,
10475
+ SINGLETON_TRACKED_TAG,
10189
10476
  STATE_STORE,
10190
10477
  STATE_STORE_CANONICAL,
10191
10478
  SUMMARY_CHILD_LIMIT,
@@ -10261,6 +10548,7 @@ export {
10261
10548
  isLegacyDescriptor,
10262
10549
  isNonEmptyRunScope,
10263
10550
  isWorkflowControlFlowSignal,
10551
+ legacySingletonHolders,
10264
10552
  listSignalWaitersOfRuns,
10265
10553
  liveHandoff,
10266
10554
  log,
@@ -10278,8 +10566,10 @@ export {
10278
10566
  pageFanFromStore,
10279
10567
  parentRunIdOf,
10280
10568
  parseDuration,
10569
+ parseWebhookToken,
10281
10570
  partitionOfToken,
10282
10571
  planDispatch,
10572
+ poolNamespace,
10283
10573
  prevCronFireMs,
10284
10574
  queryCheckpointsOf,
10285
10575
  readSearchAttributes,
@@ -10315,6 +10605,8 @@ export {
10315
10605
  tenantGroup,
10316
10606
  useDurableExecution,
10317
10607
  warn,
10608
+ withSingletonTracked,
10609
+ withoutSingletonMarkers,
10318
10610
  workflowName
10319
10611
  };
10320
10612
  //# sourceMappingURL=index.js.map