@dudousxd/nestjs-durable-core 0.57.0 → 0.59.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 +538 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +460 -4
- package/dist/index.d.ts +460 -4
- package/dist/index.js +522 -3
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -822,6 +822,358 @@ function eventMatches(payload, match) {
|
|
|
822
822
|
}
|
|
823
823
|
__name(eventMatches, "eventMatches");
|
|
824
824
|
|
|
825
|
+
// src/handshake/descriptor.ts
|
|
826
|
+
var CURRENT_PROTOCOL_VERSION = 1;
|
|
827
|
+
var LEGACY_V1_PROTOCOL = {
|
|
828
|
+
version: 1,
|
|
829
|
+
range: [
|
|
830
|
+
1,
|
|
831
|
+
1
|
|
832
|
+
]
|
|
833
|
+
};
|
|
834
|
+
var LEGACY_V1_CAPABILITIES = Object.freeze([
|
|
835
|
+
"saga",
|
|
836
|
+
"signals",
|
|
837
|
+
"search-attributes",
|
|
838
|
+
"priority",
|
|
839
|
+
"entities",
|
|
840
|
+
"child-workflows",
|
|
841
|
+
"singleton",
|
|
842
|
+
"schedules",
|
|
843
|
+
"continue-as-new",
|
|
844
|
+
"queries",
|
|
845
|
+
"cancellation"
|
|
846
|
+
]);
|
|
847
|
+
function isLegacyDescriptor(raw) {
|
|
848
|
+
return raw.protocol === void 0;
|
|
849
|
+
}
|
|
850
|
+
__name(isLegacyDescriptor, "isLegacyDescriptor");
|
|
851
|
+
function normalizeDescriptor(raw) {
|
|
852
|
+
return {
|
|
853
|
+
instanceId: raw.instanceId,
|
|
854
|
+
runtime: raw.runtime,
|
|
855
|
+
sdk: raw.sdk ?? {
|
|
856
|
+
name: "unknown",
|
|
857
|
+
version: "0"
|
|
858
|
+
},
|
|
859
|
+
protocol: raw.protocol ?? {
|
|
860
|
+
...LEGACY_V1_PROTOCOL
|
|
861
|
+
},
|
|
862
|
+
capabilities: raw.capabilities ?? [
|
|
863
|
+
...LEGACY_V1_CAPABILITIES
|
|
864
|
+
],
|
|
865
|
+
workflows: raw.workflows ?? [],
|
|
866
|
+
steps: raw.steps ?? [],
|
|
867
|
+
...raw.partition !== void 0 ? {
|
|
868
|
+
partition: raw.partition
|
|
869
|
+
} : {},
|
|
870
|
+
...raw.namespace !== void 0 ? {
|
|
871
|
+
namespace: raw.namespace
|
|
872
|
+
} : {},
|
|
873
|
+
startedAt: raw.startedAt ?? 0
|
|
874
|
+
};
|
|
875
|
+
}
|
|
876
|
+
__name(normalizeDescriptor, "normalizeDescriptor");
|
|
877
|
+
function canonicalizeForHash(d) {
|
|
878
|
+
const set = /* @__PURE__ */ __name((xs) => [
|
|
879
|
+
...new Set(xs)
|
|
880
|
+
].sort(), "set");
|
|
881
|
+
return {
|
|
882
|
+
instanceId: d.instanceId,
|
|
883
|
+
runtime: d.runtime,
|
|
884
|
+
sdk: {
|
|
885
|
+
name: d.sdk.name,
|
|
886
|
+
version: d.sdk.version
|
|
887
|
+
},
|
|
888
|
+
protocol: {
|
|
889
|
+
version: d.protocol.version,
|
|
890
|
+
range: [
|
|
891
|
+
d.protocol.range[0],
|
|
892
|
+
d.protocol.range[1]
|
|
893
|
+
]
|
|
894
|
+
},
|
|
895
|
+
capabilities: set(d.capabilities),
|
|
896
|
+
workflows: set(d.workflows),
|
|
897
|
+
steps: set(d.steps),
|
|
898
|
+
partition: d.partition ?? null,
|
|
899
|
+
namespace: d.namespace ?? null,
|
|
900
|
+
startedAt: d.startedAt
|
|
901
|
+
};
|
|
902
|
+
}
|
|
903
|
+
__name(canonicalizeForHash, "canonicalizeForHash");
|
|
904
|
+
function fnv1a64Hex(input) {
|
|
905
|
+
const OFFSET = 0xcbf29ce484222325n;
|
|
906
|
+
const PRIME = 0x100000001b3n;
|
|
907
|
+
const MASK = 0xffffffffffffffffn;
|
|
908
|
+
let hash = OFFSET;
|
|
909
|
+
for (let i = 0; i < input.length; i += 1) {
|
|
910
|
+
hash ^= BigInt(input.charCodeAt(i) & 255);
|
|
911
|
+
hash ^= BigInt(input.charCodeAt(i) >> 8 & 255);
|
|
912
|
+
hash = hash * PRIME & MASK;
|
|
913
|
+
}
|
|
914
|
+
return hash.toString(16).padStart(16, "0");
|
|
915
|
+
}
|
|
916
|
+
__name(fnv1a64Hex, "fnv1a64Hex");
|
|
917
|
+
function descriptorHash(descriptor) {
|
|
918
|
+
const canonical = canonicalizeForHash(normalizeDescriptor(descriptor));
|
|
919
|
+
return fnv1a64Hex(JSON.stringify(canonical));
|
|
920
|
+
}
|
|
921
|
+
__name(descriptorHash, "descriptorHash");
|
|
922
|
+
function heartbeatStatus(descriptor, opts = {}) {
|
|
923
|
+
return {
|
|
924
|
+
ts: opts.ts ?? Date.now(),
|
|
925
|
+
status: opts.status ?? "up",
|
|
926
|
+
descriptorHash: descriptorHash(descriptor)
|
|
927
|
+
};
|
|
928
|
+
}
|
|
929
|
+
__name(heartbeatStatus, "heartbeatStatus");
|
|
930
|
+
|
|
931
|
+
// src/handshake/negotiate.ts
|
|
932
|
+
var rangesOverlap = /* @__PURE__ */ __name((a, b) => Math.max(a[0], b[0]) <= Math.min(a[1], b[1]), "rangesOverlap");
|
|
933
|
+
var difference = /* @__PURE__ */ __name((a, b) => {
|
|
934
|
+
const bset = new Set(b);
|
|
935
|
+
return [
|
|
936
|
+
...new Set(a)
|
|
937
|
+
].filter((x) => !bset.has(x)).sort();
|
|
938
|
+
}, "difference");
|
|
939
|
+
var intersection = /* @__PURE__ */ __name((a, b) => {
|
|
940
|
+
const bset = new Set(b);
|
|
941
|
+
return [
|
|
942
|
+
...new Set(a)
|
|
943
|
+
].filter((x) => bset.has(x)).sort();
|
|
944
|
+
}, "intersection");
|
|
945
|
+
function negotiate(localRaw, remoteRaw, opts = {}) {
|
|
946
|
+
const local = normalizeDescriptor(localRaw);
|
|
947
|
+
const remote = normalizeDescriptor(remoteRaw);
|
|
948
|
+
const localRange = local.protocol.range;
|
|
949
|
+
const remoteRange = remote.protocol.range;
|
|
950
|
+
const shared = intersection(local.capabilities, remote.capabilities);
|
|
951
|
+
const missingOnRemote = difference(local.capabilities, remote.capabilities);
|
|
952
|
+
const missingOnLocal = difference(remote.capabilities, local.capabilities);
|
|
953
|
+
const capabilities = {
|
|
954
|
+
shared,
|
|
955
|
+
missingOnRemote,
|
|
956
|
+
missingOnLocal
|
|
957
|
+
};
|
|
958
|
+
if (!rangesOverlap(localRange, remoteRange)) {
|
|
959
|
+
return {
|
|
960
|
+
outcome: "incompatible",
|
|
961
|
+
negotiatedProtocol: null,
|
|
962
|
+
protocol: {
|
|
963
|
+
localRange,
|
|
964
|
+
remoteRange
|
|
965
|
+
},
|
|
966
|
+
capabilities,
|
|
967
|
+
reason: {
|
|
968
|
+
code: "protocol.incompatible",
|
|
969
|
+
message: `no common protocol major: local speaks [${localRange[0]}, ${localRange[1]}], remote speaks [${remoteRange[0]}, ${remoteRange[1]}]`,
|
|
970
|
+
detail: {
|
|
971
|
+
localRange,
|
|
972
|
+
remoteRange
|
|
973
|
+
}
|
|
974
|
+
},
|
|
975
|
+
local,
|
|
976
|
+
remote
|
|
977
|
+
};
|
|
978
|
+
}
|
|
979
|
+
const negotiatedProtocol = Math.min(localRange[1], remoteRange[1]);
|
|
980
|
+
const required = opts.required ?? [];
|
|
981
|
+
const missingRequired = difference(required, remote.capabilities);
|
|
982
|
+
if (missingRequired.length > 0) {
|
|
983
|
+
return {
|
|
984
|
+
outcome: "degraded",
|
|
985
|
+
negotiatedProtocol,
|
|
986
|
+
protocol: {
|
|
987
|
+
localRange,
|
|
988
|
+
remoteRange
|
|
989
|
+
},
|
|
990
|
+
capabilities,
|
|
991
|
+
reason: {
|
|
992
|
+
code: "capability.unavailable",
|
|
993
|
+
message: `remote is missing required capability(ies): ${missingRequired.join(", ")}`,
|
|
994
|
+
detail: {
|
|
995
|
+
missingRequired,
|
|
996
|
+
missingOnRemote,
|
|
997
|
+
missingOnLocal
|
|
998
|
+
}
|
|
999
|
+
},
|
|
1000
|
+
local,
|
|
1001
|
+
remote
|
|
1002
|
+
};
|
|
1003
|
+
}
|
|
1004
|
+
if (missingOnRemote.length > 0 || missingOnLocal.length > 0) {
|
|
1005
|
+
const parts = [];
|
|
1006
|
+
if (missingOnRemote.length > 0) parts.push(`remote lacks ${missingOnRemote.join(", ")}`);
|
|
1007
|
+
if (missingOnLocal.length > 0) parts.push(`local lacks ${missingOnLocal.join(", ")}`);
|
|
1008
|
+
return {
|
|
1009
|
+
outcome: "degraded",
|
|
1010
|
+
negotiatedProtocol,
|
|
1011
|
+
protocol: {
|
|
1012
|
+
localRange,
|
|
1013
|
+
remoteRange
|
|
1014
|
+
},
|
|
1015
|
+
capabilities,
|
|
1016
|
+
reason: {
|
|
1017
|
+
code: "capability.unavailable",
|
|
1018
|
+
message: `capability mismatch: ${parts.join("; ")}`,
|
|
1019
|
+
detail: {
|
|
1020
|
+
missingOnRemote,
|
|
1021
|
+
missingOnLocal
|
|
1022
|
+
}
|
|
1023
|
+
},
|
|
1024
|
+
local,
|
|
1025
|
+
remote
|
|
1026
|
+
};
|
|
1027
|
+
}
|
|
1028
|
+
return {
|
|
1029
|
+
outcome: "compatible",
|
|
1030
|
+
negotiatedProtocol,
|
|
1031
|
+
protocol: {
|
|
1032
|
+
localRange,
|
|
1033
|
+
remoteRange
|
|
1034
|
+
},
|
|
1035
|
+
capabilities,
|
|
1036
|
+
local,
|
|
1037
|
+
remote
|
|
1038
|
+
};
|
|
1039
|
+
}
|
|
1040
|
+
__name(negotiate, "negotiate");
|
|
1041
|
+
|
|
1042
|
+
// src/handshake/routing.ts
|
|
1043
|
+
function requiredCapabilities(handler) {
|
|
1044
|
+
const list = Array.isArray(handler) ? handler : handler.requires ?? [];
|
|
1045
|
+
return [
|
|
1046
|
+
...new Set(list)
|
|
1047
|
+
];
|
|
1048
|
+
}
|
|
1049
|
+
__name(requiredCapabilities, "requiredCapabilities");
|
|
1050
|
+
function canRoute(handler, worker) {
|
|
1051
|
+
const required = requiredCapabilities(handler);
|
|
1052
|
+
if (required.length === 0) return true;
|
|
1053
|
+
const advertised = new Set(normalizeDescriptor(worker).capabilities);
|
|
1054
|
+
return required.every((cap) => advertised.has(cap));
|
|
1055
|
+
}
|
|
1056
|
+
__name(canRoute, "canRoute");
|
|
1057
|
+
function missingCapabilities(handler, worker) {
|
|
1058
|
+
const advertised = new Set(normalizeDescriptor(worker).capabilities);
|
|
1059
|
+
return requiredCapabilities(handler).filter((cap) => !advertised.has(cap));
|
|
1060
|
+
}
|
|
1061
|
+
__name(missingCapabilities, "missingCapabilities");
|
|
1062
|
+
function capableWorkers(handler, workers) {
|
|
1063
|
+
return workers.filter((w) => canRoute(handler, w));
|
|
1064
|
+
}
|
|
1065
|
+
__name(capableWorkers, "capableWorkers");
|
|
1066
|
+
function resolveRouting(handler, workers) {
|
|
1067
|
+
const requires = requiredCapabilities(handler);
|
|
1068
|
+
const capable = capableWorkers(handler, workers).map(normalizeDescriptor);
|
|
1069
|
+
if (capable.length > 0) {
|
|
1070
|
+
return {
|
|
1071
|
+
status: "routable",
|
|
1072
|
+
workers: capable,
|
|
1073
|
+
requires
|
|
1074
|
+
};
|
|
1075
|
+
}
|
|
1076
|
+
const suffix = requires.length > 0 ? ` (requires ${requires.join(", ")})` : "";
|
|
1077
|
+
return {
|
|
1078
|
+
status: "blocked",
|
|
1079
|
+
reason: `blocked: no compatible worker${suffix}`,
|
|
1080
|
+
requires
|
|
1081
|
+
};
|
|
1082
|
+
}
|
|
1083
|
+
__name(resolveRouting, "resolveRouting");
|
|
1084
|
+
|
|
1085
|
+
// src/handshake/dispatch-routing.ts
|
|
1086
|
+
function controlPlaneDescriptor(opts) {
|
|
1087
|
+
const version = CURRENT_PROTOCOL_VERSION;
|
|
1088
|
+
return {
|
|
1089
|
+
instanceId: opts.instanceId,
|
|
1090
|
+
runtime: "node",
|
|
1091
|
+
sdk: opts.sdk ?? {
|
|
1092
|
+
name: "@dudousxd/nestjs-durable-core",
|
|
1093
|
+
version: String(version)
|
|
1094
|
+
},
|
|
1095
|
+
protocol: opts.protocol ?? {
|
|
1096
|
+
version,
|
|
1097
|
+
range: [
|
|
1098
|
+
version,
|
|
1099
|
+
version
|
|
1100
|
+
]
|
|
1101
|
+
},
|
|
1102
|
+
capabilities: opts.capabilities ?? [],
|
|
1103
|
+
workflows: [],
|
|
1104
|
+
steps: [],
|
|
1105
|
+
...opts.partition !== void 0 ? {
|
|
1106
|
+
partition: opts.partition
|
|
1107
|
+
} : {},
|
|
1108
|
+
...opts.namespace !== void 0 ? {
|
|
1109
|
+
namespace: opts.namespace
|
|
1110
|
+
} : {},
|
|
1111
|
+
startedAt: opts.startedAt ?? 0
|
|
1112
|
+
};
|
|
1113
|
+
}
|
|
1114
|
+
__name(controlPlaneDescriptor, "controlPlaneDescriptor");
|
|
1115
|
+
var WorkflowBlocked = class extends Error {
|
|
1116
|
+
static {
|
|
1117
|
+
__name(this, "WorkflowBlocked");
|
|
1118
|
+
}
|
|
1119
|
+
plan;
|
|
1120
|
+
constructor(plan) {
|
|
1121
|
+
super(plan.reason), this.plan = plan;
|
|
1122
|
+
this.name = "WorkflowBlocked";
|
|
1123
|
+
}
|
|
1124
|
+
};
|
|
1125
|
+
function planDispatch(requires, liveDescriptors, cp, token) {
|
|
1126
|
+
const required = requiredCapabilities({
|
|
1127
|
+
requires
|
|
1128
|
+
});
|
|
1129
|
+
const workers = liveDescriptors.map(normalizeDescriptor);
|
|
1130
|
+
const capable = workers.filter((w) => canRoute({
|
|
1131
|
+
requires
|
|
1132
|
+
}, w));
|
|
1133
|
+
const compatible = capable.filter((w) => negotiate(cp, w).outcome !== "incompatible");
|
|
1134
|
+
if (compatible.length > 0) return {
|
|
1135
|
+
status: "routable",
|
|
1136
|
+
workers: compatible
|
|
1137
|
+
};
|
|
1138
|
+
if (capable.length > 0) {
|
|
1139
|
+
const workerRanges = capable.map((w) => w.protocol.range);
|
|
1140
|
+
return {
|
|
1141
|
+
status: "blocked",
|
|
1142
|
+
code: "protocol.incompatible",
|
|
1143
|
+
reason: `blocked: no protocol-compatible worker${required.length > 0 ? ` (requires ${required.join(", ")})` : ""}`,
|
|
1144
|
+
requires: required,
|
|
1145
|
+
diagnostics: {
|
|
1146
|
+
code: "protocol.incompatible",
|
|
1147
|
+
token,
|
|
1148
|
+
requires: required,
|
|
1149
|
+
liveWorkers: workers.length,
|
|
1150
|
+
controlPlaneRange: cp.protocol.range,
|
|
1151
|
+
workerRanges,
|
|
1152
|
+
controlPlane: cp,
|
|
1153
|
+
workers
|
|
1154
|
+
}
|
|
1155
|
+
};
|
|
1156
|
+
}
|
|
1157
|
+
const advertised = new Set(workers.flatMap((w) => w.capabilities));
|
|
1158
|
+
const missingCapabilities2 = required.filter((cap) => !advertised.has(cap));
|
|
1159
|
+
return {
|
|
1160
|
+
status: "blocked",
|
|
1161
|
+
code: "capability.unavailable",
|
|
1162
|
+
reason: `blocked: no compatible worker${required.length > 0 ? ` (requires ${required.join(", ")})` : ""}`,
|
|
1163
|
+
requires: required,
|
|
1164
|
+
diagnostics: {
|
|
1165
|
+
code: "capability.unavailable",
|
|
1166
|
+
token,
|
|
1167
|
+
requires: required,
|
|
1168
|
+
liveWorkers: workers.length,
|
|
1169
|
+
missingCapabilities: missingCapabilities2,
|
|
1170
|
+
controlPlane: cp,
|
|
1171
|
+
workers
|
|
1172
|
+
}
|
|
1173
|
+
};
|
|
1174
|
+
}
|
|
1175
|
+
__name(planDispatch, "planDispatch");
|
|
1176
|
+
|
|
825
1177
|
// src/tenant-group.ts
|
|
826
1178
|
function tenantGroup(baseGroup, tenant) {
|
|
827
1179
|
return tenant !== void 0 && tenant !== "" && tenant !== "default" ? `${baseGroup}@${tenant}` : baseGroup;
|
|
@@ -1086,6 +1438,22 @@ var TransportPool = class {
|
|
|
1086
1438
|
transport.onStartRun?.(handler);
|
|
1087
1439
|
}
|
|
1088
1440
|
}
|
|
1441
|
+
/**
|
|
1442
|
+
* The live handshake descriptors advertised for `group` (design §7.2), merged across every transport
|
|
1443
|
+
* that can report them. Returns `[]` when NO transport implements
|
|
1444
|
+
* {@link Transport.readWorkerDescriptors} (a pure in-process pool) OR when descriptors are supported
|
|
1445
|
+
* but none is published — both are the "guard disengaged / legacy assume-compatible" case, so the
|
|
1446
|
+
* engine's routing guard treats an empty result identically (design §7.7).
|
|
1447
|
+
*/
|
|
1448
|
+
async readWorkerDescriptors(group) {
|
|
1449
|
+
const descriptors = [];
|
|
1450
|
+
for (const { transport } of this.transports) {
|
|
1451
|
+
if (transport.readWorkerDescriptors) {
|
|
1452
|
+
descriptors.push(...await transport.readWorkerDescriptors(group));
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
return descriptors;
|
|
1456
|
+
}
|
|
1089
1457
|
/** Distinct worker groups with a live heartbeat, merged across every transport that can report it. */
|
|
1090
1458
|
async listWorkerGroups() {
|
|
1091
1459
|
const groups = /* @__PURE__ */ new Set();
|
|
@@ -1181,6 +1549,8 @@ function resolveStepPolicy(config, opts) {
|
|
|
1181
1549
|
if (jitter !== void 0) policy.jitter = jitter;
|
|
1182
1550
|
const timeoutMs = opts?.timeoutMs ?? config?.timeoutMs;
|
|
1183
1551
|
if (timeoutMs !== void 0) policy.timeoutMs = timeoutMs;
|
|
1552
|
+
const requires = opts?.requires ?? config?.requires;
|
|
1553
|
+
if (requires !== void 0) policy.requires = requires;
|
|
1184
1554
|
return policy;
|
|
1185
1555
|
}
|
|
1186
1556
|
__name(resolveStepPolicy, "resolveStepPolicy");
|
|
@@ -1886,6 +2256,10 @@ var WorkflowEngine = class {
|
|
|
1886
2256
|
eventBufferTtlMs;
|
|
1887
2257
|
remoteRedispatchMs;
|
|
1888
2258
|
remoteRedispatchMax;
|
|
2259
|
+
/** How long a `blocked` run waits before the recovery poll re-checks the live fleet (design §7.5). */
|
|
2260
|
+
blockedPollMs;
|
|
2261
|
+
/** This engine's control-plane handshake descriptor — negotiated against each worker (design §7.3). */
|
|
2262
|
+
cpDescriptor;
|
|
1889
2263
|
webhookUrl;
|
|
1890
2264
|
traceparent;
|
|
1891
2265
|
context;
|
|
@@ -1957,6 +2331,13 @@ var WorkflowEngine = class {
|
|
|
1957
2331
|
this.eventBufferTtlMs = deps.eventBufferTtlMs || void 0;
|
|
1958
2332
|
this.remoteRedispatchMs = deps.remoteRedispatchMs || void 0;
|
|
1959
2333
|
this.remoteRedispatchMax = Math.max(1, deps.remoteRedispatchMax ?? 10);
|
|
2334
|
+
this.blockedPollMs = deps.blockedPollMs ?? 5e3;
|
|
2335
|
+
this.cpDescriptor = controlPlaneDescriptor({
|
|
2336
|
+
instanceId: `cp-${this.instanceId}`,
|
|
2337
|
+
...this.namespace !== void 0 ? {
|
|
2338
|
+
namespace: this.namespace
|
|
2339
|
+
} : {}
|
|
2340
|
+
});
|
|
1960
2341
|
this.webhookUrl = deps.webhookUrl;
|
|
1961
2342
|
this.traceparent = deps.traceparent;
|
|
1962
2343
|
this.context = deps.context;
|
|
@@ -2110,6 +2491,7 @@ var WorkflowEngine = class {
|
|
|
2110
2491
|
searchAttributesSchema: opts?.searchAttributesSchema,
|
|
2111
2492
|
onEvent: opts?.onEvent,
|
|
2112
2493
|
eventBatch: opts?.eventBatch,
|
|
2494
|
+
requires: opts?.requires,
|
|
2113
2495
|
// A real, retained in-process body — so `workflowBody` can hand it to an in-app worker, and so
|
|
2114
2496
|
// it is NOT confused with the throwing stub `registerRemote` installs for a body-less remote.
|
|
2115
2497
|
hasBody: true
|
|
@@ -2158,6 +2540,7 @@ var WorkflowEngine = class {
|
|
|
2158
2540
|
singleton: opts.singleton,
|
|
2159
2541
|
executionTimeoutMs: opts.executionTimeout != null ? parseDuration(opts.executionTimeout) : void 0,
|
|
2160
2542
|
validateInput: opts.validateInput,
|
|
2543
|
+
requires: opts.requires,
|
|
2161
2544
|
remote: {
|
|
2162
2545
|
group: opts.group,
|
|
2163
2546
|
executor: opts.executor
|
|
@@ -2641,9 +3024,13 @@ var WorkflowEngine = class {
|
|
|
2641
3024
|
*/
|
|
2642
3025
|
async resumeDueTimers(nowMs = this.clock()) {
|
|
2643
3026
|
const due = await this.store.listDueTimers(nowMs, this.namespace);
|
|
2644
|
-
|
|
3027
|
+
const blocked = await this.dueBlockedRuns(nowMs);
|
|
3028
|
+
if (due.length === 0 && blocked.length === 0) return [];
|
|
2645
3029
|
const waiterByRun = indexWaitersByRun(await this.store.listSignalWaiters(""));
|
|
2646
|
-
return this.resumeLeased(
|
|
3030
|
+
return this.resumeLeased([
|
|
3031
|
+
...due,
|
|
3032
|
+
...blocked
|
|
3033
|
+
], nowMs, (run) => this.reconcileSignalWaiter(run, waiterByRun.get(run.id)));
|
|
2647
3034
|
}
|
|
2648
3035
|
/**
|
|
2649
3036
|
* The reconcile safety net's per-run check (see {@link resumeDueTimers}): if this suspended run has
|
|
@@ -2726,7 +3113,10 @@ var WorkflowEngine = class {
|
|
|
2726
3113
|
* inline — or null if the run is unknown. The dashboard "retry" goes through here so it can't block
|
|
2727
3114
|
* the HTTP request on workflow execution.
|
|
2728
3115
|
*/
|
|
2729
|
-
async requeue(runId) {
|
|
3116
|
+
async requeue(runId, _seen) {
|
|
3117
|
+
const seen = _seen ?? /* @__PURE__ */ new Set();
|
|
3118
|
+
if (seen.has(runId)) return null;
|
|
3119
|
+
seen.add(runId);
|
|
2730
3120
|
const run = await this.store.getRun(runId);
|
|
2731
3121
|
if (!run) return null;
|
|
2732
3122
|
if (run.status === "failed" || run.status === "dead") {
|
|
@@ -2750,12 +3140,28 @@ var WorkflowEngine = class {
|
|
|
2750
3140
|
status: "running",
|
|
2751
3141
|
output: void 0
|
|
2752
3142
|
});
|
|
3143
|
+
const childId = cp.name.slice("signal:child:".length);
|
|
3144
|
+
const parked = [];
|
|
3145
|
+
for (; ; ) {
|
|
3146
|
+
const buffered = await this.store.takeBufferedSignal(`child:${childId}`);
|
|
3147
|
+
if (!buffered) break;
|
|
3148
|
+
parked.push(buffered.payload);
|
|
3149
|
+
}
|
|
3150
|
+
for (const payload of parked) await this.store.bufferSignal(`child:${childId}`, payload);
|
|
3151
|
+
const bufferedSuccess = parked.some((payload) => typeof payload === "object" && payload !== null && payload.ok === true);
|
|
3152
|
+
if (!bufferedSuccess) {
|
|
3153
|
+
const child = await this.store.getRun(childId);
|
|
3154
|
+
if (child && (child.status === "failed" || child.status === "dead")) {
|
|
3155
|
+
await this.requeue(childId, seen);
|
|
3156
|
+
}
|
|
3157
|
+
}
|
|
2753
3158
|
}
|
|
2754
3159
|
}
|
|
2755
3160
|
}
|
|
2756
3161
|
await this.store.releaseRunLock(runId);
|
|
2757
3162
|
await this.store.updateRun(runId, {
|
|
2758
3163
|
status: "pending",
|
|
3164
|
+
error: void 0,
|
|
2759
3165
|
updatedAt: /* @__PURE__ */ new Date()
|
|
2760
3166
|
});
|
|
2761
3167
|
await this.runDispatcher.dispatch(runId);
|
|
@@ -3075,6 +3481,10 @@ var WorkflowEngine = class {
|
|
|
3075
3481
|
*/
|
|
3076
3482
|
notifyParent(runId, completion) {
|
|
3077
3483
|
void this.signal(`child:${runId}`, completion).catch(() => void 0);
|
|
3484
|
+
const at = runId.lastIndexOf("~retry~");
|
|
3485
|
+
if (at !== -1 && completion.ok) {
|
|
3486
|
+
void this.signal(`child:${runId.slice(0, at)}`, completion).catch(() => void 0);
|
|
3487
|
+
}
|
|
3078
3488
|
}
|
|
3079
3489
|
/**
|
|
3080
3490
|
* Deferred child start shared by the in-process ctx host and the remote `startChild` command.
|
|
@@ -3529,6 +3939,7 @@ var WorkflowEngine = class {
|
|
|
3529
3939
|
error: outcome.error
|
|
3530
3940
|
};
|
|
3531
3941
|
}
|
|
3942
|
+
if (outcome.kind === "blocked") return this.parkBlocked(run, outcome.blocked);
|
|
3532
3943
|
const latest = await this.store.getRun(run.id);
|
|
3533
3944
|
if (latest?.status === "cancelled") {
|
|
3534
3945
|
return {
|
|
@@ -3628,6 +4039,8 @@ var WorkflowEngine = class {
|
|
|
3628
4039
|
status: "suspended"
|
|
3629
4040
|
};
|
|
3630
4041
|
}
|
|
4042
|
+
const wfPlan = await this.planRoute(tenantGroup(sanitizeQueueToken(remote.group), void 0), registered.requires);
|
|
4043
|
+
if (wfPlan?.status === "blocked") return this.parkBlocked(run, wfPlan);
|
|
3631
4044
|
const history = await this.remoteHistory(run.id);
|
|
3632
4045
|
if (remote.executor.dispatch) {
|
|
3633
4046
|
const taskId = `${run.id}:wf:${globalThis.crypto.randomUUID().slice(0, 8)}`;
|
|
@@ -4051,6 +4464,15 @@ var WorkflowEngine = class {
|
|
|
4051
4464
|
status: "completed"
|
|
4052
4465
|
};
|
|
4053
4466
|
}
|
|
4467
|
+
if (err instanceof WorkflowBlocked) {
|
|
4468
|
+
if (this.cancelRequested.has(run.id)) {
|
|
4469
|
+
return this.compensateAndCancel(run, compensations, replay);
|
|
4470
|
+
}
|
|
4471
|
+
return this.settleRun(run, {
|
|
4472
|
+
kind: "blocked",
|
|
4473
|
+
blocked: err.plan
|
|
4474
|
+
});
|
|
4475
|
+
}
|
|
4054
4476
|
if (err instanceof WorkflowSuspended) {
|
|
4055
4477
|
if (this.cancelRequested.has(run.id)) {
|
|
4056
4478
|
return this.compensateAndCancel(run, compensations, replay);
|
|
@@ -4397,6 +4819,85 @@ var WorkflowEngine = class {
|
|
|
4397
4819
|
run: result
|
|
4398
4820
|
};
|
|
4399
4821
|
}
|
|
4822
|
+
/**
|
|
4823
|
+
* Plan a dispatch to `token` for a handler requiring `requires` (handshake design §7.5). Returns:
|
|
4824
|
+
* - `undefined` — NO live descriptors published on this token: the guard does not engage, so a
|
|
4825
|
+
* pre-handshake fleet (or an in-process transport that advertises none) keeps flowing exactly as
|
|
4826
|
+
* before (design §7.7);
|
|
4827
|
+
* - a `routable` plan — ≥1 live worker is capability-capable AND protocol-compatible (dispatch);
|
|
4828
|
+
* - a `blocked` plan — descriptors exist but none can run it (park the run, never dispatch into it).
|
|
4829
|
+
*/
|
|
4830
|
+
async planRoute(token, requires) {
|
|
4831
|
+
const descriptors = await this.pool.readWorkerDescriptors(token);
|
|
4832
|
+
if (descriptors.length === 0) return void 0;
|
|
4833
|
+
return planDispatch(requires ?? [], descriptors, this.cpDescriptor, token);
|
|
4834
|
+
}
|
|
4835
|
+
/** Throw {@link WorkflowBlocked} when no live worker can run a dispatch to `token` — so the caller's
|
|
4836
|
+
* run-execution catch parks the run `blocked` (with the plan) instead of persisting a `pending`
|
|
4837
|
+
* checkpoint + dispatching into a queue nobody consumes (design §7.5). A no-op when the guard does
|
|
4838
|
+
* not engage (no descriptors) or a capable+compatible worker exists. */
|
|
4839
|
+
async ensureRoutable(token, requires) {
|
|
4840
|
+
const plan = await this.planRoute(token, requires);
|
|
4841
|
+
if (plan?.status === "blocked") throw new WorkflowBlocked(plan);
|
|
4842
|
+
}
|
|
4843
|
+
/**
|
|
4844
|
+
* Park a run `blocked` (handshake design §7.5/§7.6): no live worker on its next-dispatch group is
|
|
4845
|
+
* capability-capable + protocol-compatible. Persists the `blocked` status with a human reason + the
|
|
4846
|
+
* machine `code` + a `wakeAt` so the blocked-recovery poll re-drives it, emits the LOUD structured
|
|
4847
|
+
* `capability.unavailable`/`protocol.incompatible` diagnostics (both descriptors + the precise delta),
|
|
4848
|
+
* and releases the run lease so any instance can re-drive it. Re-checks the persisted status first so
|
|
4849
|
+
* a concurrent cancel/complete is never clobbered. `releaseRunLock` is idempotent, so a caller whose
|
|
4850
|
+
* own `finally` also releases (the run-execution path) is safe.
|
|
4851
|
+
*/
|
|
4852
|
+
async parkBlocked(run, blocked) {
|
|
4853
|
+
const current = await this.store.getRun(run.id);
|
|
4854
|
+
if (current && (current.status === "cancelled" || current.status === "completed")) {
|
|
4855
|
+
await this.store.releaseRunLock(run.id);
|
|
4856
|
+
return {
|
|
4857
|
+
runId: run.id,
|
|
4858
|
+
status: current.status,
|
|
4859
|
+
output: current.output,
|
|
4860
|
+
error: current.error
|
|
4861
|
+
};
|
|
4862
|
+
}
|
|
4863
|
+
const error = {
|
|
4864
|
+
message: blocked.reason,
|
|
4865
|
+
code: blocked.code,
|
|
4866
|
+
retryable: true
|
|
4867
|
+
};
|
|
4868
|
+
await this.store.updateRun(run.id, {
|
|
4869
|
+
status: "blocked",
|
|
4870
|
+
error,
|
|
4871
|
+
wakeAt: this.clock() + this.blockedPollMs,
|
|
4872
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
4873
|
+
});
|
|
4874
|
+
this.emit({
|
|
4875
|
+
type: "run.blocked",
|
|
4876
|
+
runId: run.id,
|
|
4877
|
+
workflow: run.workflow,
|
|
4878
|
+
namespace: run.namespace,
|
|
4879
|
+
error,
|
|
4880
|
+
requires: blocked.requires,
|
|
4881
|
+
diagnostics: blocked.diagnostics
|
|
4882
|
+
});
|
|
4883
|
+
await this.store.releaseRunLock(run.id);
|
|
4884
|
+
return {
|
|
4885
|
+
runId: run.id,
|
|
4886
|
+
status: "blocked",
|
|
4887
|
+
error
|
|
4888
|
+
};
|
|
4889
|
+
}
|
|
4890
|
+
/** Runs parked `blocked` whose recovery poll is due (`wakeAt <= nowMs`), namespace-scoped like
|
|
4891
|
+
* {@link resumeDueTimers}. Re-driving one re-checks the live fleet: it dispatches if a
|
|
4892
|
+
* capable+compatible worker has appeared, else re-parks with a fresh `wakeAt` (design §7.5). */
|
|
4893
|
+
async dueBlockedRuns(nowMs) {
|
|
4894
|
+
const blocked = await this.store.listRuns({
|
|
4895
|
+
statuses: [
|
|
4896
|
+
"blocked"
|
|
4897
|
+
]
|
|
4898
|
+
});
|
|
4899
|
+
return blocked.filter((r) => (this.namespace === void 0 || r.namespace === void 0 || r.namespace === this.namespace) && r.wakeAt !== void 0 && r.wakeAt <= nowMs);
|
|
4900
|
+
}
|
|
4400
4901
|
async callRemote(runId, seq, step, input, queue, transport, replay, admission) {
|
|
4401
4902
|
const existing = replay?.get(seq) ?? await this.store.getCheckpoint(runId, seq);
|
|
4402
4903
|
if (existing && existing.name !== step.name) {
|
|
@@ -4462,6 +4963,7 @@ var WorkflowEngine = class {
|
|
|
4462
4963
|
if (this.clock() < existing.wakeAt) throw new WorkflowSuspended(existing.wakeAt);
|
|
4463
4964
|
attempt = existing.attempts + 1;
|
|
4464
4965
|
}
|
|
4966
|
+
await this.ensureRoutable(tenantGroup(sanitizeQueueToken(step.name), step.partition), step.requires);
|
|
4465
4967
|
const id = stepId(runId, seq);
|
|
4466
4968
|
if (queue && this.admission.handles(queue)) {
|
|
4467
4969
|
const decision = await this.admission.tryAdmit(queue, {
|
|
@@ -4648,6 +5150,7 @@ var WorkflowEngine = class {
|
|
|
4648
5150
|
/** In-memory await path for a remote step with a liveness `timeoutMs` (re-dispatch on timeout). */
|
|
4649
5151
|
async callRemoteInMemory(runId, seq, step, input, transport) {
|
|
4650
5152
|
if (this.pool.size === 0) throw new Error("remote steps require a Transport");
|
|
5153
|
+
await this.ensureRoutable(tenantGroup(sanitizeQueueToken(step.name), step.partition), step.requires);
|
|
4651
5154
|
const validInput = step.input ? step.input.parse(input) : input;
|
|
4652
5155
|
const id = stepId(runId, seq);
|
|
4653
5156
|
const enqueuedAt = /* @__PURE__ */ new Date();
|
|
@@ -5546,6 +6049,7 @@ var InMemoryTransport = class {
|
|
|
5546
6049
|
};
|
|
5547
6050
|
export {
|
|
5548
6051
|
CONTROL_FLOW_SIGNAL,
|
|
6052
|
+
CURRENT_PROTOCOL_VERSION,
|
|
5549
6053
|
CodecStateStore,
|
|
5550
6054
|
ContinueAsNew,
|
|
5551
6055
|
DURABLE_OPTIONS,
|
|
@@ -5562,6 +6066,8 @@ export {
|
|
|
5562
6066
|
InMemoryAdmissionBackend,
|
|
5563
6067
|
InMemoryStateStore,
|
|
5564
6068
|
InMemoryTransport,
|
|
6069
|
+
LEGACY_V1_CAPABILITIES,
|
|
6070
|
+
LEGACY_V1_PROTOCOL,
|
|
5565
6071
|
NonDeterminismError,
|
|
5566
6072
|
QueueController,
|
|
5567
6073
|
RemoteStepError,
|
|
@@ -5576,6 +6082,7 @@ export {
|
|
|
5576
6082
|
TRANSPORT,
|
|
5577
6083
|
TRANSPORT_CANONICAL,
|
|
5578
6084
|
WORKFLOW_NAME_KEY,
|
|
6085
|
+
WorkflowBlocked,
|
|
5579
6086
|
WorkflowEngine,
|
|
5580
6087
|
WorkflowSuspended,
|
|
5581
6088
|
applyAttributeQuery,
|
|
@@ -5583,23 +6090,35 @@ export {
|
|
|
5583
6090
|
attributeOperand,
|
|
5584
6091
|
bindWorkflowClass,
|
|
5585
6092
|
breakpointToken,
|
|
6093
|
+
canRoute,
|
|
6094
|
+
capableWorkers,
|
|
5586
6095
|
classifyWaiterToken,
|
|
5587
6096
|
collectMetrics,
|
|
6097
|
+
controlPlaneDescriptor,
|
|
5588
6098
|
createStepLogger,
|
|
5589
6099
|
currentWorkflowCtx,
|
|
6100
|
+
descriptorHash,
|
|
5590
6101
|
eventMatchOf,
|
|
5591
6102
|
eventMatches,
|
|
5592
6103
|
eventNameOf,
|
|
5593
6104
|
eventPrefix,
|
|
5594
6105
|
eventToken,
|
|
6106
|
+
heartbeatStatus,
|
|
5595
6107
|
indexWaitersByRun,
|
|
5596
6108
|
isEventToken,
|
|
6109
|
+
isLegacyDescriptor,
|
|
5597
6110
|
isWorkflowControlFlowSignal,
|
|
5598
6111
|
matchesAttributes,
|
|
6112
|
+
missingCapabilities,
|
|
6113
|
+
negotiate,
|
|
5599
6114
|
normalizeAttributeRows,
|
|
6115
|
+
normalizeDescriptor,
|
|
5600
6116
|
parseDuration,
|
|
6117
|
+
planDispatch,
|
|
5601
6118
|
prevCronFireMs,
|
|
5602
6119
|
readSearchAttributes,
|
|
6120
|
+
requiredCapabilities,
|
|
6121
|
+
resolveRouting,
|
|
5603
6122
|
resolveRunWaiting,
|
|
5604
6123
|
runInWorkflowCtx,
|
|
5605
6124
|
runSchedules,
|