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