@dudousxd/nestjs-durable-core 0.58.0 → 0.60.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 +523 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +477 -15
- package/dist/index.d.ts +477 -15
- package/dist/index.js +506 -2
- 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
|
|
@@ -3552,6 +3939,7 @@ var WorkflowEngine = class {
|
|
|
3552
3939
|
error: outcome.error
|
|
3553
3940
|
};
|
|
3554
3941
|
}
|
|
3942
|
+
if (outcome.kind === "blocked") return this.parkBlocked(run, outcome.blocked);
|
|
3555
3943
|
const latest = await this.store.getRun(run.id);
|
|
3556
3944
|
if (latest?.status === "cancelled") {
|
|
3557
3945
|
return {
|
|
@@ -3651,6 +4039,8 @@ var WorkflowEngine = class {
|
|
|
3651
4039
|
status: "suspended"
|
|
3652
4040
|
};
|
|
3653
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);
|
|
3654
4044
|
const history = await this.remoteHistory(run.id);
|
|
3655
4045
|
if (remote.executor.dispatch) {
|
|
3656
4046
|
const taskId = `${run.id}:wf:${globalThis.crypto.randomUUID().slice(0, 8)}`;
|
|
@@ -4074,6 +4464,15 @@ var WorkflowEngine = class {
|
|
|
4074
4464
|
status: "completed"
|
|
4075
4465
|
};
|
|
4076
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
|
+
}
|
|
4077
4476
|
if (err instanceof WorkflowSuspended) {
|
|
4078
4477
|
if (this.cancelRequested.has(run.id)) {
|
|
4079
4478
|
return this.compensateAndCancel(run, compensations, replay);
|
|
@@ -4420,6 +4819,85 @@ var WorkflowEngine = class {
|
|
|
4420
4819
|
run: result
|
|
4421
4820
|
};
|
|
4422
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
|
+
}
|
|
4423
4901
|
async callRemote(runId, seq, step, input, queue, transport, replay, admission) {
|
|
4424
4902
|
const existing = replay?.get(seq) ?? await this.store.getCheckpoint(runId, seq);
|
|
4425
4903
|
if (existing && existing.name !== step.name) {
|
|
@@ -4485,6 +4963,7 @@ var WorkflowEngine = class {
|
|
|
4485
4963
|
if (this.clock() < existing.wakeAt) throw new WorkflowSuspended(existing.wakeAt);
|
|
4486
4964
|
attempt = existing.attempts + 1;
|
|
4487
4965
|
}
|
|
4966
|
+
await this.ensureRoutable(tenantGroup(sanitizeQueueToken(step.name), step.partition), step.requires);
|
|
4488
4967
|
const id = stepId(runId, seq);
|
|
4489
4968
|
if (queue && this.admission.handles(queue)) {
|
|
4490
4969
|
const decision = await this.admission.tryAdmit(queue, {
|
|
@@ -4671,6 +5150,7 @@ var WorkflowEngine = class {
|
|
|
4671
5150
|
/** In-memory await path for a remote step with a liveness `timeoutMs` (re-dispatch on timeout). */
|
|
4672
5151
|
async callRemoteInMemory(runId, seq, step, input, transport) {
|
|
4673
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);
|
|
4674
5154
|
const validInput = step.input ? step.input.parse(input) : input;
|
|
4675
5155
|
const id = stepId(runId, seq);
|
|
4676
5156
|
const enqueuedAt = /* @__PURE__ */ new Date();
|
|
@@ -4789,6 +5269,13 @@ var TERMINAL_RUN_STATUSES = [
|
|
|
4789
5269
|
"dead"
|
|
4790
5270
|
];
|
|
4791
5271
|
|
|
5272
|
+
// src/run-gateway.ts
|
|
5273
|
+
var RunGateway = class {
|
|
5274
|
+
static {
|
|
5275
|
+
__name(this, "RunGateway");
|
|
5276
|
+
}
|
|
5277
|
+
};
|
|
5278
|
+
|
|
4792
5279
|
// src/codec-state-store.ts
|
|
4793
5280
|
var CodecStateStore = class {
|
|
4794
5281
|
static {
|
|
@@ -5569,6 +6056,7 @@ var InMemoryTransport = class {
|
|
|
5569
6056
|
};
|
|
5570
6057
|
export {
|
|
5571
6058
|
CONTROL_FLOW_SIGNAL,
|
|
6059
|
+
CURRENT_PROTOCOL_VERSION,
|
|
5572
6060
|
CodecStateStore,
|
|
5573
6061
|
ContinueAsNew,
|
|
5574
6062
|
DURABLE_OPTIONS,
|
|
@@ -5585,12 +6073,15 @@ export {
|
|
|
5585
6073
|
InMemoryAdmissionBackend,
|
|
5586
6074
|
InMemoryStateStore,
|
|
5587
6075
|
InMemoryTransport,
|
|
6076
|
+
LEGACY_V1_CAPABILITIES,
|
|
6077
|
+
LEGACY_V1_PROTOCOL,
|
|
5588
6078
|
NonDeterminismError,
|
|
5589
6079
|
QueueController,
|
|
5590
6080
|
RemoteStepError,
|
|
5591
6081
|
RemoteStepTimeout,
|
|
5592
6082
|
RemoteWorkflowExecutor,
|
|
5593
6083
|
RemoteWorkflowTimeout,
|
|
6084
|
+
RunGateway,
|
|
5594
6085
|
STATE_STORE,
|
|
5595
6086
|
STATE_STORE_CANONICAL,
|
|
5596
6087
|
SignalTimeoutError,
|
|
@@ -5599,6 +6090,7 @@ export {
|
|
|
5599
6090
|
TRANSPORT,
|
|
5600
6091
|
TRANSPORT_CANONICAL,
|
|
5601
6092
|
WORKFLOW_NAME_KEY,
|
|
6093
|
+
WorkflowBlocked,
|
|
5602
6094
|
WorkflowEngine,
|
|
5603
6095
|
WorkflowSuspended,
|
|
5604
6096
|
applyAttributeQuery,
|
|
@@ -5606,23 +6098,35 @@ export {
|
|
|
5606
6098
|
attributeOperand,
|
|
5607
6099
|
bindWorkflowClass,
|
|
5608
6100
|
breakpointToken,
|
|
6101
|
+
canRoute,
|
|
6102
|
+
capableWorkers,
|
|
5609
6103
|
classifyWaiterToken,
|
|
5610
6104
|
collectMetrics,
|
|
6105
|
+
controlPlaneDescriptor,
|
|
5611
6106
|
createStepLogger,
|
|
5612
6107
|
currentWorkflowCtx,
|
|
6108
|
+
descriptorHash,
|
|
5613
6109
|
eventMatchOf,
|
|
5614
6110
|
eventMatches,
|
|
5615
6111
|
eventNameOf,
|
|
5616
6112
|
eventPrefix,
|
|
5617
6113
|
eventToken,
|
|
6114
|
+
heartbeatStatus,
|
|
5618
6115
|
indexWaitersByRun,
|
|
5619
6116
|
isEventToken,
|
|
6117
|
+
isLegacyDescriptor,
|
|
5620
6118
|
isWorkflowControlFlowSignal,
|
|
5621
6119
|
matchesAttributes,
|
|
6120
|
+
missingCapabilities,
|
|
6121
|
+
negotiate,
|
|
5622
6122
|
normalizeAttributeRows,
|
|
6123
|
+
normalizeDescriptor,
|
|
5623
6124
|
parseDuration,
|
|
6125
|
+
planDispatch,
|
|
5624
6126
|
prevCronFireMs,
|
|
5625
6127
|
readSearchAttributes,
|
|
6128
|
+
requiredCapabilities,
|
|
6129
|
+
resolveRouting,
|
|
5626
6130
|
resolveRunWaiting,
|
|
5627
6131
|
runInWorkflowCtx,
|
|
5628
6132
|
runSchedules,
|