@dudousxd/nestjs-durable-core 0.72.1 → 0.74.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
@@ -56,6 +56,8 @@ __export(index_exports, {
56
56
  RemoteWorkflowExecutor: () => RemoteWorkflowExecutor,
57
57
  RemoteWorkflowTimeout: () => RemoteWorkflowTimeout,
58
58
  RunGateway: () => RunGateway,
59
+ SINGLETON_ACTIVE_STATUSES: () => SINGLETON_ACTIVE_STATUSES,
60
+ SINGLETON_ADMITTED_TAG: () => SINGLETON_ADMITTED_TAG,
59
61
  STATE_STORE: () => STATE_STORE,
60
62
  STATE_STORE_CANONICAL: () => STATE_STORE_CANONICAL,
61
63
  ScheduleAlreadyExistsError: () => ScheduleAlreadyExistsError,
@@ -86,6 +88,7 @@ __export(index_exports, {
86
88
  capableWorkers: () => capableWorkers,
87
89
  classifyWaiterToken: () => classifyWaiterToken,
88
90
  clearDurableExecutionWrappers: () => clearDurableExecutionWrappers,
91
+ clearTerminalSingletonAdmission: () => clearTerminalSingletonAdmission,
89
92
  collectMetrics: () => collectMetrics,
90
93
  concurrencyTag: () => concurrencyTag,
91
94
  controlPlaneDescriptor: () => controlPlaneDescriptor,
@@ -114,6 +117,7 @@ __export(index_exports, {
114
117
  isLegacyDescriptor: () => isLegacyDescriptor,
115
118
  isNonEmptyRunScope: () => isNonEmptyRunScope,
116
119
  isWorkflowControlFlowSignal: () => isWorkflowControlFlowSignal,
120
+ listSignalWaitersOfRuns: () => listSignalWaitersOfRuns,
117
121
  log: () => log,
118
122
  matchesAttributes: () => matchesAttributes,
119
123
  mergeRunFacetRows: () => mergeRunFacetRows,
@@ -124,6 +128,7 @@ __export(index_exports, {
124
128
  normalizeAttributeRows: () => normalizeAttributeRows,
125
129
  normalizeDescriptor: () => normalizeDescriptor,
126
130
  observedAnnouncements: () => observedAnnouncements,
131
+ parentRunIdOf: () => parentRunIdOf,
127
132
  parseDuration: () => parseDuration,
128
133
  partitionOfToken: () => partitionOfToken,
129
134
  planDispatch: () => planDispatch,
@@ -143,6 +148,8 @@ __export(index_exports, {
143
148
  runValueFacetsFromRuns: () => runValueFacetsFromRuns,
144
149
  sanitizeQueueToken: () => sanitizeQueueToken,
145
150
  scheduledRunId: () => scheduledRunId,
151
+ singletonAdmissionDenialPatch: () => singletonAdmissionDenialPatch,
152
+ singletonAdmissionTags: () => singletonAdmissionTags,
146
153
  sqlComparator: () => sqlComparator,
147
154
  stepConfigOf: () => stepConfigOf,
148
155
  stepId: () => stepId,
@@ -545,12 +552,15 @@ var WorkflowSuspended = class extends Error {
545
552
  }
546
553
  /** Epoch ms to auto-resume (durable sleep), or undefined when waiting on an external signal. */
547
554
  wakeAt;
555
+ /** Set when awaited children are what the run is waiting on — see {@link FanJoin}. */
556
+ fan;
548
557
  /** Marks this as a control-flow signal — see {@link isWorkflowControlFlowSignal}. */
549
558
  [CONTROL_FLOW_SIGNAL] = true;
550
- constructor(wakeAt) {
559
+ constructor(wakeAt, fan) {
551
560
  super("workflow suspended");
552
561
  this.name = "WorkflowSuspended";
553
562
  this.wakeAt = wakeAt;
563
+ this.fan = fan;
554
564
  }
555
565
  };
556
566
  var ContinueAsNew = class extends Error {
@@ -977,6 +987,21 @@ function instantCheckpoint(p) {
977
987
  }
978
988
  __name(instantCheckpoint, "instantCheckpoint");
979
989
 
990
+ // src/completion.ts
991
+ function isFailedCompletion(payload) {
992
+ const c = payload;
993
+ return !!c && typeof c === "object" && "ok" in c && c.ok === false;
994
+ }
995
+ __name(isFailedCompletion, "isFailedCompletion");
996
+ function unwrapCompletion(payload, label) {
997
+ const c = payload;
998
+ if (c && typeof c === "object" && "ok" in c && c.ok === false) {
999
+ throw new FatalError(`${label} failed: ${c.error}`);
1000
+ }
1001
+ return c?.value;
1002
+ }
1003
+ __name(unwrapCompletion, "unwrapCompletion");
1004
+
980
1005
  // src/concurrency.ts
981
1006
  var DEFAULT_CONCURRENCY_STATUSES = [
982
1007
  "pending",
@@ -1216,6 +1241,81 @@ function eventMatches(payload, match) {
1216
1241
  }
1217
1242
  __name(eventMatches, "eventMatches");
1218
1243
 
1244
+ // src/fan-join.ts
1245
+ function allChildId(runId, firstSeq, index, wave) {
1246
+ return `${allBase(runId, firstSeq, wave)}${index}`;
1247
+ }
1248
+ __name(allChildId, "allChildId");
1249
+ function allGroup(firstSeq, mode) {
1250
+ return mode === "failFast" ? `all:${firstSeq}:failFast` : `all:${firstSeq}`;
1251
+ }
1252
+ __name(allGroup, "allGroup");
1253
+ function allWait(runId, firstSeq, wave) {
1254
+ const base = allBase(runId, firstSeq, wave);
1255
+ return {
1256
+ waiterPrefix: `child:${base}`,
1257
+ checkpointPrefix: `signal:child:${base}`
1258
+ };
1259
+ }
1260
+ __name(allWait, "allWait");
1261
+ function fanJoin(waits) {
1262
+ return {
1263
+ waits
1264
+ };
1265
+ }
1266
+ __name(fanJoin, "fanJoin");
1267
+ var FOLDED_PREFIX = "fan:";
1268
+ function allFoldedName(runId, firstSeq, wave) {
1269
+ return `${FOLDED_PREFIX}${allBase(runId, firstSeq, wave)}`;
1270
+ }
1271
+ __name(allFoldedName, "allFoldedName");
1272
+ function foldedChildIds(checkpoint) {
1273
+ const base = checkpoint.name.slice(FOLDED_PREFIX.length);
1274
+ const { from, values } = checkpoint.output;
1275
+ return values.map((_, j) => `${base}${from + j}`);
1276
+ }
1277
+ __name(foldedChildIds, "foldedChildIds");
1278
+ function allBase(runId, firstSeq, wave) {
1279
+ return wave === void 0 ? `${runId}.all.${firstSeq}.` : `${runId}.all.${firstSeq}.w${wave}.`;
1280
+ }
1281
+ __name(allBase, "allBase");
1282
+ function allWaves(size, concurrency) {
1283
+ if (concurrency === void 0 || concurrency >= size) return void 0;
1284
+ if (!Number.isInteger(concurrency) || concurrency < 1) {
1285
+ throw new Error(`ctx.all: concurrency must be a positive integer, got ${concurrency}`);
1286
+ }
1287
+ if (concurrency === 1) return {
1288
+ waveSize: 1,
1289
+ maxWaves: 1
1290
+ };
1291
+ return {
1292
+ waveSize: Math.floor(concurrency / 2),
1293
+ maxWaves: 2
1294
+ };
1295
+ }
1296
+ __name(allWaves, "allWaves");
1297
+ var ENGINE_CHILD_ID = /^(.+)\.(?:all\.\d+\.(?:w\d+\.)?\d+|child\.\d+)$/;
1298
+ function parentRunIdOf(runId) {
1299
+ return ENGINE_CHILD_ID.exec(runId)?.[1];
1300
+ }
1301
+ __name(parentRunIdOf, "parentRunIdOf");
1302
+ var GROUP = /^all:(\d+)(:failFast)?$/;
1303
+ var REST = /^(?:w(\d+)\.)?\d+$/;
1304
+ function fanOfWaiter(waiter) {
1305
+ const group = waiter.parallelGroup ? GROUP.exec(waiter.parallelGroup) : null;
1306
+ if (!group) return void 0;
1307
+ const base = `child:${waiter.runId}.all.${group[1]}.`;
1308
+ if (!waiter.token.startsWith(base)) return void 0;
1309
+ const rest = REST.exec(waiter.token.slice(base.length));
1310
+ if (!rest) return void 0;
1311
+ const waiterPrefix = rest[1] === void 0 ? base : `${base}w${rest[1]}.`;
1312
+ return {
1313
+ waiterPrefix,
1314
+ failFast: group[2] !== void 0
1315
+ };
1316
+ }
1317
+ __name(fanOfWaiter, "fanOfWaiter");
1318
+
1219
1319
  // src/handshake/descriptor.ts
1220
1320
  var CURRENT_PROTOCOL_VERSION = 1;
1221
1321
  var LEGACY_V1_PROTOCOL = {
@@ -1957,6 +2057,23 @@ function resolveRunWaiting(run, waiterByRun) {
1957
2057
  return waiter ? classifyWaiterToken(waiter.token) : void 0;
1958
2058
  }
1959
2059
  __name(resolveRunWaiting, "resolveRunWaiting");
2060
+ var RUN_ID_BATCH = 500;
2061
+ async function listSignalWaitersOfRuns(store, runIds) {
2062
+ const unique = [
2063
+ ...new Set(runIds)
2064
+ ];
2065
+ if (unique.length === 0) return [];
2066
+ if (!store.listSignalWaitersByRun) {
2067
+ const ids = new Set(unique);
2068
+ return (await store.listSignalWaiters("")).filter((w) => ids.has(w.runId));
2069
+ }
2070
+ const out = [];
2071
+ for (let at = 0; at < unique.length; at += RUN_ID_BATCH) {
2072
+ out.push(...await store.listSignalWaitersByRun(unique.slice(at, at + RUN_ID_BATCH)));
2073
+ }
2074
+ return out;
2075
+ }
2076
+ __name(listSignalWaitersOfRuns, "listSignalWaitersOfRuns");
1960
2077
  function indexWaitersByRun(waiters) {
1961
2078
  const byRun = /* @__PURE__ */ new Map();
1962
2079
  for (const w of waiters) if (!byRun.has(w.runId)) byRun.set(w.runId, w);
@@ -2444,6 +2561,54 @@ function errorMessage(error2) {
2444
2561
  }
2445
2562
  __name(errorMessage, "errorMessage");
2446
2563
 
2564
+ // src/singleton-admission.ts
2565
+ var SINGLETON_ADMITTED_TAG = "durable:singleton:admitted";
2566
+ var SINGLETON_ACTIVE_STATUSES = [
2567
+ "pending",
2568
+ "running",
2569
+ "suspended",
2570
+ "blocked",
2571
+ "cancelling"
2572
+ ];
2573
+ function singletonAdmissionTags(runs, runId, tag, workflow, limit) {
2574
+ if (!Number.isInteger(limit) || limit < 1) throw new Error("singleton limit must be a positive integer");
2575
+ const active = runs.filter((run) => run.workflow === workflow && SINGLETON_ACTIVE_STATUSES.includes(run.status) && run.tags?.includes(tag));
2576
+ const candidate = active.find((run) => run.id === runId);
2577
+ if (!candidate) return null;
2578
+ if (candidate.tags?.includes(SINGLETON_ADMITTED_TAG)) return candidate.tags;
2579
+ if (candidate.status === "cancelling") return null;
2580
+ const holders = active.filter((run) => run.tags?.includes(SINGLETON_ADMITTED_TAG));
2581
+ const remaining = limit - holders.length;
2582
+ if (remaining <= 0) return null;
2583
+ 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));
2584
+ if (!waiters.slice(0, remaining).some((run) => run.id === runId)) return null;
2585
+ return [
2586
+ ...candidate.tags ?? [],
2587
+ SINGLETON_ADMITTED_TAG
2588
+ ];
2589
+ }
2590
+ __name(singletonAdmissionTags, "singletonAdmissionTags");
2591
+ async function clearTerminalSingletonAdmission(runId, patch, getRun) {
2592
+ if (!patch.status || !TERMINAL_RUN_STATUSES.includes(patch.status)) return patch;
2593
+ const tags = "tags" in patch ? patch.tags : (await getRun(runId))?.tags;
2594
+ return {
2595
+ ...patch,
2596
+ tags: tags?.filter((tag) => tag !== SINGLETON_ADMITTED_TAG)
2597
+ };
2598
+ }
2599
+ __name(clearTerminalSingletonAdmission, "clearTerminalSingletonAdmission");
2600
+ function singletonAdmissionDenialPatch(runs, runId, tag, workflow, retryWakeAt) {
2601
+ if (retryWakeAt === void 0) return void 0;
2602
+ const candidate = runs.find((run) => run.id === runId && run.workflow === workflow && run.tags?.includes(tag) && SINGLETON_ACTIVE_STATUSES.includes(run.status));
2603
+ if (!candidate || candidate.status === "cancelling") return void 0;
2604
+ return {
2605
+ status: "suspended",
2606
+ wakeAt: retryWakeAt,
2607
+ updatedAt: /* @__PURE__ */ new Date()
2608
+ };
2609
+ }
2610
+ __name(singletonAdmissionDenialPatch, "singletonAdmissionDenialPatch");
2611
+
2447
2612
  // src/singleton-gate.ts
2448
2613
  var SINGLETON_RETRY_MS = 1e3;
2449
2614
  var SINGLETON_RETRY_JITTER_MS = 250;
@@ -2457,6 +2622,9 @@ var SingletonGate = class {
2457
2622
  }
2458
2623
  /** The tag a singleton run carries, so the gate can find others sharing its key. */
2459
2624
  tag(cfg, input) {
2625
+ if (!this.deps.store.tryAdmitSingleton) {
2626
+ throw new Error("Singleton workflows require StateStore.tryAdmitSingleton for atomic durable admission");
2627
+ }
2460
2628
  return `singleton:${cfg.key(input)}`;
2461
2629
  }
2462
2630
  /** Next wake time for a gated run: the base retry delay jittered to avoid a wakeup stampede. */
@@ -2465,7 +2633,7 @@ var SingletonGate = class {
2465
2633
  }
2466
2634
  /**
2467
2635
  * Reject a start that would grow the same-key backlog past `limit + maxQueueDepth` (counting
2468
- * `pending`/`running`/`suspended` runs sharing the key in one scan). No-op when no `maxQueueDepth`
2636
+ * `pending`/`running`/`suspended`/`blocked`/`cancelling` runs sharing the key in one scan). No-op when no `maxQueueDepth`
2469
2637
  * is configured.
2470
2638
  */
2471
2639
  async assertCapacity(workflow, cfg, input) {
@@ -2474,34 +2642,25 @@ var SingletonGate = class {
2474
2642
  const queued = await this.deps.store.listRuns({
2475
2643
  tag: this.tag(cfg, input),
2476
2644
  workflow,
2477
- statuses: [
2478
- "pending",
2479
- "running",
2480
- "suspended",
2481
- "cancelling"
2482
- ]
2645
+ statuses: SINGLETON_ACTIVE_STATUSES
2483
2646
  });
2484
2647
  if (queued.length >= cap) {
2485
2648
  throw new SingletonQueueFullError(workflow, cfg.key(input), cfg.maxQueueDepth);
2486
2649
  }
2487
2650
  }
2488
- /**
2489
- * Whether `run` may run now under its key: it's among the `limit` oldest in-flight (running or
2490
- * suspended) runs sharing the key, by `(createdAt, id)` order. A consistent store gives every
2491
- * instance the same ordering, so admission is race-free + FIFO.
2492
- */
2651
+ /** Preserve durable holders and atomically claim one of the remaining slots. */
2493
2652
  async admit(run, cfg) {
2494
- const inflight = (await this.deps.store.listRuns({
2495
- tag: this.tag(cfg, run.input),
2496
- workflow: run.workflow,
2497
- statuses: [
2498
- "running",
2499
- "suspended",
2500
- "cancelling"
2501
- ]
2502
- })).sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime() || a.id.localeCompare(b.id));
2503
- const idx = inflight.findIndex((r) => r.id === run.id);
2504
- return idx >= 0 && idx < (cfg.limit ?? 1);
2653
+ if (!this.deps.store.tryAdmitSingleton) {
2654
+ throw new Error("Singleton workflows require StateStore.tryAdmitSingleton for atomic durable admission");
2655
+ }
2656
+ const admitted = await this.deps.store.tryAdmitSingleton(run.id, this.tag(cfg, run.input), run.workflow, cfg.limit ?? 1, this.retryWakeAt());
2657
+ if (admitted && !run.tags?.includes(SINGLETON_ADMITTED_TAG)) {
2658
+ run.tags = [
2659
+ ...run.tags ?? [],
2660
+ SINGLETON_ADMITTED_TAG
2661
+ ];
2662
+ }
2663
+ return admitted;
2505
2664
  }
2506
2665
  /**
2507
2666
  * Notify-on-release: a singleton run settled (freeing a slot), so dispatch the oldest gated
@@ -2725,16 +2884,6 @@ var TransportPool = class {
2725
2884
  }
2726
2885
  };
2727
2886
 
2728
- // src/completion.ts
2729
- function unwrapCompletion(payload, label) {
2730
- const c = payload;
2731
- if (c && typeof c === "object" && "ok" in c && c.ok === false) {
2732
- throw new FatalError(`${label} failed: ${c.error}`);
2733
- }
2734
- return c?.value;
2735
- }
2736
- __name(unwrapCompletion, "unwrapCompletion");
2737
-
2738
2887
  // src/step-name-symbol.ts
2739
2888
  var DURABLE_STEP_NAME = Symbol.for("durable.step.name");
2740
2889
  var DURABLE_STEP_CONFIG = Symbol.for("durable.step.config");
@@ -2756,6 +2905,8 @@ function stepConfigOf(ref) {
2756
2905
  __name(stepConfigOf, "stepConfigOf");
2757
2906
 
2758
2907
  // src/workflow-ctx.ts
2908
+ var FAN_CHUNK = 500;
2909
+ var FOLD_MIN_FAN = 100;
2759
2910
  function normalizeChildOptions(options) {
2760
2911
  return typeof options === "string" ? {
2761
2912
  childId: options
@@ -3098,7 +3249,7 @@ function createWorkflowCtx(host, runId, compensations, workflow = "", searchAttr
3098
3249
  if (existing && existing.status === "completed") {
3099
3250
  return unwrapCompletion(existing.output, `child "${id}"`);
3100
3251
  }
3101
- await store.putSignalWaiter({
3252
+ if (!existing) await store.putSignalWaiter({
3102
3253
  token: `child:${id}`,
3103
3254
  runId,
3104
3255
  seq: current
@@ -3129,16 +3280,33 @@ function createWorkflowCtx(host, runId, compensations, workflow = "", searchAttr
3129
3280
  status: "running"
3130
3281
  }));
3131
3282
  }
3132
- throw new WorkflowSuspended();
3283
+ throw new WorkflowSuspended(void 0, fanJoin([
3284
+ {
3285
+ waiterPrefix: `child:${id}`,
3286
+ checkpointPrefix: `signal:child:${id}`
3287
+ }
3288
+ ]));
3133
3289
  }, "child");
3134
3290
  const all = /* @__PURE__ */ __name(async (workflow2, inputs, opts) => {
3135
3291
  if (inputs.length === 0) return [];
3136
3292
  const mode = opts?.mode ?? "waitAll";
3137
3293
  const name = workflowName(workflow2);
3294
+ const waves = allWaves(inputs.length, opts?.concurrency);
3138
3295
  const positions = inputs.map(() => pos.next());
3139
- const group = `all:${positions[0]}`;
3140
- const id = /* @__PURE__ */ __name((i) => `${runId}.all.${positions[0]}.${i}`, "id");
3141
- const existing = await Promise.all(positions.map((seq) => readCheckpoint(seq)));
3296
+ const firstSeq = positions[0];
3297
+ const group = allGroup(firstSeq, mode);
3298
+ const waveOf = /* @__PURE__ */ __name((i) => waves ? Math.floor(i / waves.waveSize) : void 0, "waveOf");
3299
+ const id = /* @__PURE__ */ __name((i) => allChildId(runId, firstSeq, i, waveOf(i)), "id");
3300
+ const { checkpoints: existing, folded } = await readFan(positions, [
3301
+ allWait(runId, firstSeq).checkpointPrefix,
3302
+ allFoldedName(runId, firstSeq)
3303
+ ]);
3304
+ const waveCount = waves ? Math.ceil(inputs.length / waves.waveSize) : 1;
3305
+ const waveItems = /* @__PURE__ */ __name((k) => {
3306
+ if (!waves) return inputs.map((_, i) => i);
3307
+ const from = k * waves.waveSize;
3308
+ return inputs.map((_, i) => i).slice(from, from + waves.waveSize);
3309
+ }, "waveItems");
3142
3310
  if (mode === "failFast") {
3143
3311
  for (let i = 0; i < inputs.length; i += 1) {
3144
3312
  const cp = existing[i];
@@ -3157,32 +3325,91 @@ function createWorkflowCtx(host, runId, compensations, workflow = "", searchAttr
3157
3325
  }
3158
3326
  }
3159
3327
  }
3160
- let pending = false;
3161
- for (let i = 0; i < inputs.length; i += 1) {
3162
- const cp = existing[i];
3163
- if (cp?.status === "completed") continue;
3164
- pending = true;
3165
- const seq = positions[i];
3166
- const childId = id(i);
3167
- await store.putSignalWaiter({
3168
- token: `child:${childId}`,
3169
- runId,
3170
- seq,
3171
- parallelGroup: group
3172
- });
3173
- if (!await store.getRun(childId)) host.startChild(name, inputs[i], childId);
3174
- if (!cp) {
3175
- await writeCheckpoint(instantCheckpoint({
3328
+ if (store.compactCheckpoints && inputs.length >= FOLD_MIN_FAN) {
3329
+ for (let k = 0; k < waveCount; k += 1) {
3330
+ const items = waveItems(k);
3331
+ const first = items[0];
3332
+ const last = items[items.length - 1];
3333
+ if (folded[first]) continue;
3334
+ const values = [];
3335
+ for (const i of items) {
3336
+ const c = existing[i]?.status === "completed" ? existing[i]?.output : void 0;
3337
+ if (!c || typeof c !== "object" || c.ok !== true) break;
3338
+ values.push(c.value);
3339
+ }
3340
+ if (values.length < items.length) continue;
3341
+ const into = instantCheckpoint({
3176
3342
  runId,
3177
- seq,
3178
- name: `signal:child:${childId}`,
3343
+ seq: positions[first],
3344
+ name: allFoldedName(runId, firstSeq, waves ? k : void 0),
3179
3345
  kind: "signal",
3180
- status: "running",
3346
+ output: {
3347
+ from: first,
3348
+ values
3349
+ },
3181
3350
  parallelGroup: group
3351
+ });
3352
+ await store.compactCheckpoints(runId, positions[first], positions[last], into);
3353
+ for (const i of items) replay?.delete(positions[i]);
3354
+ replay?.set(into.seq, into);
3355
+ }
3356
+ }
3357
+ const sent = /* @__PURE__ */ __name((k) => waveItems(k).some((i) => existing[i]), "sent");
3358
+ const settled = /* @__PURE__ */ __name((k) => waveItems(k).every((i) => existing[i]?.status === "completed"), "settled");
3359
+ const active = [];
3360
+ let unsettled = 0;
3361
+ for (let k = 0; k < waveCount; k += 1) {
3362
+ if (settled(k)) continue;
3363
+ if (sent(k) || unsettled < (waves?.maxWaves ?? 1)) {
3364
+ active.push(k);
3365
+ unsettled += 1;
3366
+ }
3367
+ }
3368
+ if (active.length > 0) {
3369
+ const toRegister = [];
3370
+ const toEnsure = [];
3371
+ for (const k of active) {
3372
+ for (const i of waveItems(k)) {
3373
+ const cp = existing[i];
3374
+ if (cp?.status === "completed") continue;
3375
+ (cp ? toEnsure : toRegister).push(i);
3376
+ }
3377
+ }
3378
+ for (let at = 0; at < toRegister.length; at += FAN_CHUNK) {
3379
+ const chunk = toRegister.slice(at, at + FAN_CHUNK);
3380
+ const entries = chunk.map((i) => ({
3381
+ // Carry the fan `group` onto the waiter too: the child's terminal `signal:child:`
3382
+ // checkpoint is (re)written by engine.signal when the child notifies the parent,
3383
+ // OVERWRITING the running placeholder at the same seq — so without this the resolved
3384
+ // checkpoint would lose the group and the dashboard would render the resolved fan as a
3385
+ // sequential chain.
3386
+ waiter: {
3387
+ token: `child:${id(i)}`,
3388
+ runId,
3389
+ seq: positions[i],
3390
+ parallelGroup: group
3391
+ },
3392
+ checkpoint: instantCheckpoint({
3393
+ runId,
3394
+ seq: positions[i],
3395
+ name: `signal:child:${id(i)}`,
3396
+ kind: "signal",
3397
+ status: "running",
3398
+ parallelGroup: group
3399
+ })
3400
+ }));
3401
+ await registerChildWaits(entries);
3402
+ for (const i of chunk) host.startChild(name, inputs[i], id(i));
3403
+ }
3404
+ for (let at = 0; at < toEnsure.length; at += FAN_CHUNK) {
3405
+ await Promise.all(toEnsure.slice(at, at + FAN_CHUNK).map(async (i) => {
3406
+ if (!await store.getRun(id(i))) host.startChild(name, inputs[i], id(i));
3182
3407
  }));
3183
3408
  }
3184
3409
  }
3185
- if (pending) throw new WorkflowSuspended();
3410
+ if (active.length > 0) {
3411
+ throw new WorkflowSuspended(void 0, fanJoin(active.map((k) => allWait(runId, firstSeq, waves ? k : void 0))));
3412
+ }
3186
3413
  const outputs = [];
3187
3414
  const failures = [];
3188
3415
  for (let i = 0; i < inputs.length; i += 1) {
@@ -3201,6 +3428,67 @@ function createWorkflowCtx(host, runId, compensations, workflow = "", searchAttr
3201
3428
  if (failures.length > 0) throw new GatherError(failures, inputs.length);
3202
3429
  return outputs;
3203
3430
  }, "all");
3431
+ const readFan = /* @__PURE__ */ __name(async (positions, prefixes) => {
3432
+ const checkpoints = positions.map((seq) => replay?.get(seq) ?? null);
3433
+ const folded = positions.map(() => false);
3434
+ const expand = /* @__PURE__ */ __name(() => {
3435
+ for (let i = 0; i < checkpoints.length; i += 1) {
3436
+ const cp = checkpoints[i];
3437
+ if (!cp?.name.startsWith(FOLDED_PREFIX) || folded[i]) continue;
3438
+ const fold = cp.output;
3439
+ if (!fold || typeof fold.from !== "number" || !Array.isArray(fold.values)) {
3440
+ throw new Error(`ctx.all: folded checkpoint "${cp.name}" at seq ${cp.seq} is malformed`);
3441
+ }
3442
+ const { from, values } = fold;
3443
+ values.forEach((value, j) => {
3444
+ checkpoints[from + j] = {
3445
+ ...cp,
3446
+ seq: positions[from + j],
3447
+ output: {
3448
+ ok: true,
3449
+ value
3450
+ }
3451
+ };
3452
+ folded[from + j] = true;
3453
+ });
3454
+ }
3455
+ }, "expand");
3456
+ expand();
3457
+ const missing = positions.filter((_, i) => !checkpoints[i]);
3458
+ if (missing.length === 0) return {
3459
+ checkpoints,
3460
+ folded
3461
+ };
3462
+ if (missing.length > 1 && store.listCheckpointsByNamePrefix) {
3463
+ const bySeq = /* @__PURE__ */ new Map();
3464
+ for (const cp of await store.listCheckpointsByNamePrefix(runId, prefixes)) {
3465
+ bySeq.set(cp.seq, cp);
3466
+ }
3467
+ positions.forEach((seq, i) => {
3468
+ checkpoints[i] ??= bySeq.get(seq) ?? null;
3469
+ });
3470
+ } else {
3471
+ await Promise.all(positions.map(async (seq, i) => {
3472
+ checkpoints[i] ??= await readCheckpoint(seq);
3473
+ }));
3474
+ }
3475
+ expand();
3476
+ return {
3477
+ checkpoints,
3478
+ folded
3479
+ };
3480
+ }, "readFan");
3481
+ const registerChildWaits = /* @__PURE__ */ __name(async (entries) => {
3482
+ if (store.registerChildWaits) {
3483
+ await store.registerChildWaits(entries);
3484
+ } else {
3485
+ await Promise.all(entries.map(async (e) => {
3486
+ await store.putSignalWaiter(e.waiter);
3487
+ await store.saveCheckpoint(e.checkpoint);
3488
+ }));
3489
+ }
3490
+ for (const e of entries) replay?.set(e.checkpoint.seq, e.checkpoint);
3491
+ }, "registerChildWaits");
3204
3492
  const startChild = /* @__PURE__ */ __name(async (workflow2, input, options) => {
3205
3493
  const { childId, priority, version } = normalizeChildOptions(options);
3206
3494
  const current = pos.next();
@@ -4018,7 +4306,7 @@ var WorkflowEngine = class {
4018
4306
  VERSION_UNDECLARED_TAG
4019
4307
  ] : []
4020
4308
  ];
4021
- const tags = mergeTags(registered.tags, stampedTags);
4309
+ const tags = mergeTags(registered.tags, stampedTags)?.filter((tag) => tag !== SINGLETON_ADMITTED_TAG);
4022
4310
  if (registered.singleton) {
4023
4311
  await this.singletons.assertCapacity(name, registered.singleton, input);
4024
4312
  }
@@ -4093,6 +4381,12 @@ var WorkflowEngine = class {
4093
4381
  await this.store.releaseRunLock(runId);
4094
4382
  throw new NamespaceMismatch();
4095
4383
  }
4384
+ if (run.status === "failed" && run.tags?.includes(SINGLETON_ADMITTED_TAG)) {
4385
+ run.tags = run.tags.filter((tag) => tag !== SINGLETON_ADMITTED_TAG);
4386
+ await this.store.updateRun(run.id, {
4387
+ tags: run.tags
4388
+ });
4389
+ }
4096
4390
  const registered = this.workflows.get(versionKey(run.workflow, run.workflowVersion)) ?? await this.findInheritedRegistration(run) ?? await this.resolveRemoteByConvention(run);
4097
4391
  if (!registered) {
4098
4392
  throw new Error(`workflow ${run.workflow}@${run.workflowVersion} is not registered \u2014 keep the prior version deployed so in-flight runs can drain (skew protection)`);
@@ -4234,15 +4528,16 @@ var WorkflowEngine = class {
4234
4528
  */
4235
4529
  async findParentRunId(childRunId) {
4236
4530
  const token = `child:${childRunId}`;
4237
- for (const waiter of await this.store.listSignalWaiters("child:")) {
4238
- if (waiter.token === token) return waiter.runId;
4239
- }
4240
- return void 0;
4531
+ const waiters = await this.store.listSignalWaiters(token);
4532
+ return waiters.find((waiter) => waiter.token === token)?.runId;
4241
4533
  }
4242
4534
  /** Track an in-flight execution so {@link drain} can wait for it. */
4243
4535
  track(p) {
4244
4536
  this.inflight.add(p);
4245
- void p.finally(() => this.inflight.delete(p));
4537
+ const done = /* @__PURE__ */ __name(() => {
4538
+ this.inflight.delete(p);
4539
+ }, "done");
4540
+ void p.then(done, done);
4246
4541
  return p;
4247
4542
  }
4248
4543
  /**
@@ -4455,7 +4750,10 @@ var WorkflowEngine = class {
4455
4750
  const due = await this.store.listDueTimers(nowMs, this.namespace);
4456
4751
  const blocked = await this.dueBlockedRuns(nowMs);
4457
4752
  if (due.length === 0 && blocked.length === 0) return [];
4458
- const waiterByRun = indexWaitersByRun(await this.store.listSignalWaiters(""));
4753
+ const waiterByRun = indexWaitersByRun(await listSignalWaitersOfRuns(this.store, [
4754
+ ...due,
4755
+ ...blocked
4756
+ ].map((run) => run.id)));
4459
4757
  return this.resumeLeased([
4460
4758
  ...due,
4461
4759
  ...blocked
@@ -4565,11 +4863,6 @@ var WorkflowEngine = class {
4565
4863
  } : {}
4566
4864
  });
4567
4865
  } else if (cp.kind === "signal" && cp.name.startsWith("signal:child:") && cp.status === "completed" && isFailureCompletion(cp.output)) {
4568
- await this.store.saveCheckpoint({
4569
- ...cp,
4570
- status: "running",
4571
- output: void 0
4572
- });
4573
4866
  const childId = cp.name.slice("signal:child:".length);
4574
4867
  const parked = [];
4575
4868
  for (; ; ) {
@@ -4577,8 +4870,28 @@ var WorkflowEngine = class {
4577
4870
  if (!buffered) break;
4578
4871
  parked.push(buffered.payload);
4579
4872
  }
4873
+ const successAt = parked.findIndex((payload) => typeof payload === "object" && payload !== null && payload.ok === true);
4874
+ const success = successAt === -1 ? void 0 : parked.splice(successAt, 1)[0];
4580
4875
  for (const payload of parked) await this.store.bufferSignal(`child:${childId}`, payload);
4581
- const bufferedSuccess = parked.some((payload) => typeof payload === "object" && payload !== null && payload.ok === true);
4876
+ const bufferedSuccess = success !== void 0;
4877
+ if (bufferedSuccess) {
4878
+ await this.store.saveCheckpoint({
4879
+ ...cp,
4880
+ output: success
4881
+ });
4882
+ } else {
4883
+ await this.store.putSignalWaiter({
4884
+ token: `child:${childId}`,
4885
+ runId,
4886
+ seq: cp.seq,
4887
+ parallelGroup: cp.parallelGroup
4888
+ });
4889
+ await this.store.saveCheckpoint({
4890
+ ...cp,
4891
+ status: "running",
4892
+ output: void 0
4893
+ });
4894
+ }
4582
4895
  if (!bufferedSuccess) {
4583
4896
  const child = await this.store.getRun(childId);
4584
4897
  if (child && (child.status === "failed" || child.status === "dead")) {
@@ -4593,7 +4906,8 @@ var WorkflowEngine = class {
4593
4906
  status: "pending",
4594
4907
  error: void 0,
4595
4908
  ...resurrecting ? {
4596
- recoveryAttempts: 0
4909
+ recoveryAttempts: 0,
4910
+ tags: run.tags?.filter((tag) => tag !== SINGLETON_ADMITTED_TAG)
4597
4911
  } : {},
4598
4912
  updatedAt: /* @__PURE__ */ new Date()
4599
4913
  });
@@ -4712,10 +5026,19 @@ var WorkflowEngine = class {
4712
5026
  off();
4713
5027
  resolve(toResult(run));
4714
5028
  }, "finish");
5029
+ const fail = /* @__PURE__ */ __name((error2) => {
5030
+ if (done) return;
5031
+ done = true;
5032
+ if (timer) clearTimeout(timer);
5033
+ if (poller) clearInterval(poller);
5034
+ off();
5035
+ reject(error2);
5036
+ }, "fail");
4715
5037
  const check = /* @__PURE__ */ __name(() => {
5038
+ if (done) return;
4716
5039
  void this.store.getRun(runId).then((run) => {
4717
5040
  if (run && isSettled(run)) finish(run);
4718
- });
5041
+ }, fail);
4719
5042
  }, "check");
4720
5043
  const poller = opts?.until === "terminal" ? (() => {
4721
5044
  const interval = setInterval(check, 250);
@@ -4848,12 +5171,62 @@ var WorkflowEngine = class {
4848
5171
  // for an ordinary (non-fan) signal/child await.
4849
5172
  parallelGroup: waiter.parallelGroup
4850
5173
  }));
5174
+ if (await this.fanStillWaiting(waiter, payload)) {
5175
+ return {
5176
+ runId: waiter.runId,
5177
+ status: "suspended"
5178
+ };
5179
+ }
4851
5180
  return this.resumeImplicit(waiter.runId).catch((err) => {
4852
5181
  if (err instanceof NamespaceMismatch) return null;
4853
5182
  throw err;
4854
5183
  });
4855
5184
  }
4856
5185
  /**
5186
+ * The `ctx.all` join barrier: true when `waiter` was one child of a fan (or of one wave of it)
5187
+ * whose siblings are still out, so waking the parent now would only replay the fan and re-suspend.
5188
+ * Skipping that wake is what keeps a fan of N children at O(N) work instead of N replays of N items.
5189
+ *
5190
+ * Safe because the LAST sibling to settle always wakes the parent: each child consumes its own
5191
+ * waiter before it asks, so whichever asks last sees none left. A failed child of a failFast fan
5192
+ * always wakes it. Anything that is not a `ctx.all` waiter, or a store that cannot answer the
5193
+ * question cheaply, wakes the parent as before.
5194
+ */
5195
+ async fanStillWaiting(waiter, payload) {
5196
+ if (!this.store.hasSignalWaiter) return false;
5197
+ const fan = fanOfWaiter(waiter);
5198
+ if (!fan) return false;
5199
+ if (fan.failFast && isFailedCompletion(payload)) return false;
5200
+ return this.store.hasSignalWaiter(waiter.runId, fan.waiterPrefix);
5201
+ }
5202
+ /**
5203
+ * Re-drive a run that just suspended on awaited children (a `ctx.all` fan or wave, a `ctx.child`)
5204
+ * which, by now, have settled.
5205
+ *
5206
+ * {@link execute} drops a wake that finds the run's lease held. So if a child settles while the
5207
+ * parent is still executing (the turn that dispatched it, or one woken by an earlier child), that
5208
+ * wake is spent, and the turn suspends on a snapshot that predates the child. With the `ctx.all`
5209
+ * barrier that wake was the only one the fan would get. Nothing would wake the run until the
5210
+ * `reconcileMs` sweep, minutes later.
5211
+ *
5212
+ * Called after the turn released its lease, so any child that settles from here on wakes the run
5213
+ * itself. Only re-drives for a group with no waiter left whose checkpoints are all settled, which
5214
+ * the next turn is then guaranteed to see — so it cannot loop.
5215
+ */
5216
+ async redriveSettledChildren(runId, fan) {
5217
+ if (!this.store.hasSignalWaiter) return;
5218
+ for (const wait of fan.waits) {
5219
+ if (await this.store.hasSignalWaiter(runId, wait.waiterPrefix)) continue;
5220
+ const checkpoints = this.store.listCheckpointsByNamePrefix ? await this.store.listCheckpointsByNamePrefix(runId, [
5221
+ wait.checkpointPrefix
5222
+ ]) : (await this.store.listCheckpoints(runId)).filter((cp) => cp.name.startsWith(wait.checkpointPrefix));
5223
+ if (checkpoints.length > 0 && checkpoints.every((cp) => cp.status === "completed")) {
5224
+ await this.resumeImplicit(runId);
5225
+ return;
5226
+ }
5227
+ }
5228
+ }
5229
+ /**
4857
5230
  * Deliver an external signal to the run waiting on `token`, resume it with `payload`, and return
4858
5231
  * the run result — or null if no run is (or ends up) waiting for it.
4859
5232
  *
@@ -5146,23 +5519,11 @@ var WorkflowEngine = class {
5146
5519
  }
5147
5520
  /** The given runs plus every descendant, children before parents. One waiter scan for the batch. */
5148
5521
  async collectSubtrees(rootIds) {
5149
- const waitersByParent = /* @__PURE__ */ new Map();
5150
- for (const w of await this.store.listSignalWaiters("child:")) {
5151
- const list = waitersByParent.get(w.runId) ?? [];
5152
- list.push(w.token.slice("child:".length));
5153
- waitersByParent.set(w.runId, list);
5154
- }
5155
- const prefixes = [
5156
- "signal:child:",
5157
- "spawn:"
5158
- ];
5522
+ const prefixes = CHILD_EDGE_PREFIXES;
5159
5523
  const childrenOf = /* @__PURE__ */ __name(async (parent) => {
5160
- const ids = new Set(waitersByParent.get(parent) ?? []);
5524
+ const ids = new Set(await this.awaitedChildIds(parent));
5161
5525
  const cps = this.store.listCheckpointsByNamePrefix ? await this.store.listCheckpointsByNamePrefix(parent, prefixes) : (await this.store.listCheckpoints(parent)).filter((cp) => prefixes.some((p) => cp.name.startsWith(p)));
5162
- for (const cp of cps) {
5163
- if (cp.name.startsWith("signal:child:")) ids.add(cp.name.slice("signal:child:".length));
5164
- if (cp.name.startsWith("spawn:") && typeof cp.output === "string") ids.add(cp.output);
5165
- }
5526
+ for (const id of childIdsOf(cps)) ids.add(id);
5166
5527
  return [
5167
5528
  ...ids
5168
5529
  ];
@@ -5212,22 +5573,20 @@ var WorkflowEngine = class {
5212
5573
  * so a completed parent (or a completed awaited child) would otherwise drop out of the tree. The
5213
5574
  * `signal:child:<id>` checkpoint (the placeholder written when the parent first awaits the child,
5214
5575
  * overwritten as completed/failed when it settles) persists across completion, so we read both and
5215
- * dedupe — the edge stays stable for a finished run, not just a live one.
5576
+ * dedupe — the edge stays stable for a finished run, not just a live one. A settled `ctx.all`
5577
+ * group folded into one `fan:` checkpoint stands for every child in it.
5216
5578
  */
5579
+ /** The children `parentRunId` is awaiting right now, read off its own live `child:` waiters. */
5580
+ async awaitedChildIds(parentRunId) {
5581
+ return (await listSignalWaitersOfRuns(this.store, [
5582
+ parentRunId
5583
+ ])).filter((w) => w.token.startsWith("child:")).map((w) => w.token.slice("child:".length));
5584
+ }
5217
5585
  async getRunChildren(parentRunId) {
5218
- const childIds = /* @__PURE__ */ new Set();
5219
- for (const w of await this.store.listSignalWaiters("child:")) {
5220
- if (w.runId === parentRunId) childIds.add(w.token.slice("child:".length));
5221
- }
5222
- const prefixes = [
5223
- "signal:child:",
5224
- "spawn:"
5225
- ];
5586
+ const childIds = new Set(await this.awaitedChildIds(parentRunId));
5587
+ const prefixes = CHILD_EDGE_PREFIXES;
5226
5588
  const childCheckpoints = this.store.listCheckpointsByNamePrefix ? await this.store.listCheckpointsByNamePrefix(parentRunId, prefixes) : (await this.store.listCheckpoints(parentRunId)).filter((cp) => prefixes.some((p) => cp.name.startsWith(p)));
5227
- for (const cp of childCheckpoints) {
5228
- if (cp.name.startsWith("signal:child:")) childIds.add(cp.name.slice("signal:child:".length));
5229
- if (cp.name.startsWith("spawn:") && typeof cp.output === "string") childIds.add(cp.output);
5230
- }
5589
+ for (const id of childIdsOf(childCheckpoints)) childIds.add(id);
5231
5590
  return [
5232
5591
  ...childIds
5233
5592
  ];
@@ -5498,6 +5857,26 @@ var WorkflowEngine = class {
5498
5857
  durationMs: Date.now() - step.startedAt.getTime()
5499
5858
  });
5500
5859
  }
5860
+ /** Admission persists the wait atomically; never overwrite a cancellation after it returns. */
5861
+ async singletonWaitResult(run) {
5862
+ const current = await this.store.getRun(run.id);
5863
+ await this.store.releaseRunLock(run.id);
5864
+ if (!current) throw new Error(`run ${run.id} was deleted during singleton admission`);
5865
+ if (current.status === "suspended") {
5866
+ this.emit({
5867
+ type: "run.suspended",
5868
+ runId: current.id,
5869
+ workflow: current.workflow,
5870
+ namespace: current.namespace
5871
+ });
5872
+ }
5873
+ return {
5874
+ runId: current.id,
5875
+ status: current.status,
5876
+ output: current.output,
5877
+ error: current.error
5878
+ };
5879
+ }
5501
5880
  async execute(run, fn) {
5502
5881
  const registered = this.workflows.get(versionKey(run.workflow, run.workflowVersion)) ?? await this.findInheritedRegistration(run) ?? await this.resolveRemoteByConvention(run);
5503
5882
  const lockNow = this.clock();
@@ -5667,23 +6046,7 @@ var WorkflowEngine = class {
5667
6046
  });
5668
6047
  }
5669
6048
  if (registered.singleton && !await this.singletons.admit(run, registered.singleton)) {
5670
- const wakeAt = this.singletons.retryWakeAt();
5671
- await this.store.updateRun(run.id, {
5672
- status: "suspended",
5673
- wakeAt,
5674
- updatedAt: /* @__PURE__ */ new Date()
5675
- });
5676
- this.emit({
5677
- type: "run.suspended",
5678
- runId: run.id,
5679
- workflow: run.workflow,
5680
- namespace: run.namespace
5681
- });
5682
- await this.store.releaseRunLock(run.id);
5683
- return {
5684
- runId: run.id,
5685
- status: "suspended"
5686
- };
6049
+ return this.singletonWaitResult(run);
5687
6050
  }
5688
6051
  const wfPlan = await this.planRoute(tenantGroup(sanitizeQueueToken(remote.group), void 0), registered.requires);
5689
6052
  if (wfPlan?.status === "blocked") return this.parkBlocked(run, wfPlan);
@@ -6297,28 +6660,13 @@ var WorkflowEngine = class {
6297
6660
  });
6298
6661
  }
6299
6662
  if (run.status !== "cancelling" && registered?.singleton && !await this.singletons.admit(run, registered.singleton)) {
6300
- const wakeAt = this.singletons.retryWakeAt();
6301
- await this.store.updateRun(run.id, {
6302
- status: "suspended",
6303
- wakeAt,
6304
- updatedAt: /* @__PURE__ */ new Date()
6305
- });
6306
- this.emit({
6307
- type: "run.suspended",
6308
- runId: run.id,
6309
- workflow: run.workflow,
6310
- namespace: run.namespace
6311
- });
6312
- await this.store.releaseRunLock(run.id);
6313
- return {
6314
- runId: run.id,
6315
- status: "suspended"
6316
- };
6663
+ return this.singletonWaitResult(run);
6317
6664
  }
6318
6665
  const compensations = [];
6319
6666
  const snapshot = await this.store.listCheckpoints(run.id);
6320
6667
  const replay = /* @__PURE__ */ new Map();
6321
6668
  for (const cp of snapshot) replay.set(cp.seq, cp);
6669
+ let suspendedOnFan;
6322
6670
  const ctx = createWorkflowCtx(this.ctxHostFor(replay, run.namespace), run.id, compensations, run.workflow, registered?.searchAttributesSchema);
6323
6671
  try {
6324
6672
  const output = await runInWorkflowCtx(ctx, () => fn(ctx, run.input));
@@ -6370,10 +6718,12 @@ var WorkflowEngine = class {
6370
6718
  if (this.cancelRequested.has(run.id)) {
6371
6719
  return await this.compensateAndCancel(run, compensations, replay);
6372
6720
  }
6373
- return await this.settleRun(run, {
6721
+ const suspended = await this.settleRun(run, {
6374
6722
  kind: "suspended",
6375
6723
  wakeAt: err.wakeAt
6376
6724
  });
6725
+ suspendedOnFan = err.fan;
6726
+ return suspended;
6377
6727
  }
6378
6728
  if (this.cancelRequested.has(run.id)) {
6379
6729
  return await this.compensateAndCancel(run, compensations, replay);
@@ -6393,6 +6743,9 @@ var WorkflowEngine = class {
6393
6743
  });
6394
6744
  } finally {
6395
6745
  await this.store.releaseRunLock(run.id);
6746
+ if (suspendedOnFan) {
6747
+ void this.redriveSettledChildren(run.id, suspendedOnFan).catch(() => void 0);
6748
+ }
6396
6749
  }
6397
6750
  }
6398
6751
  /**
@@ -7207,6 +7560,21 @@ var RemoteStepError = class extends Error {
7207
7560
  this.stepError = stepError;
7208
7561
  }
7209
7562
  };
7563
+ var CHILD_EDGE_PREFIXES = [
7564
+ "signal:child:",
7565
+ FOLDED_PREFIX,
7566
+ "spawn:"
7567
+ ];
7568
+ function childIdsOf(checkpoints) {
7569
+ const ids = [];
7570
+ for (const cp of checkpoints) {
7571
+ if (cp.name.startsWith("signal:child:")) ids.push(cp.name.slice("signal:child:".length));
7572
+ if (cp.name.startsWith(FOLDED_PREFIX)) ids.push(...foldedChildIds(cp));
7573
+ if (cp.name.startsWith("spawn:") && typeof cp.output === "string") ids.push(cp.output);
7574
+ }
7575
+ return ids;
7576
+ }
7577
+ __name(childIdsOf, "childIdsOf");
7210
7578
 
7211
7579
  // src/run-facets.ts
7212
7580
  function facetOrigin(origin) {
@@ -7332,6 +7700,8 @@ var CodecStateStore = class {
7332
7700
  }
7333
7701
  inner;
7334
7702
  codec;
7703
+ /** Atomic claims touch only searchable metadata and bypass payload encoding. */
7704
+ tryAdmitSingleton;
7335
7705
  /**
7336
7706
  * Forwarded verbatim — a facet cell counts `status`/`origin`, neither of which the codec touches
7337
7707
  * (it encodes payloads: input/output/error). Bound as a PROPERTY, and only when the inner store
@@ -7354,6 +7724,14 @@ var CodecStateStore = class {
7354
7724
  /** Forwarded verbatim — a count reads no payload. Bound only when the inner store counts, so the
7355
7725
  * engine's facet/listing fallback still applies through the wrapper. */
7356
7726
  countRuns;
7727
+ /** The placeholder checkpoints are encoded like any other checkpoint. */
7728
+ registerChildWaits;
7729
+ /** The folded checkpoint carries the children's outputs, so it is encoded. */
7730
+ compactCheckpoints;
7731
+ /** Forwarded verbatim — a waiter carries no payload. */
7732
+ listSignalWaitersByRun;
7733
+ /** Forwarded verbatim — a waiter carries no payload. */
7734
+ hasSignalWaiter;
7357
7735
  /** Schedules persist their run `input` inside `spec` — encoded like any other payload. Bound only
7358
7736
  * when the inner store persists schedules, so `engine.schedules` reports absence faithfully. */
7359
7737
  saveSchedule;
@@ -7364,12 +7742,24 @@ var CodecStateStore = class {
7364
7742
  constructor(inner, codec) {
7365
7743
  this.inner = inner;
7366
7744
  this.codec = codec;
7745
+ if (inner.tryAdmitSingleton) this.tryAdmitSingleton = inner.tryAdmitSingleton.bind(inner);
7367
7746
  const deleteRuns = inner.deleteRuns;
7368
7747
  if (deleteRuns) this.deleteRuns = (ids) => deleteRuns.call(inner, ids);
7369
7748
  const prune = inner.pruneTerminalRuns;
7370
7749
  if (prune) this.pruneTerminalRuns = (policy, now, limit) => prune.call(inner, policy, now, limit);
7371
7750
  const count = inner.countRuns;
7372
7751
  if (count) this.countRuns = (query) => count.call(inner, query);
7752
+ const registerChildWaits = inner.registerChildWaits;
7753
+ if (registerChildWaits) this.registerChildWaits = (entries) => registerChildWaits.call(inner, entries.map((e) => ({
7754
+ waiter: e.waiter,
7755
+ checkpoint: this.encCp(e.checkpoint)
7756
+ })));
7757
+ const compact = inner.compactCheckpoints;
7758
+ if (compact) this.compactCheckpoints = (runId, fromSeq, toSeq, into) => compact.call(inner, runId, fromSeq, toSeq, this.encCp(into));
7759
+ const byRun = inner.listSignalWaitersByRun;
7760
+ if (byRun) this.listSignalWaitersByRun = (runIds) => byRun.call(inner, runIds);
7761
+ const hasWaiter = inner.hasSignalWaiter;
7762
+ if (hasWaiter) this.hasSignalWaiter = (runId, tokenPrefix) => hasWaiter.call(inner, runId, tokenPrefix);
7373
7763
  const { saveSchedule, getSchedule, updateSchedule, deleteSchedule, listSchedules } = inner;
7374
7764
  if (saveSchedule && getSchedule && updateSchedule && deleteSchedule && listSchedules) {
7375
7765
  this.saveSchedule = (r) => saveSchedule.call(inner, this.encSchedule(r));
@@ -7816,6 +8206,28 @@ var InMemoryStateStore = class {
7816
8206
  byRun.set(runId, row);
7817
8207
  }
7818
8208
  }
8209
+ async tryAdmitSingleton(runId, tag, workflow, limit, retryWakeAt) {
8210
+ const runs = [
8211
+ ...this.runs.values()
8212
+ ];
8213
+ const tags = singletonAdmissionTags(runs, runId, tag, workflow, limit);
8214
+ if (!tags) {
8215
+ const patch = singletonAdmissionDenialPatch(runs, runId, tag, workflow, retryWakeAt);
8216
+ const candidate = this.runs.get(runId);
8217
+ if (patch && candidate) this.runs.set(runId, {
8218
+ ...candidate,
8219
+ ...patch
8220
+ });
8221
+ return false;
8222
+ }
8223
+ const run = this.runs.get(runId);
8224
+ if (!run) return false;
8225
+ this.runs.set(runId, {
8226
+ ...run,
8227
+ tags
8228
+ });
8229
+ return true;
8230
+ }
7819
8231
  async createRun(run) {
7820
8232
  this.runs.set(run.id, {
7821
8233
  ...run,
@@ -7823,7 +8235,11 @@ var InMemoryStateStore = class {
7823
8235
  });
7824
8236
  this.reindexAttributes(run.id, run.searchAttributes);
7825
8237
  }
7826
- async updateRun(runId, patch) {
8238
+ async updateRun(runId, inputPatch) {
8239
+ const patch = inputPatch.status && TERMINAL_RUN_STATUSES.includes(inputPatch.status) ? {
8240
+ ...inputPatch,
8241
+ tags: ("tags" in inputPatch ? inputPatch.tags : this.runs.get(runId)?.tags)?.filter((tag) => tag !== SINGLETON_ADMITTED_TAG)
8242
+ } : inputPatch;
7827
8243
  const existing = this.runs.get(runId);
7828
8244
  if (!existing) throw new Error(`run ${runId} not found`);
7829
8245
  const next = {
@@ -7954,6 +8370,33 @@ var InMemoryStateStore = class {
7954
8370
  ...w
7955
8371
  }));
7956
8372
  }
8373
+ async registerChildWaits(entries) {
8374
+ for (const { waiter } of entries) this.signalWaiters.set(waiter.token, {
8375
+ ...waiter
8376
+ });
8377
+ for (const { checkpoint } of entries) await this.saveCheckpoint(checkpoint);
8378
+ }
8379
+ async compactCheckpoints(runId, fromSeq, toSeq, into) {
8380
+ for (let seq = fromSeq; seq <= toSeq; seq += 1) this.checkpoints.delete(this.key(runId, seq));
8381
+ this.checkpoints.set(this.key(runId, into.seq), {
8382
+ ...into,
8383
+ runId
8384
+ });
8385
+ }
8386
+ async listSignalWaitersByRun(runIds) {
8387
+ const ids = new Set(runIds);
8388
+ return [
8389
+ ...this.signalWaiters.values()
8390
+ ].filter((w) => ids.has(w.runId)).map((w) => ({
8391
+ ...w
8392
+ }));
8393
+ }
8394
+ async hasSignalWaiter(runId, tokenPrefix) {
8395
+ for (const w of this.signalWaiters.values()) {
8396
+ if (w.runId === runId && w.token.startsWith(tokenPrefix)) return true;
8397
+ }
8398
+ return false;
8399
+ }
7957
8400
  async removeSignalWaiter(waiter) {
7958
8401
  const current = this.signalWaiters.get(waiter.token);
7959
8402
  if (current && current.runId === waiter.runId && current.seq === waiter.seq) {
@@ -8248,6 +8691,8 @@ var InMemoryTransport = class {
8248
8691
  RemoteWorkflowExecutor,
8249
8692
  RemoteWorkflowTimeout,
8250
8693
  RunGateway,
8694
+ SINGLETON_ACTIVE_STATUSES,
8695
+ SINGLETON_ADMITTED_TAG,
8251
8696
  STATE_STORE,
8252
8697
  STATE_STORE_CANONICAL,
8253
8698
  ScheduleAlreadyExistsError,
@@ -8278,6 +8723,7 @@ var InMemoryTransport = class {
8278
8723
  capableWorkers,
8279
8724
  classifyWaiterToken,
8280
8725
  clearDurableExecutionWrappers,
8726
+ clearTerminalSingletonAdmission,
8281
8727
  collectMetrics,
8282
8728
  concurrencyTag,
8283
8729
  controlPlaneDescriptor,
@@ -8306,6 +8752,7 @@ var InMemoryTransport = class {
8306
8752
  isLegacyDescriptor,
8307
8753
  isNonEmptyRunScope,
8308
8754
  isWorkflowControlFlowSignal,
8755
+ listSignalWaitersOfRuns,
8309
8756
  log,
8310
8757
  matchesAttributes,
8311
8758
  mergeRunFacetRows,
@@ -8316,6 +8763,7 @@ var InMemoryTransport = class {
8316
8763
  normalizeAttributeRows,
8317
8764
  normalizeDescriptor,
8318
8765
  observedAnnouncements,
8766
+ parentRunIdOf,
8319
8767
  parseDuration,
8320
8768
  partitionOfToken,
8321
8769
  planDispatch,
@@ -8335,6 +8783,8 @@ var InMemoryTransport = class {
8335
8783
  runValueFacetsFromRuns,
8336
8784
  sanitizeQueueToken,
8337
8785
  scheduledRunId,
8786
+ singletonAdmissionDenialPatch,
8787
+ singletonAdmissionTags,
8338
8788
  sqlComparator,
8339
8789
  stepConfigOf,
8340
8790
  stepId,