@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.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
|
|
3187
|
-
const
|
|
3188
|
-
const
|
|
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
|
-
|
|
3208
|
-
|
|
3209
|
-
|
|
3210
|
-
|
|
3211
|
-
|
|
3212
|
-
|
|
3213
|
-
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
|
|
3217
|
-
|
|
3218
|
-
|
|
3219
|
-
|
|
3220
|
-
|
|
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:
|
|
3343
|
+
seq: positions[first],
|
|
3344
|
+
name: allFoldedName(runId, firstSeq, waves ? k : void 0),
|
|
3226
3345
|
kind: "signal",
|
|
3227
|
-
|
|
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 (
|
|
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
|
-
|
|
4291
|
-
|
|
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
|
|
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 =
|
|
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
|
|
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(
|
|
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
|
|
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 =
|
|
5285
|
-
|
|
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
|
|
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
|
-
|
|
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,
|