@dudousxd/nestjs-durable-core 0.80.0 → 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.cjs CHANGED
@@ -36,6 +36,7 @@ __export(index_exports, {
36
36
  DURABLE_STEP_NAME: () => DURABLE_STEP_NAME,
37
37
  DurableWorkflow: () => DurableWorkflow,
38
38
  ENGINE_MINTED_TAG_PREFIXES: () => ENGINE_MINTED_TAG_PREFIXES,
39
+ ENTITY_WORKFLOW: () => ENTITY_WORKFLOW,
39
40
  EVENT_BUFFER_SCAN_LIMIT: () => EVENT_BUFFER_SCAN_LIMIT,
40
41
  EVENT_TOKEN_PREFIX: () => EVENT_TOKEN_PREFIX,
41
42
  Entities: () => Entities,
@@ -61,6 +62,8 @@ __export(index_exports, {
61
62
  RunGateway: () => RunGateway,
62
63
  SINGLETON_ACTIVE_STATUSES: () => SINGLETON_ACTIVE_STATUSES,
63
64
  SINGLETON_ADMITTED_TAG: () => SINGLETON_ADMITTED_TAG,
65
+ SINGLETON_RESERVED_TAGS: () => SINGLETON_RESERVED_TAGS,
66
+ SINGLETON_TRACKED_TAG: () => SINGLETON_TRACKED_TAG,
64
67
  STATE_STORE: () => STATE_STORE,
65
68
  STATE_STORE_CANONICAL: () => STATE_STORE_CANONICAL,
66
69
  SUMMARY_CHILD_LIMIT: () => SUMMARY_CHILD_LIMIT,
@@ -136,6 +139,7 @@ __export(index_exports, {
136
139
  isLegacyDescriptor: () => isLegacyDescriptor,
137
140
  isNonEmptyRunScope: () => isNonEmptyRunScope,
138
141
  isWorkflowControlFlowSignal: () => isWorkflowControlFlowSignal,
142
+ legacySingletonHolders: () => legacySingletonHolders,
139
143
  listSignalWaitersOfRuns: () => listSignalWaitersOfRuns,
140
144
  liveHandoff: () => liveHandoff,
141
145
  log: () => log,
@@ -153,8 +157,10 @@ __export(index_exports, {
153
157
  pageFanFromStore: () => pageFanFromStore,
154
158
  parentRunIdOf: () => parentRunIdOf,
155
159
  parseDuration: () => parseDuration,
160
+ parseWebhookToken: () => parseWebhookToken,
156
161
  partitionOfToken: () => partitionOfToken,
157
162
  planDispatch: () => planDispatch,
163
+ poolNamespace: () => poolNamespace,
158
164
  prevCronFireMs: () => prevCronFireMs,
159
165
  queryCheckpointsOf: () => queryCheckpointsOf,
160
166
  readSearchAttributes: () => readSearchAttributes,
@@ -190,6 +196,8 @@ __export(index_exports, {
190
196
  tenantGroup: () => tenantGroup,
191
197
  useDurableExecution: () => useDurableExecution,
192
198
  warn: () => warn,
199
+ withSingletonTracked: () => withSingletonTracked,
200
+ withoutSingletonMarkers: () => withoutSingletonMarkers,
193
201
  workflowName: () => workflowName
194
202
  });
195
203
  module.exports = __toCommonJS(index_exports);
@@ -1121,6 +1129,9 @@ async function assertConcurrency(store, workflow, quota) {
1121
1129
  __name(assertConcurrency, "assertConcurrency");
1122
1130
 
1123
1131
  // src/entities.ts
1132
+ var NUDGE = {
1133
+ nudge: true
1134
+ };
1124
1135
  var ENTITY_WORKFLOW = "__entity";
1125
1136
  var entityRunId = /* @__PURE__ */ __name((name, key) => `entity:${name}:${key}`, "entityRunId");
1126
1137
  var Entities = class {
@@ -1128,9 +1139,11 @@ var Entities = class {
1128
1139
  __name(this, "Entities");
1129
1140
  }
1130
1141
  host;
1142
+ workflowHost;
1131
1143
  configs = /* @__PURE__ */ new Map();
1132
- constructor(host) {
1144
+ constructor(host, workflowHost) {
1133
1145
  this.host = host;
1146
+ this.workflowHost = workflowHost;
1134
1147
  host.register(ENTITY_WORKFLOW, "1", (ctx, input) => this.run(ctx, input));
1135
1148
  }
1136
1149
  register(name, config) {
@@ -1152,6 +1165,52 @@ var Entities = class {
1152
1165
  }
1153
1166
  });
1154
1167
  }
1168
+ /**
1169
+ * The durable half of a send from INSIDE a workflow (`ctx.callEntity` / `ctx.signalEntity`): write
1170
+ * the op to the entity's signal buffer and return once it is stored, so the caller can checkpoint
1171
+ * after it. The op carries `id`, the caller's stable `<runId>:<seq>`, so if the caller dies between
1172
+ * this write and its checkpoint and sends again on replay, the entity skips the second copy. The
1173
+ * entity's run is also ensured here, awaited (`start` only enqueues, it never runs the body
1174
+ * inline), so an op sent on first contact can't be stranded in a buffer no run will ever read if
1175
+ * the process dies right after. The wake-up ({@link kick}) is separate and may be lost: a busy
1176
+ * entity reads the buffer at its next wait, and a parked one is paired with it by the engine's
1177
+ * `reconcileMs` sweep. Returns `false` on a host without a workflow host.
1178
+ */
1179
+ async enqueue(name, key, op, arg, id, reply) {
1180
+ if (!this.workflowHost) return false;
1181
+ const message = {
1182
+ op,
1183
+ arg,
1184
+ id,
1185
+ ...reply ? {
1186
+ reply
1187
+ } : {}
1188
+ };
1189
+ const runId = entityRunId(name, key);
1190
+ await this.workflowHost.bufferSignal(runId, message);
1191
+ await this.workflowHost.ensureStarted(ENTITY_WORKFLOW, {
1192
+ name,
1193
+ key
1194
+ }, runId);
1195
+ return true;
1196
+ }
1197
+ /**
1198
+ * Make the entity act on what {@link enqueue} buffered: wake its run if it is parked on its wait
1199
+ * (the run itself was ensured by `enqueue`; ensured again here, idempotently, for a caller replay
1200
+ * that only re-kicks). A run that is busy needs nothing: its next wait reads the buffer before
1201
+ * suspending. The wake carries a no-op `nudge`, never the op itself, so the op stays in
1202
+ * the buffer in arrival order and a lost or repeated kick costs at most one empty iteration.
1203
+ */
1204
+ async kick(name, key) {
1205
+ const host = this.workflowHost;
1206
+ if (!host) return;
1207
+ const runId = entityRunId(name, key);
1208
+ await host.ensureStarted(ENTITY_WORKFLOW, {
1209
+ name,
1210
+ key
1211
+ }, runId);
1212
+ await host.wakeWaiter(runId, NUDGE);
1213
+ }
1155
1214
  async signal(name, key, op, arg) {
1156
1215
  await this.dispatch(name, key, op, arg);
1157
1216
  }
@@ -1165,11 +1224,18 @@ var Entities = class {
1165
1224
  if (!config) throw new FatalError(`entity "${name}" is not registered`);
1166
1225
  const token = entityRunId(name, key);
1167
1226
  let state = config.initialState();
1227
+ const seen = /* @__PURE__ */ new Set();
1168
1228
  for (let i = 0; ; i += 1) {
1169
1229
  const msg = await ctx.waitForSignal(token);
1230
+ if (msg.nudge) continue;
1231
+ if (msg.id !== void 0) {
1232
+ if (seen.has(msg.id)) continue;
1233
+ seen.add(msg.id);
1234
+ }
1235
+ const op = msg.op;
1170
1236
  const out = await ctx.localStep(`op:${i}`, async () => {
1171
- const handler = config.handlers[msg.op];
1172
- if (!handler) throw new FatalError(`entity "${name}" has no handler for op "${msg.op}"`);
1237
+ const handler = config.handlers[op];
1238
+ if (!handler) throw new FatalError(`entity "${name}" has no handler for op "${op}"`);
1173
1239
  const result = await handler(state, msg.arg);
1174
1240
  return {
1175
1241
  result,
@@ -1179,8 +1245,9 @@ var Entities = class {
1179
1245
  state = out.state;
1180
1246
  await ctx.setEvent("state", state);
1181
1247
  if (msg.reply) {
1248
+ const reply = msg.reply;
1182
1249
  await ctx.localStep(`reply:${i}`, async () => {
1183
- await this.host.signal(msg.reply, out.result);
1250
+ await this.host.signal(reply, out.result);
1184
1251
  });
1185
1252
  }
1186
1253
  }
@@ -2104,6 +2171,10 @@ function checkpointNameKind(name) {
2104
2171
  return "step";
2105
2172
  }
2106
2173
  __name(checkpointNameKind, "checkpointNameKind");
2174
+ function poolNamespace(namespace) {
2175
+ return namespace === "" ? "default" : namespace;
2176
+ }
2177
+ __name(poolNamespace, "poolNamespace");
2107
2178
  var TERMINAL_RUN_STATUSES = [
2108
2179
  "completed",
2109
2180
  "failed",
@@ -2235,6 +2306,19 @@ var WEBHOOK_PREFIX = "wh:";
2235
2306
  var CHILD_PREFIX = "child:";
2236
2307
  var BREAKPOINT_PREFIX = "bp:";
2237
2308
  var EVENT_PREFIX = EVENT_TOKEN_PREFIX;
2309
+ function parseWebhookToken(token) {
2310
+ if (!token.startsWith(WEBHOOK_PREFIX)) return null;
2311
+ const rest = token.slice(WEBHOOK_PREFIX.length);
2312
+ const at = rest.lastIndexOf(":");
2313
+ if (at <= 0) return null;
2314
+ const seqText = rest.slice(at + 1);
2315
+ if (!/^\d+$/.test(seqText)) return null;
2316
+ return {
2317
+ runId: rest.slice(0, at),
2318
+ seq: Number(seqText)
2319
+ };
2320
+ }
2321
+ __name(parseWebhookToken, "parseWebhookToken");
2238
2322
  function classifyWaiterToken(token) {
2239
2323
  if (token.startsWith(WEBHOOK_PREFIX)) return {
2240
2324
  on: "webhook",
@@ -2772,6 +2856,11 @@ __name(errorMessage, "errorMessage");
2772
2856
 
2773
2857
  // src/singleton-admission.ts
2774
2858
  var SINGLETON_ADMITTED_TAG = "durable:singleton:admitted";
2859
+ var SINGLETON_TRACKED_TAG = "durable:singleton:tracked";
2860
+ var SINGLETON_RESERVED_TAGS = [
2861
+ SINGLETON_ADMITTED_TAG,
2862
+ SINGLETON_TRACKED_TAG
2863
+ ];
2775
2864
  var SINGLETON_ACTIVE_STATUSES = [
2776
2865
  "pending",
2777
2866
  "running",
@@ -2779,6 +2868,33 @@ var SINGLETON_ACTIVE_STATUSES = [
2779
2868
  "blocked",
2780
2869
  "cancelling"
2781
2870
  ];
2871
+ var LEGACY_SCAN_STATUSES = [
2872
+ "running",
2873
+ "suspended",
2874
+ "cancelling"
2875
+ ];
2876
+ var byAdmissionOrder = /* @__PURE__ */ __name((a, b) => a.createdAt.getTime() - b.createdAt.getTime() || a.id.localeCompare(b.id), "byAdmissionOrder");
2877
+ function withoutSingletonMarkers(tags) {
2878
+ return tags?.filter((tag) => !SINGLETON_RESERVED_TAGS.includes(tag));
2879
+ }
2880
+ __name(withoutSingletonMarkers, "withoutSingletonMarkers");
2881
+ function withSingletonTracked(tags) {
2882
+ const current = tags ?? [];
2883
+ if (current.includes(SINGLETON_ADMITTED_TAG) || current.includes(SINGLETON_TRACKED_TAG)) {
2884
+ return current;
2885
+ }
2886
+ return [
2887
+ ...current,
2888
+ SINGLETON_TRACKED_TAG
2889
+ ];
2890
+ }
2891
+ __name(withSingletonTracked, "withSingletonTracked");
2892
+ function legacySingletonHolders(active, runId, limit) {
2893
+ const scanned = active.filter((run) => LEGACY_SCAN_STATUSES.includes(run.status)).sort(byAdmissionOrder);
2894
+ const window = new Set(scanned.slice(0, limit).map((run) => run.id));
2895
+ return scanned.filter((run) => run.id !== runId && !run.tags?.includes(SINGLETON_ADMITTED_TAG) && (!run.tags?.includes(SINGLETON_TRACKED_TAG) || window.has(run.id)));
2896
+ }
2897
+ __name(legacySingletonHolders, "legacySingletonHolders");
2782
2898
  function singletonAdmissionTags(runs, runId, tag, workflow, limit) {
2783
2899
  if (!Number.isInteger(limit) || limit < 1) throw new Error("singleton limit must be a positive integer");
2784
2900
  const active = runs.filter((run) => run.workflow === workflow && SINGLETON_ACTIVE_STATUSES.includes(run.status) && run.tags?.includes(tag));
@@ -2787,12 +2903,13 @@ function singletonAdmissionTags(runs, runId, tag, workflow, limit) {
2787
2903
  if (candidate.tags?.includes(SINGLETON_ADMITTED_TAG)) return candidate.tags;
2788
2904
  if (candidate.status === "cancelling") return null;
2789
2905
  const holders = active.filter((run) => run.tags?.includes(SINGLETON_ADMITTED_TAG));
2790
- const remaining = limit - holders.length;
2906
+ const legacy = new Set(legacySingletonHolders(active, runId, limit).map((run) => run.id));
2907
+ const remaining = limit - holders.length - legacy.size;
2791
2908
  if (remaining <= 0) return null;
2792
- 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));
2909
+ const waiters = active.filter((run) => !run.tags?.includes(SINGLETON_ADMITTED_TAG) && !legacy.has(run.id)).sort(byAdmissionOrder);
2793
2910
  if (!waiters.slice(0, remaining).some((run) => run.id === runId)) return null;
2794
2911
  return [
2795
- ...candidate.tags ?? [],
2912
+ ...(candidate.tags ?? []).filter((t) => t !== SINGLETON_TRACKED_TAG),
2796
2913
  SINGLETON_ADMITTED_TAG
2797
2914
  ];
2798
2915
  }
@@ -2802,7 +2919,7 @@ async function clearTerminalSingletonAdmission(runId, patch, getRun) {
2802
2919
  const tags = "tags" in patch ? patch.tags : (await getRun(runId))?.tags;
2803
2920
  return {
2804
2921
  ...patch,
2805
- tags: tags?.filter((tag) => tag !== SINGLETON_ADMITTED_TAG)
2922
+ tags: withoutSingletonMarkers(tags)
2806
2923
  };
2807
2924
  }
2808
2925
  __name(clearTerminalSingletonAdmission, "clearTerminalSingletonAdmission");
@@ -2813,6 +2930,7 @@ function singletonAdmissionDenialPatch(runs, runId, tag, workflow, retryWakeAt)
2813
2930
  return {
2814
2931
  status: "suspended",
2815
2932
  wakeAt: retryWakeAt,
2933
+ tags: withSingletonTracked(candidate.tags),
2816
2934
  updatedAt: /* @__PURE__ */ new Date()
2817
2935
  };
2818
2936
  }
@@ -2865,7 +2983,7 @@ var SingletonGate = class {
2865
2983
  const admitted = await this.deps.store.tryAdmitSingleton(run.id, this.tag(cfg, run.input), run.workflow, cfg.limit ?? 1, this.retryWakeAt());
2866
2984
  if (admitted && !run.tags?.includes(SINGLETON_ADMITTED_TAG)) {
2867
2985
  run.tags = [
2868
- ...run.tags ?? [],
2986
+ ...(run.tags ?? []).filter((tag) => tag !== SINGLETON_TRACKED_TAG),
2869
2987
  SINGLETON_ADMITTED_TAG
2870
2988
  ];
2871
2989
  }
@@ -3760,30 +3878,82 @@ function createWorkflowCtx(host, runId, compensations, workflow = "", searchAttr
3760
3878
  }));
3761
3879
  return id;
3762
3880
  }, "startChild");
3881
+ const sendEntityOp = /* @__PURE__ */ __name(async (name, key, op, arg, seq, reply) => await host.enqueueEntityOp?.(name, key, op, arg, `${runId}:${seq}`, reply) ?? false, "sendEntityOp");
3763
3882
  const callEntity = /* @__PURE__ */ __name(async (name, key, op, arg) => {
3764
3883
  const current = pos.next();
3765
- const existing = await readCheckpoint(current);
3884
+ let existing = await readCheckpoint(current);
3766
3885
  if (existing && existing.status === "completed") return existing.output;
3767
3886
  const reply = `entityreply:${runId}:${current}`;
3887
+ const intentName = `signal:${reply}`;
3888
+ if (existing && existing.name !== intentName) {
3889
+ throw new NonDeterminismError(runId, current, intentName, existing.name);
3890
+ }
3891
+ if (existing && replay?.has(current)) {
3892
+ existing = await store.getCheckpoint(runId, current) ?? existing;
3893
+ if (existing.status === "completed") return existing.output;
3894
+ }
3895
+ const markSent = /* @__PURE__ */ __name(() => store.saveCheckpointIfPending?.(instantCheckpoint({
3896
+ runId,
3897
+ seq: current,
3898
+ name: intentName,
3899
+ kind: "signal",
3900
+ status: "pending",
3901
+ output: {
3902
+ sent: true
3903
+ }
3904
+ })) ?? Promise.resolve(false), "markSent");
3905
+ if (existing) {
3906
+ const buffered = await consumeBuffered(reply, current);
3907
+ if (buffered) {
3908
+ await store.removeSignalWaiter({
3909
+ token: reply,
3910
+ runId,
3911
+ seq: current
3912
+ });
3913
+ return buffered.value;
3914
+ }
3915
+ if (existing.output?.sent !== true) {
3916
+ if (await sendEntityOp(name, key, op, arg, current, reply)) await markSent();
3917
+ }
3918
+ host.kickEntity?.(name, key);
3919
+ throw new WorkflowSuspended();
3920
+ }
3768
3921
  await store.putSignalWaiter({
3769
3922
  token: reply,
3770
3923
  runId,
3771
3924
  seq: current
3772
3925
  });
3773
- host.signalEntity?.(name, key, op, arg, reply);
3926
+ if (!host.enqueueEntityOp) {
3927
+ host.signalEntity?.(name, key, op, arg, reply);
3928
+ throw new WorkflowSuspended();
3929
+ }
3930
+ await writeCheckpoint(instantCheckpoint({
3931
+ runId,
3932
+ seq: current,
3933
+ name: intentName,
3934
+ kind: "signal",
3935
+ status: "pending",
3936
+ output: {
3937
+ sent: false
3938
+ }
3939
+ }));
3940
+ if (await sendEntityOp(name, key, op, arg, current, reply)) await markSent();
3941
+ host.kickEntity?.(name, key);
3774
3942
  throw new WorkflowSuspended();
3775
3943
  }, "callEntity");
3776
3944
  const signalEntity = /* @__PURE__ */ __name(async (name, key, op, arg) => {
3777
3945
  const current = pos.next();
3778
3946
  const existing = await readCheckpoint(current);
3779
3947
  if (existing && existing.status === "completed") return;
3780
- host.signalEntity?.(name, key, op, arg);
3948
+ const durable = await sendEntityOp(name, key, op, arg, current);
3949
+ if (!durable) host.signalEntity?.(name, key, op, arg);
3781
3950
  await writeCheckpoint(instantCheckpoint({
3782
3951
  runId,
3783
3952
  seq: current,
3784
3953
  name: `entitysig:${name}:${key}`,
3785
3954
  kind: "local"
3786
3955
  }));
3956
+ if (durable) host.kickEntity?.(name, key);
3787
3957
  }, "signalEntity");
3788
3958
  const breakpoint = /* @__PURE__ */ __name(async (label) => {
3789
3959
  const current = pos.next();
@@ -3851,17 +4021,15 @@ function createWorkflowCtx(host, runId, compensations, workflow = "", searchAttr
3851
4021
  const deadlineSeq = timeoutMs == null ? -1 : pos.next();
3852
4022
  const existing = await readCheckpoint(current);
3853
4023
  if (existing && existing.status === "completed") return existing.output;
3854
- if (timeoutMs == null) {
3855
- await store.putSignalWaiter({
4024
+ const buffered = await consumeBuffered(token, current);
4025
+ if (buffered) return buffered.value;
4026
+ const deadline = timeoutMs == null ? void 0 : await stampDeadline(deadlineSeq, token, timeoutMs);
4027
+ if (deadline !== void 0 && host.clock() >= deadline) {
4028
+ await store.removeSignalWaiter({
3856
4029
  token,
3857
4030
  runId,
3858
4031
  seq: current
3859
- });
3860
- throw new WorkflowSuspended();
3861
- }
3862
- const deadline = await stampDeadline(deadlineSeq, token, timeoutMs);
3863
- if (host.clock() >= deadline) {
3864
- await store.takeSignalWaiter(token).catch(() => void 0);
4032
+ }).catch(() => void 0);
3865
4033
  throw new SignalTimeoutError(token, timeoutMs);
3866
4034
  }
3867
4035
  await store.putSignalWaiter({
@@ -3869,6 +4037,15 @@ function createWorkflowCtx(host, runId, compensations, workflow = "", searchAttr
3869
4037
  runId,
3870
4038
  seq: current
3871
4039
  });
4040
+ const lateBuffered = await consumeBuffered(token, current);
4041
+ if (lateBuffered) {
4042
+ await store.removeSignalWaiter({
4043
+ token,
4044
+ runId,
4045
+ seq: current
4046
+ });
4047
+ return lateBuffered.value;
4048
+ }
3872
4049
  throw new WorkflowSuspended(deadline);
3873
4050
  }, "wait");
3874
4051
  return {
@@ -4060,6 +4237,12 @@ var SETTLE_ELIGIBLE_STATUSES = [
4060
4237
  "failed",
4061
4238
  "cancelling"
4062
4239
  ];
4240
+ var TIMEOUT_SWEPT_STATUSES = [
4241
+ "pending",
4242
+ "running",
4243
+ "suspended",
4244
+ "blocked"
4245
+ ];
4063
4246
  var SUSPEND_ELIGIBLE_STATUSES = SETTLE_ELIGIBLE_STATUSES.filter((status) => status !== "cancelling");
4064
4247
  var RECOVERY_BATCH = 500;
4065
4248
  var NamespaceMismatch = class NamespaceMismatch2 extends Error {
@@ -4082,6 +4265,12 @@ var WorkflowEngine = class {
4082
4265
  instanceId;
4083
4266
  namespace;
4084
4267
  leaseMs;
4268
+ /**
4269
+ * Leases held by this `instanceId` that expire strictly before this epoch ms were taken by a
4270
+ * PREVIOUS process with the same id (every lease this one takes or renews expires at
4271
+ * `clock() + leaseMs`, so at or after it). See {@link orphanedRuns}.
4272
+ */
4273
+ ownLeaseCutoffMs;
4085
4274
  maxRecoveryAttempts;
4086
4275
  remoteAdvanceSilenceMs;
4087
4276
  reconcileMs;
@@ -4203,8 +4392,9 @@ var WorkflowEngine = class {
4203
4392
  this.admission = deps.admission ?? new InMemoryAdmissionBackend(this.clock);
4204
4393
  this.admission.onFreed?.((queue) => this.wakeQueueWaiters(queue));
4205
4394
  this.instanceId = deps.instanceId ?? globalThis.crypto.randomUUID();
4206
- this.namespace = deps.namespace;
4395
+ this.namespace = poolNamespace(deps.namespace);
4207
4396
  this.leaseMs = deps.leaseMs ?? 3e4;
4397
+ this.ownLeaseCutoffMs = this.clock() + this.leaseMs;
4208
4398
  this.maxRecoveryAttempts = deps.maxRecoveryAttempts;
4209
4399
  this.remoteAdvanceSilenceMs = deps.remoteAdvanceSilenceMs;
4210
4400
  this.reconcileMs = deps.reconcileMs === void 0 ? 3e5 : deps.reconcileMs || void 0;
@@ -4324,7 +4514,11 @@ var WorkflowEngine = class {
4324
4514
  }
4325
4515
  });
4326
4516
  this.accumulators = new EventAccumulators(this);
4327
- this.entities = new Entities(this);
4517
+ this.entities = new Entities(this, {
4518
+ bufferSignal: /* @__PURE__ */ __name((token, payload) => this.store.bufferSignal(token, payload), "bufferSignal"),
4519
+ wakeWaiter: /* @__PURE__ */ __name((token, payload) => this.wakeSignalWaiter(token, payload), "wakeWaiter"),
4520
+ ensureStarted: /* @__PURE__ */ __name((workflow, input, runId) => this.start(workflow, input, runId), "ensureStarted")
4521
+ });
4328
4522
  }
4329
4523
  /**
4330
4524
  * Register a **durable entity** (a virtual object): a keyed actor whose `handlers` run **serialized
@@ -4600,7 +4794,7 @@ var WorkflowEngine = class {
4600
4794
  // tenant-suffixed group the run will dispatch to — an operator starting a tenant's run via the
4601
4795
  // `onStartRun` wire (opts.namespace = tenant) must find the live `<workflow>@<tenant>` group,
4602
4796
  // not the bare group. Without this the start-run entry path can't reach a suffixed-only worker.
4603
- namespace: opts?.namespace ?? this.namespace,
4797
+ namespace: poolNamespace(opts?.namespace) ?? this.namespace,
4604
4798
  input,
4605
4799
  createdAt: /* @__PURE__ */ new Date(),
4606
4800
  updatedAt: /* @__PURE__ */ new Date()
@@ -4637,7 +4831,7 @@ var WorkflowEngine = class {
4637
4831
  VERSION_UNDECLARED_TAG
4638
4832
  ] : []
4639
4833
  ];
4640
- const tags = mergeTags(registered.tags, stampedTags)?.filter((tag) => tag !== SINGLETON_ADMITTED_TAG);
4834
+ const tags = mergeTags(registered.tags, stampedTags)?.filter((tag) => !SINGLETON_RESERVED_TAGS.includes(tag));
4641
4835
  if (registered.singleton) {
4642
4836
  await this.singletons.assertCapacity(name, registered.singleton, input);
4643
4837
  }
@@ -4651,7 +4845,7 @@ var WorkflowEngine = class {
4651
4845
  ...delayed ? {
4652
4846
  wakeAt: startAtMs
4653
4847
  } : {},
4654
- namespace: opts?.namespace ?? this.namespace,
4848
+ namespace: poolNamespace(opts?.namespace) ?? this.namespace,
4655
4849
  // DERIVED from the registration, exactly like `namespace` is derived from this engine — there is
4656
4850
  // deliberately no `opts.origin`, so a caller cannot label its run as another package's work.
4657
4851
  // `undefined` when the registration carried none (remote/convention/synthesized child, or a
@@ -4833,12 +5027,12 @@ var WorkflowEngine = class {
4833
5027
  error: run.error
4834
5028
  };
4835
5029
  }
4836
- if (this.namespace !== void 0 && run.namespace !== void 0 && run.namespace !== this.namespace) {
5030
+ if (this.namespace !== void 0 && run.namespace !== void 0 && poolNamespace(run.namespace) !== this.namespace) {
4837
5031
  await this.store.releaseRunLock(runId);
4838
5032
  throw new NamespaceMismatch();
4839
5033
  }
4840
- if (run.status === "failed" && run.tags?.includes(SINGLETON_ADMITTED_TAG)) {
4841
- run.tags = run.tags.filter((tag) => tag !== SINGLETON_ADMITTED_TAG);
5034
+ if (run.status === "failed" && (run.tags?.includes(SINGLETON_ADMITTED_TAG) || run.tags?.includes(SINGLETON_TRACKED_TAG))) {
5035
+ run.tags = withoutSingletonMarkers(run.tags);
4842
5036
  await this.store.updateRun(run.id, {
4843
5037
  tags: run.tags
4844
5038
  });
@@ -5057,6 +5251,15 @@ var WorkflowEngine = class {
5057
5251
  * `execution_timeout` error (terminal, so a late step result can't resurrect it), AND its children
5058
5252
  * are cascaded exactly as an explicit {@link cancel} cascades them.
5059
5253
  *
5254
+ * WHICH RUNS. Every run not yet finished, counted from `createdAt`: `pending` and `blocked` as well
5255
+ * as `running` and `suspended`. A run nobody picks up (no worker on its queue) or nobody can run (no
5256
+ * worker with its capabilities) is as stuck as one that hangs mid-body, and the cap is the backstop
5257
+ * for exactly that — leaving them out meant a run that never started lived forever while one that
5258
+ * started and hung was cut off. Cancelling one a worker is picking up at that moment is safe: the
5259
+ * worker's settle only writes over a live status ({@link SETTLE_ELIGIBLE_STATUSES}), so it can't
5260
+ * bring the run back. `cancelling` is left alone on purpose — a compensating cancel's saga undo is
5261
+ * running, it ends `cancelled` by itself, and cutting it short would skip the undo.
5262
+ *
5060
5263
  * WHY THE TERMINAL WRITE IS DIRECT, not a call to {@link cancel}. Two reasons, both still standing:
5061
5264
  * the run must land with the `execution_timeout` error `cancel` knows nothing about (`cancel` writes
5062
5265
  * a plain `cancelled`, and the code is what a dashboard/alert distinguishes a timeout by), and this
@@ -5069,8 +5272,8 @@ var WorkflowEngine = class {
5069
5272
  * children kept running with the parent gone and nothing pointing at them — a silent orphan you
5070
5273
  * could only find by reading the runs table by hand. {@link cancelChildren} is the SAME recursive
5071
5274
  * walk `cancel` uses, so children of children go too, and it inherits that path's two guards:
5072
- * `cancel` returns early on a run that is already `completed`/`cancelled`/`dead` (a child that beat
5073
- * the sweep to the finish is never clobbered, and a re-cancel is a no-op), and it writes the
5275
+ * `cancel` returns early on a run that is already terminal (a child that beat the sweep to the
5276
+ * finish is never clobbered, and a re-cancel is a no-op), and it writes the
5074
5277
  * parent's terminal status BEFORE recursing, so a cyclic parent→child graph terminates on the
5075
5278
  * second visit instead of looping. Two pollers sweeping the same run concurrently therefore write
5076
5279
  * the same terminal state twice and cascade into already-cancelled children — idempotent, not
@@ -5086,18 +5289,11 @@ var WorkflowEngine = class {
5086
5289
  if (this.draining) return;
5087
5290
  if (reg.executionTimeoutMs == null) continue;
5088
5291
  const deadline = now - reg.executionTimeoutMs;
5089
- const inflight = [
5090
- ...await this.store.listRuns({
5091
- workflow: reg.name,
5092
- status: "running",
5093
- namespace: this.namespace
5094
- }),
5095
- ...await this.store.listRuns({
5096
- workflow: reg.name,
5097
- status: "suspended",
5098
- namespace: this.namespace
5099
- })
5100
- ];
5292
+ const inflight = await this.store.listRuns({
5293
+ workflow: reg.name,
5294
+ statuses: TIMEOUT_SWEPT_STATUSES,
5295
+ namespace: this.namespace
5296
+ });
5101
5297
  for (const run of inflight) {
5102
5298
  if (this.draining) return;
5103
5299
  if (run.createdAt.getTime() > deadline) continue;
@@ -5130,48 +5326,103 @@ var WorkflowEngine = class {
5130
5326
  const results = [];
5131
5327
  for (const listed of await this.orphanedRuns(nowMs)) {
5132
5328
  if (this.draining) break;
5133
- const acquired = await this.store.tryLockRun(listed.id, this.instanceId, nowMs + this.leaseMs, nowMs);
5134
- if (!acquired) continue;
5135
- const run = await this.store.getRun(listed.id);
5136
- if (!run || run.status !== "running" && run.status !== "cancelling") {
5137
- await this.store.releaseRunLock(listed.id);
5138
- continue;
5139
- }
5140
- if (run.status === "running") {
5141
- const dispatched = await this.inFlightRemoteStep(run.id);
5142
- if (dispatched) {
5143
- await this.store.updateRun(run.id, {
5144
- status: "suspended",
5145
- wakeAt: this.reconcileWakeAt(dispatched.wakeAt ?? void 0),
5146
- updatedAt: /* @__PURE__ */ new Date()
5147
- });
5148
- await this.store.releaseRunLock(run.id);
5149
- results.push({
5150
- runId: run.id,
5151
- status: "suspended"
5152
- });
5153
- continue;
5154
- }
5155
- }
5156
- const settled = await this.countRecovery(run);
5157
- if (settled) {
5158
- results.push(settled);
5159
- continue;
5160
- }
5161
- await this.store.releaseRunLock(run.id);
5162
- if (run.status !== "cancelling") {
5329
+ const outcome = this.isOwnLiveLease(listed, nowMs) ? await this.reclaimOwnLease(listed, nowMs) : await this.recoverOrphan(listed, nowMs);
5330
+ if (!outcome) continue;
5331
+ if (outcome.dispatch) await this.runDispatcher.dispatch(outcome.result.runId);
5332
+ results.push(outcome.result);
5333
+ }
5334
+ return results;
5335
+ }
5336
+ /** A run whose still-live lease is held by this very `instanceId` (see {@link reclaimOwnLease}). */
5337
+ isOwnLiveLease(run, nowMs) {
5338
+ return run.lockedBy === this.instanceId && (run.lockedUntil ?? 0) > nowMs;
5339
+ }
5340
+ /**
5341
+ * Recover a run whose lease is still live but held by THIS `instanceId` and predates this process
5342
+ * (listed by {@link orphanedRuns}): the previous process with this stable id died without a
5343
+ * graceful shutdown (OOM, SIGKILL), so nothing is renewing that lease and waiting for it to expire
5344
+ * only delays the run. The lease is taken back with `renewRunLock` (a compare-and-set on the
5345
+ * owner; `tryLockRun` refuses a live lease), so the lease alone cannot fence a live execution in
5346
+ * THIS process from its own recovery: the in-process run gate does. A run with a
5347
+ * gate is executing (or about to) here and is skipped; otherwise a gate is held while the run is
5348
+ * recovered, so a resume landing meanwhile queues behind the recovery instead of running beside it.
5349
+ * The gate is released before the re-dispatch, which may resume the run on this instance.
5350
+ */
5351
+ async reclaimOwnLease(listed, nowMs) {
5352
+ if (this.runGates.has(listed.id)) return void 0;
5353
+ let release;
5354
+ const tail = new Promise((resolve) => {
5355
+ release = resolve;
5356
+ });
5357
+ const gate = {
5358
+ tail
5359
+ };
5360
+ this.runGates.set(listed.id, gate);
5361
+ this.closeGateAfter(listed.id, gate, tail);
5362
+ let outcome;
5363
+ try {
5364
+ const fresh = await this.store.getRun(listed.id);
5365
+ const liveHere = fresh?.lockedBy === this.instanceId && (fresh.lockedUntil ?? 0) > nowMs && (fresh.lockedUntil ?? 0) >= this.ownLeaseCutoffMs;
5366
+ if (fresh && !liveHere) outcome = await this.recoverOrphan(listed, nowMs, true);
5367
+ } finally {
5368
+ release(outcome?.result ?? {
5369
+ runId: listed.id,
5370
+ status: listed.status
5371
+ });
5372
+ }
5373
+ return outcome;
5374
+ }
5375
+ /**
5376
+ * One run of a {@link recoverIncomplete} pass: take its lease and decide on it. Resolves the
5377
+ * pass's result for it and whether it must be re-dispatched, or `undefined` when it was not ours.
5378
+ * `ownLease`: the run's live lease is this instance id's own, left by a previous process (see
5379
+ * {@link reclaimOwnLease}), so it is taken back by owner rather than acquired as a free one.
5380
+ */
5381
+ async recoverOrphan(listed, nowMs, ownLease = false) {
5382
+ 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);
5383
+ if (!acquired) return void 0;
5384
+ const run = await this.store.getRun(listed.id);
5385
+ if (!run || run.status !== "running" && run.status !== "cancelling") {
5386
+ await this.store.releaseRunLock(listed.id);
5387
+ return void 0;
5388
+ }
5389
+ if (run.status === "running") {
5390
+ const dispatched = await this.inFlightRemoteStep(run.id);
5391
+ if (dispatched) {
5163
5392
  await this.store.updateRun(run.id, {
5164
- status: "pending",
5393
+ status: "suspended",
5394
+ wakeAt: this.reconcileWakeAt(dispatched.wakeAt ?? void 0),
5165
5395
  updatedAt: /* @__PURE__ */ new Date()
5166
5396
  });
5397
+ await this.store.releaseRunLock(run.id);
5398
+ return {
5399
+ result: {
5400
+ runId: run.id,
5401
+ status: "suspended"
5402
+ },
5403
+ dispatch: false
5404
+ };
5167
5405
  }
5168
- await this.runDispatcher.dispatch(run.id);
5169
- results.push({
5170
- runId: run.id,
5171
- status: run.status === "cancelling" ? "cancelling" : "pending"
5406
+ }
5407
+ const settled = await this.countRecovery(run);
5408
+ if (settled) return {
5409
+ result: settled,
5410
+ dispatch: false
5411
+ };
5412
+ await this.store.releaseRunLock(run.id);
5413
+ if (run.status !== "cancelling") {
5414
+ await this.store.updateRun(run.id, {
5415
+ status: "pending",
5416
+ updatedAt: /* @__PURE__ */ new Date()
5172
5417
  });
5173
5418
  }
5174
- return results;
5419
+ return {
5420
+ result: {
5421
+ runId: run.id,
5422
+ status: run.status === "cancelling" ? "cancelling" : "pending"
5423
+ },
5424
+ dispatch: true
5425
+ };
5175
5426
  }
5176
5427
  /**
5177
5428
  * The runs {@link recoverIncomplete} should try to reclaim: incomplete runs whose lease is free or
@@ -5181,12 +5432,20 @@ var WorkflowEngine = class {
5181
5432
  * ({@link StateStore.listOrphanedRuns}, bounded by {@link RECOVERY_BATCH}); else the full list is
5182
5433
  * filtered here, which still spares the probes. Shuffled, like the due-timer pass, so instances
5183
5434
  * recovering at once spread over the batch instead of contending for the same leases.
5435
+ *
5436
+ * Also listed: runs still leased by THIS `instanceId` with a lease taken before this process
5437
+ * booted ({@link ownLeaseCutoffMs}). With a stable `instanceId` (pinned to the pod identity), that
5438
+ * is the previous process, killed without a graceful shutdown; see {@link reclaimOwnLease}.
5184
5439
  */
5185
5440
  async orphanedRuns(nowMs) {
5441
+ const ownedBy = {
5442
+ owner: this.instanceId,
5443
+ lockedBefore: this.ownLeaseCutoffMs
5444
+ };
5186
5445
  if (this.store.listOrphanedRuns) {
5187
- return shuffle(await this.store.listOrphanedRuns(nowMs, RECOVERY_BATCH, this.namespace));
5446
+ return shuffle(await this.store.listOrphanedRuns(nowMs, RECOVERY_BATCH, this.namespace, ownedBy));
5188
5447
  }
5189
- return shuffle((await this.store.listIncompleteRuns(this.namespace)).filter((run) => run.lockedUntil == null || run.lockedUntil <= nowMs));
5448
+ 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));
5190
5449
  }
5191
5450
  /**
5192
5451
  * The remote step this run dispatched and is still awaiting a result for, if any — the signature
@@ -5229,6 +5488,7 @@ var WorkflowEngine = class {
5229
5488
  error: error2,
5230
5489
  recoveryAttempts: attempts
5231
5490
  });
5491
+ this.notifyParent(run.id, failedCompletion(error2));
5232
5492
  return {
5233
5493
  runId: run.id,
5234
5494
  status: "dead",
@@ -5435,7 +5695,7 @@ var WorkflowEngine = class {
5435
5695
  error: void 0,
5436
5696
  ...resurrecting ? {
5437
5697
  recoveryAttempts: 0,
5438
- tags: run.tags?.filter((tag) => tag !== SINGLETON_ADMITTED_TAG)
5698
+ tags: withoutSingletonMarkers(run.tags)
5439
5699
  } : {},
5440
5700
  updatedAt: /* @__PURE__ */ new Date()
5441
5701
  });
@@ -5847,6 +6107,16 @@ var WorkflowEngine = class {
5847
6107
  return this.deliverSignal(lateWaiter, reclaimed.payload);
5848
6108
  }
5849
6109
  /**
6110
+ * Wake the run parked on `token` with `payload`, if there is one; otherwise do nothing — unlike
6111
+ * {@link signal}, it never buffers. For a wake-up whose real payload is ALREADY buffered on the
6112
+ * token (an entity op, see `Entities.kick`): a run that isn't parked reads that buffer at its next
6113
+ * wait, so there is nothing to keep.
6114
+ */
6115
+ async wakeSignalWaiter(token, payload) {
6116
+ const waiter = await this.store.takeSignalWaiter(token);
6117
+ return waiter ? this.deliverSignal(waiter, payload) : null;
6118
+ }
6119
+ /**
5850
6120
  * Ensure a run exists for `runId`, then deliver a signal to it — atomically race-free thanks to
5851
6121
  * signal buffering: if the run is new (or busy / not yet waiting), the signal is buffered and
5852
6122
  * consumed when it reaches `waitForSignal(token)`. The canonical **durable-entity / accumulator**
@@ -6442,6 +6712,25 @@ var WorkflowEngine = class {
6442
6712
  });
6443
6713
  }
6444
6714
  /** Admission persists the wait atomically; never overwrite a cancellation after it returns. */
6715
+ /**
6716
+ * A run's first execution: `pending` -> `running`. A singleton run not yet admitted is stamped
6717
+ * {@link SINGLETON_TRACKED_TAG} in the same write, BEFORE it competes for a slot, so a run this
6718
+ * replica is about to admit or deny is never mistaken for a holder admitted by a replica older
6719
+ * than 0.73 (see `legacySingletonHolders`).
6720
+ */
6721
+ async markStarted(run, singleton) {
6722
+ const tags = singleton ? withSingletonTracked(run.tags) : run.tags;
6723
+ const retag = tags !== run.tags;
6724
+ await this.store.updateRun(run.id, {
6725
+ status: "running",
6726
+ ...retag ? {
6727
+ tags
6728
+ } : {},
6729
+ updatedAt: /* @__PURE__ */ new Date()
6730
+ });
6731
+ run.status = "running";
6732
+ if (retag) run.tags = tags;
6733
+ }
6445
6734
  async singletonWaitResult(run) {
6446
6735
  const current = await this.store.getRun(run.id);
6447
6736
  await this.store.releaseRunLock(run.id);
@@ -6815,11 +7104,7 @@ ${run.workflowVersion}`;
6815
7104
  }
6816
7105
  const remote = registered.remote;
6817
7106
  if (run.status === "pending") {
6818
- await this.store.updateRun(run.id, {
6819
- status: "running",
6820
- updatedAt: /* @__PURE__ */ new Date()
6821
- });
6822
- run.status = "running";
7107
+ await this.markStarted(run, registered?.singleton !== void 0);
6823
7108
  this.emit({
6824
7109
  type: "run.started",
6825
7110
  runId: run.id,
@@ -7435,11 +7720,7 @@ ${run.workflowVersion}`;
7435
7720
  async runExecutionTurn(run, fn, registered) {
7436
7721
  if (run.status === "cancelling") this.cancelRequested.add(run.id);
7437
7722
  if (run.status === "pending") {
7438
- await this.store.updateRun(run.id, {
7439
- status: "running",
7440
- updatedAt: /* @__PURE__ */ new Date()
7441
- });
7442
- run.status = "running";
7723
+ await this.markStarted(run, registered?.singleton !== void 0);
7443
7724
  this.emit({
7444
7725
  type: "run.started",
7445
7726
  runId: run.id,
@@ -7815,6 +8096,17 @@ ${run.workflowVersion}`;
7815
8096
  signalEntity: /* @__PURE__ */ __name((name, key, op, arg, reply) => {
7816
8097
  queueMicrotask(() => void this.entities.dispatch(name, key, op, arg, reply).catch(() => void 0));
7817
8098
  }, "signalEntity"),
8099
+ // The durable half of a workflow-side entity send: awaited inside the turn (a store write
8100
+ // only, never an inline entity turn), so the ctx can checkpoint after it. See Entities.enqueue.
8101
+ enqueueEntityOp: /* @__PURE__ */ __name((name, key, op, arg, id, reply) => this.entities.enqueue(name, key, op, arg, id, reply), "enqueueEntityOp"),
8102
+ // Start/wake the entity for what enqueueEntityOp buffered — deferred like signalEntity above,
8103
+ // so it never re-enters a still-executing turn. A failed wake loses nothing: the op stays
8104
+ // buffered for the entity's next wait (and recovery's buffered-entity wake).
8105
+ kickEntity: /* @__PURE__ */ __name((name, key) => {
8106
+ queueMicrotask(() => void this.entities.kick(name, key).catch((err) => {
8107
+ console.warn(`[nestjs-durable] waking entity "${name}" key "${key}" failed (its op stays buffered for the next wake):`, err);
8108
+ }));
8109
+ }, "kickEntity"),
7818
8110
  interceptStep: /* @__PURE__ */ __name((invocation, body) => this.interceptStep(invocation, body), "interceptStep")
7819
8111
  };
7820
8112
  }
@@ -7928,7 +8220,7 @@ ${run.workflowVersion}`;
7928
8220
  updatedAt: /* @__PURE__ */ new Date()
7929
8221
  });
7930
8222
  if (!applied) {
7931
- await this.store.releaseRunLock(run.id);
8223
+ await this.releaseLeaseBestEffort(run.id);
7932
8224
  return this.echoCurrentStatus(run.id);
7933
8225
  }
7934
8226
  this.emit({
@@ -7940,7 +8232,7 @@ ${run.workflowVersion}`;
7940
8232
  requires: blocked.requires,
7941
8233
  diagnostics: blocked.diagnostics
7942
8234
  });
7943
- await this.store.releaseRunLock(run.id);
8235
+ await this.releaseLeaseBestEffort(run.id);
7944
8236
  return {
7945
8237
  runId: run.id,
7946
8238
  status: "blocked",
@@ -7956,7 +8248,7 @@ ${run.workflowVersion}`;
7956
8248
  "blocked"
7957
8249
  ]
7958
8250
  });
7959
- return blocked.filter((r) => (this.namespace === void 0 || r.namespace === void 0 || r.namespace === this.namespace) && r.wakeAt !== void 0 && r.wakeAt <= nowMs);
8251
+ return blocked.filter((r) => (this.namespace === void 0 || r.namespace === void 0 || poolNamespace(r.namespace) === this.namespace) && r.wakeAt !== void 0 && r.wakeAt <= nowMs);
7960
8252
  }
7961
8253
  async callRemote(runId, seq, step, input, queue, transport, replay, admission, routing) {
7962
8254
  const group = stepGroup(step, routing?.namespace);
@@ -8495,7 +8787,7 @@ ${run.workflowVersion}`;
8495
8787
  const run = await this.store.getRun(decision.runId);
8496
8788
  if (!run || run.awaitingDecisionTaskId !== decision.taskId) return;
8497
8789
  if (run.status === "cancelled" || run.status === "completed" || run.status === "dead") return;
8498
- if (this.namespace !== void 0 && run.namespace !== void 0 && run.namespace !== this.namespace) {
8790
+ if (this.namespace !== void 0 && run.namespace !== void 0 && poolNamespace(run.namespace) !== this.namespace) {
8499
8791
  return;
8500
8792
  }
8501
8793
  const nowMs = this.clock();
@@ -9335,7 +9627,7 @@ var CodecStateStore = class {
9335
9627
  const deleteRuns = inner.deleteRuns;
9336
9628
  if (deleteRuns) this.deleteRuns = (ids) => deleteRuns.call(inner, ids);
9337
9629
  const orphans = inner.listOrphanedRuns;
9338
- if (orphans) this.listOrphanedRuns = async (nowMs, limit, namespace) => (await orphans.call(inner, nowMs, limit, namespace)).map((r) => this.decRun(r));
9630
+ if (orphans) this.listOrphanedRuns = async (nowMs, limit, namespace, ownedBy) => (await orphans.call(inner, nowMs, limit, namespace, ownedBy)).map((r) => this.decRun(r));
9339
9631
  const updateIf = inner.updateRunIf;
9340
9632
  if (updateIf) this.updateRunIf = (runId, expected, patch) => updateIf.call(inner, runId, expected, this.encRunPatch(patch));
9341
9633
  const saveIfPending = inner.saveCheckpointIfPending;
@@ -9856,7 +10148,7 @@ var InMemoryStateStore = class {
9856
10148
  async createRun(run) {
9857
10149
  this.runs.set(run.id, {
9858
10150
  ...run,
9859
- namespace: run.namespace ?? "default"
10151
+ namespace: run.namespace || "default"
9860
10152
  });
9861
10153
  this.reindexAttributes(run.id, run.searchAttributes);
9862
10154
  }
@@ -9871,7 +10163,7 @@ var InMemoryStateStore = class {
9871
10163
  applyRunPatch(runId, inputPatch) {
9872
10164
  const patch = inputPatch.status && TERMINAL_RUN_STATUSES.includes(inputPatch.status) ? {
9873
10165
  ...inputPatch,
9874
- tags: ("tags" in inputPatch ? inputPatch.tags : this.runs.get(runId)?.tags)?.filter((tag) => tag !== SINGLETON_ADMITTED_TAG)
10166
+ tags: withoutSingletonMarkers("tags" in inputPatch ? inputPatch.tags : this.runs.get(runId)?.tags)
9875
10167
  } : inputPatch;
9876
10168
  const existing = this.runs.get(runId);
9877
10169
  if (!existing) throw new Error(`run ${runId} not found`);
@@ -9959,10 +10251,10 @@ var InMemoryStateStore = class {
9959
10251
  return true;
9960
10252
  });
9961
10253
  }
9962
- async listOrphanedRuns(nowMs, limit, namespace) {
10254
+ async listOrphanedRuns(nowMs, limit, namespace, ownedBy) {
9963
10255
  return [
9964
10256
  ...this.runs.values()
9965
- ].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) => ({
10257
+ ].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) => ({
9966
10258
  ...r
9967
10259
  }));
9968
10260
  }
@@ -10355,6 +10647,7 @@ var InMemoryTransport = class {
10355
10647
  DURABLE_STEP_NAME,
10356
10648
  DurableWorkflow,
10357
10649
  ENGINE_MINTED_TAG_PREFIXES,
10650
+ ENTITY_WORKFLOW,
10358
10651
  EVENT_BUFFER_SCAN_LIMIT,
10359
10652
  EVENT_TOKEN_PREFIX,
10360
10653
  Entities,
@@ -10380,6 +10673,8 @@ var InMemoryTransport = class {
10380
10673
  RunGateway,
10381
10674
  SINGLETON_ACTIVE_STATUSES,
10382
10675
  SINGLETON_ADMITTED_TAG,
10676
+ SINGLETON_RESERVED_TAGS,
10677
+ SINGLETON_TRACKED_TAG,
10383
10678
  STATE_STORE,
10384
10679
  STATE_STORE_CANONICAL,
10385
10680
  SUMMARY_CHILD_LIMIT,
@@ -10455,6 +10750,7 @@ var InMemoryTransport = class {
10455
10750
  isLegacyDescriptor,
10456
10751
  isNonEmptyRunScope,
10457
10752
  isWorkflowControlFlowSignal,
10753
+ legacySingletonHolders,
10458
10754
  listSignalWaitersOfRuns,
10459
10755
  liveHandoff,
10460
10756
  log,
@@ -10472,8 +10768,10 @@ var InMemoryTransport = class {
10472
10768
  pageFanFromStore,
10473
10769
  parentRunIdOf,
10474
10770
  parseDuration,
10771
+ parseWebhookToken,
10475
10772
  partitionOfToken,
10476
10773
  planDispatch,
10774
+ poolNamespace,
10477
10775
  prevCronFireMs,
10478
10776
  queryCheckpointsOf,
10479
10777
  readSearchAttributes,
@@ -10509,6 +10807,8 @@ var InMemoryTransport = class {
10509
10807
  tenantGroup,
10510
10808
  useDurableExecution,
10511
10809
  warn,
10810
+ withSingletonTracked,
10811
+ withoutSingletonMarkers,
10512
10812
  workflowName
10513
10813
  });
10514
10814
  //# sourceMappingURL=index.cjs.map