@nice-code/action 0.10.0 → 0.12.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/build/{ActionDevtoolsCore-yfJ9tkvl.js → ActionDevtoolsCore-9PsnscvK.mjs} +2 -2
- package/build/ActionDevtoolsCore-9PsnscvK.mjs.map +1 -0
- package/build/{ActionDevtoolsCore-B4s6aGvI.d.ts → ActionDevtoolsCore-CCRLYASa.d.cts} +2 -2
- package/build/{ActionDevtoolsCore-Pg7ERO3L.d.ts → ActionDevtoolsCore-CYGD2o6C.d.mts} +2 -2
- package/build/{ActionDevtoolsCore-BLeY_N-3.js → ActionDevtoolsCore-DtgXwPBZ.cjs} +2 -2
- package/build/ActionDevtoolsCore-DtgXwPBZ.cjs.map +1 -0
- package/build/{ActionPayload.types-BN-rXFBK.d.ts → ActionPayload.types-BN-rXFBK.d.cts} +1 -1
- package/build/{ActionPayload.types-D28ELKXC.d.ts → ActionPayload.types-D28ELKXC.d.mts} +1 -1
- package/build/{RunningAction.types-C176rqHG.js → RunningAction.types-C176rqHG.mjs} +1 -1
- package/build/RunningAction.types-C176rqHG.mjs.map +1 -0
- package/build/{RunningAction.types-DjCX1xp5.js → RunningAction.types-DjCX1xp5.cjs} +1 -1
- package/build/RunningAction.types-DjCX1xp5.cjs.map +1 -0
- package/build/devtools/browser/index.cjs +3886 -0
- package/build/devtools/browser/index.cjs.map +1 -0
- package/build/devtools/browser/{index.d.ts → index.d.cts} +2 -2
- package/build/devtools/browser/index.d.mts +17 -0
- package/build/devtools/browser/{index.js → index.mjs} +2 -2
- package/build/devtools/browser/index.mjs.map +1 -0
- package/build/devtools/server/index.cjs +108 -0
- package/build/devtools/server/index.cjs.map +1 -0
- package/build/devtools/server/{index.d.ts → index.d.cts} +2 -2
- package/build/devtools/server/index.d.mts +35 -0
- package/build/devtools/server/{index.js → index.mjs} +3 -3
- package/build/devtools/server/index.mjs.map +1 -0
- package/build/index.cjs +4045 -0
- package/build/index.cjs.map +1 -0
- package/build/{index.d.ts → index.d.cts} +1 -1
- package/build/index.d.mts +2 -0
- package/build/{index.js → index.mjs} +2 -2
- package/build/index.mjs.map +1 -0
- package/build/react-query/index.cjs +70 -0
- package/build/react-query/index.cjs.map +1 -0
- package/build/react-query/{index.d.ts → index.d.cts} +2 -2
- package/build/react-query/index.d.mts +17 -0
- package/build/react-query/{index.js → index.mjs} +1 -1
- package/build/react-query/index.mjs.map +1 -0
- package/package.json +39 -15
- package/build/ActionDevtoolsCore-BLeY_N-3.js.map +0 -1
- package/build/ActionDevtoolsCore-yfJ9tkvl.js.map +0 -1
- package/build/RunningAction.types-C176rqHG.js.map +0 -1
- package/build/RunningAction.types-DjCX1xp5.js.map +0 -1
- package/build/devtools/browser/index.js.map +0 -1
- package/build/devtools/server/index.js.map +0 -1
- package/build/index.js.map +0 -1
- package/build/react-query/index.js.map +0 -1
package/build/index.cjs
ADDED
|
@@ -0,0 +1,4045 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
//#region \0rolldown/runtime.js
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __copyProps = (to, from, except, desc) => {
|
|
10
|
+
if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
11
|
+
key = keys[i];
|
|
12
|
+
if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
|
|
13
|
+
get: ((k) => from[k]).bind(null, key),
|
|
14
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
return to;
|
|
18
|
+
};
|
|
19
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
|
|
20
|
+
value: mod,
|
|
21
|
+
enumerable: true
|
|
22
|
+
}) : target, mod));
|
|
23
|
+
//#endregion
|
|
24
|
+
const require_RunningAction_types = require("./RunningAction.types-DjCX1xp5.cjs");
|
|
25
|
+
let nanoid = require("nanoid");
|
|
26
|
+
let _nice_code_error = require("@nice-code/error");
|
|
27
|
+
let std_env = require("std-env");
|
|
28
|
+
let _nice_code_common_errors = require("@nice-code/common-errors");
|
|
29
|
+
let msgpackr = require("msgpackr");
|
|
30
|
+
let _nice_code_util = require("@nice-code/util");
|
|
31
|
+
let valibot = require("valibot");
|
|
32
|
+
valibot = __toESM(valibot, 1);
|
|
33
|
+
const UNSET_RUNTIME_ENV_ID = "_unset_";
|
|
34
|
+
//#endregion
|
|
35
|
+
//#region src/ActionRuntime/utils/runtimeCoordinateToStringIds.ts
|
|
36
|
+
function runtimeCoordinateToStringIds(coordinate) {
|
|
37
|
+
return [
|
|
38
|
+
`envId[${coordinate.envId}]perId[${coordinate.perId ?? "_"}]:insId[${coordinate.insId ?? "_"}]`,
|
|
39
|
+
`envId[${coordinate.envId}]perId[${coordinate.perId ?? "_"}]:insId[_]`,
|
|
40
|
+
`envId[${coordinate.envId}]perId[_]:insId[_]`
|
|
41
|
+
];
|
|
42
|
+
}
|
|
43
|
+
//#endregion
|
|
44
|
+
//#region src/ActionRuntime/RuntimeCoordinate.ts
|
|
45
|
+
var RuntimeCoordinate = class RuntimeCoordinate {
|
|
46
|
+
envId;
|
|
47
|
+
perId;
|
|
48
|
+
insId;
|
|
49
|
+
static get unknown() {
|
|
50
|
+
return new RuntimeCoordinate({ envId: UNSET_RUNTIME_ENV_ID });
|
|
51
|
+
}
|
|
52
|
+
static env(envId) {
|
|
53
|
+
return new RuntimeCoordinate({ envId });
|
|
54
|
+
}
|
|
55
|
+
withPersistentId(perId) {
|
|
56
|
+
return this.specify({ perId });
|
|
57
|
+
}
|
|
58
|
+
constructor({ envId, perId, insId }) {
|
|
59
|
+
this.envId = envId;
|
|
60
|
+
this.perId = perId;
|
|
61
|
+
this.insId = insId;
|
|
62
|
+
}
|
|
63
|
+
specify(specifics) {
|
|
64
|
+
return new RuntimeCoordinate({
|
|
65
|
+
envId: this.envId,
|
|
66
|
+
perId: this.perId,
|
|
67
|
+
insId: this.insId,
|
|
68
|
+
...specifics
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
specifyIfUnset(specifics) {
|
|
72
|
+
return new RuntimeCoordinate({
|
|
73
|
+
envId: this.envId,
|
|
74
|
+
perId: this.perId ?? specifics.perId,
|
|
75
|
+
insId: this.insId ?? specifics.insId
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
toJsonObject() {
|
|
79
|
+
return {
|
|
80
|
+
envId: this.envId,
|
|
81
|
+
perId: this.perId,
|
|
82
|
+
insId: this.insId
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
isExactlySame(other) {
|
|
86
|
+
return this.envId === other.envId && this.perId === other.perId && this.insId === other.insId;
|
|
87
|
+
}
|
|
88
|
+
isSameFor(other) {
|
|
89
|
+
return {
|
|
90
|
+
id: this.envId === other.envId,
|
|
91
|
+
perId: this.envId === other.envId && this.perId === other.perId,
|
|
92
|
+
insId: this.envId === other.envId && this.perId === other.perId && this.insId === other.insId
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
similarityLevel(other) {
|
|
96
|
+
const sameFor = this.isSameFor(other);
|
|
97
|
+
if (sameFor.insId) return 3;
|
|
98
|
+
if (sameFor.perId) return 2;
|
|
99
|
+
if (sameFor.id) return 1;
|
|
100
|
+
return 0;
|
|
101
|
+
}
|
|
102
|
+
get stringId() {
|
|
103
|
+
return `envId[${this.envId}]perId[${this.perId ?? "_"}]:insId[${this.insId ?? "_"}]`;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Takes the "Runtime Coordinate" and generates a list of full coordinate IDs representing the runtime
|
|
107
|
+
* with decreasing levels of specificity.
|
|
108
|
+
*
|
|
109
|
+
* The first full coordinate ID is the most specific (including instance ID), while the last ID is
|
|
110
|
+
* the least specific (only environment ID).
|
|
111
|
+
*
|
|
112
|
+
* Example output for a RuntimeCoordinate with envId "web_app", perId "user123", and insId "instance456":
|
|
113
|
+
* [
|
|
114
|
+
* "envId[web_app]perId[user123]:insId[instance456]",
|
|
115
|
+
* "envId[web_app]perId[user123]:insId[_]",
|
|
116
|
+
* "envId[web_app]perId[_]:insId[_]"
|
|
117
|
+
* ]
|
|
118
|
+
*
|
|
119
|
+
* @returns a list of "full" runtime coordinate IDs with decreasing accuracy for targeting a runtime.
|
|
120
|
+
*/
|
|
121
|
+
toStringIds() {
|
|
122
|
+
return runtimeCoordinateToStringIds(this);
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
//#endregion
|
|
126
|
+
//#region src/ActionDefinition/Action/ActionBase.ts
|
|
127
|
+
var ActionBase = class {
|
|
128
|
+
form;
|
|
129
|
+
_domain;
|
|
130
|
+
id;
|
|
131
|
+
domain;
|
|
132
|
+
allDomains;
|
|
133
|
+
schema;
|
|
134
|
+
constructor(form, _domain, id) {
|
|
135
|
+
this.form = form;
|
|
136
|
+
this._domain = _domain;
|
|
137
|
+
this.id = id;
|
|
138
|
+
this.domain = _domain.domain;
|
|
139
|
+
this.allDomains = _domain.allDomains;
|
|
140
|
+
this.schema = _domain.actionSchema[id];
|
|
141
|
+
}
|
|
142
|
+
toJsonObject() {
|
|
143
|
+
return {
|
|
144
|
+
form: this.form,
|
|
145
|
+
domain: this.domain,
|
|
146
|
+
allDomains: this.allDomains,
|
|
147
|
+
id: this.id
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
toJsonString() {
|
|
151
|
+
return JSON.stringify(this.toJsonObject());
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
//#endregion
|
|
155
|
+
//#region src/ActionDefinition/Action/Context/ActionContext.ts
|
|
156
|
+
var ActionContext = class extends ActionBase {
|
|
157
|
+
_domain;
|
|
158
|
+
form = "context";
|
|
159
|
+
_routing;
|
|
160
|
+
timeCreated;
|
|
161
|
+
cuid;
|
|
162
|
+
originClient;
|
|
163
|
+
constructor(_domain, id, hydrationData) {
|
|
164
|
+
super("context", _domain, id);
|
|
165
|
+
this._domain = _domain;
|
|
166
|
+
this.timeCreated = hydrationData.timeCreated;
|
|
167
|
+
this.cuid = hydrationData.cuid;
|
|
168
|
+
this._routing = hydrationData.routing;
|
|
169
|
+
this.originClient = hydrationData.originClient;
|
|
170
|
+
}
|
|
171
|
+
_setOriginClient(client) {
|
|
172
|
+
this.originClient = client;
|
|
173
|
+
}
|
|
174
|
+
toJsonString() {
|
|
175
|
+
return JSON.stringify(this.toJsonObject());
|
|
176
|
+
}
|
|
177
|
+
toContextDataJsonObject() {
|
|
178
|
+
return {
|
|
179
|
+
timeCreated: this.timeCreated,
|
|
180
|
+
cuid: this.cuid,
|
|
181
|
+
routing: this.routing.map((item) => ({
|
|
182
|
+
runtime: item.runtime.toJsonObject(),
|
|
183
|
+
handler: item.handler,
|
|
184
|
+
time: item.time
|
|
185
|
+
})),
|
|
186
|
+
originClient: this.originClient.toJsonObject()
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
toJsonObject() {
|
|
190
|
+
return {
|
|
191
|
+
...super.toJsonObject(),
|
|
192
|
+
...this.toContextDataJsonObject()
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
get routing() {
|
|
196
|
+
return this._routing;
|
|
197
|
+
}
|
|
198
|
+
addRouteItem(item) {
|
|
199
|
+
this._routing.push(item);
|
|
200
|
+
}
|
|
201
|
+
deserializeInput(serialized) {
|
|
202
|
+
return this.schema.deserializeInput(serialized);
|
|
203
|
+
}
|
|
204
|
+
serializeInput(raw) {
|
|
205
|
+
return this.schema.serializeInput(raw);
|
|
206
|
+
}
|
|
207
|
+
validateInput(input) {
|
|
208
|
+
return this.schema.validateInput(input, {
|
|
209
|
+
domain: this.domain,
|
|
210
|
+
actionId: this.id
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
validateOutput(output) {
|
|
214
|
+
return this.schema.validateOutput(output, {
|
|
215
|
+
domain: this.domain,
|
|
216
|
+
actionId: this.id
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
//#endregion
|
|
221
|
+
//#region src/ActionDefinition/Action/Payload/ActionPayload.ts
|
|
222
|
+
var ActionPayload = class extends ActionBase {
|
|
223
|
+
form = "data";
|
|
224
|
+
type;
|
|
225
|
+
context;
|
|
226
|
+
time;
|
|
227
|
+
constructor(context, type, data) {
|
|
228
|
+
super("data", context._domain, context.id);
|
|
229
|
+
this.context = context;
|
|
230
|
+
this.type = type;
|
|
231
|
+
this.time = data.time;
|
|
232
|
+
}
|
|
233
|
+
toBaseJsonObject() {
|
|
234
|
+
return {
|
|
235
|
+
...super.toJsonObject(),
|
|
236
|
+
type: this.type,
|
|
237
|
+
context: this.context.toContextDataJsonObject(),
|
|
238
|
+
time: this.time
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
//#endregion
|
|
243
|
+
//#region src/utils/hashPayloadData.ts
|
|
244
|
+
function stableStringify(value) {
|
|
245
|
+
if (value === null || value === void 0) return String(value);
|
|
246
|
+
if (typeof value !== "object") return JSON.stringify(value) ?? "undefined";
|
|
247
|
+
if (Array.isArray(value)) return `[${value.map(stableStringify).join(",")}]`;
|
|
248
|
+
return "{" + Object.keys(value).sort().map((k) => `${JSON.stringify(k)}:${stableStringify(value[k])}`).join(",") + "}";
|
|
249
|
+
}
|
|
250
|
+
function fnv1a32(str) {
|
|
251
|
+
let hash = 2166136261;
|
|
252
|
+
for (let i = 0; i < str.length; i++) hash = (hash ^ str.charCodeAt(i)) * 16777619 >>> 0;
|
|
253
|
+
return hash.toString(16).padStart(8, "0");
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Produces a deterministic 8-char hex hash of any JSON-serializable value.
|
|
257
|
+
* Useful for grouping/comparing action inputs, outputs, and progress payloads.
|
|
258
|
+
*/
|
|
259
|
+
function hashPayloadData(data) {
|
|
260
|
+
return fnv1a32(stableStringify(data));
|
|
261
|
+
}
|
|
262
|
+
//#endregion
|
|
263
|
+
//#region src/ActionDefinition/Action/Payload/ActionPayload.types.ts
|
|
264
|
+
let EActionPayloadType = /* @__PURE__ */ function(EActionPayloadType) {
|
|
265
|
+
EActionPayloadType["request"] = "request";
|
|
266
|
+
EActionPayloadType["progress"] = "progress";
|
|
267
|
+
EActionPayloadType["result"] = "result";
|
|
268
|
+
EActionPayloadType["stream"] = "stream";
|
|
269
|
+
EActionPayloadType["push"] = "push";
|
|
270
|
+
return EActionPayloadType;
|
|
271
|
+
}({});
|
|
272
|
+
/**
|
|
273
|
+
*
|
|
274
|
+
* [ PROGRESS ]
|
|
275
|
+
*
|
|
276
|
+
*/
|
|
277
|
+
let EActionProgressType = /* @__PURE__ */ function(EActionProgressType) {
|
|
278
|
+
EActionProgressType["none"] = "none";
|
|
279
|
+
EActionProgressType["percentage"] = "percentage";
|
|
280
|
+
EActionProgressType["custom"] = "custom";
|
|
281
|
+
return EActionProgressType;
|
|
282
|
+
}({});
|
|
283
|
+
//#endregion
|
|
284
|
+
//#region src/ActionDefinition/Action/Payload/ActionPayload_Progress.ts
|
|
285
|
+
var ActionPayload_Progress = class extends ActionPayload {
|
|
286
|
+
progress;
|
|
287
|
+
constructor(params, progress, data) {
|
|
288
|
+
super(params.context, "progress", data);
|
|
289
|
+
this.progress = progress;
|
|
290
|
+
}
|
|
291
|
+
toJsonObject() {
|
|
292
|
+
return {
|
|
293
|
+
...this.toBaseJsonObject(),
|
|
294
|
+
progress: this.progress
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
toJsonString() {
|
|
298
|
+
return JSON.stringify(this.toJsonObject());
|
|
299
|
+
}
|
|
300
|
+
toHttpResponse() {
|
|
301
|
+
return new Response(this.toJsonString(), {
|
|
302
|
+
status: 200,
|
|
303
|
+
headers: { "Content-Type": "application/json" }
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
};
|
|
307
|
+
//#endregion
|
|
308
|
+
//#region src/ActionDefinition/Action/Payload/ActionPayload_Result.ts
|
|
309
|
+
var ActionPayload_Result = class extends ActionPayload {
|
|
310
|
+
result;
|
|
311
|
+
outputHash;
|
|
312
|
+
constructor(params, result, data) {
|
|
313
|
+
super(params.context, "result", data);
|
|
314
|
+
this.result = result;
|
|
315
|
+
this.outputHash = result.ok ? hashPayloadData(this.context.schema.serializeOutput(result.output)) : hashPayloadData(result.error.message);
|
|
316
|
+
}
|
|
317
|
+
toJsonObject() {
|
|
318
|
+
const wireResult = this.result.ok ? {
|
|
319
|
+
ok: true,
|
|
320
|
+
output: this.context.schema.serializeOutput(this.result.output)
|
|
321
|
+
} : this.result;
|
|
322
|
+
return {
|
|
323
|
+
...this.toBaseJsonObject(),
|
|
324
|
+
result: wireResult,
|
|
325
|
+
outputHash: this.outputHash
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
toJsonString() {
|
|
329
|
+
return JSON.stringify(this.toJsonObject());
|
|
330
|
+
}
|
|
331
|
+
toHttpResponse({ useErrorStatus = true } = {}) {
|
|
332
|
+
return new Response(this.toJsonString(), {
|
|
333
|
+
status: this.result.ok ? 200 : useErrorStatus ? this.result.error.httpStatusCode : 500,
|
|
334
|
+
headers: { "Content-Type": "application/json" }
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
};
|
|
338
|
+
//#endregion
|
|
339
|
+
//#region src/ActionDefinition/Action/Payload/ActionPayload_Request.ts
|
|
340
|
+
var ActionPayload_Request = class extends ActionPayload {
|
|
341
|
+
input;
|
|
342
|
+
inputHash;
|
|
343
|
+
_callSite;
|
|
344
|
+
constructor(params, input, data) {
|
|
345
|
+
super(params.context, "request", data);
|
|
346
|
+
this.input = input;
|
|
347
|
+
this.inputHash = hashPayloadData(this.context.schema.serializeInput(input));
|
|
348
|
+
}
|
|
349
|
+
successResult(...args) {
|
|
350
|
+
const output = args[0];
|
|
351
|
+
const finalOutput = this.context.schema.validateOutput(output, {
|
|
352
|
+
domain: this.domain,
|
|
353
|
+
actionId: this.id
|
|
354
|
+
});
|
|
355
|
+
return new ActionPayload_Result(this, {
|
|
356
|
+
ok: true,
|
|
357
|
+
output: finalOutput
|
|
358
|
+
}, { time: Date.now() });
|
|
359
|
+
}
|
|
360
|
+
errorResult(err) {
|
|
361
|
+
return new ActionPayload_Result(this, {
|
|
362
|
+
ok: false,
|
|
363
|
+
error: err
|
|
364
|
+
}, { time: Date.now() });
|
|
365
|
+
}
|
|
366
|
+
progress(progress) {
|
|
367
|
+
return new ActionPayload_Progress(this, progress, { time: Date.now() });
|
|
368
|
+
}
|
|
369
|
+
toJsonObject() {
|
|
370
|
+
return {
|
|
371
|
+
...super.toBaseJsonObject(),
|
|
372
|
+
input: this.context.schema.serializeInput(this.input),
|
|
373
|
+
inputHash: this.inputHash
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
toJsonString() {
|
|
377
|
+
return JSON.stringify(this.toJsonObject());
|
|
378
|
+
}
|
|
379
|
+
async runToOutput(options) {
|
|
380
|
+
const result = await (await this.run(options)).waitForResultPayload();
|
|
381
|
+
if (result.result.ok) return result.result.output;
|
|
382
|
+
throw result.result.error;
|
|
383
|
+
}
|
|
384
|
+
async runToResultPayload(options) {
|
|
385
|
+
return (await this.run(options)).waitForResultPayload();
|
|
386
|
+
}
|
|
387
|
+
async run(options) {
|
|
388
|
+
if (this._callSite == null) this._callSite = (/* @__PURE__ */ new Error()).stack;
|
|
389
|
+
return this._domain.runAction(this, options);
|
|
390
|
+
}
|
|
391
|
+
};
|
|
392
|
+
//#endregion
|
|
393
|
+
//#region src/ActionDefinition/Action/Core/ActionCore.ts
|
|
394
|
+
var ActionCore = class extends ActionBase {
|
|
395
|
+
_domain;
|
|
396
|
+
form = "core";
|
|
397
|
+
constructor(_domain, id) {
|
|
398
|
+
super("core", _domain, id);
|
|
399
|
+
this._domain = _domain;
|
|
400
|
+
}
|
|
401
|
+
is(action) {
|
|
402
|
+
return action instanceof ActionPayload && action.domain === this.domain && action.id === this.id;
|
|
403
|
+
}
|
|
404
|
+
toJsonObject() {
|
|
405
|
+
return {
|
|
406
|
+
id: this.id,
|
|
407
|
+
form: this.form,
|
|
408
|
+
domain: this.domain,
|
|
409
|
+
allDomains: this.allDomains
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
request(...args) {
|
|
413
|
+
const input = args[0];
|
|
414
|
+
const validatedInput = this.schema.validateInput(input, {
|
|
415
|
+
actionId: this.id,
|
|
416
|
+
domain: this.domain
|
|
417
|
+
});
|
|
418
|
+
return new ActionPayload_Request({ context: new ActionContext(this._domain, this.id, {
|
|
419
|
+
cuid: (0, nanoid.nanoid)(),
|
|
420
|
+
timeCreated: Date.now(),
|
|
421
|
+
routing: [],
|
|
422
|
+
originClient: RuntimeCoordinate.unknown
|
|
423
|
+
}) }, validatedInput, { time: Date.now() });
|
|
424
|
+
}
|
|
425
|
+
deserializeInput(serialized) {
|
|
426
|
+
return this.schema.deserializeInput(serialized);
|
|
427
|
+
}
|
|
428
|
+
serializeInput(raw) {
|
|
429
|
+
return this.schema.serializeInput(raw);
|
|
430
|
+
}
|
|
431
|
+
validateInput(input) {
|
|
432
|
+
return this.schema.validateInput(input, {
|
|
433
|
+
domain: this.domain,
|
|
434
|
+
actionId: this.id
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
validateOutput(output) {
|
|
438
|
+
return this.schema.validateOutput(output, {
|
|
439
|
+
domain: this.domain,
|
|
440
|
+
actionId: this.id
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
};
|
|
444
|
+
//#endregion
|
|
445
|
+
//#region src/ActionDefinition/Action/RunningAction.ts
|
|
446
|
+
var RunningAction = class {
|
|
447
|
+
_state;
|
|
448
|
+
context;
|
|
449
|
+
cuid;
|
|
450
|
+
id;
|
|
451
|
+
_domain;
|
|
452
|
+
domain;
|
|
453
|
+
allDomains;
|
|
454
|
+
parentCuid;
|
|
455
|
+
callSite;
|
|
456
|
+
_resultPayloadPromise;
|
|
457
|
+
_resolveResult;
|
|
458
|
+
_rejectResult;
|
|
459
|
+
_isAborted = false;
|
|
460
|
+
_updates = [];
|
|
461
|
+
_updateListeners = [];
|
|
462
|
+
constructor(initialState) {
|
|
463
|
+
this.context = initialState.context;
|
|
464
|
+
this.cuid = initialState.context.cuid;
|
|
465
|
+
this.id = initialState.context.id;
|
|
466
|
+
this.domain = initialState.context.domain;
|
|
467
|
+
this.allDomains = initialState.context.allDomains;
|
|
468
|
+
this._domain = initialState.context._domain;
|
|
469
|
+
this.parentCuid = initialState.parentCuid;
|
|
470
|
+
this.callSite = initialState.callSite;
|
|
471
|
+
this._resultPayloadPromise = new Promise((resolve, reject) => {
|
|
472
|
+
this._resolveResult = resolve;
|
|
473
|
+
this._rejectResult = reject;
|
|
474
|
+
});
|
|
475
|
+
this._resultPayloadPromise.catch(() => {});
|
|
476
|
+
this._state = {
|
|
477
|
+
request: initialState.request,
|
|
478
|
+
progress: initialState.progress ?? [],
|
|
479
|
+
result: initialState.result
|
|
480
|
+
};
|
|
481
|
+
this._sendUpdate({
|
|
482
|
+
type: "started",
|
|
483
|
+
runningAction: this,
|
|
484
|
+
time: Date.now()
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
get state() {
|
|
488
|
+
return this._state;
|
|
489
|
+
}
|
|
490
|
+
abort(reason) {
|
|
491
|
+
this._abort(reason);
|
|
492
|
+
}
|
|
493
|
+
addUpdateListeners(listeners) {
|
|
494
|
+
this._updateListeners.push(...listeners);
|
|
495
|
+
for (const event of this._updates) for (const listener of listeners) listener(event);
|
|
496
|
+
return () => {
|
|
497
|
+
for (const listener of listeners) {
|
|
498
|
+
const i = this._updateListeners.indexOf(listener);
|
|
499
|
+
if (i !== -1) this._updateListeners.splice(i, 1);
|
|
500
|
+
}
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
async *iterateUpdates() {
|
|
504
|
+
const queue = [];
|
|
505
|
+
let resolveWaiter = null;
|
|
506
|
+
const unsubscribe = this.addUpdateListeners([(event) => {
|
|
507
|
+
queue.push(event);
|
|
508
|
+
if (resolveWaiter) {
|
|
509
|
+
resolveWaiter();
|
|
510
|
+
resolveWaiter = null;
|
|
511
|
+
}
|
|
512
|
+
}]);
|
|
513
|
+
try {
|
|
514
|
+
while (true) {
|
|
515
|
+
if (queue.length === 0) await new Promise((resolve) => {
|
|
516
|
+
resolveWaiter = resolve;
|
|
517
|
+
});
|
|
518
|
+
const event = queue.shift();
|
|
519
|
+
yield event;
|
|
520
|
+
if (event.type === "finished") break;
|
|
521
|
+
}
|
|
522
|
+
} finally {
|
|
523
|
+
unsubscribe();
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
_sendUpdate(update) {
|
|
527
|
+
this._updates.push(update);
|
|
528
|
+
for (const listener of this._updateListeners) listener(update);
|
|
529
|
+
}
|
|
530
|
+
_completeWithResult(result) {
|
|
531
|
+
if (this._state.result != null || this._isAborted) return false;
|
|
532
|
+
this._state = {
|
|
533
|
+
request: this._state.request,
|
|
534
|
+
progress: this._state.progress,
|
|
535
|
+
result
|
|
536
|
+
};
|
|
537
|
+
this._resolveResult(result);
|
|
538
|
+
this._sendUpdate({
|
|
539
|
+
type: "finished",
|
|
540
|
+
finishType: "success",
|
|
541
|
+
runningAction: this,
|
|
542
|
+
time: Date.now(),
|
|
543
|
+
response: result
|
|
544
|
+
});
|
|
545
|
+
return true;
|
|
546
|
+
}
|
|
547
|
+
_abort(reason) {
|
|
548
|
+
if (this._state.result != null || this._isAborted) return false;
|
|
549
|
+
this._isAborted = true;
|
|
550
|
+
this._rejectResult(reason);
|
|
551
|
+
this._sendUpdate({
|
|
552
|
+
type: "finished",
|
|
553
|
+
finishType: "aborted",
|
|
554
|
+
runningAction: this,
|
|
555
|
+
time: Date.now(),
|
|
556
|
+
reason
|
|
557
|
+
});
|
|
558
|
+
return true;
|
|
559
|
+
}
|
|
560
|
+
_failWithError(error) {
|
|
561
|
+
if (this._state.result != null || this._isAborted) return false;
|
|
562
|
+
this._isAborted = true;
|
|
563
|
+
this._rejectResult(error);
|
|
564
|
+
this._sendUpdate({
|
|
565
|
+
type: "finished",
|
|
566
|
+
finishType: "failed",
|
|
567
|
+
runningAction: this,
|
|
568
|
+
time: Date.now(),
|
|
569
|
+
error
|
|
570
|
+
});
|
|
571
|
+
return true;
|
|
572
|
+
}
|
|
573
|
+
_updateProgress(progress) {
|
|
574
|
+
if (this._state.result != null || this._isAborted) return;
|
|
575
|
+
this._state.progress.push(progress);
|
|
576
|
+
this._sendUpdate({
|
|
577
|
+
type: "progress",
|
|
578
|
+
runningAction: this,
|
|
579
|
+
time: Date.now(),
|
|
580
|
+
progress: progress.progress
|
|
581
|
+
});
|
|
582
|
+
}
|
|
583
|
+
waitForResultPayload() {
|
|
584
|
+
return this._resultPayloadPromise;
|
|
585
|
+
}
|
|
586
|
+
_resolveFromJson(resultJson) {
|
|
587
|
+
if (this._state.result != null || this._isAborted) return false;
|
|
588
|
+
const result = this._domain.hydrateResultPayload(resultJson);
|
|
589
|
+
return this._completeWithResult(result);
|
|
590
|
+
}
|
|
591
|
+
};
|
|
592
|
+
//#endregion
|
|
593
|
+
//#region src/errors/err_nice_action.ts
|
|
594
|
+
let EErrId_NiceAction = /* @__PURE__ */ function(EErrId_NiceAction) {
|
|
595
|
+
EErrId_NiceAction["not_implemented"] = "not_implemented";
|
|
596
|
+
EErrId_NiceAction["action_id_not_in_domain"] = "action_id_not_in_domain";
|
|
597
|
+
EErrId_NiceAction["domain_already_exists_in_hierarchy"] = "domain_already_exists_in_hierarchy";
|
|
598
|
+
EErrId_NiceAction["domain_no_handler"] = "domain_no_handler";
|
|
599
|
+
EErrId_NiceAction["hydration_domain_mismatch"] = "hydration_domain_mismatch";
|
|
600
|
+
EErrId_NiceAction["hydration_action_state_mismatch"] = "hydration_action_state_mismatch";
|
|
601
|
+
EErrId_NiceAction["hydration_action_id_not_found"] = "hydration_action_id_not_found";
|
|
602
|
+
EErrId_NiceAction["no_action_execution_handler"] = "no_action_execution_handler";
|
|
603
|
+
EErrId_NiceAction["wire_action_not_payload"] = "wire_action_not_payload";
|
|
604
|
+
EErrId_NiceAction["wire_not_action_data"] = "wire_not_action_data";
|
|
605
|
+
EErrId_NiceAction["client_runtime_already_registered"] = "client_runtime_already_registered";
|
|
606
|
+
EErrId_NiceAction["client_runtime_not_registered"] = "client_runtime_not_registered";
|
|
607
|
+
EErrId_NiceAction["runtime_reset"] = "runtime_reset";
|
|
608
|
+
EErrId_NiceAction["no_client_runtimes_registered"] = "no_client_runtimes_registered";
|
|
609
|
+
EErrId_NiceAction["action_input_validation_failed"] = "action_input_validation_failed";
|
|
610
|
+
EErrId_NiceAction["action_input_validation_promise"] = "action_input_validation_promise";
|
|
611
|
+
EErrId_NiceAction["action_output_validation_failed"] = "action_output_validation_failed";
|
|
612
|
+
EErrId_NiceAction["action_output_validation_promise"] = "action_output_validation_promise";
|
|
613
|
+
return EErrId_NiceAction;
|
|
614
|
+
}({});
|
|
615
|
+
const err_nice_action = _nice_code_error.err_nice.createChildDomain({
|
|
616
|
+
domain: "err_nice_action",
|
|
617
|
+
defaultHttpStatusCode: 500,
|
|
618
|
+
schema: {
|
|
619
|
+
["not_implemented"]: (0, _nice_code_error.err)({ message: ({ label }) => `The "${label}" functionality is not implemented yet.` }),
|
|
620
|
+
["action_id_not_in_domain"]: (0, _nice_code_error.err)({ message: ({ actionId, domain }) => `Action with id "${actionId}" does not exist in domain "${domain}".` }),
|
|
621
|
+
["domain_already_exists_in_hierarchy"]: (0, _nice_code_error.err)({ message: ({ domain, allParentDomains, parentDomain }) => `Domain "${domain}" already exists in the hierarchy under the parent "${parentDomain}". All parent domains ["${allParentDomains.join(", ")}"]` }),
|
|
622
|
+
["domain_no_handler"]: (0, _nice_code_error.err)({ message: ({ domain }) => `Domain "${domain}" has no action handler registered.` }),
|
|
623
|
+
["hydration_domain_mismatch"]: (0, _nice_code_error.err)({ message: ({ expected, received }) => `Cannot hydrate action: domain mismatch. Expected "${expected}", got "${received}".` }),
|
|
624
|
+
["hydration_action_state_mismatch"]: (0, _nice_code_error.err)({ message: ({ expected, received }) => `Cannot hydrate action: action state type mismatch. Expected "${expected}", got "${received}".` }),
|
|
625
|
+
["hydration_action_id_not_found"]: (0, _nice_code_error.err)({ message: ({ domain, actionId }) => `Cannot hydrate action: id "${actionId}" does not exist in domain "${domain}".` }),
|
|
626
|
+
["no_action_execution_handler"]: (0, _nice_code_error.err)({ message: ({ domain, actionId, specifiedClient }) => `${specifiedClient ? ` The targeted client runtime [${specifiedClient.stringId}] has no` : "No"} action handler registered for "${actionId}" in domain "${domain}".` }),
|
|
627
|
+
["wire_action_not_payload"]: (0, _nice_code_error.err)({ message: ({ domain, actionId, actionState }) => `Cannot handle wire for action "${actionId}" in domain "${domain}": expected action form of "data" and type of "request", "progress" or "result", got "${actionState}".` }),
|
|
628
|
+
["wire_not_action_data"]: (0, _nice_code_error.err)({ message: () => `Cannot handle wire for action: expected an object with a "domain" property of type string, a "form" property of "data" and a "type" property of "request", "progress" or "result".` }),
|
|
629
|
+
["runtime_reset"]: (0, _nice_code_error.err)({ message: () => `Runtime has been reset.` }),
|
|
630
|
+
["client_runtime_already_registered"]: (0, _nice_code_error.err)({ message: ({ context, client }) => `Environment is already registered${context?.domain ? ` on domain "${context.domain}"` : ""} for client [${client.stringId}]. Each client specifier (exact match on all properties) may only be registered once.` }),
|
|
631
|
+
["client_runtime_not_registered"]: (0, _nice_code_error.err)({ message: ({ context, clientStringId }) => `No runtime registered${context?.domain ? ` on domain "${context.domain}"` : ""} for client [${clientStringId}].` }),
|
|
632
|
+
["no_client_runtimes_registered"]: (0, _nice_code_error.err)({ message: ({ context }) => `No runtimes registered${context?.domain ? ` on domain "${context.domain}"` : ""}. Add handlers to a runtime via runtime.addHandlers([handler]) before executing actions.` }),
|
|
633
|
+
["action_input_validation_failed"]: (0, _nice_code_error.err)({
|
|
634
|
+
message: ({ domain, actionId, validationMessage }) => `Input validation failed for action "${actionId}" in domain "${domain}":\n${validationMessage}`,
|
|
635
|
+
httpStatusCode: 400
|
|
636
|
+
}),
|
|
637
|
+
["action_input_validation_promise"]: (0, _nice_code_error.err)({
|
|
638
|
+
message: ({ domain, actionId }) => `Input validation for action "${actionId}" in domain "${domain}" returned a promise, which is not supported.`,
|
|
639
|
+
httpStatusCode: 400
|
|
640
|
+
}),
|
|
641
|
+
["action_output_validation_failed"]: (0, _nice_code_error.err)({
|
|
642
|
+
message: ({ domain, actionId, validationMessage }) => `Output validation failed for action "${actionId}" in domain "${domain}":\n${validationMessage}`,
|
|
643
|
+
httpStatusCode: 500
|
|
644
|
+
}),
|
|
645
|
+
["action_output_validation_promise"]: (0, _nice_code_error.err)({
|
|
646
|
+
message: ({ domain, actionId }) => `Output validation for action "${actionId}" in domain "${domain}" returned a promise, which is not supported.`,
|
|
647
|
+
httpStatusCode: 500
|
|
648
|
+
})
|
|
649
|
+
}
|
|
650
|
+
});
|
|
651
|
+
//#endregion
|
|
652
|
+
//#region src/utils/isAction_Base_JsonObject.ts
|
|
653
|
+
const isAction_Base_JsonObject = (obj) => {
|
|
654
|
+
return typeof obj === "object" && obj !== null && typeof obj.domain === "string" && typeof obj.id === "string" && typeof obj.form === "string";
|
|
655
|
+
};
|
|
656
|
+
//#endregion
|
|
657
|
+
//#region src/utils/isActionPayload_Result_JsonObject.ts
|
|
658
|
+
const isActionPayload_Result_JsonObject = (obj) => {
|
|
659
|
+
return isAction_Base_JsonObject(obj) && obj.result != null && obj.form === "data" && obj.type === "result";
|
|
660
|
+
};
|
|
661
|
+
//#endregion
|
|
662
|
+
//#region src/utils/getAssumedRuntimeEnvironment.ts
|
|
663
|
+
const getAssumedRuntimeInfo = () => {
|
|
664
|
+
return {
|
|
665
|
+
assumed: true,
|
|
666
|
+
runtimeName: std_env.runtime
|
|
667
|
+
};
|
|
668
|
+
};
|
|
669
|
+
//#endregion
|
|
670
|
+
//#region src/utils/isActionPayload_Progress_JsonObject.ts
|
|
671
|
+
const isActionPayload_Progress_JsonObject = (obj) => {
|
|
672
|
+
return isAction_Base_JsonObject(obj) && "progress" in obj && obj.form === "data" && obj.type === "progress";
|
|
673
|
+
};
|
|
674
|
+
//#endregion
|
|
675
|
+
//#region src/utils/isActionPayload_Request_JsonObject.ts
|
|
676
|
+
const isActionPayload_Request_JsonObject = (obj) => {
|
|
677
|
+
return isAction_Base_JsonObject(obj) && "input" in obj && obj.form === "data" && obj.type === "request";
|
|
678
|
+
};
|
|
679
|
+
//#endregion
|
|
680
|
+
//#region src/utils/isActionPayload_Any_JsonObject.ts
|
|
681
|
+
function isActionPayload_Any_JsonObject(obj) {
|
|
682
|
+
return isActionPayload_Request_JsonObject(obj) || isActionPayload_Result_JsonObject(obj) || isActionPayload_Progress_JsonObject(obj);
|
|
683
|
+
}
|
|
684
|
+
//#endregion
|
|
685
|
+
//#region src/ActionRuntime/HandlerCallStack.ts
|
|
686
|
+
const _stack = [];
|
|
687
|
+
function pushHandlerCuid(cuid) {
|
|
688
|
+
_stack.push(cuid);
|
|
689
|
+
}
|
|
690
|
+
function popHandlerCuid() {
|
|
691
|
+
_stack.pop();
|
|
692
|
+
}
|
|
693
|
+
function peekHandlerCuid() {
|
|
694
|
+
return _stack[_stack.length - 1];
|
|
695
|
+
}
|
|
696
|
+
//#endregion
|
|
697
|
+
//#region src/ActionRuntime/ActionDomainManager.ts
|
|
698
|
+
var ActionDomainManager = class {
|
|
699
|
+
_domains = /* @__PURE__ */ new Map();
|
|
700
|
+
addDomain(domain) {
|
|
701
|
+
this._domains.set(domain.domain, domain);
|
|
702
|
+
}
|
|
703
|
+
getDomains() {
|
|
704
|
+
return [...this._domains.values()];
|
|
705
|
+
}
|
|
706
|
+
verifyIsActionJson(action) {
|
|
707
|
+
if (typeof action.domain !== "string" || typeof action.id !== "string") throw err_nice_action.fromId("wire_not_action_data");
|
|
708
|
+
}
|
|
709
|
+
getActionDomain(action) {
|
|
710
|
+
this.verifyIsActionJson(action);
|
|
711
|
+
const domain = this._domains.get(action.domain);
|
|
712
|
+
if (!domain) return;
|
|
713
|
+
return domain;
|
|
714
|
+
}
|
|
715
|
+
getActionDomainOrThrow(action) {
|
|
716
|
+
this.verifyIsActionJson(action);
|
|
717
|
+
const domain = this._domains.get(action.domain);
|
|
718
|
+
if (!domain) throw err_nice_action.fromId("domain_no_handler", { domain: action.domain });
|
|
719
|
+
return domain;
|
|
720
|
+
}
|
|
721
|
+
hydrateActionPayload(actionJson) {
|
|
722
|
+
return this.getActionDomainOrThrow(actionJson).hydrateAnyAction(actionJson);
|
|
723
|
+
}
|
|
724
|
+
};
|
|
725
|
+
//#endregion
|
|
726
|
+
//#region src/ActionRuntime/Routing/ActionRouter.ts
|
|
727
|
+
var ActionRouter = class {
|
|
728
|
+
domainManager = new ActionDomainManager();
|
|
729
|
+
actionRouteData = /* @__PURE__ */ new Map();
|
|
730
|
+
_context;
|
|
731
|
+
constructor(context) {
|
|
732
|
+
this._context = context;
|
|
733
|
+
}
|
|
734
|
+
/** Copy all routes from another router into this one, replacing any overlapping keys. */
|
|
735
|
+
mergeRouter(actionRouter) {
|
|
736
|
+
for (const domain of actionRouter.getDomains()) this.domainManager.addDomain(domain);
|
|
737
|
+
for (const [matchKey, routeDataEntries] of actionRouter.actionRouteData.entries()) this.actionRouteData.set(matchKey, [...routeDataEntries]);
|
|
738
|
+
}
|
|
739
|
+
addDomainsFromOther(actionRouter) {
|
|
740
|
+
for (const domain of actionRouter.getDomains()) this.domainManager.addDomain(domain);
|
|
741
|
+
}
|
|
742
|
+
/** All FNs registered for an action, ID-specific entries first then domain wildcard. */
|
|
743
|
+
getRouteDataEntriesForAction(action) {
|
|
744
|
+
const idKey = `dom[${action.domain}]id[${action.id}]`;
|
|
745
|
+
const domKey = `dom[${action.domain}]id[_]`;
|
|
746
|
+
return [...this.actionRouteData.get(idKey) ?? [], ...this.actionRouteData.get(domKey) ?? []];
|
|
747
|
+
}
|
|
748
|
+
/** First FN registered for an action (ID-specific beats domain wildcard). */
|
|
749
|
+
getRouteDataForAction(action) {
|
|
750
|
+
return this.getRouteDataEntriesForAction(action)[0];
|
|
751
|
+
}
|
|
752
|
+
throwNoHandlerForAction(action, context) {
|
|
753
|
+
if (this._context.contextType === "handler_route") throw err_nice_action.fromId("no_action_execution_handler", {
|
|
754
|
+
domain: action.domain,
|
|
755
|
+
actionId: action.id,
|
|
756
|
+
specifiedClient: context.targetLocalRuntime?.coordinate
|
|
757
|
+
});
|
|
758
|
+
if (this._context.contextType === "runtime_to_handler") throw err_nice_action.fromId("no_action_execution_handler", {
|
|
759
|
+
domain: action.domain,
|
|
760
|
+
actionId: action.id,
|
|
761
|
+
specifiedClient: this._context.runtime.coordinate
|
|
762
|
+
});
|
|
763
|
+
throw err_nice_action.fromId("no_action_execution_handler", {
|
|
764
|
+
domain: action.domain,
|
|
765
|
+
actionId: action.id
|
|
766
|
+
});
|
|
767
|
+
}
|
|
768
|
+
getRouteDataEntriesForActionOrThrow(action, context) {
|
|
769
|
+
const entries = this.getRouteDataEntriesForAction(action);
|
|
770
|
+
if (entries.length === 0) this.throwNoHandlerForAction(action, context);
|
|
771
|
+
return entries;
|
|
772
|
+
}
|
|
773
|
+
getRouteDataForActionOrThrow(action, context) {
|
|
774
|
+
const routeData = this.getRouteDataForAction(action);
|
|
775
|
+
if (!routeData) this.throwNoHandlerForAction(action, context);
|
|
776
|
+
return routeData;
|
|
777
|
+
}
|
|
778
|
+
/** All FNs stored under an exact match key. */
|
|
779
|
+
getForKey(key) {
|
|
780
|
+
return this.actionRouteData.get(key) ?? [];
|
|
781
|
+
}
|
|
782
|
+
/** Every match key that has at least one registered FN. */
|
|
783
|
+
getRegisteredKeys() {
|
|
784
|
+
return [...this.actionRouteData.keys()];
|
|
785
|
+
}
|
|
786
|
+
getDomains() {
|
|
787
|
+
return this.domainManager.getDomains();
|
|
788
|
+
}
|
|
789
|
+
/** Register a handler for all actions in a domain, replacing any existing one. */
|
|
790
|
+
forDomain(domain, routeData) {
|
|
791
|
+
this.domainManager.addDomain(domain);
|
|
792
|
+
this.actionRouteData.set(`dom[${domain.domain}]id[_]`, [routeData]);
|
|
793
|
+
return this;
|
|
794
|
+
}
|
|
795
|
+
forAction(action, routeData) {
|
|
796
|
+
return this.forActionId(action._domain, action.id, routeData);
|
|
797
|
+
}
|
|
798
|
+
/** Register a handler for a specific action, replacing any existing one. */
|
|
799
|
+
forActionId(domain, id, routeData) {
|
|
800
|
+
this.domainManager.addDomain(domain);
|
|
801
|
+
this.actionRouteData.set(`dom[${domain.domain}]id[${id}]`, [routeData]);
|
|
802
|
+
return this;
|
|
803
|
+
}
|
|
804
|
+
/** Register one handler for several action IDs, replacing any existing ones. */
|
|
805
|
+
forActionIds(domain, ids, routeData) {
|
|
806
|
+
this.domainManager.addDomain(domain);
|
|
807
|
+
for (const id of ids) this.forActionId(domain, id, routeData);
|
|
808
|
+
return this;
|
|
809
|
+
}
|
|
810
|
+
/** Register per-action handlers from a cases map, replacing any existing ones. */
|
|
811
|
+
forDomainActionCases(domain, cases) {
|
|
812
|
+
this.domainManager.addDomain(domain);
|
|
813
|
+
for (const id of Object.keys(cases)) {
|
|
814
|
+
const routeData = cases[id];
|
|
815
|
+
if (routeData != null) this.actionRouteData.set(`dom[${domain.domain}]id[${id}]`, [routeData]);
|
|
816
|
+
}
|
|
817
|
+
return this;
|
|
818
|
+
}
|
|
819
|
+
/** Append a handler for all actions in a domain (accumulates alongside existing). */
|
|
820
|
+
addForDomain(domain, routeData) {
|
|
821
|
+
this.domainManager.addDomain(domain);
|
|
822
|
+
this._push(`dom[${domain.domain}]id[_]`, routeData);
|
|
823
|
+
return this;
|
|
824
|
+
}
|
|
825
|
+
/** Append a handler for a specific action (accumulates alongside existing). */
|
|
826
|
+
addForAction(domain, id, routeData) {
|
|
827
|
+
this.domainManager.addDomain(domain);
|
|
828
|
+
this._push(`dom[${domain.domain}]id[${id}]`, routeData);
|
|
829
|
+
return this;
|
|
830
|
+
}
|
|
831
|
+
/** Append one handler for several action IDs (accumulates alongside existing). */
|
|
832
|
+
addForActionIds(domain, ids, routeData) {
|
|
833
|
+
this.domainManager.addDomain(domain);
|
|
834
|
+
for (const id of ids) this.addForAction(domain, id, routeData);
|
|
835
|
+
return this;
|
|
836
|
+
}
|
|
837
|
+
/** Append per-action handlers from a cases map (accumulates alongside existing). */
|
|
838
|
+
addForDomainActionCases(domain, cases) {
|
|
839
|
+
this.domainManager.addDomain(domain);
|
|
840
|
+
for (const id of Object.keys(cases)) {
|
|
841
|
+
const routeData = cases[id];
|
|
842
|
+
if (routeData != null) this._push(`dom[${domain.domain}]id[${id}]`, routeData);
|
|
843
|
+
}
|
|
844
|
+
return this;
|
|
845
|
+
}
|
|
846
|
+
/** Append a handler directly by its raw match key (used when the key is known ahead of time). */
|
|
847
|
+
addForKey(key, routeData) {
|
|
848
|
+
this._push(key, routeData);
|
|
849
|
+
return this;
|
|
850
|
+
}
|
|
851
|
+
_push(key, routeData) {
|
|
852
|
+
const existing = this.actionRouteData.get(key);
|
|
853
|
+
if (existing != null) existing.push(routeData);
|
|
854
|
+
else this.actionRouteData.set(key, [routeData]);
|
|
855
|
+
}
|
|
856
|
+
};
|
|
857
|
+
//#endregion
|
|
858
|
+
//#region src/ActionRuntime/Handler/ActionHandler.ts
|
|
859
|
+
var ActionHandler = class {
|
|
860
|
+
cuid;
|
|
861
|
+
constructor() {
|
|
862
|
+
this.cuid = (0, nanoid.nanoid)();
|
|
863
|
+
}
|
|
864
|
+
getActionRouter() {
|
|
865
|
+
return this.actionRouter;
|
|
866
|
+
}
|
|
867
|
+
};
|
|
868
|
+
//#endregion
|
|
869
|
+
//#region src/ActionRuntime/Handler/ExternalClient/err_nice_external_client.ts
|
|
870
|
+
const err_nice_external_client = err_nice_action.createChildDomain({
|
|
871
|
+
domain: "err_nice_external_client",
|
|
872
|
+
schema: {}
|
|
873
|
+
});
|
|
874
|
+
//#endregion
|
|
875
|
+
//#region src/ActionRuntime/Handler/ExternalClient/Transport/err_nice_transport.ts
|
|
876
|
+
let EErrId_NiceTransport = /* @__PURE__ */ function(EErrId_NiceTransport) {
|
|
877
|
+
EErrId_NiceTransport["timeout"] = "timeout";
|
|
878
|
+
EErrId_NiceTransport["not_found"] = "not_found";
|
|
879
|
+
EErrId_NiceTransport["unsupported"] = "unsupported";
|
|
880
|
+
EErrId_NiceTransport["initialization_failed"] = "initialization_failed";
|
|
881
|
+
EErrId_NiceTransport["send_failed"] = "send_failed";
|
|
882
|
+
EErrId_NiceTransport["invalid_action_response"] = "invalid_action_response";
|
|
883
|
+
return EErrId_NiceTransport;
|
|
884
|
+
}({});
|
|
885
|
+
const err_nice_transport = err_nice_external_client.createChildDomain({
|
|
886
|
+
domain: "err_nice_transport",
|
|
887
|
+
schema: {
|
|
888
|
+
["timeout"]: (0, _nice_code_error.err)({ message: ({ timeout }) => `ActionConnect transport timed out after ${timeout}ms.` }),
|
|
889
|
+
["not_found"]: (0, _nice_code_error.err)({ message: ({ actionId }) => `No connected transport found for action "${actionId}".` }),
|
|
890
|
+
["unsupported"]: (0, _nice_code_error.err)({ message: ({ transportTypes }) => `${transportTypes.length} Transport(s) [${transportTypes.join(", ")}] found but returned "unsupported" status.` }),
|
|
891
|
+
["initialization_failed"]: (0, _nice_code_error.err)({ message: ({ actionId }) => `Transports found for action "${actionId}", but none are ready.` }),
|
|
892
|
+
["send_failed"]: (0, _nice_code_error.err)({
|
|
893
|
+
message: ({ actionId, httpStatusCode, message }) => `Failed to send action "${actionId}" [${httpStatusCode ?? "Unknown status"}]: ${message ?? "Unknown error"}.`,
|
|
894
|
+
httpStatusCode: ({ httpStatusCode }) => httpStatusCode ?? 500
|
|
895
|
+
}),
|
|
896
|
+
["invalid_action_response"]: (0, _nice_code_error.err)({ message: ({ actionId }) => `Invalid action response JSON structure for action "${actionId}"` })
|
|
897
|
+
}
|
|
898
|
+
});
|
|
899
|
+
//#endregion
|
|
900
|
+
//#region src/ActionRuntime/Handler/ExternalClient/Transport/Transport.types.ts
|
|
901
|
+
let ETransportType = /* @__PURE__ */ function(ETransportType) {
|
|
902
|
+
ETransportType["ws"] = "ws";
|
|
903
|
+
ETransportType["http"] = "http";
|
|
904
|
+
ETransportType["custom"] = "custom";
|
|
905
|
+
return ETransportType;
|
|
906
|
+
}({});
|
|
907
|
+
/**
|
|
908
|
+
*
|
|
909
|
+
* TRANSPORT READINESS RESPONSE
|
|
910
|
+
*
|
|
911
|
+
*/
|
|
912
|
+
let ETransportStatus = /* @__PURE__ */ function(ETransportStatus) {
|
|
913
|
+
ETransportStatus["uninitialized"] = "uninitialized";
|
|
914
|
+
ETransportStatus["unsupported"] = "unsupported";
|
|
915
|
+
ETransportStatus["initializing"] = "initializing";
|
|
916
|
+
ETransportStatus["ready"] = "ready";
|
|
917
|
+
ETransportStatus["failed"] = "failed";
|
|
918
|
+
return ETransportStatus;
|
|
919
|
+
}({});
|
|
920
|
+
//#endregion
|
|
921
|
+
//#region src/ActionRuntime/Handler/ExternalClient/Transport/ConnectionTransportManager.ts
|
|
922
|
+
var ConnectionTransportManager = class {
|
|
923
|
+
_cache;
|
|
924
|
+
_transports = [];
|
|
925
|
+
constructor(_cache) {
|
|
926
|
+
this._cache = _cache;
|
|
927
|
+
}
|
|
928
|
+
addTransport(transport) {
|
|
929
|
+
this._transports.push(transport);
|
|
930
|
+
}
|
|
931
|
+
/**
|
|
932
|
+
* The highest-priority transport (first declared). Used to label an action's route *before* the
|
|
933
|
+
* transport has finished connecting — so a still-connecting action shows its (expected) destination
|
|
934
|
+
* instead of an "unknown" hop. {@link getReadyTransport} still decides the real winner, and the
|
|
935
|
+
* caller corrects the hop if a lower-priority transport ends up serving the action.
|
|
936
|
+
*/
|
|
937
|
+
getPreferredTransport() {
|
|
938
|
+
return this._transports[0];
|
|
939
|
+
}
|
|
940
|
+
async getReadyTransport(routeActionParams) {
|
|
941
|
+
const action = routeActionParams.action;
|
|
942
|
+
const candidates = [];
|
|
943
|
+
const unavailableTransports = [];
|
|
944
|
+
for (const transport of this._transports) {
|
|
945
|
+
const cacheKey = transport.getCacheKey(routeActionParams);
|
|
946
|
+
if (cacheKey != null) {
|
|
947
|
+
const cached = this._cache.get(cacheKey);
|
|
948
|
+
if (cached != null) {
|
|
949
|
+
if (cached instanceof Promise) {
|
|
950
|
+
candidates.push(cached);
|
|
951
|
+
continue;
|
|
952
|
+
}
|
|
953
|
+
candidates.push(Promise.resolve({
|
|
954
|
+
...cached,
|
|
955
|
+
transport
|
|
956
|
+
}));
|
|
957
|
+
break;
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
const statusInfo = transport.getTransport(routeActionParams);
|
|
961
|
+
if (statusInfo.status === "ready") {
|
|
962
|
+
const readyData = statusInfo.readyData;
|
|
963
|
+
if (cacheKey != null) {
|
|
964
|
+
const entry = {
|
|
965
|
+
methods: readyData,
|
|
966
|
+
transport
|
|
967
|
+
};
|
|
968
|
+
this._cache.set(cacheKey, entry);
|
|
969
|
+
readyData.addOnDisconnectListener?.(() => {
|
|
970
|
+
if (this._cache.get(cacheKey) === entry) this._cache.delete(cacheKey);
|
|
971
|
+
});
|
|
972
|
+
}
|
|
973
|
+
candidates.push(Promise.resolve({
|
|
974
|
+
methods: readyData,
|
|
975
|
+
transport
|
|
976
|
+
}));
|
|
977
|
+
break;
|
|
978
|
+
}
|
|
979
|
+
if (statusInfo.status === "unsupported") {
|
|
980
|
+
unavailableTransports.push(transport);
|
|
981
|
+
continue;
|
|
982
|
+
}
|
|
983
|
+
if (statusInfo.status === "initializing") {
|
|
984
|
+
const promise = statusInfo.initializationPromise.then((info) => {
|
|
985
|
+
if (info.status === "failed") throw info.error;
|
|
986
|
+
if (info.status === "unsupported") throw err_nice_transport.fromId("unsupported", { transportTypes: [transport.type] });
|
|
987
|
+
const readyData = info.readyData;
|
|
988
|
+
const entry = {
|
|
989
|
+
methods: readyData,
|
|
990
|
+
transport
|
|
991
|
+
};
|
|
992
|
+
if (cacheKey != null) if (this._cache.get(cacheKey) === promise) {
|
|
993
|
+
this._cache.set(cacheKey, entry);
|
|
994
|
+
readyData.addOnDisconnectListener?.(() => {
|
|
995
|
+
if (this._cache.get(cacheKey) === entry) this._cache.delete(cacheKey);
|
|
996
|
+
});
|
|
997
|
+
} else readyData.disconnect?.();
|
|
998
|
+
return entry;
|
|
999
|
+
}).catch((e) => {
|
|
1000
|
+
if (cacheKey != null && this._cache.get(cacheKey) === promise) this._cache.delete(cacheKey);
|
|
1001
|
+
throw e;
|
|
1002
|
+
});
|
|
1003
|
+
if (cacheKey != null) this._cache.set(cacheKey, promise);
|
|
1004
|
+
candidates.push(promise);
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
if (candidates.length === 0) {
|
|
1008
|
+
if (unavailableTransports.length > 0) throw err_nice_transport.fromId("unsupported", { transportTypes: unavailableTransports.map((t) => t.type) });
|
|
1009
|
+
throw err_nice_transport.fromId("not_found", { actionId: action.id });
|
|
1010
|
+
}
|
|
1011
|
+
let lastError;
|
|
1012
|
+
for (const candidate of candidates) try {
|
|
1013
|
+
return await candidate;
|
|
1014
|
+
} catch (e) {
|
|
1015
|
+
lastError = e;
|
|
1016
|
+
}
|
|
1017
|
+
throw err_nice_transport.fromId("initialization_failed", { actionId: action.id }).withOriginError(lastError);
|
|
1018
|
+
}
|
|
1019
|
+
};
|
|
1020
|
+
//#endregion
|
|
1021
|
+
//#region src/ActionRuntime/Handler/ExternalClient/ActionExternalClientHandler.ts
|
|
1022
|
+
var ActionExternalClientHandler = class extends ActionHandler {
|
|
1023
|
+
externalClient;
|
|
1024
|
+
handlerType = "external";
|
|
1025
|
+
cuid;
|
|
1026
|
+
_defaultTimeout;
|
|
1027
|
+
_transportCache = /* @__PURE__ */ new Map();
|
|
1028
|
+
transportManager = new ConnectionTransportManager(this._transportCache);
|
|
1029
|
+
_incomingActionDataListeners = [];
|
|
1030
|
+
actionRouter = new ActionRouter({
|
|
1031
|
+
contextType: "handler_route",
|
|
1032
|
+
handler: this
|
|
1033
|
+
});
|
|
1034
|
+
constructor({ runtimeCoordinate: externalClientSpecifier, transports, defaultTimeout }) {
|
|
1035
|
+
super();
|
|
1036
|
+
this.externalClient = externalClientSpecifier;
|
|
1037
|
+
this.cuid = (0, nanoid.nanoid)();
|
|
1038
|
+
this._defaultTimeout = defaultTimeout ?? 1e4;
|
|
1039
|
+
for (const transport of transports) {
|
|
1040
|
+
const connection = transport._createConnection({ resolvers: { onIncomingActionDataJson: (json) => {
|
|
1041
|
+
for (const l of this._incomingActionDataListeners) l(json);
|
|
1042
|
+
} } });
|
|
1043
|
+
connection.definition = transport;
|
|
1044
|
+
this.transportManager.addTransport(connection);
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
forDomain(domain) {
|
|
1048
|
+
this.actionRouter.forDomain(domain, true);
|
|
1049
|
+
return this;
|
|
1050
|
+
}
|
|
1051
|
+
forAction(action) {
|
|
1052
|
+
this.actionRouter.forAction(action, true);
|
|
1053
|
+
return this;
|
|
1054
|
+
}
|
|
1055
|
+
forActionIds(domain, ids) {
|
|
1056
|
+
this.actionRouter.forActionIds(domain, ids, true);
|
|
1057
|
+
return this;
|
|
1058
|
+
}
|
|
1059
|
+
_setIncomingActionDataListener(listener) {
|
|
1060
|
+
this._incomingActionDataListeners.push(listener);
|
|
1061
|
+
}
|
|
1062
|
+
async handleActionRequest(action, config) {
|
|
1063
|
+
const localRuntime = config?.targetLocalRuntime ?? ActionRuntime.getDefault();
|
|
1064
|
+
const localClient = localRuntime.coordinate;
|
|
1065
|
+
const incomingTimeout = config?.timeout ?? this._defaultTimeout;
|
|
1066
|
+
const parentCuid = peekHandlerCuid();
|
|
1067
|
+
const callSite = action._callSite ?? (/* @__PURE__ */ new Error()).stack;
|
|
1068
|
+
const routeParams = {
|
|
1069
|
+
action,
|
|
1070
|
+
localClient,
|
|
1071
|
+
externalClient: this.externalClient
|
|
1072
|
+
};
|
|
1073
|
+
const preferredTransport = this.transportManager.getPreferredTransport();
|
|
1074
|
+
const routeItem = preferredTransport != null ? {
|
|
1075
|
+
runtime: localClient,
|
|
1076
|
+
handler: this.toHandlerRouteItem(preferredTransport, routeParams),
|
|
1077
|
+
time: Date.now()
|
|
1078
|
+
} : void 0;
|
|
1079
|
+
if (routeItem != null) action.context.addRouteItem(routeItem);
|
|
1080
|
+
const runningAction = new RunningAction({
|
|
1081
|
+
context: action.context,
|
|
1082
|
+
request: action,
|
|
1083
|
+
parentCuid,
|
|
1084
|
+
callSite
|
|
1085
|
+
});
|
|
1086
|
+
localRuntime.registerRunningAction(runningAction);
|
|
1087
|
+
this._dispatchWhenTransportReady(runningAction, routeParams, routeItem, incomingTimeout);
|
|
1088
|
+
return runningAction;
|
|
1089
|
+
}
|
|
1090
|
+
async _dispatchWhenTransportReady(runningAction, routeParams, routeItem, incomingTimeout) {
|
|
1091
|
+
const action = routeParams.action;
|
|
1092
|
+
try {
|
|
1093
|
+
const { methods, transport } = await this.transportManager.getReadyTransport(routeParams);
|
|
1094
|
+
const handlerRouteItem = this.toHandlerRouteItem(transport, routeParams);
|
|
1095
|
+
if (routeItem != null) {
|
|
1096
|
+
routeItem.handler = handlerRouteItem;
|
|
1097
|
+
routeItem.time = Date.now();
|
|
1098
|
+
} else action.context.addRouteItem({
|
|
1099
|
+
runtime: routeParams.localClient,
|
|
1100
|
+
handler: handlerRouteItem,
|
|
1101
|
+
time: Date.now()
|
|
1102
|
+
});
|
|
1103
|
+
const sendInput = {
|
|
1104
|
+
...routeParams,
|
|
1105
|
+
runningAction,
|
|
1106
|
+
timeout: incomingTimeout
|
|
1107
|
+
};
|
|
1108
|
+
if (action.type === "request" && methods.updateRunConfig != null) sendInput.timeout = methods.updateRunConfig(sendInput)?.timeout ?? incomingTimeout;
|
|
1109
|
+
methods.sendActionData(sendInput);
|
|
1110
|
+
} catch (err) {
|
|
1111
|
+
runningAction._abort(err);
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
/**
|
|
1115
|
+
* Dispatch a result or progress payload directly back to the external client via the best
|
|
1116
|
+
* available bidirectional transport (WebSocket / Custom). Used for return-path routing when the
|
|
1117
|
+
* local runtime recognises that it has a direct channel to the action's originClient.
|
|
1118
|
+
*
|
|
1119
|
+
* Returns `true` if the payload was sent, `false` if no suitable transport was available.
|
|
1120
|
+
*/
|
|
1121
|
+
async sendReturnPayload(payload, config) {
|
|
1122
|
+
const localClient = config.targetLocalRuntime.coordinate;
|
|
1123
|
+
try {
|
|
1124
|
+
const { methods } = await this.transportManager.getReadyTransport({
|
|
1125
|
+
action: payload,
|
|
1126
|
+
localClient,
|
|
1127
|
+
externalClient: this.externalClient
|
|
1128
|
+
});
|
|
1129
|
+
if (methods.sendReturnData == null) return false;
|
|
1130
|
+
methods.sendReturnData(payload, {
|
|
1131
|
+
localClient,
|
|
1132
|
+
externalClient: this.externalClient
|
|
1133
|
+
});
|
|
1134
|
+
return true;
|
|
1135
|
+
} catch {
|
|
1136
|
+
return false;
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
toJsonObject() {
|
|
1140
|
+
return {
|
|
1141
|
+
type: this.handlerType,
|
|
1142
|
+
client: this.externalClient
|
|
1143
|
+
};
|
|
1144
|
+
}
|
|
1145
|
+
toHandlerRouteItem(transport, input) {
|
|
1146
|
+
return {
|
|
1147
|
+
type: this.handlerType,
|
|
1148
|
+
client: this.externalClient,
|
|
1149
|
+
transOrd: transport.transOrd,
|
|
1150
|
+
transType: transport.type,
|
|
1151
|
+
transInfo: transport.getRouteInfo(input)
|
|
1152
|
+
};
|
|
1153
|
+
}
|
|
1154
|
+
clearTransportCache() {
|
|
1155
|
+
for (const entry of this._transportCache.values()) if (entry instanceof Promise) entry.then((ready) => ready.methods.disconnect?.()).catch(() => {});
|
|
1156
|
+
else entry.methods.disconnect?.();
|
|
1157
|
+
this._transportCache.clear();
|
|
1158
|
+
}
|
|
1159
|
+
};
|
|
1160
|
+
const createExternalClientHandler = (config) => {
|
|
1161
|
+
return new ActionExternalClientHandler(config);
|
|
1162
|
+
};
|
|
1163
|
+
//#endregion
|
|
1164
|
+
//#region src/ActionRuntime/ActionRuntime.ts
|
|
1165
|
+
var ActionRuntime = class {
|
|
1166
|
+
_coordinate;
|
|
1167
|
+
timeCreated;
|
|
1168
|
+
runtimeInfo = getAssumedRuntimeInfo();
|
|
1169
|
+
actionRouter;
|
|
1170
|
+
_pendingRunningActions = /* @__PURE__ */ new Map();
|
|
1171
|
+
_registeredExternalHandlers = [];
|
|
1172
|
+
_applied = false;
|
|
1173
|
+
static getDefault() {
|
|
1174
|
+
return getDefaultActionRuntime();
|
|
1175
|
+
}
|
|
1176
|
+
constructor(coordinate) {
|
|
1177
|
+
this._coordinate = coordinate.specifyIfUnset({ insId: (0, nanoid.nanoid)(14) });
|
|
1178
|
+
this.timeCreated = Date.now();
|
|
1179
|
+
this.actionRouter = new ActionRouter({
|
|
1180
|
+
contextType: "runtime_to_handler",
|
|
1181
|
+
runtime: this
|
|
1182
|
+
});
|
|
1183
|
+
}
|
|
1184
|
+
get coordinate() {
|
|
1185
|
+
return this._coordinate;
|
|
1186
|
+
}
|
|
1187
|
+
specifyRuntimeCoordinate(specifics) {
|
|
1188
|
+
if (specifics.envId != null && this._coordinate.envId !== specifics.envId) throw err_nice_action.fromId("not_implemented", { label: `updating RuntimeCoordinate with a different "envId" ("${this._coordinate.envId}" → "${specifics.envId}")` });
|
|
1189
|
+
this._coordinate = this._coordinate.specify(specifics);
|
|
1190
|
+
this.apply();
|
|
1191
|
+
}
|
|
1192
|
+
registerRunningAction(ra) {
|
|
1193
|
+
this._pendingRunningActions.set(ra.cuid, ra);
|
|
1194
|
+
ra.addUpdateListeners([(update) => {
|
|
1195
|
+
if (update.type === "finished") this._pendingRunningActions.delete(ra.cuid);
|
|
1196
|
+
}]);
|
|
1197
|
+
}
|
|
1198
|
+
resolveIncomingActionPayload(json) {
|
|
1199
|
+
if (json.type === "request") {
|
|
1200
|
+
this.handleActionPayloadWire(json).catch((err) => {
|
|
1201
|
+
console.error(`[ActionRuntime] Incoming action [${json.domain}:${json.id}:${json.form}:${json.type}] unhandled:`, err);
|
|
1202
|
+
});
|
|
1203
|
+
return;
|
|
1204
|
+
}
|
|
1205
|
+
this._pendingRunningActions.get(json.context.cuid)?._resolveFromJson(json);
|
|
1206
|
+
}
|
|
1207
|
+
async handleActionPayloadWire(wire) {
|
|
1208
|
+
let action;
|
|
1209
|
+
if (isActionPayload_Any_JsonObject(wire)) action = this.actionRouter.domainManager.getActionDomainOrThrow(wire).hydrateAnyAction(wire);
|
|
1210
|
+
if (action == null) throw err_nice_action.fromId("wire_not_action_data");
|
|
1211
|
+
return this.handleActionPayload(action);
|
|
1212
|
+
}
|
|
1213
|
+
async handleActionPayload(action, options) {
|
|
1214
|
+
if (action.type === "request") {
|
|
1215
|
+
const observers = action.context._domain._collectActionObservers();
|
|
1216
|
+
let handlerForAction;
|
|
1217
|
+
try {
|
|
1218
|
+
handlerForAction = this.getHandlerForActionOrThrow(action, options);
|
|
1219
|
+
} catch (err) {
|
|
1220
|
+
const runningAction = new RunningAction({
|
|
1221
|
+
context: action.context,
|
|
1222
|
+
request: action
|
|
1223
|
+
});
|
|
1224
|
+
runningAction.addUpdateListeners(observers);
|
|
1225
|
+
runningAction._completeWithResult(action.errorResult((0, _nice_code_error.castNiceError)(err)));
|
|
1226
|
+
return runningAction;
|
|
1227
|
+
}
|
|
1228
|
+
const runningAction = await handlerForAction.handleActionRequest(action, {
|
|
1229
|
+
...options,
|
|
1230
|
+
targetLocalRuntime: this
|
|
1231
|
+
});
|
|
1232
|
+
runningAction.addUpdateListeners(observers);
|
|
1233
|
+
this._trySetupReturnDispatch(runningAction);
|
|
1234
|
+
return runningAction;
|
|
1235
|
+
}
|
|
1236
|
+
throw err_nice_action.fromId("not_implemented", { label: `Handling incoming action payloads of type "${action.type}"` });
|
|
1237
|
+
}
|
|
1238
|
+
/**
|
|
1239
|
+
* @internal
|
|
1240
|
+
*
|
|
1241
|
+
* Return the first handler registered for the given action, or `undefined`
|
|
1242
|
+
* if none has been registered (action-ID-specific beats domain-wildcard).
|
|
1243
|
+
*/
|
|
1244
|
+
_getHandlerForAction(action, options) {
|
|
1245
|
+
const handlers = this.actionRouter.getRouteDataEntriesForAction(action);
|
|
1246
|
+
const targetExternalClient = options?.targetExternalClient;
|
|
1247
|
+
const possibleHandlers = handlers.filter((handler) => {
|
|
1248
|
+
if (handler.handlerType === "external") {
|
|
1249
|
+
if (targetExternalClient && !targetExternalClient.isSameFor(handler.externalClient).id) return false;
|
|
1250
|
+
return true;
|
|
1251
|
+
}
|
|
1252
|
+
if (targetExternalClient != null) return false;
|
|
1253
|
+
if (action.type === "request") return true;
|
|
1254
|
+
return false;
|
|
1255
|
+
});
|
|
1256
|
+
if (possibleHandlers.length === 0) return;
|
|
1257
|
+
const scoringExternalClient = targetExternalClient ?? RuntimeCoordinate.unknown;
|
|
1258
|
+
let handlerScore = -1;
|
|
1259
|
+
let handler;
|
|
1260
|
+
for (const possibleHandler of possibleHandlers) {
|
|
1261
|
+
if (possibleHandler.handlerType === "local" && handler == null) return possibleHandler;
|
|
1262
|
+
if (possibleHandler.handlerType === "external") {
|
|
1263
|
+
const score = scoringExternalClient.similarityLevel(possibleHandler.externalClient);
|
|
1264
|
+
if (score > handlerScore) {
|
|
1265
|
+
handlerScore = score;
|
|
1266
|
+
handler = possibleHandler;
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
return handler;
|
|
1271
|
+
}
|
|
1272
|
+
getHandlerForActionOrThrow(action, options) {
|
|
1273
|
+
const handler = this._getHandlerForAction(action, options);
|
|
1274
|
+
if (handler == null) throw err_nice_action.fromId("no_action_execution_handler", {
|
|
1275
|
+
actionId: action.id,
|
|
1276
|
+
domain: action.domain,
|
|
1277
|
+
specifiedClient: options?.targetExternalClient
|
|
1278
|
+
});
|
|
1279
|
+
return handler;
|
|
1280
|
+
}
|
|
1281
|
+
/**
|
|
1282
|
+
* Register one or more handlers. Each handler's own `actionRouter` defines
|
|
1283
|
+
* which domains/actions it handles — those routing keys are mirrored into
|
|
1284
|
+
* this runtime's router so the same action can be served by multiple handlers.
|
|
1285
|
+
* Duplicate registrations (same handler cuid for the same key) are skipped.
|
|
1286
|
+
*/
|
|
1287
|
+
addHandlers(handlers) {
|
|
1288
|
+
for (const handler of handlers) {
|
|
1289
|
+
if (handler.handlerType === "external") {
|
|
1290
|
+
handler._setIncomingActionDataListener((json) => this.resolveIncomingActionPayload(json));
|
|
1291
|
+
this._registeredExternalHandlers.push(handler);
|
|
1292
|
+
}
|
|
1293
|
+
const handlerRouter = handler.getActionRouter();
|
|
1294
|
+
this.actionRouter.addDomainsFromOther(handlerRouter);
|
|
1295
|
+
if (this._applied) this.apply();
|
|
1296
|
+
for (const key of handlerRouter.getRegisteredKeys()) if (!this.actionRouter.getForKey(key).some((h) => h.cuid === handler.cuid)) this.actionRouter.addForKey(key, handler);
|
|
1297
|
+
}
|
|
1298
|
+
return this;
|
|
1299
|
+
}
|
|
1300
|
+
/**
|
|
1301
|
+
* Declare an external "backend client" in one call: build an
|
|
1302
|
+
* {@link ActionExternalClientHandler} for `externalCoordinate` carrying the given
|
|
1303
|
+
* `transports`, route the listed `domains`/`actions` to it, register it (plus any
|
|
1304
|
+
* `localHandlers` — e.g. server→client push handlers that share the same channel)
|
|
1305
|
+
* on this runtime, and `apply()`. Returns the external handler so the caller can
|
|
1306
|
+
* later `clearTransportCache()` it.
|
|
1307
|
+
*
|
|
1308
|
+
* Sugar over `new ActionExternalClientHandler(...).forDomain(...)` + `addHandlers([...])`,
|
|
1309
|
+
* so a single runtime can host one handler per backend target with its transports
|
|
1310
|
+
* declared once and reused across every action routed to that backend.
|
|
1311
|
+
*/
|
|
1312
|
+
connectTo(externalCoordinate, options) {
|
|
1313
|
+
const handler = new ActionExternalClientHandler({
|
|
1314
|
+
runtimeCoordinate: externalCoordinate,
|
|
1315
|
+
transports: options.transports,
|
|
1316
|
+
defaultTimeout: options.defaultTimeout
|
|
1317
|
+
});
|
|
1318
|
+
for (const domain of options.domains ?? []) handler.forDomain(domain);
|
|
1319
|
+
for (const action of options.actions ?? []) handler.forAction(action);
|
|
1320
|
+
this.addHandlers([handler, ...options.localHandlers ?? []]);
|
|
1321
|
+
this.apply();
|
|
1322
|
+
return handler;
|
|
1323
|
+
}
|
|
1324
|
+
applyRuntimeForDomain(domain) {
|
|
1325
|
+
const rootDomain = domain.rootDomain;
|
|
1326
|
+
if (!rootDomain._hasRuntime(this)) rootDomain._registerRuntime(this);
|
|
1327
|
+
}
|
|
1328
|
+
/**
|
|
1329
|
+
* Register this runtime with all root domains covered by its currently-added handlers,
|
|
1330
|
+
* making it eligible to execute actions dispatched from those domains.
|
|
1331
|
+
* After apply() is called, any subsequent addHandlers() calls also auto-register.
|
|
1332
|
+
*/
|
|
1333
|
+
apply() {
|
|
1334
|
+
this._applied = true;
|
|
1335
|
+
for (const domain of this.actionRouter.getDomains()) this.applyRuntimeForDomain(domain);
|
|
1336
|
+
return this;
|
|
1337
|
+
}
|
|
1338
|
+
/**
|
|
1339
|
+
* Find the best registered external handler that can reach `originClient` directly.
|
|
1340
|
+
* Used to locate the return-path channel for dispatching results back to the action origin.
|
|
1341
|
+
* Returns `undefined` if no handler matches (score > 0 required, i.e. at least id must match).
|
|
1342
|
+
*/
|
|
1343
|
+
getReturnHandlerForOrigin(originClient) {
|
|
1344
|
+
if (originClient.envId === "_unset_") return void 0;
|
|
1345
|
+
let bestScore = -1;
|
|
1346
|
+
let bestHandler;
|
|
1347
|
+
for (const handler of this._registeredExternalHandlers) {
|
|
1348
|
+
const score = originClient.similarityLevel(handler.externalClient);
|
|
1349
|
+
if (score > bestScore) {
|
|
1350
|
+
bestScore = score;
|
|
1351
|
+
bestHandler = handler;
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
return bestScore > 0 ? bestHandler : void 0;
|
|
1355
|
+
}
|
|
1356
|
+
resetRuntime() {
|
|
1357
|
+
for (const ra of this._pendingRunningActions.values()) ra._abort(err_nice_action.fromId("runtime_reset"));
|
|
1358
|
+
for (const handler of this._registeredExternalHandlers) handler.clearTransportCache();
|
|
1359
|
+
}
|
|
1360
|
+
_trySetupReturnDispatch(runningAction) {
|
|
1361
|
+
const originClient = runningAction.context.originClient;
|
|
1362
|
+
if (originClient.envId === "_unset_" || originClient.isSameFor(this._coordinate).id) return;
|
|
1363
|
+
runningAction.addUpdateListeners([(update) => {
|
|
1364
|
+
if (update.type === "finished" && update.finishType === "success") this.getReturnHandlerForOrigin(originClient)?.sendReturnPayload(update.response, { targetLocalRuntime: this }).catch(() => {});
|
|
1365
|
+
}]);
|
|
1366
|
+
}
|
|
1367
|
+
};
|
|
1368
|
+
const runtimeState = {
|
|
1369
|
+
defaultLocalRuntime: void 0,
|
|
1370
|
+
assumedRuntimeInfo: void 0
|
|
1371
|
+
};
|
|
1372
|
+
function getDefaultActionRuntime() {
|
|
1373
|
+
if (runtimeState.assumedRuntimeInfo == null) runtimeState.assumedRuntimeInfo = getAssumedRuntimeInfo();
|
|
1374
|
+
if (runtimeState.defaultLocalRuntime == null) runtimeState.defaultLocalRuntime = new ActionRuntime(RuntimeCoordinate.unknown.specify({ perId: `${runtimeState.assumedRuntimeInfo?.runtimeName ?? "unknown"}-runtime` }));
|
|
1375
|
+
return runtimeState.defaultLocalRuntime;
|
|
1376
|
+
}
|
|
1377
|
+
//#endregion
|
|
1378
|
+
//#region src/ActionRuntime/Handler/Local/ActionLocalHandler.ts
|
|
1379
|
+
var ActionLocalHandler = class extends ActionHandler {
|
|
1380
|
+
handlerType = "local";
|
|
1381
|
+
actionRouter = new ActionRouter({
|
|
1382
|
+
contextType: "handler_route",
|
|
1383
|
+
handler: this
|
|
1384
|
+
});
|
|
1385
|
+
constructor() {
|
|
1386
|
+
super();
|
|
1387
|
+
}
|
|
1388
|
+
/**
|
|
1389
|
+
* Register a handler for all actions in a domain.
|
|
1390
|
+
* Receives the full primed action — use `matchAction()` to narrow to a specific action id.
|
|
1391
|
+
* Useful for forwarding all domain actions to a remote endpoint.
|
|
1392
|
+
* Lower priority than `forAction`.
|
|
1393
|
+
*/
|
|
1394
|
+
forDomain(domain, handler) {
|
|
1395
|
+
this.actionRouter.forDomain(domain, handler);
|
|
1396
|
+
return this;
|
|
1397
|
+
}
|
|
1398
|
+
/**
|
|
1399
|
+
* Register a handler for a base action instance. Takes priority over domain-wide handlers.
|
|
1400
|
+
* Receives the full primed action with narrowed input type.
|
|
1401
|
+
* Useful for handling specific actions locally while forwarding the rest of the domain. For example, a local "ping" action that checks connectivity without needing a round trip.
|
|
1402
|
+
*/
|
|
1403
|
+
forAction(action, handler) {
|
|
1404
|
+
this.actionRouter.forAction(action, handler);
|
|
1405
|
+
return this;
|
|
1406
|
+
}
|
|
1407
|
+
/**
|
|
1408
|
+
* Register a handler for multiple action IDs (first-match-wins among cases).
|
|
1409
|
+
* Receives the full primed action narrowed to the union of those IDs.
|
|
1410
|
+
* Use `act.coreAction.id` to branch on which action was dispatched.
|
|
1411
|
+
*/
|
|
1412
|
+
forActionIds(domain, ids, handler) {
|
|
1413
|
+
this.actionRouter.forActionIds(domain, ids, handler);
|
|
1414
|
+
return this;
|
|
1415
|
+
}
|
|
1416
|
+
/**
|
|
1417
|
+
* Register per-action handlers for a domain using a single map, without needing
|
|
1418
|
+
* separate `forAction` calls. Unregistered action IDs are unaffected.
|
|
1419
|
+
*
|
|
1420
|
+
* @example
|
|
1421
|
+
* ```ts
|
|
1422
|
+
* handler.forDomainActionCases(userDomain, {
|
|
1423
|
+
* getUser: (primed) => db.getUser(primed.input.userId),
|
|
1424
|
+
* deleteUser: (primed) => db.deleteUser(primed.input.userId),
|
|
1425
|
+
* });
|
|
1426
|
+
* ```
|
|
1427
|
+
*/
|
|
1428
|
+
forDomainActionCases(domain, cases) {
|
|
1429
|
+
this.actionRouter.forDomainActionCases(domain, cases);
|
|
1430
|
+
return this;
|
|
1431
|
+
}
|
|
1432
|
+
async handleActionRequest(action, config) {
|
|
1433
|
+
const targetLocalRuntime = config?.targetLocalRuntime ?? ActionRuntime.getDefault();
|
|
1434
|
+
const handler = this.actionRouter.getRouteDataForActionOrThrow(action, { targetLocalRuntime });
|
|
1435
|
+
action.context.addRouteItem({
|
|
1436
|
+
runtime: targetLocalRuntime.coordinate,
|
|
1437
|
+
handler: this.toHandlerRouteItem(),
|
|
1438
|
+
time: Date.now()
|
|
1439
|
+
});
|
|
1440
|
+
const runningAction = new RunningAction({
|
|
1441
|
+
context: action.context,
|
|
1442
|
+
request: action,
|
|
1443
|
+
parentCuid: peekHandlerCuid(),
|
|
1444
|
+
callSite: action._callSite ?? (/* @__PURE__ */ new Error()).stack
|
|
1445
|
+
});
|
|
1446
|
+
this._handleRunningAction(handler, runningAction).catch((err) => {
|
|
1447
|
+
if (err instanceof _nice_code_error.NiceError) runningAction._completeWithResult(action.errorResult(err));
|
|
1448
|
+
else if ((0, _nice_code_error.isNiceErrorObject)(err)) runningAction._completeWithResult(action.errorResult((0, _nice_code_error.castNiceError)(err)));
|
|
1449
|
+
else runningAction._abort(err);
|
|
1450
|
+
});
|
|
1451
|
+
return runningAction;
|
|
1452
|
+
}
|
|
1453
|
+
async _handleRunningAction(handler, runningAction) {
|
|
1454
|
+
const state = runningAction.state;
|
|
1455
|
+
if (state.result != null) return;
|
|
1456
|
+
await Promise.resolve();
|
|
1457
|
+
pushHandlerCuid(runningAction.cuid);
|
|
1458
|
+
try {
|
|
1459
|
+
const rawResult = await handler(state.request);
|
|
1460
|
+
let result;
|
|
1461
|
+
if (rawResult instanceof ActionPayload_Result) result = rawResult;
|
|
1462
|
+
else if (rawResult != null && isActionPayload_Result_JsonObject(rawResult)) result = this.actionRouter.domainManager.getActionDomainOrThrow(state.request).hydrateResultPayload(rawResult);
|
|
1463
|
+
else result = state.request.successResult(rawResult);
|
|
1464
|
+
runningAction._completeWithResult(result);
|
|
1465
|
+
} finally {
|
|
1466
|
+
popHandlerCuid();
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
async handlePayloadWireOrThrow(wire, config) {
|
|
1470
|
+
const hydratedAction = this.actionRouter.domainManager.hydrateActionPayload(wire);
|
|
1471
|
+
if (!(hydratedAction instanceof ActionPayload_Request)) throw err_nice_action.fromId("wire_action_not_payload", {
|
|
1472
|
+
domain: hydratedAction.domain,
|
|
1473
|
+
actionId: hydratedAction.id,
|
|
1474
|
+
actionState: hydratedAction.type ?? hydratedAction.form
|
|
1475
|
+
});
|
|
1476
|
+
return await this.handleActionRequest(hydratedAction, config);
|
|
1477
|
+
}
|
|
1478
|
+
toJsonObject() {
|
|
1479
|
+
return { type: this.handlerType };
|
|
1480
|
+
}
|
|
1481
|
+
toHandlerRouteItem() {
|
|
1482
|
+
return { type: this.handlerType };
|
|
1483
|
+
}
|
|
1484
|
+
};
|
|
1485
|
+
const createLocalHandler = () => {
|
|
1486
|
+
return new ActionLocalHandler();
|
|
1487
|
+
};
|
|
1488
|
+
//#endregion
|
|
1489
|
+
//#region src/utils/isAction_Context_JsonObject.ts
|
|
1490
|
+
const isAction_Context_JsonObject = (obj) => {
|
|
1491
|
+
return isAction_Base_JsonObject(obj) && obj.form === "context";
|
|
1492
|
+
};
|
|
1493
|
+
//#endregion
|
|
1494
|
+
//#region src/utils/isAction_Core_JsonObject.ts
|
|
1495
|
+
const isAction_Core_JsonObject = (obj) => {
|
|
1496
|
+
return isAction_Base_JsonObject(obj) && obj.form === "core";
|
|
1497
|
+
};
|
|
1498
|
+
//#endregion
|
|
1499
|
+
//#region src/utils/isAction_Any_JsonObject.ts
|
|
1500
|
+
function isAction_Any_JsonObject(obj) {
|
|
1501
|
+
return isActionPayload_Any_JsonObject(obj) || isAction_Context_JsonObject(obj) || isAction_Core_JsonObject(obj);
|
|
1502
|
+
}
|
|
1503
|
+
//#endregion
|
|
1504
|
+
//#region src/utils/assertIsActionJson.ts
|
|
1505
|
+
function assertIsActionJson(obj) {
|
|
1506
|
+
if (!isAction_Any_JsonObject(obj)) throw err_nice_action.fromId("wire_not_action_data");
|
|
1507
|
+
}
|
|
1508
|
+
//#endregion
|
|
1509
|
+
//#region src/utils/isAction_Any_Instance.ts
|
|
1510
|
+
function isAction_Any_Instance(value) {
|
|
1511
|
+
return value instanceof ActionCore || value instanceof ActionPayload || value instanceof ActionContext;
|
|
1512
|
+
}
|
|
1513
|
+
//#endregion
|
|
1514
|
+
//#region src/ActionDefinition/Domain/ActionDomainBase.ts
|
|
1515
|
+
var ActionDomainBase = class {
|
|
1516
|
+
domain;
|
|
1517
|
+
allDomains;
|
|
1518
|
+
actionSchema;
|
|
1519
|
+
_listeners = [];
|
|
1520
|
+
constructor(definition) {
|
|
1521
|
+
this.domain = definition.domain;
|
|
1522
|
+
this.allDomains = definition.allDomains;
|
|
1523
|
+
this.actionSchema = definition.actionSchema;
|
|
1524
|
+
}
|
|
1525
|
+
/**
|
|
1526
|
+
* Add an observer that is called after every action dispatched through this domain.
|
|
1527
|
+
* Returns an unsubscribe function — call it to remove the listener.
|
|
1528
|
+
*/
|
|
1529
|
+
addActionListener(listener) {
|
|
1530
|
+
this._listeners.push(listener);
|
|
1531
|
+
return () => {
|
|
1532
|
+
this._listeners = this._listeners.filter((l) => l !== listener);
|
|
1533
|
+
};
|
|
1534
|
+
}
|
|
1535
|
+
/**
|
|
1536
|
+
* @internal
|
|
1537
|
+
* Observers registered directly on this domain via {@link addActionListener}.
|
|
1538
|
+
* Used to wire observers (e.g. devtools) onto RunningActions that aren't created
|
|
1539
|
+
* through the local-dispatch path — notably inbound actions pushed from a backend
|
|
1540
|
+
* or another client over a bidirectional transport.
|
|
1541
|
+
*/
|
|
1542
|
+
_getActionObservers() {
|
|
1543
|
+
return this._listeners;
|
|
1544
|
+
}
|
|
1545
|
+
};
|
|
1546
|
+
//#endregion
|
|
1547
|
+
//#region src/ActionRuntime/ActionRuntimeManager.ts
|
|
1548
|
+
var ActionRuntimeManager = class {
|
|
1549
|
+
_runtimes = /* @__PURE__ */ new Map();
|
|
1550
|
+
_preferredRuntimeClientId = null;
|
|
1551
|
+
_context;
|
|
1552
|
+
constructor(context) {
|
|
1553
|
+
this._context = context ?? {};
|
|
1554
|
+
}
|
|
1555
|
+
registerRuntime(runtime) {
|
|
1556
|
+
const runtimeId = runtime.coordinate.stringId;
|
|
1557
|
+
if (this._runtimes.has(runtimeId)) throw err_nice_action.fromId("client_runtime_already_registered", {
|
|
1558
|
+
context: this._context,
|
|
1559
|
+
client: runtime.coordinate
|
|
1560
|
+
});
|
|
1561
|
+
for (const id of runtime.coordinate.toStringIds()) {
|
|
1562
|
+
if (this._runtimes.has(id)) continue;
|
|
1563
|
+
this._runtimes.set(id, runtime);
|
|
1564
|
+
}
|
|
1565
|
+
}
|
|
1566
|
+
getRuntimeAndHandlerForAction(action, options, throwOnIssue) {
|
|
1567
|
+
const localRuntime = options?.targetLocalRuntime;
|
|
1568
|
+
if (localRuntime != null) {
|
|
1569
|
+
const runtime = throwOnIssue ? this.getBestRuntimeOrThrow(options?.targetLocalRuntime?.coordinate) : this.getBestRuntime(options?.targetLocalRuntime?.coordinate);
|
|
1570
|
+
if (runtime == null) return;
|
|
1571
|
+
const handler = runtime._getHandlerForAction(action, options);
|
|
1572
|
+
if (handler != null) return {
|
|
1573
|
+
handler,
|
|
1574
|
+
runtime
|
|
1575
|
+
};
|
|
1576
|
+
if (throwOnIssue) throw err_nice_action.fromId("no_action_execution_handler", {
|
|
1577
|
+
domain: action.domain,
|
|
1578
|
+
actionId: action.id,
|
|
1579
|
+
specifiedClient: localRuntime.coordinate
|
|
1580
|
+
});
|
|
1581
|
+
}
|
|
1582
|
+
for (const runtime of this._runtimes.values()) {
|
|
1583
|
+
const handler = runtime._getHandlerForAction(action);
|
|
1584
|
+
if (handler) return {
|
|
1585
|
+
handler,
|
|
1586
|
+
runtime
|
|
1587
|
+
};
|
|
1588
|
+
}
|
|
1589
|
+
if (throwOnIssue) throw err_nice_action.fromId("no_action_execution_handler", {
|
|
1590
|
+
domain: action.domain,
|
|
1591
|
+
actionId: action.id,
|
|
1592
|
+
specifiedClient: options?.targetLocalRuntime?.coordinate
|
|
1593
|
+
});
|
|
1594
|
+
}
|
|
1595
|
+
getRuntimeAndHandlerForActionOrThrow(action, options) {
|
|
1596
|
+
return this.getRuntimeAndHandlerForAction(action, options, true);
|
|
1597
|
+
}
|
|
1598
|
+
setPreferredRuntime(runtime) {
|
|
1599
|
+
const runtimeId = runtime.coordinate.stringId;
|
|
1600
|
+
this._preferredRuntimeClientId = runtimeId;
|
|
1601
|
+
}
|
|
1602
|
+
getPreferredRuntime() {
|
|
1603
|
+
if (this._preferredRuntimeClientId) {
|
|
1604
|
+
const runtime = this._runtimes.get(this._preferredRuntimeClientId);
|
|
1605
|
+
if (runtime) return runtime;
|
|
1606
|
+
}
|
|
1607
|
+
return this._runtimes.values().next().value;
|
|
1608
|
+
}
|
|
1609
|
+
getBestRuntimeForSpecifier(clientSpecifier) {
|
|
1610
|
+
const ids = new RuntimeCoordinate(clientSpecifier).toStringIds();
|
|
1611
|
+
for (const id of ids) {
|
|
1612
|
+
const runtime = this._runtimes.get(id);
|
|
1613
|
+
if (runtime) return runtime;
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
1616
|
+
getBestRuntime(clientSpecifier) {
|
|
1617
|
+
return clientSpecifier != null ? this.getBestRuntimeForSpecifier(clientSpecifier) : this.getPreferredRuntime();
|
|
1618
|
+
}
|
|
1619
|
+
hasRuntime(runtime) {
|
|
1620
|
+
return this._runtimes.has(runtime.coordinate.stringId);
|
|
1621
|
+
}
|
|
1622
|
+
getBestRuntimeOrThrow(specifier) {
|
|
1623
|
+
const runtime = this.getBestRuntime(specifier);
|
|
1624
|
+
if (!runtime) {
|
|
1625
|
+
if (specifier == null) throw err_nice_action.fromId("no_client_runtimes_registered", { context: this._context });
|
|
1626
|
+
throw err_nice_action.fromId("client_runtime_not_registered", {
|
|
1627
|
+
context: this._context,
|
|
1628
|
+
clientStringId: runtimeCoordinateToStringIds(specifier)[0]
|
|
1629
|
+
});
|
|
1630
|
+
}
|
|
1631
|
+
return runtime;
|
|
1632
|
+
}
|
|
1633
|
+
};
|
|
1634
|
+
//#endregion
|
|
1635
|
+
//#region src/ActionDefinition/Domain/ActionRootDomain.ts
|
|
1636
|
+
var ActionRootDomain = class extends ActionDomainBase {
|
|
1637
|
+
domainDefinition;
|
|
1638
|
+
_actionRuntimeManager;
|
|
1639
|
+
constructor(domainDefinition) {
|
|
1640
|
+
const domainId = domainDefinition.domain;
|
|
1641
|
+
super({
|
|
1642
|
+
domain: domainId,
|
|
1643
|
+
allDomains: [domainId],
|
|
1644
|
+
actionSchema: {}
|
|
1645
|
+
});
|
|
1646
|
+
this.domainDefinition = domainDefinition;
|
|
1647
|
+
this._actionRuntimeManager = new ActionRuntimeManager({ domain: domainId });
|
|
1648
|
+
}
|
|
1649
|
+
createChildDomain(subDomainDef) {
|
|
1650
|
+
if (this.allDomains.includes(subDomainDef.domain)) throw err_nice_action.fromId("domain_already_exists_in_hierarchy", {
|
|
1651
|
+
domain: subDomainDef.domain,
|
|
1652
|
+
allParentDomains: this.allDomains,
|
|
1653
|
+
parentDomain: this.domain
|
|
1654
|
+
});
|
|
1655
|
+
return new ActionDomain({
|
|
1656
|
+
allDomains: [...this.allDomains, subDomainDef.domain],
|
|
1657
|
+
domain: subDomainDef.domain,
|
|
1658
|
+
actionSchema: subDomainDef.actions
|
|
1659
|
+
}, { rootDomain: this });
|
|
1660
|
+
}
|
|
1661
|
+
_registerRuntime(runtime) {
|
|
1662
|
+
this._actionRuntimeManager.registerRuntime(runtime);
|
|
1663
|
+
}
|
|
1664
|
+
_hasRuntime(runtime) {
|
|
1665
|
+
return this._actionRuntimeManager.hasRuntime(runtime);
|
|
1666
|
+
}
|
|
1667
|
+
getRuntime(clientSpecifier) {
|
|
1668
|
+
return this._actionRuntimeManager.getBestRuntimeForSpecifier(clientSpecifier);
|
|
1669
|
+
}
|
|
1670
|
+
async _runAction(actionPayload, options) {
|
|
1671
|
+
const allListeners = [...this._listeners, ...options?.listeners ?? []];
|
|
1672
|
+
let handlerAndRuntime;
|
|
1673
|
+
try {
|
|
1674
|
+
handlerAndRuntime = this._actionRuntimeManager.getRuntimeAndHandlerForActionOrThrow(actionPayload, options);
|
|
1675
|
+
} catch (err) {
|
|
1676
|
+
const runningAction = new RunningAction({
|
|
1677
|
+
context: actionPayload.context,
|
|
1678
|
+
request: actionPayload,
|
|
1679
|
+
callSite: actionPayload._callSite
|
|
1680
|
+
});
|
|
1681
|
+
runningAction.addUpdateListeners(allListeners);
|
|
1682
|
+
runningAction._failWithError(err);
|
|
1683
|
+
throw err;
|
|
1684
|
+
}
|
|
1685
|
+
const { handler, runtime } = handlerAndRuntime;
|
|
1686
|
+
actionPayload.context._setOriginClient(runtime.coordinate);
|
|
1687
|
+
const runningAction = await handler.handleActionRequest(actionPayload, { targetLocalRuntime: runtime });
|
|
1688
|
+
runningAction.addUpdateListeners(allListeners);
|
|
1689
|
+
return runningAction;
|
|
1690
|
+
}
|
|
1691
|
+
};
|
|
1692
|
+
//#endregion
|
|
1693
|
+
//#region src/ActionDefinition/Domain/ActionDomain.ts
|
|
1694
|
+
var ActionDomain = class ActionDomain extends ActionDomainBase {
|
|
1695
|
+
_rootDomain;
|
|
1696
|
+
_actionMap;
|
|
1697
|
+
constructor(definition, { rootDomain }) {
|
|
1698
|
+
super(definition);
|
|
1699
|
+
this._rootDomain = rootDomain;
|
|
1700
|
+
this._actionMap = this.createActionMap();
|
|
1701
|
+
}
|
|
1702
|
+
get rootDomain() {
|
|
1703
|
+
return this._rootDomain;
|
|
1704
|
+
}
|
|
1705
|
+
/**
|
|
1706
|
+
* @internal
|
|
1707
|
+
* All action observers that should see actions on this domain: the root domain's
|
|
1708
|
+
* observers plus this subdomain's own. Mirrors the listener set the local-dispatch
|
|
1709
|
+
* path assembles in `runAction`/`_runAction`, so inbound actions (pushed from a
|
|
1710
|
+
* backend or another client) can be wired up identically and surface in devtools.
|
|
1711
|
+
*/
|
|
1712
|
+
_collectActionObservers() {
|
|
1713
|
+
return [...this._rootDomain._getActionObservers(), ...this._getActionObservers()];
|
|
1714
|
+
}
|
|
1715
|
+
_registerRuntime(runtime) {
|
|
1716
|
+
this._rootDomain._registerRuntime(runtime);
|
|
1717
|
+
}
|
|
1718
|
+
createChildDomain(subDomainDef) {
|
|
1719
|
+
if (this.allDomains.includes(subDomainDef.domain)) throw err_nice_action.fromId("domain_already_exists_in_hierarchy", {
|
|
1720
|
+
domain: subDomainDef.domain,
|
|
1721
|
+
allParentDomains: this.allDomains,
|
|
1722
|
+
parentDomain: this.domain
|
|
1723
|
+
});
|
|
1724
|
+
return new ActionDomain({
|
|
1725
|
+
allDomains: [...this.allDomains, subDomainDef.domain],
|
|
1726
|
+
domain: subDomainDef.domain,
|
|
1727
|
+
actionSchema: subDomainDef.actions
|
|
1728
|
+
}, { rootDomain: this._rootDomain });
|
|
1729
|
+
}
|
|
1730
|
+
get action() {
|
|
1731
|
+
return this._actionMap;
|
|
1732
|
+
}
|
|
1733
|
+
actionsMap() {
|
|
1734
|
+
return this._actionMap;
|
|
1735
|
+
}
|
|
1736
|
+
actionForId(id) {
|
|
1737
|
+
if (!this.actionSchema[id]) throw err_nice_action.fromId("action_id_not_in_domain", {
|
|
1738
|
+
domain: this.domain,
|
|
1739
|
+
actionId: id
|
|
1740
|
+
});
|
|
1741
|
+
return new ActionCore(this, id);
|
|
1742
|
+
}
|
|
1743
|
+
wrapAsPartialLocalHandler(wrappedActionExecutor) {
|
|
1744
|
+
const _handler = new ActionLocalHandler();
|
|
1745
|
+
const executor = wrappedActionExecutor;
|
|
1746
|
+
for (const actionKey in wrappedActionExecutor) {
|
|
1747
|
+
if (!this.actionSchema[actionKey]) continue;
|
|
1748
|
+
_handler.forAction(this.actionForId(actionKey), (request) => executor[request.id](request.input));
|
|
1749
|
+
}
|
|
1750
|
+
return _handler;
|
|
1751
|
+
}
|
|
1752
|
+
wrapAsLocalHandler(wrappedActionExecutor) {
|
|
1753
|
+
const _handler = new ActionLocalHandler();
|
|
1754
|
+
const executor = wrappedActionExecutor;
|
|
1755
|
+
return _handler.forDomain(this, (request) => executor[request.id](request.input));
|
|
1756
|
+
}
|
|
1757
|
+
hydrateContext(id, contextData) {
|
|
1758
|
+
return new ActionContext(this, id, {
|
|
1759
|
+
timeCreated: contextData.timeCreated,
|
|
1760
|
+
cuid: contextData.cuid,
|
|
1761
|
+
routing: contextData.routing.map((item) => {
|
|
1762
|
+
return {
|
|
1763
|
+
runtime: new RuntimeCoordinate(item.runtime),
|
|
1764
|
+
handler: item.handler,
|
|
1765
|
+
time: item.time
|
|
1766
|
+
};
|
|
1767
|
+
}),
|
|
1768
|
+
originClient: contextData.originClient ? new RuntimeCoordinate(contextData.originClient) : RuntimeCoordinate.unknown
|
|
1769
|
+
});
|
|
1770
|
+
}
|
|
1771
|
+
isDomainAction(action) {
|
|
1772
|
+
return isAction_Any_Instance(action) && action.domain === this.domain;
|
|
1773
|
+
}
|
|
1774
|
+
hydrateRequestPayload(serialized) {
|
|
1775
|
+
if (serialized.type !== "request") throw err_nice_action.fromId("hydration_action_state_mismatch", {
|
|
1776
|
+
expected: "request",
|
|
1777
|
+
received: serialized.type
|
|
1778
|
+
});
|
|
1779
|
+
if (serialized.domain !== this.domain) throw err_nice_action.fromId("hydration_domain_mismatch", {
|
|
1780
|
+
expected: this.domain,
|
|
1781
|
+
received: serialized.domain
|
|
1782
|
+
});
|
|
1783
|
+
const id = serialized.id;
|
|
1784
|
+
if (!this.actionSchema[id]) throw err_nice_action.fromId("hydration_action_id_not_found", {
|
|
1785
|
+
domain: this.domain,
|
|
1786
|
+
actionId: serialized.id
|
|
1787
|
+
});
|
|
1788
|
+
const contextAction = this.hydrateContext(id, serialized.context);
|
|
1789
|
+
return new ActionPayload_Request({ context: contextAction }, contextAction.deserializeInput(serialized.input), { time: serialized.time });
|
|
1790
|
+
}
|
|
1791
|
+
hydrateResultPayload(serialized) {
|
|
1792
|
+
if (serialized.type !== "result") throw err_nice_action.fromId("hydration_action_state_mismatch", {
|
|
1793
|
+
expected: "result",
|
|
1794
|
+
received: serialized.type
|
|
1795
|
+
});
|
|
1796
|
+
if (serialized.domain !== this.domain) throw err_nice_action.fromId("hydration_domain_mismatch", {
|
|
1797
|
+
expected: this.domain,
|
|
1798
|
+
received: serialized.domain
|
|
1799
|
+
});
|
|
1800
|
+
const id = serialized.id;
|
|
1801
|
+
if (!this.actionSchema[id]) throw err_nice_action.fromId("hydration_action_id_not_found", {
|
|
1802
|
+
domain: this.domain,
|
|
1803
|
+
actionId: serialized.id
|
|
1804
|
+
});
|
|
1805
|
+
const contextAction = this.hydrateContext(id, serialized.context);
|
|
1806
|
+
const result = serialized.result.ok ? {
|
|
1807
|
+
ok: true,
|
|
1808
|
+
output: contextAction.schema.deserializeOutput(serialized.result.output)
|
|
1809
|
+
} : serialized.result;
|
|
1810
|
+
return new ActionPayload_Result({ context: contextAction }, result, { time: serialized.time });
|
|
1811
|
+
}
|
|
1812
|
+
hydrateAnyAction(actionJson) {
|
|
1813
|
+
assertIsActionJson(actionJson);
|
|
1814
|
+
if (actionJson.form === "data") {
|
|
1815
|
+
if (actionJson.type === "request") return this.hydrateRequestPayload(actionJson);
|
|
1816
|
+
if (actionJson.type === "result") return this.hydrateResultPayload(actionJson);
|
|
1817
|
+
}
|
|
1818
|
+
return this.actionForId(actionJson.id);
|
|
1819
|
+
}
|
|
1820
|
+
async runAction(request, options) {
|
|
1821
|
+
const allListeners = [...options?.listeners ?? [], ...this._listeners];
|
|
1822
|
+
return this._rootDomain._runAction(request, {
|
|
1823
|
+
...options,
|
|
1824
|
+
listeners: allListeners
|
|
1825
|
+
});
|
|
1826
|
+
}
|
|
1827
|
+
createActionMap() {
|
|
1828
|
+
const map = {};
|
|
1829
|
+
for (const id in this.actionSchema) map[id] = new ActionCore(this, id);
|
|
1830
|
+
return map;
|
|
1831
|
+
}
|
|
1832
|
+
};
|
|
1833
|
+
//#endregion
|
|
1834
|
+
//#region src/ActionDefinition/Domain/helpers/createRootActionDomain.ts
|
|
1835
|
+
const createActionRootDomain = (definition) => {
|
|
1836
|
+
return new ActionRootDomain(definition);
|
|
1837
|
+
};
|
|
1838
|
+
//#endregion
|
|
1839
|
+
//#region src/ActionDefinition/Schema/ActionSchema.ts
|
|
1840
|
+
var ActionSchema = class {
|
|
1841
|
+
_errorDeclarations = [];
|
|
1842
|
+
inputOptions;
|
|
1843
|
+
outputOptions;
|
|
1844
|
+
get inputSchema() {
|
|
1845
|
+
return this.inputOptions?.schema;
|
|
1846
|
+
}
|
|
1847
|
+
get outputSchema() {
|
|
1848
|
+
return this.outputOptions?.schema;
|
|
1849
|
+
}
|
|
1850
|
+
/**
|
|
1851
|
+
* Declare the input schema (JSON-native or with explicit SERDE type param).
|
|
1852
|
+
* For non-JSON-native inputs, prefer the 3-argument form below to avoid
|
|
1853
|
+
* needing explicit type parameters.
|
|
1854
|
+
*/
|
|
1855
|
+
input(options) {
|
|
1856
|
+
this.inputOptions = options;
|
|
1857
|
+
return this;
|
|
1858
|
+
}
|
|
1859
|
+
/**
|
|
1860
|
+
* Declare the output schema (JSON-native or with explicit SERDE type param).
|
|
1861
|
+
* For non-JSON-native outputs, prefer the 3-argument form below to avoid
|
|
1862
|
+
* needing explicit type parameters.
|
|
1863
|
+
*/
|
|
1864
|
+
output(options) {
|
|
1865
|
+
this.outputOptions = options;
|
|
1866
|
+
return this;
|
|
1867
|
+
}
|
|
1868
|
+
throws(domain, ids) {
|
|
1869
|
+
this._errorDeclarations.push({
|
|
1870
|
+
_domain: domain,
|
|
1871
|
+
_ids: ids
|
|
1872
|
+
});
|
|
1873
|
+
return this;
|
|
1874
|
+
}
|
|
1875
|
+
/**
|
|
1876
|
+
* Serialize raw input to a JSON-serializable form.
|
|
1877
|
+
* Uses the schema's serialization.serialize if defined; otherwise the input
|
|
1878
|
+
* is already JSON-native and is returned as-is.
|
|
1879
|
+
*/
|
|
1880
|
+
serializeInput(rawInput) {
|
|
1881
|
+
if (this.inputOptions?.serialization) return this.inputOptions.serialization.serialize(rawInput);
|
|
1882
|
+
return rawInput;
|
|
1883
|
+
}
|
|
1884
|
+
/**
|
|
1885
|
+
* Deserialize a JSON value back into the raw input type.
|
|
1886
|
+
* Uses serialization.deserialize if defined; otherwise the value is cast
|
|
1887
|
+
* directly (it's already in the correct shape).
|
|
1888
|
+
*/
|
|
1889
|
+
deserializeInput(serialized) {
|
|
1890
|
+
if (this.inputOptions?.serialization) return this.inputOptions.serialization.deserialize(serialized);
|
|
1891
|
+
return serialized;
|
|
1892
|
+
}
|
|
1893
|
+
/**
|
|
1894
|
+
* Validate raw input against the schema defined via `.input({ schema })`.
|
|
1895
|
+
* Throws `action_input_validation_failed` if validation fails.
|
|
1896
|
+
* Returns the validated (and possibly coerced) value on success.
|
|
1897
|
+
* If no input schema was declared, the value is passed through as-is.
|
|
1898
|
+
*/
|
|
1899
|
+
validateInput(value, meta) {
|
|
1900
|
+
if (this.inputOptions?.schema == null) return value;
|
|
1901
|
+
const result = this.inputOptions.schema["~standard"].validate(value);
|
|
1902
|
+
if (result instanceof Promise) throw err_nice_action.fromId("action_input_validation_promise", {
|
|
1903
|
+
domain: meta.domain,
|
|
1904
|
+
actionId: meta.actionId
|
|
1905
|
+
});
|
|
1906
|
+
if (result.issues != null) throw err_nice_action.fromId("action_input_validation_failed", {
|
|
1907
|
+
domain: meta.domain,
|
|
1908
|
+
actionId: meta.actionId,
|
|
1909
|
+
validationMessage: (0, _nice_code_common_errors.extractMessageFromStandardSchema)(result)
|
|
1910
|
+
});
|
|
1911
|
+
return result.value;
|
|
1912
|
+
}
|
|
1913
|
+
validateOutput(value, meta) {
|
|
1914
|
+
if (this.outputOptions?.schema == null) return value;
|
|
1915
|
+
const result = this.outputOptions.schema["~standard"].validate(value);
|
|
1916
|
+
if (result instanceof Promise) throw err_nice_action.fromId("action_output_validation_promise", {
|
|
1917
|
+
domain: meta.domain,
|
|
1918
|
+
actionId: meta.actionId
|
|
1919
|
+
});
|
|
1920
|
+
if (result.issues != null) throw err_nice_action.fromId("action_output_validation_failed", {
|
|
1921
|
+
domain: meta.domain,
|
|
1922
|
+
actionId: meta.actionId,
|
|
1923
|
+
validationMessage: (0, _nice_code_common_errors.extractMessageFromStandardSchema)(result)
|
|
1924
|
+
});
|
|
1925
|
+
return result.value;
|
|
1926
|
+
}
|
|
1927
|
+
/**
|
|
1928
|
+
* Serialize raw output to a JSON-serializable form.
|
|
1929
|
+
*/
|
|
1930
|
+
serializeOutput(rawOutput) {
|
|
1931
|
+
if (this.outputOptions?.serialization) return this.outputOptions.serialization.serialize(rawOutput);
|
|
1932
|
+
return rawOutput;
|
|
1933
|
+
}
|
|
1934
|
+
/**
|
|
1935
|
+
* Deserialize a JSON value back into the raw output type.
|
|
1936
|
+
*/
|
|
1937
|
+
deserializeOutput(serialized) {
|
|
1938
|
+
if (this.outputOptions?.serialization) return this.outputOptions.serialization.deserialize(serialized);
|
|
1939
|
+
return serialized;
|
|
1940
|
+
}
|
|
1941
|
+
};
|
|
1942
|
+
const actionSchema = () => {
|
|
1943
|
+
return new ActionSchema();
|
|
1944
|
+
};
|
|
1945
|
+
//#endregion
|
|
1946
|
+
//#region src/ActionRuntime/Handler/ExternalClient/Transport/Transport.ts
|
|
1947
|
+
/**
|
|
1948
|
+
* Reusable transport definition. Devs construct these (`HttpTransport.create({ createRequest })`,
|
|
1949
|
+
* `WebSocketTransport.create({ createWebSocket })`, …) and pass them to an
|
|
1950
|
+
* `ActionExternalClientHandler`. A single
|
|
1951
|
+
* definition can be shared across multiple handlers — each handler builds its own live
|
|
1952
|
+
* {@link TransportConnection} via {@link TransportConnection._createConnection}.
|
|
1953
|
+
*/
|
|
1954
|
+
var Transport = class {};
|
|
1955
|
+
//#endregion
|
|
1956
|
+
//#region src/ActionRuntime/Handler/ExternalClient/Transport/helpers/addTransportStatusMetadata.ts
|
|
1957
|
+
function addTransportStatusMetadata(transportStatus) {
|
|
1958
|
+
if (transportStatus.status === "ready") return {
|
|
1959
|
+
status: "ready",
|
|
1960
|
+
readyData: transportStatus.readyData
|
|
1961
|
+
};
|
|
1962
|
+
if (transportStatus.status === "initializing") return {
|
|
1963
|
+
status: "initializing",
|
|
1964
|
+
initializationPromise: transportStatus.initializationPromise,
|
|
1965
|
+
timeStarted: Date.now()
|
|
1966
|
+
};
|
|
1967
|
+
if (transportStatus.status === "failed") return {
|
|
1968
|
+
status: "failed",
|
|
1969
|
+
error: transportStatus.error,
|
|
1970
|
+
timeFailed: Date.now()
|
|
1971
|
+
};
|
|
1972
|
+
if (transportStatus.status === "unsupported") return { status: "unsupported" };
|
|
1973
|
+
return { status: "uninitialized" };
|
|
1974
|
+
}
|
|
1975
|
+
//#endregion
|
|
1976
|
+
//#region src/ActionRuntime/Handler/ExternalClient/Transport/TransportConnection.ts
|
|
1977
|
+
let transportOrd = 0;
|
|
1978
|
+
/**
|
|
1979
|
+
* Live, per-handler transport runtime built from a reusable {@link Transport} definition. Holds the
|
|
1980
|
+
* connection-scoped state (ordinal, initialized config, sockets / abort sets) that must not be shared
|
|
1981
|
+
* across handlers. Construct these via `definition._createConnection(...)`, never directly.
|
|
1982
|
+
*/
|
|
1983
|
+
var TransportConnection = class {
|
|
1984
|
+
def;
|
|
1985
|
+
transOrd = transportOrd++;
|
|
1986
|
+
type;
|
|
1987
|
+
initialized;
|
|
1988
|
+
/** Backref to the public definition that created this connection (used for devtools route info). */
|
|
1989
|
+
definition;
|
|
1990
|
+
constructor(def) {
|
|
1991
|
+
this.def = def;
|
|
1992
|
+
this.type = def.type;
|
|
1993
|
+
this.initialized = def.initialize();
|
|
1994
|
+
}
|
|
1995
|
+
/**
|
|
1996
|
+
* Devtools route info for an action routed through this live connection. Defaults to the stateless
|
|
1997
|
+
* {@link definition}'s info; connections override to enrich it from live state (e.g. the actual
|
|
1998
|
+
* resolved socket URL) when the definition couldn't resolve it on its own.
|
|
1999
|
+
*/
|
|
2000
|
+
getRouteInfo(input) {
|
|
2001
|
+
return this.definition?.getRouteInfo(input);
|
|
2002
|
+
}
|
|
2003
|
+
_getCacheKey(input) {
|
|
2004
|
+
const parts = this.initialized.getTransportCacheKey?.(input);
|
|
2005
|
+
if (parts == null) return null;
|
|
2006
|
+
return parts.join("\0");
|
|
2007
|
+
}
|
|
2008
|
+
getCacheKey(input) {
|
|
2009
|
+
const inner = this._getCacheKey(input);
|
|
2010
|
+
if (inner == null) return null;
|
|
2011
|
+
return `${this.transOrd}:${inner}`;
|
|
2012
|
+
}
|
|
2013
|
+
getTransport(input) {
|
|
2014
|
+
return this._processTransportStatus(input);
|
|
2015
|
+
}
|
|
2016
|
+
_processTransportStatus(input) {
|
|
2017
|
+
const transportStatusInfo = addTransportStatusMetadata(this.initialized.getTransport(input));
|
|
2018
|
+
if (transportStatusInfo.status !== "initializing" && transportStatusInfo.status !== "ready") return transportStatusInfo;
|
|
2019
|
+
if (transportStatusInfo.status === "initializing") {
|
|
2020
|
+
const promiseForReadyData = transportStatusInfo.initializationPromise.then((result) => {
|
|
2021
|
+
if (result.status === "ready") return {
|
|
2022
|
+
status: "ready",
|
|
2023
|
+
readyData: this._finalizeTransportMethods(result.readyData)
|
|
2024
|
+
};
|
|
2025
|
+
return result;
|
|
2026
|
+
});
|
|
2027
|
+
return {
|
|
2028
|
+
status: "initializing",
|
|
2029
|
+
timeStarted: transportStatusInfo.timeStarted,
|
|
2030
|
+
initializationPromise: promiseForReadyData
|
|
2031
|
+
};
|
|
2032
|
+
}
|
|
2033
|
+
return {
|
|
2034
|
+
status: "ready",
|
|
2035
|
+
readyData: this._finalizeTransportMethods(transportStatusInfo.readyData)
|
|
2036
|
+
};
|
|
2037
|
+
}
|
|
2038
|
+
};
|
|
2039
|
+
//#endregion
|
|
2040
|
+
//#region src/ActionRuntime/Handler/ExternalClient/Transport/Custom/CustomConnection.ts
|
|
2041
|
+
var CustomConnection = class extends TransportConnection {
|
|
2042
|
+
constructor(def) {
|
|
2043
|
+
super({
|
|
2044
|
+
...def,
|
|
2045
|
+
type: "custom"
|
|
2046
|
+
});
|
|
2047
|
+
}
|
|
2048
|
+
_finalizeTransportMethods(inputs) {
|
|
2049
|
+
return {
|
|
2050
|
+
sendActionData: inputs.sendActionData,
|
|
2051
|
+
sendReturnData: inputs.sendReturnData,
|
|
2052
|
+
updateRunConfig: inputs.updateRunConfig
|
|
2053
|
+
};
|
|
2054
|
+
}
|
|
2055
|
+
};
|
|
2056
|
+
//#endregion
|
|
2057
|
+
//#region src/ActionRuntime/Handler/ExternalClient/Transport/Custom/CustomTransport.ts
|
|
2058
|
+
/**
|
|
2059
|
+
* Reusable custom transport definition for channels nice-action doesn't model natively. Create one
|
|
2060
|
+
* with `CustomTransport.create({ sendActionData })` for the simple case, or
|
|
2061
|
+
* `CustomTransport.createAdvanced({ getTransport })` for full control over the lifecycle.
|
|
2062
|
+
*/
|
|
2063
|
+
var CustomTransport = class CustomTransport extends Transport {
|
|
2064
|
+
options;
|
|
2065
|
+
type = "custom";
|
|
2066
|
+
constructor(options) {
|
|
2067
|
+
super();
|
|
2068
|
+
this.options = options;
|
|
2069
|
+
}
|
|
2070
|
+
static create(options) {
|
|
2071
|
+
return new CustomTransport({
|
|
2072
|
+
...options,
|
|
2073
|
+
mode: "send"
|
|
2074
|
+
});
|
|
2075
|
+
}
|
|
2076
|
+
static createAdvanced(options) {
|
|
2077
|
+
return new CustomTransport({
|
|
2078
|
+
...options,
|
|
2079
|
+
mode: "advanced"
|
|
2080
|
+
});
|
|
2081
|
+
}
|
|
2082
|
+
_createConnection(_ctx) {
|
|
2083
|
+
const options = this.options;
|
|
2084
|
+
let getTransport;
|
|
2085
|
+
if (options.mode === "advanced") getTransport = options.getTransport;
|
|
2086
|
+
else getTransport = () => ({
|
|
2087
|
+
status: "ready",
|
|
2088
|
+
readyData: {
|
|
2089
|
+
sendActionData: options.sendActionData,
|
|
2090
|
+
sendReturnData: options.sendReturnData,
|
|
2091
|
+
updateRunConfig: options.updateRunConfig,
|
|
2092
|
+
closeTransport: options.closeTransport ?? (() => {})
|
|
2093
|
+
}
|
|
2094
|
+
});
|
|
2095
|
+
return new CustomConnection({ initialize: () => ({
|
|
2096
|
+
getTransportCacheKey: options.getTransportCacheKey,
|
|
2097
|
+
getTransport
|
|
2098
|
+
}) });
|
|
2099
|
+
}
|
|
2100
|
+
getRouteInfo(input) {
|
|
2101
|
+
if (this.options.getRouteInfo != null) return this.options.getRouteInfo(input);
|
|
2102
|
+
return {
|
|
2103
|
+
type: "custom",
|
|
2104
|
+
summary: this.options.label ?? "custom"
|
|
2105
|
+
};
|
|
2106
|
+
}
|
|
2107
|
+
};
|
|
2108
|
+
//#endregion
|
|
2109
|
+
//#region src/ActionRuntime/Handler/ExternalClient/Transport/err_nice_transport_ws.ts
|
|
2110
|
+
let EErrId_NiceTransport_WebSocket = /* @__PURE__ */ function(EErrId_NiceTransport_WebSocket) {
|
|
2111
|
+
EErrId_NiceTransport_WebSocket["ws_disconnected"] = "ws_disconnected";
|
|
2112
|
+
EErrId_NiceTransport_WebSocket["ws_create_failed"] = "ws_create_failed";
|
|
2113
|
+
EErrId_NiceTransport_WebSocket["ws_error"] = "ws_error";
|
|
2114
|
+
return EErrId_NiceTransport_WebSocket;
|
|
2115
|
+
}({});
|
|
2116
|
+
const err_nice_transport_ws = err_nice_transport.createChildDomain({
|
|
2117
|
+
domain: "ws_transport",
|
|
2118
|
+
schema: {
|
|
2119
|
+
["ws_disconnected"]: (0, _nice_code_error.err)({ message: () => `WebSocket transport disconnected.` }),
|
|
2120
|
+
["ws_create_failed"]: (0, _nice_code_error.err)({ message: ({ originalError }) => `Failed to create WebSocket transport.${originalError ? ` Original error: ${originalError.message}` : ""}` }),
|
|
2121
|
+
["ws_error"]: (0, _nice_code_error.err)({ message: ({ originalError }) => `WebSocket transport error.${originalError ? ` Original error: ${originalError.message}` : ""}` })
|
|
2122
|
+
}
|
|
2123
|
+
});
|
|
2124
|
+
//#endregion
|
|
2125
|
+
//#region src/ActionRuntime/Handler/ExternalClient/Transport/Http/HttpConnection.ts
|
|
2126
|
+
var HttpConnection = class extends TransportConnection {
|
|
2127
|
+
constructor(def) {
|
|
2128
|
+
super({
|
|
2129
|
+
...def,
|
|
2130
|
+
type: "http"
|
|
2131
|
+
});
|
|
2132
|
+
}
|
|
2133
|
+
_finalizeTransportMethods(methods) {
|
|
2134
|
+
return {
|
|
2135
|
+
sendActionData: (input) => {
|
|
2136
|
+
const request = methods.createRequest(input);
|
|
2137
|
+
this.send({
|
|
2138
|
+
...input,
|
|
2139
|
+
params: { request },
|
|
2140
|
+
runningAction: input.runningAction
|
|
2141
|
+
}).catch((err) => input.runningAction._abort(err));
|
|
2142
|
+
},
|
|
2143
|
+
updateRunConfig: methods.updateRunConfig
|
|
2144
|
+
};
|
|
2145
|
+
}
|
|
2146
|
+
async send(input) {
|
|
2147
|
+
const { action, runningAction, timeout, params: { request } } = input;
|
|
2148
|
+
const wire = action.toJsonObject();
|
|
2149
|
+
const ac = new AbortController();
|
|
2150
|
+
let timedOut = false;
|
|
2151
|
+
const timeoutId = setTimeout(() => {
|
|
2152
|
+
timedOut = true;
|
|
2153
|
+
ac.abort();
|
|
2154
|
+
}, timeout);
|
|
2155
|
+
const unsubscribe = input.runningAction.addUpdateListeners([(update) => {
|
|
2156
|
+
if (update.type === "finished") {
|
|
2157
|
+
clearTimeout(timeoutId);
|
|
2158
|
+
ac.abort();
|
|
2159
|
+
}
|
|
2160
|
+
}]);
|
|
2161
|
+
try {
|
|
2162
|
+
const res = await fetch(request.url, {
|
|
2163
|
+
method: "POST",
|
|
2164
|
+
headers: {
|
|
2165
|
+
"Content-Type": "application/json",
|
|
2166
|
+
...request.headers
|
|
2167
|
+
},
|
|
2168
|
+
body: request.body ?? JSON.stringify(wire),
|
|
2169
|
+
signal: ac.signal
|
|
2170
|
+
});
|
|
2171
|
+
if (!res.ok) {
|
|
2172
|
+
if (action.type === "request") try {
|
|
2173
|
+
const jsonData = await res.json();
|
|
2174
|
+
if (isActionPayload_Result_JsonObject(jsonData)) runningAction._completeWithResult(action._domain.hydrateResultPayload(jsonData));
|
|
2175
|
+
else if ((0, _nice_code_error.isNiceErrorObject)(jsonData)) runningAction._completeWithResult(action.errorResult((0, _nice_code_error.castNiceError)(jsonData)));
|
|
2176
|
+
else runningAction._completeWithResult(action.errorResult(err_nice_transport.fromId("invalid_action_response", { actionId: action.id })));
|
|
2177
|
+
} catch (e) {
|
|
2178
|
+
throw err_nice_transport.fromId("send_failed", {
|
|
2179
|
+
actionState: action.type,
|
|
2180
|
+
actionId: action.id,
|
|
2181
|
+
httpStatusCode: res.status,
|
|
2182
|
+
message: e.message
|
|
2183
|
+
}).withOriginError(e);
|
|
2184
|
+
}
|
|
2185
|
+
else {
|
|
2186
|
+
let text;
|
|
2187
|
+
try {
|
|
2188
|
+
text = await res.text();
|
|
2189
|
+
} catch (e) {
|
|
2190
|
+
console.warn(`Failed to read error response body for failed HTTP request in HttpConnection:`, e);
|
|
2191
|
+
}
|
|
2192
|
+
throw err_nice_transport.fromId("send_failed", {
|
|
2193
|
+
actionState: action.type,
|
|
2194
|
+
actionId: action.id,
|
|
2195
|
+
httpStatusCode: res.status,
|
|
2196
|
+
message: text ?? `HTTP error with status ${res.status}`
|
|
2197
|
+
});
|
|
2198
|
+
}
|
|
2199
|
+
return;
|
|
2200
|
+
}
|
|
2201
|
+
if (action.type === "request") {
|
|
2202
|
+
const json = await res.json();
|
|
2203
|
+
if (!isActionPayload_Result_JsonObject(json)) throw err_nice_transport.fromId("invalid_action_response", { actionId: action.id });
|
|
2204
|
+
runningAction._completeWithResult(action._domain.hydrateResultPayload(json));
|
|
2205
|
+
}
|
|
2206
|
+
} catch (err) {
|
|
2207
|
+
if (timedOut) throw err_nice_transport.fromId("timeout", { timeout });
|
|
2208
|
+
if (err instanceof _nice_code_error.NiceError) throw err;
|
|
2209
|
+
throw err_nice_transport.fromId("send_failed", {
|
|
2210
|
+
actionState: action.type,
|
|
2211
|
+
actionId: action.id,
|
|
2212
|
+
message: err instanceof Error ? err.message : String(err)
|
|
2213
|
+
}).withOriginError(err instanceof Error ? err : void 0);
|
|
2214
|
+
} finally {
|
|
2215
|
+
clearTimeout(timeoutId);
|
|
2216
|
+
unsubscribe();
|
|
2217
|
+
}
|
|
2218
|
+
}
|
|
2219
|
+
};
|
|
2220
|
+
//#endregion
|
|
2221
|
+
//#region src/ActionRuntime/Handler/ExternalClient/Transport/Http/HttpTransport.ts
|
|
2222
|
+
function shortPath(url) {
|
|
2223
|
+
try {
|
|
2224
|
+
return new URL(url).pathname || url;
|
|
2225
|
+
} catch {
|
|
2226
|
+
return url;
|
|
2227
|
+
}
|
|
2228
|
+
}
|
|
2229
|
+
/**
|
|
2230
|
+
* Reusable HTTP transport definition. Create one with `HttpTransport.create({ createRequest })` — the
|
|
2231
|
+
* single `createRequest` function lets you keep it simple (`() => ({ url })`) or derive the request
|
|
2232
|
+
* per action.
|
|
2233
|
+
*/
|
|
2234
|
+
var HttpTransport = class HttpTransport extends Transport {
|
|
2235
|
+
options;
|
|
2236
|
+
type = "http";
|
|
2237
|
+
constructor(options) {
|
|
2238
|
+
super();
|
|
2239
|
+
this.options = options;
|
|
2240
|
+
}
|
|
2241
|
+
static create(options) {
|
|
2242
|
+
return new HttpTransport(options);
|
|
2243
|
+
}
|
|
2244
|
+
_createConnection(_ctx) {
|
|
2245
|
+
return new HttpConnection({ initialize: () => ({
|
|
2246
|
+
getTransportCacheKey: this.options.getTransportCacheKey,
|
|
2247
|
+
getTransport: () => ({
|
|
2248
|
+
status: "ready",
|
|
2249
|
+
readyData: {
|
|
2250
|
+
createRequest: this.options.createRequest,
|
|
2251
|
+
updateRunConfig: this.options.updateRunConfig
|
|
2252
|
+
}
|
|
2253
|
+
})
|
|
2254
|
+
}) });
|
|
2255
|
+
}
|
|
2256
|
+
getRouteInfo(input) {
|
|
2257
|
+
const { url } = this.options.createRequest(input);
|
|
2258
|
+
return {
|
|
2259
|
+
type: "http",
|
|
2260
|
+
method: "POST",
|
|
2261
|
+
url,
|
|
2262
|
+
summary: `POST ${shortPath(url)}`
|
|
2263
|
+
};
|
|
2264
|
+
}
|
|
2265
|
+
};
|
|
2266
|
+
//#endregion
|
|
2267
|
+
//#region src/ActionRuntime/Handler/ExternalClient/Transport/WebSocket/actionFrameCrypto.ts
|
|
2268
|
+
const ENCRYPTED_ENVELOPE_LENGTH = 2;
|
|
2269
|
+
/**
|
|
2270
|
+
* Build the encrypt/decrypt transform for a connection whose handshake settled on the `encrypted`
|
|
2271
|
+
* level. Keyed by the link + `linkedClientId`, so it reuses the cached shared AES-GCM key.
|
|
2272
|
+
*/
|
|
2273
|
+
function createActionFrameCrypto({ link, linkedClientId }) {
|
|
2274
|
+
return {
|
|
2275
|
+
async encryptFrame(frame) {
|
|
2276
|
+
const { nonce, ciphertext } = await link.encryptBytesForLinkedClient({
|
|
2277
|
+
linkedClientId,
|
|
2278
|
+
dataToEncrypt: frame
|
|
2279
|
+
});
|
|
2280
|
+
return (0, msgpackr.pack)([nonce, ciphertext]);
|
|
2281
|
+
},
|
|
2282
|
+
async decryptFrame(frame) {
|
|
2283
|
+
if (typeof frame === "string") throw new Error("[ws-crypto] expected an encrypted binary frame, received text");
|
|
2284
|
+
const envelope = (0, msgpackr.unpack)(frame instanceof ArrayBuffer ? new Uint8Array(frame) : frame);
|
|
2285
|
+
if (!Array.isArray(envelope) || envelope.length !== ENCRYPTED_ENVELOPE_LENGTH) throw new Error("[ws-crypto] malformed encrypted frame envelope");
|
|
2286
|
+
const [nonce, ciphertext] = envelope;
|
|
2287
|
+
if (!(nonce instanceof Uint8Array) || !(ciphertext instanceof Uint8Array)) throw new Error("[ws-crypto] malformed encrypted frame fields");
|
|
2288
|
+
return await link.decryptBytesFromLinkedClient({
|
|
2289
|
+
linkedClientId,
|
|
2290
|
+
dataToDecrypt: {
|
|
2291
|
+
nonce,
|
|
2292
|
+
ciphertext
|
|
2293
|
+
}
|
|
2294
|
+
});
|
|
2295
|
+
}
|
|
2296
|
+
};
|
|
2297
|
+
}
|
|
2298
|
+
//#endregion
|
|
2299
|
+
//#region src/ActionRuntime/Handler/ExternalClient/Transport/WebSocket/actionWsHandshake.ts
|
|
2300
|
+
/**
|
|
2301
|
+
* Authenticated handshake for the WebSocket channel. Run once per connection, before any action
|
|
2302
|
+
* frames flow, it:
|
|
2303
|
+
* - exchanges each side's {@link RuntimeCoordinate} + public keys,
|
|
2304
|
+
* - checks both ends share the same wire dictionary version (closes the positional-dictionary footgun),
|
|
2305
|
+
* - has the client prove control of its verify (Ed25519) key by signing a fresh challenge that binds
|
|
2306
|
+
* both nonces, both identities, the dictionary version, the level, and every exchanged public key,
|
|
2307
|
+
* - establishes a `ClientCryptoKeyLink` link on both sides (so the server can verify the signature and,
|
|
2308
|
+
* for the `encrypted` level, both can derive a shared AES-GCM key with the verify keys folded in).
|
|
2309
|
+
*
|
|
2310
|
+
* This module is transport-agnostic: it produces/consumes message objects. The transport + server
|
|
2311
|
+
* handler drive the I/O and the connection phase (Step 4). Keep `none`-level connections from ever
|
|
2312
|
+
* reaching here — they skip the handshake entirely.
|
|
2313
|
+
*/
|
|
2314
|
+
const HANDSHAKE_PROTOCOL = "nice-ws-hs/1";
|
|
2315
|
+
/** How much the channel protects after the handshake — chosen by the consumer (perf vs security). */
|
|
2316
|
+
let ESecurityLevel = /* @__PURE__ */ function(ESecurityLevel) {
|
|
2317
|
+
/** No handshake; identity is self-asserted (fastest, dev / trusted networks). */
|
|
2318
|
+
ESecurityLevel["none"] = "none";
|
|
2319
|
+
/** Handshake authenticates identity (sign/verify + key pin); frames stay plaintext over TLS. */
|
|
2320
|
+
ESecurityLevel["authenticated"] = "authenticated";
|
|
2321
|
+
/** Authenticated handshake + every frame AES-GCM encrypted with the derived shared key. */
|
|
2322
|
+
ESecurityLevel["encrypted"] = "encrypted";
|
|
2323
|
+
return ESecurityLevel;
|
|
2324
|
+
}({});
|
|
2325
|
+
let EHandshakeMessageType = /* @__PURE__ */ function(EHandshakeMessageType) {
|
|
2326
|
+
EHandshakeMessageType["hello"] = "hello";
|
|
2327
|
+
EHandshakeMessageType["welcome"] = "welcome";
|
|
2328
|
+
EHandshakeMessageType["prove"] = "prove";
|
|
2329
|
+
EHandshakeMessageType["accept"] = "accept";
|
|
2330
|
+
EHandshakeMessageType["reject"] = "reject";
|
|
2331
|
+
return EHandshakeMessageType;
|
|
2332
|
+
}({});
|
|
2333
|
+
const vEd25519Raw = valibot.custom((val) => typeof val === "string" && val.startsWith("ed25519::raw_base64::"));
|
|
2334
|
+
const vX25519Raw = valibot.custom((val) => typeof val === "string" && val.startsWith("x25519::raw_base64::"));
|
|
2335
|
+
const vCoordinate = valibot.object({
|
|
2336
|
+
envId: valibot.string(),
|
|
2337
|
+
perId: valibot.optional(valibot.string()),
|
|
2338
|
+
insId: valibot.optional(valibot.string())
|
|
2339
|
+
});
|
|
2340
|
+
const vSecurityLevel = valibot.picklist([
|
|
2341
|
+
"none",
|
|
2342
|
+
"authenticated",
|
|
2343
|
+
"encrypted"
|
|
2344
|
+
]);
|
|
2345
|
+
const vHsHello = valibot.object({
|
|
2346
|
+
t: valibot.literal("hello"),
|
|
2347
|
+
protocol: valibot.string(),
|
|
2348
|
+
securityLevel: vSecurityLevel,
|
|
2349
|
+
dictionaryVersion: valibot.string(),
|
|
2350
|
+
client: vCoordinate,
|
|
2351
|
+
clientNonce: valibot.string(),
|
|
2352
|
+
verifyPublicKey: vEd25519Raw,
|
|
2353
|
+
exchangePublicKey: valibot.optional(vX25519Raw)
|
|
2354
|
+
});
|
|
2355
|
+
const vHsWelcome = valibot.object({
|
|
2356
|
+
t: valibot.literal("welcome"),
|
|
2357
|
+
securityLevel: vSecurityLevel,
|
|
2358
|
+
dictionaryVersion: valibot.string(),
|
|
2359
|
+
server: vCoordinate,
|
|
2360
|
+
serverNonce: valibot.string(),
|
|
2361
|
+
verifyPublicKey: vEd25519Raw,
|
|
2362
|
+
exchangePublicKey: valibot.optional(vX25519Raw)
|
|
2363
|
+
});
|
|
2364
|
+
const vHsProve = valibot.object({
|
|
2365
|
+
t: valibot.literal("prove"),
|
|
2366
|
+
signatureBase64: valibot.string()
|
|
2367
|
+
});
|
|
2368
|
+
const vHsAccept = valibot.object({
|
|
2369
|
+
t: valibot.literal("accept"),
|
|
2370
|
+
signatureBase64: valibot.optional(valibot.string())
|
|
2371
|
+
});
|
|
2372
|
+
const vHsReject = valibot.object({
|
|
2373
|
+
t: valibot.literal("reject"),
|
|
2374
|
+
reason: valibot.string()
|
|
2375
|
+
});
|
|
2376
|
+
const vHandshakeMessage = valibot.variant("t", [
|
|
2377
|
+
vHsHello,
|
|
2378
|
+
vHsWelcome,
|
|
2379
|
+
vHsProve,
|
|
2380
|
+
vHsAccept,
|
|
2381
|
+
vHsReject
|
|
2382
|
+
]);
|
|
2383
|
+
/** Serialize a handshake message for the wire (handshake frames are JSON — they aren't the hot path). */
|
|
2384
|
+
function encodeHandshakeMessage(message) {
|
|
2385
|
+
return JSON.stringify(message);
|
|
2386
|
+
}
|
|
2387
|
+
/** Parse + structurally validate an incoming handshake frame; `undefined` if it isn't one. */
|
|
2388
|
+
function decodeHandshakeMessage(raw) {
|
|
2389
|
+
let parsed;
|
|
2390
|
+
try {
|
|
2391
|
+
parsed = JSON.parse(raw);
|
|
2392
|
+
} catch {
|
|
2393
|
+
return;
|
|
2394
|
+
}
|
|
2395
|
+
const result = valibot.safeParse(vHandshakeMessage, parsed);
|
|
2396
|
+
return result.success ? result.output : void 0;
|
|
2397
|
+
}
|
|
2398
|
+
/** Stable link id for a runtime coordinate — the key both the crypto link and the connection use. */
|
|
2399
|
+
function runtimeLinkId(coordinate) {
|
|
2400
|
+
return `runtime::${new RuntimeCoordinate(coordinate).stringId}`;
|
|
2401
|
+
}
|
|
2402
|
+
function coordId(coordinate) {
|
|
2403
|
+
return new RuntimeCoordinate(coordinate).stringId;
|
|
2404
|
+
}
|
|
2405
|
+
function sessionSalt(clientNonce, serverNonce) {
|
|
2406
|
+
return `${clientNonce}::${serverNonce}`;
|
|
2407
|
+
}
|
|
2408
|
+
function handshakeInfo(dictionaryVersion) {
|
|
2409
|
+
return `${HANDSHAKE_PROTOCOL}::${dictionaryVersion}`;
|
|
2410
|
+
}
|
|
2411
|
+
/**
|
|
2412
|
+
* The exact string both sides sign/verify. JSON-encoded ordered array so field boundaries are
|
|
2413
|
+
* unambiguous; binds identities, nonces (freshness), version, level, and all exchanged public keys
|
|
2414
|
+
* (authenticating the keys via the signature, complementing `bindVerifyKeysIntoDerivation`).
|
|
2415
|
+
*/
|
|
2416
|
+
function buildHandshakeChallenge(parts) {
|
|
2417
|
+
return JSON.stringify([
|
|
2418
|
+
HANDSHAKE_PROTOCOL,
|
|
2419
|
+
parts.securityLevel,
|
|
2420
|
+
parts.dictionaryVersion,
|
|
2421
|
+
parts.clientCoordId,
|
|
2422
|
+
parts.serverCoordId,
|
|
2423
|
+
parts.clientNonce,
|
|
2424
|
+
parts.serverNonce,
|
|
2425
|
+
parts.clientVerifyKey,
|
|
2426
|
+
parts.serverVerifyKey,
|
|
2427
|
+
parts.clientExchangeKey ?? "_",
|
|
2428
|
+
parts.serverExchangeKey ?? "_"
|
|
2429
|
+
]);
|
|
2430
|
+
}
|
|
2431
|
+
function reject(reason) {
|
|
2432
|
+
return {
|
|
2433
|
+
t: "reject",
|
|
2434
|
+
reason
|
|
2435
|
+
};
|
|
2436
|
+
}
|
|
2437
|
+
function tofuPinKey(client) {
|
|
2438
|
+
return `${client.envId}::${client.perId ?? client.insId ?? "_"}`;
|
|
2439
|
+
}
|
|
2440
|
+
/**
|
|
2441
|
+
* In-memory trust-on-first-use resolver: trusts (and pins) the first verify key seen for a client
|
|
2442
|
+
* identity, then rejects a different key for that identity. The default; replace with a storage-backed
|
|
2443
|
+
* resolver for cross-restart pinning (see Step 5).
|
|
2444
|
+
*/
|
|
2445
|
+
function createInMemoryTofuVerifyKeyResolver() {
|
|
2446
|
+
const pinned = /* @__PURE__ */ new Map();
|
|
2447
|
+
return { async resolve({ client, verifyPublicKey }) {
|
|
2448
|
+
const key = tofuPinKey(client);
|
|
2449
|
+
const existing = pinned.get(key);
|
|
2450
|
+
if (existing == null) {
|
|
2451
|
+
pinned.set(key, verifyPublicKey);
|
|
2452
|
+
return { trusted: true };
|
|
2453
|
+
}
|
|
2454
|
+
if (existing === verifyPublicKey) return { trusted: true };
|
|
2455
|
+
return {
|
|
2456
|
+
trusted: false,
|
|
2457
|
+
reason: "verify key changed for client identity (pin mismatch)"
|
|
2458
|
+
};
|
|
2459
|
+
} };
|
|
2460
|
+
}
|
|
2461
|
+
/**
|
|
2462
|
+
* Storage-backed trust-on-first-use resolver: pins survive process restarts / Durable Object eviction
|
|
2463
|
+
* (e.g. back it with `createDurableObjectStorageAdapter`). Same policy as the in-memory variant — trust
|
|
2464
|
+
* + pin the first verify key per client identity, reject a different one thereafter.
|
|
2465
|
+
*/
|
|
2466
|
+
function createStorageTofuVerifyKeyResolver(storageAdapter) {
|
|
2467
|
+
const storage = (0, _nice_code_util.createTypedStorage)({ storageAdapter });
|
|
2468
|
+
return { async resolve({ client, verifyPublicKey }) {
|
|
2469
|
+
const key = tofuPinKey(client);
|
|
2470
|
+
const existing = (await storage.getJson("pins"))?.[key];
|
|
2471
|
+
if (existing == null) {
|
|
2472
|
+
await storage.updateJsonWithDef("pins", {}, (current) => ({
|
|
2473
|
+
...current,
|
|
2474
|
+
[key]: verifyPublicKey
|
|
2475
|
+
}));
|
|
2476
|
+
return { trusted: true };
|
|
2477
|
+
}
|
|
2478
|
+
if (existing === verifyPublicKey) return { trusted: true };
|
|
2479
|
+
return {
|
|
2480
|
+
trusted: false,
|
|
2481
|
+
reason: "verify key changed for client identity (pin mismatch)"
|
|
2482
|
+
};
|
|
2483
|
+
} };
|
|
2484
|
+
}
|
|
2485
|
+
function createClientHandshake(config) {
|
|
2486
|
+
const { link, localCoordinate, dictionaryVersion, securityLevel } = config;
|
|
2487
|
+
const wantsEncryption = securityLevel === "encrypted";
|
|
2488
|
+
const clientNonce = (0, nanoid.nanoid)();
|
|
2489
|
+
let pending;
|
|
2490
|
+
return {
|
|
2491
|
+
async createHello() {
|
|
2492
|
+
return {
|
|
2493
|
+
t: "hello",
|
|
2494
|
+
protocol: HANDSHAKE_PROTOCOL,
|
|
2495
|
+
securityLevel,
|
|
2496
|
+
dictionaryVersion,
|
|
2497
|
+
client: localCoordinate,
|
|
2498
|
+
clientNonce,
|
|
2499
|
+
verifyPublicKey: await link.getLocalVerifyPublicKey(),
|
|
2500
|
+
exchangePublicKey: wantsEncryption ? await link.getLocalExchangePublicKey() : void 0
|
|
2501
|
+
};
|
|
2502
|
+
},
|
|
2503
|
+
async onWelcome(welcome) {
|
|
2504
|
+
if (welcome.dictionaryVersion !== dictionaryVersion) throw new Error("[ws-handshake] server dictionary version mismatch");
|
|
2505
|
+
if (welcome.securityLevel !== securityLevel) throw new Error("[ws-handshake] server security level mismatch");
|
|
2506
|
+
if (wantsEncryption && welcome.exchangePublicKey == null) throw new Error("[ws-handshake] server did not provide an exchange key for encryption");
|
|
2507
|
+
const linkedServerId = runtimeLinkId(welcome.server);
|
|
2508
|
+
await link.linkClient({
|
|
2509
|
+
linkedClientId: linkedServerId,
|
|
2510
|
+
verifyPublicKey: welcome.verifyPublicKey,
|
|
2511
|
+
...wantsEncryption ? {
|
|
2512
|
+
exchangePublicKey: welcome.exchangePublicKey,
|
|
2513
|
+
saltString: sessionSalt(clientNonce, welcome.serverNonce),
|
|
2514
|
+
infoString: handshakeInfo(dictionaryVersion),
|
|
2515
|
+
bindVerifyKeysIntoDerivation: true
|
|
2516
|
+
} : {}
|
|
2517
|
+
});
|
|
2518
|
+
const challenge = buildHandshakeChallenge({
|
|
2519
|
+
securityLevel,
|
|
2520
|
+
dictionaryVersion,
|
|
2521
|
+
clientCoordId: coordId(localCoordinate),
|
|
2522
|
+
serverCoordId: coordId(welcome.server),
|
|
2523
|
+
clientNonce,
|
|
2524
|
+
serverNonce: welcome.serverNonce,
|
|
2525
|
+
clientVerifyKey: await link.getLocalVerifyPublicKey(),
|
|
2526
|
+
serverVerifyKey: welcome.verifyPublicKey,
|
|
2527
|
+
clientExchangeKey: wantsEncryption ? await link.getLocalExchangePublicKey() : void 0,
|
|
2528
|
+
serverExchangeKey: welcome.exchangePublicKey
|
|
2529
|
+
});
|
|
2530
|
+
pending = {
|
|
2531
|
+
linkedServerId,
|
|
2532
|
+
server: welcome.server,
|
|
2533
|
+
challenge
|
|
2534
|
+
};
|
|
2535
|
+
return {
|
|
2536
|
+
t: "prove",
|
|
2537
|
+
signatureBase64: (await link.signChallenge([challenge])).signatureBase64
|
|
2538
|
+
};
|
|
2539
|
+
},
|
|
2540
|
+
async onAccept(accept) {
|
|
2541
|
+
if (pending == null) throw new Error("[ws-handshake] accept before welcome");
|
|
2542
|
+
if (accept.signatureBase64 != null) {
|
|
2543
|
+
if (!await link.verifyChallengeFromLinkedClient({
|
|
2544
|
+
linkedClientId: pending.linkedServerId,
|
|
2545
|
+
challenge: pending.challenge,
|
|
2546
|
+
signatureBase64: accept.signatureBase64
|
|
2547
|
+
})) throw new Error("[ws-handshake] server signature invalid");
|
|
2548
|
+
}
|
|
2549
|
+
return {
|
|
2550
|
+
linkedClientId: pending.linkedServerId,
|
|
2551
|
+
remote: pending.server,
|
|
2552
|
+
securityLevel
|
|
2553
|
+
};
|
|
2554
|
+
}
|
|
2555
|
+
};
|
|
2556
|
+
}
|
|
2557
|
+
function createServerHandshake(config) {
|
|
2558
|
+
const { link, localCoordinate, dictionaryVersion } = config;
|
|
2559
|
+
const allowedLevels = Array.isArray(config.securityLevel) ? config.securityLevel : [config.securityLevel];
|
|
2560
|
+
const verifyKeyResolver = config.verifyKeyResolver ?? createInMemoryTofuVerifyKeyResolver();
|
|
2561
|
+
const serverNonce = (0, nanoid.nanoid)();
|
|
2562
|
+
let pending;
|
|
2563
|
+
let result;
|
|
2564
|
+
return {
|
|
2565
|
+
async onHello(hello) {
|
|
2566
|
+
if (hello.protocol !== HANDSHAKE_PROTOCOL) return reject("unsupported handshake protocol");
|
|
2567
|
+
if (hello.dictionaryVersion !== dictionaryVersion) return reject("dictionary version mismatch");
|
|
2568
|
+
const negotiatedLevel = hello.securityLevel;
|
|
2569
|
+
if (negotiatedLevel === "none" || !allowedLevels.includes(negotiatedLevel)) return reject("security level not allowed");
|
|
2570
|
+
const wantsEncryption = negotiatedLevel === "encrypted";
|
|
2571
|
+
if (wantsEncryption && hello.exchangePublicKey == null) return reject("missing exchange key for encryption");
|
|
2572
|
+
const linkedClientId = runtimeLinkId(hello.client);
|
|
2573
|
+
await link.linkClient({
|
|
2574
|
+
linkedClientId,
|
|
2575
|
+
verifyPublicKey: hello.verifyPublicKey,
|
|
2576
|
+
...wantsEncryption ? {
|
|
2577
|
+
exchangePublicKey: hello.exchangePublicKey,
|
|
2578
|
+
saltString: sessionSalt(hello.clientNonce, serverNonce),
|
|
2579
|
+
infoString: handshakeInfo(dictionaryVersion),
|
|
2580
|
+
bindVerifyKeysIntoDerivation: true
|
|
2581
|
+
} : {}
|
|
2582
|
+
});
|
|
2583
|
+
const serverVerifyKey = await link.getLocalVerifyPublicKey();
|
|
2584
|
+
const serverExchangeKey = wantsEncryption ? await link.getLocalExchangePublicKey() : void 0;
|
|
2585
|
+
const keyMaterial = wantsEncryption && hello.exchangePublicKey != null ? {
|
|
2586
|
+
verifyPublicKey: hello.verifyPublicKey,
|
|
2587
|
+
exchangePublicKey: hello.exchangePublicKey,
|
|
2588
|
+
saltString: sessionSalt(hello.clientNonce, serverNonce),
|
|
2589
|
+
infoString: handshakeInfo(dictionaryVersion),
|
|
2590
|
+
bindVerifyKeysIntoDerivation: true
|
|
2591
|
+
} : void 0;
|
|
2592
|
+
pending = {
|
|
2593
|
+
client: hello.client,
|
|
2594
|
+
linkedClientId,
|
|
2595
|
+
clientVerifyKey: hello.verifyPublicKey,
|
|
2596
|
+
negotiatedLevel,
|
|
2597
|
+
keyMaterial,
|
|
2598
|
+
challenge: buildHandshakeChallenge({
|
|
2599
|
+
securityLevel: negotiatedLevel,
|
|
2600
|
+
dictionaryVersion,
|
|
2601
|
+
clientCoordId: coordId(hello.client),
|
|
2602
|
+
serverCoordId: coordId(localCoordinate),
|
|
2603
|
+
clientNonce: hello.clientNonce,
|
|
2604
|
+
serverNonce,
|
|
2605
|
+
clientVerifyKey: hello.verifyPublicKey,
|
|
2606
|
+
serverVerifyKey,
|
|
2607
|
+
clientExchangeKey: hello.exchangePublicKey,
|
|
2608
|
+
serverExchangeKey
|
|
2609
|
+
})
|
|
2610
|
+
};
|
|
2611
|
+
return {
|
|
2612
|
+
t: "welcome",
|
|
2613
|
+
securityLevel: negotiatedLevel,
|
|
2614
|
+
dictionaryVersion,
|
|
2615
|
+
server: localCoordinate,
|
|
2616
|
+
serverNonce,
|
|
2617
|
+
verifyPublicKey: serverVerifyKey,
|
|
2618
|
+
exchangePublicKey: serverExchangeKey
|
|
2619
|
+
};
|
|
2620
|
+
},
|
|
2621
|
+
async onProve(prove) {
|
|
2622
|
+
if (pending == null) return reject("prove before hello");
|
|
2623
|
+
if (!await link.verifyChallengeFromLinkedClient({
|
|
2624
|
+
linkedClientId: pending.linkedClientId,
|
|
2625
|
+
challenge: pending.challenge,
|
|
2626
|
+
signatureBase64: prove.signatureBase64
|
|
2627
|
+
})) return reject("invalid client signature");
|
|
2628
|
+
const trust = await verifyKeyResolver.resolve({
|
|
2629
|
+
client: pending.client,
|
|
2630
|
+
verifyPublicKey: pending.clientVerifyKey
|
|
2631
|
+
});
|
|
2632
|
+
if (!trust.trusted) return reject(trust.reason ?? "client verify key not trusted");
|
|
2633
|
+
result = {
|
|
2634
|
+
linkedClientId: pending.linkedClientId,
|
|
2635
|
+
remote: pending.client,
|
|
2636
|
+
securityLevel: pending.negotiatedLevel,
|
|
2637
|
+
encryptionKeyMaterial: pending.keyMaterial
|
|
2638
|
+
};
|
|
2639
|
+
return {
|
|
2640
|
+
t: "accept",
|
|
2641
|
+
signatureBase64: (await link.signChallenge([pending.challenge])).signatureBase64
|
|
2642
|
+
};
|
|
2643
|
+
},
|
|
2644
|
+
/** The completed handshake result once `onProve` has accepted, else `undefined`. */
|
|
2645
|
+
getResult() {
|
|
2646
|
+
return result;
|
|
2647
|
+
}
|
|
2648
|
+
};
|
|
2649
|
+
}
|
|
2650
|
+
//#endregion
|
|
2651
|
+
//#region src/ActionRuntime/Handler/ExternalClient/Transport/WebSocket/actionWireCodec.ts
|
|
2652
|
+
/**
|
|
2653
|
+
* Shared building blocks for the binary action codecs (the stateless {@link createBinaryWsAdapter} and
|
|
2654
|
+
* the per-connection `createBinaryWsSessionFactory`). Both map a `domain:id` route to a tiny integer
|
|
2655
|
+
* and reduce the verbose JSON wire to a positional tuple — they only differ in how much context they
|
|
2656
|
+
* carry per frame, so the dictionary + payload (de)assembly live here.
|
|
2657
|
+
*/
|
|
2658
|
+
/**
|
|
2659
|
+
* Tiny integer codes for the payload type, so the verbose `"request"`/`"result"`/`"progress"`
|
|
2660
|
+
* strings never hit the wire. The index in {@link ReversePayloadType} must line up with the value.
|
|
2661
|
+
*/
|
|
2662
|
+
const PayloadTypeToInt = {
|
|
2663
|
+
["request"]: 0,
|
|
2664
|
+
["result"]: 1,
|
|
2665
|
+
["progress"]: 2
|
|
2666
|
+
};
|
|
2667
|
+
const ReversePayloadType = [
|
|
2668
|
+
"request",
|
|
2669
|
+
"result",
|
|
2670
|
+
"progress"
|
|
2671
|
+
];
|
|
2672
|
+
/**
|
|
2673
|
+
* Build the positional `domain:id` ↔ integer dictionary. Both ends of a channel MUST build it from
|
|
2674
|
+
* the same domains in the same order — the mapping is positional, so a mismatch routes to the wrong
|
|
2675
|
+
* action. Add new transported domains to the end of the list.
|
|
2676
|
+
*/
|
|
2677
|
+
function buildActionRouteDictionary(domains) {
|
|
2678
|
+
const routeToInt = /* @__PURE__ */ new Map();
|
|
2679
|
+
const intToRoute = [];
|
|
2680
|
+
for (const dom of domains) for (const actionId of Object.keys(dom.actionSchema)) {
|
|
2681
|
+
const routeKey = `${dom.domain}:${actionId}`;
|
|
2682
|
+
if (routeToInt.has(routeKey)) continue;
|
|
2683
|
+
routeToInt.set(routeKey, intToRoute.length);
|
|
2684
|
+
intToRoute.push({
|
|
2685
|
+
domain: dom.domain,
|
|
2686
|
+
id: actionId,
|
|
2687
|
+
allDomains: dom.allDomains
|
|
2688
|
+
});
|
|
2689
|
+
}
|
|
2690
|
+
return {
|
|
2691
|
+
routeToInt,
|
|
2692
|
+
intToRoute
|
|
2693
|
+
};
|
|
2694
|
+
}
|
|
2695
|
+
/** Pull the type-specific payload (`input` / `result` / `progress`) out of a wire JSON object. */
|
|
2696
|
+
function extractWirePayload(json) {
|
|
2697
|
+
if (json.type === "request") return json.input;
|
|
2698
|
+
if (json.type === "result") return json.result;
|
|
2699
|
+
if (json.type === "progress") return json.progress;
|
|
2700
|
+
}
|
|
2701
|
+
/**
|
|
2702
|
+
* Reassemble a full wire JSON object from its decoded parts. `inputHash`/`outputHash` are emitted
|
|
2703
|
+
* empty — the hydration constructors recompute them — and the result still satisfies
|
|
2704
|
+
* `isActionPayload_Any_JsonObject` so it flows through validation like a JSON frame.
|
|
2705
|
+
*/
|
|
2706
|
+
function assembleWireJson(routeMeta, payloadType, time, context, payloadData) {
|
|
2707
|
+
const base = {
|
|
2708
|
+
form: "data",
|
|
2709
|
+
domain: routeMeta.domain,
|
|
2710
|
+
id: routeMeta.id,
|
|
2711
|
+
allDomains: routeMeta.allDomains,
|
|
2712
|
+
time,
|
|
2713
|
+
context
|
|
2714
|
+
};
|
|
2715
|
+
if (payloadType === "request") return {
|
|
2716
|
+
...base,
|
|
2717
|
+
type: "request",
|
|
2718
|
+
input: payloadData,
|
|
2719
|
+
inputHash: ""
|
|
2720
|
+
};
|
|
2721
|
+
if (payloadType === "result") return {
|
|
2722
|
+
...base,
|
|
2723
|
+
type: "result",
|
|
2724
|
+
result: payloadData,
|
|
2725
|
+
outputHash: ""
|
|
2726
|
+
};
|
|
2727
|
+
return {
|
|
2728
|
+
...base,
|
|
2729
|
+
type: "progress",
|
|
2730
|
+
progress: payloadData
|
|
2731
|
+
};
|
|
2732
|
+
}
|
|
2733
|
+
//#endregion
|
|
2734
|
+
//#region src/ActionRuntime/Handler/ExternalClient/Transport/WebSocket/createBinaryWsAdapter.ts
|
|
2735
|
+
/**
|
|
2736
|
+
* Positional layout of the stateless binary envelope. A flat tuple (rather than an object) strips the
|
|
2737
|
+
* repeated `domain`/`id`/`form`/`type` and context key names from every frame, and we carry only the
|
|
2738
|
+
* context fields the receiver can't recompute: `cuid` (correlation) and `originClient` (return
|
|
2739
|
+
* routing).
|
|
2740
|
+
*
|
|
2741
|
+
* [ routeInt, typeInt, time, cuid, originClient, payloadData ]
|
|
2742
|
+
*
|
|
2743
|
+
* Dropped vs the JSON wire: `form`/`type` strings, `inputHash`/`outputHash` (recomputed on hydrate),
|
|
2744
|
+
* `context.timeCreated` (reconstructed from `time`) and `context.routing` (rebuilt empty — the
|
|
2745
|
+
* receiver re-stamps its own route items as it handles the action). For the leanest possible frames
|
|
2746
|
+
* (integer correlation, identity dropped after a handshake), use `createBinaryWsSessionFactory`.
|
|
2747
|
+
*/
|
|
2748
|
+
const ENVELOPE$1 = {
|
|
2749
|
+
route: 0,
|
|
2750
|
+
type: 1,
|
|
2751
|
+
time: 2,
|
|
2752
|
+
cuid: 3,
|
|
2753
|
+
originClient: 4,
|
|
2754
|
+
payload: 5
|
|
2755
|
+
};
|
|
2756
|
+
const ENVELOPE_LENGTH$1 = 6;
|
|
2757
|
+
/**
|
|
2758
|
+
* Builds a *stateless* `formatMessage` pipeline for {@link WebSocketTransport}, packing action
|
|
2759
|
+
* payloads into a compact msgpackr binary frame instead of JSON. The `domain`/`id` route collapses to
|
|
2760
|
+
* a single integer drawn from a shared dictionary; `form`/`type`, the recomputable
|
|
2761
|
+
* `inputHash`/`outputHash`, and the per-frame `context.routing`/`context.timeCreated` are all dropped
|
|
2762
|
+
* (see {@link ENVELOPE}).
|
|
2763
|
+
*
|
|
2764
|
+
* No validation runs here: `incoming` blindly reconstructs the wire JSON shape and hands it back to
|
|
2765
|
+
* the connection, which flows into `ActionRuntime` → `domain.hydrateAnyAction()` where the Valibot
|
|
2766
|
+
* schemas validate it exactly as they would for a JSON frame.
|
|
2767
|
+
*
|
|
2768
|
+
* Both ends of the socket MUST construct the adapter with the same domains in the same order — the
|
|
2769
|
+
* integer dictionary is positional. Mismatched dictionaries will route to the wrong action.
|
|
2770
|
+
*
|
|
2771
|
+
* Because `incoming` returns `undefined` for text frames, a binary server can still serve plain-JSON
|
|
2772
|
+
* clients on the same runtime (the connection falls back to its built-in JSON parser).
|
|
2773
|
+
*/
|
|
2774
|
+
function createBinaryWsAdapter(domains) {
|
|
2775
|
+
const { routeToInt, intToRoute } = buildActionRouteDictionary(domains);
|
|
2776
|
+
return {
|
|
2777
|
+
outgoing: (input) => {
|
|
2778
|
+
const json = input.action.toJsonObject();
|
|
2779
|
+
const routeKey = `${json.domain}:${json.id}`;
|
|
2780
|
+
const routeInt = routeToInt.get(routeKey);
|
|
2781
|
+
if (routeInt == null) throw new Error(`[binary-ws] Cannot pack unregistered action route: ${routeKey}`);
|
|
2782
|
+
const envelope = new Array(ENVELOPE_LENGTH$1);
|
|
2783
|
+
envelope[ENVELOPE$1.route] = routeInt;
|
|
2784
|
+
envelope[ENVELOPE$1.type] = PayloadTypeToInt[json.type];
|
|
2785
|
+
envelope[ENVELOPE$1.time] = json.time;
|
|
2786
|
+
envelope[ENVELOPE$1.cuid] = json.context.cuid;
|
|
2787
|
+
envelope[ENVELOPE$1.originClient] = json.context.originClient;
|
|
2788
|
+
envelope[ENVELOPE$1.payload] = extractWirePayload(json);
|
|
2789
|
+
return (0, msgpackr.pack)(envelope);
|
|
2790
|
+
},
|
|
2791
|
+
incoming: (frame) => {
|
|
2792
|
+
let buffer;
|
|
2793
|
+
if (frame instanceof ArrayBuffer) buffer = new Uint8Array(frame);
|
|
2794
|
+
else if (frame instanceof Uint8Array) buffer = frame;
|
|
2795
|
+
else return;
|
|
2796
|
+
try {
|
|
2797
|
+
const envelope = (0, msgpackr.unpack)(buffer);
|
|
2798
|
+
if (!Array.isArray(envelope) || envelope.length !== ENVELOPE_LENGTH$1) return void 0;
|
|
2799
|
+
const routeMeta = intToRoute[envelope[ENVELOPE$1.route]];
|
|
2800
|
+
const payloadType = ReversePayloadType[envelope[ENVELOPE$1.type]];
|
|
2801
|
+
if (routeMeta == null || payloadType == null) return void 0;
|
|
2802
|
+
const time = envelope[ENVELOPE$1.time];
|
|
2803
|
+
return assembleWireJson(routeMeta, payloadType, time, {
|
|
2804
|
+
cuid: envelope[ENVELOPE$1.cuid],
|
|
2805
|
+
timeCreated: time,
|
|
2806
|
+
routing: [],
|
|
2807
|
+
originClient: envelope[ENVELOPE$1.originClient]
|
|
2808
|
+
}, envelope[ENVELOPE$1.payload]);
|
|
2809
|
+
} catch (e) {
|
|
2810
|
+
console.error("[binary-ws] Failed to unpack binary action frame", e);
|
|
2811
|
+
return;
|
|
2812
|
+
}
|
|
2813
|
+
}
|
|
2814
|
+
};
|
|
2815
|
+
}
|
|
2816
|
+
//#endregion
|
|
2817
|
+
//#region src/ActionRuntime/Handler/ExternalClient/Transport/WebSocket/createBinaryWsSessionFactory.ts
|
|
2818
|
+
/**
|
|
2819
|
+
* Positional layout of the *session* binary envelope — the leanest frame. Compared to the stateless
|
|
2820
|
+
* adapter it replaces the 21-char `cuid` with a small per-connection integer and only carries
|
|
2821
|
+
* `originClient` on the very first request of each direction (the peer remembers it afterwards).
|
|
2822
|
+
*
|
|
2823
|
+
* [ routeInt, typeInt, corrId, time, originClient?, payloadData ]
|
|
2824
|
+
*/
|
|
2825
|
+
const ENVELOPE = {
|
|
2826
|
+
route: 0,
|
|
2827
|
+
type: 1,
|
|
2828
|
+
corr: 2,
|
|
2829
|
+
time: 3,
|
|
2830
|
+
originClient: 4,
|
|
2831
|
+
payload: 5
|
|
2832
|
+
};
|
|
2833
|
+
const ENVELOPE_LENGTH = 6;
|
|
2834
|
+
/**
|
|
2835
|
+
* How long a pending correlation entry is kept before it's swept. A correlation only matters until its
|
|
2836
|
+
* action resolves or times out, so anything older than the longest realistic action timeout can be
|
|
2837
|
+
* dropped — this bounds memory when requests time out or a connection dies mid-flight (their replies
|
|
2838
|
+
* would never arrive, leaving the entry orphaned). Generous default so live correlations are never
|
|
2839
|
+
* pruned (the default transport timeout is 10s).
|
|
2840
|
+
*/
|
|
2841
|
+
const DEFAULT_CORRELATION_TTL_MS = 5 * 6e4;
|
|
2842
|
+
function isKnownIdentity(coordinate) {
|
|
2843
|
+
return coordinate != null && coordinate.envId !== "_unset_";
|
|
2844
|
+
}
|
|
2845
|
+
/**
|
|
2846
|
+
* Drop entries older than `ttlMs`. Maps keep insertion order and entries are inserted in time order,
|
|
2847
|
+
* so the oldest are first — stop sweeping at the first live entry.
|
|
2848
|
+
*/
|
|
2849
|
+
function pruneExpired(map, now, ttlMs) {
|
|
2850
|
+
for (const [key, entry] of map) {
|
|
2851
|
+
if (now - entry.time <= ttlMs) break;
|
|
2852
|
+
map.delete(key);
|
|
2853
|
+
}
|
|
2854
|
+
}
|
|
2855
|
+
/**
|
|
2856
|
+
* Builds a factory of *stateful, per-connection* codecs for {@link WebSocketTransport} /
|
|
2857
|
+
* `ActionServerHandler` — the maximally compact binary wire. Call the returned factory once per live
|
|
2858
|
+
* connection (each socket on the client, each accepted connection on the server) so every channel
|
|
2859
|
+
* gets its own correlation + identity state.
|
|
2860
|
+
*
|
|
2861
|
+
* On top of everything {@link createBinaryWsAdapter} drops, a session also drops:
|
|
2862
|
+
* - **`cuid`** — replaced by a per-connection integer correlation id. The initiator maps it to its
|
|
2863
|
+
* real cuid; the responder echoes it; each side reconstructs the cuid from its own map. Correlation
|
|
2864
|
+
* only needs to be unique per socket, so a counter suffices.
|
|
2865
|
+
* - **`originClient` after the first request** — the first request each side sends carries its
|
|
2866
|
+
* identity; the peer remembers it and injects it into later frames. Replies omit it entirely (a
|
|
2867
|
+
* reply carries the initiator's own origin, which the initiator already knows).
|
|
2868
|
+
*
|
|
2869
|
+
* Both ends MUST build the factory from the same domains in the same order (positional dictionary).
|
|
2870
|
+
* Text frames still return `undefined` from `incoming`, so JSON clients remain interoperable.
|
|
2871
|
+
*
|
|
2872
|
+
* Hibernation note: after a server connection is evicted its session resets, so a still-connected
|
|
2873
|
+
* client (whose session persists) will keep omitting `originClient`. The server must therefore restore
|
|
2874
|
+
* the connection→client binding from its own store (see `ActionServerHandler.rehydrateConnection`) and
|
|
2875
|
+
* inject `originClient` from there — the session alone can't recover it.
|
|
2876
|
+
*/
|
|
2877
|
+
function createBinaryWsSessionFactory(domains, options) {
|
|
2878
|
+
const { routeToInt, intToRoute } = buildActionRouteDictionary(domains);
|
|
2879
|
+
const unknownIdentity = RuntimeCoordinate.unknown.toJsonObject();
|
|
2880
|
+
const ttlMs = options?.correlationTtlMs ?? DEFAULT_CORRELATION_TTL_MS;
|
|
2881
|
+
return () => {
|
|
2882
|
+
let outCounter = 0;
|
|
2883
|
+
const corrToCuid = /* @__PURE__ */ new Map();
|
|
2884
|
+
const cuidToCorr = /* @__PURE__ */ new Map();
|
|
2885
|
+
let selfIdentity;
|
|
2886
|
+
let peerIdentity;
|
|
2887
|
+
return {
|
|
2888
|
+
outgoing: (input) => {
|
|
2889
|
+
const json = input.action.toJsonObject();
|
|
2890
|
+
const routeKey = `${json.domain}:${json.id}`;
|
|
2891
|
+
const routeInt = routeToInt.get(routeKey);
|
|
2892
|
+
if (routeInt == null) throw new Error(`[binary-ws] Cannot pack unregistered action route: ${routeKey}`);
|
|
2893
|
+
const now = Date.now();
|
|
2894
|
+
pruneExpired(corrToCuid, now, ttlMs);
|
|
2895
|
+
pruneExpired(cuidToCorr, now, ttlMs);
|
|
2896
|
+
let corr;
|
|
2897
|
+
let wireIdentity;
|
|
2898
|
+
if (json.type === "request") {
|
|
2899
|
+
corr = outCounter++;
|
|
2900
|
+
corrToCuid.set(corr, {
|
|
2901
|
+
value: json.context.cuid,
|
|
2902
|
+
time: now
|
|
2903
|
+
});
|
|
2904
|
+
if (selfIdentity == null && isKnownIdentity(json.context.originClient)) {
|
|
2905
|
+
selfIdentity = json.context.originClient;
|
|
2906
|
+
wireIdentity = json.context.originClient;
|
|
2907
|
+
}
|
|
2908
|
+
} else {
|
|
2909
|
+
corr = cuidToCorr.get(json.context.cuid)?.value ?? -1;
|
|
2910
|
+
if (json.type === "result") cuidToCorr.delete(json.context.cuid);
|
|
2911
|
+
}
|
|
2912
|
+
const envelope = new Array(ENVELOPE_LENGTH);
|
|
2913
|
+
envelope[ENVELOPE.route] = routeInt;
|
|
2914
|
+
envelope[ENVELOPE.type] = PayloadTypeToInt[json.type];
|
|
2915
|
+
envelope[ENVELOPE.corr] = corr;
|
|
2916
|
+
envelope[ENVELOPE.time] = json.time;
|
|
2917
|
+
envelope[ENVELOPE.originClient] = wireIdentity;
|
|
2918
|
+
envelope[ENVELOPE.payload] = extractWirePayload(json);
|
|
2919
|
+
return (0, msgpackr.pack)(envelope);
|
|
2920
|
+
},
|
|
2921
|
+
incoming: (frame) => {
|
|
2922
|
+
let buffer;
|
|
2923
|
+
if (frame instanceof ArrayBuffer) buffer = new Uint8Array(frame);
|
|
2924
|
+
else if (frame instanceof Uint8Array) buffer = frame;
|
|
2925
|
+
else return;
|
|
2926
|
+
try {
|
|
2927
|
+
const envelope = (0, msgpackr.unpack)(buffer);
|
|
2928
|
+
if (!Array.isArray(envelope) || envelope.length !== ENVELOPE_LENGTH) return void 0;
|
|
2929
|
+
const routeMeta = intToRoute[envelope[ENVELOPE.route]];
|
|
2930
|
+
const payloadType = ReversePayloadType[envelope[ENVELOPE.type]];
|
|
2931
|
+
if (routeMeta == null || payloadType == null) return void 0;
|
|
2932
|
+
const now = Date.now();
|
|
2933
|
+
pruneExpired(corrToCuid, now, ttlMs);
|
|
2934
|
+
pruneExpired(cuidToCorr, now, ttlMs);
|
|
2935
|
+
const corr = envelope[ENVELOPE.corr];
|
|
2936
|
+
const time = envelope[ENVELOPE.time];
|
|
2937
|
+
const wireIdentity = envelope[ENVELOPE.originClient];
|
|
2938
|
+
let cuid;
|
|
2939
|
+
let originClient;
|
|
2940
|
+
if (payloadType === "request") {
|
|
2941
|
+
cuid = (0, nanoid.nanoid)();
|
|
2942
|
+
cuidToCorr.set(cuid, {
|
|
2943
|
+
value: corr,
|
|
2944
|
+
time: now
|
|
2945
|
+
});
|
|
2946
|
+
if (isKnownIdentity(wireIdentity)) peerIdentity = wireIdentity;
|
|
2947
|
+
originClient = peerIdentity ?? unknownIdentity;
|
|
2948
|
+
} else {
|
|
2949
|
+
cuid = corrToCuid.get(corr)?.value ?? (0, nanoid.nanoid)();
|
|
2950
|
+
if (payloadType === "result") corrToCuid.delete(corr);
|
|
2951
|
+
originClient = selfIdentity ?? unknownIdentity;
|
|
2952
|
+
}
|
|
2953
|
+
return assembleWireJson(routeMeta, payloadType, time, {
|
|
2954
|
+
cuid,
|
|
2955
|
+
timeCreated: time,
|
|
2956
|
+
routing: [],
|
|
2957
|
+
originClient
|
|
2958
|
+
}, envelope[ENVELOPE.payload]);
|
|
2959
|
+
} catch (e) {
|
|
2960
|
+
console.error("[binary-ws] Failed to unpack binary action session frame", e);
|
|
2961
|
+
return;
|
|
2962
|
+
}
|
|
2963
|
+
}
|
|
2964
|
+
};
|
|
2965
|
+
};
|
|
2966
|
+
}
|
|
2967
|
+
//#endregion
|
|
2968
|
+
//#region src/utils/decodeActionFrame.ts
|
|
2969
|
+
/**
|
|
2970
|
+
* Decode a single inbound channel frame (text or binary) into validated action wire JSON, or
|
|
2971
|
+
* `undefined` if it isn't a recognisable action payload.
|
|
2972
|
+
*
|
|
2973
|
+
* Shared by the WebSocket transport's message listener and the server-side `ActionServerHandler` so
|
|
2974
|
+
* both decode identically: a binary `decoder.incoming` (e.g. msgpackr) takes precedence, and plain
|
|
2975
|
+
* text frames fall back to JSON — keeping binary and JSON clients interoperable on one channel.
|
|
2976
|
+
*/
|
|
2977
|
+
function decodeActionFrame(frame, decoder) {
|
|
2978
|
+
const decoded = decoder?.incoming?.(frame) ?? (typeof frame === "string" ? parseJsonActionFrame(frame) : void 0);
|
|
2979
|
+
return decoded != null && isActionPayload_Any_JsonObject(decoded) ? decoded : void 0;
|
|
2980
|
+
}
|
|
2981
|
+
function parseJsonActionFrame(message) {
|
|
2982
|
+
try {
|
|
2983
|
+
const json = JSON.parse(message);
|
|
2984
|
+
return isActionPayload_Any_JsonObject(json) ? json : void 0;
|
|
2985
|
+
} catch {
|
|
2986
|
+
return;
|
|
2987
|
+
}
|
|
2988
|
+
}
|
|
2989
|
+
//#endregion
|
|
2990
|
+
//#region src/ActionRuntime/Handler/ExternalClient/Transport/helpers/createUnsetTransportResolvers.ts
|
|
2991
|
+
const createUnsetTransportResolvers = (type) => ({ onIncomingActionDataJson: (json) => {
|
|
2992
|
+
console.warn(`Received incoming action JSON [${json.domain}:${json.id}] on Transport [${type}] but no incoming data listener has been set.`);
|
|
2993
|
+
} });
|
|
2994
|
+
//#endregion
|
|
2995
|
+
//#region src/ActionRuntime/Handler/ExternalClient/Transport/WebSocket/ws_util.ts
|
|
2996
|
+
/**
|
|
2997
|
+
* Send a text or binary frame over a socket. A binary formatter may hand back a `Uint8Array` whose
|
|
2998
|
+
* backing buffer is typed as `ArrayBufferLike` (msgpackr pools buffers / may be `SharedArrayBuffer`),
|
|
2999
|
+
* which `WebSocket.send`'s `BufferSource` parameter rejects — copy it into a fresh `ArrayBuffer`-backed
|
|
3000
|
+
* view so the type (and the bytes) are safe to send.
|
|
3001
|
+
*/
|
|
3002
|
+
function sendFrame(ws, data) {
|
|
3003
|
+
if (typeof data === "string" || data instanceof ArrayBuffer) {
|
|
3004
|
+
ws.send(data);
|
|
3005
|
+
return;
|
|
3006
|
+
}
|
|
3007
|
+
ws.send(new Uint8Array(data));
|
|
3008
|
+
}
|
|
3009
|
+
/** Normalize any frame form to bytes (for the AES-GCM layer, which works on `Uint8Array`). */
|
|
3010
|
+
function toFrameBytes(frame) {
|
|
3011
|
+
if (typeof frame === "string") return new TextEncoder().encode(frame);
|
|
3012
|
+
if (frame instanceof ArrayBuffer) return new Uint8Array(frame);
|
|
3013
|
+
return frame;
|
|
3014
|
+
}
|
|
3015
|
+
/** Compact a WebSocket URL to `host/pathname` for devtools display, falling back to the raw url. */
|
|
3016
|
+
function shortWs(url) {
|
|
3017
|
+
try {
|
|
3018
|
+
const u = new URL(url);
|
|
3019
|
+
return `${u.host}${u.pathname}`;
|
|
3020
|
+
} catch {
|
|
3021
|
+
return url;
|
|
3022
|
+
}
|
|
3023
|
+
}
|
|
3024
|
+
//#endregion
|
|
3025
|
+
//#region src/ActionRuntime/Handler/ExternalClient/Transport/WebSocket/WebSocketConnection.ts
|
|
3026
|
+
const HANDSHAKE_TIMEOUT_MS = 15e3;
|
|
3027
|
+
var WebSocketConnection = class extends TransportConnection {
|
|
3028
|
+
resolvers;
|
|
3029
|
+
/** URL of the most recently resolved live socket — surfaced to devtools when the definition can't. */
|
|
3030
|
+
_liveSocketUrl;
|
|
3031
|
+
/** Sockets we closed on purpose (via `disconnect`), so their `close` event stays quiet. */
|
|
3032
|
+
_intentionalCloses = /* @__PURE__ */ new WeakSet();
|
|
3033
|
+
constructor(def, resolvers) {
|
|
3034
|
+
super({
|
|
3035
|
+
...def,
|
|
3036
|
+
type: "ws"
|
|
3037
|
+
});
|
|
3038
|
+
this.resolvers = resolvers ?? createUnsetTransportResolvers("ws");
|
|
3039
|
+
}
|
|
3040
|
+
_getCacheKey(_input) {
|
|
3041
|
+
return this.initialized.getTransportCacheKey?.(_input).join("\0") ?? "";
|
|
3042
|
+
}
|
|
3043
|
+
_processTransportStatus(input) {
|
|
3044
|
+
const transportStatusInfo = addTransportStatusMetadata(this.initialized.getTransport(input));
|
|
3045
|
+
if (transportStatusInfo.status !== "initializing" && transportStatusInfo.status !== "ready") return transportStatusInfo;
|
|
3046
|
+
if (transportStatusInfo.status === "ready") {
|
|
3047
|
+
const ws = transportStatusInfo.readyData.ws;
|
|
3048
|
+
if (ws.readyState !== WebSocket.OPEN || this._isSecure(transportStatusInfo.readyData)) {
|
|
3049
|
+
const readyData = transportStatusInfo.readyData;
|
|
3050
|
+
const initialization = async () => {
|
|
3051
|
+
await this._awaitOpen(ws);
|
|
3052
|
+
return {
|
|
3053
|
+
status: "ready",
|
|
3054
|
+
readyData: await this._finalize(readyData)
|
|
3055
|
+
};
|
|
3056
|
+
};
|
|
3057
|
+
return {
|
|
3058
|
+
status: "initializing",
|
|
3059
|
+
timeStarted: Date.now(),
|
|
3060
|
+
initializationPromise: initialization()
|
|
3061
|
+
};
|
|
3062
|
+
}
|
|
3063
|
+
}
|
|
3064
|
+
if (transportStatusInfo.status === "initializing") return {
|
|
3065
|
+
status: "initializing",
|
|
3066
|
+
initializationPromise: transportStatusInfo.initializationPromise.then(async (result) => {
|
|
3067
|
+
if (result.status === "ready") {
|
|
3068
|
+
await this._awaitOpen(result.readyData.ws);
|
|
3069
|
+
return {
|
|
3070
|
+
status: "ready",
|
|
3071
|
+
readyData: await this._finalize(result.readyData)
|
|
3072
|
+
};
|
|
3073
|
+
}
|
|
3074
|
+
return result;
|
|
3075
|
+
}),
|
|
3076
|
+
timeStarted: transportStatusInfo.timeStarted
|
|
3077
|
+
};
|
|
3078
|
+
return {
|
|
3079
|
+
status: "ready",
|
|
3080
|
+
readyData: this._finalizeTransportMethods(transportStatusInfo.readyData)
|
|
3081
|
+
};
|
|
3082
|
+
}
|
|
3083
|
+
getRouteInfo(input) {
|
|
3084
|
+
const base = this.definition?.getRouteInfo(input);
|
|
3085
|
+
if (base?.url != null || this._liveSocketUrl == null) return base;
|
|
3086
|
+
return {
|
|
3087
|
+
type: "ws",
|
|
3088
|
+
...base,
|
|
3089
|
+
url: this._liveSocketUrl,
|
|
3090
|
+
summary: `ws ${shortWs(this._liveSocketUrl)}`
|
|
3091
|
+
};
|
|
3092
|
+
}
|
|
3093
|
+
_isSecure(wsData) {
|
|
3094
|
+
return wsData.secureChannel != null && wsData.secureChannel.securityLevel !== "none";
|
|
3095
|
+
}
|
|
3096
|
+
_awaitOpen(ws) {
|
|
3097
|
+
if (ws.readyState === WebSocket.OPEN) return Promise.resolve();
|
|
3098
|
+
return new Promise((resolve, reject) => {
|
|
3099
|
+
ws.addEventListener("open", () => resolve(), { once: true });
|
|
3100
|
+
ws.addEventListener("error", (event) => reject(event), { once: true });
|
|
3101
|
+
ws.addEventListener("close", (event) => reject(/* @__PURE__ */ new Error(`WebSocket closed before open: code=${event.code}`)), { once: true });
|
|
3102
|
+
});
|
|
3103
|
+
}
|
|
3104
|
+
/** Non-secure connections finalize synchronously; secure ones run the handshake first. */
|
|
3105
|
+
_finalize(wsData) {
|
|
3106
|
+
return this._isSecure(wsData) ? this._finalizeSecureMethods(wsData) : this._finalizeTransportMethods(wsData);
|
|
3107
|
+
}
|
|
3108
|
+
_finalizeTransportMethods(wsData) {
|
|
3109
|
+
const ws = wsData.ws;
|
|
3110
|
+
const disconnectListeners = [];
|
|
3111
|
+
const abortSet = /* @__PURE__ */ new Set();
|
|
3112
|
+
this._captureSocketUrl(ws);
|
|
3113
|
+
this._attachLifecycle(ws, disconnectListeners, abortSet);
|
|
3114
|
+
ws.addEventListener("message", async (event) => {
|
|
3115
|
+
const frame = await this._normalizeFrame(event.data);
|
|
3116
|
+
if (frame !== void 0) await this._handleIncomingActionFrame(frame, wsData, void 0);
|
|
3117
|
+
});
|
|
3118
|
+
return this._buildSendMethods(ws, wsData, void 0, disconnectListeners, abortSet);
|
|
3119
|
+
}
|
|
3120
|
+
/**
|
|
3121
|
+
* Secure path: a single message listener feeds the handshake until it completes, then routes action
|
|
3122
|
+
* frames (decrypting for the `encrypted` level). Frames that arrive in the gap between accept and
|
|
3123
|
+
* activation are buffered and flushed, so nothing is lost.
|
|
3124
|
+
*/
|
|
3125
|
+
async _finalizeSecureMethods(wsData) {
|
|
3126
|
+
const ws = wsData.ws;
|
|
3127
|
+
const disconnectListeners = [];
|
|
3128
|
+
const abortSet = /* @__PURE__ */ new Set();
|
|
3129
|
+
this._captureSocketUrl(ws);
|
|
3130
|
+
this._attachLifecycle(ws, disconnectListeners, abortSet);
|
|
3131
|
+
let active = false;
|
|
3132
|
+
let crypto;
|
|
3133
|
+
const handshakeQueue = [];
|
|
3134
|
+
const handshakeWaiters = [];
|
|
3135
|
+
const pendingActionFrames = [];
|
|
3136
|
+
ws.addEventListener("message", async (event) => {
|
|
3137
|
+
const frame = await this._normalizeFrame(event.data);
|
|
3138
|
+
if (frame === void 0) return;
|
|
3139
|
+
if (active) {
|
|
3140
|
+
await this._handleIncomingActionFrame(frame, wsData, crypto);
|
|
3141
|
+
return;
|
|
3142
|
+
}
|
|
3143
|
+
if (typeof frame === "string") {
|
|
3144
|
+
const message = decodeHandshakeMessage(frame);
|
|
3145
|
+
if (message != null) {
|
|
3146
|
+
const waiter = handshakeWaiters.shift();
|
|
3147
|
+
if (waiter != null) waiter(message);
|
|
3148
|
+
else handshakeQueue.push(message);
|
|
3149
|
+
return;
|
|
3150
|
+
}
|
|
3151
|
+
}
|
|
3152
|
+
pendingActionFrames.push(frame);
|
|
3153
|
+
});
|
|
3154
|
+
const nextHandshakeMessage = () => {
|
|
3155
|
+
const queued = handshakeQueue.shift();
|
|
3156
|
+
if (queued != null) return Promise.resolve(queued);
|
|
3157
|
+
return new Promise((resolve, reject) => {
|
|
3158
|
+
const timeout = setTimeout(() => reject(/* @__PURE__ */ new Error("[ws-handshake] timed out waiting for server reply")), HANDSHAKE_TIMEOUT_MS);
|
|
3159
|
+
handshakeWaiters.push((message) => {
|
|
3160
|
+
clearTimeout(timeout);
|
|
3161
|
+
resolve(message);
|
|
3162
|
+
});
|
|
3163
|
+
});
|
|
3164
|
+
};
|
|
3165
|
+
crypto = await this._runClientHandshake(ws, wsData.secureChannel, nextHandshakeMessage);
|
|
3166
|
+
active = true;
|
|
3167
|
+
for (const frame of pendingActionFrames) await this._handleIncomingActionFrame(frame, wsData, crypto);
|
|
3168
|
+
pendingActionFrames.length = 0;
|
|
3169
|
+
return this._buildSendMethods(ws, wsData, crypto, disconnectListeners, abortSet);
|
|
3170
|
+
}
|
|
3171
|
+
async _runClientHandshake(ws, secure, nextHandshakeMessage) {
|
|
3172
|
+
await secure.link.initialize();
|
|
3173
|
+
const handshake = createClientHandshake({
|
|
3174
|
+
link: secure.link,
|
|
3175
|
+
localCoordinate: secure.localCoordinate,
|
|
3176
|
+
dictionaryVersion: secure.dictionaryVersion,
|
|
3177
|
+
securityLevel: secure.securityLevel
|
|
3178
|
+
});
|
|
3179
|
+
sendFrame(ws, encodeHandshakeMessage(await handshake.createHello()));
|
|
3180
|
+
const welcome = await nextHandshakeMessage();
|
|
3181
|
+
if (welcome.t === "reject") throw new Error(`[ws-handshake] rejected by server: ${welcome.reason}`);
|
|
3182
|
+
if (welcome.t !== "welcome") throw new Error(`[ws-handshake] expected welcome, got ${welcome.t}`);
|
|
3183
|
+
sendFrame(ws, encodeHandshakeMessage(await handshake.onWelcome(welcome)));
|
|
3184
|
+
const accept = await nextHandshakeMessage();
|
|
3185
|
+
if (accept.t === "reject") throw new Error(`[ws-handshake] rejected by server: ${accept.reason}`);
|
|
3186
|
+
if (accept.t !== "accept") throw new Error(`[ws-handshake] expected accept, got ${accept.t}`);
|
|
3187
|
+
const result = await handshake.onAccept(accept);
|
|
3188
|
+
return result.securityLevel === "encrypted" ? createActionFrameCrypto({
|
|
3189
|
+
link: secure.link,
|
|
3190
|
+
linkedClientId: result.linkedClientId
|
|
3191
|
+
}) : void 0;
|
|
3192
|
+
}
|
|
3193
|
+
_buildSendMethods(ws, wsData, crypto, disconnectListeners, abortSet) {
|
|
3194
|
+
let sendChain = Promise.resolve();
|
|
3195
|
+
const enqueueSend = (frame) => {
|
|
3196
|
+
if (ws.readyState !== WebSocket.OPEN) return;
|
|
3197
|
+
if (crypto == null) {
|
|
3198
|
+
sendFrame(ws, frame);
|
|
3199
|
+
return;
|
|
3200
|
+
}
|
|
3201
|
+
const bytes = toFrameBytes(frame);
|
|
3202
|
+
sendChain = sendChain.then(() => crypto.encryptFrame(bytes)).then((encrypted) => {
|
|
3203
|
+
if (ws.readyState === WebSocket.OPEN) sendFrame(ws, encrypted);
|
|
3204
|
+
}).catch((err) => console.error("[ws] failed to encrypt/send frame", err));
|
|
3205
|
+
};
|
|
3206
|
+
const sendActionData = (inputs) => {
|
|
3207
|
+
const { action, runningAction, timeout } = inputs;
|
|
3208
|
+
if (ws.readyState !== WebSocket.OPEN) {
|
|
3209
|
+
if (action.type === "request") runningAction._abort(err_nice_transport_ws.fromId("ws_disconnected"));
|
|
3210
|
+
return;
|
|
3211
|
+
}
|
|
3212
|
+
if (action.type === "request") {
|
|
3213
|
+
abortSet.add(runningAction);
|
|
3214
|
+
const timeoutId = setTimeout(() => {
|
|
3215
|
+
runningAction._abort(err_nice_transport.fromId("timeout", { timeout }));
|
|
3216
|
+
}, timeout);
|
|
3217
|
+
runningAction.addUpdateListeners([(update) => {
|
|
3218
|
+
if (update.type === "finished") {
|
|
3219
|
+
clearTimeout(timeoutId);
|
|
3220
|
+
abortSet.delete(runningAction);
|
|
3221
|
+
}
|
|
3222
|
+
}]);
|
|
3223
|
+
}
|
|
3224
|
+
enqueueSend(wsData.formatMessage?.outgoing(inputs) ?? JSON.stringify(inputs.action.toJsonObject()));
|
|
3225
|
+
};
|
|
3226
|
+
return {
|
|
3227
|
+
sendActionData,
|
|
3228
|
+
updateRunConfig: wsData.updateRunConfig,
|
|
3229
|
+
addOnDisconnectListener: (cb) => {
|
|
3230
|
+
disconnectListeners.push(cb);
|
|
3231
|
+
},
|
|
3232
|
+
disconnect: () => {
|
|
3233
|
+
this._intentionalCloses.add(ws);
|
|
3234
|
+
try {
|
|
3235
|
+
ws.close();
|
|
3236
|
+
} catch {}
|
|
3237
|
+
},
|
|
3238
|
+
sendReturnData: (payload, clients) => {
|
|
3239
|
+
enqueueSend((clients != null ? wsData.formatMessage?.outgoing({
|
|
3240
|
+
action: payload,
|
|
3241
|
+
...clients
|
|
3242
|
+
}) : void 0) ?? JSON.stringify(payload.toJsonObject()));
|
|
3243
|
+
}
|
|
3244
|
+
};
|
|
3245
|
+
}
|
|
3246
|
+
/** Decode (and, when encrypted, decrypt) one inbound action frame and hand it to the runtime. */
|
|
3247
|
+
async _handleIncomingActionFrame(frame, wsData, crypto) {
|
|
3248
|
+
let decoded = frame;
|
|
3249
|
+
if (crypto != null) try {
|
|
3250
|
+
decoded = await crypto.decryptFrame(frame);
|
|
3251
|
+
} catch (err) {
|
|
3252
|
+
console.error("[ws] failed to decrypt incoming frame", err);
|
|
3253
|
+
return;
|
|
3254
|
+
}
|
|
3255
|
+
const rawJson = decodeActionFrame(decoded, wsData.formatMessage);
|
|
3256
|
+
if (rawJson != null) this.resolvers.onIncomingActionDataJson(rawJson);
|
|
3257
|
+
}
|
|
3258
|
+
/** Accept text + binary frames (ArrayBuffer / Uint8Array / Blob); Blobs are converted to a buffer. */
|
|
3259
|
+
async _normalizeFrame(data) {
|
|
3260
|
+
if (typeof data === "string" || data instanceof ArrayBuffer || data instanceof Uint8Array) return data;
|
|
3261
|
+
if (typeof Blob !== "undefined" && data instanceof Blob) return await data.arrayBuffer();
|
|
3262
|
+
}
|
|
3263
|
+
_captureSocketUrl(ws) {
|
|
3264
|
+
if (ws.url != null && ws.url !== "") this._liveSocketUrl = ws.url;
|
|
3265
|
+
}
|
|
3266
|
+
_attachLifecycle(ws, disconnectListeners, abortSet) {
|
|
3267
|
+
ws.addEventListener("close", (event) => {
|
|
3268
|
+
if (!this._intentionalCloses.has(ws)) console.error("WebSocket closed:", event);
|
|
3269
|
+
for (const cb of disconnectListeners) cb();
|
|
3270
|
+
this._abortAll(abortSet, err_nice_transport_ws.fromId("ws_disconnected"));
|
|
3271
|
+
});
|
|
3272
|
+
ws.addEventListener("error", (event) => {
|
|
3273
|
+
console.error("WebSocket error:", event);
|
|
3274
|
+
for (const cb of disconnectListeners) cb();
|
|
3275
|
+
this._abortAll(abortSet, err_nice_transport_ws.fromId("ws_error", { originalError: event instanceof Error ? event : void 0 }));
|
|
3276
|
+
});
|
|
3277
|
+
}
|
|
3278
|
+
_abortAll(abortSet, error) {
|
|
3279
|
+
const snapshot = [...abortSet];
|
|
3280
|
+
for (const ra of snapshot) ra._abort(error);
|
|
3281
|
+
}
|
|
3282
|
+
};
|
|
3283
|
+
//#endregion
|
|
3284
|
+
//#region src/ActionRuntime/Handler/ExternalClient/Transport/WebSocket/WebSocketTransport.ts
|
|
3285
|
+
/**
|
|
3286
|
+
* Reusable WebSocket transport definition. Create one with `WebSocketTransport.create({ createWebSocket })`
|
|
3287
|
+
* for the common case, or `WebSocketTransport.createAdvanced({ getTransport })` for full control over
|
|
3288
|
+
* readiness. The underlying socket is cached (via `getTransportCacheKey`) and reused across actions.
|
|
3289
|
+
*/
|
|
3290
|
+
var WebSocketTransport = class WebSocketTransport extends Transport {
|
|
3291
|
+
options;
|
|
3292
|
+
type = "ws";
|
|
3293
|
+
constructor(options) {
|
|
3294
|
+
super();
|
|
3295
|
+
this.options = options;
|
|
3296
|
+
}
|
|
3297
|
+
static create(options) {
|
|
3298
|
+
return new WebSocketTransport({
|
|
3299
|
+
...options,
|
|
3300
|
+
mode: "socket"
|
|
3301
|
+
});
|
|
3302
|
+
}
|
|
3303
|
+
static createAdvanced(options) {
|
|
3304
|
+
return new WebSocketTransport({
|
|
3305
|
+
...options,
|
|
3306
|
+
mode: "advanced"
|
|
3307
|
+
});
|
|
3308
|
+
}
|
|
3309
|
+
_createConnection(ctx) {
|
|
3310
|
+
const options = this.options;
|
|
3311
|
+
let getTransport;
|
|
3312
|
+
if (options.mode === "advanced") getTransport = options.getTransport;
|
|
3313
|
+
else getTransport = (input) => ({
|
|
3314
|
+
status: "ready",
|
|
3315
|
+
readyData: {
|
|
3316
|
+
ws: options.createWebSocket(input),
|
|
3317
|
+
formatMessage: options.createFormatMessage?.() ?? options.formatMessage,
|
|
3318
|
+
updateRunConfig: options.updateRunConfig,
|
|
3319
|
+
secureChannel: options.security
|
|
3320
|
+
}
|
|
3321
|
+
});
|
|
3322
|
+
return new WebSocketConnection({ initialize: () => ({
|
|
3323
|
+
getTransportCacheKey: options.getTransportCacheKey,
|
|
3324
|
+
getTransport
|
|
3325
|
+
}) }, ctx.resolvers);
|
|
3326
|
+
}
|
|
3327
|
+
getRouteInfo(input) {
|
|
3328
|
+
if (this.options.getRouteInfo != null) return this.options.getRouteInfo(input);
|
|
3329
|
+
return {
|
|
3330
|
+
type: "ws",
|
|
3331
|
+
summary: "ws"
|
|
3332
|
+
};
|
|
3333
|
+
}
|
|
3334
|
+
};
|
|
3335
|
+
//#endregion
|
|
3336
|
+
//#region src/ActionRuntime/Handler/ExternalClient/Transport/WebSocket/secureWsChannel.ts
|
|
3337
|
+
/**
|
|
3338
|
+
* Derive a stable wire-dictionary version from the ordered route list (FNV-1a over `domain:id,…`), so
|
|
3339
|
+
* the version moves automatically whenever the transported domains change — a stale peer is then
|
|
3340
|
+
* rejected by the handshake instead of silently misrouting a positionally-packed frame.
|
|
3341
|
+
*/
|
|
3342
|
+
function deriveDictionaryVersion(domains) {
|
|
3343
|
+
const { intToRoute } = buildActionRouteDictionary(domains);
|
|
3344
|
+
const signature = intToRoute.map((route) => `${route.domain}:${route.id}`).join(",");
|
|
3345
|
+
let hash = 2166136261;
|
|
3346
|
+
for (let i = 0; i < signature.length; i++) {
|
|
3347
|
+
hash ^= signature.charCodeAt(i);
|
|
3348
|
+
hash = Math.imul(hash, 16777619);
|
|
3349
|
+
}
|
|
3350
|
+
return `auto:${(hash >>> 0).toString(16).padStart(8, "0")}`;
|
|
3351
|
+
}
|
|
3352
|
+
/**
|
|
3353
|
+
* Bundle a secure channel's shared identity from its transported domains. Both ends MUST call this
|
|
3354
|
+
* with the same domains in the same order (the binary wire dictionary is positional). The
|
|
3355
|
+
* `dictionaryVersion` is derived from those domains unless you pin an explicit one.
|
|
3356
|
+
*/
|
|
3357
|
+
function defineSecureWsChannel(options) {
|
|
3358
|
+
return {
|
|
3359
|
+
dictionaryVersion: options.dictionaryVersion ?? deriveDictionaryVersion(options.domains),
|
|
3360
|
+
createCodec: createBinaryWsSessionFactory(options.domains, options.sessionOptions)
|
|
3361
|
+
};
|
|
3362
|
+
}
|
|
3363
|
+
/**
|
|
3364
|
+
* Build a {@link WebSocketTransport} for the secure binary channel with the boilerplate folded in: it
|
|
3365
|
+
* creates the {@link ClientCryptoKeyLink} from `storageAdapter`, opens an `arraybuffer` socket to
|
|
3366
|
+
* `url`, caches it per endpoint, installs the channel's per-connection codec, and assembles the
|
|
3367
|
+
* `security` block from the runtime coordinate + channel version. Pass `createWebSocket` /
|
|
3368
|
+
* `getTransportCacheKey` to take over those bits when you need to.
|
|
3369
|
+
*/
|
|
3370
|
+
function createSecureWebSocketTransport(options) {
|
|
3371
|
+
const link = new _nice_code_util.ClientCryptoKeyLink({ storageAdapter: options.storageAdapter });
|
|
3372
|
+
return WebSocketTransport.create({
|
|
3373
|
+
createWebSocket: options.createWebSocket ?? (() => {
|
|
3374
|
+
const ws = new WebSocket(options.url);
|
|
3375
|
+
ws.binaryType = "arraybuffer";
|
|
3376
|
+
return ws;
|
|
3377
|
+
}),
|
|
3378
|
+
getTransportCacheKey: options.getTransportCacheKey ?? (() => [options.url]),
|
|
3379
|
+
createFormatMessage: options.channel.createCodec,
|
|
3380
|
+
updateRunConfig: options.updateRunConfig,
|
|
3381
|
+
getRouteInfo: options.getRouteInfo,
|
|
3382
|
+
security: {
|
|
3383
|
+
securityLevel: options.securityLevel,
|
|
3384
|
+
link,
|
|
3385
|
+
localCoordinate: options.runtime.coordinate.toJsonObject(),
|
|
3386
|
+
dictionaryVersion: options.channel.dictionaryVersion
|
|
3387
|
+
}
|
|
3388
|
+
});
|
|
3389
|
+
}
|
|
3390
|
+
//#endregion
|
|
3391
|
+
//#region src/ActionRuntime/Handler/Server/WsConnectionStateStore.ts
|
|
3392
|
+
/**
|
|
3393
|
+
* A typed per-connection state store that co-owns the app state and the server handler's routing
|
|
3394
|
+
* binding in one attachment, so neither the consumer nor the handler has to hand-merge the two. Create
|
|
3395
|
+
* it through {@link ActionServerHandler.createConnectionState} (which also wires binding persistence and
|
|
3396
|
+
* replays surviving connections after a wake), then `get`/`set`/`clearApp` the app state directly.
|
|
3397
|
+
*
|
|
3398
|
+
* ```ts
|
|
3399
|
+
* const players = serverHandler.createConnectionState({
|
|
3400
|
+
* schema: vs_player,
|
|
3401
|
+
* read: (ws) => ws.deserializeAttachment(),
|
|
3402
|
+
* write: (ws, v) => ws.serializeAttachment(v),
|
|
3403
|
+
* getConnections: () => ctx.getWebSockets(),
|
|
3404
|
+
* });
|
|
3405
|
+
* players.set(ws, player); // binding is preserved automatically
|
|
3406
|
+
* const player = players.get(ws);
|
|
3407
|
+
* ```
|
|
3408
|
+
*/
|
|
3409
|
+
var WsConnectionStateStore = class {
|
|
3410
|
+
options;
|
|
3411
|
+
constructor(options) {
|
|
3412
|
+
this.options = options;
|
|
3413
|
+
}
|
|
3414
|
+
/** The validated app state for a connection, or `null` if unset / invalid. */
|
|
3415
|
+
get(connection) {
|
|
3416
|
+
return this._readAttachment(connection).app ?? null;
|
|
3417
|
+
}
|
|
3418
|
+
/** Set the app state, preserving the runtime binding already pinned to the connection. */
|
|
3419
|
+
set(connection, app) {
|
|
3420
|
+
const existing = this._readAttachment(connection);
|
|
3421
|
+
this.options.write(connection, {
|
|
3422
|
+
app,
|
|
3423
|
+
binding: existing.binding
|
|
3424
|
+
});
|
|
3425
|
+
}
|
|
3426
|
+
/** Clear the app state but keep the binding (e.g. a spectator that stopped watching). */
|
|
3427
|
+
clearApp(connection) {
|
|
3428
|
+
const existing = this._readAttachment(connection);
|
|
3429
|
+
this.options.write(connection, { binding: existing.binding });
|
|
3430
|
+
}
|
|
3431
|
+
/** Every live connection paired with its (validated) app state — for rebuilding in-memory state after a wake. */
|
|
3432
|
+
entries() {
|
|
3433
|
+
return this.options.getConnections().map((connection) => [connection, this._readAttachment(connection).app ?? null]);
|
|
3434
|
+
}
|
|
3435
|
+
/** @internal Persist a freshly-bound connection's binding, preserving any app state already stored. */
|
|
3436
|
+
_persistBinding(connection, binding) {
|
|
3437
|
+
const existing = this._readAttachment(connection);
|
|
3438
|
+
this.options.write(connection, {
|
|
3439
|
+
app: existing.app,
|
|
3440
|
+
binding
|
|
3441
|
+
});
|
|
3442
|
+
}
|
|
3443
|
+
/** @internal The persisted binding for a connection, if any (used to replay routing after a wake). */
|
|
3444
|
+
_readBinding(connection) {
|
|
3445
|
+
return this._readAttachment(connection).binding;
|
|
3446
|
+
}
|
|
3447
|
+
_readAttachment(connection) {
|
|
3448
|
+
try {
|
|
3449
|
+
const raw = this.options.read(connection);
|
|
3450
|
+
if (typeof raw !== "object" || raw === null) return {};
|
|
3451
|
+
const attachment = raw;
|
|
3452
|
+
const result = {};
|
|
3453
|
+
if (attachment.binding != null) result.binding = attachment.binding;
|
|
3454
|
+
if (attachment.app !== void 0) {
|
|
3455
|
+
const app = this._validateApp(attachment.app);
|
|
3456
|
+
if (app !== void 0) result.app = app;
|
|
3457
|
+
}
|
|
3458
|
+
return result;
|
|
3459
|
+
} catch {
|
|
3460
|
+
return {};
|
|
3461
|
+
}
|
|
3462
|
+
}
|
|
3463
|
+
_validateApp(value) {
|
|
3464
|
+
const schema = this.options.schema;
|
|
3465
|
+
if (schema == null) return value;
|
|
3466
|
+
const result = schema["~standard"].validate(value);
|
|
3467
|
+
if (result instanceof Promise) return void 0;
|
|
3468
|
+
if (result.issues != null) return void 0;
|
|
3469
|
+
return result.value;
|
|
3470
|
+
}
|
|
3471
|
+
};
|
|
3472
|
+
//#endregion
|
|
3473
|
+
//#region src/ActionRuntime/Handler/Server/ActionServerHandler.ts
|
|
3474
|
+
/**
|
|
3475
|
+
* Server-side handler for backends that accept many client connections over a single open channel
|
|
3476
|
+
* (WebSockets, Durable Objects, …). It is transport-agnostic: you feed it inbound frames with
|
|
3477
|
+
* {@link receive} and tell it how to write outbound frames via the `send` option.
|
|
3478
|
+
*
|
|
3479
|
+
* Add it alongside your local execution handler:
|
|
3480
|
+
* ```ts
|
|
3481
|
+
* const serverHandler = createServerHandler({ clientEnv, formatMessage, send: (ws, f) => ws.send(f) });
|
|
3482
|
+
* runtime.addHandlers([localHandler, serverHandler]);
|
|
3483
|
+
* // per inbound message (e.g. a Durable Object's webSocketMessage):
|
|
3484
|
+
* serverHandler.receive(ws, message);
|
|
3485
|
+
* ```
|
|
3486
|
+
*
|
|
3487
|
+
* Inbound requests route to your local handler; the runtime's return dispatch then calls this
|
|
3488
|
+
* handler back (it is an external handler keyed to `clientEnv`) to send the result to the originating
|
|
3489
|
+
* connection. The handler keeps a per-connection identity registry so each result lands on the right
|
|
3490
|
+
* socket, and remembers each connection's encoding so binary and JSON clients can share the channel.
|
|
3491
|
+
*
|
|
3492
|
+
* It registers an empty action router, so it is never chosen to *execute* an inbound request — only
|
|
3493
|
+
* to ferry results/pushes back out.
|
|
3494
|
+
*/
|
|
3495
|
+
var ActionServerHandler = class extends ActionExternalClientHandler {
|
|
3496
|
+
_formatMessage;
|
|
3497
|
+
_createFormatMessage;
|
|
3498
|
+
_send;
|
|
3499
|
+
_runtime;
|
|
3500
|
+
_serverTimeout;
|
|
3501
|
+
_onConnectionBound;
|
|
3502
|
+
/** Incoming-data listeners installed by the runtime (`resolveIncomingActionPayload`). */
|
|
3503
|
+
_incomingListeners = [];
|
|
3504
|
+
_security;
|
|
3505
|
+
/** Normalized accepted levels; whether `none` (plain) is allowed; whether any level needs a handshake. */
|
|
3506
|
+
_allowedLevels;
|
|
3507
|
+
_noneAllowed;
|
|
3508
|
+
_handshakeMode;
|
|
3509
|
+
_connByClient = /* @__PURE__ */ new Map();
|
|
3510
|
+
_clientByConn = /* @__PURE__ */ new Map();
|
|
3511
|
+
_connEncoding = /* @__PURE__ */ new Map();
|
|
3512
|
+
_codecByConn = /* @__PURE__ */ new Map();
|
|
3513
|
+
_handshakeByConn = /* @__PURE__ */ new Map();
|
|
3514
|
+
_cryptoByConn = /* @__PURE__ */ new Map();
|
|
3515
|
+
_authedConns = /* @__PURE__ */ new Set();
|
|
3516
|
+
_plainConns = /* @__PURE__ */ new Set();
|
|
3517
|
+
_inboundChainByConn = /* @__PURE__ */ new Map();
|
|
3518
|
+
_outboundChainByConn = /* @__PURE__ */ new Map();
|
|
3519
|
+
constructor(options) {
|
|
3520
|
+
super({
|
|
3521
|
+
runtimeCoordinate: options.clientEnv,
|
|
3522
|
+
transports: []
|
|
3523
|
+
});
|
|
3524
|
+
this._formatMessage = options.formatMessage;
|
|
3525
|
+
this._createFormatMessage = options.createFormatMessage;
|
|
3526
|
+
this._send = options.send;
|
|
3527
|
+
this._runtime = options.runtime;
|
|
3528
|
+
this._serverTimeout = options.defaultTimeout ?? 1e4;
|
|
3529
|
+
this._onConnectionBound = options.onConnectionBound;
|
|
3530
|
+
this._security = options.security;
|
|
3531
|
+
this._allowedLevels = options.security == null ? [] : Array.isArray(options.security.securityLevel) ? options.security.securityLevel : [options.security.securityLevel];
|
|
3532
|
+
this._noneAllowed = this._allowedLevels.includes("none");
|
|
3533
|
+
this._handshakeMode = this._allowedLevels.some((level) => level !== "none");
|
|
3534
|
+
}
|
|
3535
|
+
/**
|
|
3536
|
+
* The codec for a connection: a per-connection session (cached) when a factory was provided, else
|
|
3537
|
+
* the single shared `formatMessage`.
|
|
3538
|
+
*/
|
|
3539
|
+
_codecFor(connection) {
|
|
3540
|
+
if (this._createFormatMessage != null) {
|
|
3541
|
+
let codec = this._codecByConn.get(connection);
|
|
3542
|
+
if (codec == null) {
|
|
3543
|
+
codec = this._createFormatMessage();
|
|
3544
|
+
this._codecByConn.set(connection, codec);
|
|
3545
|
+
}
|
|
3546
|
+
return codec;
|
|
3547
|
+
}
|
|
3548
|
+
if (this._formatMessage != null) return this._formatMessage;
|
|
3549
|
+
throw err_nice_transport.fromId("not_found", { actionId: "server-handler-codec (provide formatMessage or createFormatMessage)" });
|
|
3550
|
+
}
|
|
3551
|
+
_setIncomingActionDataListener(listener) {
|
|
3552
|
+
this._incomingListeners.push(listener);
|
|
3553
|
+
}
|
|
3554
|
+
/**
|
|
3555
|
+
* Register (or replace) the connection-bound persistence callback after construction. Used by
|
|
3556
|
+
* lifecycle helpers like {@link createHibernatableWsServerAdapter} so persistence and replay are
|
|
3557
|
+
* owned by one place instead of being split across the constructor options.
|
|
3558
|
+
*/
|
|
3559
|
+
setOnConnectionBound(onConnectionBound) {
|
|
3560
|
+
this._onConnectionBound = onConnectionBound;
|
|
3561
|
+
}
|
|
3562
|
+
/**
|
|
3563
|
+
* Create a typed per-connection state store that co-owns the consumer's app state and this handler's
|
|
3564
|
+
* routing binding in one attachment. It registers itself as the connection-bound persistence callback
|
|
3565
|
+
* (so bindings are written without overwriting app state) and immediately replays every live
|
|
3566
|
+
* connection's stored binding via {@link rehydrateConnection} — so on a transport that resumes after
|
|
3567
|
+
* eviction (e.g. a Durable Object waking from hibernation) both the app identity and the action
|
|
3568
|
+
* routing come back from a single attachment, with no storage reads and no hand-rolled merge.
|
|
3569
|
+
*
|
|
3570
|
+
* This supersedes {@link createHibernatableWsServerAdapter} for app code that also pins its own state
|
|
3571
|
+
* to the connection. Construct it once when the handler is built, then `get`/`set` app state directly.
|
|
3572
|
+
*/
|
|
3573
|
+
createConnectionState(options) {
|
|
3574
|
+
const store = new WsConnectionStateStore(options);
|
|
3575
|
+
this.setOnConnectionBound((connection, binding) => store._persistBinding(connection, binding));
|
|
3576
|
+
for (const connection of options.getConnections()) {
|
|
3577
|
+
const binding = store._readBinding(connection);
|
|
3578
|
+
if (binding != null) this.rehydrateConnection(connection, binding);
|
|
3579
|
+
}
|
|
3580
|
+
return store;
|
|
3581
|
+
}
|
|
3582
|
+
/**
|
|
3583
|
+
* Feed one inbound frame from a connection into the runtime. Decodes text or binary, binds the
|
|
3584
|
+
* connection to the requesting client's identity, then routes it (requests execute locally;
|
|
3585
|
+
* results/progress resolve pending server-initiated actions).
|
|
3586
|
+
*/
|
|
3587
|
+
receive(connection, frame) {
|
|
3588
|
+
if (this._security == null || !this._handshakeMode) {
|
|
3589
|
+
this._receivePlain(connection, frame);
|
|
3590
|
+
return;
|
|
3591
|
+
}
|
|
3592
|
+
const next = (this._inboundChainByConn.get(connection) ?? Promise.resolve()).then(() => this._receiveSecure(connection, frame)).catch((err) => console.error("[ws-server] failed to process inbound frame", err));
|
|
3593
|
+
this._inboundChainByConn.set(connection, next);
|
|
3594
|
+
}
|
|
3595
|
+
_receivePlain(connection, frame) {
|
|
3596
|
+
const wire = decodeActionFrame(frame, this._codecFor(connection));
|
|
3597
|
+
if (wire == null) return;
|
|
3598
|
+
const encoding = typeof frame === "string" ? "json" : "binary";
|
|
3599
|
+
this._connEncoding.set(connection, encoding);
|
|
3600
|
+
if (wire.type === "request") this._resolveRequestIdentity(connection, wire, encoding);
|
|
3601
|
+
for (const listener of this._incomingListeners) listener(wire);
|
|
3602
|
+
}
|
|
3603
|
+
async _receiveSecure(connection, frame) {
|
|
3604
|
+
const security = this._security;
|
|
3605
|
+
if (security == null) return;
|
|
3606
|
+
if (this._plainConns.has(connection)) {
|
|
3607
|
+
this._receivePlain(connection, frame);
|
|
3608
|
+
return;
|
|
3609
|
+
}
|
|
3610
|
+
if (!this._authedConns.has(connection)) {
|
|
3611
|
+
const message = typeof frame === "string" ? decodeHandshakeMessage(frame) : void 0;
|
|
3612
|
+
if (message == null) {
|
|
3613
|
+
if (this._noneAllowed) {
|
|
3614
|
+
this._plainConns.add(connection);
|
|
3615
|
+
this._receivePlain(connection, frame);
|
|
3616
|
+
}
|
|
3617
|
+
return;
|
|
3618
|
+
}
|
|
3619
|
+
await security.link.initialize();
|
|
3620
|
+
let handshake = this._handshakeByConn.get(connection);
|
|
3621
|
+
if (handshake == null) {
|
|
3622
|
+
handshake = createServerHandshake({
|
|
3623
|
+
link: security.link,
|
|
3624
|
+
localCoordinate: security.localCoordinate,
|
|
3625
|
+
dictionaryVersion: security.dictionaryVersion,
|
|
3626
|
+
securityLevel: security.securityLevel,
|
|
3627
|
+
verifyKeyResolver: security.verifyKeyResolver
|
|
3628
|
+
});
|
|
3629
|
+
this._handshakeByConn.set(connection, handshake);
|
|
3630
|
+
}
|
|
3631
|
+
if (message.t === "hello") this._send(connection, encodeHandshakeMessage(await handshake.onHello(message)));
|
|
3632
|
+
else if (message.t === "prove") {
|
|
3633
|
+
const reply = await handshake.onProve(message);
|
|
3634
|
+
this._send(connection, encodeHandshakeMessage(reply));
|
|
3635
|
+
const result = handshake.getResult();
|
|
3636
|
+
if (reply.t === "accept" && result != null) this._completeServerHandshake(connection, result);
|
|
3637
|
+
}
|
|
3638
|
+
return;
|
|
3639
|
+
}
|
|
3640
|
+
let bytes = frame;
|
|
3641
|
+
const cryptoReady = this._cryptoByConn.get(connection);
|
|
3642
|
+
if (cryptoReady != null) try {
|
|
3643
|
+
bytes = await (await cryptoReady).decryptFrame(frame);
|
|
3644
|
+
} catch (err) {
|
|
3645
|
+
console.error("[ws-server] failed to decrypt inbound frame", err);
|
|
3646
|
+
return;
|
|
3647
|
+
}
|
|
3648
|
+
const wire = decodeActionFrame(bytes, this._codecFor(connection));
|
|
3649
|
+
if (wire == null) return;
|
|
3650
|
+
if (wire.type === "request") {
|
|
3651
|
+
const bound = this._clientByConn.get(connection);
|
|
3652
|
+
if (bound != null) wire.context.originClient = bound.toJsonObject();
|
|
3653
|
+
}
|
|
3654
|
+
for (const listener of this._incomingListeners) listener(wire);
|
|
3655
|
+
}
|
|
3656
|
+
_completeServerHandshake(connection, result) {
|
|
3657
|
+
const clientCoord = new RuntimeCoordinate(result.remote);
|
|
3658
|
+
this._bindConnection(connection, clientCoord);
|
|
3659
|
+
this._connEncoding.set(connection, "binary");
|
|
3660
|
+
this._authedConns.add(connection);
|
|
3661
|
+
this._handshakeByConn.delete(connection);
|
|
3662
|
+
if (result.securityLevel === "encrypted" && this._security != null) this._cryptoByConn.set(connection, Promise.resolve(createActionFrameCrypto({
|
|
3663
|
+
link: this._security.link,
|
|
3664
|
+
linkedClientId: result.linkedClientId
|
|
3665
|
+
})));
|
|
3666
|
+
this._onConnectionBound?.(connection, {
|
|
3667
|
+
client: clientCoord.toJsonObject(),
|
|
3668
|
+
encoding: "binary",
|
|
3669
|
+
secure: {
|
|
3670
|
+
securityLevel: result.securityLevel,
|
|
3671
|
+
linkedClientId: result.linkedClientId,
|
|
3672
|
+
keyMaterial: result.encryptionKeyMaterial
|
|
3673
|
+
}
|
|
3674
|
+
});
|
|
3675
|
+
}
|
|
3676
|
+
/**
|
|
3677
|
+
* Ensure an inbound request carries the client's identity and that this connection is bound to it,
|
|
3678
|
+
* so its result can be routed back. A session codec omits `originClient` after the first request, so
|
|
3679
|
+
* when it's missing we restore it from the (possibly rehydrated) binding instead. (Plain mode only;
|
|
3680
|
+
* secure mode binds the authenticated coordinate at handshake time.)
|
|
3681
|
+
*/
|
|
3682
|
+
_resolveRequestIdentity(connection, wire, encoding) {
|
|
3683
|
+
const wireOrigin = wire.context.originClient;
|
|
3684
|
+
if (wireOrigin != null && wireOrigin.envId !== "_unset_") {
|
|
3685
|
+
const clientCoord = new RuntimeCoordinate(wireOrigin);
|
|
3686
|
+
const isNewBinding = this._clientByConn.get(connection)?.stringId !== clientCoord.stringId;
|
|
3687
|
+
this._bindConnection(connection, clientCoord);
|
|
3688
|
+
if (isNewBinding) this._onConnectionBound?.(connection, {
|
|
3689
|
+
client: clientCoord.toJsonObject(),
|
|
3690
|
+
encoding
|
|
3691
|
+
});
|
|
3692
|
+
return;
|
|
3693
|
+
}
|
|
3694
|
+
const bound = this._clientByConn.get(connection);
|
|
3695
|
+
if (bound != null) wire.context.originClient = bound.toJsonObject();
|
|
3696
|
+
}
|
|
3697
|
+
/**
|
|
3698
|
+
* Restore a connection→client binding without an inbound frame — for transports that resume after
|
|
3699
|
+
* eviction. Pair it with the {@link IActionServerHandlerOptions.onConnectionBound} hook: persist
|
|
3700
|
+
* the binding there, then replay each live connection here when the channel comes back (e.g. a
|
|
3701
|
+
* Durable Object iterating `ctx.getWebSockets()` as it wakes from hibernation).
|
|
3702
|
+
*/
|
|
3703
|
+
rehydrateConnection(connection, binding) {
|
|
3704
|
+
this._bindConnection(connection, new RuntimeCoordinate(binding.client));
|
|
3705
|
+
this._connEncoding.set(connection, binding.encoding);
|
|
3706
|
+
const secure = binding.secure;
|
|
3707
|
+
if (secure == null) return;
|
|
3708
|
+
this._authedConns.add(connection);
|
|
3709
|
+
if (secure.securityLevel === "encrypted" && secure.keyMaterial != null && this._security != null) {
|
|
3710
|
+
const security = this._security;
|
|
3711
|
+
const { linkedClientId, keyMaterial } = secure;
|
|
3712
|
+
const cryptoReady = security.link.initialize().then(() => security.link.linkClient({
|
|
3713
|
+
linkedClientId,
|
|
3714
|
+
verifyPublicKey: keyMaterial.verifyPublicKey,
|
|
3715
|
+
exchangePublicKey: keyMaterial.exchangePublicKey,
|
|
3716
|
+
saltString: keyMaterial.saltString,
|
|
3717
|
+
infoString: keyMaterial.infoString,
|
|
3718
|
+
bindVerifyKeysIntoDerivation: keyMaterial.bindVerifyKeysIntoDerivation
|
|
3719
|
+
})).then(() => createActionFrameCrypto({
|
|
3720
|
+
link: security.link,
|
|
3721
|
+
linkedClientId
|
|
3722
|
+
}));
|
|
3723
|
+
this._cryptoByConn.set(connection, cryptoReady);
|
|
3724
|
+
const gate = cryptoReady.then(() => {}, (err) => console.error("[ws-server] failed to restore encrypted session", err));
|
|
3725
|
+
this._inboundChainByConn.set(connection, gate);
|
|
3726
|
+
this._outboundChainByConn.set(connection, gate);
|
|
3727
|
+
}
|
|
3728
|
+
}
|
|
3729
|
+
toHandlerRouteItem() {
|
|
3730
|
+
return {
|
|
3731
|
+
type: this.handlerType,
|
|
3732
|
+
client: this.externalClient,
|
|
3733
|
+
transType: "custom",
|
|
3734
|
+
transOrd: 0
|
|
3735
|
+
};
|
|
3736
|
+
}
|
|
3737
|
+
/** Forget a connection (call on socket close) so stale entries don't misroute later results. */
|
|
3738
|
+
dropConnection(connection) {
|
|
3739
|
+
const coord = this._clientByConn.get(connection);
|
|
3740
|
+
if (coord != null && this._connByClient.get(coord.stringId) === connection) this._connByClient.delete(coord.stringId);
|
|
3741
|
+
this._clientByConn.delete(connection);
|
|
3742
|
+
this._connEncoding.delete(connection);
|
|
3743
|
+
this._codecByConn.delete(connection);
|
|
3744
|
+
this._handshakeByConn.delete(connection);
|
|
3745
|
+
this._cryptoByConn.delete(connection);
|
|
3746
|
+
this._authedConns.delete(connection);
|
|
3747
|
+
this._plainConns.delete(connection);
|
|
3748
|
+
this._inboundChainByConn.delete(connection);
|
|
3749
|
+
this._outboundChainByConn.delete(connection);
|
|
3750
|
+
}
|
|
3751
|
+
/** Live connection for a client coordinate, if currently registered. */
|
|
3752
|
+
getConnectionForClient(client) {
|
|
3753
|
+
return this._connByClient.get(client.stringId);
|
|
3754
|
+
}
|
|
3755
|
+
/**
|
|
3756
|
+
* Send (and optionally await) a server-initiated action to a specific connected client. Pass the
|
|
3757
|
+
* connection token directly (e.g. the `ws`) or a client `RuntimeCoordinate` to look one up.
|
|
3758
|
+
*/
|
|
3759
|
+
pushToClient(runtime, target, request, options) {
|
|
3760
|
+
const connection = this._resolveConnection(target);
|
|
3761
|
+
return this._dispatch(runtime, connection, request, options?.timeout);
|
|
3762
|
+
}
|
|
3763
|
+
/**
|
|
3764
|
+
* Build a local handler whose cases are connection-aware: each case receives the primed request and
|
|
3765
|
+
* the originating client's live connection (resolved from `originClient`), so handlers don't repeat
|
|
3766
|
+
* the `getConnectionForClient(action.context.originClient)` lookup. Cases may return raw output or
|
|
3767
|
+
* nothing, just like {@link ActionLocalHandler.forDomainActionCases}. Add the returned handler to the
|
|
3768
|
+
* runtime alongside this server handler:
|
|
3769
|
+
* ```ts
|
|
3770
|
+
* runtime.addHandlers([serverHandler.forConnectionDomainCases(domain, { … }), serverHandler]);
|
|
3771
|
+
* ```
|
|
3772
|
+
*/
|
|
3773
|
+
forConnectionDomainCases(domain, cases) {
|
|
3774
|
+
const handler = new ActionLocalHandler();
|
|
3775
|
+
const executor = cases;
|
|
3776
|
+
const wrapped = {};
|
|
3777
|
+
const wrappedRecord = wrapped;
|
|
3778
|
+
for (const id in cases) {
|
|
3779
|
+
const caseFn = executor[id];
|
|
3780
|
+
if (caseFn == null) continue;
|
|
3781
|
+
wrappedRecord[id] = (request) => caseFn(request, this.getConnectionForClient(request.context.originClient));
|
|
3782
|
+
}
|
|
3783
|
+
return handler.forDomainActionCases(domain, wrapped);
|
|
3784
|
+
}
|
|
3785
|
+
/**
|
|
3786
|
+
* Fan a server-initiated request out to every currently-bound connection. A fresh request is built
|
|
3787
|
+
* per connection (each push mutates its own action context) and dispatched fire-and-forget. Pass
|
|
3788
|
+
* `except` to skip the originating socket and `where` to filter by connection (e.g. read its
|
|
3789
|
+
* attachment for a role). Iterating bound connections (rather than every accepted socket) skips
|
|
3790
|
+
* sockets that are still mid-handshake and so can't yet receive a frame.
|
|
3791
|
+
*/
|
|
3792
|
+
broadcast(makeRequest, options) {
|
|
3793
|
+
const runtime = options?.runtime ?? this._runtime;
|
|
3794
|
+
if (runtime == null) throw err_nice_transport.fromId("not_found", { actionId: "server-handler-runtime (construct with `runtime` or pass `options.runtime`)" });
|
|
3795
|
+
for (const connection of this._clientByConn.keys()) {
|
|
3796
|
+
if (options?.except != null && connection === options.except) continue;
|
|
3797
|
+
if (options?.where != null && !options.where(connection)) continue;
|
|
3798
|
+
try {
|
|
3799
|
+
this.pushToClient(runtime, connection, makeRequest(), { timeout: options?.timeout });
|
|
3800
|
+
} catch (error) {
|
|
3801
|
+
if (options?.onError != null) options.onError(error, connection);
|
|
3802
|
+
else console.error("[ws-server] broadcast push failed", error);
|
|
3803
|
+
}
|
|
3804
|
+
}
|
|
3805
|
+
}
|
|
3806
|
+
async sendReturnPayload(payload, config) {
|
|
3807
|
+
const connection = this._connByClient.get(payload.context.originClient.stringId);
|
|
3808
|
+
if (connection == null) return false;
|
|
3809
|
+
this._sendPayload(connection, payload, config.targetLocalRuntime.coordinate);
|
|
3810
|
+
return true;
|
|
3811
|
+
}
|
|
3812
|
+
async handleActionRequest(action, config) {
|
|
3813
|
+
const runtime = config?.targetLocalRuntime ?? ActionRuntime.getDefault();
|
|
3814
|
+
const connection = this._resolveSingleConnection();
|
|
3815
|
+
return this._dispatch(runtime, connection, action, config?.timeout);
|
|
3816
|
+
}
|
|
3817
|
+
_dispatch(runtime, connection, action, timeout) {
|
|
3818
|
+
const timeoutMs = timeout ?? this._serverTimeout;
|
|
3819
|
+
action.context._setOriginClient(runtime.coordinate);
|
|
3820
|
+
action.context.addRouteItem({
|
|
3821
|
+
runtime: runtime.coordinate,
|
|
3822
|
+
handler: this.toHandlerRouteItem(),
|
|
3823
|
+
time: Date.now()
|
|
3824
|
+
});
|
|
3825
|
+
const runningAction = new RunningAction({
|
|
3826
|
+
context: action.context,
|
|
3827
|
+
request: action,
|
|
3828
|
+
parentCuid: peekHandlerCuid(),
|
|
3829
|
+
callSite: action._callSite
|
|
3830
|
+
});
|
|
3831
|
+
runtime.registerRunningAction(runningAction);
|
|
3832
|
+
const timeoutId = setTimeout(() => {
|
|
3833
|
+
runningAction._abort(err_nice_transport.fromId("timeout", { timeout: timeoutMs }));
|
|
3834
|
+
}, timeoutMs);
|
|
3835
|
+
runningAction.addUpdateListeners([(update) => {
|
|
3836
|
+
if (update.type === "finished") clearTimeout(timeoutId);
|
|
3837
|
+
}]);
|
|
3838
|
+
try {
|
|
3839
|
+
this._sendPayload(connection, action, runtime.coordinate);
|
|
3840
|
+
} catch (err) {
|
|
3841
|
+
runningAction._abort(err);
|
|
3842
|
+
}
|
|
3843
|
+
return runningAction;
|
|
3844
|
+
}
|
|
3845
|
+
_sendPayload(connection, payload, localClient) {
|
|
3846
|
+
const frame = (this._connEncoding.get(connection) ?? "binary") === "json" ? JSON.stringify(payload.toJsonObject()) : this._codecFor(connection).outgoing({
|
|
3847
|
+
action: payload,
|
|
3848
|
+
localClient,
|
|
3849
|
+
externalClient: this.externalClient
|
|
3850
|
+
});
|
|
3851
|
+
const cryptoReady = this._cryptoByConn.get(connection);
|
|
3852
|
+
if (cryptoReady == null) {
|
|
3853
|
+
this._send(connection, frame);
|
|
3854
|
+
return;
|
|
3855
|
+
}
|
|
3856
|
+
const bytes = toFrameBytes(frame);
|
|
3857
|
+
const next = (this._outboundChainByConn.get(connection) ?? Promise.resolve()).then(() => cryptoReady).then((crypto) => crypto.encryptFrame(bytes)).then((encrypted) => this._send(connection, encrypted)).catch((err) => console.error("[ws-server] failed to encrypt/send frame", err));
|
|
3858
|
+
this._outboundChainByConn.set(connection, next);
|
|
3859
|
+
}
|
|
3860
|
+
_bindConnection(connection, client) {
|
|
3861
|
+
this._connByClient.set(client.stringId, connection);
|
|
3862
|
+
this._clientByConn.set(connection, client);
|
|
3863
|
+
}
|
|
3864
|
+
_resolveConnection(target) {
|
|
3865
|
+
if (target instanceof RuntimeCoordinate) {
|
|
3866
|
+
const connection = this._connByClient.get(target.stringId);
|
|
3867
|
+
if (connection == null) throw err_nice_transport.fromId("not_found", { actionId: target.stringId });
|
|
3868
|
+
return connection;
|
|
3869
|
+
}
|
|
3870
|
+
return target;
|
|
3871
|
+
}
|
|
3872
|
+
_resolveSingleConnection() {
|
|
3873
|
+
if (this._clientByConn.size !== 1) throw err_nice_transport.fromId("not_found", { actionId: "server-handler-target (use pushToClient with an explicit connection or client coordinate)" });
|
|
3874
|
+
return this._clientByConn.keys().next().value;
|
|
3875
|
+
}
|
|
3876
|
+
};
|
|
3877
|
+
const createServerHandler = (options) => {
|
|
3878
|
+
return new ActionServerHandler(options);
|
|
3879
|
+
};
|
|
3880
|
+
//#endregion
|
|
3881
|
+
//#region src/ActionRuntime/Handler/Server/createActionFetchHandler.ts
|
|
3882
|
+
/** Permissive defaults — fine for a public action endpoint; override (or disable) via `cors`. */
|
|
3883
|
+
const DEFAULT_CORS_HEADERS = {
|
|
3884
|
+
"Access-Control-Allow-Origin": "*",
|
|
3885
|
+
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
|
|
3886
|
+
"Access-Control-Allow-Headers": "Content-Type",
|
|
3887
|
+
"Access-Control-Max-Age": "86400"
|
|
3888
|
+
};
|
|
3889
|
+
/**
|
|
3890
|
+
* Build the `fetch` handler a server/Durable-Object exposes for action traffic, folding in the
|
|
3891
|
+
* boilerplate every endpoint repeats: CORS (incl. the `OPTIONS` preflight), routing the `/action`
|
|
3892
|
+
* `POST` body through the runtime (`handleActionPayloadWire` → `waitForResultPayload` →
|
|
3893
|
+
* `toHttpResponse`), an optional WebSocket-upgrade hook, and a `404` fallback.
|
|
3894
|
+
*
|
|
3895
|
+
* It only touches web-standard `Request`/`Response`, so it stays transport-agnostic — the one
|
|
3896
|
+
* environment-specific bit (the WS upgrade) is injected via {@link IActionFetchHandlerOptions.onWebSocketUpgrade}:
|
|
3897
|
+
* ```ts
|
|
3898
|
+
* this.fetchHandler = createActionFetchHandler(this.runtime, {
|
|
3899
|
+
* onWebSocketUpgrade: () => {
|
|
3900
|
+
* const pair = new WebSocketPair();
|
|
3901
|
+
* this.ctx.acceptWebSocket(pair[1]);
|
|
3902
|
+
* return new Response(null, { status: 101, webSocket: pair[0] });
|
|
3903
|
+
* },
|
|
3904
|
+
* });
|
|
3905
|
+
* // async fetch(request) { return this.fetchHandler(request); }
|
|
3906
|
+
* ```
|
|
3907
|
+
*/
|
|
3908
|
+
function createActionFetchHandler(runtime, options = {}) {
|
|
3909
|
+
const corsHeaders = options.cors === false ? {} : options.cors ?? DEFAULT_CORS_HEADERS;
|
|
3910
|
+
const isActionPath = options.isActionPath ?? ((url) => url.pathname.endsWith("/action"));
|
|
3911
|
+
const isWebSocketPath = options.isWebSocketPath ?? ((url) => url.pathname.endsWith("/ws"));
|
|
3912
|
+
const withCors = (response) => {
|
|
3913
|
+
if (options.cors === false) return response;
|
|
3914
|
+
const headers = new Headers(response.headers);
|
|
3915
|
+
for (const [key, value] of Object.entries(corsHeaders)) headers.set(key, value);
|
|
3916
|
+
return new Response(response.body, {
|
|
3917
|
+
status: response.status,
|
|
3918
|
+
headers
|
|
3919
|
+
});
|
|
3920
|
+
};
|
|
3921
|
+
return async (request) => {
|
|
3922
|
+
if (request.method === "OPTIONS") return withCors(new Response(null, { status: 204 }));
|
|
3923
|
+
const url = new URL(request.url);
|
|
3924
|
+
if (options.onWebSocketUpgrade != null && request.headers.get("Upgrade") === "websocket" && isWebSocketPath(url)) return options.onWebSocketUpgrade(request, url);
|
|
3925
|
+
if (request.method === "POST" && isActionPath(url)) return withCors((await (await runtime.handleActionPayloadWire(await request.json())).waitForResultPayload()).toHttpResponse({ useErrorStatus: options.useErrorStatus }));
|
|
3926
|
+
return withCors(new Response("Not found", { status: 404 }));
|
|
3927
|
+
};
|
|
3928
|
+
}
|
|
3929
|
+
//#endregion
|
|
3930
|
+
//#region src/ActionRuntime/Handler/Server/createSecureActionServer.ts
|
|
3931
|
+
/** Default accepted set: negotiate per connection to whatever the client picks. */
|
|
3932
|
+
const DEFAULT_SERVER_SECURITY_LEVELS = [
|
|
3933
|
+
"none",
|
|
3934
|
+
"authenticated",
|
|
3935
|
+
"encrypted"
|
|
3936
|
+
];
|
|
3937
|
+
/**
|
|
3938
|
+
* Build an {@link ActionServerHandler} for the secure binary channel with the boilerplate folded in:
|
|
3939
|
+
* it creates the {@link ClientCryptoKeyLink} and the storage-backed TOFU resolver from a single
|
|
3940
|
+
* `storageAdapter`, installs the channel's per-connection codec, and assembles the `security` block
|
|
3941
|
+
* from the runtime coordinate + channel version (accepting all three levels by default).
|
|
3942
|
+
*
|
|
3943
|
+
* For a hibernatable transport (e.g. a Durable Object), pair it with
|
|
3944
|
+
* {@link createHibernatableWsServerAdapter} to wire persistence + replay.
|
|
3945
|
+
*/
|
|
3946
|
+
function createSecureActionServerHandler(options) {
|
|
3947
|
+
const link = new _nice_code_util.ClientCryptoKeyLink({ storageAdapter: options.storageAdapter });
|
|
3948
|
+
return new ActionServerHandler({
|
|
3949
|
+
clientEnv: options.clientEnv,
|
|
3950
|
+
createFormatMessage: options.channel.createCodec,
|
|
3951
|
+
send: options.send,
|
|
3952
|
+
runtime: options.runtime,
|
|
3953
|
+
defaultTimeout: options.defaultTimeout,
|
|
3954
|
+
security: {
|
|
3955
|
+
securityLevel: options.securityLevel ?? DEFAULT_SERVER_SECURITY_LEVELS,
|
|
3956
|
+
link,
|
|
3957
|
+
localCoordinate: options.runtime.coordinate.toJsonObject(),
|
|
3958
|
+
dictionaryVersion: options.channel.dictionaryVersion,
|
|
3959
|
+
verifyKeyResolver: options.verifyKeyResolver ?? createStorageTofuVerifyKeyResolver(options.storageAdapter)
|
|
3960
|
+
}
|
|
3961
|
+
});
|
|
3962
|
+
}
|
|
3963
|
+
/**
|
|
3964
|
+
* Wire the hibernation lifecycle for a server handler on a transport whose sockets outlive process
|
|
3965
|
+
* eviction (e.g. a Durable Object's hibernatable WebSockets). It owns persistence end to end:
|
|
3966
|
+
* registers `setAttachment` as the handler's connection-bound callback and immediately replays every
|
|
3967
|
+
* live connection's stored binding via `getAttachment`, so results/pushes still route after a wake.
|
|
3968
|
+
*
|
|
3969
|
+
* Construct it once when the handler is built, then forward socket events:
|
|
3970
|
+
* ```ts
|
|
3971
|
+
* const wsServer = createHibernatableWsServerAdapter({ handler, getWebSockets, getAttachment, setAttachment });
|
|
3972
|
+
* // webSocketMessage(ws, msg) => wsServer.receive(ws, msg);
|
|
3973
|
+
* // webSocketClose/Error(ws) => wsServer.drop(ws);
|
|
3974
|
+
* ```
|
|
3975
|
+
*/
|
|
3976
|
+
function createHibernatableWsServerAdapter(options) {
|
|
3977
|
+
const { handler, getWebSockets, getAttachment, setAttachment } = options;
|
|
3978
|
+
handler.setOnConnectionBound(setAttachment);
|
|
3979
|
+
for (const connection of getWebSockets()) {
|
|
3980
|
+
const binding = getAttachment(connection);
|
|
3981
|
+
if (binding != null) handler.rehydrateConnection(connection, binding);
|
|
3982
|
+
}
|
|
3983
|
+
return {
|
|
3984
|
+
receive: (connection, frame) => handler.receive(connection, frame),
|
|
3985
|
+
drop: (connection) => handler.dropConnection(connection)
|
|
3986
|
+
};
|
|
3987
|
+
}
|
|
3988
|
+
//#endregion
|
|
3989
|
+
exports.ActionCore = ActionCore;
|
|
3990
|
+
exports.ActionDomain = ActionDomain;
|
|
3991
|
+
exports.ActionExternalClientHandler = ActionExternalClientHandler;
|
|
3992
|
+
exports.ActionLocalHandler = ActionLocalHandler;
|
|
3993
|
+
exports.ActionRootDomain = ActionRootDomain;
|
|
3994
|
+
exports.ActionRuntime = ActionRuntime;
|
|
3995
|
+
exports.ActionSchema = ActionSchema;
|
|
3996
|
+
exports.ActionServerHandler = ActionServerHandler;
|
|
3997
|
+
exports.CustomTransport = CustomTransport;
|
|
3998
|
+
exports.EActionPayloadType = EActionPayloadType;
|
|
3999
|
+
exports.EActionProgressType = EActionProgressType;
|
|
4000
|
+
exports.EErrId_NiceAction = EErrId_NiceAction;
|
|
4001
|
+
exports.EErrId_NiceTransport = EErrId_NiceTransport;
|
|
4002
|
+
exports.EErrId_NiceTransport_WebSocket = EErrId_NiceTransport_WebSocket;
|
|
4003
|
+
exports.EHandshakeMessageType = EHandshakeMessageType;
|
|
4004
|
+
exports.ERunningActionFinishedType = require_RunningAction_types.ERunningActionFinishedType;
|
|
4005
|
+
exports.ERunningActionState = require_RunningAction_types.ERunningActionState;
|
|
4006
|
+
exports.ERunningActionUpdateType = require_RunningAction_types.ERunningActionUpdateType;
|
|
4007
|
+
exports.ESecurityLevel = ESecurityLevel;
|
|
4008
|
+
exports.ETransportStatus = ETransportStatus;
|
|
4009
|
+
exports.ETransportType = ETransportType;
|
|
4010
|
+
exports.HttpTransport = HttpTransport;
|
|
4011
|
+
exports.RunningAction = RunningAction;
|
|
4012
|
+
exports.RuntimeCoordinate = RuntimeCoordinate;
|
|
4013
|
+
exports.Transport = Transport;
|
|
4014
|
+
exports.WebSocketTransport = WebSocketTransport;
|
|
4015
|
+
exports.WsConnectionStateStore = WsConnectionStateStore;
|
|
4016
|
+
exports.actionSchema = actionSchema;
|
|
4017
|
+
exports.createActionFetchHandler = createActionFetchHandler;
|
|
4018
|
+
exports.createActionFrameCrypto = createActionFrameCrypto;
|
|
4019
|
+
exports.createActionRootDomain = createActionRootDomain;
|
|
4020
|
+
exports.createBinaryWsAdapter = createBinaryWsAdapter;
|
|
4021
|
+
exports.createBinaryWsSessionFactory = createBinaryWsSessionFactory;
|
|
4022
|
+
exports.createClientHandshake = createClientHandshake;
|
|
4023
|
+
exports.createExternalClientHandler = createExternalClientHandler;
|
|
4024
|
+
exports.createHibernatableWsServerAdapter = createHibernatableWsServerAdapter;
|
|
4025
|
+
exports.createInMemoryTofuVerifyKeyResolver = createInMemoryTofuVerifyKeyResolver;
|
|
4026
|
+
exports.createLocalHandler = createLocalHandler;
|
|
4027
|
+
exports.createSecureActionServerHandler = createSecureActionServerHandler;
|
|
4028
|
+
exports.createSecureWebSocketTransport = createSecureWebSocketTransport;
|
|
4029
|
+
exports.createServerHandler = createServerHandler;
|
|
4030
|
+
exports.createServerHandshake = createServerHandshake;
|
|
4031
|
+
exports.createStorageTofuVerifyKeyResolver = createStorageTofuVerifyKeyResolver;
|
|
4032
|
+
exports.decodeActionFrame = decodeActionFrame;
|
|
4033
|
+
exports.decodeHandshakeMessage = decodeHandshakeMessage;
|
|
4034
|
+
exports.defineSecureWsChannel = defineSecureWsChannel;
|
|
4035
|
+
exports.encodeHandshakeMessage = encodeHandshakeMessage;
|
|
4036
|
+
exports.err_nice_action = err_nice_action;
|
|
4037
|
+
exports.err_nice_external_client = err_nice_external_client;
|
|
4038
|
+
exports.err_nice_transport = err_nice_transport;
|
|
4039
|
+
exports.err_nice_transport_ws = err_nice_transport_ws;
|
|
4040
|
+
exports.isActionPayload_Any_JsonObject = isActionPayload_Any_JsonObject;
|
|
4041
|
+
exports.isActionPayload_Request_JsonObject = isActionPayload_Request_JsonObject;
|
|
4042
|
+
exports.isActionPayload_Result_JsonObject = isActionPayload_Result_JsonObject;
|
|
4043
|
+
exports.runtimeLinkId = runtimeLinkId;
|
|
4044
|
+
|
|
4045
|
+
//# sourceMappingURL=index.cjs.map
|