@dudousxd/nestjs-durable-core 0.85.5 → 0.85.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -646,6 +646,386 @@ var BoundedRunner = class {
646
646
  };
647
647
  var LOCAL_RUN_CONCURRENCY = 256;
648
648
 
649
+ // src/fan-join.ts
650
+ function allChildId(runId, firstSeq, index, wave) {
651
+ return `${allBase(runId, firstSeq, wave)}${index}`;
652
+ }
653
+ __name(allChildId, "allChildId");
654
+ function allGroup(firstSeq, size, mode) {
655
+ const group = `all:${firstSeq}:n${size}`;
656
+ return mode === "failFast" ? `${group}:failFast` : group;
657
+ }
658
+ __name(allGroup, "allGroup");
659
+ function stepFanGroup(firstSeq, size, mode) {
660
+ const group = `steps:${firstSeq}:${size}`;
661
+ return mode === "failFast" ? `${group}:failFast` : group;
662
+ }
663
+ __name(stepFanGroup, "stepFanGroup");
664
+ function parseStepFanGroup(group) {
665
+ const fan = parseFanGroup(group);
666
+ if (fan?.kind !== "steps" || fan.size === void 0) return void 0;
667
+ return {
668
+ firstSeq: fan.firstSeq,
669
+ size: fan.size
670
+ };
671
+ }
672
+ __name(parseStepFanGroup, "parseStepFanGroup");
673
+ function parseFanGroup(group) {
674
+ const m = group ? /^(all|steps):(\d+)(?::n?(\d+))?(:failFast)?$/.exec(group) : null;
675
+ if (!m) return void 0;
676
+ if (m[1] === "steps" && m[3] === void 0) return void 0;
677
+ return {
678
+ kind: m[1],
679
+ firstSeq: Number(m[2]),
680
+ size: m[3] === void 0 ? void 0 : Number(m[3]),
681
+ failFast: m[4] !== void 0
682
+ };
683
+ }
684
+ __name(parseFanGroup, "parseFanGroup");
685
+ function allWait(runId, firstSeq, wave) {
686
+ const base = allBase(runId, firstSeq, wave);
687
+ return {
688
+ waiterPrefix: `child:${base}`,
689
+ checkpointPrefix: `signal:child:${base}`
690
+ };
691
+ }
692
+ __name(allWait, "allWait");
693
+ function fanJoin(waits) {
694
+ return {
695
+ waits
696
+ };
697
+ }
698
+ __name(fanJoin, "fanJoin");
699
+ var FOLDED_PREFIX = "fan:";
700
+ function allFoldedName(runId, firstSeq, wave) {
701
+ return `${FOLDED_PREFIX}${allBase(runId, firstSeq, wave)}`;
702
+ }
703
+ __name(allFoldedName, "allFoldedName");
704
+ function foldedChildIds(checkpoint) {
705
+ const base = checkpoint.name.slice(FOLDED_PREFIX.length);
706
+ const { from, values } = checkpoint.output;
707
+ return values.map((_, j) => `${base}${from + j}`);
708
+ }
709
+ __name(foldedChildIds, "foldedChildIds");
710
+ function allBase(runId, firstSeq, wave) {
711
+ return wave === void 0 ? `${runId}.all.${firstSeq}.` : `${runId}.all.${firstSeq}.w${wave}.`;
712
+ }
713
+ __name(allBase, "allBase");
714
+ function allWaves(size, concurrency) {
715
+ if (concurrency === void 0 || concurrency >= size) return void 0;
716
+ if (!Number.isInteger(concurrency) || concurrency < 1) {
717
+ throw new Error(`ctx.all: concurrency must be a positive integer, got ${concurrency}`);
718
+ }
719
+ if (concurrency === 1) return {
720
+ waveSize: 1,
721
+ maxWaves: 1
722
+ };
723
+ return {
724
+ waveSize: Math.floor(concurrency / 2),
725
+ maxWaves: 2
726
+ };
727
+ }
728
+ __name(allWaves, "allWaves");
729
+ var ENGINE_CHILD_ID = /^(.+)\.(?:all\.\d+\.(?:w\d+\.)?\d+|child\.\d+|onError\.\d+)$/;
730
+ function parentRunIdOf(runId) {
731
+ return ENGINE_CHILD_ID.exec(runId)?.[1];
732
+ }
733
+ __name(parentRunIdOf, "parentRunIdOf");
734
+ var GROUP = /^all:(\d+)(?::n\d+)?(:failFast)?$/;
735
+ var REST = /^(?:w(\d+)\.)?\d+$/;
736
+ function fanOfWaiter(waiter) {
737
+ const group = waiter.parallelGroup ? GROUP.exec(waiter.parallelGroup) : null;
738
+ if (!group) return void 0;
739
+ const base = `child:${waiter.runId}.all.${group[1]}.`;
740
+ if (!waiter.token.startsWith(base)) return void 0;
741
+ const rest = REST.exec(waiter.token.slice(base.length));
742
+ if (!rest) return void 0;
743
+ const waiterPrefix = rest[1] === void 0 ? base : `${base}w${rest[1]}.`;
744
+ return {
745
+ waiterPrefix,
746
+ failFast: group[2] !== void 0
747
+ };
748
+ }
749
+ __name(fanOfWaiter, "fanOfWaiter");
750
+
751
+ // src/interfaces.ts
752
+ function checkpointNameKind(name) {
753
+ if (name.startsWith("signal:child:")) return "child";
754
+ if (name.startsWith("fan:")) return "folded";
755
+ if (name.startsWith("spawn:")) return "spawn";
756
+ return "step";
757
+ }
758
+ __name(checkpointNameKind, "checkpointNameKind");
759
+ function poolNamespace(namespace) {
760
+ return namespace === "" ? "default" : namespace;
761
+ }
762
+ __name(poolNamespace, "poolNamespace");
763
+ var TERMINAL_RUN_STATUSES = [
764
+ "completed",
765
+ "failed",
766
+ "cancelled",
767
+ "dead"
768
+ ];
769
+ function isNonEmptyRunScope(scope) {
770
+ if (!scope) return false;
771
+ return scope.workflow !== void 0 || scope.workflows !== void 0 || scope.tag !== void 0 || scope.tags !== void 0 || scope.namespace !== void 0 || scope.namespaces !== void 0 || scope.attributes !== void 0 && scope.attributes.length > 0;
772
+ }
773
+ __name(isNonEmptyRunScope, "isNonEmptyRunScope");
774
+
775
+ // src/events.ts
776
+ var enc = /* @__PURE__ */ __name((s) => Buffer.from(s, "utf8").toString("base64"), "enc");
777
+ var dec = /* @__PURE__ */ __name((s) => Buffer.from(s, "base64").toString("utf8"), "dec");
778
+ var EVENT_BUFFER_SCAN_LIMIT = 50;
779
+ var EVENT_TOKEN_PREFIX = "event:";
780
+ function eventPrefix(name) {
781
+ return `${EVENT_TOKEN_PREFIX}${enc(name)}:`;
782
+ }
783
+ __name(eventPrefix, "eventPrefix");
784
+ function isEventToken(token) {
785
+ return token.startsWith(EVENT_TOKEN_PREFIX);
786
+ }
787
+ __name(isEventToken, "isEventToken");
788
+ function eventToken(name, match, runId, seq) {
789
+ return `${eventPrefix(name)}${enc(JSON.stringify(match ?? {}))}:${runId}#${seq}`;
790
+ }
791
+ __name(eventToken, "eventToken");
792
+ function eventNameOf(token) {
793
+ try {
794
+ return dec(token.split(":")[1] ?? "");
795
+ } catch {
796
+ return token;
797
+ }
798
+ }
799
+ __name(eventNameOf, "eventNameOf");
800
+ function eventMatchOf(token) {
801
+ try {
802
+ return JSON.parse(dec(token.split(":")[2] ?? ""));
803
+ } catch {
804
+ return {};
805
+ }
806
+ }
807
+ __name(eventMatchOf, "eventMatchOf");
808
+ function eventMatches(payload, match) {
809
+ const keys = Object.keys(match);
810
+ if (keys.length === 0) return true;
811
+ if (!payload || typeof payload !== "object") return false;
812
+ const p = payload;
813
+ return keys.every((k) => JSON.stringify(p[k]) === JSON.stringify(match[k]));
814
+ }
815
+ __name(eventMatches, "eventMatches");
816
+
817
+ // src/run-waiting.ts
818
+ var WEBHOOK_PREFIX = "wh:";
819
+ var CHILD_PREFIX = "child:";
820
+ var BREAKPOINT_PREFIX = "bp:";
821
+ var EVENT_PREFIX = EVENT_TOKEN_PREFIX;
822
+ function parseWebhookToken(token) {
823
+ if (!token.startsWith(WEBHOOK_PREFIX)) return null;
824
+ const rest = token.slice(WEBHOOK_PREFIX.length);
825
+ const at = rest.lastIndexOf(":");
826
+ if (at <= 0) return null;
827
+ const seqText = rest.slice(at + 1);
828
+ if (!/^\d+$/.test(seqText)) return null;
829
+ return {
830
+ runId: rest.slice(0, at),
831
+ seq: Number(seqText)
832
+ };
833
+ }
834
+ __name(parseWebhookToken, "parseWebhookToken");
835
+ function classifyWaiterToken(token) {
836
+ if (token.startsWith(WEBHOOK_PREFIX)) return {
837
+ on: "webhook",
838
+ name: token
839
+ };
840
+ if (token.startsWith(BREAKPOINT_PREFIX)) return {
841
+ on: "breakpoint",
842
+ name: "breakpoint"
843
+ };
844
+ if (token.startsWith(CHILD_PREFIX)) return {
845
+ on: "child",
846
+ name: token.slice(CHILD_PREFIX.length)
847
+ };
848
+ if (token.startsWith(EVENT_PREFIX)) return {
849
+ on: "signal",
850
+ name: eventNameOf(token)
851
+ };
852
+ return {
853
+ on: "signal",
854
+ name: token
855
+ };
856
+ }
857
+ __name(classifyWaiterToken, "classifyWaiterToken");
858
+ function resolveRunWaiting(run, waiterByRun) {
859
+ if (run.status !== "suspended") return void 0;
860
+ const waiter = waiterByRun.get(run.id);
861
+ return waiter ? classifyWaiterToken(waiter.token) : void 0;
862
+ }
863
+ __name(resolveRunWaiting, "resolveRunWaiting");
864
+ var RUN_ID_BATCH = 500;
865
+ async function listSignalWaitersOfRuns(store, runIds) {
866
+ const unique = [
867
+ ...new Set(runIds)
868
+ ];
869
+ if (unique.length === 0) return [];
870
+ if (!store.listSignalWaitersByRun) {
871
+ const ids = new Set(unique);
872
+ return (await store.listSignalWaiters("")).filter((w) => ids.has(w.runId));
873
+ }
874
+ const out = [];
875
+ for (let at = 0; at < unique.length; at += RUN_ID_BATCH) {
876
+ out.push(...await store.listSignalWaitersByRun(unique.slice(at, at + RUN_ID_BATCH)));
877
+ }
878
+ return out;
879
+ }
880
+ __name(listSignalWaitersOfRuns, "listSignalWaitersOfRuns");
881
+ function indexWaitersByRun(waiters) {
882
+ const byRun = /* @__PURE__ */ new Map();
883
+ for (const w of waiters) if (!byRun.has(w.runId)) byRun.set(w.runId, w);
884
+ return byRun;
885
+ }
886
+ __name(indexWaitersByRun, "indexWaitersByRun");
887
+ var GET_RUN_FALLBACK_PARALLEL = 50;
888
+ async function getRunsByIds(store, runIds) {
889
+ const { runs, unread, error: error2 } = await readRunsByIds(store, runIds);
890
+ if (unread.length > 0) throw error2;
891
+ return runs;
892
+ }
893
+ __name(getRunsByIds, "getRunsByIds");
894
+ async function readRunsByIds(store, runIds) {
895
+ const unique = [
896
+ ...new Set(runIds)
897
+ ];
898
+ const runs = /* @__PURE__ */ new Map();
899
+ const unread = [];
900
+ let error2;
901
+ const batch = store.getRuns ? RUN_ID_BATCH : GET_RUN_FALLBACK_PARALLEL;
902
+ for (let at = 0; at < unique.length; at += batch) {
903
+ const ids = unique.slice(at, at + batch);
904
+ if (store.getRuns) {
905
+ try {
906
+ for (const run of await store.getRuns(ids)) runs.set(run.id, run);
907
+ } catch (err) {
908
+ error2 ??= err;
909
+ unread.push(...ids);
910
+ }
911
+ continue;
912
+ }
913
+ const reads = await Promise.allSettled(ids.map((id) => store.getRun(id)));
914
+ reads.forEach((read, k) => {
915
+ if (read.status === "fulfilled") {
916
+ if (read.value) runs.set(read.value.id, read.value);
917
+ return;
918
+ }
919
+ error2 ??= read.reason;
920
+ unread.push(ids[k]);
921
+ });
922
+ }
923
+ return {
924
+ runs,
925
+ unread,
926
+ ...unread.length > 0 ? {
927
+ error: error2
928
+ } : {}
929
+ };
930
+ }
931
+ __name(readRunsByIds, "readRunsByIds");
932
+
933
+ // src/buffered-child-signals.ts
934
+ var CHILD_TOKEN_PREFIX = "child:";
935
+ var CHILD_SIGNAL_NAME_PREFIX = "signal:child:";
936
+ var RETRYABLE = /* @__PURE__ */ new Set([
937
+ "failed",
938
+ "dead"
939
+ ]);
940
+ var FAN_CHILD = /^(.+\.all\.\d+\.(?:w\d+\.)?)(\d+)$/;
941
+ function canonicalJson(value) {
942
+ const plain = JSON.stringify(value);
943
+ return plain === void 0 ? "null" : sortedJson(JSON.parse(plain));
944
+ }
945
+ __name(canonicalJson, "canonicalJson");
946
+ function sortedJson(value) {
947
+ if (Array.isArray(value)) return `[${value.map(sortedJson).join(",")}]`;
948
+ if (value !== null && typeof value === "object") {
949
+ const entries = Object.entries(value).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
950
+ return `{${entries.map(([k, v]) => `${JSON.stringify(k)}:${sortedJson(v)}`).join(",")}}`;
951
+ }
952
+ return JSON.stringify(value);
953
+ }
954
+ __name(sortedJson, "sortedJson");
955
+ function settledNames(childId) {
956
+ const signal = `${CHILD_SIGNAL_NAME_PREFIX}${childId}`;
957
+ const fan = FAN_CHILD.exec(childId);
958
+ if (!fan) return {
959
+ signal
960
+ };
961
+ return {
962
+ signal,
963
+ fold: {
964
+ name: `${FOLDED_PREFIX}${fan[1]}`,
965
+ index: Number(fan[2])
966
+ }
967
+ };
968
+ }
969
+ __name(settledNames, "settledNames");
970
+ function parentHoldsChild(checkpoints, childId, completion) {
971
+ const { signal, fold } = settledNames(childId);
972
+ const expected = canonicalJson(completion);
973
+ return checkpoints.some((cp) => {
974
+ if (cp.status !== "completed") return false;
975
+ if (cp.name === signal) return canonicalJson(cp.output) === expected;
976
+ if (fold && cp.name === fold.name) {
977
+ const { from, values } = cp.output ?? {};
978
+ return typeof from === "number" && Array.isArray(values) && fold.index >= from && fold.index < from + values.length;
979
+ }
980
+ return false;
981
+ });
982
+ }
983
+ __name(parentHoldsChild, "parentHoldsChild");
984
+ async function parentCheckpoints(store, parentId, childId) {
985
+ const { signal, fold } = settledNames(childId);
986
+ const prefixes = fold ? [
987
+ signal,
988
+ fold.name
989
+ ] : [
990
+ signal
991
+ ];
992
+ return store.listCheckpointsByNamePrefix ? store.listCheckpointsByNamePrefix(parentId, prefixes) : store.listCheckpoints(parentId);
993
+ }
994
+ __name(parentCheckpoints, "parentCheckpoints");
995
+ async function childCompletionNeedsBuffer(store, childId, completion, parent) {
996
+ const parentId = parentRunIdOf(childId);
997
+ if (!parentId) return false;
998
+ const run = parent === void 0 ? await store.getRun(parentId) : parent;
999
+ if (!run) return false;
1000
+ if (TERMINAL_RUN_STATUSES.includes(run.status) && !RETRYABLE.has(run.status)) return false;
1001
+ return !parentHoldsChild(await parentCheckpoints(store, parentId, childId), childId, completion);
1002
+ }
1003
+ __name(childCompletionNeedsBuffer, "childCompletionNeedsBuffer");
1004
+ async function unneededChildSignals(store, rows) {
1005
+ const parentIds = rows.flatMap((row) => {
1006
+ const parentId = parentRunIdOf(row.token.slice(CHILD_TOKEN_PREFIX.length));
1007
+ return parentId ? [
1008
+ parentId
1009
+ ] : [];
1010
+ });
1011
+ const { runs, unread } = await readRunsByIds(store, parentIds);
1012
+ const unreadable = new Set(unread);
1013
+ const doomed = [];
1014
+ for (const row of rows) {
1015
+ if (!row.token.startsWith(CHILD_TOKEN_PREFIX)) continue;
1016
+ const childId = row.token.slice(CHILD_TOKEN_PREFIX.length);
1017
+ const parentId = parentRunIdOf(childId);
1018
+ if (parentId && unreadable.has(parentId)) continue;
1019
+ const parent = parentId ? runs.get(parentId) ?? null : null;
1020
+ try {
1021
+ if (!await childCompletionNeedsBuffer(store, childId, row.payload, parent)) doomed.push(row);
1022
+ } catch {
1023
+ }
1024
+ }
1025
+ return doomed;
1026
+ }
1027
+ __name(unneededChildSignals, "unneededChildSignals");
1028
+
649
1029
  // src/execution-hooks.ts
650
1030
  var wrappers = [];
651
1031
  function useDurableExecution(wrapper) {
@@ -975,6 +1355,32 @@ function completionError(c) {
975
1355
  };
976
1356
  }
977
1357
  __name(completionError, "completionError");
1358
+ function childCompletion(child) {
1359
+ if (child.status === "completed") return {
1360
+ ok: true,
1361
+ value: child.output
1362
+ };
1363
+ if (child.status === "failed") return failedCompletion(child.error ?? {
1364
+ message: "failed"
1365
+ });
1366
+ if (child.status === "dead") return failedCompletion(child.error ?? {
1367
+ message: "dead"
1368
+ });
1369
+ if (child.status === "cancelled") return cancelledCompletion(child.error);
1370
+ return void 0;
1371
+ }
1372
+ __name(childCompletion, "childCompletion");
1373
+ var CHILD_CANCELLED = "cancelled";
1374
+ function cancelledCompletion(error2) {
1375
+ const base = error2 ?? {
1376
+ message: "cancelled"
1377
+ };
1378
+ return failedCompletion({
1379
+ ...base,
1380
+ code: base.code ?? CHILD_CANCELLED
1381
+ });
1382
+ }
1383
+ __name(cancelledCompletion, "cancelledCompletion");
978
1384
 
979
1385
  // src/concurrency.ts
980
1386
  var DEFAULT_CONCURRENCY_STATUSES = [
@@ -1241,150 +1647,6 @@ var EventAccumulators = class {
1241
1647
  }
1242
1648
  };
1243
1649
 
1244
- // src/events.ts
1245
- var enc = /* @__PURE__ */ __name((s) => Buffer.from(s, "utf8").toString("base64"), "enc");
1246
- var dec = /* @__PURE__ */ __name((s) => Buffer.from(s, "base64").toString("utf8"), "dec");
1247
- var EVENT_BUFFER_SCAN_LIMIT = 50;
1248
- var EVENT_TOKEN_PREFIX = "event:";
1249
- function eventPrefix(name) {
1250
- return `${EVENT_TOKEN_PREFIX}${enc(name)}:`;
1251
- }
1252
- __name(eventPrefix, "eventPrefix");
1253
- function isEventToken(token) {
1254
- return token.startsWith(EVENT_TOKEN_PREFIX);
1255
- }
1256
- __name(isEventToken, "isEventToken");
1257
- function eventToken(name, match, runId, seq) {
1258
- return `${eventPrefix(name)}${enc(JSON.stringify(match ?? {}))}:${runId}#${seq}`;
1259
- }
1260
- __name(eventToken, "eventToken");
1261
- function eventNameOf(token) {
1262
- try {
1263
- return dec(token.split(":")[1] ?? "");
1264
- } catch {
1265
- return token;
1266
- }
1267
- }
1268
- __name(eventNameOf, "eventNameOf");
1269
- function eventMatchOf(token) {
1270
- try {
1271
- return JSON.parse(dec(token.split(":")[2] ?? ""));
1272
- } catch {
1273
- return {};
1274
- }
1275
- }
1276
- __name(eventMatchOf, "eventMatchOf");
1277
- function eventMatches(payload, match) {
1278
- const keys = Object.keys(match);
1279
- if (keys.length === 0) return true;
1280
- if (!payload || typeof payload !== "object") return false;
1281
- const p = payload;
1282
- return keys.every((k) => JSON.stringify(p[k]) === JSON.stringify(match[k]));
1283
- }
1284
- __name(eventMatches, "eventMatches");
1285
-
1286
- // src/fan-join.ts
1287
- function allChildId(runId, firstSeq, index, wave) {
1288
- return `${allBase(runId, firstSeq, wave)}${index}`;
1289
- }
1290
- __name(allChildId, "allChildId");
1291
- function allGroup(firstSeq, size, mode) {
1292
- const group = `all:${firstSeq}:n${size}`;
1293
- return mode === "failFast" ? `${group}:failFast` : group;
1294
- }
1295
- __name(allGroup, "allGroup");
1296
- function stepFanGroup(firstSeq, size, mode) {
1297
- const group = `steps:${firstSeq}:${size}`;
1298
- return mode === "failFast" ? `${group}:failFast` : group;
1299
- }
1300
- __name(stepFanGroup, "stepFanGroup");
1301
- function parseStepFanGroup(group) {
1302
- const fan = parseFanGroup(group);
1303
- if (fan?.kind !== "steps" || fan.size === void 0) return void 0;
1304
- return {
1305
- firstSeq: fan.firstSeq,
1306
- size: fan.size
1307
- };
1308
- }
1309
- __name(parseStepFanGroup, "parseStepFanGroup");
1310
- function parseFanGroup(group) {
1311
- const m = group ? /^(all|steps):(\d+)(?::n?(\d+))?(:failFast)?$/.exec(group) : null;
1312
- if (!m) return void 0;
1313
- if (m[1] === "steps" && m[3] === void 0) return void 0;
1314
- return {
1315
- kind: m[1],
1316
- firstSeq: Number(m[2]),
1317
- size: m[3] === void 0 ? void 0 : Number(m[3]),
1318
- failFast: m[4] !== void 0
1319
- };
1320
- }
1321
- __name(parseFanGroup, "parseFanGroup");
1322
- function allWait(runId, firstSeq, wave) {
1323
- const base = allBase(runId, firstSeq, wave);
1324
- return {
1325
- waiterPrefix: `child:${base}`,
1326
- checkpointPrefix: `signal:child:${base}`
1327
- };
1328
- }
1329
- __name(allWait, "allWait");
1330
- function fanJoin(waits) {
1331
- return {
1332
- waits
1333
- };
1334
- }
1335
- __name(fanJoin, "fanJoin");
1336
- var FOLDED_PREFIX = "fan:";
1337
- function allFoldedName(runId, firstSeq, wave) {
1338
- return `${FOLDED_PREFIX}${allBase(runId, firstSeq, wave)}`;
1339
- }
1340
- __name(allFoldedName, "allFoldedName");
1341
- function foldedChildIds(checkpoint) {
1342
- const base = checkpoint.name.slice(FOLDED_PREFIX.length);
1343
- const { from, values } = checkpoint.output;
1344
- return values.map((_, j) => `${base}${from + j}`);
1345
- }
1346
- __name(foldedChildIds, "foldedChildIds");
1347
- function allBase(runId, firstSeq, wave) {
1348
- return wave === void 0 ? `${runId}.all.${firstSeq}.` : `${runId}.all.${firstSeq}.w${wave}.`;
1349
- }
1350
- __name(allBase, "allBase");
1351
- function allWaves(size, concurrency) {
1352
- if (concurrency === void 0 || concurrency >= size) return void 0;
1353
- if (!Number.isInteger(concurrency) || concurrency < 1) {
1354
- throw new Error(`ctx.all: concurrency must be a positive integer, got ${concurrency}`);
1355
- }
1356
- if (concurrency === 1) return {
1357
- waveSize: 1,
1358
- maxWaves: 1
1359
- };
1360
- return {
1361
- waveSize: Math.floor(concurrency / 2),
1362
- maxWaves: 2
1363
- };
1364
- }
1365
- __name(allWaves, "allWaves");
1366
- var ENGINE_CHILD_ID = /^(.+)\.(?:all\.\d+\.(?:w\d+\.)?\d+|child\.\d+|onError\.\d+)$/;
1367
- function parentRunIdOf(runId) {
1368
- return ENGINE_CHILD_ID.exec(runId)?.[1];
1369
- }
1370
- __name(parentRunIdOf, "parentRunIdOf");
1371
- var GROUP = /^all:(\d+)(?::n\d+)?(:failFast)?$/;
1372
- var REST = /^(?:w(\d+)\.)?\d+$/;
1373
- function fanOfWaiter(waiter) {
1374
- const group = waiter.parallelGroup ? GROUP.exec(waiter.parallelGroup) : null;
1375
- if (!group) return void 0;
1376
- const base = `child:${waiter.runId}.all.${group[1]}.`;
1377
- if (!waiter.token.startsWith(base)) return void 0;
1378
- const rest = REST.exec(waiter.token.slice(base.length));
1379
- if (!rest) return void 0;
1380
- const waiterPrefix = rest[1] === void 0 ? base : `${base}w${rest[1]}.`;
1381
- return {
1382
- waiterPrefix,
1383
- failFast: group[2] !== void 0
1384
- };
1385
- }
1386
- __name(fanOfWaiter, "fanOfWaiter");
1387
-
1388
1650
  // src/handshake/descriptor.ts
1389
1651
  var CURRENT_PROTOCOL_VERSION = 1;
1390
1652
  var LEGACY_V1_PROTOCOL = {
@@ -2068,33 +2330,9 @@ function planDispatch(requires, liveDescriptors, cp, token) {
2068
2330
  controlPlane: cp,
2069
2331
  workers
2070
2332
  }
2071
- };
2072
- }
2073
- __name(planDispatch, "planDispatch");
2074
-
2075
- // src/interfaces.ts
2076
- function checkpointNameKind(name) {
2077
- if (name.startsWith("signal:child:")) return "child";
2078
- if (name.startsWith("fan:")) return "folded";
2079
- if (name.startsWith("spawn:")) return "spawn";
2080
- return "step";
2081
- }
2082
- __name(checkpointNameKind, "checkpointNameKind");
2083
- function poolNamespace(namespace) {
2084
- return namespace === "" ? "default" : namespace;
2085
- }
2086
- __name(poolNamespace, "poolNamespace");
2087
- var TERMINAL_RUN_STATUSES = [
2088
- "completed",
2089
- "failed",
2090
- "cancelled",
2091
- "dead"
2092
- ];
2093
- function isNonEmptyRunScope(scope) {
2094
- if (!scope) return false;
2095
- return scope.workflow !== void 0 || scope.workflows !== void 0 || scope.tag !== void 0 || scope.tags !== void 0 || scope.namespace !== void 0 || scope.namespaces !== void 0 || scope.attributes !== void 0 && scope.attributes.length > 0;
2333
+ };
2096
2334
  }
2097
- __name(isNonEmptyRunScope, "isNonEmptyRunScope");
2335
+ __name(planDispatch, "planDispatch");
2098
2336
 
2099
2337
  // src/on-error.ts
2100
2338
  var UNROUTABLE_CODES = /* @__PURE__ */ new Set([
@@ -2270,122 +2508,6 @@ var RunDispatchBatcher = class {
2270
2508
  }
2271
2509
  };
2272
2510
 
2273
- // src/run-waiting.ts
2274
- var WEBHOOK_PREFIX = "wh:";
2275
- var CHILD_PREFIX = "child:";
2276
- var BREAKPOINT_PREFIX = "bp:";
2277
- var EVENT_PREFIX = EVENT_TOKEN_PREFIX;
2278
- function parseWebhookToken(token) {
2279
- if (!token.startsWith(WEBHOOK_PREFIX)) return null;
2280
- const rest = token.slice(WEBHOOK_PREFIX.length);
2281
- const at = rest.lastIndexOf(":");
2282
- if (at <= 0) return null;
2283
- const seqText = rest.slice(at + 1);
2284
- if (!/^\d+$/.test(seqText)) return null;
2285
- return {
2286
- runId: rest.slice(0, at),
2287
- seq: Number(seqText)
2288
- };
2289
- }
2290
- __name(parseWebhookToken, "parseWebhookToken");
2291
- function classifyWaiterToken(token) {
2292
- if (token.startsWith(WEBHOOK_PREFIX)) return {
2293
- on: "webhook",
2294
- name: token
2295
- };
2296
- if (token.startsWith(BREAKPOINT_PREFIX)) return {
2297
- on: "breakpoint",
2298
- name: "breakpoint"
2299
- };
2300
- if (token.startsWith(CHILD_PREFIX)) return {
2301
- on: "child",
2302
- name: token.slice(CHILD_PREFIX.length)
2303
- };
2304
- if (token.startsWith(EVENT_PREFIX)) return {
2305
- on: "signal",
2306
- name: eventNameOf(token)
2307
- };
2308
- return {
2309
- on: "signal",
2310
- name: token
2311
- };
2312
- }
2313
- __name(classifyWaiterToken, "classifyWaiterToken");
2314
- function resolveRunWaiting(run, waiterByRun) {
2315
- if (run.status !== "suspended") return void 0;
2316
- const waiter = waiterByRun.get(run.id);
2317
- return waiter ? classifyWaiterToken(waiter.token) : void 0;
2318
- }
2319
- __name(resolveRunWaiting, "resolveRunWaiting");
2320
- var RUN_ID_BATCH = 500;
2321
- async function listSignalWaitersOfRuns(store, runIds) {
2322
- const unique = [
2323
- ...new Set(runIds)
2324
- ];
2325
- if (unique.length === 0) return [];
2326
- if (!store.listSignalWaitersByRun) {
2327
- const ids = new Set(unique);
2328
- return (await store.listSignalWaiters("")).filter((w) => ids.has(w.runId));
2329
- }
2330
- const out = [];
2331
- for (let at = 0; at < unique.length; at += RUN_ID_BATCH) {
2332
- out.push(...await store.listSignalWaitersByRun(unique.slice(at, at + RUN_ID_BATCH)));
2333
- }
2334
- return out;
2335
- }
2336
- __name(listSignalWaitersOfRuns, "listSignalWaitersOfRuns");
2337
- function indexWaitersByRun(waiters) {
2338
- const byRun = /* @__PURE__ */ new Map();
2339
- for (const w of waiters) if (!byRun.has(w.runId)) byRun.set(w.runId, w);
2340
- return byRun;
2341
- }
2342
- __name(indexWaitersByRun, "indexWaitersByRun");
2343
- var GET_RUN_FALLBACK_PARALLEL = 50;
2344
- async function getRunsByIds(store, runIds) {
2345
- const { runs, unread, error: error2 } = await readRunsByIds(store, runIds);
2346
- if (unread.length > 0) throw error2;
2347
- return runs;
2348
- }
2349
- __name(getRunsByIds, "getRunsByIds");
2350
- async function readRunsByIds(store, runIds) {
2351
- const unique = [
2352
- ...new Set(runIds)
2353
- ];
2354
- const runs = /* @__PURE__ */ new Map();
2355
- const unread = [];
2356
- let error2;
2357
- const batch = store.getRuns ? RUN_ID_BATCH : GET_RUN_FALLBACK_PARALLEL;
2358
- for (let at = 0; at < unique.length; at += batch) {
2359
- const ids = unique.slice(at, at + batch);
2360
- if (store.getRuns) {
2361
- try {
2362
- for (const run of await store.getRuns(ids)) runs.set(run.id, run);
2363
- } catch (err) {
2364
- error2 ??= err;
2365
- unread.push(...ids);
2366
- }
2367
- continue;
2368
- }
2369
- const reads = await Promise.allSettled(ids.map((id) => store.getRun(id)));
2370
- reads.forEach((read, k) => {
2371
- if (read.status === "fulfilled") {
2372
- if (read.value) runs.set(read.value.id, read.value);
2373
- return;
2374
- }
2375
- error2 ??= read.reason;
2376
- unread.push(ids[k]);
2377
- });
2378
- }
2379
- return {
2380
- runs,
2381
- unread,
2382
- ...unread.length > 0 ? {
2383
- error: error2
2384
- } : {}
2385
- };
2386
- }
2387
- __name(readRunsByIds, "readRunsByIds");
2388
-
2389
2511
  // src/scheduler.ts
2390
2512
  import { createRequire } from "node:module";
2391
2513
 
@@ -3842,7 +3964,23 @@ function createWorkflowCtx(host, runId, compensations, workflow = "", searchAttr
3842
3964
  status: "running"
3843
3965
  }));
3844
3966
  }
3845
- if (!await store.getRun(id)) host.startChild(workflowName(workflow2), input, id, priority, version);
3967
+ const childRun = await store.getRun(id);
3968
+ if (!childRun) {
3969
+ host.startChild(workflowName(workflow2), input, id, priority, version);
3970
+ } else {
3971
+ const done = (await settleFinishedChildren([
3972
+ {
3973
+ id,
3974
+ seq: current
3975
+ }
3976
+ ], /* @__PURE__ */ new Map([
3977
+ [
3978
+ id,
3979
+ childRun
3980
+ ]
3981
+ ]))).get(id);
3982
+ if (done) return unwrapCompletion(done, `child "${id}"`);
3983
+ }
3846
3984
  throw new WorkflowSuspended(void 0, fanJoin([
3847
3985
  {
3848
3986
  waiterPrefix: `child:${id}`,
@@ -3863,164 +4001,174 @@ function createWorkflowCtx(host, runId, compensations, workflow = "", searchAttr
3863
4001
  const group = allGroup(firstSeq, inputs.length, mode);
3864
4002
  const waveOf = /* @__PURE__ */ __name((i) => waves ? Math.floor(i / waves.waveSize) : void 0, "waveOf");
3865
4003
  const id = /* @__PURE__ */ __name((i) => allChildId(runId, firstSeq, i, waveOf(i)), "id");
3866
- const { checkpoints: existing, folded } = await readFan(positions, [
3867
- allWait(runId, firstSeq).checkpointPrefix,
3868
- allFoldedName(runId, firstSeq)
3869
- ]);
3870
- const waveCount = waves ? Math.ceil(inputs.length / waves.waveSize) : 1;
3871
- const waveItems = /* @__PURE__ */ __name((k) => {
3872
- if (!waves) return inputs.map((_, i) => i);
3873
- const from = k * waves.waveSize;
3874
- return inputs.map((_, i) => i).slice(from, from + waves.waveSize);
3875
- }, "waveItems");
3876
- if (mode === "failFast") {
3877
- const failure = /* @__PURE__ */ __name((i2) => {
3878
- const cp = existing[i2];
3879
- const c2 = cp?.status === "completed" ? cp.output : null;
3880
- return c2 && c2.ok === false ? c2 : null;
3881
- }, "failure");
3882
- const first = /* @__PURE__ */ __name((cancelled) => inputs.findIndex((_, i2) => {
3883
- const c2 = failure(i2);
3884
- return c2 !== null && c2.code === "cancelled" === cancelled;
3885
- }), "first");
3886
- const i = first(false) !== -1 ? first(false) : first(true);
3887
- const c = i === -1 ? null : failure(i);
3888
- if (c) {
3889
- for (let j = 0; j < inputs.length; j += 1) {
3890
- if (j !== i && existing[j]?.status !== "completed") host.cancelChild?.(id(j));
3891
- }
3892
- throw new GatherError([
3893
- {
3894
- index: i,
3895
- id: id(i),
3896
- error: c.error ?? "unknown"
4004
+ for (; ; ) {
4005
+ const { checkpoints: existing, folded } = await readFan(positions, [
4006
+ allWait(runId, firstSeq).checkpointPrefix,
4007
+ allFoldedName(runId, firstSeq)
4008
+ ]);
4009
+ const waveCount = waves ? Math.ceil(inputs.length / waves.waveSize) : 1;
4010
+ const waveItems = /* @__PURE__ */ __name((k) => {
4011
+ if (!waves) return inputs.map((_, i) => i);
4012
+ const from = k * waves.waveSize;
4013
+ return inputs.map((_, i) => i).slice(from, from + waves.waveSize);
4014
+ }, "waveItems");
4015
+ if (mode === "failFast") {
4016
+ const failure = /* @__PURE__ */ __name((i2) => {
4017
+ const cp = existing[i2];
4018
+ const c2 = cp?.status === "completed" ? cp.output : null;
4019
+ return c2 && c2.ok === false ? c2 : null;
4020
+ }, "failure");
4021
+ const first = /* @__PURE__ */ __name((cancelled) => inputs.findIndex((_, i2) => {
4022
+ const c2 = failure(i2);
4023
+ return c2 !== null && c2.code === "cancelled" === cancelled;
4024
+ }), "first");
4025
+ const i = first(false) !== -1 ? first(false) : first(true);
4026
+ const c = i === -1 ? null : failure(i);
4027
+ if (c) {
4028
+ for (let j = 0; j < inputs.length; j += 1) {
4029
+ if (j !== i && existing[j]?.status !== "completed") host.cancelChild?.(id(j));
3897
4030
  }
3898
- ], inputs.length);
3899
- }
3900
- }
3901
- if (store.compactCheckpoints && inputs.length >= FOLD_MIN_FAN) {
3902
- for (let k = 0; k < waveCount; k += 1) {
3903
- const items = waveItems(k);
3904
- const first = items[0];
3905
- const last = items[items.length - 1];
3906
- if (folded[first]) continue;
3907
- const values = [];
3908
- for (const i of items) {
3909
- const c = existing[i]?.status === "completed" ? existing[i]?.output : void 0;
3910
- if (!c || typeof c !== "object" || c.ok !== true) break;
3911
- values.push(c.value);
3912
- }
3913
- if (values.length < items.length) continue;
3914
- const into = instantCheckpoint({
3915
- runId,
3916
- seq: positions[first],
3917
- name: allFoldedName(runId, firstSeq, waves ? k : void 0),
3918
- kind: "signal",
3919
- output: {
3920
- from: first,
3921
- values
3922
- },
3923
- parallelGroup: group
3924
- });
3925
- await store.compactCheckpoints(runId, positions[first], positions[last], into);
3926
- for (const i of items) replay?.delete(positions[i]);
3927
- replay?.set(into.seq, into);
3928
- }
3929
- }
3930
- const sent = /* @__PURE__ */ __name((k) => waveItems(k).some((i) => existing[i]), "sent");
3931
- const settled = /* @__PURE__ */ __name((k) => waveItems(k).every((i) => existing[i]?.status === "completed"), "settled");
3932
- const active = [];
3933
- let unsettled = 0;
3934
- for (let k = 0; k < waveCount; k += 1) {
3935
- if (settled(k)) continue;
3936
- if (sent(k) || unsettled < (waves?.maxWaves ?? 1)) {
3937
- active.push(k);
3938
- unsettled += 1;
3939
- }
3940
- }
3941
- if (active.length > 0) {
3942
- const toRegister = [];
3943
- const toEnsure = [];
3944
- for (const k of active) {
3945
- for (const i of waveItems(k)) {
3946
- const cp = existing[i];
3947
- if (cp?.status === "completed") continue;
3948
- (cp ? toEnsure : toRegister).push(i);
4031
+ throw new GatherError([
4032
+ {
4033
+ index: i,
4034
+ id: id(i),
4035
+ error: c.error ?? "unknown"
4036
+ }
4037
+ ], inputs.length);
3949
4038
  }
3950
4039
  }
3951
- for (let at = 0; at < toRegister.length; at += FAN_CHUNK) {
3952
- const chunk = toRegister.slice(at, at + FAN_CHUNK);
3953
- const entries = chunk.map((i) => ({
3954
- // Carry the fan `group` onto the waiter too: the child's terminal `signal:child:`
3955
- // checkpoint is (re)written by engine.signal when the child notifies the parent,
3956
- // OVERWRITING the running placeholder at the same seq — so without this the resolved
3957
- // checkpoint would lose the group and the dashboard would render the resolved fan as a
3958
- // sequential chain.
3959
- waiter: {
3960
- token: `child:${id(i)}`,
3961
- runId,
3962
- seq: positions[i],
3963
- parallelGroup: group
3964
- },
3965
- checkpoint: instantCheckpoint({
4040
+ if (store.compactCheckpoints && inputs.length >= FOLD_MIN_FAN) {
4041
+ for (let k = 0; k < waveCount; k += 1) {
4042
+ const items = waveItems(k);
4043
+ const first = items[0];
4044
+ const last = items[items.length - 1];
4045
+ if (folded[first]) continue;
4046
+ const values = [];
4047
+ for (const i of items) {
4048
+ const c = existing[i]?.status === "completed" ? existing[i]?.output : void 0;
4049
+ if (!c || typeof c !== "object" || c.ok !== true) break;
4050
+ values.push(c.value);
4051
+ }
4052
+ if (values.length < items.length) continue;
4053
+ const into = instantCheckpoint({
3966
4054
  runId,
3967
- seq: positions[i],
3968
- name: `signal:child:${id(i)}`,
4055
+ seq: positions[first],
4056
+ name: allFoldedName(runId, firstSeq, waves ? k : void 0),
3969
4057
  kind: "signal",
3970
- status: "running",
4058
+ output: {
4059
+ from: first,
4060
+ values
4061
+ },
3971
4062
  parallelGroup: group
3972
- })
3973
- }));
3974
- await registerChildWaits(entries);
3975
- for (const i of chunk) host.startChild(name, inputs[i], id(i));
3976
- }
3977
- if (toEnsure.length > 0) {
3978
- const { runs: started, unread } = await readRunsByIds(store, toEnsure.map((i) => id(i)));
3979
- const skip = new Set(unread);
3980
- for (const i of toEnsure) {
3981
- if (!started.has(id(i)) && !skip.has(id(i))) host.startChild(name, inputs[i], id(i));
4063
+ });
4064
+ await store.compactCheckpoints(runId, positions[first], positions[last], into);
4065
+ for (const i of items) replay?.delete(positions[i]);
4066
+ replay?.set(into.seq, into);
3982
4067
  }
3983
4068
  }
3984
- }
3985
- if (active.length > 0) {
3986
- throw new WorkflowSuspended(void 0, fanJoin(active.map((k) => allWait(runId, firstSeq, waves ? k : void 0))));
3987
- }
3988
- const outputs = [];
3989
- const results = [];
3990
- const failures = [];
3991
- for (let i = 0; i < inputs.length; i += 1) {
3992
- const c = existing[i]?.output;
3993
- if (c && typeof c === "object" && "ok" in c && c.ok === false) {
3994
- const { message, ...rest } = completionError({
3995
- ...c,
3996
- error: c.error ?? "unknown"
3997
- });
3998
- const failure = {
4069
+ const sent = /* @__PURE__ */ __name((k) => waveItems(k).some((i) => existing[i]), "sent");
4070
+ const settled = /* @__PURE__ */ __name((k) => waveItems(k).every((i) => existing[i]?.status === "completed"), "settled");
4071
+ const active = [];
4072
+ let unsettled = 0;
4073
+ for (let k = 0; k < waveCount; k += 1) {
4074
+ if (settled(k)) continue;
4075
+ if (sent(k) || unsettled < (waves?.maxWaves ?? 1)) {
4076
+ active.push(k);
4077
+ unsettled += 1;
4078
+ }
4079
+ }
4080
+ let known;
4081
+ if (active.length > 0) {
4082
+ const toRegister = [];
4083
+ const toEnsure = [];
4084
+ for (const k of active) {
4085
+ for (const i of waveItems(k)) {
4086
+ const cp = existing[i];
4087
+ if (cp?.status === "completed") continue;
4088
+ (cp ? toEnsure : toRegister).push(i);
4089
+ }
4090
+ }
4091
+ for (let at = 0; at < toRegister.length; at += FAN_CHUNK) {
4092
+ const chunk = toRegister.slice(at, at + FAN_CHUNK);
4093
+ const entries = chunk.map((i) => ({
4094
+ // Carry the fan `group` onto the waiter too: the child's terminal `signal:child:`
4095
+ // checkpoint is (re)written by engine.signal when the child notifies the parent,
4096
+ // OVERWRITING the running placeholder at the same seq — so without this the resolved
4097
+ // checkpoint would lose the group and the dashboard would render the resolved fan as a
4098
+ // sequential chain.
4099
+ waiter: {
4100
+ token: `child:${id(i)}`,
4101
+ runId,
4102
+ seq: positions[i],
4103
+ parallelGroup: group
4104
+ },
4105
+ checkpoint: instantCheckpoint({
4106
+ runId,
4107
+ seq: positions[i],
4108
+ name: `signal:child:${id(i)}`,
4109
+ kind: "signal",
4110
+ status: "running",
4111
+ parallelGroup: group
4112
+ })
4113
+ }));
4114
+ await registerChildWaits(entries);
4115
+ for (const i of chunk) host.startChild(name, inputs[i], id(i));
4116
+ }
4117
+ if (toEnsure.length > 0) {
4118
+ const { runs: started, unread } = await readRunsByIds(store, toEnsure.map((i) => id(i)));
4119
+ const skip = new Set(unread);
4120
+ for (const i of toEnsure) {
4121
+ if (!started.has(id(i)) && !skip.has(id(i))) host.startChild(name, inputs[i], id(i));
4122
+ }
4123
+ known = started;
4124
+ }
4125
+ const now2 = await settleFinishedChildren(toEnsure.map((i) => ({
3999
4126
  id: id(i),
4000
- error: message,
4001
- ...rest
4002
- };
4003
- failures.push({
4004
- index: i,
4005
- ...failure
4006
- });
4007
- results.push({
4008
- status: "rejected",
4009
- ...failure
4010
- });
4011
- outputs.push(void 0);
4012
- } else {
4013
- const value = c?.value;
4014
- results.push({
4015
- status: "fulfilled",
4016
- value
4017
- });
4018
- outputs.push(value);
4127
+ seq: positions[i],
4128
+ parallelGroup: group
4129
+ })), known);
4130
+ if (now2.size > 0) continue;
4131
+ }
4132
+ if (active.length > 0) {
4133
+ throw new WorkflowSuspended(void 0, fanJoin(active.map((k) => allWait(runId, firstSeq, waves ? k : void 0))));
4134
+ }
4135
+ const outputs = [];
4136
+ const results = [];
4137
+ const failures = [];
4138
+ for (let i = 0; i < inputs.length; i += 1) {
4139
+ const c = existing[i]?.output;
4140
+ if (c && typeof c === "object" && "ok" in c && c.ok === false) {
4141
+ const { message, ...rest } = completionError({
4142
+ ...c,
4143
+ error: c.error ?? "unknown"
4144
+ });
4145
+ const failure = {
4146
+ id: id(i),
4147
+ error: message,
4148
+ ...rest
4149
+ };
4150
+ failures.push({
4151
+ index: i,
4152
+ ...failure
4153
+ });
4154
+ results.push({
4155
+ status: "rejected",
4156
+ ...failure
4157
+ });
4158
+ outputs.push(void 0);
4159
+ } else {
4160
+ const value = c?.value;
4161
+ results.push({
4162
+ status: "fulfilled",
4163
+ value
4164
+ });
4165
+ outputs.push(value);
4166
+ }
4019
4167
  }
4168
+ if (settle) return results;
4169
+ if (failures.length > 0) throw new GatherError(failures, inputs.length);
4170
+ return outputs;
4020
4171
  }
4021
- if (settle) return results;
4022
- if (failures.length > 0) throw new GatherError(failures, inputs.length);
4023
- return outputs;
4024
4172
  }, "fan");
4025
4173
  const all = /* @__PURE__ */ __name((workflow2, inputs, opts) => fan(workflow2, inputs, opts, false), "all");
4026
4174
  const allSettled = /* @__PURE__ */ __name((workflow2, inputs, opts) => fan(workflow2, inputs, opts, true), "allSettled");
@@ -4106,6 +4254,39 @@ function createWorkflowCtx(host, runId, compensations, workflow = "", searchAttr
4106
4254
  }
4107
4255
  for (const e of entries) replay?.set(e.checkpoint.seq, e.checkpoint);
4108
4256
  }, "registerChildWaits");
4257
+ const settleFinishedChildren = /* @__PURE__ */ __name(async (awaited, known) => {
4258
+ const settled = /* @__PURE__ */ new Map();
4259
+ if (awaited.length === 0) return settled;
4260
+ const runs = new Map(known ?? []);
4261
+ const toRead = awaited.filter((a) => !runs.has(a.id)).map((a) => a.id);
4262
+ if (toRead.length > 0) {
4263
+ for (const [id, run] of (await readRunsByIds(store, toRead)).runs) runs.set(id, run);
4264
+ }
4265
+ for (const a of awaited) {
4266
+ const child2 = runs.get(a.id);
4267
+ const completion = child2 ? childCompletion(child2) : void 0;
4268
+ if (!completion) continue;
4269
+ const token = `child:${a.id}`;
4270
+ const taken = await store.takeSignalWaiter(token);
4271
+ if (!taken) continue;
4272
+ if (taken.runId !== runId || taken.seq !== a.seq) {
4273
+ await store.putSignalWaiter(taken);
4274
+ continue;
4275
+ }
4276
+ await writeCheckpoint(instantCheckpoint({
4277
+ runId,
4278
+ seq: a.seq,
4279
+ name: `signal:child:${a.id}`,
4280
+ kind: "signal",
4281
+ output: completion,
4282
+ ...a.parallelGroup !== void 0 ? {
4283
+ parallelGroup: a.parallelGroup
4284
+ } : {}
4285
+ }));
4286
+ settled.set(a.id, completion);
4287
+ }
4288
+ return settled;
4289
+ }, "settleFinishedChildren");
4109
4290
  const startChild = /* @__PURE__ */ __name(async (workflow2, input, options) => {
4110
4291
  const { childId, priority, version } = normalizeChildOptions(options);
4111
4292
  const current = pos.next();
@@ -4463,33 +4644,10 @@ var pendingStateKey = /* @__PURE__ */ __name((at) => String(new Date(at).getTime
4463
4644
  var freshPendingKey = /* @__PURE__ */ __name(() => `${Date.now()}-${globalThis.crypto.randomUUID().slice(0, 8)}`, "freshPendingKey");
4464
4645
  var wakeRequestToken = /* @__PURE__ */ __name((runId) => `durable:wake-after-turn:${runId}`, "wakeRequestToken");
4465
4646
  var CHILD_SIGNAL_PREFIX = "signal:child:";
4647
+ var BUFFERED_SWEEP_PAGE = 500;
4648
+ var BUFFERED_SWEEP_PAGES_PER_CALL = 4;
4649
+ var BUFFERED_SWEEP_REST_MS = 10 * 6e4;
4466
4650
  var DELIVER_CHUNK = 50;
4467
- function childCompletion(child) {
4468
- if (child.status === "completed") return {
4469
- ok: true,
4470
- value: child.output
4471
- };
4472
- if (child.status === "failed") return failedCompletion(child.error ?? {
4473
- message: "failed"
4474
- });
4475
- if (child.status === "dead") return failedCompletion(child.error ?? {
4476
- message: "dead"
4477
- });
4478
- if (child.status === "cancelled") return cancelledCompletion(child.error);
4479
- return void 0;
4480
- }
4481
- __name(childCompletion, "childCompletion");
4482
- var CHILD_CANCELLED = "cancelled";
4483
- function cancelledCompletion(error2) {
4484
- const base = error2 ?? {
4485
- message: "cancelled"
4486
- };
4487
- return failedCompletion({
4488
- ...base,
4489
- code: base.code ?? CHILD_CANCELLED
4490
- });
4491
- }
4492
- __name(cancelledCompletion, "cancelledCompletion");
4493
4651
  var RESULT_BATCH_MAX = 256;
4494
4652
  function shuffle(items) {
4495
4653
  for (let i = items.length - 1; i > 0; i -= 1) {
@@ -4705,6 +4863,10 @@ var WorkflowEngine = class {
4705
4863
  /** Runs already paged about, by id → the `updatedAt` of the paged episode (see {@link sweepStalled}). */
4706
4864
  notifiedStalled = /* @__PURE__ */ new Map();
4707
4865
  lastStalledSweepAt = Number.NEGATIVE_INFINITY;
4866
+ /** {@link sweepBufferedSignals}: the id the pass in progress resumes after, `undefined` between passes. */
4867
+ bufferedSweepCursor;
4868
+ bufferedSweepActive = false;
4869
+ lastBufferedSweepEndedAt = Number.NEGATIVE_INFINITY;
4708
4870
  stalledAfterMs;
4709
4871
  draining = false;
4710
4872
  /** Wait already spent on this shutdown before `drain()` (see `beginDrain`). */
@@ -5310,6 +5472,50 @@ var WorkflowEngine = class {
5310
5472
  return () => this.deadListeners.delete(listener);
5311
5473
  }
5312
5474
  /**
5475
+ * Delete buffered child completions no parent will ever consume (see
5476
+ * `childCompletionNeedsBuffer`): those of root runs, of children whose parent already settled them
5477
+ * or is gone, or terminal for good. Such rows were written by earlier versions, which buffered every
5478
+ * completion that found no waiter, and nothing else ever removes them.
5479
+ *
5480
+ * Walks `durable_buffered_signals` by primary key, {@link BUFFERED_SWEEP_PAGE} rows at a time and
5481
+ * at most {@link BUFFERED_SWEEP_PAGES_PER_CALL} pages per call, so a large backlog drains over a
5482
+ * few timer ticks without a long query or a big delete. A pass that reaches the end of the table
5483
+ * rests {@link BUFFERED_SWEEP_REST_MS} before the next one. Call it every tick (the NestJS timer
5484
+ * poller does). Skipped while draining, on a namespaced engine (a buffered signal carries no
5485
+ * namespace, and a scoped read would report another tenant's parent as gone), and on a store
5486
+ * without `listBufferedSignals`/`deleteBufferedSignals`. Returns how many rows it deleted.
5487
+ */
5488
+ async sweepBufferedSignals(now = this.clock()) {
5489
+ const { listBufferedSignals, deleteBufferedSignals } = this.store;
5490
+ if (!listBufferedSignals || !deleteBufferedSignals) return 0;
5491
+ if (this.draining || this.namespace !== void 0 || this.bufferedSweepActive) return 0;
5492
+ if (this.bufferedSweepCursor === void 0 && now - this.lastBufferedSweepEndedAt < BUFFERED_SWEEP_REST_MS) {
5493
+ return 0;
5494
+ }
5495
+ this.bufferedSweepActive = true;
5496
+ let deleted = 0;
5497
+ try {
5498
+ for (let page = 0; page < BUFFERED_SWEEP_PAGES_PER_CALL; page++) {
5499
+ const rows = await listBufferedSignals.call(this.store, CHILD_TOKEN_PREFIX, this.bufferedSweepCursor, BUFFERED_SWEEP_PAGE);
5500
+ const doomed = await unneededChildSignals(this.store, rows);
5501
+ if (doomed.length > 0) {
5502
+ await deleteBufferedSignals.call(this.store, doomed.map((row) => row.id));
5503
+ deleted += doomed.length;
5504
+ }
5505
+ const last = rows.at(-1);
5506
+ if (rows.length < BUFFERED_SWEEP_PAGE || !last) {
5507
+ this.bufferedSweepCursor = void 0;
5508
+ this.lastBufferedSweepEndedAt = now;
5509
+ break;
5510
+ }
5511
+ this.bufferedSweepCursor = last.id;
5512
+ }
5513
+ } finally {
5514
+ this.bufferedSweepActive = false;
5515
+ }
5516
+ return deleted;
5517
+ }
5518
+ /**
5313
5519
  * Register a pager hook for STRANDED runs: an in-flight run untouched past `stalledAfterMs` that is
5314
5520
  * either suspended with NO wake timer (wake-forever: nothing will ever re-drive it) or parked on an
5315
5521
  * old pending remote step with no live lease (dispatched and, most likely, lost). The timer poller
@@ -7016,7 +7222,9 @@ var WorkflowEngine = class {
7016
7222
  await this.deliverSignal(waiter, completion);
7017
7223
  return;
7018
7224
  }
7019
- if (cancelled || await this.parentHoldsCompletion(token, completion)) return;
7225
+ if (cancelled) return;
7226
+ const childId = token.slice(CHILD_TOKEN_PREFIX.length);
7227
+ if (!await childCompletionNeedsBuffer(this.store, childId, completion)) return;
7020
7228
  await this.signal(token, completion);
7021
7229
  return;
7022
7230
  } catch (err) {
@@ -7030,24 +7238,6 @@ var WorkflowEngine = class {
7030
7238
  }
7031
7239
  }
7032
7240
  /**
7033
- * Whether the parent of the child behind `token` (`child:<id>`) already holds `completion` as that
7034
- * child's settled `signal:child:<id>` checkpoint: its own turn settled the child from the child's
7035
- * row, so this notification is a duplicate. A different completion (a fix-and-replay run's success
7036
- * for a child the parent saw fail) is not, and is buffered for the parent's retry. Only for a child
7037
- * whose id the engine minted (its parent is derivable from it).
7038
- */
7039
- async parentHoldsCompletion(token, completion) {
7040
- const childId = token.slice("child:".length);
7041
- const parentId = parentRunIdOf(childId);
7042
- if (!parentId) return false;
7043
- const name = `${CHILD_SIGNAL_PREFIX}${childId}`;
7044
- const checkpoints = this.store.listCheckpointsByNamePrefix ? await this.store.listCheckpointsByNamePrefix(parentId, [
7045
- name
7046
- ]) : await this.store.listCheckpoints(parentId);
7047
- const expected = JSON.stringify(completion);
7048
- return checkpoints.some((cp) => cp.name === name && cp.status === "completed" && JSON.stringify(cp.output) === expected);
7049
- }
7050
- /**
7051
7241
  * Deferred child start shared by the in-process ctx host and the remote `startChild` command.
7052
7242
  * Deferred (microtask) so a fast child can't reentrantly resume a still-suspending parent. A start
7053
7243
  * that THROWS (unregistered/unroutable workflow, input validation, singleton back-pressure, store
@@ -7062,15 +7252,22 @@ var WorkflowEngine = class {
7062
7252
  startChildDeferred(workflow, input, childId, opts) {
7063
7253
  this.localWork.submit(`start:${childId}`, async () => {
7064
7254
  if (this.draining) return;
7065
- await this.start(workflow, input, childId, opts).catch((err) => {
7255
+ const started = await this.start(workflow, input, childId, opts).catch((err) => {
7066
7256
  const message = err instanceof Error ? err.message : String(err);
7067
7257
  this.notifyParent(childId, {
7068
7258
  ok: false,
7069
7259
  error: `child workflow "${workflow}" failed to start: ${message}`
7070
7260
  });
7261
+ return void 0;
7071
7262
  });
7263
+ if (started) this.redeliverFinishedChild(childId, started);
7072
7264
  });
7073
7265
  }
7266
+ /** {@link startChildDeferred}: re-deliver the completion of a child `start` found finished. */
7267
+ redeliverFinishedChild(childId, run) {
7268
+ const completion = childCompletion(run);
7269
+ if (completion) this.inBackground(this.deliverToParent(`child:${childId}`, completion));
7270
+ }
7074
7271
  /**
7075
7272
  * Cancel a run (e.g. from the dashboard). Returns null if the run does not exist. Pass
7076
7273
  * `{ compensate: true }` to undo the saga first: the suspended run is resumed so its completed
@@ -8675,10 +8872,13 @@ ${run.workflowVersion}`;
8675
8872
  // dashboard renders the fan as one parallel group. Undefined for a lone (non-fan) child.
8676
8873
  parallelGroup: cmd.parallelGroup
8677
8874
  });
8678
- if (!await this.store.getRun(childId)) {
8875
+ const existingChild = await this.store.getRun(childId);
8876
+ if (!existingChild) {
8679
8877
  this.startChildDeferred(cmd.workflow, cmd.input, childId, {
8680
8878
  namespace: run.namespace
8681
8879
  });
8880
+ } else {
8881
+ this.redeliverFinishedChild(childId, existingChild);
8682
8882
  }
8683
8883
  } else {
8684
8884
  throw new Error(`remote workflow command '${cmd.kind}' is not supported yet`);
@@ -10689,6 +10889,10 @@ var CodecStateStore = class {
10689
10889
  compactCheckpoints;
10690
10890
  /** Decodes each run like {@link getRun}. Bound only when the inner store has it. */
10691
10891
  getRuns;
10892
+ /** Decodes each payload like {@link takeBufferedSignal}. Bound only when the inner store has it. */
10893
+ listBufferedSignals;
10894
+ /** Forwarded verbatim — deletes by row id. */
10895
+ deleteBufferedSignals;
10692
10896
  /** Forwarded verbatim — a waiter carries no payload. */
10693
10897
  listSignalWaitersByRun;
10694
10898
  /** Forwarded verbatim — a waiter carries no payload. */
@@ -10734,6 +10938,13 @@ var CodecStateStore = class {
10734
10938
  if (compact) this.compactCheckpoints = (runId, fromSeq, toSeq, into) => compact.call(inner, runId, fromSeq, toSeq, this.encCp(into));
10735
10939
  const getRuns = inner.getRuns;
10736
10940
  if (getRuns) this.getRuns = async (runIds) => (await getRuns.call(inner, runIds)).map((r) => this.decRun(r));
10941
+ const listBuffered = inner.listBufferedSignals;
10942
+ if (listBuffered) this.listBufferedSignals = async (tokenPrefix, afterId, limit) => (await listBuffered.call(inner, tokenPrefix, afterId, limit)).map((row) => ({
10943
+ ...row,
10944
+ payload: this.dec(row.payload)
10945
+ }));
10946
+ const deleteBuffered = inner.deleteBufferedSignals;
10947
+ if (deleteBuffered) this.deleteBufferedSignals = (ids) => deleteBuffered.call(inner, ids);
10737
10948
  const byRun = inner.listSignalWaitersByRun;
10738
10949
  if (byRun) this.listSignalWaitersByRun = (runIds) => byRun.call(inner, runIds);
10739
10950
  const hasWaiter = inner.hasSignalWaiter;
@@ -11479,20 +11690,47 @@ var InMemoryStateStore = class {
11479
11690
  });
11480
11691
  }
11481
11692
  bufferedSignals = /* @__PURE__ */ new Map();
11693
+ bufferedSignalSeq = 0;
11482
11694
  async bufferSignal(token, payload) {
11483
11695
  const queue = this.bufferedSignals.get(token) ?? [];
11484
- queue.push(payload);
11696
+ queue.push({
11697
+ id: ++this.bufferedSignalSeq,
11698
+ payload
11699
+ });
11485
11700
  this.bufferedSignals.set(token, queue);
11486
11701
  }
11487
11702
  async takeBufferedSignal(token) {
11488
11703
  const queue = this.bufferedSignals.get(token);
11489
11704
  if (!queue || queue.length === 0) return null;
11490
- const payload = queue.shift();
11705
+ const head = queue.shift();
11491
11706
  if (queue.length === 0) this.bufferedSignals.delete(token);
11492
11707
  return {
11493
- payload
11708
+ payload: head.payload
11494
11709
  };
11495
11710
  }
11711
+ async listBufferedSignals(tokenPrefix, afterId, limit) {
11712
+ const after = afterId === void 0 ? 0 : Number(afterId);
11713
+ const rows = [];
11714
+ for (const [token, queue] of this.bufferedSignals) {
11715
+ if (!token.startsWith(tokenPrefix)) continue;
11716
+ for (const { id, payload } of queue) {
11717
+ if (id > after) rows.push({
11718
+ id: String(id),
11719
+ token,
11720
+ payload
11721
+ });
11722
+ }
11723
+ }
11724
+ return rows.sort((a, b) => Number(a.id) - Number(b.id)).slice(0, limit);
11725
+ }
11726
+ async deleteBufferedSignals(ids) {
11727
+ const doomed = new Set(ids.map(Number));
11728
+ for (const [token, queue] of this.bufferedSignals) {
11729
+ const kept = queue.filter((row) => !doomed.has(row.id));
11730
+ if (kept.length === 0) this.bufferedSignals.delete(token);
11731
+ else this.bufferedSignals.set(token, kept);
11732
+ }
11733
+ }
11496
11734
  /** `id -> row`, so {@link removeBufferedEvent} can delete an exact row regardless of iteration
11497
11735
  * order — mirrors the SQL stores' PK-by-id delete. */
11498
11736
  bufferedEvents = /* @__PURE__ */ new Map();