@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.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
|
|
@@ -2817,7 +3220,10 @@ var WorkflowEngine = class {
|
|
|
2817
3220
|
* inline — or null if the run is unknown. The dashboard "retry" goes through here so it can't block
|
|
2818
3221
|
* the HTTP request on workflow execution.
|
|
2819
3222
|
*/
|
|
2820
|
-
async requeue(runId) {
|
|
3223
|
+
async requeue(runId, _seen) {
|
|
3224
|
+
const seen = _seen ?? /* @__PURE__ */ new Set();
|
|
3225
|
+
if (seen.has(runId)) return null;
|
|
3226
|
+
seen.add(runId);
|
|
2821
3227
|
const run = await this.store.getRun(runId);
|
|
2822
3228
|
if (!run) return null;
|
|
2823
3229
|
if (run.status === "failed" || run.status === "dead") {
|
|
@@ -2841,12 +3247,28 @@ var WorkflowEngine = class {
|
|
|
2841
3247
|
status: "running",
|
|
2842
3248
|
output: void 0
|
|
2843
3249
|
});
|
|
3250
|
+
const childId = cp.name.slice("signal:child:".length);
|
|
3251
|
+
const parked = [];
|
|
3252
|
+
for (; ; ) {
|
|
3253
|
+
const buffered = await this.store.takeBufferedSignal(`child:${childId}`);
|
|
3254
|
+
if (!buffered) break;
|
|
3255
|
+
parked.push(buffered.payload);
|
|
3256
|
+
}
|
|
3257
|
+
for (const payload of parked) await this.store.bufferSignal(`child:${childId}`, payload);
|
|
3258
|
+
const bufferedSuccess = parked.some((payload) => typeof payload === "object" && payload !== null && payload.ok === true);
|
|
3259
|
+
if (!bufferedSuccess) {
|
|
3260
|
+
const child = await this.store.getRun(childId);
|
|
3261
|
+
if (child && (child.status === "failed" || child.status === "dead")) {
|
|
3262
|
+
await this.requeue(childId, seen);
|
|
3263
|
+
}
|
|
3264
|
+
}
|
|
2844
3265
|
}
|
|
2845
3266
|
}
|
|
2846
3267
|
}
|
|
2847
3268
|
await this.store.releaseRunLock(runId);
|
|
2848
3269
|
await this.store.updateRun(runId, {
|
|
2849
3270
|
status: "pending",
|
|
3271
|
+
error: void 0,
|
|
2850
3272
|
updatedAt: /* @__PURE__ */ new Date()
|
|
2851
3273
|
});
|
|
2852
3274
|
await this.runDispatcher.dispatch(runId);
|
|
@@ -3166,6 +3588,10 @@ var WorkflowEngine = class {
|
|
|
3166
3588
|
*/
|
|
3167
3589
|
notifyParent(runId, completion) {
|
|
3168
3590
|
void this.signal(`child:${runId}`, completion).catch(() => void 0);
|
|
3591
|
+
const at = runId.lastIndexOf("~retry~");
|
|
3592
|
+
if (at !== -1 && completion.ok) {
|
|
3593
|
+
void this.signal(`child:${runId.slice(0, at)}`, completion).catch(() => void 0);
|
|
3594
|
+
}
|
|
3169
3595
|
}
|
|
3170
3596
|
/**
|
|
3171
3597
|
* Deferred child start shared by the in-process ctx host and the remote `startChild` command.
|
|
@@ -3620,6 +4046,7 @@ var WorkflowEngine = class {
|
|
|
3620
4046
|
error: outcome.error
|
|
3621
4047
|
};
|
|
3622
4048
|
}
|
|
4049
|
+
if (outcome.kind === "blocked") return this.parkBlocked(run, outcome.blocked);
|
|
3623
4050
|
const latest = await this.store.getRun(run.id);
|
|
3624
4051
|
if (latest?.status === "cancelled") {
|
|
3625
4052
|
return {
|
|
@@ -3719,6 +4146,8 @@ var WorkflowEngine = class {
|
|
|
3719
4146
|
status: "suspended"
|
|
3720
4147
|
};
|
|
3721
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);
|
|
3722
4151
|
const history = await this.remoteHistory(run.id);
|
|
3723
4152
|
if (remote.executor.dispatch) {
|
|
3724
4153
|
const taskId = `${run.id}:wf:${globalThis.crypto.randomUUID().slice(0, 8)}`;
|
|
@@ -4142,6 +4571,15 @@ var WorkflowEngine = class {
|
|
|
4142
4571
|
status: "completed"
|
|
4143
4572
|
};
|
|
4144
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
|
+
}
|
|
4145
4583
|
if (err instanceof WorkflowSuspended) {
|
|
4146
4584
|
if (this.cancelRequested.has(run.id)) {
|
|
4147
4585
|
return this.compensateAndCancel(run, compensations, replay);
|
|
@@ -4488,6 +4926,85 @@ var WorkflowEngine = class {
|
|
|
4488
4926
|
run: result
|
|
4489
4927
|
};
|
|
4490
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
|
+
}
|
|
4491
5008
|
async callRemote(runId, seq, step, input, queue, transport, replay, admission) {
|
|
4492
5009
|
const existing = replay?.get(seq) ?? await this.store.getCheckpoint(runId, seq);
|
|
4493
5010
|
if (existing && existing.name !== step.name) {
|
|
@@ -4553,6 +5070,7 @@ var WorkflowEngine = class {
|
|
|
4553
5070
|
if (this.clock() < existing.wakeAt) throw new WorkflowSuspended(existing.wakeAt);
|
|
4554
5071
|
attempt = existing.attempts + 1;
|
|
4555
5072
|
}
|
|
5073
|
+
await this.ensureRoutable(tenantGroup(sanitizeQueueToken(step.name), step.partition), step.requires);
|
|
4556
5074
|
const id = stepId(runId, seq);
|
|
4557
5075
|
if (queue && this.admission.handles(queue)) {
|
|
4558
5076
|
const decision = await this.admission.tryAdmit(queue, {
|
|
@@ -4739,6 +5257,7 @@ var WorkflowEngine = class {
|
|
|
4739
5257
|
/** In-memory await path for a remote step with a liveness `timeoutMs` (re-dispatch on timeout). */
|
|
4740
5258
|
async callRemoteInMemory(runId, seq, step, input, transport) {
|
|
4741
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);
|
|
4742
5261
|
const validInput = step.input ? step.input.parse(input) : input;
|
|
4743
5262
|
const id = stepId(runId, seq);
|
|
4744
5263
|
const enqueuedAt = /* @__PURE__ */ new Date();
|
|
@@ -5638,6 +6157,7 @@ var InMemoryTransport = class {
|
|
|
5638
6157
|
// Annotate the CommonJS export names for ESM import in node:
|
|
5639
6158
|
0 && (module.exports = {
|
|
5640
6159
|
CONTROL_FLOW_SIGNAL,
|
|
6160
|
+
CURRENT_PROTOCOL_VERSION,
|
|
5641
6161
|
CodecStateStore,
|
|
5642
6162
|
ContinueAsNew,
|
|
5643
6163
|
DURABLE_OPTIONS,
|
|
@@ -5654,6 +6174,8 @@ var InMemoryTransport = class {
|
|
|
5654
6174
|
InMemoryAdmissionBackend,
|
|
5655
6175
|
InMemoryStateStore,
|
|
5656
6176
|
InMemoryTransport,
|
|
6177
|
+
LEGACY_V1_CAPABILITIES,
|
|
6178
|
+
LEGACY_V1_PROTOCOL,
|
|
5657
6179
|
NonDeterminismError,
|
|
5658
6180
|
QueueController,
|
|
5659
6181
|
RemoteStepError,
|
|
@@ -5668,6 +6190,7 @@ var InMemoryTransport = class {
|
|
|
5668
6190
|
TRANSPORT,
|
|
5669
6191
|
TRANSPORT_CANONICAL,
|
|
5670
6192
|
WORKFLOW_NAME_KEY,
|
|
6193
|
+
WorkflowBlocked,
|
|
5671
6194
|
WorkflowEngine,
|
|
5672
6195
|
WorkflowSuspended,
|
|
5673
6196
|
applyAttributeQuery,
|
|
@@ -5675,23 +6198,35 @@ var InMemoryTransport = class {
|
|
|
5675
6198
|
attributeOperand,
|
|
5676
6199
|
bindWorkflowClass,
|
|
5677
6200
|
breakpointToken,
|
|
6201
|
+
canRoute,
|
|
6202
|
+
capableWorkers,
|
|
5678
6203
|
classifyWaiterToken,
|
|
5679
6204
|
collectMetrics,
|
|
6205
|
+
controlPlaneDescriptor,
|
|
5680
6206
|
createStepLogger,
|
|
5681
6207
|
currentWorkflowCtx,
|
|
6208
|
+
descriptorHash,
|
|
5682
6209
|
eventMatchOf,
|
|
5683
6210
|
eventMatches,
|
|
5684
6211
|
eventNameOf,
|
|
5685
6212
|
eventPrefix,
|
|
5686
6213
|
eventToken,
|
|
6214
|
+
heartbeatStatus,
|
|
5687
6215
|
indexWaitersByRun,
|
|
5688
6216
|
isEventToken,
|
|
6217
|
+
isLegacyDescriptor,
|
|
5689
6218
|
isWorkflowControlFlowSignal,
|
|
5690
6219
|
matchesAttributes,
|
|
6220
|
+
missingCapabilities,
|
|
6221
|
+
negotiate,
|
|
5691
6222
|
normalizeAttributeRows,
|
|
6223
|
+
normalizeDescriptor,
|
|
5692
6224
|
parseDuration,
|
|
6225
|
+
planDispatch,
|
|
5693
6226
|
prevCronFireMs,
|
|
5694
6227
|
readSearchAttributes,
|
|
6228
|
+
requiredCapabilities,
|
|
6229
|
+
resolveRouting,
|
|
5695
6230
|
resolveRunWaiting,
|
|
5696
6231
|
runInWorkflowCtx,
|
|
5697
6232
|
runSchedules,
|