@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 +438 -76
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +136 -12
- package/dist/index.d.ts +136 -12
- package/dist/index.js +436 -76
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -389,12 +389,15 @@ var WorkflowSuspended = class extends Error {
|
|
|
389
389
|
}
|
|
390
390
|
/** Epoch ms to auto-resume (durable sleep), or undefined when waiting on an external signal. */
|
|
391
391
|
wakeAt;
|
|
392
|
+
/** Set when awaited children are what the run is waiting on — see {@link FanJoin}. */
|
|
393
|
+
fan;
|
|
392
394
|
/** Marks this as a control-flow signal — see {@link isWorkflowControlFlowSignal}. */
|
|
393
395
|
[CONTROL_FLOW_SIGNAL] = true;
|
|
394
|
-
constructor(wakeAt) {
|
|
396
|
+
constructor(wakeAt, fan) {
|
|
395
397
|
super("workflow suspended");
|
|
396
398
|
this.name = "WorkflowSuspended";
|
|
397
399
|
this.wakeAt = wakeAt;
|
|
400
|
+
this.fan = fan;
|
|
398
401
|
}
|
|
399
402
|
};
|
|
400
403
|
var ContinueAsNew = class extends Error {
|
|
@@ -821,6 +824,21 @@ function instantCheckpoint(p) {
|
|
|
821
824
|
}
|
|
822
825
|
__name(instantCheckpoint, "instantCheckpoint");
|
|
823
826
|
|
|
827
|
+
// src/completion.ts
|
|
828
|
+
function isFailedCompletion(payload) {
|
|
829
|
+
const c = payload;
|
|
830
|
+
return !!c && typeof c === "object" && "ok" in c && c.ok === false;
|
|
831
|
+
}
|
|
832
|
+
__name(isFailedCompletion, "isFailedCompletion");
|
|
833
|
+
function unwrapCompletion(payload, label) {
|
|
834
|
+
const c = payload;
|
|
835
|
+
if (c && typeof c === "object" && "ok" in c && c.ok === false) {
|
|
836
|
+
throw new FatalError(`${label} failed: ${c.error}`);
|
|
837
|
+
}
|
|
838
|
+
return c?.value;
|
|
839
|
+
}
|
|
840
|
+
__name(unwrapCompletion, "unwrapCompletion");
|
|
841
|
+
|
|
824
842
|
// src/concurrency.ts
|
|
825
843
|
var DEFAULT_CONCURRENCY_STATUSES = [
|
|
826
844
|
"pending",
|
|
@@ -1060,6 +1078,81 @@ function eventMatches(payload, match) {
|
|
|
1060
1078
|
}
|
|
1061
1079
|
__name(eventMatches, "eventMatches");
|
|
1062
1080
|
|
|
1081
|
+
// src/fan-join.ts
|
|
1082
|
+
function allChildId(runId, firstSeq, index, wave) {
|
|
1083
|
+
return `${allBase(runId, firstSeq, wave)}${index}`;
|
|
1084
|
+
}
|
|
1085
|
+
__name(allChildId, "allChildId");
|
|
1086
|
+
function allGroup(firstSeq, mode) {
|
|
1087
|
+
return mode === "failFast" ? `all:${firstSeq}:failFast` : `all:${firstSeq}`;
|
|
1088
|
+
}
|
|
1089
|
+
__name(allGroup, "allGroup");
|
|
1090
|
+
function allWait(runId, firstSeq, wave) {
|
|
1091
|
+
const base = allBase(runId, firstSeq, wave);
|
|
1092
|
+
return {
|
|
1093
|
+
waiterPrefix: `child:${base}`,
|
|
1094
|
+
checkpointPrefix: `signal:child:${base}`
|
|
1095
|
+
};
|
|
1096
|
+
}
|
|
1097
|
+
__name(allWait, "allWait");
|
|
1098
|
+
function fanJoin(waits) {
|
|
1099
|
+
return {
|
|
1100
|
+
waits
|
|
1101
|
+
};
|
|
1102
|
+
}
|
|
1103
|
+
__name(fanJoin, "fanJoin");
|
|
1104
|
+
var FOLDED_PREFIX = "fan:";
|
|
1105
|
+
function allFoldedName(runId, firstSeq, wave) {
|
|
1106
|
+
return `${FOLDED_PREFIX}${allBase(runId, firstSeq, wave)}`;
|
|
1107
|
+
}
|
|
1108
|
+
__name(allFoldedName, "allFoldedName");
|
|
1109
|
+
function foldedChildIds(checkpoint) {
|
|
1110
|
+
const base = checkpoint.name.slice(FOLDED_PREFIX.length);
|
|
1111
|
+
const { from, values } = checkpoint.output;
|
|
1112
|
+
return values.map((_, j) => `${base}${from + j}`);
|
|
1113
|
+
}
|
|
1114
|
+
__name(foldedChildIds, "foldedChildIds");
|
|
1115
|
+
function allBase(runId, firstSeq, wave) {
|
|
1116
|
+
return wave === void 0 ? `${runId}.all.${firstSeq}.` : `${runId}.all.${firstSeq}.w${wave}.`;
|
|
1117
|
+
}
|
|
1118
|
+
__name(allBase, "allBase");
|
|
1119
|
+
function allWaves(size, concurrency) {
|
|
1120
|
+
if (concurrency === void 0 || concurrency >= size) return void 0;
|
|
1121
|
+
if (!Number.isInteger(concurrency) || concurrency < 1) {
|
|
1122
|
+
throw new Error(`ctx.all: concurrency must be a positive integer, got ${concurrency}`);
|
|
1123
|
+
}
|
|
1124
|
+
if (concurrency === 1) return {
|
|
1125
|
+
waveSize: 1,
|
|
1126
|
+
maxWaves: 1
|
|
1127
|
+
};
|
|
1128
|
+
return {
|
|
1129
|
+
waveSize: Math.floor(concurrency / 2),
|
|
1130
|
+
maxWaves: 2
|
|
1131
|
+
};
|
|
1132
|
+
}
|
|
1133
|
+
__name(allWaves, "allWaves");
|
|
1134
|
+
var ENGINE_CHILD_ID = /^(.+)\.(?:all\.\d+\.(?:w\d+\.)?\d+|child\.\d+)$/;
|
|
1135
|
+
function parentRunIdOf(runId) {
|
|
1136
|
+
return ENGINE_CHILD_ID.exec(runId)?.[1];
|
|
1137
|
+
}
|
|
1138
|
+
__name(parentRunIdOf, "parentRunIdOf");
|
|
1139
|
+
var GROUP = /^all:(\d+)(:failFast)?$/;
|
|
1140
|
+
var REST = /^(?:w(\d+)\.)?\d+$/;
|
|
1141
|
+
function fanOfWaiter(waiter) {
|
|
1142
|
+
const group = waiter.parallelGroup ? GROUP.exec(waiter.parallelGroup) : null;
|
|
1143
|
+
if (!group) return void 0;
|
|
1144
|
+
const base = `child:${waiter.runId}.all.${group[1]}.`;
|
|
1145
|
+
if (!waiter.token.startsWith(base)) return void 0;
|
|
1146
|
+
const rest = REST.exec(waiter.token.slice(base.length));
|
|
1147
|
+
if (!rest) return void 0;
|
|
1148
|
+
const waiterPrefix = rest[1] === void 0 ? base : `${base}w${rest[1]}.`;
|
|
1149
|
+
return {
|
|
1150
|
+
waiterPrefix,
|
|
1151
|
+
failFast: group[2] !== void 0
|
|
1152
|
+
};
|
|
1153
|
+
}
|
|
1154
|
+
__name(fanOfWaiter, "fanOfWaiter");
|
|
1155
|
+
|
|
1063
1156
|
// src/handshake/descriptor.ts
|
|
1064
1157
|
var CURRENT_PROTOCOL_VERSION = 1;
|
|
1065
1158
|
var LEGACY_V1_PROTOCOL = {
|
|
@@ -1801,6 +1894,23 @@ function resolveRunWaiting(run, waiterByRun) {
|
|
|
1801
1894
|
return waiter ? classifyWaiterToken(waiter.token) : void 0;
|
|
1802
1895
|
}
|
|
1803
1896
|
__name(resolveRunWaiting, "resolveRunWaiting");
|
|
1897
|
+
var RUN_ID_BATCH = 500;
|
|
1898
|
+
async function listSignalWaitersOfRuns(store, runIds) {
|
|
1899
|
+
const unique = [
|
|
1900
|
+
...new Set(runIds)
|
|
1901
|
+
];
|
|
1902
|
+
if (unique.length === 0) return [];
|
|
1903
|
+
if (!store.listSignalWaitersByRun) {
|
|
1904
|
+
const ids = new Set(unique);
|
|
1905
|
+
return (await store.listSignalWaiters("")).filter((w) => ids.has(w.runId));
|
|
1906
|
+
}
|
|
1907
|
+
const out = [];
|
|
1908
|
+
for (let at = 0; at < unique.length; at += RUN_ID_BATCH) {
|
|
1909
|
+
out.push(...await store.listSignalWaitersByRun(unique.slice(at, at + RUN_ID_BATCH)));
|
|
1910
|
+
}
|
|
1911
|
+
return out;
|
|
1912
|
+
}
|
|
1913
|
+
__name(listSignalWaitersOfRuns, "listSignalWaitersOfRuns");
|
|
1804
1914
|
function indexWaitersByRun(waiters) {
|
|
1805
1915
|
const byRun = /* @__PURE__ */ new Map();
|
|
1806
1916
|
for (const w of waiters) if (!byRun.has(w.runId)) byRun.set(w.runId, w);
|
|
@@ -2611,16 +2721,6 @@ var TransportPool = class {
|
|
|
2611
2721
|
}
|
|
2612
2722
|
};
|
|
2613
2723
|
|
|
2614
|
-
// src/completion.ts
|
|
2615
|
-
function unwrapCompletion(payload, label) {
|
|
2616
|
-
const c = payload;
|
|
2617
|
-
if (c && typeof c === "object" && "ok" in c && c.ok === false) {
|
|
2618
|
-
throw new FatalError(`${label} failed: ${c.error}`);
|
|
2619
|
-
}
|
|
2620
|
-
return c?.value;
|
|
2621
|
-
}
|
|
2622
|
-
__name(unwrapCompletion, "unwrapCompletion");
|
|
2623
|
-
|
|
2624
2724
|
// src/step-name-symbol.ts
|
|
2625
2725
|
var DURABLE_STEP_NAME = Symbol.for("durable.step.name");
|
|
2626
2726
|
var DURABLE_STEP_CONFIG = Symbol.for("durable.step.config");
|
|
@@ -2642,6 +2742,8 @@ function stepConfigOf(ref) {
|
|
|
2642
2742
|
__name(stepConfigOf, "stepConfigOf");
|
|
2643
2743
|
|
|
2644
2744
|
// src/workflow-ctx.ts
|
|
2745
|
+
var FAN_CHUNK = 500;
|
|
2746
|
+
var FOLD_MIN_FAN = 100;
|
|
2645
2747
|
function normalizeChildOptions(options) {
|
|
2646
2748
|
return typeof options === "string" ? {
|
|
2647
2749
|
childId: options
|
|
@@ -2984,7 +3086,7 @@ function createWorkflowCtx(host, runId, compensations, workflow = "", searchAttr
|
|
|
2984
3086
|
if (existing && existing.status === "completed") {
|
|
2985
3087
|
return unwrapCompletion(existing.output, `child "${id}"`);
|
|
2986
3088
|
}
|
|
2987
|
-
await store.putSignalWaiter({
|
|
3089
|
+
if (!existing) await store.putSignalWaiter({
|
|
2988
3090
|
token: `child:${id}`,
|
|
2989
3091
|
runId,
|
|
2990
3092
|
seq: current
|
|
@@ -3015,16 +3117,33 @@ function createWorkflowCtx(host, runId, compensations, workflow = "", searchAttr
|
|
|
3015
3117
|
status: "running"
|
|
3016
3118
|
}));
|
|
3017
3119
|
}
|
|
3018
|
-
throw new WorkflowSuspended(
|
|
3120
|
+
throw new WorkflowSuspended(void 0, fanJoin([
|
|
3121
|
+
{
|
|
3122
|
+
waiterPrefix: `child:${id}`,
|
|
3123
|
+
checkpointPrefix: `signal:child:${id}`
|
|
3124
|
+
}
|
|
3125
|
+
]));
|
|
3019
3126
|
}, "child");
|
|
3020
3127
|
const all = /* @__PURE__ */ __name(async (workflow2, inputs, opts) => {
|
|
3021
3128
|
if (inputs.length === 0) return [];
|
|
3022
3129
|
const mode = opts?.mode ?? "waitAll";
|
|
3023
3130
|
const name = workflowName(workflow2);
|
|
3131
|
+
const waves = allWaves(inputs.length, opts?.concurrency);
|
|
3024
3132
|
const positions = inputs.map(() => pos.next());
|
|
3025
|
-
const
|
|
3026
|
-
const
|
|
3027
|
-
const
|
|
3133
|
+
const firstSeq = positions[0];
|
|
3134
|
+
const group = allGroup(firstSeq, mode);
|
|
3135
|
+
const waveOf = /* @__PURE__ */ __name((i) => waves ? Math.floor(i / waves.waveSize) : void 0, "waveOf");
|
|
3136
|
+
const id = /* @__PURE__ */ __name((i) => allChildId(runId, firstSeq, i, waveOf(i)), "id");
|
|
3137
|
+
const { checkpoints: existing, folded } = await readFan(positions, [
|
|
3138
|
+
allWait(runId, firstSeq).checkpointPrefix,
|
|
3139
|
+
allFoldedName(runId, firstSeq)
|
|
3140
|
+
]);
|
|
3141
|
+
const waveCount = waves ? Math.ceil(inputs.length / waves.waveSize) : 1;
|
|
3142
|
+
const waveItems = /* @__PURE__ */ __name((k) => {
|
|
3143
|
+
if (!waves) return inputs.map((_, i) => i);
|
|
3144
|
+
const from = k * waves.waveSize;
|
|
3145
|
+
return inputs.map((_, i) => i).slice(from, from + waves.waveSize);
|
|
3146
|
+
}, "waveItems");
|
|
3028
3147
|
if (mode === "failFast") {
|
|
3029
3148
|
for (let i = 0; i < inputs.length; i += 1) {
|
|
3030
3149
|
const cp = existing[i];
|
|
@@ -3043,32 +3162,91 @@ function createWorkflowCtx(host, runId, compensations, workflow = "", searchAttr
|
|
|
3043
3162
|
}
|
|
3044
3163
|
}
|
|
3045
3164
|
}
|
|
3046
|
-
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
if (!cp) {
|
|
3061
|
-
await writeCheckpoint(instantCheckpoint({
|
|
3165
|
+
if (store.compactCheckpoints && inputs.length >= FOLD_MIN_FAN) {
|
|
3166
|
+
for (let k = 0; k < waveCount; k += 1) {
|
|
3167
|
+
const items = waveItems(k);
|
|
3168
|
+
const first = items[0];
|
|
3169
|
+
const last = items[items.length - 1];
|
|
3170
|
+
if (folded[first]) continue;
|
|
3171
|
+
const values = [];
|
|
3172
|
+
for (const i of items) {
|
|
3173
|
+
const c = existing[i]?.status === "completed" ? existing[i]?.output : void 0;
|
|
3174
|
+
if (!c || typeof c !== "object" || c.ok !== true) break;
|
|
3175
|
+
values.push(c.value);
|
|
3176
|
+
}
|
|
3177
|
+
if (values.length < items.length) continue;
|
|
3178
|
+
const into = instantCheckpoint({
|
|
3062
3179
|
runId,
|
|
3063
|
-
seq,
|
|
3064
|
-
name:
|
|
3180
|
+
seq: positions[first],
|
|
3181
|
+
name: allFoldedName(runId, firstSeq, waves ? k : void 0),
|
|
3065
3182
|
kind: "signal",
|
|
3066
|
-
|
|
3183
|
+
output: {
|
|
3184
|
+
from: first,
|
|
3185
|
+
values
|
|
3186
|
+
},
|
|
3067
3187
|
parallelGroup: group
|
|
3188
|
+
});
|
|
3189
|
+
await store.compactCheckpoints(runId, positions[first], positions[last], into);
|
|
3190
|
+
for (const i of items) replay?.delete(positions[i]);
|
|
3191
|
+
replay?.set(into.seq, into);
|
|
3192
|
+
}
|
|
3193
|
+
}
|
|
3194
|
+
const sent = /* @__PURE__ */ __name((k) => waveItems(k).some((i) => existing[i]), "sent");
|
|
3195
|
+
const settled = /* @__PURE__ */ __name((k) => waveItems(k).every((i) => existing[i]?.status === "completed"), "settled");
|
|
3196
|
+
const active = [];
|
|
3197
|
+
let unsettled = 0;
|
|
3198
|
+
for (let k = 0; k < waveCount; k += 1) {
|
|
3199
|
+
if (settled(k)) continue;
|
|
3200
|
+
if (sent(k) || unsettled < (waves?.maxWaves ?? 1)) {
|
|
3201
|
+
active.push(k);
|
|
3202
|
+
unsettled += 1;
|
|
3203
|
+
}
|
|
3204
|
+
}
|
|
3205
|
+
if (active.length > 0) {
|
|
3206
|
+
const toRegister = [];
|
|
3207
|
+
const toEnsure = [];
|
|
3208
|
+
for (const k of active) {
|
|
3209
|
+
for (const i of waveItems(k)) {
|
|
3210
|
+
const cp = existing[i];
|
|
3211
|
+
if (cp?.status === "completed") continue;
|
|
3212
|
+
(cp ? toEnsure : toRegister).push(i);
|
|
3213
|
+
}
|
|
3214
|
+
}
|
|
3215
|
+
for (let at = 0; at < toRegister.length; at += FAN_CHUNK) {
|
|
3216
|
+
const chunk = toRegister.slice(at, at + FAN_CHUNK);
|
|
3217
|
+
const entries = chunk.map((i) => ({
|
|
3218
|
+
// Carry the fan `group` onto the waiter too: the child's terminal `signal:child:`
|
|
3219
|
+
// checkpoint is (re)written by engine.signal when the child notifies the parent,
|
|
3220
|
+
// OVERWRITING the running placeholder at the same seq — so without this the resolved
|
|
3221
|
+
// checkpoint would lose the group and the dashboard would render the resolved fan as a
|
|
3222
|
+
// sequential chain.
|
|
3223
|
+
waiter: {
|
|
3224
|
+
token: `child:${id(i)}`,
|
|
3225
|
+
runId,
|
|
3226
|
+
seq: positions[i],
|
|
3227
|
+
parallelGroup: group
|
|
3228
|
+
},
|
|
3229
|
+
checkpoint: instantCheckpoint({
|
|
3230
|
+
runId,
|
|
3231
|
+
seq: positions[i],
|
|
3232
|
+
name: `signal:child:${id(i)}`,
|
|
3233
|
+
kind: "signal",
|
|
3234
|
+
status: "running",
|
|
3235
|
+
parallelGroup: group
|
|
3236
|
+
})
|
|
3237
|
+
}));
|
|
3238
|
+
await registerChildWaits(entries);
|
|
3239
|
+
for (const i of chunk) host.startChild(name, inputs[i], id(i));
|
|
3240
|
+
}
|
|
3241
|
+
for (let at = 0; at < toEnsure.length; at += FAN_CHUNK) {
|
|
3242
|
+
await Promise.all(toEnsure.slice(at, at + FAN_CHUNK).map(async (i) => {
|
|
3243
|
+
if (!await store.getRun(id(i))) host.startChild(name, inputs[i], id(i));
|
|
3068
3244
|
}));
|
|
3069
3245
|
}
|
|
3070
3246
|
}
|
|
3071
|
-
if (
|
|
3247
|
+
if (active.length > 0) {
|
|
3248
|
+
throw new WorkflowSuspended(void 0, fanJoin(active.map((k) => allWait(runId, firstSeq, waves ? k : void 0))));
|
|
3249
|
+
}
|
|
3072
3250
|
const outputs = [];
|
|
3073
3251
|
const failures = [];
|
|
3074
3252
|
for (let i = 0; i < inputs.length; i += 1) {
|
|
@@ -3087,6 +3265,67 @@ function createWorkflowCtx(host, runId, compensations, workflow = "", searchAttr
|
|
|
3087
3265
|
if (failures.length > 0) throw new GatherError(failures, inputs.length);
|
|
3088
3266
|
return outputs;
|
|
3089
3267
|
}, "all");
|
|
3268
|
+
const readFan = /* @__PURE__ */ __name(async (positions, prefixes) => {
|
|
3269
|
+
const checkpoints = positions.map((seq) => replay?.get(seq) ?? null);
|
|
3270
|
+
const folded = positions.map(() => false);
|
|
3271
|
+
const expand = /* @__PURE__ */ __name(() => {
|
|
3272
|
+
for (let i = 0; i < checkpoints.length; i += 1) {
|
|
3273
|
+
const cp = checkpoints[i];
|
|
3274
|
+
if (!cp?.name.startsWith(FOLDED_PREFIX) || folded[i]) continue;
|
|
3275
|
+
const fold = cp.output;
|
|
3276
|
+
if (!fold || typeof fold.from !== "number" || !Array.isArray(fold.values)) {
|
|
3277
|
+
throw new Error(`ctx.all: folded checkpoint "${cp.name}" at seq ${cp.seq} is malformed`);
|
|
3278
|
+
}
|
|
3279
|
+
const { from, values } = fold;
|
|
3280
|
+
values.forEach((value, j) => {
|
|
3281
|
+
checkpoints[from + j] = {
|
|
3282
|
+
...cp,
|
|
3283
|
+
seq: positions[from + j],
|
|
3284
|
+
output: {
|
|
3285
|
+
ok: true,
|
|
3286
|
+
value
|
|
3287
|
+
}
|
|
3288
|
+
};
|
|
3289
|
+
folded[from + j] = true;
|
|
3290
|
+
});
|
|
3291
|
+
}
|
|
3292
|
+
}, "expand");
|
|
3293
|
+
expand();
|
|
3294
|
+
const missing = positions.filter((_, i) => !checkpoints[i]);
|
|
3295
|
+
if (missing.length === 0) return {
|
|
3296
|
+
checkpoints,
|
|
3297
|
+
folded
|
|
3298
|
+
};
|
|
3299
|
+
if (missing.length > 1 && store.listCheckpointsByNamePrefix) {
|
|
3300
|
+
const bySeq = /* @__PURE__ */ new Map();
|
|
3301
|
+
for (const cp of await store.listCheckpointsByNamePrefix(runId, prefixes)) {
|
|
3302
|
+
bySeq.set(cp.seq, cp);
|
|
3303
|
+
}
|
|
3304
|
+
positions.forEach((seq, i) => {
|
|
3305
|
+
checkpoints[i] ??= bySeq.get(seq) ?? null;
|
|
3306
|
+
});
|
|
3307
|
+
} else {
|
|
3308
|
+
await Promise.all(positions.map(async (seq, i) => {
|
|
3309
|
+
checkpoints[i] ??= await readCheckpoint(seq);
|
|
3310
|
+
}));
|
|
3311
|
+
}
|
|
3312
|
+
expand();
|
|
3313
|
+
return {
|
|
3314
|
+
checkpoints,
|
|
3315
|
+
folded
|
|
3316
|
+
};
|
|
3317
|
+
}, "readFan");
|
|
3318
|
+
const registerChildWaits = /* @__PURE__ */ __name(async (entries) => {
|
|
3319
|
+
if (store.registerChildWaits) {
|
|
3320
|
+
await store.registerChildWaits(entries);
|
|
3321
|
+
} else {
|
|
3322
|
+
await Promise.all(entries.map(async (e) => {
|
|
3323
|
+
await store.putSignalWaiter(e.waiter);
|
|
3324
|
+
await store.saveCheckpoint(e.checkpoint);
|
|
3325
|
+
}));
|
|
3326
|
+
}
|
|
3327
|
+
for (const e of entries) replay?.set(e.checkpoint.seq, e.checkpoint);
|
|
3328
|
+
}, "registerChildWaits");
|
|
3090
3329
|
const startChild = /* @__PURE__ */ __name(async (workflow2, input, options) => {
|
|
3091
3330
|
const { childId, priority, version } = normalizeChildOptions(options);
|
|
3092
3331
|
const current = pos.next();
|
|
@@ -4126,10 +4365,8 @@ var WorkflowEngine = class {
|
|
|
4126
4365
|
*/
|
|
4127
4366
|
async findParentRunId(childRunId) {
|
|
4128
4367
|
const token = `child:${childRunId}`;
|
|
4129
|
-
|
|
4130
|
-
|
|
4131
|
-
}
|
|
4132
|
-
return void 0;
|
|
4368
|
+
const waiters = await this.store.listSignalWaiters(token);
|
|
4369
|
+
return waiters.find((waiter) => waiter.token === token)?.runId;
|
|
4133
4370
|
}
|
|
4134
4371
|
/** Track an in-flight execution so {@link drain} can wait for it. */
|
|
4135
4372
|
track(p) {
|
|
@@ -4350,7 +4587,10 @@ var WorkflowEngine = class {
|
|
|
4350
4587
|
const due = await this.store.listDueTimers(nowMs, this.namespace);
|
|
4351
4588
|
const blocked = await this.dueBlockedRuns(nowMs);
|
|
4352
4589
|
if (due.length === 0 && blocked.length === 0) return [];
|
|
4353
|
-
const waiterByRun = indexWaitersByRun(await this.store
|
|
4590
|
+
const waiterByRun = indexWaitersByRun(await listSignalWaitersOfRuns(this.store, [
|
|
4591
|
+
...due,
|
|
4592
|
+
...blocked
|
|
4593
|
+
].map((run) => run.id)));
|
|
4354
4594
|
return this.resumeLeased([
|
|
4355
4595
|
...due,
|
|
4356
4596
|
...blocked
|
|
@@ -4460,11 +4700,6 @@ var WorkflowEngine = class {
|
|
|
4460
4700
|
} : {}
|
|
4461
4701
|
});
|
|
4462
4702
|
} else if (cp.kind === "signal" && cp.name.startsWith("signal:child:") && cp.status === "completed" && isFailureCompletion(cp.output)) {
|
|
4463
|
-
await this.store.saveCheckpoint({
|
|
4464
|
-
...cp,
|
|
4465
|
-
status: "running",
|
|
4466
|
-
output: void 0
|
|
4467
|
-
});
|
|
4468
4703
|
const childId = cp.name.slice("signal:child:".length);
|
|
4469
4704
|
const parked = [];
|
|
4470
4705
|
for (; ; ) {
|
|
@@ -4472,8 +4707,28 @@ var WorkflowEngine = class {
|
|
|
4472
4707
|
if (!buffered) break;
|
|
4473
4708
|
parked.push(buffered.payload);
|
|
4474
4709
|
}
|
|
4710
|
+
const successAt = parked.findIndex((payload) => typeof payload === "object" && payload !== null && payload.ok === true);
|
|
4711
|
+
const success = successAt === -1 ? void 0 : parked.splice(successAt, 1)[0];
|
|
4475
4712
|
for (const payload of parked) await this.store.bufferSignal(`child:${childId}`, payload);
|
|
4476
|
-
const bufferedSuccess =
|
|
4713
|
+
const bufferedSuccess = success !== void 0;
|
|
4714
|
+
if (bufferedSuccess) {
|
|
4715
|
+
await this.store.saveCheckpoint({
|
|
4716
|
+
...cp,
|
|
4717
|
+
output: success
|
|
4718
|
+
});
|
|
4719
|
+
} else {
|
|
4720
|
+
await this.store.putSignalWaiter({
|
|
4721
|
+
token: `child:${childId}`,
|
|
4722
|
+
runId,
|
|
4723
|
+
seq: cp.seq,
|
|
4724
|
+
parallelGroup: cp.parallelGroup
|
|
4725
|
+
});
|
|
4726
|
+
await this.store.saveCheckpoint({
|
|
4727
|
+
...cp,
|
|
4728
|
+
status: "running",
|
|
4729
|
+
output: void 0
|
|
4730
|
+
});
|
|
4731
|
+
}
|
|
4477
4732
|
if (!bufferedSuccess) {
|
|
4478
4733
|
const child = await this.store.getRun(childId);
|
|
4479
4734
|
if (child && (child.status === "failed" || child.status === "dead")) {
|
|
@@ -4753,12 +5008,62 @@ var WorkflowEngine = class {
|
|
|
4753
5008
|
// for an ordinary (non-fan) signal/child await.
|
|
4754
5009
|
parallelGroup: waiter.parallelGroup
|
|
4755
5010
|
}));
|
|
5011
|
+
if (await this.fanStillWaiting(waiter, payload)) {
|
|
5012
|
+
return {
|
|
5013
|
+
runId: waiter.runId,
|
|
5014
|
+
status: "suspended"
|
|
5015
|
+
};
|
|
5016
|
+
}
|
|
4756
5017
|
return this.resumeImplicit(waiter.runId).catch((err) => {
|
|
4757
5018
|
if (err instanceof NamespaceMismatch) return null;
|
|
4758
5019
|
throw err;
|
|
4759
5020
|
});
|
|
4760
5021
|
}
|
|
4761
5022
|
/**
|
|
5023
|
+
* The `ctx.all` join barrier: true when `waiter` was one child of a fan (or of one wave of it)
|
|
5024
|
+
* whose siblings are still out, so waking the parent now would only replay the fan and re-suspend.
|
|
5025
|
+
* Skipping that wake is what keeps a fan of N children at O(N) work instead of N replays of N items.
|
|
5026
|
+
*
|
|
5027
|
+
* Safe because the LAST sibling to settle always wakes the parent: each child consumes its own
|
|
5028
|
+
* waiter before it asks, so whichever asks last sees none left. A failed child of a failFast fan
|
|
5029
|
+
* always wakes it. Anything that is not a `ctx.all` waiter, or a store that cannot answer the
|
|
5030
|
+
* question cheaply, wakes the parent as before.
|
|
5031
|
+
*/
|
|
5032
|
+
async fanStillWaiting(waiter, payload) {
|
|
5033
|
+
if (!this.store.hasSignalWaiter) return false;
|
|
5034
|
+
const fan = fanOfWaiter(waiter);
|
|
5035
|
+
if (!fan) return false;
|
|
5036
|
+
if (fan.failFast && isFailedCompletion(payload)) return false;
|
|
5037
|
+
return this.store.hasSignalWaiter(waiter.runId, fan.waiterPrefix);
|
|
5038
|
+
}
|
|
5039
|
+
/**
|
|
5040
|
+
* Re-drive a run that just suspended on awaited children (a `ctx.all` fan or wave, a `ctx.child`)
|
|
5041
|
+
* which, by now, have settled.
|
|
5042
|
+
*
|
|
5043
|
+
* {@link execute} drops a wake that finds the run's lease held. So if a child settles while the
|
|
5044
|
+
* parent is still executing (the turn that dispatched it, or one woken by an earlier child), that
|
|
5045
|
+
* wake is spent, and the turn suspends on a snapshot that predates the child. With the `ctx.all`
|
|
5046
|
+
* barrier that wake was the only one the fan would get. Nothing would wake the run until the
|
|
5047
|
+
* `reconcileMs` sweep, minutes later.
|
|
5048
|
+
*
|
|
5049
|
+
* Called after the turn released its lease, so any child that settles from here on wakes the run
|
|
5050
|
+
* itself. Only re-drives for a group with no waiter left whose checkpoints are all settled, which
|
|
5051
|
+
* the next turn is then guaranteed to see — so it cannot loop.
|
|
5052
|
+
*/
|
|
5053
|
+
async redriveSettledChildren(runId, fan) {
|
|
5054
|
+
if (!this.store.hasSignalWaiter) return;
|
|
5055
|
+
for (const wait of fan.waits) {
|
|
5056
|
+
if (await this.store.hasSignalWaiter(runId, wait.waiterPrefix)) continue;
|
|
5057
|
+
const checkpoints = this.store.listCheckpointsByNamePrefix ? await this.store.listCheckpointsByNamePrefix(runId, [
|
|
5058
|
+
wait.checkpointPrefix
|
|
5059
|
+
]) : (await this.store.listCheckpoints(runId)).filter((cp) => cp.name.startsWith(wait.checkpointPrefix));
|
|
5060
|
+
if (checkpoints.length > 0 && checkpoints.every((cp) => cp.status === "completed")) {
|
|
5061
|
+
await this.resumeImplicit(runId);
|
|
5062
|
+
return;
|
|
5063
|
+
}
|
|
5064
|
+
}
|
|
5065
|
+
}
|
|
5066
|
+
/**
|
|
4762
5067
|
* Deliver an external signal to the run waiting on `token`, resume it with `payload`, and return
|
|
4763
5068
|
* the run result — or null if no run is (or ends up) waiting for it.
|
|
4764
5069
|
*
|
|
@@ -5051,23 +5356,11 @@ var WorkflowEngine = class {
|
|
|
5051
5356
|
}
|
|
5052
5357
|
/** The given runs plus every descendant, children before parents. One waiter scan for the batch. */
|
|
5053
5358
|
async collectSubtrees(rootIds) {
|
|
5054
|
-
const
|
|
5055
|
-
for (const w of await this.store.listSignalWaiters("child:")) {
|
|
5056
|
-
const list = waitersByParent.get(w.runId) ?? [];
|
|
5057
|
-
list.push(w.token.slice("child:".length));
|
|
5058
|
-
waitersByParent.set(w.runId, list);
|
|
5059
|
-
}
|
|
5060
|
-
const prefixes = [
|
|
5061
|
-
"signal:child:",
|
|
5062
|
-
"spawn:"
|
|
5063
|
-
];
|
|
5359
|
+
const prefixes = CHILD_EDGE_PREFIXES;
|
|
5064
5360
|
const childrenOf = /* @__PURE__ */ __name(async (parent) => {
|
|
5065
|
-
const ids = new Set(
|
|
5361
|
+
const ids = new Set(await this.awaitedChildIds(parent));
|
|
5066
5362
|
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)));
|
|
5067
|
-
for (const
|
|
5068
|
-
if (cp.name.startsWith("signal:child:")) ids.add(cp.name.slice("signal:child:".length));
|
|
5069
|
-
if (cp.name.startsWith("spawn:") && typeof cp.output === "string") ids.add(cp.output);
|
|
5070
|
-
}
|
|
5363
|
+
for (const id of childIdsOf(cps)) ids.add(id);
|
|
5071
5364
|
return [
|
|
5072
5365
|
...ids
|
|
5073
5366
|
];
|
|
@@ -5117,22 +5410,20 @@ var WorkflowEngine = class {
|
|
|
5117
5410
|
* so a completed parent (or a completed awaited child) would otherwise drop out of the tree. The
|
|
5118
5411
|
* `signal:child:<id>` checkpoint (the placeholder written when the parent first awaits the child,
|
|
5119
5412
|
* overwritten as completed/failed when it settles) persists across completion, so we read both and
|
|
5120
|
-
* dedupe — the edge stays stable for a finished run, not just a live one.
|
|
5413
|
+
* dedupe — the edge stays stable for a finished run, not just a live one. A settled `ctx.all`
|
|
5414
|
+
* group folded into one `fan:` checkpoint stands for every child in it.
|
|
5121
5415
|
*/
|
|
5416
|
+
/** The children `parentRunId` is awaiting right now, read off its own live `child:` waiters. */
|
|
5417
|
+
async awaitedChildIds(parentRunId) {
|
|
5418
|
+
return (await listSignalWaitersOfRuns(this.store, [
|
|
5419
|
+
parentRunId
|
|
5420
|
+
])).filter((w) => w.token.startsWith("child:")).map((w) => w.token.slice("child:".length));
|
|
5421
|
+
}
|
|
5122
5422
|
async getRunChildren(parentRunId) {
|
|
5123
|
-
const childIds =
|
|
5124
|
-
|
|
5125
|
-
if (w.runId === parentRunId) childIds.add(w.token.slice("child:".length));
|
|
5126
|
-
}
|
|
5127
|
-
const prefixes = [
|
|
5128
|
-
"signal:child:",
|
|
5129
|
-
"spawn:"
|
|
5130
|
-
];
|
|
5423
|
+
const childIds = new Set(await this.awaitedChildIds(parentRunId));
|
|
5424
|
+
const prefixes = CHILD_EDGE_PREFIXES;
|
|
5131
5425
|
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)));
|
|
5132
|
-
for (const
|
|
5133
|
-
if (cp.name.startsWith("signal:child:")) childIds.add(cp.name.slice("signal:child:".length));
|
|
5134
|
-
if (cp.name.startsWith("spawn:") && typeof cp.output === "string") childIds.add(cp.output);
|
|
5135
|
-
}
|
|
5426
|
+
for (const id of childIdsOf(childCheckpoints)) childIds.add(id);
|
|
5136
5427
|
return [
|
|
5137
5428
|
...childIds
|
|
5138
5429
|
];
|
|
@@ -6212,6 +6503,7 @@ var WorkflowEngine = class {
|
|
|
6212
6503
|
const snapshot = await this.store.listCheckpoints(run.id);
|
|
6213
6504
|
const replay = /* @__PURE__ */ new Map();
|
|
6214
6505
|
for (const cp of snapshot) replay.set(cp.seq, cp);
|
|
6506
|
+
let suspendedOnFan;
|
|
6215
6507
|
const ctx = createWorkflowCtx(this.ctxHostFor(replay, run.namespace), run.id, compensations, run.workflow, registered?.searchAttributesSchema);
|
|
6216
6508
|
try {
|
|
6217
6509
|
const output = await runInWorkflowCtx(ctx, () => fn(ctx, run.input));
|
|
@@ -6263,10 +6555,12 @@ var WorkflowEngine = class {
|
|
|
6263
6555
|
if (this.cancelRequested.has(run.id)) {
|
|
6264
6556
|
return await this.compensateAndCancel(run, compensations, replay);
|
|
6265
6557
|
}
|
|
6266
|
-
|
|
6558
|
+
const suspended = await this.settleRun(run, {
|
|
6267
6559
|
kind: "suspended",
|
|
6268
6560
|
wakeAt: err.wakeAt
|
|
6269
6561
|
});
|
|
6562
|
+
suspendedOnFan = err.fan;
|
|
6563
|
+
return suspended;
|
|
6270
6564
|
}
|
|
6271
6565
|
if (this.cancelRequested.has(run.id)) {
|
|
6272
6566
|
return await this.compensateAndCancel(run, compensations, replay);
|
|
@@ -6286,6 +6580,9 @@ var WorkflowEngine = class {
|
|
|
6286
6580
|
});
|
|
6287
6581
|
} finally {
|
|
6288
6582
|
await this.store.releaseRunLock(run.id);
|
|
6583
|
+
if (suspendedOnFan) {
|
|
6584
|
+
void this.redriveSettledChildren(run.id, suspendedOnFan).catch(() => void 0);
|
|
6585
|
+
}
|
|
6289
6586
|
}
|
|
6290
6587
|
}
|
|
6291
6588
|
/**
|
|
@@ -7100,6 +7397,21 @@ var RemoteStepError = class extends Error {
|
|
|
7100
7397
|
this.stepError = stepError;
|
|
7101
7398
|
}
|
|
7102
7399
|
};
|
|
7400
|
+
var CHILD_EDGE_PREFIXES = [
|
|
7401
|
+
"signal:child:",
|
|
7402
|
+
FOLDED_PREFIX,
|
|
7403
|
+
"spawn:"
|
|
7404
|
+
];
|
|
7405
|
+
function childIdsOf(checkpoints) {
|
|
7406
|
+
const ids = [];
|
|
7407
|
+
for (const cp of checkpoints) {
|
|
7408
|
+
if (cp.name.startsWith("signal:child:")) ids.push(cp.name.slice("signal:child:".length));
|
|
7409
|
+
if (cp.name.startsWith(FOLDED_PREFIX)) ids.push(...foldedChildIds(cp));
|
|
7410
|
+
if (cp.name.startsWith("spawn:") && typeof cp.output === "string") ids.push(cp.output);
|
|
7411
|
+
}
|
|
7412
|
+
return ids;
|
|
7413
|
+
}
|
|
7414
|
+
__name(childIdsOf, "childIdsOf");
|
|
7103
7415
|
|
|
7104
7416
|
// src/run-facets.ts
|
|
7105
7417
|
function facetOrigin(origin) {
|
|
@@ -7249,6 +7561,14 @@ var CodecStateStore = class {
|
|
|
7249
7561
|
/** Forwarded verbatim — a count reads no payload. Bound only when the inner store counts, so the
|
|
7250
7562
|
* engine's facet/listing fallback still applies through the wrapper. */
|
|
7251
7563
|
countRuns;
|
|
7564
|
+
/** The placeholder checkpoints are encoded like any other checkpoint. */
|
|
7565
|
+
registerChildWaits;
|
|
7566
|
+
/** The folded checkpoint carries the children's outputs, so it is encoded. */
|
|
7567
|
+
compactCheckpoints;
|
|
7568
|
+
/** Forwarded verbatim — a waiter carries no payload. */
|
|
7569
|
+
listSignalWaitersByRun;
|
|
7570
|
+
/** Forwarded verbatim — a waiter carries no payload. */
|
|
7571
|
+
hasSignalWaiter;
|
|
7252
7572
|
/** Schedules persist their run `input` inside `spec` — encoded like any other payload. Bound only
|
|
7253
7573
|
* when the inner store persists schedules, so `engine.schedules` reports absence faithfully. */
|
|
7254
7574
|
saveSchedule;
|
|
@@ -7266,6 +7586,17 @@ var CodecStateStore = class {
|
|
|
7266
7586
|
if (prune) this.pruneTerminalRuns = (policy, now, limit) => prune.call(inner, policy, now, limit);
|
|
7267
7587
|
const count = inner.countRuns;
|
|
7268
7588
|
if (count) this.countRuns = (query) => count.call(inner, query);
|
|
7589
|
+
const registerChildWaits = inner.registerChildWaits;
|
|
7590
|
+
if (registerChildWaits) this.registerChildWaits = (entries) => registerChildWaits.call(inner, entries.map((e) => ({
|
|
7591
|
+
waiter: e.waiter,
|
|
7592
|
+
checkpoint: this.encCp(e.checkpoint)
|
|
7593
|
+
})));
|
|
7594
|
+
const compact = inner.compactCheckpoints;
|
|
7595
|
+
if (compact) this.compactCheckpoints = (runId, fromSeq, toSeq, into) => compact.call(inner, runId, fromSeq, toSeq, this.encCp(into));
|
|
7596
|
+
const byRun = inner.listSignalWaitersByRun;
|
|
7597
|
+
if (byRun) this.listSignalWaitersByRun = (runIds) => byRun.call(inner, runIds);
|
|
7598
|
+
const hasWaiter = inner.hasSignalWaiter;
|
|
7599
|
+
if (hasWaiter) this.hasSignalWaiter = (runId, tokenPrefix) => hasWaiter.call(inner, runId, tokenPrefix);
|
|
7269
7600
|
const { saveSchedule, getSchedule, updateSchedule, deleteSchedule, listSchedules } = inner;
|
|
7270
7601
|
if (saveSchedule && getSchedule && updateSchedule && deleteSchedule && listSchedules) {
|
|
7271
7602
|
this.saveSchedule = (r) => saveSchedule.call(inner, this.encSchedule(r));
|
|
@@ -7876,6 +8207,33 @@ var InMemoryStateStore = class {
|
|
|
7876
8207
|
...w
|
|
7877
8208
|
}));
|
|
7878
8209
|
}
|
|
8210
|
+
async registerChildWaits(entries) {
|
|
8211
|
+
for (const { waiter } of entries) this.signalWaiters.set(waiter.token, {
|
|
8212
|
+
...waiter
|
|
8213
|
+
});
|
|
8214
|
+
for (const { checkpoint } of entries) await this.saveCheckpoint(checkpoint);
|
|
8215
|
+
}
|
|
8216
|
+
async compactCheckpoints(runId, fromSeq, toSeq, into) {
|
|
8217
|
+
for (let seq = fromSeq; seq <= toSeq; seq += 1) this.checkpoints.delete(this.key(runId, seq));
|
|
8218
|
+
this.checkpoints.set(this.key(runId, into.seq), {
|
|
8219
|
+
...into,
|
|
8220
|
+
runId
|
|
8221
|
+
});
|
|
8222
|
+
}
|
|
8223
|
+
async listSignalWaitersByRun(runIds) {
|
|
8224
|
+
const ids = new Set(runIds);
|
|
8225
|
+
return [
|
|
8226
|
+
...this.signalWaiters.values()
|
|
8227
|
+
].filter((w) => ids.has(w.runId)).map((w) => ({
|
|
8228
|
+
...w
|
|
8229
|
+
}));
|
|
8230
|
+
}
|
|
8231
|
+
async hasSignalWaiter(runId, tokenPrefix) {
|
|
8232
|
+
for (const w of this.signalWaiters.values()) {
|
|
8233
|
+
if (w.runId === runId && w.token.startsWith(tokenPrefix)) return true;
|
|
8234
|
+
}
|
|
8235
|
+
return false;
|
|
8236
|
+
}
|
|
7879
8237
|
async removeSignalWaiter(waiter) {
|
|
7880
8238
|
const current = this.signalWaiters.get(waiter.token);
|
|
7881
8239
|
if (current && current.runId === waiter.runId && current.seq === waiter.seq) {
|
|
@@ -8230,6 +8588,7 @@ export {
|
|
|
8230
8588
|
isLegacyDescriptor,
|
|
8231
8589
|
isNonEmptyRunScope,
|
|
8232
8590
|
isWorkflowControlFlowSignal,
|
|
8591
|
+
listSignalWaitersOfRuns,
|
|
8233
8592
|
log,
|
|
8234
8593
|
matchesAttributes,
|
|
8235
8594
|
mergeRunFacetRows,
|
|
@@ -8240,6 +8599,7 @@ export {
|
|
|
8240
8599
|
normalizeAttributeRows,
|
|
8241
8600
|
normalizeDescriptor,
|
|
8242
8601
|
observedAnnouncements,
|
|
8602
|
+
parentRunIdOf,
|
|
8243
8603
|
parseDuration,
|
|
8244
8604
|
partitionOfToken,
|
|
8245
8605
|
planDispatch,
|