@dudousxd/nestjs-durable-core 0.73.0 → 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
@@ -117,6 +117,7 @@ __export(index_exports, {
117
117
  isLegacyDescriptor: () => isLegacyDescriptor,
118
118
  isNonEmptyRunScope: () => isNonEmptyRunScope,
119
119
  isWorkflowControlFlowSignal: () => isWorkflowControlFlowSignal,
120
+ listSignalWaitersOfRuns: () => listSignalWaitersOfRuns,
120
121
  log: () => log,
121
122
  matchesAttributes: () => matchesAttributes,
122
123
  mergeRunFacetRows: () => mergeRunFacetRows,
@@ -127,6 +128,7 @@ __export(index_exports, {
127
128
  normalizeAttributeRows: () => normalizeAttributeRows,
128
129
  normalizeDescriptor: () => normalizeDescriptor,
129
130
  observedAnnouncements: () => observedAnnouncements,
131
+ parentRunIdOf: () => parentRunIdOf,
130
132
  parseDuration: () => parseDuration,
131
133
  partitionOfToken: () => partitionOfToken,
132
134
  planDispatch: () => planDispatch,
@@ -550,12 +552,15 @@ var WorkflowSuspended = class extends Error {
550
552
  }
551
553
  /** Epoch ms to auto-resume (durable sleep), or undefined when waiting on an external signal. */
552
554
  wakeAt;
555
+ /** Set when awaited children are what the run is waiting on — see {@link FanJoin}. */
556
+ fan;
553
557
  /** Marks this as a control-flow signal — see {@link isWorkflowControlFlowSignal}. */
554
558
  [CONTROL_FLOW_SIGNAL] = true;
555
- constructor(wakeAt) {
559
+ constructor(wakeAt, fan) {
556
560
  super("workflow suspended");
557
561
  this.name = "WorkflowSuspended";
558
562
  this.wakeAt = wakeAt;
563
+ this.fan = fan;
559
564
  }
560
565
  };
561
566
  var ContinueAsNew = class extends Error {
@@ -982,6 +987,21 @@ function instantCheckpoint(p) {
982
987
  }
983
988
  __name(instantCheckpoint, "instantCheckpoint");
984
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
+
985
1005
  // src/concurrency.ts
986
1006
  var DEFAULT_CONCURRENCY_STATUSES = [
987
1007
  "pending",
@@ -1221,6 +1241,81 @@ function eventMatches(payload, match) {
1221
1241
  }
1222
1242
  __name(eventMatches, "eventMatches");
1223
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
+
1224
1319
  // src/handshake/descriptor.ts
1225
1320
  var CURRENT_PROTOCOL_VERSION = 1;
1226
1321
  var LEGACY_V1_PROTOCOL = {
@@ -1962,6 +2057,23 @@ function resolveRunWaiting(run, waiterByRun) {
1962
2057
  return waiter ? classifyWaiterToken(waiter.token) : void 0;
1963
2058
  }
1964
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");
1965
2077
  function indexWaitersByRun(waiters) {
1966
2078
  const byRun = /* @__PURE__ */ new Map();
1967
2079
  for (const w of waiters) if (!byRun.has(w.runId)) byRun.set(w.runId, w);
@@ -2772,16 +2884,6 @@ var TransportPool = class {
2772
2884
  }
2773
2885
  };
2774
2886
 
2775
- // src/completion.ts
2776
- function unwrapCompletion(payload, label) {
2777
- const c = payload;
2778
- if (c && typeof c === "object" && "ok" in c && c.ok === false) {
2779
- throw new FatalError(`${label} failed: ${c.error}`);
2780
- }
2781
- return c?.value;
2782
- }
2783
- __name(unwrapCompletion, "unwrapCompletion");
2784
-
2785
2887
  // src/step-name-symbol.ts
2786
2888
  var DURABLE_STEP_NAME = Symbol.for("durable.step.name");
2787
2889
  var DURABLE_STEP_CONFIG = Symbol.for("durable.step.config");
@@ -2803,6 +2905,8 @@ function stepConfigOf(ref) {
2803
2905
  __name(stepConfigOf, "stepConfigOf");
2804
2906
 
2805
2907
  // src/workflow-ctx.ts
2908
+ var FAN_CHUNK = 500;
2909
+ var FOLD_MIN_FAN = 100;
2806
2910
  function normalizeChildOptions(options) {
2807
2911
  return typeof options === "string" ? {
2808
2912
  childId: options
@@ -3145,7 +3249,7 @@ function createWorkflowCtx(host, runId, compensations, workflow = "", searchAttr
3145
3249
  if (existing && existing.status === "completed") {
3146
3250
  return unwrapCompletion(existing.output, `child "${id}"`);
3147
3251
  }
3148
- await store.putSignalWaiter({
3252
+ if (!existing) await store.putSignalWaiter({
3149
3253
  token: `child:${id}`,
3150
3254
  runId,
3151
3255
  seq: current
@@ -3176,16 +3280,33 @@ function createWorkflowCtx(host, runId, compensations, workflow = "", searchAttr
3176
3280
  status: "running"
3177
3281
  }));
3178
3282
  }
3179
- throw new WorkflowSuspended();
3283
+ throw new WorkflowSuspended(void 0, fanJoin([
3284
+ {
3285
+ waiterPrefix: `child:${id}`,
3286
+ checkpointPrefix: `signal:child:${id}`
3287
+ }
3288
+ ]));
3180
3289
  }, "child");
3181
3290
  const all = /* @__PURE__ */ __name(async (workflow2, inputs, opts) => {
3182
3291
  if (inputs.length === 0) return [];
3183
3292
  const mode = opts?.mode ?? "waitAll";
3184
3293
  const name = workflowName(workflow2);
3294
+ const waves = allWaves(inputs.length, opts?.concurrency);
3185
3295
  const positions = inputs.map(() => pos.next());
3186
- const group = `all:${positions[0]}`;
3187
- const id = /* @__PURE__ */ __name((i) => `${runId}.all.${positions[0]}.${i}`, "id");
3188
- 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");
3189
3310
  if (mode === "failFast") {
3190
3311
  for (let i = 0; i < inputs.length; i += 1) {
3191
3312
  const cp = existing[i];
@@ -3204,32 +3325,91 @@ function createWorkflowCtx(host, runId, compensations, workflow = "", searchAttr
3204
3325
  }
3205
3326
  }
3206
3327
  }
3207
- let pending = false;
3208
- for (let i = 0; i < inputs.length; i += 1) {
3209
- const cp = existing[i];
3210
- if (cp?.status === "completed") continue;
3211
- pending = true;
3212
- const seq = positions[i];
3213
- const childId = id(i);
3214
- await store.putSignalWaiter({
3215
- token: `child:${childId}`,
3216
- runId,
3217
- seq,
3218
- parallelGroup: group
3219
- });
3220
- if (!await store.getRun(childId)) host.startChild(name, inputs[i], childId);
3221
- if (!cp) {
3222
- 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({
3223
3342
  runId,
3224
- seq,
3225
- name: `signal:child:${childId}`,
3343
+ seq: positions[first],
3344
+ name: allFoldedName(runId, firstSeq, waves ? k : void 0),
3226
3345
  kind: "signal",
3227
- status: "running",
3346
+ output: {
3347
+ from: first,
3348
+ values
3349
+ },
3228
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));
3229
3407
  }));
3230
3408
  }
3231
3409
  }
3232
- 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
+ }
3233
3413
  const outputs = [];
3234
3414
  const failures = [];
3235
3415
  for (let i = 0; i < inputs.length; i += 1) {
@@ -3248,6 +3428,67 @@ function createWorkflowCtx(host, runId, compensations, workflow = "", searchAttr
3248
3428
  if (failures.length > 0) throw new GatherError(failures, inputs.length);
3249
3429
  return outputs;
3250
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");
3251
3492
  const startChild = /* @__PURE__ */ __name(async (workflow2, input, options) => {
3252
3493
  const { childId, priority, version } = normalizeChildOptions(options);
3253
3494
  const current = pos.next();
@@ -4287,10 +4528,8 @@ var WorkflowEngine = class {
4287
4528
  */
4288
4529
  async findParentRunId(childRunId) {
4289
4530
  const token = `child:${childRunId}`;
4290
- for (const waiter of await this.store.listSignalWaiters("child:")) {
4291
- if (waiter.token === token) return waiter.runId;
4292
- }
4293
- return void 0;
4531
+ const waiters = await this.store.listSignalWaiters(token);
4532
+ return waiters.find((waiter) => waiter.token === token)?.runId;
4294
4533
  }
4295
4534
  /** Track an in-flight execution so {@link drain} can wait for it. */
4296
4535
  track(p) {
@@ -4511,7 +4750,10 @@ var WorkflowEngine = class {
4511
4750
  const due = await this.store.listDueTimers(nowMs, this.namespace);
4512
4751
  const blocked = await this.dueBlockedRuns(nowMs);
4513
4752
  if (due.length === 0 && blocked.length === 0) return [];
4514
- const waiterByRun = indexWaitersByRun(await this.store.listSignalWaiters(""));
4753
+ const waiterByRun = indexWaitersByRun(await listSignalWaitersOfRuns(this.store, [
4754
+ ...due,
4755
+ ...blocked
4756
+ ].map((run) => run.id)));
4515
4757
  return this.resumeLeased([
4516
4758
  ...due,
4517
4759
  ...blocked
@@ -4621,11 +4863,6 @@ var WorkflowEngine = class {
4621
4863
  } : {}
4622
4864
  });
4623
4865
  } else if (cp.kind === "signal" && cp.name.startsWith("signal:child:") && cp.status === "completed" && isFailureCompletion(cp.output)) {
4624
- await this.store.saveCheckpoint({
4625
- ...cp,
4626
- status: "running",
4627
- output: void 0
4628
- });
4629
4866
  const childId = cp.name.slice("signal:child:".length);
4630
4867
  const parked = [];
4631
4868
  for (; ; ) {
@@ -4633,8 +4870,28 @@ var WorkflowEngine = class {
4633
4870
  if (!buffered) break;
4634
4871
  parked.push(buffered.payload);
4635
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];
4636
4875
  for (const payload of parked) await this.store.bufferSignal(`child:${childId}`, payload);
4637
- 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
+ }
4638
4895
  if (!bufferedSuccess) {
4639
4896
  const child = await this.store.getRun(childId);
4640
4897
  if (child && (child.status === "failed" || child.status === "dead")) {
@@ -4914,12 +5171,62 @@ var WorkflowEngine = class {
4914
5171
  // for an ordinary (non-fan) signal/child await.
4915
5172
  parallelGroup: waiter.parallelGroup
4916
5173
  }));
5174
+ if (await this.fanStillWaiting(waiter, payload)) {
5175
+ return {
5176
+ runId: waiter.runId,
5177
+ status: "suspended"
5178
+ };
5179
+ }
4917
5180
  return this.resumeImplicit(waiter.runId).catch((err) => {
4918
5181
  if (err instanceof NamespaceMismatch) return null;
4919
5182
  throw err;
4920
5183
  });
4921
5184
  }
4922
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
+ /**
4923
5230
  * Deliver an external signal to the run waiting on `token`, resume it with `payload`, and return
4924
5231
  * the run result — or null if no run is (or ends up) waiting for it.
4925
5232
  *
@@ -5212,23 +5519,11 @@ var WorkflowEngine = class {
5212
5519
  }
5213
5520
  /** The given runs plus every descendant, children before parents. One waiter scan for the batch. */
5214
5521
  async collectSubtrees(rootIds) {
5215
- const waitersByParent = /* @__PURE__ */ new Map();
5216
- for (const w of await this.store.listSignalWaiters("child:")) {
5217
- const list = waitersByParent.get(w.runId) ?? [];
5218
- list.push(w.token.slice("child:".length));
5219
- waitersByParent.set(w.runId, list);
5220
- }
5221
- const prefixes = [
5222
- "signal:child:",
5223
- "spawn:"
5224
- ];
5522
+ const prefixes = CHILD_EDGE_PREFIXES;
5225
5523
  const childrenOf = /* @__PURE__ */ __name(async (parent) => {
5226
- const ids = new Set(waitersByParent.get(parent) ?? []);
5524
+ const ids = new Set(await this.awaitedChildIds(parent));
5227
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)));
5228
- for (const cp of cps) {
5229
- if (cp.name.startsWith("signal:child:")) ids.add(cp.name.slice("signal:child:".length));
5230
- if (cp.name.startsWith("spawn:") && typeof cp.output === "string") ids.add(cp.output);
5231
- }
5526
+ for (const id of childIdsOf(cps)) ids.add(id);
5232
5527
  return [
5233
5528
  ...ids
5234
5529
  ];
@@ -5278,22 +5573,20 @@ var WorkflowEngine = class {
5278
5573
  * so a completed parent (or a completed awaited child) would otherwise drop out of the tree. The
5279
5574
  * `signal:child:<id>` checkpoint (the placeholder written when the parent first awaits the child,
5280
5575
  * overwritten as completed/failed when it settles) persists across completion, so we read both and
5281
- * 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.
5282
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
+ }
5283
5585
  async getRunChildren(parentRunId) {
5284
- const childIds = /* @__PURE__ */ new Set();
5285
- for (const w of await this.store.listSignalWaiters("child:")) {
5286
- if (w.runId === parentRunId) childIds.add(w.token.slice("child:".length));
5287
- }
5288
- const prefixes = [
5289
- "signal:child:",
5290
- "spawn:"
5291
- ];
5586
+ const childIds = new Set(await this.awaitedChildIds(parentRunId));
5587
+ const prefixes = CHILD_EDGE_PREFIXES;
5292
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)));
5293
- for (const cp of childCheckpoints) {
5294
- if (cp.name.startsWith("signal:child:")) childIds.add(cp.name.slice("signal:child:".length));
5295
- if (cp.name.startsWith("spawn:") && typeof cp.output === "string") childIds.add(cp.output);
5296
- }
5589
+ for (const id of childIdsOf(childCheckpoints)) childIds.add(id);
5297
5590
  return [
5298
5591
  ...childIds
5299
5592
  ];
@@ -6373,6 +6666,7 @@ var WorkflowEngine = class {
6373
6666
  const snapshot = await this.store.listCheckpoints(run.id);
6374
6667
  const replay = /* @__PURE__ */ new Map();
6375
6668
  for (const cp of snapshot) replay.set(cp.seq, cp);
6669
+ let suspendedOnFan;
6376
6670
  const ctx = createWorkflowCtx(this.ctxHostFor(replay, run.namespace), run.id, compensations, run.workflow, registered?.searchAttributesSchema);
6377
6671
  try {
6378
6672
  const output = await runInWorkflowCtx(ctx, () => fn(ctx, run.input));
@@ -6424,10 +6718,12 @@ var WorkflowEngine = class {
6424
6718
  if (this.cancelRequested.has(run.id)) {
6425
6719
  return await this.compensateAndCancel(run, compensations, replay);
6426
6720
  }
6427
- return await this.settleRun(run, {
6721
+ const suspended = await this.settleRun(run, {
6428
6722
  kind: "suspended",
6429
6723
  wakeAt: err.wakeAt
6430
6724
  });
6725
+ suspendedOnFan = err.fan;
6726
+ return suspended;
6431
6727
  }
6432
6728
  if (this.cancelRequested.has(run.id)) {
6433
6729
  return await this.compensateAndCancel(run, compensations, replay);
@@ -6447,6 +6743,9 @@ var WorkflowEngine = class {
6447
6743
  });
6448
6744
  } finally {
6449
6745
  await this.store.releaseRunLock(run.id);
6746
+ if (suspendedOnFan) {
6747
+ void this.redriveSettledChildren(run.id, suspendedOnFan).catch(() => void 0);
6748
+ }
6450
6749
  }
6451
6750
  }
6452
6751
  /**
@@ -7261,6 +7560,21 @@ var RemoteStepError = class extends Error {
7261
7560
  this.stepError = stepError;
7262
7561
  }
7263
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");
7264
7578
 
7265
7579
  // src/run-facets.ts
7266
7580
  function facetOrigin(origin) {
@@ -7410,6 +7724,14 @@ var CodecStateStore = class {
7410
7724
  /** Forwarded verbatim — a count reads no payload. Bound only when the inner store counts, so the
7411
7725
  * engine's facet/listing fallback still applies through the wrapper. */
7412
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;
7413
7735
  /** Schedules persist their run `input` inside `spec` — encoded like any other payload. Bound only
7414
7736
  * when the inner store persists schedules, so `engine.schedules` reports absence faithfully. */
7415
7737
  saveSchedule;
@@ -7427,6 +7749,17 @@ var CodecStateStore = class {
7427
7749
  if (prune) this.pruneTerminalRuns = (policy, now, limit) => prune.call(inner, policy, now, limit);
7428
7750
  const count = inner.countRuns;
7429
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);
7430
7763
  const { saveSchedule, getSchedule, updateSchedule, deleteSchedule, listSchedules } = inner;
7431
7764
  if (saveSchedule && getSchedule && updateSchedule && deleteSchedule && listSchedules) {
7432
7765
  this.saveSchedule = (r) => saveSchedule.call(inner, this.encSchedule(r));
@@ -8037,6 +8370,33 @@ var InMemoryStateStore = class {
8037
8370
  ...w
8038
8371
  }));
8039
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
+ }
8040
8400
  async removeSignalWaiter(waiter) {
8041
8401
  const current = this.signalWaiters.get(waiter.token);
8042
8402
  if (current && current.runId === waiter.runId && current.seq === waiter.seq) {
@@ -8392,6 +8752,7 @@ var InMemoryTransport = class {
8392
8752
  isLegacyDescriptor,
8393
8753
  isNonEmptyRunScope,
8394
8754
  isWorkflowControlFlowSignal,
8755
+ listSignalWaitersOfRuns,
8395
8756
  log,
8396
8757
  matchesAttributes,
8397
8758
  mergeRunFacetRows,
@@ -8402,6 +8763,7 @@ var InMemoryTransport = class {
8402
8763
  normalizeAttributeRows,
8403
8764
  normalizeDescriptor,
8404
8765
  observedAnnouncements,
8766
+ parentRunIdOf,
8405
8767
  parseDuration,
8406
8768
  partitionOfToken,
8407
8769
  planDispatch,