@dudousxd/nestjs-durable-core 0.58.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 +514 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +459 -3
- package/dist/index.d.ts +459 -3
- package/dist/index.js +498 -2
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -23,6 +23,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
23
23
|
var index_exports = {};
|
|
24
24
|
__export(index_exports, {
|
|
25
25
|
CONTROL_FLOW_SIGNAL: () => CONTROL_FLOW_SIGNAL,
|
|
26
|
+
CURRENT_PROTOCOL_VERSION: () => CURRENT_PROTOCOL_VERSION,
|
|
26
27
|
CodecStateStore: () => CodecStateStore,
|
|
27
28
|
ContinueAsNew: () => ContinueAsNew,
|
|
28
29
|
DURABLE_OPTIONS: () => DURABLE_OPTIONS,
|
|
@@ -39,6 +40,8 @@ __export(index_exports, {
|
|
|
39
40
|
InMemoryAdmissionBackend: () => InMemoryAdmissionBackend,
|
|
40
41
|
InMemoryStateStore: () => InMemoryStateStore,
|
|
41
42
|
InMemoryTransport: () => InMemoryTransport,
|
|
43
|
+
LEGACY_V1_CAPABILITIES: () => LEGACY_V1_CAPABILITIES,
|
|
44
|
+
LEGACY_V1_PROTOCOL: () => LEGACY_V1_PROTOCOL,
|
|
42
45
|
NonDeterminismError: () => NonDeterminismError,
|
|
43
46
|
QueueController: () => QueueController,
|
|
44
47
|
RemoteStepError: () => RemoteStepError,
|
|
@@ -53,6 +56,7 @@ __export(index_exports, {
|
|
|
53
56
|
TRANSPORT: () => TRANSPORT,
|
|
54
57
|
TRANSPORT_CANONICAL: () => TRANSPORT_CANONICAL,
|
|
55
58
|
WORKFLOW_NAME_KEY: () => WORKFLOW_NAME_KEY,
|
|
59
|
+
WorkflowBlocked: () => WorkflowBlocked,
|
|
56
60
|
WorkflowEngine: () => WorkflowEngine,
|
|
57
61
|
WorkflowSuspended: () => WorkflowSuspended,
|
|
58
62
|
applyAttributeQuery: () => applyAttributeQuery,
|
|
@@ -60,23 +64,35 @@ __export(index_exports, {
|
|
|
60
64
|
attributeOperand: () => attributeOperand,
|
|
61
65
|
bindWorkflowClass: () => bindWorkflowClass,
|
|
62
66
|
breakpointToken: () => breakpointToken,
|
|
67
|
+
canRoute: () => canRoute,
|
|
68
|
+
capableWorkers: () => capableWorkers,
|
|
63
69
|
classifyWaiterToken: () => classifyWaiterToken,
|
|
64
70
|
collectMetrics: () => collectMetrics,
|
|
71
|
+
controlPlaneDescriptor: () => controlPlaneDescriptor,
|
|
65
72
|
createStepLogger: () => createStepLogger,
|
|
66
73
|
currentWorkflowCtx: () => currentWorkflowCtx,
|
|
74
|
+
descriptorHash: () => descriptorHash,
|
|
67
75
|
eventMatchOf: () => eventMatchOf,
|
|
68
76
|
eventMatches: () => eventMatches,
|
|
69
77
|
eventNameOf: () => eventNameOf,
|
|
70
78
|
eventPrefix: () => eventPrefix,
|
|
71
79
|
eventToken: () => eventToken,
|
|
80
|
+
heartbeatStatus: () => heartbeatStatus,
|
|
72
81
|
indexWaitersByRun: () => indexWaitersByRun,
|
|
73
82
|
isEventToken: () => isEventToken,
|
|
83
|
+
isLegacyDescriptor: () => isLegacyDescriptor,
|
|
74
84
|
isWorkflowControlFlowSignal: () => isWorkflowControlFlowSignal,
|
|
75
85
|
matchesAttributes: () => matchesAttributes,
|
|
86
|
+
missingCapabilities: () => missingCapabilities,
|
|
87
|
+
negotiate: () => negotiate,
|
|
76
88
|
normalizeAttributeRows: () => normalizeAttributeRows,
|
|
89
|
+
normalizeDescriptor: () => normalizeDescriptor,
|
|
77
90
|
parseDuration: () => parseDuration,
|
|
91
|
+
planDispatch: () => planDispatch,
|
|
78
92
|
prevCronFireMs: () => prevCronFireMs,
|
|
79
93
|
readSearchAttributes: () => readSearchAttributes,
|
|
94
|
+
requiredCapabilities: () => requiredCapabilities,
|
|
95
|
+
resolveRouting: () => resolveRouting,
|
|
80
96
|
resolveRunWaiting: () => resolveRunWaiting,
|
|
81
97
|
runInWorkflowCtx: () => runInWorkflowCtx,
|
|
82
98
|
runSchedules: () => runSchedules,
|
|
@@ -913,6 +929,358 @@ function eventMatches(payload, match) {
|
|
|
913
929
|
}
|
|
914
930
|
__name(eventMatches, "eventMatches");
|
|
915
931
|
|
|
932
|
+
// src/handshake/descriptor.ts
|
|
933
|
+
var CURRENT_PROTOCOL_VERSION = 1;
|
|
934
|
+
var LEGACY_V1_PROTOCOL = {
|
|
935
|
+
version: 1,
|
|
936
|
+
range: [
|
|
937
|
+
1,
|
|
938
|
+
1
|
|
939
|
+
]
|
|
940
|
+
};
|
|
941
|
+
var LEGACY_V1_CAPABILITIES = Object.freeze([
|
|
942
|
+
"saga",
|
|
943
|
+
"signals",
|
|
944
|
+
"search-attributes",
|
|
945
|
+
"priority",
|
|
946
|
+
"entities",
|
|
947
|
+
"child-workflows",
|
|
948
|
+
"singleton",
|
|
949
|
+
"schedules",
|
|
950
|
+
"continue-as-new",
|
|
951
|
+
"queries",
|
|
952
|
+
"cancellation"
|
|
953
|
+
]);
|
|
954
|
+
function isLegacyDescriptor(raw) {
|
|
955
|
+
return raw.protocol === void 0;
|
|
956
|
+
}
|
|
957
|
+
__name(isLegacyDescriptor, "isLegacyDescriptor");
|
|
958
|
+
function normalizeDescriptor(raw) {
|
|
959
|
+
return {
|
|
960
|
+
instanceId: raw.instanceId,
|
|
961
|
+
runtime: raw.runtime,
|
|
962
|
+
sdk: raw.sdk ?? {
|
|
963
|
+
name: "unknown",
|
|
964
|
+
version: "0"
|
|
965
|
+
},
|
|
966
|
+
protocol: raw.protocol ?? {
|
|
967
|
+
...LEGACY_V1_PROTOCOL
|
|
968
|
+
},
|
|
969
|
+
capabilities: raw.capabilities ?? [
|
|
970
|
+
...LEGACY_V1_CAPABILITIES
|
|
971
|
+
],
|
|
972
|
+
workflows: raw.workflows ?? [],
|
|
973
|
+
steps: raw.steps ?? [],
|
|
974
|
+
...raw.partition !== void 0 ? {
|
|
975
|
+
partition: raw.partition
|
|
976
|
+
} : {},
|
|
977
|
+
...raw.namespace !== void 0 ? {
|
|
978
|
+
namespace: raw.namespace
|
|
979
|
+
} : {},
|
|
980
|
+
startedAt: raw.startedAt ?? 0
|
|
981
|
+
};
|
|
982
|
+
}
|
|
983
|
+
__name(normalizeDescriptor, "normalizeDescriptor");
|
|
984
|
+
function canonicalizeForHash(d) {
|
|
985
|
+
const set = /* @__PURE__ */ __name((xs) => [
|
|
986
|
+
...new Set(xs)
|
|
987
|
+
].sort(), "set");
|
|
988
|
+
return {
|
|
989
|
+
instanceId: d.instanceId,
|
|
990
|
+
runtime: d.runtime,
|
|
991
|
+
sdk: {
|
|
992
|
+
name: d.sdk.name,
|
|
993
|
+
version: d.sdk.version
|
|
994
|
+
},
|
|
995
|
+
protocol: {
|
|
996
|
+
version: d.protocol.version,
|
|
997
|
+
range: [
|
|
998
|
+
d.protocol.range[0],
|
|
999
|
+
d.protocol.range[1]
|
|
1000
|
+
]
|
|
1001
|
+
},
|
|
1002
|
+
capabilities: set(d.capabilities),
|
|
1003
|
+
workflows: set(d.workflows),
|
|
1004
|
+
steps: set(d.steps),
|
|
1005
|
+
partition: d.partition ?? null,
|
|
1006
|
+
namespace: d.namespace ?? null,
|
|
1007
|
+
startedAt: d.startedAt
|
|
1008
|
+
};
|
|
1009
|
+
}
|
|
1010
|
+
__name(canonicalizeForHash, "canonicalizeForHash");
|
|
1011
|
+
function fnv1a64Hex(input) {
|
|
1012
|
+
const OFFSET = 0xcbf29ce484222325n;
|
|
1013
|
+
const PRIME = 0x100000001b3n;
|
|
1014
|
+
const MASK = 0xffffffffffffffffn;
|
|
1015
|
+
let hash = OFFSET;
|
|
1016
|
+
for (let i = 0; i < input.length; i += 1) {
|
|
1017
|
+
hash ^= BigInt(input.charCodeAt(i) & 255);
|
|
1018
|
+
hash ^= BigInt(input.charCodeAt(i) >> 8 & 255);
|
|
1019
|
+
hash = hash * PRIME & MASK;
|
|
1020
|
+
}
|
|
1021
|
+
return hash.toString(16).padStart(16, "0");
|
|
1022
|
+
}
|
|
1023
|
+
__name(fnv1a64Hex, "fnv1a64Hex");
|
|
1024
|
+
function descriptorHash(descriptor) {
|
|
1025
|
+
const canonical = canonicalizeForHash(normalizeDescriptor(descriptor));
|
|
1026
|
+
return fnv1a64Hex(JSON.stringify(canonical));
|
|
1027
|
+
}
|
|
1028
|
+
__name(descriptorHash, "descriptorHash");
|
|
1029
|
+
function heartbeatStatus(descriptor, opts = {}) {
|
|
1030
|
+
return {
|
|
1031
|
+
ts: opts.ts ?? Date.now(),
|
|
1032
|
+
status: opts.status ?? "up",
|
|
1033
|
+
descriptorHash: descriptorHash(descriptor)
|
|
1034
|
+
};
|
|
1035
|
+
}
|
|
1036
|
+
__name(heartbeatStatus, "heartbeatStatus");
|
|
1037
|
+
|
|
1038
|
+
// src/handshake/negotiate.ts
|
|
1039
|
+
var rangesOverlap = /* @__PURE__ */ __name((a, b) => Math.max(a[0], b[0]) <= Math.min(a[1], b[1]), "rangesOverlap");
|
|
1040
|
+
var difference = /* @__PURE__ */ __name((a, b) => {
|
|
1041
|
+
const bset = new Set(b);
|
|
1042
|
+
return [
|
|
1043
|
+
...new Set(a)
|
|
1044
|
+
].filter((x) => !bset.has(x)).sort();
|
|
1045
|
+
}, "difference");
|
|
1046
|
+
var intersection = /* @__PURE__ */ __name((a, b) => {
|
|
1047
|
+
const bset = new Set(b);
|
|
1048
|
+
return [
|
|
1049
|
+
...new Set(a)
|
|
1050
|
+
].filter((x) => bset.has(x)).sort();
|
|
1051
|
+
}, "intersection");
|
|
1052
|
+
function negotiate(localRaw, remoteRaw, opts = {}) {
|
|
1053
|
+
const local = normalizeDescriptor(localRaw);
|
|
1054
|
+
const remote = normalizeDescriptor(remoteRaw);
|
|
1055
|
+
const localRange = local.protocol.range;
|
|
1056
|
+
const remoteRange = remote.protocol.range;
|
|
1057
|
+
const shared = intersection(local.capabilities, remote.capabilities);
|
|
1058
|
+
const missingOnRemote = difference(local.capabilities, remote.capabilities);
|
|
1059
|
+
const missingOnLocal = difference(remote.capabilities, local.capabilities);
|
|
1060
|
+
const capabilities = {
|
|
1061
|
+
shared,
|
|
1062
|
+
missingOnRemote,
|
|
1063
|
+
missingOnLocal
|
|
1064
|
+
};
|
|
1065
|
+
if (!rangesOverlap(localRange, remoteRange)) {
|
|
1066
|
+
return {
|
|
1067
|
+
outcome: "incompatible",
|
|
1068
|
+
negotiatedProtocol: null,
|
|
1069
|
+
protocol: {
|
|
1070
|
+
localRange,
|
|
1071
|
+
remoteRange
|
|
1072
|
+
},
|
|
1073
|
+
capabilities,
|
|
1074
|
+
reason: {
|
|
1075
|
+
code: "protocol.incompatible",
|
|
1076
|
+
message: `no common protocol major: local speaks [${localRange[0]}, ${localRange[1]}], remote speaks [${remoteRange[0]}, ${remoteRange[1]}]`,
|
|
1077
|
+
detail: {
|
|
1078
|
+
localRange,
|
|
1079
|
+
remoteRange
|
|
1080
|
+
}
|
|
1081
|
+
},
|
|
1082
|
+
local,
|
|
1083
|
+
remote
|
|
1084
|
+
};
|
|
1085
|
+
}
|
|
1086
|
+
const negotiatedProtocol = Math.min(localRange[1], remoteRange[1]);
|
|
1087
|
+
const required = opts.required ?? [];
|
|
1088
|
+
const missingRequired = difference(required, remote.capabilities);
|
|
1089
|
+
if (missingRequired.length > 0) {
|
|
1090
|
+
return {
|
|
1091
|
+
outcome: "degraded",
|
|
1092
|
+
negotiatedProtocol,
|
|
1093
|
+
protocol: {
|
|
1094
|
+
localRange,
|
|
1095
|
+
remoteRange
|
|
1096
|
+
},
|
|
1097
|
+
capabilities,
|
|
1098
|
+
reason: {
|
|
1099
|
+
code: "capability.unavailable",
|
|
1100
|
+
message: `remote is missing required capability(ies): ${missingRequired.join(", ")}`,
|
|
1101
|
+
detail: {
|
|
1102
|
+
missingRequired,
|
|
1103
|
+
missingOnRemote,
|
|
1104
|
+
missingOnLocal
|
|
1105
|
+
}
|
|
1106
|
+
},
|
|
1107
|
+
local,
|
|
1108
|
+
remote
|
|
1109
|
+
};
|
|
1110
|
+
}
|
|
1111
|
+
if (missingOnRemote.length > 0 || missingOnLocal.length > 0) {
|
|
1112
|
+
const parts = [];
|
|
1113
|
+
if (missingOnRemote.length > 0) parts.push(`remote lacks ${missingOnRemote.join(", ")}`);
|
|
1114
|
+
if (missingOnLocal.length > 0) parts.push(`local lacks ${missingOnLocal.join(", ")}`);
|
|
1115
|
+
return {
|
|
1116
|
+
outcome: "degraded",
|
|
1117
|
+
negotiatedProtocol,
|
|
1118
|
+
protocol: {
|
|
1119
|
+
localRange,
|
|
1120
|
+
remoteRange
|
|
1121
|
+
},
|
|
1122
|
+
capabilities,
|
|
1123
|
+
reason: {
|
|
1124
|
+
code: "capability.unavailable",
|
|
1125
|
+
message: `capability mismatch: ${parts.join("; ")}`,
|
|
1126
|
+
detail: {
|
|
1127
|
+
missingOnRemote,
|
|
1128
|
+
missingOnLocal
|
|
1129
|
+
}
|
|
1130
|
+
},
|
|
1131
|
+
local,
|
|
1132
|
+
remote
|
|
1133
|
+
};
|
|
1134
|
+
}
|
|
1135
|
+
return {
|
|
1136
|
+
outcome: "compatible",
|
|
1137
|
+
negotiatedProtocol,
|
|
1138
|
+
protocol: {
|
|
1139
|
+
localRange,
|
|
1140
|
+
remoteRange
|
|
1141
|
+
},
|
|
1142
|
+
capabilities,
|
|
1143
|
+
local,
|
|
1144
|
+
remote
|
|
1145
|
+
};
|
|
1146
|
+
}
|
|
1147
|
+
__name(negotiate, "negotiate");
|
|
1148
|
+
|
|
1149
|
+
// src/handshake/routing.ts
|
|
1150
|
+
function requiredCapabilities(handler) {
|
|
1151
|
+
const list = Array.isArray(handler) ? handler : handler.requires ?? [];
|
|
1152
|
+
return [
|
|
1153
|
+
...new Set(list)
|
|
1154
|
+
];
|
|
1155
|
+
}
|
|
1156
|
+
__name(requiredCapabilities, "requiredCapabilities");
|
|
1157
|
+
function canRoute(handler, worker) {
|
|
1158
|
+
const required = requiredCapabilities(handler);
|
|
1159
|
+
if (required.length === 0) return true;
|
|
1160
|
+
const advertised = new Set(normalizeDescriptor(worker).capabilities);
|
|
1161
|
+
return required.every((cap) => advertised.has(cap));
|
|
1162
|
+
}
|
|
1163
|
+
__name(canRoute, "canRoute");
|
|
1164
|
+
function missingCapabilities(handler, worker) {
|
|
1165
|
+
const advertised = new Set(normalizeDescriptor(worker).capabilities);
|
|
1166
|
+
return requiredCapabilities(handler).filter((cap) => !advertised.has(cap));
|
|
1167
|
+
}
|
|
1168
|
+
__name(missingCapabilities, "missingCapabilities");
|
|
1169
|
+
function capableWorkers(handler, workers) {
|
|
1170
|
+
return workers.filter((w) => canRoute(handler, w));
|
|
1171
|
+
}
|
|
1172
|
+
__name(capableWorkers, "capableWorkers");
|
|
1173
|
+
function resolveRouting(handler, workers) {
|
|
1174
|
+
const requires = requiredCapabilities(handler);
|
|
1175
|
+
const capable = capableWorkers(handler, workers).map(normalizeDescriptor);
|
|
1176
|
+
if (capable.length > 0) {
|
|
1177
|
+
return {
|
|
1178
|
+
status: "routable",
|
|
1179
|
+
workers: capable,
|
|
1180
|
+
requires
|
|
1181
|
+
};
|
|
1182
|
+
}
|
|
1183
|
+
const suffix = requires.length > 0 ? ` (requires ${requires.join(", ")})` : "";
|
|
1184
|
+
return {
|
|
1185
|
+
status: "blocked",
|
|
1186
|
+
reason: `blocked: no compatible worker${suffix}`,
|
|
1187
|
+
requires
|
|
1188
|
+
};
|
|
1189
|
+
}
|
|
1190
|
+
__name(resolveRouting, "resolveRouting");
|
|
1191
|
+
|
|
1192
|
+
// src/handshake/dispatch-routing.ts
|
|
1193
|
+
function controlPlaneDescriptor(opts) {
|
|
1194
|
+
const version = CURRENT_PROTOCOL_VERSION;
|
|
1195
|
+
return {
|
|
1196
|
+
instanceId: opts.instanceId,
|
|
1197
|
+
runtime: "node",
|
|
1198
|
+
sdk: opts.sdk ?? {
|
|
1199
|
+
name: "@dudousxd/nestjs-durable-core",
|
|
1200
|
+
version: String(version)
|
|
1201
|
+
},
|
|
1202
|
+
protocol: opts.protocol ?? {
|
|
1203
|
+
version,
|
|
1204
|
+
range: [
|
|
1205
|
+
version,
|
|
1206
|
+
version
|
|
1207
|
+
]
|
|
1208
|
+
},
|
|
1209
|
+
capabilities: opts.capabilities ?? [],
|
|
1210
|
+
workflows: [],
|
|
1211
|
+
steps: [],
|
|
1212
|
+
...opts.partition !== void 0 ? {
|
|
1213
|
+
partition: opts.partition
|
|
1214
|
+
} : {},
|
|
1215
|
+
...opts.namespace !== void 0 ? {
|
|
1216
|
+
namespace: opts.namespace
|
|
1217
|
+
} : {},
|
|
1218
|
+
startedAt: opts.startedAt ?? 0
|
|
1219
|
+
};
|
|
1220
|
+
}
|
|
1221
|
+
__name(controlPlaneDescriptor, "controlPlaneDescriptor");
|
|
1222
|
+
var WorkflowBlocked = class extends Error {
|
|
1223
|
+
static {
|
|
1224
|
+
__name(this, "WorkflowBlocked");
|
|
1225
|
+
}
|
|
1226
|
+
plan;
|
|
1227
|
+
constructor(plan) {
|
|
1228
|
+
super(plan.reason), this.plan = plan;
|
|
1229
|
+
this.name = "WorkflowBlocked";
|
|
1230
|
+
}
|
|
1231
|
+
};
|
|
1232
|
+
function planDispatch(requires, liveDescriptors, cp, token) {
|
|
1233
|
+
const required = requiredCapabilities({
|
|
1234
|
+
requires
|
|
1235
|
+
});
|
|
1236
|
+
const workers = liveDescriptors.map(normalizeDescriptor);
|
|
1237
|
+
const capable = workers.filter((w) => canRoute({
|
|
1238
|
+
requires
|
|
1239
|
+
}, w));
|
|
1240
|
+
const compatible = capable.filter((w) => negotiate(cp, w).outcome !== "incompatible");
|
|
1241
|
+
if (compatible.length > 0) return {
|
|
1242
|
+
status: "routable",
|
|
1243
|
+
workers: compatible
|
|
1244
|
+
};
|
|
1245
|
+
if (capable.length > 0) {
|
|
1246
|
+
const workerRanges = capable.map((w) => w.protocol.range);
|
|
1247
|
+
return {
|
|
1248
|
+
status: "blocked",
|
|
1249
|
+
code: "protocol.incompatible",
|
|
1250
|
+
reason: `blocked: no protocol-compatible worker${required.length > 0 ? ` (requires ${required.join(", ")})` : ""}`,
|
|
1251
|
+
requires: required,
|
|
1252
|
+
diagnostics: {
|
|
1253
|
+
code: "protocol.incompatible",
|
|
1254
|
+
token,
|
|
1255
|
+
requires: required,
|
|
1256
|
+
liveWorkers: workers.length,
|
|
1257
|
+
controlPlaneRange: cp.protocol.range,
|
|
1258
|
+
workerRanges,
|
|
1259
|
+
controlPlane: cp,
|
|
1260
|
+
workers
|
|
1261
|
+
}
|
|
1262
|
+
};
|
|
1263
|
+
}
|
|
1264
|
+
const advertised = new Set(workers.flatMap((w) => w.capabilities));
|
|
1265
|
+
const missingCapabilities2 = required.filter((cap) => !advertised.has(cap));
|
|
1266
|
+
return {
|
|
1267
|
+
status: "blocked",
|
|
1268
|
+
code: "capability.unavailable",
|
|
1269
|
+
reason: `blocked: no compatible worker${required.length > 0 ? ` (requires ${required.join(", ")})` : ""}`,
|
|
1270
|
+
requires: required,
|
|
1271
|
+
diagnostics: {
|
|
1272
|
+
code: "capability.unavailable",
|
|
1273
|
+
token,
|
|
1274
|
+
requires: required,
|
|
1275
|
+
liveWorkers: workers.length,
|
|
1276
|
+
missingCapabilities: missingCapabilities2,
|
|
1277
|
+
controlPlane: cp,
|
|
1278
|
+
workers
|
|
1279
|
+
}
|
|
1280
|
+
};
|
|
1281
|
+
}
|
|
1282
|
+
__name(planDispatch, "planDispatch");
|
|
1283
|
+
|
|
916
1284
|
// src/tenant-group.ts
|
|
917
1285
|
function tenantGroup(baseGroup, tenant) {
|
|
918
1286
|
return tenant !== void 0 && tenant !== "" && tenant !== "default" ? `${baseGroup}@${tenant}` : baseGroup;
|
|
@@ -1177,6 +1545,22 @@ var TransportPool = class {
|
|
|
1177
1545
|
transport.onStartRun?.(handler);
|
|
1178
1546
|
}
|
|
1179
1547
|
}
|
|
1548
|
+
/**
|
|
1549
|
+
* The live handshake descriptors advertised for `group` (design §7.2), merged across every transport
|
|
1550
|
+
* that can report them. Returns `[]` when NO transport implements
|
|
1551
|
+
* {@link Transport.readWorkerDescriptors} (a pure in-process pool) OR when descriptors are supported
|
|
1552
|
+
* but none is published — both are the "guard disengaged / legacy assume-compatible" case, so the
|
|
1553
|
+
* engine's routing guard treats an empty result identically (design §7.7).
|
|
1554
|
+
*/
|
|
1555
|
+
async readWorkerDescriptors(group) {
|
|
1556
|
+
const descriptors = [];
|
|
1557
|
+
for (const { transport } of this.transports) {
|
|
1558
|
+
if (transport.readWorkerDescriptors) {
|
|
1559
|
+
descriptors.push(...await transport.readWorkerDescriptors(group));
|
|
1560
|
+
}
|
|
1561
|
+
}
|
|
1562
|
+
return descriptors;
|
|
1563
|
+
}
|
|
1180
1564
|
/** Distinct worker groups with a live heartbeat, merged across every transport that can report it. */
|
|
1181
1565
|
async listWorkerGroups() {
|
|
1182
1566
|
const groups = /* @__PURE__ */ new Set();
|
|
@@ -1272,6 +1656,8 @@ function resolveStepPolicy(config, opts) {
|
|
|
1272
1656
|
if (jitter !== void 0) policy.jitter = jitter;
|
|
1273
1657
|
const timeoutMs = opts?.timeoutMs ?? config?.timeoutMs;
|
|
1274
1658
|
if (timeoutMs !== void 0) policy.timeoutMs = timeoutMs;
|
|
1659
|
+
const requires = opts?.requires ?? config?.requires;
|
|
1660
|
+
if (requires !== void 0) policy.requires = requires;
|
|
1275
1661
|
return policy;
|
|
1276
1662
|
}
|
|
1277
1663
|
__name(resolveStepPolicy, "resolveStepPolicy");
|
|
@@ -1977,6 +2363,10 @@ var WorkflowEngine = class {
|
|
|
1977
2363
|
eventBufferTtlMs;
|
|
1978
2364
|
remoteRedispatchMs;
|
|
1979
2365
|
remoteRedispatchMax;
|
|
2366
|
+
/** How long a `blocked` run waits before the recovery poll re-checks the live fleet (design §7.5). */
|
|
2367
|
+
blockedPollMs;
|
|
2368
|
+
/** This engine's control-plane handshake descriptor — negotiated against each worker (design §7.3). */
|
|
2369
|
+
cpDescriptor;
|
|
1980
2370
|
webhookUrl;
|
|
1981
2371
|
traceparent;
|
|
1982
2372
|
context;
|
|
@@ -2048,6 +2438,13 @@ var WorkflowEngine = class {
|
|
|
2048
2438
|
this.eventBufferTtlMs = deps.eventBufferTtlMs || void 0;
|
|
2049
2439
|
this.remoteRedispatchMs = deps.remoteRedispatchMs || void 0;
|
|
2050
2440
|
this.remoteRedispatchMax = Math.max(1, deps.remoteRedispatchMax ?? 10);
|
|
2441
|
+
this.blockedPollMs = deps.blockedPollMs ?? 5e3;
|
|
2442
|
+
this.cpDescriptor = controlPlaneDescriptor({
|
|
2443
|
+
instanceId: `cp-${this.instanceId}`,
|
|
2444
|
+
...this.namespace !== void 0 ? {
|
|
2445
|
+
namespace: this.namespace
|
|
2446
|
+
} : {}
|
|
2447
|
+
});
|
|
2051
2448
|
this.webhookUrl = deps.webhookUrl;
|
|
2052
2449
|
this.traceparent = deps.traceparent;
|
|
2053
2450
|
this.context = deps.context;
|
|
@@ -2201,6 +2598,7 @@ var WorkflowEngine = class {
|
|
|
2201
2598
|
searchAttributesSchema: opts?.searchAttributesSchema,
|
|
2202
2599
|
onEvent: opts?.onEvent,
|
|
2203
2600
|
eventBatch: opts?.eventBatch,
|
|
2601
|
+
requires: opts?.requires,
|
|
2204
2602
|
// A real, retained in-process body — so `workflowBody` can hand it to an in-app worker, and so
|
|
2205
2603
|
// it is NOT confused with the throwing stub `registerRemote` installs for a body-less remote.
|
|
2206
2604
|
hasBody: true
|
|
@@ -2249,6 +2647,7 @@ var WorkflowEngine = class {
|
|
|
2249
2647
|
singleton: opts.singleton,
|
|
2250
2648
|
executionTimeoutMs: opts.executionTimeout != null ? parseDuration(opts.executionTimeout) : void 0,
|
|
2251
2649
|
validateInput: opts.validateInput,
|
|
2650
|
+
requires: opts.requires,
|
|
2252
2651
|
remote: {
|
|
2253
2652
|
group: opts.group,
|
|
2254
2653
|
executor: opts.executor
|
|
@@ -2732,9 +3131,13 @@ var WorkflowEngine = class {
|
|
|
2732
3131
|
*/
|
|
2733
3132
|
async resumeDueTimers(nowMs = this.clock()) {
|
|
2734
3133
|
const due = await this.store.listDueTimers(nowMs, this.namespace);
|
|
2735
|
-
|
|
3134
|
+
const blocked = await this.dueBlockedRuns(nowMs);
|
|
3135
|
+
if (due.length === 0 && blocked.length === 0) return [];
|
|
2736
3136
|
const waiterByRun = indexWaitersByRun(await this.store.listSignalWaiters(""));
|
|
2737
|
-
return this.resumeLeased(
|
|
3137
|
+
return this.resumeLeased([
|
|
3138
|
+
...due,
|
|
3139
|
+
...blocked
|
|
3140
|
+
], nowMs, (run) => this.reconcileSignalWaiter(run, waiterByRun.get(run.id)));
|
|
2738
3141
|
}
|
|
2739
3142
|
/**
|
|
2740
3143
|
* The reconcile safety net's per-run check (see {@link resumeDueTimers}): if this suspended run has
|
|
@@ -3643,6 +4046,7 @@ var WorkflowEngine = class {
|
|
|
3643
4046
|
error: outcome.error
|
|
3644
4047
|
};
|
|
3645
4048
|
}
|
|
4049
|
+
if (outcome.kind === "blocked") return this.parkBlocked(run, outcome.blocked);
|
|
3646
4050
|
const latest = await this.store.getRun(run.id);
|
|
3647
4051
|
if (latest?.status === "cancelled") {
|
|
3648
4052
|
return {
|
|
@@ -3742,6 +4146,8 @@ var WorkflowEngine = class {
|
|
|
3742
4146
|
status: "suspended"
|
|
3743
4147
|
};
|
|
3744
4148
|
}
|
|
4149
|
+
const wfPlan = await this.planRoute(tenantGroup(sanitizeQueueToken(remote.group), void 0), registered.requires);
|
|
4150
|
+
if (wfPlan?.status === "blocked") return this.parkBlocked(run, wfPlan);
|
|
3745
4151
|
const history = await this.remoteHistory(run.id);
|
|
3746
4152
|
if (remote.executor.dispatch) {
|
|
3747
4153
|
const taskId = `${run.id}:wf:${globalThis.crypto.randomUUID().slice(0, 8)}`;
|
|
@@ -4165,6 +4571,15 @@ var WorkflowEngine = class {
|
|
|
4165
4571
|
status: "completed"
|
|
4166
4572
|
};
|
|
4167
4573
|
}
|
|
4574
|
+
if (err instanceof WorkflowBlocked) {
|
|
4575
|
+
if (this.cancelRequested.has(run.id)) {
|
|
4576
|
+
return this.compensateAndCancel(run, compensations, replay);
|
|
4577
|
+
}
|
|
4578
|
+
return this.settleRun(run, {
|
|
4579
|
+
kind: "blocked",
|
|
4580
|
+
blocked: err.plan
|
|
4581
|
+
});
|
|
4582
|
+
}
|
|
4168
4583
|
if (err instanceof WorkflowSuspended) {
|
|
4169
4584
|
if (this.cancelRequested.has(run.id)) {
|
|
4170
4585
|
return this.compensateAndCancel(run, compensations, replay);
|
|
@@ -4511,6 +4926,85 @@ var WorkflowEngine = class {
|
|
|
4511
4926
|
run: result
|
|
4512
4927
|
};
|
|
4513
4928
|
}
|
|
4929
|
+
/**
|
|
4930
|
+
* Plan a dispatch to `token` for a handler requiring `requires` (handshake design §7.5). Returns:
|
|
4931
|
+
* - `undefined` — NO live descriptors published on this token: the guard does not engage, so a
|
|
4932
|
+
* pre-handshake fleet (or an in-process transport that advertises none) keeps flowing exactly as
|
|
4933
|
+
* before (design §7.7);
|
|
4934
|
+
* - a `routable` plan — ≥1 live worker is capability-capable AND protocol-compatible (dispatch);
|
|
4935
|
+
* - a `blocked` plan — descriptors exist but none can run it (park the run, never dispatch into it).
|
|
4936
|
+
*/
|
|
4937
|
+
async planRoute(token, requires) {
|
|
4938
|
+
const descriptors = await this.pool.readWorkerDescriptors(token);
|
|
4939
|
+
if (descriptors.length === 0) return void 0;
|
|
4940
|
+
return planDispatch(requires ?? [], descriptors, this.cpDescriptor, token);
|
|
4941
|
+
}
|
|
4942
|
+
/** Throw {@link WorkflowBlocked} when no live worker can run a dispatch to `token` — so the caller's
|
|
4943
|
+
* run-execution catch parks the run `blocked` (with the plan) instead of persisting a `pending`
|
|
4944
|
+
* checkpoint + dispatching into a queue nobody consumes (design §7.5). A no-op when the guard does
|
|
4945
|
+
* not engage (no descriptors) or a capable+compatible worker exists. */
|
|
4946
|
+
async ensureRoutable(token, requires) {
|
|
4947
|
+
const plan = await this.planRoute(token, requires);
|
|
4948
|
+
if (plan?.status === "blocked") throw new WorkflowBlocked(plan);
|
|
4949
|
+
}
|
|
4950
|
+
/**
|
|
4951
|
+
* Park a run `blocked` (handshake design §7.5/§7.6): no live worker on its next-dispatch group is
|
|
4952
|
+
* capability-capable + protocol-compatible. Persists the `blocked` status with a human reason + the
|
|
4953
|
+
* machine `code` + a `wakeAt` so the blocked-recovery poll re-drives it, emits the LOUD structured
|
|
4954
|
+
* `capability.unavailable`/`protocol.incompatible` diagnostics (both descriptors + the precise delta),
|
|
4955
|
+
* and releases the run lease so any instance can re-drive it. Re-checks the persisted status first so
|
|
4956
|
+
* a concurrent cancel/complete is never clobbered. `releaseRunLock` is idempotent, so a caller whose
|
|
4957
|
+
* own `finally` also releases (the run-execution path) is safe.
|
|
4958
|
+
*/
|
|
4959
|
+
async parkBlocked(run, blocked) {
|
|
4960
|
+
const current = await this.store.getRun(run.id);
|
|
4961
|
+
if (current && (current.status === "cancelled" || current.status === "completed")) {
|
|
4962
|
+
await this.store.releaseRunLock(run.id);
|
|
4963
|
+
return {
|
|
4964
|
+
runId: run.id,
|
|
4965
|
+
status: current.status,
|
|
4966
|
+
output: current.output,
|
|
4967
|
+
error: current.error
|
|
4968
|
+
};
|
|
4969
|
+
}
|
|
4970
|
+
const error = {
|
|
4971
|
+
message: blocked.reason,
|
|
4972
|
+
code: blocked.code,
|
|
4973
|
+
retryable: true
|
|
4974
|
+
};
|
|
4975
|
+
await this.store.updateRun(run.id, {
|
|
4976
|
+
status: "blocked",
|
|
4977
|
+
error,
|
|
4978
|
+
wakeAt: this.clock() + this.blockedPollMs,
|
|
4979
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
4980
|
+
});
|
|
4981
|
+
this.emit({
|
|
4982
|
+
type: "run.blocked",
|
|
4983
|
+
runId: run.id,
|
|
4984
|
+
workflow: run.workflow,
|
|
4985
|
+
namespace: run.namespace,
|
|
4986
|
+
error,
|
|
4987
|
+
requires: blocked.requires,
|
|
4988
|
+
diagnostics: blocked.diagnostics
|
|
4989
|
+
});
|
|
4990
|
+
await this.store.releaseRunLock(run.id);
|
|
4991
|
+
return {
|
|
4992
|
+
runId: run.id,
|
|
4993
|
+
status: "blocked",
|
|
4994
|
+
error
|
|
4995
|
+
};
|
|
4996
|
+
}
|
|
4997
|
+
/** Runs parked `blocked` whose recovery poll is due (`wakeAt <= nowMs`), namespace-scoped like
|
|
4998
|
+
* {@link resumeDueTimers}. Re-driving one re-checks the live fleet: it dispatches if a
|
|
4999
|
+
* capable+compatible worker has appeared, else re-parks with a fresh `wakeAt` (design §7.5). */
|
|
5000
|
+
async dueBlockedRuns(nowMs) {
|
|
5001
|
+
const blocked = await this.store.listRuns({
|
|
5002
|
+
statuses: [
|
|
5003
|
+
"blocked"
|
|
5004
|
+
]
|
|
5005
|
+
});
|
|
5006
|
+
return blocked.filter((r) => (this.namespace === void 0 || r.namespace === void 0 || r.namespace === this.namespace) && r.wakeAt !== void 0 && r.wakeAt <= nowMs);
|
|
5007
|
+
}
|
|
4514
5008
|
async callRemote(runId, seq, step, input, queue, transport, replay, admission) {
|
|
4515
5009
|
const existing = replay?.get(seq) ?? await this.store.getCheckpoint(runId, seq);
|
|
4516
5010
|
if (existing && existing.name !== step.name) {
|
|
@@ -4576,6 +5070,7 @@ var WorkflowEngine = class {
|
|
|
4576
5070
|
if (this.clock() < existing.wakeAt) throw new WorkflowSuspended(existing.wakeAt);
|
|
4577
5071
|
attempt = existing.attempts + 1;
|
|
4578
5072
|
}
|
|
5073
|
+
await this.ensureRoutable(tenantGroup(sanitizeQueueToken(step.name), step.partition), step.requires);
|
|
4579
5074
|
const id = stepId(runId, seq);
|
|
4580
5075
|
if (queue && this.admission.handles(queue)) {
|
|
4581
5076
|
const decision = await this.admission.tryAdmit(queue, {
|
|
@@ -4762,6 +5257,7 @@ var WorkflowEngine = class {
|
|
|
4762
5257
|
/** In-memory await path for a remote step with a liveness `timeoutMs` (re-dispatch on timeout). */
|
|
4763
5258
|
async callRemoteInMemory(runId, seq, step, input, transport) {
|
|
4764
5259
|
if (this.pool.size === 0) throw new Error("remote steps require a Transport");
|
|
5260
|
+
await this.ensureRoutable(tenantGroup(sanitizeQueueToken(step.name), step.partition), step.requires);
|
|
4765
5261
|
const validInput = step.input ? step.input.parse(input) : input;
|
|
4766
5262
|
const id = stepId(runId, seq);
|
|
4767
5263
|
const enqueuedAt = /* @__PURE__ */ new Date();
|
|
@@ -5661,6 +6157,7 @@ var InMemoryTransport = class {
|
|
|
5661
6157
|
// Annotate the CommonJS export names for ESM import in node:
|
|
5662
6158
|
0 && (module.exports = {
|
|
5663
6159
|
CONTROL_FLOW_SIGNAL,
|
|
6160
|
+
CURRENT_PROTOCOL_VERSION,
|
|
5664
6161
|
CodecStateStore,
|
|
5665
6162
|
ContinueAsNew,
|
|
5666
6163
|
DURABLE_OPTIONS,
|
|
@@ -5677,6 +6174,8 @@ var InMemoryTransport = class {
|
|
|
5677
6174
|
InMemoryAdmissionBackend,
|
|
5678
6175
|
InMemoryStateStore,
|
|
5679
6176
|
InMemoryTransport,
|
|
6177
|
+
LEGACY_V1_CAPABILITIES,
|
|
6178
|
+
LEGACY_V1_PROTOCOL,
|
|
5680
6179
|
NonDeterminismError,
|
|
5681
6180
|
QueueController,
|
|
5682
6181
|
RemoteStepError,
|
|
@@ -5691,6 +6190,7 @@ var InMemoryTransport = class {
|
|
|
5691
6190
|
TRANSPORT,
|
|
5692
6191
|
TRANSPORT_CANONICAL,
|
|
5693
6192
|
WORKFLOW_NAME_KEY,
|
|
6193
|
+
WorkflowBlocked,
|
|
5694
6194
|
WorkflowEngine,
|
|
5695
6195
|
WorkflowSuspended,
|
|
5696
6196
|
applyAttributeQuery,
|
|
@@ -5698,23 +6198,35 @@ var InMemoryTransport = class {
|
|
|
5698
6198
|
attributeOperand,
|
|
5699
6199
|
bindWorkflowClass,
|
|
5700
6200
|
breakpointToken,
|
|
6201
|
+
canRoute,
|
|
6202
|
+
capableWorkers,
|
|
5701
6203
|
classifyWaiterToken,
|
|
5702
6204
|
collectMetrics,
|
|
6205
|
+
controlPlaneDescriptor,
|
|
5703
6206
|
createStepLogger,
|
|
5704
6207
|
currentWorkflowCtx,
|
|
6208
|
+
descriptorHash,
|
|
5705
6209
|
eventMatchOf,
|
|
5706
6210
|
eventMatches,
|
|
5707
6211
|
eventNameOf,
|
|
5708
6212
|
eventPrefix,
|
|
5709
6213
|
eventToken,
|
|
6214
|
+
heartbeatStatus,
|
|
5710
6215
|
indexWaitersByRun,
|
|
5711
6216
|
isEventToken,
|
|
6217
|
+
isLegacyDescriptor,
|
|
5712
6218
|
isWorkflowControlFlowSignal,
|
|
5713
6219
|
matchesAttributes,
|
|
6220
|
+
missingCapabilities,
|
|
6221
|
+
negotiate,
|
|
5714
6222
|
normalizeAttributeRows,
|
|
6223
|
+
normalizeDescriptor,
|
|
5715
6224
|
parseDuration,
|
|
6225
|
+
planDispatch,
|
|
5716
6226
|
prevCronFireMs,
|
|
5717
6227
|
readSearchAttributes,
|
|
6228
|
+
requiredCapabilities,
|
|
6229
|
+
resolveRouting,
|
|
5718
6230
|
resolveRunWaiting,
|
|
5719
6231
|
runInWorkflowCtx,
|
|
5720
6232
|
runSchedules,
|