@dudousxd/nestjs-durable-core 0.63.0 → 0.65.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 +362 -14
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +492 -127
- package/dist/index.d.ts +492 -127
- package/dist/index.js +355 -14
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -481,6 +481,57 @@ function backoffDelay(attempt, options) {
|
|
|
481
481
|
}
|
|
482
482
|
__name(backoffDelay, "backoffDelay");
|
|
483
483
|
|
|
484
|
+
// src/execution-hooks.ts
|
|
485
|
+
var wrappers = [];
|
|
486
|
+
function useDurableExecution(wrapper) {
|
|
487
|
+
wrappers.push(wrapper);
|
|
488
|
+
return () => {
|
|
489
|
+
const i = wrappers.indexOf(wrapper);
|
|
490
|
+
if (i >= 0) wrappers.splice(i, 1);
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
__name(useDurableExecution, "useDurableExecution");
|
|
494
|
+
function clearDurableExecutionWrappers() {
|
|
495
|
+
wrappers.length = 0;
|
|
496
|
+
}
|
|
497
|
+
__name(clearDurableExecutionWrappers, "clearDurableExecutionWrappers");
|
|
498
|
+
function hasDurableExecutionWrappers() {
|
|
499
|
+
return wrappers.length > 0;
|
|
500
|
+
}
|
|
501
|
+
__name(hasDurableExecutionWrappers, "hasDurableExecutionWrappers");
|
|
502
|
+
async function runDurableExecution(execution, body) {
|
|
503
|
+
if (wrappers.length === 0) return body();
|
|
504
|
+
let outcome;
|
|
505
|
+
const inner = /* @__PURE__ */ __name(async () => {
|
|
506
|
+
try {
|
|
507
|
+
const value = await body();
|
|
508
|
+
outcome = {
|
|
509
|
+
ok: true,
|
|
510
|
+
value
|
|
511
|
+
};
|
|
512
|
+
return value;
|
|
513
|
+
} catch (error2) {
|
|
514
|
+
outcome = {
|
|
515
|
+
ok: false,
|
|
516
|
+
error: error2
|
|
517
|
+
};
|
|
518
|
+
throw error2;
|
|
519
|
+
}
|
|
520
|
+
}, "inner");
|
|
521
|
+
const chain = wrappers.reduceRight((next, wrapper) => () => wrapper(execution, next), inner);
|
|
522
|
+
try {
|
|
523
|
+
await chain();
|
|
524
|
+
} catch (error2) {
|
|
525
|
+
if (outcome === void 0) throw error2;
|
|
526
|
+
}
|
|
527
|
+
if (outcome === void 0) {
|
|
528
|
+
throw new Error("a durable execution wrapper returned without calling next() \u2014 the unit of work never ran");
|
|
529
|
+
}
|
|
530
|
+
if (outcome.ok) return outcome.value;
|
|
531
|
+
throw outcome.error;
|
|
532
|
+
}
|
|
533
|
+
__name(runDurableExecution, "runDurableExecution");
|
|
534
|
+
|
|
484
535
|
// src/step-logger.ts
|
|
485
536
|
function withDuration(start, now, data) {
|
|
486
537
|
if (data && typeof data === "object" && "durationMs" in data) {
|
|
@@ -622,7 +673,14 @@ async function runStepHandler(task, handler) {
|
|
|
622
673
|
} : result, "withEvents");
|
|
623
674
|
try {
|
|
624
675
|
const logger = createStepLogger(events, Date.now);
|
|
625
|
-
const output = await
|
|
676
|
+
const output = await runDurableExecution({
|
|
677
|
+
unit: "step",
|
|
678
|
+
runId: task.runId,
|
|
679
|
+
workflow: void 0,
|
|
680
|
+
name: task.name,
|
|
681
|
+
seq: task.seq,
|
|
682
|
+
attempt: task.attempt
|
|
683
|
+
}, async () => runInStepLogger(logger, () => handler(task.input, logger)));
|
|
626
684
|
return withEvents({
|
|
627
685
|
...base,
|
|
628
686
|
status: "completed",
|
|
@@ -923,6 +981,9 @@ function normalizeDescriptor(raw) {
|
|
|
923
981
|
],
|
|
924
982
|
workflows: raw.workflows ?? [],
|
|
925
983
|
steps: raw.steps ?? [],
|
|
984
|
+
...raw.registrations !== void 0 ? {
|
|
985
|
+
registrations: raw.registrations
|
|
986
|
+
} : {},
|
|
926
987
|
...raw.partition !== void 0 ? {
|
|
927
988
|
partition: raw.partition
|
|
928
989
|
} : {},
|
|
@@ -933,6 +994,23 @@ function normalizeDescriptor(raw) {
|
|
|
933
994
|
};
|
|
934
995
|
}
|
|
935
996
|
__name(normalizeDescriptor, "normalizeDescriptor");
|
|
997
|
+
function canonicalizeRegistrations(regs) {
|
|
998
|
+
const canonical = regs.map((r) => ({
|
|
999
|
+
name: r.name,
|
|
1000
|
+
version: r.version ?? null,
|
|
1001
|
+
group: r.group ?? null,
|
|
1002
|
+
requires: r.requires ? [
|
|
1003
|
+
...new Set(r.requires)
|
|
1004
|
+
].sort() : null,
|
|
1005
|
+
origin: r.origin ?? null
|
|
1006
|
+
}));
|
|
1007
|
+
const byJson = /* @__PURE__ */ new Map();
|
|
1008
|
+
for (const entry of canonical) byJson.set(JSON.stringify(entry), entry);
|
|
1009
|
+
return [
|
|
1010
|
+
...byJson.keys()
|
|
1011
|
+
].sort().map((k) => byJson.get(k));
|
|
1012
|
+
}
|
|
1013
|
+
__name(canonicalizeRegistrations, "canonicalizeRegistrations");
|
|
936
1014
|
function canonicalizeForHash(d) {
|
|
937
1015
|
const set = /* @__PURE__ */ __name((xs) => [
|
|
938
1016
|
...new Set(xs)
|
|
@@ -954,6 +1032,9 @@ function canonicalizeForHash(d) {
|
|
|
954
1032
|
capabilities: set(d.capabilities),
|
|
955
1033
|
workflows: set(d.workflows),
|
|
956
1034
|
steps: set(d.steps),
|
|
1035
|
+
...d.registrations !== void 0 ? {
|
|
1036
|
+
registrations: canonicalizeRegistrations(d.registrations)
|
|
1037
|
+
} : {},
|
|
957
1038
|
partition: d.partition ?? null,
|
|
958
1039
|
namespace: d.namespace ?? null,
|
|
959
1040
|
startedAt: d.startedAt
|
|
@@ -986,6 +1067,144 @@ function heartbeatStatus(descriptor, opts = {}) {
|
|
|
986
1067
|
};
|
|
987
1068
|
}
|
|
988
1069
|
__name(heartbeatStatus, "heartbeatStatus");
|
|
1070
|
+
function describeWorker(opts) {
|
|
1071
|
+
const version = CURRENT_PROTOCOL_VERSION;
|
|
1072
|
+
const registrations = [
|
|
1073
|
+
...opts.registrations ?? []
|
|
1074
|
+
].sort((a, b) => `${a.name}@${a.version ?? ""}`.localeCompare(`${b.name}@${b.version ?? ""}`));
|
|
1075
|
+
return {
|
|
1076
|
+
instanceId: opts.instanceId,
|
|
1077
|
+
runtime: opts.runtime,
|
|
1078
|
+
sdk: {
|
|
1079
|
+
name: opts.sdk.name,
|
|
1080
|
+
version: opts.sdk.version
|
|
1081
|
+
},
|
|
1082
|
+
protocol: {
|
|
1083
|
+
version,
|
|
1084
|
+
range: [
|
|
1085
|
+
version,
|
|
1086
|
+
version
|
|
1087
|
+
]
|
|
1088
|
+
},
|
|
1089
|
+
capabilities: [
|
|
1090
|
+
...LEGACY_V1_CAPABILITIES
|
|
1091
|
+
],
|
|
1092
|
+
workflows: [
|
|
1093
|
+
...opts.workflows ?? []
|
|
1094
|
+
].sort(),
|
|
1095
|
+
steps: [
|
|
1096
|
+
...opts.steps
|
|
1097
|
+
].sort(),
|
|
1098
|
+
...registrations.length > 0 ? {
|
|
1099
|
+
registrations
|
|
1100
|
+
} : {},
|
|
1101
|
+
...opts.partition ? {
|
|
1102
|
+
partition: opts.partition
|
|
1103
|
+
} : {},
|
|
1104
|
+
...opts.namespace && opts.namespace !== "default" ? {
|
|
1105
|
+
namespace: opts.namespace
|
|
1106
|
+
} : {},
|
|
1107
|
+
startedAt: opts.startedAt
|
|
1108
|
+
};
|
|
1109
|
+
}
|
|
1110
|
+
__name(describeWorker, "describeWorker");
|
|
1111
|
+
|
|
1112
|
+
// src/handshake/announced.ts
|
|
1113
|
+
function announcementsOf(raw) {
|
|
1114
|
+
const d = normalizeDescriptor(raw);
|
|
1115
|
+
const identity = {
|
|
1116
|
+
instanceId: d.instanceId,
|
|
1117
|
+
runtime: d.runtime
|
|
1118
|
+
};
|
|
1119
|
+
const announcements = (d.registrations ?? []).map((reg) => ({
|
|
1120
|
+
...reg,
|
|
1121
|
+
...identity
|
|
1122
|
+
}));
|
|
1123
|
+
const covered = new Set(announcements.map((a) => a.name));
|
|
1124
|
+
for (const name of d.workflows) {
|
|
1125
|
+
if (!covered.has(name)) announcements.push({
|
|
1126
|
+
name,
|
|
1127
|
+
...identity
|
|
1128
|
+
});
|
|
1129
|
+
}
|
|
1130
|
+
return announcements;
|
|
1131
|
+
}
|
|
1132
|
+
__name(announcementsOf, "announcementsOf");
|
|
1133
|
+
function announcementKey(a) {
|
|
1134
|
+
return a.version === void 0 ? a.name : `${a.name}@${a.version}`;
|
|
1135
|
+
}
|
|
1136
|
+
__name(announcementKey, "announcementKey");
|
|
1137
|
+
function stated(values) {
|
|
1138
|
+
return [
|
|
1139
|
+
...new Set(values.filter((v) => v !== void 0))
|
|
1140
|
+
].sort();
|
|
1141
|
+
}
|
|
1142
|
+
__name(stated, "stated");
|
|
1143
|
+
function aggregateAnnouncements(descriptors) {
|
|
1144
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
1145
|
+
for (const descriptor of descriptors) {
|
|
1146
|
+
for (const announcement of announcementsOf(descriptor)) {
|
|
1147
|
+
const key = announcementKey(announcement);
|
|
1148
|
+
const bucket = byKey.get(key);
|
|
1149
|
+
if (bucket) bucket.push(announcement);
|
|
1150
|
+
else byKey.set(key, [
|
|
1151
|
+
announcement
|
|
1152
|
+
]);
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
const out = [];
|
|
1156
|
+
for (const [key, announcements] of byKey) {
|
|
1157
|
+
const groups = stated(announcements.map((a) => a.group));
|
|
1158
|
+
const origins = stated(announcements.map((a) => a.origin));
|
|
1159
|
+
const requiresByJson = /* @__PURE__ */ new Map();
|
|
1160
|
+
for (const a of announcements) {
|
|
1161
|
+
if (a.requires === void 0) continue;
|
|
1162
|
+
const canonical = [
|
|
1163
|
+
...new Set(a.requires)
|
|
1164
|
+
].sort();
|
|
1165
|
+
requiresByJson.set(JSON.stringify(canonical), canonical);
|
|
1166
|
+
}
|
|
1167
|
+
const requires = [
|
|
1168
|
+
...requiresByJson.values()
|
|
1169
|
+
].sort((x, y) => x.join(",").localeCompare(y.join(",")));
|
|
1170
|
+
const disagreements = [];
|
|
1171
|
+
if (groups.length > 1) disagreements.push({
|
|
1172
|
+
axis: "group",
|
|
1173
|
+
values: groups
|
|
1174
|
+
});
|
|
1175
|
+
if (origins.length > 1) disagreements.push({
|
|
1176
|
+
axis: "origin",
|
|
1177
|
+
values: origins
|
|
1178
|
+
});
|
|
1179
|
+
if (requires.length > 1) {
|
|
1180
|
+
disagreements.push({
|
|
1181
|
+
axis: "requires",
|
|
1182
|
+
values: requires.map((r) => r.join(","))
|
|
1183
|
+
});
|
|
1184
|
+
}
|
|
1185
|
+
const first = announcements[0];
|
|
1186
|
+
if (!first) continue;
|
|
1187
|
+
out.push({
|
|
1188
|
+
key,
|
|
1189
|
+
name: first.name,
|
|
1190
|
+
...first.version !== void 0 ? {
|
|
1191
|
+
version: first.version
|
|
1192
|
+
} : {},
|
|
1193
|
+
groups,
|
|
1194
|
+
origins,
|
|
1195
|
+
requires,
|
|
1196
|
+
runtimes: [
|
|
1197
|
+
...new Set(announcements.map((a) => a.runtime))
|
|
1198
|
+
].sort(),
|
|
1199
|
+
instances: [
|
|
1200
|
+
...new Set(announcements.map((a) => a.instanceId))
|
|
1201
|
+
].sort(),
|
|
1202
|
+
disagreements
|
|
1203
|
+
});
|
|
1204
|
+
}
|
|
1205
|
+
return out.sort((a, b) => a.key.localeCompare(b.key));
|
|
1206
|
+
}
|
|
1207
|
+
__name(aggregateAnnouncements, "aggregateAnnouncements");
|
|
989
1208
|
|
|
990
1209
|
// src/handshake/negotiate.ts
|
|
991
1210
|
var rangesOverlap = /* @__PURE__ */ __name((a, b) => Math.max(a[0], b[0]) <= Math.min(a[1], b[1]), "rangesOverlap");
|
|
@@ -1513,6 +1732,30 @@ var TransportPool = class {
|
|
|
1513
1732
|
}
|
|
1514
1733
|
return descriptors;
|
|
1515
1734
|
}
|
|
1735
|
+
/**
|
|
1736
|
+
* Every live descriptor in the deployment, merged across every transport that can report them and
|
|
1737
|
+
* de-duplicated by `instanceId` — one worker publishes the same descriptor under each token it
|
|
1738
|
+
* consumes, and it must count once. Returns `[]` when no transport implements
|
|
1739
|
+
* {@link Transport.readAllWorkerDescriptors} (a pure in-process pool advertises nothing).
|
|
1740
|
+
*
|
|
1741
|
+
* First writer wins on a duplicate id: the copies are the same bytes by construction (one process,
|
|
1742
|
+
* one descriptor, published N times), so there is nothing to choose between. Two DIFFERENT
|
|
1743
|
+
* processes sharing an `instanceId` would collapse into one here — that is a misconfiguration the
|
|
1744
|
+
* fleet already suffers from (they also share heartbeat and descriptor keys, so they overwrite each
|
|
1745
|
+
* other's advertisement), not something this read can repair.
|
|
1746
|
+
*/
|
|
1747
|
+
async readAllWorkerDescriptors() {
|
|
1748
|
+
const byInstance = /* @__PURE__ */ new Map();
|
|
1749
|
+
for (const { transport } of this.transports) {
|
|
1750
|
+
if (!transport.readAllWorkerDescriptors) continue;
|
|
1751
|
+
for (const descriptor of await transport.readAllWorkerDescriptors()) {
|
|
1752
|
+
if (!byInstance.has(descriptor.instanceId)) byInstance.set(descriptor.instanceId, descriptor);
|
|
1753
|
+
}
|
|
1754
|
+
}
|
|
1755
|
+
return [
|
|
1756
|
+
...byInstance.values()
|
|
1757
|
+
];
|
|
1758
|
+
}
|
|
1516
1759
|
/** Distinct worker groups with a live heartbeat, merged across every transport that can report it. */
|
|
1517
1760
|
async listWorkerGroups() {
|
|
1518
1761
|
const groups = /* @__PURE__ */ new Set();
|
|
@@ -1913,7 +2156,7 @@ function createWorkflowCtx(host, runId, compensations, workflow = "", searchAttr
|
|
|
1913
2156
|
return unwrapCompletion(await waitForSignal(`task:${runId}:${name}`), `task "${name}"`);
|
|
1914
2157
|
}, "task");
|
|
1915
2158
|
const child = /* @__PURE__ */ __name(async (workflow2, input, options) => {
|
|
1916
|
-
const { childId, priority } = normalizeChildOptions(options);
|
|
2159
|
+
const { childId, priority, version } = normalizeChildOptions(options);
|
|
1917
2160
|
const current = pos.next();
|
|
1918
2161
|
const id = childId ?? `${runId}.child.${current}`;
|
|
1919
2162
|
const existing = await readCheckpoint(current);
|
|
@@ -1941,7 +2184,7 @@ function createWorkflowCtx(host, runId, compensations, workflow = "", searchAttr
|
|
|
1941
2184
|
}));
|
|
1942
2185
|
return unwrapCompletion(buffered.payload, `child "${id}"`);
|
|
1943
2186
|
}
|
|
1944
|
-
if (!await store.getRun(id)) host.startChild(workflowName(workflow2), input, id, priority);
|
|
2187
|
+
if (!await store.getRun(id)) host.startChild(workflowName(workflow2), input, id, priority, version);
|
|
1945
2188
|
if (!existing) {
|
|
1946
2189
|
await writeCheckpoint(instantCheckpoint({
|
|
1947
2190
|
runId,
|
|
@@ -2024,12 +2267,12 @@ function createWorkflowCtx(host, runId, compensations, workflow = "", searchAttr
|
|
|
2024
2267
|
return outputs;
|
|
2025
2268
|
}, "all");
|
|
2026
2269
|
const startChild = /* @__PURE__ */ __name(async (workflow2, input, options) => {
|
|
2027
|
-
const { childId, priority } = normalizeChildOptions(options);
|
|
2270
|
+
const { childId, priority, version } = normalizeChildOptions(options);
|
|
2028
2271
|
const current = pos.next();
|
|
2029
2272
|
const id = childId ?? `${runId}.child.${current}`;
|
|
2030
2273
|
const existing = await readCheckpoint(current);
|
|
2031
2274
|
if (existing && existing.status === "completed") return existing.output;
|
|
2032
|
-
if (!await store.getRun(id)) host.startChild(workflowName(workflow2), input, id, priority);
|
|
2275
|
+
if (!await store.getRun(id)) host.startChild(workflowName(workflow2), input, id, priority, version);
|
|
2033
2276
|
await writeCheckpoint(instantCheckpoint({
|
|
2034
2277
|
runId,
|
|
2035
2278
|
seq: current,
|
|
@@ -2589,6 +2832,15 @@ var WorkflowEngine = class {
|
|
|
2589
2832
|
const reg = this.workflows.get(versionKey(name, version));
|
|
2590
2833
|
return reg?.hasBody ? reg.fn : void 0;
|
|
2591
2834
|
}
|
|
2835
|
+
/** The registered versions of `name`, for the pinned-start error — "2 is not registered" is a
|
|
2836
|
+
* guess-the-typo error without them, and the answer (which versions this deploy actually carries)
|
|
2837
|
+
* is right here. `none` when the name is unknown entirely. */
|
|
2838
|
+
registeredVersions(name) {
|
|
2839
|
+
const versions = [
|
|
2840
|
+
...this.workflows.values()
|
|
2841
|
+
].filter((w) => w.name === name).map((w) => w.version);
|
|
2842
|
+
return versions.length ? versions.join(", ") : "none";
|
|
2843
|
+
}
|
|
2592
2844
|
/**
|
|
2593
2845
|
* Register a workflow whose body runs in another SDK (e.g. Python). The engine owns the run exactly
|
|
2594
2846
|
* as for a TS workflow — it persists checkpoints, recovers, runs timers — but advances it by handing
|
|
@@ -2738,12 +2990,13 @@ var WorkflowEngine = class {
|
|
|
2738
2990
|
}
|
|
2739
2991
|
async start(workflow, input, runId, opts) {
|
|
2740
2992
|
const name = workflowName(workflow);
|
|
2741
|
-
|
|
2742
|
-
|
|
2993
|
+
const pinned = opts?.version;
|
|
2994
|
+
let registered = pinned === void 0 ? this.latest.get(name) : this.workflows.get(versionKey(name, pinned));
|
|
2995
|
+
if (!registered && pinned === void 0) {
|
|
2743
2996
|
const ancestor = await this.findRemoteAncestor(runId);
|
|
2744
2997
|
if (ancestor) registered = this.synthesizeRemoteChild(name, ancestor.version, ancestor);
|
|
2745
2998
|
}
|
|
2746
|
-
if (!registered) {
|
|
2999
|
+
if (!registered && pinned === void 0) {
|
|
2747
3000
|
const conventionRun = {
|
|
2748
3001
|
id: runId,
|
|
2749
3002
|
workflow: name,
|
|
@@ -2760,7 +3013,9 @@ var WorkflowEngine = class {
|
|
|
2760
3013
|
};
|
|
2761
3014
|
registered = await this.resolveRemoteByConvention(conventionRun);
|
|
2762
3015
|
}
|
|
2763
|
-
if (!registered)
|
|
3016
|
+
if (!registered) {
|
|
3017
|
+
throw new Error(pinned === void 0 ? `workflow ${name} is not registered` : `workflow ${versionKey(name, pinned)} is not registered \u2014 a pinned start never falls back to the newest version (registered: ${this.registeredVersions(name)})`);
|
|
3018
|
+
}
|
|
2764
3019
|
await registered.validateInput?.(input);
|
|
2765
3020
|
const prior = await this.store.getRun(runId);
|
|
2766
3021
|
if (prior) {
|
|
@@ -2967,7 +3222,32 @@ var WorkflowEngine = class {
|
|
|
2967
3222
|
/**
|
|
2968
3223
|
* Cancel in-flight runs that have outlived their workflow's `executionTimeout`. Call it from the
|
|
2969
3224
|
* timer poller alongside {@link resumeDueTimers}. A timed-out run is moved to `cancelled` with an
|
|
2970
|
-
* `execution_timeout` error (terminal, so a late step result can't resurrect it)
|
|
3225
|
+
* `execution_timeout` error (terminal, so a late step result can't resurrect it), AND its children
|
|
3226
|
+
* are cascaded exactly as an explicit {@link cancel} cascades them.
|
|
3227
|
+
*
|
|
3228
|
+
* WHY THE TERMINAL WRITE IS DIRECT, not a call to {@link cancel}. Two reasons, both still standing:
|
|
3229
|
+
* the run must land with the `execution_timeout` error `cancel` knows nothing about (`cancel` writes
|
|
3230
|
+
* a plain `cancelled`, and the code is what a dashboard/alert distinguishes a timeout by), and this
|
|
3231
|
+
* is a SCAN — it walks every in-flight run of every timed workflow on every poller tick, so it must
|
|
3232
|
+
* not pay `cancel`'s per-run re-read and singleton/control-plane work on runs it isn't cancelling.
|
|
3233
|
+
* The cascade below is charged only to the runs that ACTUALLY timed out, which are rare by
|
|
3234
|
+
* construction; the scan itself is unchanged.
|
|
3235
|
+
*
|
|
3236
|
+
* The subtree, though, has no reason to differ, and before this it did: a timed-out parent's
|
|
3237
|
+
* children kept running with the parent gone and nothing pointing at them — a silent orphan you
|
|
3238
|
+
* could only find by reading the runs table by hand. {@link cancelChildren} is the SAME recursive
|
|
3239
|
+
* walk `cancel` uses, so children of children go too, and it inherits that path's two guards:
|
|
3240
|
+
* `cancel` returns early on a run that is already `completed`/`cancelled`/`dead` (a child that beat
|
|
3241
|
+
* the sweep to the finish is never clobbered, and a re-cancel is a no-op), and it writes the
|
|
3242
|
+
* parent's terminal status BEFORE recursing, so a cyclic parent→child graph terminates on the
|
|
3243
|
+
* second visit instead of looping. Two pollers sweeping the same run concurrently therefore write
|
|
3244
|
+
* the same terminal state twice and cascade into already-cancelled children — idempotent, not
|
|
3245
|
+
* corrupting. The one window it does NOT close is the pre-existing check-then-write race an
|
|
3246
|
+
* explicit `cancel` has always had (a child that completes between `cancel`'s read and its write);
|
|
3247
|
+
* closing that needs a compare-and-set in the store contract, not a change here.
|
|
3248
|
+
*
|
|
3249
|
+
* Cascade failures are swallowed per subtree for the same reason `cancelChildren` swallows them per
|
|
3250
|
+
* child: one unreachable child must not abort the sweep and leave every LATER timed-out run alive.
|
|
2971
3251
|
*/
|
|
2972
3252
|
async sweepTimeouts(now = this.clock()) {
|
|
2973
3253
|
for (const reg of new Set(this.latest.values())) {
|
|
@@ -3003,6 +3283,7 @@ var WorkflowEngine = class {
|
|
|
3003
3283
|
namespace: run.namespace,
|
|
3004
3284
|
error: error2
|
|
3005
3285
|
});
|
|
3286
|
+
await this.cancelChildren(run.id).catch(() => void 0);
|
|
3006
3287
|
}
|
|
3007
3288
|
}
|
|
3008
3289
|
}
|
|
@@ -3731,6 +4012,10 @@ var WorkflowEngine = class {
|
|
|
3731
4012
|
* `child:<id>` waiter) and fire-and-forget (`ctx.startChild`, found via its `spawn:<id>`
|
|
3732
4013
|
* checkpoint). Recursive, so a whole subtree is cancelled; the terminal guard in `cancel` stops it
|
|
3733
4014
|
* at finished / already-cancelled runs (no loops, no re-cancel).
|
|
4015
|
+
*
|
|
4016
|
+
* Two callers, deliberately the same walk: an explicit {@link cancel}, and {@link sweepTimeouts}
|
|
4017
|
+
* when a parent hits its `executionTimeout` — a timed-out parent's subtree dies the same way an
|
|
4018
|
+
* explicitly cancelled one's does, or the children are orphans nobody is holding a reference to.
|
|
3734
4019
|
*/
|
|
3735
4020
|
async cancelChildren(parentRunId, opts) {
|
|
3736
4021
|
for (const id of await this.getRunChildren(parentRunId)) {
|
|
@@ -3810,6 +4095,29 @@ var WorkflowEngine = class {
|
|
|
3810
4095
|
return out;
|
|
3811
4096
|
}
|
|
3812
4097
|
/**
|
|
4098
|
+
* What workflows the LIVE fleet announces it can execute — the deployment-wide registry a picker
|
|
4099
|
+
* ("call this existing workflow") needs, and the one question this engine's own registry cannot
|
|
4100
|
+
* answer. {@link workflowBody} answers only for this process, and a missing body is ambiguous: it
|
|
4101
|
+
* means "not registered here", but equally "registered via {@link registerRemote} against another
|
|
4102
|
+
* SDK" or "a group resolved by convention against a worker". An inferred answer would therefore
|
|
4103
|
+
* differ per replica. This one does not: every pod folds the same published announcements.
|
|
4104
|
+
*
|
|
4105
|
+
* Scoped like every other poll surface — an engine WITH a `namespace` sees only workers in it, and
|
|
4106
|
+
* an operator (namespace unset) sees the whole deployment ("ver tudo = ausência de namespace"). A
|
|
4107
|
+
* descriptor that states no namespace is `default`, matching how a single-tenant fleet publishes.
|
|
4108
|
+
*
|
|
4109
|
+
* Cost: one advertisement scan per CALL, on demand. Nothing is added to the poll loop and nothing
|
|
4110
|
+
* is retained — no pod holds a table of the fleet's registrations. Empty when no transport can
|
|
4111
|
+
* introspect the advertisement keyspace (a pure in-process transport advertises nothing).
|
|
4112
|
+
*
|
|
4113
|
+
* Workflows only, and deliberately: see the "steps are out of scope" note in `handshake/announced`.
|
|
4114
|
+
*/
|
|
4115
|
+
async announcedWorkflows() {
|
|
4116
|
+
const descriptors = await this.pool.readAllWorkerDescriptors();
|
|
4117
|
+
const mine = this.namespace === void 0 ? descriptors : descriptors.filter((d) => (d.namespace ?? "default") === this.namespace);
|
|
4118
|
+
return aggregateAnnouncements(mine);
|
|
4119
|
+
}
|
|
4120
|
+
/**
|
|
3813
4121
|
* The routing groups of every in-flight run's still-`pending` remote step — the queues where work is
|
|
3814
4122
|
* dispatched and awaiting a worker. Folded into {@link workerHealth} so a step waiting on a group
|
|
3815
4123
|
* that is NEITHER a registration NOR heartbeating (the exact shape of a run stuck on an offline
|
|
@@ -4524,8 +4832,31 @@ var WorkflowEngine = class {
|
|
|
4524
4832
|
error: error2
|
|
4525
4833
|
};
|
|
4526
4834
|
}
|
|
4527
|
-
/**
|
|
4528
|
-
|
|
4835
|
+
/**
|
|
4836
|
+
* The run body, lease held + renewed by {@link execute}, wrapped in the registered
|
|
4837
|
+
* {@link runDurableExecution} wrappers so an observability scope covers ONE WHOLE TURN.
|
|
4838
|
+
*
|
|
4839
|
+
* The wrap goes here — around the entire turn — rather than tightly around the `fn(ctx, input)`
|
|
4840
|
+
* call, because the store writes that bracket the body (the `pending` → `running` transition, the
|
|
4841
|
+
* step checkpoints, the terminal `settleRun` update) are exactly the queries an operator wants
|
|
4842
|
+
* correlated to the turn that issued them. Narrowing the wrap to the body would leave them
|
|
4843
|
+
* uncorrelated for the sake of a purity that buys nothing.
|
|
4844
|
+
*
|
|
4845
|
+
* Only the TS path is wrapped. {@link runRemoteExecution} does not execute a body at all — it
|
|
4846
|
+
* dispatches a workflow task and awaits the worker's decision — so a scope opened there would
|
|
4847
|
+
* describe a dispatch, not an execution, and would mislead anyone reading the trace.
|
|
4848
|
+
*/
|
|
4849
|
+
runExecution(run, fn, registered) {
|
|
4850
|
+
return runDurableExecution({
|
|
4851
|
+
unit: "workflow",
|
|
4852
|
+
runId: run.id,
|
|
4853
|
+
workflow: run.workflow,
|
|
4854
|
+
name: void 0,
|
|
4855
|
+
seq: void 0,
|
|
4856
|
+
attempt: void 0
|
|
4857
|
+
}, () => this.runExecutionTurn(run, fn, registered));
|
|
4858
|
+
}
|
|
4859
|
+
async runExecutionTurn(run, fn, registered) {
|
|
4529
4860
|
if (run.status === "cancelling") this.cancelRequested.add(run.id);
|
|
4530
4861
|
if (run.status === "pending") {
|
|
4531
4862
|
await this.store.updateRun(run.id, {
|
|
@@ -4865,10 +5196,13 @@ var WorkflowEngine = class {
|
|
|
4865
5196
|
// namespace) is the one executing the parent. Otherwise an operator recovery-resuming a
|
|
4866
5197
|
// `davi-local` pipeline would stamp its `processing` child `default` and it would leak off the
|
|
4867
5198
|
// tenant's worker pool onto the shared/dev workers.
|
|
4868
|
-
startChild: /* @__PURE__ */ __name((workflow, input, id, priority) => {
|
|
5199
|
+
startChild: /* @__PURE__ */ __name((workflow, input, id, priority, version) => {
|
|
4869
5200
|
this.startChildDeferred(workflow, input, id, {
|
|
4870
5201
|
priority,
|
|
4871
|
-
namespace: parentNamespace
|
|
5202
|
+
namespace: parentNamespace,
|
|
5203
|
+
// Absent unless the body pinned one (`ctx.child(ref, input, { version })`) — an omitted
|
|
5204
|
+
// pin must stay omitted, so the child keeps resolving `latest` exactly as before.
|
|
5205
|
+
version
|
|
4872
5206
|
});
|
|
4873
5207
|
}, "startChild"),
|
|
4874
5208
|
// Deferred for the same reentrancy reason as `startChild` above. `cancel()` is already
|
|
@@ -6276,6 +6610,8 @@ export {
|
|
|
6276
6610
|
WorkflowBlocked,
|
|
6277
6611
|
WorkflowEngine,
|
|
6278
6612
|
WorkflowSuspended,
|
|
6613
|
+
aggregateAnnouncements,
|
|
6614
|
+
announcementsOf,
|
|
6279
6615
|
applyAttributeQuery,
|
|
6280
6616
|
attributeColumnFor,
|
|
6281
6617
|
attributeOperand,
|
|
@@ -6284,12 +6620,14 @@ export {
|
|
|
6284
6620
|
canRoute,
|
|
6285
6621
|
capableWorkers,
|
|
6286
6622
|
classifyWaiterToken,
|
|
6623
|
+
clearDurableExecutionWrappers,
|
|
6287
6624
|
collectMetrics,
|
|
6288
6625
|
controlPlaneDescriptor,
|
|
6289
6626
|
createStepLogger,
|
|
6290
6627
|
currentStep,
|
|
6291
6628
|
currentWorkflowCtx,
|
|
6292
6629
|
debug,
|
|
6630
|
+
describeWorker,
|
|
6293
6631
|
descriptorHash,
|
|
6294
6632
|
error,
|
|
6295
6633
|
eventMatchOf,
|
|
@@ -6297,6 +6635,7 @@ export {
|
|
|
6297
6635
|
eventNameOf,
|
|
6298
6636
|
eventPrefix,
|
|
6299
6637
|
eventToken,
|
|
6638
|
+
hasDurableExecutionWrappers,
|
|
6300
6639
|
heartbeatStatus,
|
|
6301
6640
|
indexWaitersByRun,
|
|
6302
6641
|
info,
|
|
@@ -6316,6 +6655,7 @@ export {
|
|
|
6316
6655
|
requiredCapabilities,
|
|
6317
6656
|
resolveRouting,
|
|
6318
6657
|
resolveRunWaiting,
|
|
6658
|
+
runDurableExecution,
|
|
6319
6659
|
runInStepLogger,
|
|
6320
6660
|
runInWorkflowCtx,
|
|
6321
6661
|
runSchedules,
|
|
@@ -6330,6 +6670,7 @@ export {
|
|
|
6330
6670
|
subEvent,
|
|
6331
6671
|
subProcess,
|
|
6332
6672
|
tenantGroup,
|
|
6673
|
+
useDurableExecution,
|
|
6333
6674
|
warn,
|
|
6334
6675
|
workflowName
|
|
6335
6676
|
};
|