@nice-code/action 0.20.0 → 0.22.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/README.md +140 -109
- package/build/{ActionDevtoolsCore-D_JvgPmz.d.mts → ActionDevtoolsCore-CQ0vrvPD.d.cts} +2 -2
- package/build/{ActionDevtoolsCore-dV-IVPcP.d.cts → ActionDevtoolsCore-CiLBYC3K.d.mts} +2 -2
- package/build/{ActionPayload.types-CnfWlkA1.d.cts → ActionPayload.types-Dx1JPyfs.d.mts} +292 -222
- package/build/{ActionPayload.types-D0DM-g65.d.mts → ActionPayload.types-L9k0LyBd.d.cts} +292 -222
- package/build/devtools/browser/index.d.cts +1 -1
- package/build/devtools/browser/index.d.mts +1 -1
- package/build/devtools/server/index.d.cts +1 -1
- package/build/devtools/server/index.d.mts +1 -1
- package/build/httpAcceptorCarrier-DL8lf0xB.mjs +3906 -0
- package/build/httpAcceptorCarrier-DL8lf0xB.mjs.map +1 -0
- package/build/httpAcceptorCarrier-OnJxzsAD.cjs +4291 -0
- package/build/httpAcceptorCarrier-OnJxzsAD.cjs.map +1 -0
- package/build/index.cjs +395 -4125
- package/build/index.cjs.map +1 -1
- package/build/index.d.cts +2 -2
- package/build/index.d.mts +2 -2
- package/build/index.mjs +331 -4058
- package/build/index.mjs.map +1 -1
- package/build/platform/cloudflare/index.cjs +30 -2
- package/build/platform/cloudflare/index.cjs.map +1 -1
- package/build/platform/cloudflare/index.d.cts +55 -2
- package/build/platform/cloudflare/index.d.mts +55 -2
- package/build/platform/cloudflare/index.mjs +28 -2
- package/build/platform/cloudflare/index.mjs.map +1 -1
- package/build/react-query/index.d.cts +1 -1
- package/build/react-query/index.d.mts +1 -1
- package/package.json +4 -4
- package/build/wsAcceptorCarrier-BDJRIPfu.cjs +0 -103
- package/build/wsAcceptorCarrier-BDJRIPfu.cjs.map +0 -1
- package/build/wsAcceptorCarrier-CW2qX25W.mjs +0 -80
- package/build/wsAcceptorCarrier-CW2qX25W.mjs.map +0 -1
package/build/index.mjs
CHANGED
|
@@ -1,134 +1,8 @@
|
|
|
1
|
+
import { $ as EErrId_NiceAction, A as createServerHandshake, B as PeerLinkHandler, C as createAcceptorHandler, D as ESecurityLevel, E as EHandshakeMessageType, F as ActionLocalHandler, G as err_nice_external_client, H as ETransportStatus, I as createLocalHandler, J as ActionSchema, K as isActionPayload_Any_JsonObject, L as ActionRuntime, M as decodeHandshakeMessage, N as encodeHandshakeMessage, O as createClientHandshake, P as runtimeLinkId, Q as isAction_Base_JsonObject, R as ConnectorHandler, S as AcceptorHandler, T as decodeActionFrame, U as EErrId_NiceTransport, V as ETransportShape, W as err_nice_transport, X as actionSchema, Y as EActionResponseMode, Z as isActionPayload_Result_JsonObject, _ as decodeExchangeReply, a as isExchangeAcceptorCarrier, at as EActionProgressType, b as Transport, c as createConnectionStateStore, ct as RuntimeCoordinate, d as acceptChannel, et as err_nice_action, f as acceptChannelConnections, g as ExchangeTransport, h as LinkTransport, i as serveChannel, it as EActionPayloadType, j as createStorageTofuVerifyKeyResolver, k as createInMemoryTofuVerifyKeyResolver, l as createActionFetchHandler, lt as runtimeCoordinateToStringIds, m as defineChannel, n as wsAcceptorCarrier, nt as ActionPayload_Request, o as createHibernatableWsServerAdapter, ot as ActionPayload, p as connectChannel, q as isActionPayload_Request_JsonObject, r as serveHost, rt as ActionPayload_Result, s as ConnectionStateStore, st as ActionBase, t as httpAcceptorCarrier, tt as RunningAction, u as ExchangeAcceptor, v as decodeExchangeRequest, w as createActionFrameCrypto, x as createSecureAcceptorHandler, y as encodeExchange, z as createConnectorHandler } from "./httpAcceptorCarrier-DL8lf0xB.mjs";
|
|
1
2
|
import { n as ERunningActionState, r as ERunningActionUpdateType, t as ERunningActionFinishedType } from "./RunningAction.types-C176rqHG.mjs";
|
|
2
|
-
import { i as ETransportStatus, n as isExchangeAcceptorCarrier, r as ETransportShape, t as wsAcceptorCarrier } from "./wsAcceptorCarrier-CW2qX25W.mjs";
|
|
3
3
|
import { nanoid } from "nanoid";
|
|
4
|
-
import {
|
|
5
|
-
import { extractMessageFromStandardSchema } from "@nice-code/common-errors";
|
|
6
|
-
import { runtime } from "std-env";
|
|
7
|
-
import { ClientCryptoKeyLink, createTypedStorage } from "@nice-code/util";
|
|
8
|
-
import * as v from "valibot";
|
|
4
|
+
import { err } from "@nice-code/error";
|
|
9
5
|
import { pack, unpack } from "msgpackr";
|
|
10
|
-
const UNSET_RUNTIME_ENV_ID = "_unset_";
|
|
11
|
-
//#endregion
|
|
12
|
-
//#region src/ActionRuntime/utils/runtimeCoordinateToStringIds.ts
|
|
13
|
-
function runtimeCoordinateToStringIds(coordinate) {
|
|
14
|
-
return [
|
|
15
|
-
`envId[${coordinate.envId}]perId[${coordinate.perId ?? "_"}]:insId[${coordinate.insId ?? "_"}]`,
|
|
16
|
-
`envId[${coordinate.envId}]perId[${coordinate.perId ?? "_"}]:insId[_]`,
|
|
17
|
-
`envId[${coordinate.envId}]perId[_]:insId[_]`
|
|
18
|
-
];
|
|
19
|
-
}
|
|
20
|
-
//#endregion
|
|
21
|
-
//#region src/ActionRuntime/RuntimeCoordinate.ts
|
|
22
|
-
var RuntimeCoordinate = class RuntimeCoordinate {
|
|
23
|
-
envId;
|
|
24
|
-
perId;
|
|
25
|
-
insId;
|
|
26
|
-
static get unknown() {
|
|
27
|
-
return new RuntimeCoordinate({ envId: UNSET_RUNTIME_ENV_ID });
|
|
28
|
-
}
|
|
29
|
-
static env(envId) {
|
|
30
|
-
return new RuntimeCoordinate({ envId });
|
|
31
|
-
}
|
|
32
|
-
withPersistentId(perId) {
|
|
33
|
-
return this.specify({ perId });
|
|
34
|
-
}
|
|
35
|
-
constructor({ envId, perId, insId }) {
|
|
36
|
-
this.envId = envId;
|
|
37
|
-
this.perId = perId;
|
|
38
|
-
this.insId = insId;
|
|
39
|
-
}
|
|
40
|
-
specify(specifics) {
|
|
41
|
-
return new RuntimeCoordinate({
|
|
42
|
-
envId: this.envId,
|
|
43
|
-
perId: this.perId,
|
|
44
|
-
insId: this.insId,
|
|
45
|
-
...specifics
|
|
46
|
-
});
|
|
47
|
-
}
|
|
48
|
-
specifyIfUnset(specifics) {
|
|
49
|
-
return new RuntimeCoordinate({
|
|
50
|
-
envId: this.envId,
|
|
51
|
-
perId: this.perId ?? specifics.perId,
|
|
52
|
-
insId: this.insId ?? specifics.insId
|
|
53
|
-
});
|
|
54
|
-
}
|
|
55
|
-
toJsonObject() {
|
|
56
|
-
return {
|
|
57
|
-
envId: this.envId,
|
|
58
|
-
perId: this.perId,
|
|
59
|
-
insId: this.insId
|
|
60
|
-
};
|
|
61
|
-
}
|
|
62
|
-
isExactlySame(other) {
|
|
63
|
-
return this.envId === other.envId && this.perId === other.perId && this.insId === other.insId;
|
|
64
|
-
}
|
|
65
|
-
isSameFor(other) {
|
|
66
|
-
return {
|
|
67
|
-
id: this.envId === other.envId,
|
|
68
|
-
perId: this.envId === other.envId && this.perId === other.perId,
|
|
69
|
-
insId: this.envId === other.envId && this.perId === other.perId && this.insId === other.insId
|
|
70
|
-
};
|
|
71
|
-
}
|
|
72
|
-
similarityLevel(other) {
|
|
73
|
-
const sameFor = this.isSameFor(other);
|
|
74
|
-
if (sameFor.insId) return 3;
|
|
75
|
-
if (sameFor.perId) return 2;
|
|
76
|
-
if (sameFor.id) return 1;
|
|
77
|
-
return 0;
|
|
78
|
-
}
|
|
79
|
-
get stringId() {
|
|
80
|
-
return `envId[${this.envId}]perId[${this.perId ?? "_"}]:insId[${this.insId ?? "_"}]`;
|
|
81
|
-
}
|
|
82
|
-
/**
|
|
83
|
-
* Takes the "Runtime Coordinate" and generates a list of full coordinate IDs representing the runtime
|
|
84
|
-
* with decreasing levels of specificity.
|
|
85
|
-
*
|
|
86
|
-
* The first full coordinate ID is the most specific (including instance ID), while the last ID is
|
|
87
|
-
* the least specific (only environment ID).
|
|
88
|
-
*
|
|
89
|
-
* Example output for a RuntimeCoordinate with envId "web_app", perId "user123", and insId "instance456":
|
|
90
|
-
* [
|
|
91
|
-
* "envId[web_app]perId[user123]:insId[instance456]",
|
|
92
|
-
* "envId[web_app]perId[user123]:insId[_]",
|
|
93
|
-
* "envId[web_app]perId[_]:insId[_]"
|
|
94
|
-
* ]
|
|
95
|
-
*
|
|
96
|
-
* @returns a list of "full" runtime coordinate IDs with decreasing accuracy for targeting a runtime.
|
|
97
|
-
*/
|
|
98
|
-
toStringIds() {
|
|
99
|
-
return runtimeCoordinateToStringIds(this);
|
|
100
|
-
}
|
|
101
|
-
};
|
|
102
|
-
//#endregion
|
|
103
|
-
//#region src/ActionDefinition/Action/ActionBase.ts
|
|
104
|
-
var ActionBase = class {
|
|
105
|
-
form;
|
|
106
|
-
_domain;
|
|
107
|
-
id;
|
|
108
|
-
domain;
|
|
109
|
-
allDomains;
|
|
110
|
-
schema;
|
|
111
|
-
constructor(form, _domain, id) {
|
|
112
|
-
this.form = form;
|
|
113
|
-
this._domain = _domain;
|
|
114
|
-
this.id = id;
|
|
115
|
-
this.domain = _domain.domain;
|
|
116
|
-
this.allDomains = _domain.allDomains;
|
|
117
|
-
this.schema = _domain.actionSchema[id];
|
|
118
|
-
}
|
|
119
|
-
toJsonObject() {
|
|
120
|
-
return {
|
|
121
|
-
form: this.form,
|
|
122
|
-
domain: this.domain,
|
|
123
|
-
allDomains: this.allDomains,
|
|
124
|
-
id: this.id
|
|
125
|
-
};
|
|
126
|
-
}
|
|
127
|
-
toJsonString() {
|
|
128
|
-
return JSON.stringify(this.toJsonObject());
|
|
129
|
-
}
|
|
130
|
-
};
|
|
131
|
-
//#endregion
|
|
132
6
|
//#region src/ActionDefinition/Action/Context/ActionContext.ts
|
|
133
7
|
var ActionContext = class extends ActionBase {
|
|
134
8
|
_domain;
|
|
@@ -195,178 +69,6 @@ var ActionContext = class extends ActionBase {
|
|
|
195
69
|
}
|
|
196
70
|
};
|
|
197
71
|
//#endregion
|
|
198
|
-
//#region src/ActionDefinition/Action/Payload/ActionPayload.ts
|
|
199
|
-
var ActionPayload = class extends ActionBase {
|
|
200
|
-
form = "data";
|
|
201
|
-
type;
|
|
202
|
-
context;
|
|
203
|
-
time;
|
|
204
|
-
constructor(context, type, data) {
|
|
205
|
-
super("data", context._domain, context.id);
|
|
206
|
-
this.context = context;
|
|
207
|
-
this.type = type;
|
|
208
|
-
this.time = data.time;
|
|
209
|
-
}
|
|
210
|
-
toBaseJsonObject() {
|
|
211
|
-
return {
|
|
212
|
-
...super.toJsonObject(),
|
|
213
|
-
type: this.type,
|
|
214
|
-
context: this.context.toContextDataJsonObject(),
|
|
215
|
-
time: this.time
|
|
216
|
-
};
|
|
217
|
-
}
|
|
218
|
-
};
|
|
219
|
-
//#endregion
|
|
220
|
-
//#region src/utils/hashPayloadData.ts
|
|
221
|
-
function stableStringify(value) {
|
|
222
|
-
if (value === null || value === void 0) return String(value);
|
|
223
|
-
if (typeof value !== "object") return JSON.stringify(value) ?? "undefined";
|
|
224
|
-
if (Array.isArray(value)) return `[${value.map(stableStringify).join(",")}]`;
|
|
225
|
-
return "{" + Object.keys(value).sort().map((k) => `${JSON.stringify(k)}:${stableStringify(value[k])}`).join(",") + "}";
|
|
226
|
-
}
|
|
227
|
-
function fnv1a32(str) {
|
|
228
|
-
let hash = 2166136261;
|
|
229
|
-
for (let i = 0; i < str.length; i++) hash = (hash ^ str.charCodeAt(i)) * 16777619 >>> 0;
|
|
230
|
-
return hash.toString(16).padStart(8, "0");
|
|
231
|
-
}
|
|
232
|
-
/**
|
|
233
|
-
* Produces a deterministic 8-char hex hash of any JSON-serializable value.
|
|
234
|
-
* Useful for grouping/comparing action inputs, outputs, and progress payloads.
|
|
235
|
-
*/
|
|
236
|
-
function hashPayloadData(data) {
|
|
237
|
-
return fnv1a32(stableStringify(data));
|
|
238
|
-
}
|
|
239
|
-
//#endregion
|
|
240
|
-
//#region src/ActionDefinition/Action/Payload/ActionPayload.types.ts
|
|
241
|
-
let EActionPayloadType = /* @__PURE__ */ function(EActionPayloadType) {
|
|
242
|
-
EActionPayloadType["request"] = "request";
|
|
243
|
-
EActionPayloadType["progress"] = "progress";
|
|
244
|
-
EActionPayloadType["result"] = "result";
|
|
245
|
-
EActionPayloadType["stream"] = "stream";
|
|
246
|
-
EActionPayloadType["push"] = "push";
|
|
247
|
-
return EActionPayloadType;
|
|
248
|
-
}({});
|
|
249
|
-
/**
|
|
250
|
-
*
|
|
251
|
-
* [ PROGRESS ]
|
|
252
|
-
*
|
|
253
|
-
*/
|
|
254
|
-
let EActionProgressType = /* @__PURE__ */ function(EActionProgressType) {
|
|
255
|
-
EActionProgressType["none"] = "none";
|
|
256
|
-
EActionProgressType["percentage"] = "percentage";
|
|
257
|
-
EActionProgressType["custom"] = "custom";
|
|
258
|
-
return EActionProgressType;
|
|
259
|
-
}({});
|
|
260
|
-
//#endregion
|
|
261
|
-
//#region src/ActionDefinition/Action/Payload/ActionPayload_Progress.ts
|
|
262
|
-
var ActionPayload_Progress = class extends ActionPayload {
|
|
263
|
-
progress;
|
|
264
|
-
constructor(params, progress, data) {
|
|
265
|
-
super(params.context, "progress", data);
|
|
266
|
-
this.progress = progress;
|
|
267
|
-
}
|
|
268
|
-
toJsonObject() {
|
|
269
|
-
return {
|
|
270
|
-
...this.toBaseJsonObject(),
|
|
271
|
-
progress: this.progress
|
|
272
|
-
};
|
|
273
|
-
}
|
|
274
|
-
toJsonString() {
|
|
275
|
-
return JSON.stringify(this.toJsonObject());
|
|
276
|
-
}
|
|
277
|
-
toHttpResponse() {
|
|
278
|
-
return new Response(this.toJsonString(), {
|
|
279
|
-
status: 200,
|
|
280
|
-
headers: { "Content-Type": "application/json" }
|
|
281
|
-
});
|
|
282
|
-
}
|
|
283
|
-
};
|
|
284
|
-
//#endregion
|
|
285
|
-
//#region src/ActionDefinition/Action/Payload/ActionPayload_Result.ts
|
|
286
|
-
var ActionPayload_Result = class extends ActionPayload {
|
|
287
|
-
result;
|
|
288
|
-
outputHash;
|
|
289
|
-
constructor(params, result, data) {
|
|
290
|
-
super(params.context, "result", data);
|
|
291
|
-
this.result = result;
|
|
292
|
-
this.outputHash = result.ok ? hashPayloadData(this.context.schema.serializeOutput(result.output)) : hashPayloadData(result.error.message);
|
|
293
|
-
}
|
|
294
|
-
toJsonObject() {
|
|
295
|
-
const wireResult = this.result.ok ? {
|
|
296
|
-
ok: true,
|
|
297
|
-
output: this.context.schema.serializeOutput(this.result.output)
|
|
298
|
-
} : this.result;
|
|
299
|
-
return {
|
|
300
|
-
...this.toBaseJsonObject(),
|
|
301
|
-
result: wireResult,
|
|
302
|
-
outputHash: this.outputHash
|
|
303
|
-
};
|
|
304
|
-
}
|
|
305
|
-
toJsonString() {
|
|
306
|
-
return JSON.stringify(this.toJsonObject());
|
|
307
|
-
}
|
|
308
|
-
toHttpResponse({ useErrorStatus = true } = {}) {
|
|
309
|
-
return new Response(this.toJsonString(), {
|
|
310
|
-
status: this.result.ok ? 200 : useErrorStatus ? this.result.error.httpStatusCode : 500,
|
|
311
|
-
headers: { "Content-Type": "application/json" }
|
|
312
|
-
});
|
|
313
|
-
}
|
|
314
|
-
};
|
|
315
|
-
//#endregion
|
|
316
|
-
//#region src/ActionDefinition/Action/Payload/ActionPayload_Request.ts
|
|
317
|
-
var ActionPayload_Request = class extends ActionPayload {
|
|
318
|
-
input;
|
|
319
|
-
inputHash;
|
|
320
|
-
_callSite;
|
|
321
|
-
constructor(params, input, data) {
|
|
322
|
-
super(params.context, "request", data);
|
|
323
|
-
this.input = input;
|
|
324
|
-
this.inputHash = hashPayloadData(this.context.schema.serializeInput(input));
|
|
325
|
-
}
|
|
326
|
-
successResult(...args) {
|
|
327
|
-
const output = args[0];
|
|
328
|
-
const finalOutput = this.context.schema.validateOutput(output, {
|
|
329
|
-
domain: this.domain,
|
|
330
|
-
actionId: this.id
|
|
331
|
-
});
|
|
332
|
-
return new ActionPayload_Result(this, {
|
|
333
|
-
ok: true,
|
|
334
|
-
output: finalOutput
|
|
335
|
-
}, { time: Date.now() });
|
|
336
|
-
}
|
|
337
|
-
errorResult(err) {
|
|
338
|
-
return new ActionPayload_Result(this, {
|
|
339
|
-
ok: false,
|
|
340
|
-
error: err
|
|
341
|
-
}, { time: Date.now() });
|
|
342
|
-
}
|
|
343
|
-
progress(progress) {
|
|
344
|
-
return new ActionPayload_Progress(this, progress, { time: Date.now() });
|
|
345
|
-
}
|
|
346
|
-
toJsonObject() {
|
|
347
|
-
return {
|
|
348
|
-
...super.toBaseJsonObject(),
|
|
349
|
-
input: this.context.schema.serializeInput(this.input),
|
|
350
|
-
inputHash: this.inputHash
|
|
351
|
-
};
|
|
352
|
-
}
|
|
353
|
-
toJsonString() {
|
|
354
|
-
return JSON.stringify(this.toJsonObject());
|
|
355
|
-
}
|
|
356
|
-
async runToOutput(options) {
|
|
357
|
-
const result = await (await this.run(options)).waitForResultPayload();
|
|
358
|
-
if (result.result.ok) return result.result.output;
|
|
359
|
-
throw result.result.error;
|
|
360
|
-
}
|
|
361
|
-
async runToResultPayload(options) {
|
|
362
|
-
return (await this.run(options)).waitForResultPayload();
|
|
363
|
-
}
|
|
364
|
-
async run(options) {
|
|
365
|
-
if (this._callSite == null) this._callSite = (/* @__PURE__ */ new Error()).stack;
|
|
366
|
-
return this._domain.runAction(this, options);
|
|
367
|
-
}
|
|
368
|
-
};
|
|
369
|
-
//#endregion
|
|
370
72
|
//#region src/ActionDefinition/Action/Core/ActionCore.ts
|
|
371
73
|
var ActionCore = class extends ActionBase {
|
|
372
74
|
_domain;
|
|
@@ -419,2624 +121,355 @@ var ActionCore = class extends ActionBase {
|
|
|
419
121
|
}
|
|
420
122
|
};
|
|
421
123
|
//#endregion
|
|
422
|
-
//#region src/
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
124
|
+
//#region src/utils/isAction_Context_JsonObject.ts
|
|
125
|
+
const isAction_Context_JsonObject = (obj) => {
|
|
126
|
+
return isAction_Base_JsonObject(obj) && obj.form === "context";
|
|
127
|
+
};
|
|
128
|
+
//#endregion
|
|
129
|
+
//#region src/utils/isAction_Core_JsonObject.ts
|
|
130
|
+
const isAction_Core_JsonObject = (obj) => {
|
|
131
|
+
return isAction_Base_JsonObject(obj) && obj.form === "core";
|
|
132
|
+
};
|
|
133
|
+
//#endregion
|
|
134
|
+
//#region src/utils/isAction_Any_JsonObject.ts
|
|
135
|
+
function isAction_Any_JsonObject(obj) {
|
|
136
|
+
return isActionPayload_Any_JsonObject(obj) || isAction_Context_JsonObject(obj) || isAction_Core_JsonObject(obj);
|
|
137
|
+
}
|
|
138
|
+
//#endregion
|
|
139
|
+
//#region src/utils/assertIsActionJson.ts
|
|
140
|
+
function assertIsActionJson(obj) {
|
|
141
|
+
if (!isAction_Any_JsonObject(obj)) throw err_nice_action.fromId("wire_not_action_data");
|
|
142
|
+
}
|
|
143
|
+
//#endregion
|
|
144
|
+
//#region src/utils/isAction_Any_Instance.ts
|
|
145
|
+
function isAction_Any_Instance(value) {
|
|
146
|
+
return value instanceof ActionCore || value instanceof ActionPayload || value instanceof ActionContext;
|
|
147
|
+
}
|
|
148
|
+
//#endregion
|
|
149
|
+
//#region src/ActionDefinition/Domain/ActionDomainBase.ts
|
|
150
|
+
var ActionDomainBase = class {
|
|
429
151
|
domain;
|
|
430
152
|
allDomains;
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
_updates = [];
|
|
438
|
-
_updateListeners = [];
|
|
439
|
-
constructor(initialState) {
|
|
440
|
-
this.context = initialState.context;
|
|
441
|
-
this.cuid = initialState.context.cuid;
|
|
442
|
-
this.id = initialState.context.id;
|
|
443
|
-
this.domain = initialState.context.domain;
|
|
444
|
-
this.allDomains = initialState.context.allDomains;
|
|
445
|
-
this._domain = initialState.context._domain;
|
|
446
|
-
this.parentCuid = initialState.parentCuid;
|
|
447
|
-
this.callSite = initialState.callSite;
|
|
448
|
-
this._resultPayloadPromise = new Promise((resolve, reject) => {
|
|
449
|
-
this._resolveResult = resolve;
|
|
450
|
-
this._rejectResult = reject;
|
|
451
|
-
});
|
|
452
|
-
this._resultPayloadPromise.catch(() => {});
|
|
453
|
-
this._state = {
|
|
454
|
-
request: initialState.request,
|
|
455
|
-
progress: initialState.progress ?? [],
|
|
456
|
-
result: initialState.result
|
|
457
|
-
};
|
|
458
|
-
this._sendUpdate({
|
|
459
|
-
type: "started",
|
|
460
|
-
runningAction: this,
|
|
461
|
-
time: Date.now()
|
|
462
|
-
});
|
|
463
|
-
}
|
|
464
|
-
get state() {
|
|
465
|
-
return this._state;
|
|
466
|
-
}
|
|
467
|
-
abort(reason) {
|
|
468
|
-
this._abort(reason);
|
|
153
|
+
actionSchema;
|
|
154
|
+
_listeners = [];
|
|
155
|
+
constructor(definition) {
|
|
156
|
+
this.domain = definition.domain;
|
|
157
|
+
this.allDomains = definition.allDomains;
|
|
158
|
+
this.actionSchema = definition.actionSchema;
|
|
469
159
|
}
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
160
|
+
/**
|
|
161
|
+
* Add an observer that is called after every action dispatched through this domain.
|
|
162
|
+
* Returns an unsubscribe function — call it to remove the listener.
|
|
163
|
+
*/
|
|
164
|
+
addActionListener(listener) {
|
|
165
|
+
this._listeners.push(listener);
|
|
473
166
|
return () => {
|
|
474
|
-
|
|
475
|
-
const i = this._updateListeners.indexOf(listener);
|
|
476
|
-
if (i !== -1) this._updateListeners.splice(i, 1);
|
|
477
|
-
}
|
|
167
|
+
this._listeners = this._listeners.filter((l) => l !== listener);
|
|
478
168
|
};
|
|
479
169
|
}
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
}]);
|
|
490
|
-
try {
|
|
491
|
-
while (true) {
|
|
492
|
-
if (queue.length === 0) await new Promise((resolve) => {
|
|
493
|
-
resolveWaiter = resolve;
|
|
494
|
-
});
|
|
495
|
-
const event = queue.shift();
|
|
496
|
-
yield event;
|
|
497
|
-
if (event.type === "finished") break;
|
|
498
|
-
}
|
|
499
|
-
} finally {
|
|
500
|
-
unsubscribe();
|
|
501
|
-
}
|
|
170
|
+
/**
|
|
171
|
+
* @internal
|
|
172
|
+
* Observers registered directly on this domain via {@link addActionListener}.
|
|
173
|
+
* Used to wire observers (e.g. devtools) onto RunningActions that aren't created
|
|
174
|
+
* through the local-dispatch path — notably inbound actions pushed from a backend
|
|
175
|
+
* or another client over a bidirectional transport.
|
|
176
|
+
*/
|
|
177
|
+
_getActionObservers() {
|
|
178
|
+
return this._listeners;
|
|
502
179
|
}
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
180
|
+
};
|
|
181
|
+
//#endregion
|
|
182
|
+
//#region src/ActionRuntime/ActionRuntimeManager.ts
|
|
183
|
+
var ActionRuntimeManager = class {
|
|
184
|
+
_runtimes = /* @__PURE__ */ new Map();
|
|
185
|
+
_preferredRuntimeClientId = null;
|
|
186
|
+
_context;
|
|
187
|
+
constructor(context) {
|
|
188
|
+
this._context = context ?? {};
|
|
506
189
|
}
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
this.
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
result
|
|
513
|
-
};
|
|
514
|
-
this._resolveResult(result);
|
|
515
|
-
this._sendUpdate({
|
|
516
|
-
type: "finished",
|
|
517
|
-
finishType: "success",
|
|
518
|
-
runningAction: this,
|
|
519
|
-
time: Date.now(),
|
|
520
|
-
response: result
|
|
190
|
+
registerRuntime(runtime) {
|
|
191
|
+
const runtimeId = runtime.coordinate.stringId;
|
|
192
|
+
if (this._runtimes.has(runtimeId)) throw err_nice_action.fromId("client_runtime_already_registered", {
|
|
193
|
+
context: this._context,
|
|
194
|
+
client: runtime.coordinate
|
|
521
195
|
});
|
|
522
|
-
|
|
196
|
+
for (const id of runtime.coordinate.toStringIds()) {
|
|
197
|
+
if (this._runtimes.has(id)) continue;
|
|
198
|
+
this._runtimes.set(id, runtime);
|
|
199
|
+
}
|
|
523
200
|
}
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
201
|
+
getRuntimeAndHandlerForAction(action, options, throwOnIssue) {
|
|
202
|
+
const localRuntime = options?.targetLocalRuntime;
|
|
203
|
+
if (localRuntime != null) {
|
|
204
|
+
const runtime = throwOnIssue ? this.getBestRuntimeOrThrow(options?.targetLocalRuntime?.coordinate) : this.getBestRuntime(options?.targetLocalRuntime?.coordinate);
|
|
205
|
+
if (runtime == null) return;
|
|
206
|
+
const handler = runtime._getHandlerForAction(action, options);
|
|
207
|
+
if (handler != null) return {
|
|
208
|
+
handler,
|
|
209
|
+
runtime
|
|
210
|
+
};
|
|
211
|
+
if (throwOnIssue) throw err_nice_action.fromId("no_action_execution_handler", {
|
|
212
|
+
domain: action.domain,
|
|
213
|
+
actionId: action.id,
|
|
214
|
+
specifiedClient: localRuntime.coordinate
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
for (const runtime of this._runtimes.values()) {
|
|
218
|
+
const handler = runtime._getHandlerForAction(action);
|
|
219
|
+
if (handler) return {
|
|
220
|
+
handler,
|
|
221
|
+
runtime
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
if (throwOnIssue) throw err_nice_action.fromId("no_action_execution_handler", {
|
|
225
|
+
domain: action.domain,
|
|
226
|
+
actionId: action.id,
|
|
227
|
+
specifiedClient: options?.targetLocalRuntime?.coordinate
|
|
534
228
|
});
|
|
535
|
-
return true;
|
|
536
229
|
}
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
this._isAborted = true;
|
|
540
|
-
this._rejectResult(error);
|
|
541
|
-
this._sendUpdate({
|
|
542
|
-
type: "finished",
|
|
543
|
-
finishType: "failed",
|
|
544
|
-
runningAction: this,
|
|
545
|
-
time: Date.now(),
|
|
546
|
-
error
|
|
547
|
-
});
|
|
548
|
-
return true;
|
|
230
|
+
getRuntimeAndHandlerForActionOrThrow(action, options) {
|
|
231
|
+
return this.getRuntimeAndHandlerForAction(action, options, true);
|
|
549
232
|
}
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
this.
|
|
553
|
-
this._sendUpdate({
|
|
554
|
-
type: "progress",
|
|
555
|
-
runningAction: this,
|
|
556
|
-
time: Date.now(),
|
|
557
|
-
progress: progress.progress
|
|
558
|
-
});
|
|
233
|
+
setPreferredRuntime(runtime) {
|
|
234
|
+
const runtimeId = runtime.coordinate.stringId;
|
|
235
|
+
this._preferredRuntimeClientId = runtimeId;
|
|
559
236
|
}
|
|
560
|
-
|
|
561
|
-
|
|
237
|
+
getPreferredRuntime() {
|
|
238
|
+
if (this._preferredRuntimeClientId) {
|
|
239
|
+
const runtime = this._runtimes.get(this._preferredRuntimeClientId);
|
|
240
|
+
if (runtime) return runtime;
|
|
241
|
+
}
|
|
242
|
+
return this._runtimes.values().next().value;
|
|
562
243
|
}
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
const
|
|
566
|
-
|
|
244
|
+
getBestRuntimeForSpecifier(clientSpecifier) {
|
|
245
|
+
const ids = new RuntimeCoordinate(clientSpecifier).toStringIds();
|
|
246
|
+
for (const id of ids) {
|
|
247
|
+
const runtime = this._runtimes.get(id);
|
|
248
|
+
if (runtime) return runtime;
|
|
249
|
+
}
|
|
567
250
|
}
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
//#region src/errors/err_nice_action.ts
|
|
571
|
-
let EErrId_NiceAction = /* @__PURE__ */ function(EErrId_NiceAction) {
|
|
572
|
-
EErrId_NiceAction["not_implemented"] = "not_implemented";
|
|
573
|
-
EErrId_NiceAction["action_id_not_in_domain"] = "action_id_not_in_domain";
|
|
574
|
-
EErrId_NiceAction["domain_already_exists_in_hierarchy"] = "domain_already_exists_in_hierarchy";
|
|
575
|
-
EErrId_NiceAction["domain_no_handler"] = "domain_no_handler";
|
|
576
|
-
EErrId_NiceAction["hydration_domain_mismatch"] = "hydration_domain_mismatch";
|
|
577
|
-
EErrId_NiceAction["hydration_action_state_mismatch"] = "hydration_action_state_mismatch";
|
|
578
|
-
EErrId_NiceAction["hydration_action_id_not_found"] = "hydration_action_id_not_found";
|
|
579
|
-
EErrId_NiceAction["no_action_execution_handler"] = "no_action_execution_handler";
|
|
580
|
-
EErrId_NiceAction["wire_action_not_payload"] = "wire_action_not_payload";
|
|
581
|
-
EErrId_NiceAction["wire_not_action_data"] = "wire_not_action_data";
|
|
582
|
-
EErrId_NiceAction["client_runtime_already_registered"] = "client_runtime_already_registered";
|
|
583
|
-
EErrId_NiceAction["client_runtime_not_registered"] = "client_runtime_not_registered";
|
|
584
|
-
EErrId_NiceAction["runtime_reset"] = "runtime_reset";
|
|
585
|
-
EErrId_NiceAction["no_client_runtimes_registered"] = "no_client_runtimes_registered";
|
|
586
|
-
EErrId_NiceAction["action_input_validation_failed"] = "action_input_validation_failed";
|
|
587
|
-
EErrId_NiceAction["action_input_validation_promise"] = "action_input_validation_promise";
|
|
588
|
-
EErrId_NiceAction["action_output_validation_failed"] = "action_output_validation_failed";
|
|
589
|
-
EErrId_NiceAction["action_output_validation_promise"] = "action_output_validation_promise";
|
|
590
|
-
return EErrId_NiceAction;
|
|
591
|
-
}({});
|
|
592
|
-
const err_nice_action = err_nice.createChildDomain({
|
|
593
|
-
domain: "err_nice_action",
|
|
594
|
-
defaultHttpStatusCode: 500,
|
|
595
|
-
schema: {
|
|
596
|
-
["not_implemented"]: err({ message: ({ label }) => `The "${label}" functionality is not implemented yet.` }),
|
|
597
|
-
["action_id_not_in_domain"]: err({ message: ({ actionId, domain }) => `Action with id "${actionId}" does not exist in domain "${domain}".` }),
|
|
598
|
-
["domain_already_exists_in_hierarchy"]: err({ message: ({ domain, allParentDomains, parentDomain }) => `Domain "${domain}" already exists in the hierarchy under the parent "${parentDomain}". All parent domains ["${allParentDomains.join(", ")}"]` }),
|
|
599
|
-
["domain_no_handler"]: err({ message: ({ domain }) => `Domain "${domain}" has no action handler registered.` }),
|
|
600
|
-
["hydration_domain_mismatch"]: err({ message: ({ expected, received }) => `Cannot hydrate action: domain mismatch. Expected "${expected}", got "${received}".` }),
|
|
601
|
-
["hydration_action_state_mismatch"]: err({ message: ({ expected, received }) => `Cannot hydrate action: action state type mismatch. Expected "${expected}", got "${received}".` }),
|
|
602
|
-
["hydration_action_id_not_found"]: err({ message: ({ domain, actionId }) => `Cannot hydrate action: id "${actionId}" does not exist in domain "${domain}".` }),
|
|
603
|
-
["no_action_execution_handler"]: err({ message: ({ domain, actionId, specifiedClient }) => `${specifiedClient ? ` The targeted client runtime [${specifiedClient.stringId}] has no` : "No"} action handler registered for "${actionId}" in domain "${domain}".` }),
|
|
604
|
-
["wire_action_not_payload"]: 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}".` }),
|
|
605
|
-
["wire_not_action_data"]: 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".` }),
|
|
606
|
-
["runtime_reset"]: err({ message: () => `Runtime has been reset.` }),
|
|
607
|
-
["client_runtime_already_registered"]: 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.` }),
|
|
608
|
-
["client_runtime_not_registered"]: err({ message: ({ context, clientStringId }) => `No runtime registered${context?.domain ? ` on domain "${context.domain}"` : ""} for client [${clientStringId}].` }),
|
|
609
|
-
["no_client_runtimes_registered"]: err({ message: ({ context }) => `No runtimes registered${context?.domain ? ` on domain "${context.domain}"` : ""}. Add handlers to a runtime via runtime.addHandlers([handler]) before executing actions.` }),
|
|
610
|
-
["action_input_validation_failed"]: err({
|
|
611
|
-
message: ({ domain, actionId, validationMessage }) => `Input validation failed for action "${actionId}" in domain "${domain}":\n${validationMessage}`,
|
|
612
|
-
httpStatusCode: 400
|
|
613
|
-
}),
|
|
614
|
-
["action_input_validation_promise"]: err({
|
|
615
|
-
message: ({ domain, actionId }) => `Input validation for action "${actionId}" in domain "${domain}" returned a promise, which is not supported.`,
|
|
616
|
-
httpStatusCode: 400
|
|
617
|
-
}),
|
|
618
|
-
["action_output_validation_failed"]: err({
|
|
619
|
-
message: ({ domain, actionId, validationMessage }) => `Output validation failed for action "${actionId}" in domain "${domain}":\n${validationMessage}`,
|
|
620
|
-
httpStatusCode: 500
|
|
621
|
-
}),
|
|
622
|
-
["action_output_validation_promise"]: err({
|
|
623
|
-
message: ({ domain, actionId }) => `Output validation for action "${actionId}" in domain "${domain}" returned a promise, which is not supported.`,
|
|
624
|
-
httpStatusCode: 500
|
|
625
|
-
})
|
|
251
|
+
getBestRuntime(clientSpecifier) {
|
|
252
|
+
return clientSpecifier != null ? this.getBestRuntimeForSpecifier(clientSpecifier) : this.getPreferredRuntime();
|
|
626
253
|
}
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
//#region src/utils/isAction_Base_JsonObject.ts
|
|
630
|
-
const isAction_Base_JsonObject = (obj) => {
|
|
631
|
-
return typeof obj === "object" && obj !== null && typeof obj.domain === "string" && typeof obj.id === "string" && typeof obj.form === "string";
|
|
632
|
-
};
|
|
633
|
-
//#endregion
|
|
634
|
-
//#region src/utils/isActionPayload_Result_JsonObject.ts
|
|
635
|
-
const isActionPayload_Result_JsonObject = (obj) => {
|
|
636
|
-
return isAction_Base_JsonObject(obj) && obj.result != null && obj.form === "data" && obj.type === "result";
|
|
637
|
-
};
|
|
638
|
-
//#endregion
|
|
639
|
-
//#region src/ActionDefinition/Schema/ActionSchema.ts
|
|
640
|
-
/**
|
|
641
|
-
* What a sender should expect back from an action — declared on its schema so both ends agree without
|
|
642
|
-
* any wire flag (each derives the mode from the shared `domain:id`).
|
|
643
|
-
*
|
|
644
|
-
* - `payload` — a typed output (the action has `.output(...)`); the sender awaits it.
|
|
645
|
-
* - `ack` — an empty success confirming receipt (no output); the sender may await it to know the
|
|
646
|
-
* receiver handled it (or to surface an error). This is the default for an action with no output.
|
|
647
|
-
* - `none` — fire-and-forget: the receiver sends no reply and the sender doesn't wait. The sender's
|
|
648
|
-
* running action completes as soon as the frame is on the wire (no pending reply, no timeout).
|
|
649
|
-
*/
|
|
650
|
-
let EActionResponseMode = /* @__PURE__ */ function(EActionResponseMode) {
|
|
651
|
-
EActionResponseMode["payload"] = "payload";
|
|
652
|
-
EActionResponseMode["ack"] = "ack";
|
|
653
|
-
EActionResponseMode["none"] = "none";
|
|
654
|
-
return EActionResponseMode;
|
|
655
|
-
}({});
|
|
656
|
-
var ActionSchema = class {
|
|
657
|
-
_errorDeclarations = [];
|
|
658
|
-
inputOptions;
|
|
659
|
-
outputOptions;
|
|
660
|
-
_responseMode;
|
|
661
|
-
get inputSchema() {
|
|
662
|
-
return this.inputOptions?.schema;
|
|
254
|
+
hasRuntime(runtime) {
|
|
255
|
+
return this._runtimes.has(runtime.coordinate.stringId);
|
|
663
256
|
}
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
if (this._responseMode != null) return this._responseMode;
|
|
673
|
-
return this.outputOptions != null ? "payload" : "ack";
|
|
674
|
-
}
|
|
675
|
-
/**
|
|
676
|
-
* Mark this action as expecting only an acknowledgment (an empty success). Mostly for clarity — an
|
|
677
|
-
* output-less action already acks by default — but it documents intent and reads as the deliberate
|
|
678
|
-
* counterpart to {@link fireAndForget}.
|
|
679
|
-
*/
|
|
680
|
-
ack() {
|
|
681
|
-
this._responseMode = "ack";
|
|
682
|
-
return this;
|
|
683
|
-
}
|
|
684
|
-
/**
|
|
685
|
-
* Mark this action as fire-and-forget: the receiver sends no reply, and the sender's running action
|
|
686
|
-
* completes the moment the frame is sent (no awaited reply, no timeout). Ideal for high-frequency
|
|
687
|
-
* server→client pushes (presence, ticks) where an ack would only add wire chatter.
|
|
688
|
-
*/
|
|
689
|
-
fireAndForget() {
|
|
690
|
-
this._responseMode = "none";
|
|
691
|
-
return this;
|
|
692
|
-
}
|
|
693
|
-
/**
|
|
694
|
-
* Declare the input schema (JSON-native or with explicit SERDE type param).
|
|
695
|
-
* For non-JSON-native inputs, prefer the 3-argument form below to avoid
|
|
696
|
-
* needing explicit type parameters.
|
|
697
|
-
*/
|
|
698
|
-
input(options) {
|
|
699
|
-
this.inputOptions = options;
|
|
700
|
-
return this;
|
|
701
|
-
}
|
|
702
|
-
/**
|
|
703
|
-
* Declare the output schema (JSON-native or with explicit SERDE type param).
|
|
704
|
-
* For non-JSON-native outputs, prefer the 3-argument form below to avoid
|
|
705
|
-
* needing explicit type parameters.
|
|
706
|
-
*/
|
|
707
|
-
output(options) {
|
|
708
|
-
this.outputOptions = options;
|
|
709
|
-
return this;
|
|
710
|
-
}
|
|
711
|
-
throws(domain, ids) {
|
|
712
|
-
this._errorDeclarations.push({
|
|
713
|
-
_domain: domain,
|
|
714
|
-
_ids: ids
|
|
715
|
-
});
|
|
716
|
-
return this;
|
|
717
|
-
}
|
|
718
|
-
/**
|
|
719
|
-
* Serialize raw input to a JSON-serializable form.
|
|
720
|
-
* Uses the schema's serialization.serialize if defined; otherwise the input
|
|
721
|
-
* is already JSON-native and is returned as-is.
|
|
722
|
-
*/
|
|
723
|
-
serializeInput(rawInput) {
|
|
724
|
-
if (this.inputOptions?.serialization) return this.inputOptions.serialization.serialize(rawInput);
|
|
725
|
-
return rawInput;
|
|
726
|
-
}
|
|
727
|
-
/**
|
|
728
|
-
* Deserialize a JSON value back into the raw input type.
|
|
729
|
-
* Uses serialization.deserialize if defined; otherwise the value is cast
|
|
730
|
-
* directly (it's already in the correct shape).
|
|
731
|
-
*/
|
|
732
|
-
deserializeInput(serialized) {
|
|
733
|
-
if (this.inputOptions?.serialization) return this.inputOptions.serialization.deserialize(serialized);
|
|
734
|
-
return serialized;
|
|
735
|
-
}
|
|
736
|
-
/**
|
|
737
|
-
* Validate raw input against the schema defined via `.input({ schema })`.
|
|
738
|
-
* Throws `action_input_validation_failed` if validation fails.
|
|
739
|
-
* Returns the validated (and possibly coerced) value on success.
|
|
740
|
-
* If no input schema was declared, the value is passed through as-is.
|
|
741
|
-
*/
|
|
742
|
-
validateInput(value, meta) {
|
|
743
|
-
if (this.inputOptions?.schema == null) return value;
|
|
744
|
-
const result = this.inputOptions.schema["~standard"].validate(value);
|
|
745
|
-
if (result instanceof Promise) throw err_nice_action.fromId("action_input_validation_promise", {
|
|
746
|
-
domain: meta.domain,
|
|
747
|
-
actionId: meta.actionId
|
|
748
|
-
});
|
|
749
|
-
if (result.issues != null) throw err_nice_action.fromId("action_input_validation_failed", {
|
|
750
|
-
domain: meta.domain,
|
|
751
|
-
actionId: meta.actionId,
|
|
752
|
-
validationMessage: extractMessageFromStandardSchema(result)
|
|
753
|
-
});
|
|
754
|
-
return result.value;
|
|
755
|
-
}
|
|
756
|
-
validateOutput(value, meta) {
|
|
757
|
-
if (this.outputOptions?.schema == null) return value;
|
|
758
|
-
const result = this.outputOptions.schema["~standard"].validate(value);
|
|
759
|
-
if (result instanceof Promise) throw err_nice_action.fromId("action_output_validation_promise", {
|
|
760
|
-
domain: meta.domain,
|
|
761
|
-
actionId: meta.actionId
|
|
762
|
-
});
|
|
763
|
-
if (result.issues != null) throw err_nice_action.fromId("action_output_validation_failed", {
|
|
764
|
-
domain: meta.domain,
|
|
765
|
-
actionId: meta.actionId,
|
|
766
|
-
validationMessage: extractMessageFromStandardSchema(result)
|
|
767
|
-
});
|
|
768
|
-
return result.value;
|
|
769
|
-
}
|
|
770
|
-
/**
|
|
771
|
-
* Serialize raw output to a JSON-serializable form.
|
|
772
|
-
*/
|
|
773
|
-
serializeOutput(rawOutput) {
|
|
774
|
-
if (this.outputOptions?.serialization) return this.outputOptions.serialization.serialize(rawOutput);
|
|
775
|
-
return rawOutput;
|
|
776
|
-
}
|
|
777
|
-
/**
|
|
778
|
-
* Deserialize a JSON value back into the raw output type.
|
|
779
|
-
*/
|
|
780
|
-
deserializeOutput(serialized) {
|
|
781
|
-
if (this.outputOptions?.serialization) return this.outputOptions.serialization.deserialize(serialized);
|
|
782
|
-
return serialized;
|
|
783
|
-
}
|
|
784
|
-
};
|
|
785
|
-
const actionSchema = () => {
|
|
786
|
-
return new ActionSchema();
|
|
787
|
-
};
|
|
788
|
-
//#endregion
|
|
789
|
-
//#region src/utils/getAssumedRuntimeEnvironment.ts
|
|
790
|
-
const getAssumedRuntimeInfo = () => {
|
|
791
|
-
return {
|
|
792
|
-
assumed: true,
|
|
793
|
-
runtimeName: runtime
|
|
794
|
-
};
|
|
795
|
-
};
|
|
796
|
-
//#endregion
|
|
797
|
-
//#region src/utils/isActionPayload_Progress_JsonObject.ts
|
|
798
|
-
const isActionPayload_Progress_JsonObject = (obj) => {
|
|
799
|
-
return isAction_Base_JsonObject(obj) && "progress" in obj && obj.form === "data" && obj.type === "progress";
|
|
800
|
-
};
|
|
801
|
-
//#endregion
|
|
802
|
-
//#region src/utils/isActionPayload_Request_JsonObject.ts
|
|
803
|
-
const isActionPayload_Request_JsonObject = (obj) => {
|
|
804
|
-
return isAction_Base_JsonObject(obj) && "input" in obj && obj.form === "data" && obj.type === "request";
|
|
805
|
-
};
|
|
806
|
-
//#endregion
|
|
807
|
-
//#region src/utils/isActionPayload_Any_JsonObject.ts
|
|
808
|
-
function isActionPayload_Any_JsonObject(obj) {
|
|
809
|
-
return isActionPayload_Request_JsonObject(obj) || isActionPayload_Result_JsonObject(obj) || isActionPayload_Progress_JsonObject(obj);
|
|
810
|
-
}
|
|
811
|
-
//#endregion
|
|
812
|
-
//#region src/ActionRuntime/HandlerCallStack.ts
|
|
813
|
-
const _stack = [];
|
|
814
|
-
function pushHandlerCuid(cuid) {
|
|
815
|
-
_stack.push(cuid);
|
|
816
|
-
}
|
|
817
|
-
function popHandlerCuid() {
|
|
818
|
-
_stack.pop();
|
|
819
|
-
}
|
|
820
|
-
function peekHandlerCuid() {
|
|
821
|
-
return _stack[_stack.length - 1];
|
|
822
|
-
}
|
|
823
|
-
//#endregion
|
|
824
|
-
//#region src/ActionRuntime/Handler/PeerLink/Connector/err_nice_external_client.ts
|
|
825
|
-
const err_nice_external_client = err_nice_action.createChildDomain({
|
|
826
|
-
domain: "err_nice_external_client",
|
|
827
|
-
schema: {}
|
|
828
|
-
});
|
|
829
|
-
//#endregion
|
|
830
|
-
//#region src/ActionRuntime/Transport/err_nice_transport.ts
|
|
831
|
-
let EErrId_NiceTransport = /* @__PURE__ */ function(EErrId_NiceTransport) {
|
|
832
|
-
EErrId_NiceTransport["timeout"] = "timeout";
|
|
833
|
-
EErrId_NiceTransport["not_found"] = "not_found";
|
|
834
|
-
EErrId_NiceTransport["unsupported"] = "unsupported";
|
|
835
|
-
EErrId_NiceTransport["initialization_failed"] = "initialization_failed";
|
|
836
|
-
EErrId_NiceTransport["send_failed"] = "send_failed";
|
|
837
|
-
EErrId_NiceTransport["invalid_action_response"] = "invalid_action_response";
|
|
838
|
-
return EErrId_NiceTransport;
|
|
839
|
-
}({});
|
|
840
|
-
const err_nice_transport = err_nice_external_client.createChildDomain({
|
|
841
|
-
domain: "err_nice_transport",
|
|
842
|
-
schema: {
|
|
843
|
-
["timeout"]: err({ message: ({ timeout }) => `ActionConnect transport timed out after ${timeout}ms.` }),
|
|
844
|
-
["not_found"]: err({ message: ({ actionId }) => `No connected transport found for action "${actionId}".` }),
|
|
845
|
-
["unsupported"]: err({ message: ({ transportShapes }) => `${transportShapes.length} Transport(s) [${transportShapes.join(", ")}] found but returned "unsupported" status.` }),
|
|
846
|
-
["initialization_failed"]: err({ message: ({ actionId }) => `Transports found for action "${actionId}", but none are ready.` }),
|
|
847
|
-
["send_failed"]: err({
|
|
848
|
-
message: ({ actionId, httpStatusCode, message }) => `Failed to send action "${actionId}" [${httpStatusCode ?? "Unknown status"}]: ${message ?? "Unknown error"}.`,
|
|
849
|
-
httpStatusCode: ({ httpStatusCode }) => httpStatusCode ?? 500
|
|
850
|
-
}),
|
|
851
|
-
["invalid_action_response"]: err({ message: ({ actionId }) => `Invalid action response JSON structure for action "${actionId}"` })
|
|
852
|
-
}
|
|
853
|
-
});
|
|
854
|
-
//#endregion
|
|
855
|
-
//#region src/ActionRuntime/Transport/ConnectionTransportManager.ts
|
|
856
|
-
var ConnectionTransportManager = class {
|
|
857
|
-
_cache;
|
|
858
|
-
_transports = [];
|
|
859
|
-
constructor(_cache) {
|
|
860
|
-
this._cache = _cache;
|
|
861
|
-
}
|
|
862
|
-
addTransport(transport) {
|
|
863
|
-
this._transports.push(transport);
|
|
864
|
-
}
|
|
865
|
-
/**
|
|
866
|
-
* The highest-priority transport (first declared). Used to label an action's route *before* the
|
|
867
|
-
* transport has finished connecting — so a still-connecting action shows its (expected) destination
|
|
868
|
-
* instead of an "unknown" hop. {@link getReadyTransport} still decides the real winner, and the
|
|
869
|
-
* caller corrects the hop if a lower-priority transport ends up serving the action.
|
|
870
|
-
*/
|
|
871
|
-
getPreferredTransport() {
|
|
872
|
-
return this._transports[0];
|
|
873
|
-
}
|
|
874
|
-
async getReadyTransport(routeActionParams) {
|
|
875
|
-
const action = routeActionParams.action;
|
|
876
|
-
const candidates = [];
|
|
877
|
-
const unavailableTransports = [];
|
|
878
|
-
for (const transport of this._transports) {
|
|
879
|
-
if (!transport.isAvailable(routeActionParams)) {
|
|
880
|
-
unavailableTransports.push(transport);
|
|
881
|
-
continue;
|
|
882
|
-
}
|
|
883
|
-
const cacheKey = transport.getCacheKey(routeActionParams);
|
|
884
|
-
if (cacheKey != null) {
|
|
885
|
-
const cached = this._cache.get(cacheKey);
|
|
886
|
-
if (cached != null) {
|
|
887
|
-
if (cached instanceof Promise) {
|
|
888
|
-
candidates.push(cached);
|
|
889
|
-
continue;
|
|
890
|
-
}
|
|
891
|
-
candidates.push(Promise.resolve({
|
|
892
|
-
...cached,
|
|
893
|
-
transport
|
|
894
|
-
}));
|
|
895
|
-
break;
|
|
896
|
-
}
|
|
897
|
-
}
|
|
898
|
-
const statusInfo = transport.getTransport(routeActionParams);
|
|
899
|
-
if (statusInfo.status === "ready") {
|
|
900
|
-
const readyData = statusInfo.readyData;
|
|
901
|
-
if (cacheKey != null) {
|
|
902
|
-
const entry = {
|
|
903
|
-
methods: readyData,
|
|
904
|
-
transport
|
|
905
|
-
};
|
|
906
|
-
this._cache.set(cacheKey, entry);
|
|
907
|
-
readyData.addOnDisconnectListener?.(() => {
|
|
908
|
-
if (this._cache.get(cacheKey) === entry) this._cache.delete(cacheKey);
|
|
909
|
-
});
|
|
910
|
-
}
|
|
911
|
-
candidates.push(Promise.resolve({
|
|
912
|
-
methods: readyData,
|
|
913
|
-
transport
|
|
914
|
-
}));
|
|
915
|
-
break;
|
|
916
|
-
}
|
|
917
|
-
if (statusInfo.status === "unsupported") {
|
|
918
|
-
unavailableTransports.push(transport);
|
|
919
|
-
continue;
|
|
920
|
-
}
|
|
921
|
-
if (statusInfo.status === "initializing") {
|
|
922
|
-
const promise = statusInfo.initializationPromise.then((info) => {
|
|
923
|
-
if (info.status === "failed") throw info.error;
|
|
924
|
-
if (info.status === "unsupported") throw err_nice_transport.fromId("unsupported", { transportShapes: [transport.type] });
|
|
925
|
-
const readyData = info.readyData;
|
|
926
|
-
const entry = {
|
|
927
|
-
methods: readyData,
|
|
928
|
-
transport
|
|
929
|
-
};
|
|
930
|
-
if (cacheKey != null) if (this._cache.get(cacheKey) === promise) {
|
|
931
|
-
this._cache.set(cacheKey, entry);
|
|
932
|
-
readyData.addOnDisconnectListener?.(() => {
|
|
933
|
-
if (this._cache.get(cacheKey) === entry) this._cache.delete(cacheKey);
|
|
934
|
-
});
|
|
935
|
-
} else readyData.disconnect?.();
|
|
936
|
-
return entry;
|
|
937
|
-
}).catch((e) => {
|
|
938
|
-
if (cacheKey != null && this._cache.get(cacheKey) === promise) this._cache.delete(cacheKey);
|
|
939
|
-
throw e;
|
|
940
|
-
});
|
|
941
|
-
if (cacheKey != null) this._cache.set(cacheKey, promise);
|
|
942
|
-
candidates.push(promise);
|
|
943
|
-
}
|
|
944
|
-
}
|
|
945
|
-
if (candidates.length === 0) {
|
|
946
|
-
if (unavailableTransports.length > 0) throw err_nice_transport.fromId("unsupported", { transportShapes: unavailableTransports.map((t) => t.type) });
|
|
947
|
-
throw err_nice_transport.fromId("not_found", { actionId: action.id });
|
|
948
|
-
}
|
|
949
|
-
let lastError;
|
|
950
|
-
for (const candidate of candidates) try {
|
|
951
|
-
return await candidate;
|
|
952
|
-
} catch (e) {
|
|
953
|
-
lastError = e;
|
|
257
|
+
getBestRuntimeOrThrow(specifier) {
|
|
258
|
+
const runtime = this.getBestRuntime(specifier);
|
|
259
|
+
if (!runtime) {
|
|
260
|
+
if (specifier == null) throw err_nice_action.fromId("no_client_runtimes_registered", { context: this._context });
|
|
261
|
+
throw err_nice_action.fromId("client_runtime_not_registered", {
|
|
262
|
+
context: this._context,
|
|
263
|
+
clientStringId: runtimeCoordinateToStringIds(specifier)[0]
|
|
264
|
+
});
|
|
954
265
|
}
|
|
955
|
-
|
|
956
|
-
}
|
|
957
|
-
};
|
|
958
|
-
//#endregion
|
|
959
|
-
//#region src/ActionRuntime/ActionDomainManager.ts
|
|
960
|
-
var ActionDomainManager = class {
|
|
961
|
-
_domains = /* @__PURE__ */ new Map();
|
|
962
|
-
addDomain(domain) {
|
|
963
|
-
this._domains.set(domain.domain, domain);
|
|
964
|
-
}
|
|
965
|
-
getDomains() {
|
|
966
|
-
return [...this._domains.values()];
|
|
967
|
-
}
|
|
968
|
-
verifyIsActionJson(action) {
|
|
969
|
-
if (typeof action.domain !== "string" || typeof action.id !== "string") throw err_nice_action.fromId("wire_not_action_data");
|
|
970
|
-
}
|
|
971
|
-
getActionDomain(action) {
|
|
972
|
-
this.verifyIsActionJson(action);
|
|
973
|
-
const domain = this._domains.get(action.domain);
|
|
974
|
-
if (!domain) return;
|
|
975
|
-
return domain;
|
|
976
|
-
}
|
|
977
|
-
getActionDomainOrThrow(action) {
|
|
978
|
-
this.verifyIsActionJson(action);
|
|
979
|
-
const domain = this._domains.get(action.domain);
|
|
980
|
-
if (!domain) throw err_nice_action.fromId("domain_no_handler", { domain: action.domain });
|
|
981
|
-
return domain;
|
|
982
|
-
}
|
|
983
|
-
hydrateActionPayload(actionJson) {
|
|
984
|
-
return this.getActionDomainOrThrow(actionJson).hydrateAnyAction(actionJson);
|
|
266
|
+
return runtime;
|
|
985
267
|
}
|
|
986
268
|
};
|
|
987
269
|
//#endregion
|
|
988
|
-
//#region src/
|
|
989
|
-
var
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
for (const domain of actionRouter.getDomains()) this.domainManager.addDomain(domain);
|
|
999
|
-
for (const [matchKey, routeDataEntries] of actionRouter.actionRouteData.entries()) this.actionRouteData.set(matchKey, [...routeDataEntries]);
|
|
1000
|
-
}
|
|
1001
|
-
addDomainsFromOther(actionRouter) {
|
|
1002
|
-
for (const domain of actionRouter.getDomains()) this.domainManager.addDomain(domain);
|
|
1003
|
-
}
|
|
1004
|
-
/** All FNs registered for an action, ID-specific entries first then domain wildcard. */
|
|
1005
|
-
getRouteDataEntriesForAction(action) {
|
|
1006
|
-
const idKey = `dom[${action.domain}]id[${action.id}]`;
|
|
1007
|
-
const domKey = `dom[${action.domain}]id[_]`;
|
|
1008
|
-
return [...this.actionRouteData.get(idKey) ?? [], ...this.actionRouteData.get(domKey) ?? []];
|
|
1009
|
-
}
|
|
1010
|
-
/** First FN registered for an action (ID-specific beats domain wildcard). */
|
|
1011
|
-
getRouteDataForAction(action) {
|
|
1012
|
-
return this.getRouteDataEntriesForAction(action)[0];
|
|
1013
|
-
}
|
|
1014
|
-
throwNoHandlerForAction(action, context) {
|
|
1015
|
-
if (this._context.contextType === "handler_route") throw err_nice_action.fromId("no_action_execution_handler", {
|
|
1016
|
-
domain: action.domain,
|
|
1017
|
-
actionId: action.id,
|
|
1018
|
-
specifiedClient: context.targetLocalRuntime?.coordinate
|
|
1019
|
-
});
|
|
1020
|
-
if (this._context.contextType === "runtime_to_handler") throw err_nice_action.fromId("no_action_execution_handler", {
|
|
1021
|
-
domain: action.domain,
|
|
1022
|
-
actionId: action.id,
|
|
1023
|
-
specifiedClient: this._context.runtime.coordinate
|
|
1024
|
-
});
|
|
1025
|
-
throw err_nice_action.fromId("no_action_execution_handler", {
|
|
1026
|
-
domain: action.domain,
|
|
1027
|
-
actionId: action.id
|
|
270
|
+
//#region src/ActionDefinition/Domain/ActionRootDomain.ts
|
|
271
|
+
var ActionRootDomain = class extends ActionDomainBase {
|
|
272
|
+
domainDefinition;
|
|
273
|
+
_actionRuntimeManager;
|
|
274
|
+
constructor(domainDefinition) {
|
|
275
|
+
const domainId = domainDefinition.domain;
|
|
276
|
+
super({
|
|
277
|
+
domain: domainId,
|
|
278
|
+
allDomains: [domainId],
|
|
279
|
+
actionSchema: {}
|
|
1028
280
|
});
|
|
281
|
+
this.domainDefinition = domainDefinition;
|
|
282
|
+
this._actionRuntimeManager = new ActionRuntimeManager({ domain: domainId });
|
|
1029
283
|
}
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
getForKey(key) {
|
|
1042
|
-
return this.actionRouteData.get(key) ?? [];
|
|
1043
|
-
}
|
|
1044
|
-
/** Every match key that has at least one registered FN. */
|
|
1045
|
-
getRegisteredKeys() {
|
|
1046
|
-
return [...this.actionRouteData.keys()];
|
|
1047
|
-
}
|
|
1048
|
-
getDomains() {
|
|
1049
|
-
return this.domainManager.getDomains();
|
|
1050
|
-
}
|
|
1051
|
-
/** Register a handler for all actions in a domain, replacing any existing one. */
|
|
1052
|
-
forDomain(domain, routeData) {
|
|
1053
|
-
this.domainManager.addDomain(domain);
|
|
1054
|
-
this.actionRouteData.set(`dom[${domain.domain}]id[_]`, [routeData]);
|
|
1055
|
-
return this;
|
|
1056
|
-
}
|
|
1057
|
-
forAction(action, routeData) {
|
|
1058
|
-
return this.forActionId(action._domain, action.id, routeData);
|
|
1059
|
-
}
|
|
1060
|
-
/** Register a handler for a specific action, replacing any existing one. */
|
|
1061
|
-
forActionId(domain, id, routeData) {
|
|
1062
|
-
this.domainManager.addDomain(domain);
|
|
1063
|
-
this.actionRouteData.set(`dom[${domain.domain}]id[${id}]`, [routeData]);
|
|
1064
|
-
return this;
|
|
1065
|
-
}
|
|
1066
|
-
/** Register one handler for several action IDs, replacing any existing ones. */
|
|
1067
|
-
forActionIds(domain, ids, routeData) {
|
|
1068
|
-
this.domainManager.addDomain(domain);
|
|
1069
|
-
for (const id of ids) this.forActionId(domain, id, routeData);
|
|
1070
|
-
return this;
|
|
1071
|
-
}
|
|
1072
|
-
/** Register per-action handlers from a cases map, replacing any existing ones. */
|
|
1073
|
-
forDomainActionCases(domain, cases) {
|
|
1074
|
-
this.domainManager.addDomain(domain);
|
|
1075
|
-
for (const id of Object.keys(cases)) {
|
|
1076
|
-
const routeData = cases[id];
|
|
1077
|
-
if (routeData != null) this.actionRouteData.set(`dom[${domain.domain}]id[${id}]`, [routeData]);
|
|
1078
|
-
}
|
|
1079
|
-
return this;
|
|
1080
|
-
}
|
|
1081
|
-
/** Append a handler for all actions in a domain (accumulates alongside existing). */
|
|
1082
|
-
addForDomain(domain, routeData) {
|
|
1083
|
-
this.domainManager.addDomain(domain);
|
|
1084
|
-
this._push(`dom[${domain.domain}]id[_]`, routeData);
|
|
1085
|
-
return this;
|
|
1086
|
-
}
|
|
1087
|
-
/** Append a handler for a specific action (accumulates alongside existing). */
|
|
1088
|
-
addForAction(domain, id, routeData) {
|
|
1089
|
-
this.domainManager.addDomain(domain);
|
|
1090
|
-
this._push(`dom[${domain.domain}]id[${id}]`, routeData);
|
|
1091
|
-
return this;
|
|
1092
|
-
}
|
|
1093
|
-
/** Append one handler for several action IDs (accumulates alongside existing). */
|
|
1094
|
-
addForActionIds(domain, ids, routeData) {
|
|
1095
|
-
this.domainManager.addDomain(domain);
|
|
1096
|
-
for (const id of ids) this.addForAction(domain, id, routeData);
|
|
1097
|
-
return this;
|
|
1098
|
-
}
|
|
1099
|
-
/** Append per-action handlers from a cases map (accumulates alongside existing). */
|
|
1100
|
-
addForDomainActionCases(domain, cases) {
|
|
1101
|
-
this.domainManager.addDomain(domain);
|
|
1102
|
-
for (const id of Object.keys(cases)) {
|
|
1103
|
-
const routeData = cases[id];
|
|
1104
|
-
if (routeData != null) this._push(`dom[${domain.domain}]id[${id}]`, routeData);
|
|
1105
|
-
}
|
|
1106
|
-
return this;
|
|
1107
|
-
}
|
|
1108
|
-
/** Append a handler directly by its raw match key (used when the key is known ahead of time). */
|
|
1109
|
-
addForKey(key, routeData) {
|
|
1110
|
-
this._push(key, routeData);
|
|
1111
|
-
return this;
|
|
1112
|
-
}
|
|
1113
|
-
_push(key, routeData) {
|
|
1114
|
-
const existing = this.actionRouteData.get(key);
|
|
1115
|
-
if (existing != null) existing.push(routeData);
|
|
1116
|
-
else this.actionRouteData.set(key, [routeData]);
|
|
1117
|
-
}
|
|
1118
|
-
};
|
|
1119
|
-
//#endregion
|
|
1120
|
-
//#region src/ActionRuntime/Handler/ActionHandler.ts
|
|
1121
|
-
var ActionHandler = class {
|
|
1122
|
-
cuid;
|
|
1123
|
-
constructor() {
|
|
1124
|
-
this.cuid = nanoid();
|
|
1125
|
-
}
|
|
1126
|
-
getActionRouter() {
|
|
1127
|
-
return this.actionRouter;
|
|
1128
|
-
}
|
|
1129
|
-
};
|
|
1130
|
-
//#endregion
|
|
1131
|
-
//#region src/ActionRuntime/Handler/PeerLink/PeerLinkHandler.ts
|
|
1132
|
-
/**
|
|
1133
|
-
* Shared base for every handler that routes a domain set to/from *another runtime* (a "peer") — the
|
|
1134
|
-
* unified peer-link concept. Both specializations extend this as siblings, differing only in *who
|
|
1135
|
-
* establishes the connection*, which is a transport trait, not a routing one:
|
|
1136
|
-
*
|
|
1137
|
-
* - {@link ConnectorHandler} — **dial-out**: this runtime opens connection(s) to one peer
|
|
1138
|
-
* over a transport stack (with caching + fallback). The classic "client → backend" link.
|
|
1139
|
-
* - {@link AcceptorHandler} — **accept-in**: connections are accepted from many peers and fed in
|
|
1140
|
-
* via `receive()`; it keeps a per-connection registry and can push to any of them.
|
|
1141
|
-
*
|
|
1142
|
-
* To the runtime there is no "client" vs "server" — both are peer-link handlers (`handlerType =
|
|
1143
|
-
* external`) keyed to a peer coordinate, chosen by the return-path dispatch via {@link sendReturnPayload}.
|
|
1144
|
-
*/
|
|
1145
|
-
var PeerLinkHandler = class extends ActionHandler {
|
|
1146
|
-
/** The peer runtime this handler links to (an env-only coordinate for an accept-in handler). */
|
|
1147
|
-
peerClient;
|
|
1148
|
-
handlerType = "peer";
|
|
1149
|
-
actionRouter = new ActionRouter({
|
|
1150
|
-
contextType: "handler_route",
|
|
1151
|
-
handler: this
|
|
1152
|
-
});
|
|
1153
|
-
/** Listeners installed by the runtime (`resolveIncomingActionPayload`) for inbound peer frames. */
|
|
1154
|
-
_incomingActionDataListeners = [];
|
|
1155
|
-
constructor(peerCoordinate) {
|
|
1156
|
-
super();
|
|
1157
|
-
this.peerClient = peerCoordinate;
|
|
1158
|
-
}
|
|
1159
|
-
forDomain(domain) {
|
|
1160
|
-
this.actionRouter.forDomain(domain, true);
|
|
1161
|
-
return this;
|
|
1162
|
-
}
|
|
1163
|
-
forAction(action) {
|
|
1164
|
-
this.actionRouter.forAction(action, true);
|
|
1165
|
-
return this;
|
|
1166
|
-
}
|
|
1167
|
-
forActionIds(domain, ids) {
|
|
1168
|
-
this.actionRouter.forActionIds(domain, ids, true);
|
|
1169
|
-
return this;
|
|
1170
|
-
}
|
|
1171
|
-
_setIncomingActionDataListener(listener) {
|
|
1172
|
-
this._incomingActionDataListeners.push(listener);
|
|
1173
|
-
}
|
|
1174
|
-
/** Hand a decoded inbound frame to the runtime (called by each specialization's receive path). */
|
|
1175
|
-
_emitIncoming(json) {
|
|
1176
|
-
for (const listener of this._incomingActionDataListeners) listener(json);
|
|
284
|
+
createChildDomain(subDomainDef) {
|
|
285
|
+
if (this.allDomains.includes(subDomainDef.domain)) throw err_nice_action.fromId("domain_already_exists_in_hierarchy", {
|
|
286
|
+
domain: subDomainDef.domain,
|
|
287
|
+
allParentDomains: this.allDomains,
|
|
288
|
+
parentDomain: this.domain
|
|
289
|
+
});
|
|
290
|
+
return new ActionDomain({
|
|
291
|
+
allDomains: [...this.allDomains, subDomainDef.domain],
|
|
292
|
+
domain: subDomainDef.domain,
|
|
293
|
+
actionSchema: subDomainDef.actions
|
|
294
|
+
}, { rootDomain: this });
|
|
1177
295
|
}
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
* dispatch ({@link ActionRuntime.getReturnHandlerForOrigin}) prefers a handler that owns the origin's
|
|
1181
|
-
* connection over a mere coordinate match, so with several duplex acceptors a result/push routes back
|
|
1182
|
-
* over the carrier the client connected on. Defaults to `false`; an acceptor overrides it from its
|
|
1183
|
-
* connection registry.
|
|
1184
|
-
*/
|
|
1185
|
-
ownsLiveConnectionFor(_origin) {
|
|
1186
|
-
return false;
|
|
296
|
+
_registerRuntime(runtime) {
|
|
297
|
+
this._actionRuntimeManager.registerRuntime(runtime);
|
|
1187
298
|
}
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
};
|
|
1191
|
-
//#endregion
|
|
1192
|
-
//#region src/ActionRuntime/Handler/PeerLink/Connector/ConnectorHandler.ts
|
|
1193
|
-
/**
|
|
1194
|
-
* Dial-out peer link: this runtime opens connection(s) to one peer over a transport stack (cached, with
|
|
1195
|
-
* preference-ordered fallback). The classic "client → backend" handler — but to the runtime it's just a
|
|
1196
|
-
* {@link PeerLinkHandler} like the accept-in server one.
|
|
1197
|
-
*/
|
|
1198
|
-
var ConnectorHandler = class extends PeerLinkHandler {
|
|
1199
|
-
/**
|
|
1200
|
-
* Dial-out can receive (and so return) an unsolicited push only over a duplex transport. With every
|
|
1201
|
-
* transport exchange-only (HTTP), the peer can never push to us — so this link can't deliver one back.
|
|
1202
|
-
*/
|
|
1203
|
-
canPush;
|
|
1204
|
-
_defaultTimeout;
|
|
1205
|
-
_transportCache = /* @__PURE__ */ new Map();
|
|
1206
|
-
transportManager = new ConnectionTransportManager(this._transportCache);
|
|
1207
|
-
constructor({ runtimeCoordinate: peerSpecifier, transports, defaultTimeout }) {
|
|
1208
|
-
super(peerSpecifier);
|
|
1209
|
-
this._defaultTimeout = defaultTimeout ?? 1e4;
|
|
1210
|
-
this.canPush = transports.some((transport) => transport.type === "duplex");
|
|
1211
|
-
for (const transport of transports) {
|
|
1212
|
-
const connection = transport._createConnection({ resolvers: { onIncomingActionDataJson: (json) => this._emitIncoming(json) } });
|
|
1213
|
-
connection.definition = transport;
|
|
1214
|
-
this.transportManager.addTransport(connection);
|
|
1215
|
-
}
|
|
299
|
+
_hasRuntime(runtime) {
|
|
300
|
+
return this._actionRuntimeManager.hasRuntime(runtime);
|
|
1216
301
|
}
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
const localClient = localRuntime.coordinate;
|
|
1220
|
-
const incomingTimeout = config?.timeout ?? this._defaultTimeout;
|
|
1221
|
-
const parentCuid = peekHandlerCuid();
|
|
1222
|
-
const callSite = action._callSite ?? (/* @__PURE__ */ new Error()).stack;
|
|
1223
|
-
const routeParams = {
|
|
1224
|
-
action,
|
|
1225
|
-
localClient,
|
|
1226
|
-
externalClient: this.peerClient
|
|
1227
|
-
};
|
|
1228
|
-
const preferredTransport = this.transportManager.getPreferredTransport();
|
|
1229
|
-
const routeItem = preferredTransport != null ? {
|
|
1230
|
-
runtime: localClient,
|
|
1231
|
-
handler: this.toHandlerRouteItem(preferredTransport, routeParams),
|
|
1232
|
-
time: Date.now()
|
|
1233
|
-
} : void 0;
|
|
1234
|
-
if (routeItem != null) action.context.addRouteItem(routeItem);
|
|
1235
|
-
const runningAction = new RunningAction({
|
|
1236
|
-
context: action.context,
|
|
1237
|
-
request: action,
|
|
1238
|
-
parentCuid,
|
|
1239
|
-
callSite
|
|
1240
|
-
});
|
|
1241
|
-
localRuntime.registerRunningAction(runningAction);
|
|
1242
|
-
this._dispatchWhenTransportReady(runningAction, routeParams, routeItem, incomingTimeout);
|
|
1243
|
-
return runningAction;
|
|
302
|
+
getRuntime(clientSpecifier) {
|
|
303
|
+
return this._actionRuntimeManager.getBestRuntimeForSpecifier(clientSpecifier);
|
|
1244
304
|
}
|
|
1245
|
-
async
|
|
1246
|
-
const
|
|
305
|
+
async _runAction(actionPayload, options) {
|
|
306
|
+
const allListeners = [...this._listeners, ...options?.listeners ?? []];
|
|
307
|
+
let handlerAndRuntime;
|
|
1247
308
|
try {
|
|
1248
|
-
|
|
1249
|
-
const handlerRouteItem = this.toHandlerRouteItem(transport, routeParams);
|
|
1250
|
-
if (routeItem != null) {
|
|
1251
|
-
routeItem.handler = handlerRouteItem;
|
|
1252
|
-
routeItem.time = Date.now();
|
|
1253
|
-
} else action.context.addRouteItem({
|
|
1254
|
-
runtime: routeParams.localClient,
|
|
1255
|
-
handler: handlerRouteItem,
|
|
1256
|
-
time: Date.now()
|
|
1257
|
-
});
|
|
1258
|
-
const sendInput = {
|
|
1259
|
-
...routeParams,
|
|
1260
|
-
runningAction,
|
|
1261
|
-
timeout: incomingTimeout
|
|
1262
|
-
};
|
|
1263
|
-
if (action.type === "request" && methods.updateRunConfig != null) sendInput.timeout = methods.updateRunConfig(sendInput)?.timeout ?? incomingTimeout;
|
|
1264
|
-
methods.sendActionData(sendInput);
|
|
1265
|
-
if (action.type === "request" && action.schema.responseMode === "none") runningAction._completeWithResult(action.successResult(void 0));
|
|
309
|
+
handlerAndRuntime = this._actionRuntimeManager.getRuntimeAndHandlerForActionOrThrow(actionPayload, options);
|
|
1266
310
|
} catch (err) {
|
|
1267
|
-
runningAction
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
* Dispatch a result or progress payload directly back to the external client via the best
|
|
1272
|
-
* available bidirectional transport (WebSocket / Custom). Used for return-path routing when the
|
|
1273
|
-
* local runtime recognises that it has a direct channel to the action's originClient.
|
|
1274
|
-
*
|
|
1275
|
-
* Returns `true` if the payload was sent, `false` if no suitable transport was available.
|
|
1276
|
-
*/
|
|
1277
|
-
async sendReturnPayload(payload, config) {
|
|
1278
|
-
const localClient = config.targetLocalRuntime.coordinate;
|
|
1279
|
-
try {
|
|
1280
|
-
const { methods } = await this.transportManager.getReadyTransport({
|
|
1281
|
-
action: payload,
|
|
1282
|
-
localClient,
|
|
1283
|
-
externalClient: this.peerClient
|
|
1284
|
-
});
|
|
1285
|
-
if (methods.sendReturnData == null) return false;
|
|
1286
|
-
methods.sendReturnData(payload, {
|
|
1287
|
-
localClient,
|
|
1288
|
-
externalClient: this.peerClient
|
|
311
|
+
const runningAction = new RunningAction({
|
|
312
|
+
context: actionPayload.context,
|
|
313
|
+
request: actionPayload,
|
|
314
|
+
callSite: actionPayload._callSite
|
|
1289
315
|
});
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
316
|
+
runningAction.addUpdateListeners(allListeners);
|
|
317
|
+
runningAction._failWithError(err);
|
|
318
|
+
throw err;
|
|
1293
319
|
}
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
};
|
|
1300
|
-
}
|
|
1301
|
-
toHandlerRouteItem(transport, input) {
|
|
1302
|
-
return {
|
|
1303
|
-
type: this.handlerType,
|
|
1304
|
-
client: this.peerClient,
|
|
1305
|
-
transOrd: transport.transOrd,
|
|
1306
|
-
transShape: transport.type,
|
|
1307
|
-
transInfo: transport.getRouteInfo(input)
|
|
1308
|
-
};
|
|
1309
|
-
}
|
|
1310
|
-
clearTransportCache() {
|
|
1311
|
-
for (const entry of this._transportCache.values()) if (entry instanceof Promise) entry.then((ready) => ready.methods.disconnect?.()).catch(() => {});
|
|
1312
|
-
else entry.methods.disconnect?.();
|
|
1313
|
-
this._transportCache.clear();
|
|
320
|
+
const { handler, runtime } = handlerAndRuntime;
|
|
321
|
+
actionPayload.context._setOriginClient(runtime.coordinate);
|
|
322
|
+
const runningAction = await handler.handleActionRequest(actionPayload, { targetLocalRuntime: runtime });
|
|
323
|
+
runningAction.addUpdateListeners(allListeners);
|
|
324
|
+
return runningAction;
|
|
1314
325
|
}
|
|
1315
326
|
};
|
|
1316
|
-
const createConnectorHandler = (config) => {
|
|
1317
|
-
return new ConnectorHandler(config);
|
|
1318
|
-
};
|
|
1319
327
|
//#endregion
|
|
1320
|
-
//#region src/
|
|
1321
|
-
var
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
_applied = false;
|
|
1329
|
-
static getDefault() {
|
|
1330
|
-
return getDefaultActionRuntime();
|
|
1331
|
-
}
|
|
1332
|
-
constructor(coordinate) {
|
|
1333
|
-
this._coordinate = coordinate.specifyIfUnset({ insId: nanoid(14) });
|
|
1334
|
-
this.timeCreated = Date.now();
|
|
1335
|
-
this.actionRouter = new ActionRouter({
|
|
1336
|
-
contextType: "runtime_to_handler",
|
|
1337
|
-
runtime: this
|
|
1338
|
-
});
|
|
1339
|
-
}
|
|
1340
|
-
get coordinate() {
|
|
1341
|
-
return this._coordinate;
|
|
1342
|
-
}
|
|
1343
|
-
specifyRuntimeCoordinate(specifics) {
|
|
1344
|
-
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}")` });
|
|
1345
|
-
this._coordinate = this._coordinate.specify(specifics);
|
|
1346
|
-
this.apply();
|
|
1347
|
-
}
|
|
1348
|
-
registerRunningAction(ra) {
|
|
1349
|
-
this._pendingRunningActions.set(ra.cuid, ra);
|
|
1350
|
-
ra.addUpdateListeners([(update) => {
|
|
1351
|
-
if (update.type === "finished") this._pendingRunningActions.delete(ra.cuid);
|
|
1352
|
-
}]);
|
|
1353
|
-
}
|
|
1354
|
-
resolveIncomingActionPayload(json) {
|
|
1355
|
-
if (json.type === "request") {
|
|
1356
|
-
this.handleActionPayloadWire(json).catch((err) => {
|
|
1357
|
-
console.error(`[ActionRuntime] Incoming action [${json.domain}:${json.id}:${json.form}:${json.type}] unhandled:`, err);
|
|
1358
|
-
});
|
|
1359
|
-
return;
|
|
1360
|
-
}
|
|
1361
|
-
this._pendingRunningActions.get(json.context.cuid)?._resolveFromJson(json);
|
|
1362
|
-
}
|
|
1363
|
-
async handleActionPayloadWire(wire) {
|
|
1364
|
-
let action;
|
|
1365
|
-
if (isActionPayload_Any_JsonObject(wire)) action = this.actionRouter.domainManager.getActionDomainOrThrow(wire).hydrateAnyAction(wire);
|
|
1366
|
-
if (action == null) throw err_nice_action.fromId("wire_not_action_data");
|
|
1367
|
-
return this.handleActionPayload(action);
|
|
328
|
+
//#region src/ActionDefinition/Domain/ActionDomain.ts
|
|
329
|
+
var ActionDomain = class ActionDomain extends ActionDomainBase {
|
|
330
|
+
_rootDomain;
|
|
331
|
+
_actionMap;
|
|
332
|
+
constructor(definition, { rootDomain }) {
|
|
333
|
+
super(definition);
|
|
334
|
+
this._rootDomain = rootDomain;
|
|
335
|
+
this._actionMap = this.createActionMap();
|
|
1368
336
|
}
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
const observers = action.context._domain._collectActionObservers();
|
|
1372
|
-
let handlerForAction;
|
|
1373
|
-
try {
|
|
1374
|
-
handlerForAction = this.getHandlerForActionOrThrow(action, options);
|
|
1375
|
-
} catch (err) {
|
|
1376
|
-
const runningAction = new RunningAction({
|
|
1377
|
-
context: action.context,
|
|
1378
|
-
request: action
|
|
1379
|
-
});
|
|
1380
|
-
runningAction.addUpdateListeners(observers);
|
|
1381
|
-
runningAction._completeWithResult(action.errorResult(castNiceError(err)));
|
|
1382
|
-
return runningAction;
|
|
1383
|
-
}
|
|
1384
|
-
const runningAction = await handlerForAction.handleActionRequest(action, {
|
|
1385
|
-
...options,
|
|
1386
|
-
targetLocalRuntime: this
|
|
1387
|
-
});
|
|
1388
|
-
runningAction.addUpdateListeners(observers);
|
|
1389
|
-
this._trySetupReturnDispatch(runningAction);
|
|
1390
|
-
return runningAction;
|
|
1391
|
-
}
|
|
1392
|
-
throw err_nice_action.fromId("not_implemented", { label: `Handling incoming action payloads of type "${action.type}"` });
|
|
337
|
+
get rootDomain() {
|
|
338
|
+
return this._rootDomain;
|
|
1393
339
|
}
|
|
1394
340
|
/**
|
|
1395
341
|
* @internal
|
|
1396
|
-
*
|
|
1397
|
-
*
|
|
1398
|
-
*
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
const possibleHandlers = handlers.filter((handler) => {
|
|
1404
|
-
if (handler.handlerType === "peer") {
|
|
1405
|
-
if (targetPeer && !targetPeer.isSameFor(handler.peerClient).id) return false;
|
|
1406
|
-
return true;
|
|
1407
|
-
}
|
|
1408
|
-
if (targetPeer != null) return false;
|
|
1409
|
-
if (action.type === "request") return true;
|
|
1410
|
-
return false;
|
|
1411
|
-
});
|
|
1412
|
-
if (possibleHandlers.length === 0) return;
|
|
1413
|
-
const scoringPeer = targetPeer ?? RuntimeCoordinate.unknown;
|
|
1414
|
-
let handlerScore = -1;
|
|
1415
|
-
let handler;
|
|
1416
|
-
for (const possibleHandler of possibleHandlers) {
|
|
1417
|
-
if (possibleHandler.handlerType === "local") return possibleHandler;
|
|
1418
|
-
if (possibleHandler.handlerType === "peer") {
|
|
1419
|
-
const score = scoringPeer.similarityLevel(possibleHandler.peerClient);
|
|
1420
|
-
if (score > handlerScore) {
|
|
1421
|
-
handlerScore = score;
|
|
1422
|
-
handler = possibleHandler;
|
|
1423
|
-
}
|
|
1424
|
-
}
|
|
1425
|
-
}
|
|
1426
|
-
return handler;
|
|
1427
|
-
}
|
|
1428
|
-
getHandlerForActionOrThrow(action, options) {
|
|
1429
|
-
const handler = this._getHandlerForAction(action, options);
|
|
1430
|
-
if (handler == null) throw err_nice_action.fromId("no_action_execution_handler", {
|
|
1431
|
-
actionId: action.id,
|
|
1432
|
-
domain: action.domain,
|
|
1433
|
-
specifiedClient: options?.targetPeer
|
|
1434
|
-
});
|
|
1435
|
-
return handler;
|
|
1436
|
-
}
|
|
1437
|
-
/**
|
|
1438
|
-
* Register one or more handlers. Each handler's own `actionRouter` defines
|
|
1439
|
-
* which domains/actions it handles — those routing keys are mirrored into
|
|
1440
|
-
* this runtime's router so the same action can be served by multiple handlers.
|
|
1441
|
-
* Duplicate registrations (same handler cuid for the same key) are skipped.
|
|
1442
|
-
*/
|
|
1443
|
-
addHandlers(handlers) {
|
|
1444
|
-
for (const handler of handlers) {
|
|
1445
|
-
if (handler.handlerType === "peer") {
|
|
1446
|
-
handler._setIncomingActionDataListener((json) => this.resolveIncomingActionPayload(json));
|
|
1447
|
-
this._registeredPeerHandlers.push(handler);
|
|
1448
|
-
}
|
|
1449
|
-
const handlerRouter = handler.getActionRouter();
|
|
1450
|
-
this.actionRouter.addDomainsFromOther(handlerRouter);
|
|
1451
|
-
if (this._applied) this.apply();
|
|
1452
|
-
for (const key of handlerRouter.getRegisteredKeys()) if (!this.actionRouter.getForKey(key).some((h) => h.cuid === handler.cuid)) this.actionRouter.addForKey(key, handler);
|
|
1453
|
-
}
|
|
1454
|
-
return this;
|
|
1455
|
-
}
|
|
1456
|
-
/**
|
|
1457
|
-
* Declare an external "backend client" in one call: build an
|
|
1458
|
-
* {@link ConnectorHandler} for `externalCoordinate` carrying the given
|
|
1459
|
-
* `transports`, route the listed `domains`/`actions` to it, register it (plus any
|
|
1460
|
-
* `localHandlers` — e.g. server→client push handlers that share the same channel)
|
|
1461
|
-
* on this runtime, and `apply()`. Returns the external handler so the caller can
|
|
1462
|
-
* later `clearTransportCache()` it.
|
|
1463
|
-
*
|
|
1464
|
-
* Sugar over `new ConnectorHandler(...).forDomain(...)` + `addHandlers([...])`,
|
|
1465
|
-
* so a single runtime can host one handler per backend target with its transports
|
|
1466
|
-
* declared once and reused across every action routed to that backend.
|
|
1467
|
-
*/
|
|
1468
|
-
connectTo(externalCoordinate, options) {
|
|
1469
|
-
const handler = new ConnectorHandler({
|
|
1470
|
-
runtimeCoordinate: externalCoordinate,
|
|
1471
|
-
transports: options.transports,
|
|
1472
|
-
defaultTimeout: options.defaultTimeout
|
|
1473
|
-
});
|
|
1474
|
-
for (const domain of options.domains ?? []) handler.forDomain(domain);
|
|
1475
|
-
for (const action of options.actions ?? []) handler.forAction(action);
|
|
1476
|
-
this.addHandlers([handler, ...options.localHandlers ?? []]);
|
|
1477
|
-
this.apply();
|
|
1478
|
-
return handler;
|
|
1479
|
-
}
|
|
1480
|
-
applyRuntimeForDomain(domain) {
|
|
1481
|
-
const rootDomain = domain.rootDomain;
|
|
1482
|
-
if (!rootDomain._hasRuntime(this)) rootDomain._registerRuntime(this);
|
|
1483
|
-
}
|
|
1484
|
-
/**
|
|
1485
|
-
* Register this runtime with all root domains covered by its currently-added handlers,
|
|
1486
|
-
* making it eligible to execute actions dispatched from those domains.
|
|
1487
|
-
* After apply() is called, any subsequent addHandlers() calls also auto-register.
|
|
1488
|
-
*/
|
|
1489
|
-
apply() {
|
|
1490
|
-
this._applied = true;
|
|
1491
|
-
for (const domain of this.actionRouter.getDomains()) this.applyRuntimeForDomain(domain);
|
|
1492
|
-
return this;
|
|
1493
|
-
}
|
|
1494
|
-
/**
|
|
1495
|
-
* Find the best registered external handler that can reach `originClient` directly.
|
|
1496
|
-
* Used to locate the return-path channel for dispatching results back to the action origin.
|
|
1497
|
-
* Returns `undefined` if no handler matches (score > 0 required, i.e. at least id must match).
|
|
1498
|
-
*
|
|
1499
|
-
* A handler that currently holds the origin's *live* connection always wins over a mere coordinate
|
|
1500
|
-
* match — so with several duplex acceptors (e.g. WS + WebRTC) a result/push routes back over the carrier
|
|
1501
|
-
* the client actually connected on, never a same-coordinate sibling that lacks the socket. Only when no
|
|
1502
|
-
* handler owns a live connection do we fall back to the plain best-coordinate-score pick (the
|
|
1503
|
-
* single-acceptor and connector-only cases, unchanged).
|
|
1504
|
-
*/
|
|
1505
|
-
getReturnHandlerForOrigin(originClient) {
|
|
1506
|
-
if (originClient.envId === "_unset_") return void 0;
|
|
1507
|
-
let bestScore = -1;
|
|
1508
|
-
let bestHandler;
|
|
1509
|
-
let bestOwnedScore = -1;
|
|
1510
|
-
let bestOwnedHandler;
|
|
1511
|
-
for (const handler of this._registeredPeerHandlers) {
|
|
1512
|
-
if (!handler.canPush) continue;
|
|
1513
|
-
const score = originClient.similarityLevel(handler.peerClient);
|
|
1514
|
-
if (score > bestScore) {
|
|
1515
|
-
bestScore = score;
|
|
1516
|
-
bestHandler = handler;
|
|
1517
|
-
}
|
|
1518
|
-
if (score > bestOwnedScore && handler.ownsLiveConnectionFor(originClient)) {
|
|
1519
|
-
bestOwnedScore = score;
|
|
1520
|
-
bestOwnedHandler = handler;
|
|
1521
|
-
}
|
|
1522
|
-
}
|
|
1523
|
-
if (bestOwnedHandler != null && bestOwnedScore > 0) return bestOwnedHandler;
|
|
1524
|
-
return bestScore > 0 ? bestHandler : void 0;
|
|
1525
|
-
}
|
|
1526
|
-
resetRuntime() {
|
|
1527
|
-
for (const ra of this._pendingRunningActions.values()) ra._abort(err_nice_action.fromId("runtime_reset"));
|
|
1528
|
-
for (const handler of this._registeredPeerHandlers) handler.clearTransportCache();
|
|
1529
|
-
}
|
|
1530
|
-
_trySetupReturnDispatch(runningAction) {
|
|
1531
|
-
if (runningAction.context.schema.responseMode === "none") return;
|
|
1532
|
-
const originClient = runningAction.context.originClient;
|
|
1533
|
-
if (originClient.envId === "_unset_" || originClient.isSameFor(this._coordinate).id) return;
|
|
1534
|
-
runningAction.addUpdateListeners([(update) => {
|
|
1535
|
-
if (update.type === "finished" && update.finishType === "success") this.getReturnHandlerForOrigin(originClient)?.sendReturnPayload(update.response, { targetLocalRuntime: this }).catch(() => {});
|
|
1536
|
-
}]);
|
|
1537
|
-
}
|
|
1538
|
-
};
|
|
1539
|
-
const runtimeState = {
|
|
1540
|
-
defaultLocalRuntime: void 0,
|
|
1541
|
-
assumedRuntimeInfo: void 0
|
|
1542
|
-
};
|
|
1543
|
-
function getDefaultActionRuntime() {
|
|
1544
|
-
if (runtimeState.assumedRuntimeInfo == null) runtimeState.assumedRuntimeInfo = getAssumedRuntimeInfo();
|
|
1545
|
-
if (runtimeState.defaultLocalRuntime == null) runtimeState.defaultLocalRuntime = new ActionRuntime(RuntimeCoordinate.unknown.specify({ perId: `${runtimeState.assumedRuntimeInfo?.runtimeName ?? "unknown"}-runtime` }));
|
|
1546
|
-
return runtimeState.defaultLocalRuntime;
|
|
1547
|
-
}
|
|
1548
|
-
//#endregion
|
|
1549
|
-
//#region src/ActionRuntime/Handler/Local/ActionLocalHandler.ts
|
|
1550
|
-
var ActionLocalHandler = class extends ActionHandler {
|
|
1551
|
-
handlerType = "local";
|
|
1552
|
-
actionRouter = new ActionRouter({
|
|
1553
|
-
contextType: "handler_route",
|
|
1554
|
-
handler: this
|
|
1555
|
-
});
|
|
1556
|
-
constructor() {
|
|
1557
|
-
super();
|
|
1558
|
-
}
|
|
1559
|
-
/**
|
|
1560
|
-
* Register a handler for all actions in a domain.
|
|
1561
|
-
* Receives the full primed action — use `matchAction()` to narrow to a specific action id.
|
|
1562
|
-
* Useful for forwarding all domain actions to a remote endpoint.
|
|
1563
|
-
* Lower priority than `forAction`.
|
|
1564
|
-
*/
|
|
1565
|
-
forDomain(domain, handler) {
|
|
1566
|
-
this.actionRouter.forDomain(domain, handler);
|
|
1567
|
-
return this;
|
|
1568
|
-
}
|
|
1569
|
-
/**
|
|
1570
|
-
* Register a handler for a base action instance. Takes priority over domain-wide handlers.
|
|
1571
|
-
* Receives the full primed action with narrowed input type.
|
|
1572
|
-
* 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.
|
|
1573
|
-
*/
|
|
1574
|
-
forAction(action, handler) {
|
|
1575
|
-
this.actionRouter.forAction(action, handler);
|
|
1576
|
-
return this;
|
|
1577
|
-
}
|
|
1578
|
-
/**
|
|
1579
|
-
* Register a handler for multiple action IDs (first-match-wins among cases).
|
|
1580
|
-
* Receives the full primed action narrowed to the union of those IDs.
|
|
1581
|
-
* Use `act.coreAction.id` to branch on which action was dispatched.
|
|
1582
|
-
*/
|
|
1583
|
-
forActionIds(domain, ids, handler) {
|
|
1584
|
-
this.actionRouter.forActionIds(domain, ids, handler);
|
|
1585
|
-
return this;
|
|
1586
|
-
}
|
|
1587
|
-
/**
|
|
1588
|
-
* Register per-action handlers for a domain using a single map, without needing
|
|
1589
|
-
* separate `forAction` calls. Unregistered action IDs are unaffected.
|
|
1590
|
-
*
|
|
1591
|
-
* @example
|
|
1592
|
-
* ```ts
|
|
1593
|
-
* handler.forDomainActionCases(userDomain, {
|
|
1594
|
-
* getUser: (primed) => db.getUser(primed.input.userId),
|
|
1595
|
-
* deleteUser: (primed) => db.deleteUser(primed.input.userId),
|
|
1596
|
-
* });
|
|
1597
|
-
* ```
|
|
1598
|
-
*/
|
|
1599
|
-
forDomainActionCases(domain, cases) {
|
|
1600
|
-
this.actionRouter.forDomainActionCases(domain, cases);
|
|
1601
|
-
return this;
|
|
1602
|
-
}
|
|
1603
|
-
async handleActionRequest(action, config) {
|
|
1604
|
-
const targetLocalRuntime = config?.targetLocalRuntime ?? ActionRuntime.getDefault();
|
|
1605
|
-
const handler = this.actionRouter.getRouteDataForActionOrThrow(action, { targetLocalRuntime });
|
|
1606
|
-
action.context.addRouteItem({
|
|
1607
|
-
runtime: targetLocalRuntime.coordinate,
|
|
1608
|
-
handler: this.toHandlerRouteItem(),
|
|
1609
|
-
time: Date.now()
|
|
1610
|
-
});
|
|
1611
|
-
const runningAction = new RunningAction({
|
|
1612
|
-
context: action.context,
|
|
1613
|
-
request: action,
|
|
1614
|
-
parentCuid: peekHandlerCuid(),
|
|
1615
|
-
callSite: action._callSite ?? (/* @__PURE__ */ new Error()).stack
|
|
1616
|
-
});
|
|
1617
|
-
this._handleRunningAction(handler, runningAction).catch((err) => {
|
|
1618
|
-
if (err instanceof NiceError) runningAction._completeWithResult(action.errorResult(err));
|
|
1619
|
-
else if (isNiceErrorObject(err)) runningAction._completeWithResult(action.errorResult(castNiceError(err)));
|
|
1620
|
-
else runningAction._abort(err);
|
|
1621
|
-
});
|
|
1622
|
-
return runningAction;
|
|
1623
|
-
}
|
|
1624
|
-
async _handleRunningAction(handler, runningAction) {
|
|
1625
|
-
const state = runningAction.state;
|
|
1626
|
-
if (state.result != null) return;
|
|
1627
|
-
await Promise.resolve();
|
|
1628
|
-
pushHandlerCuid(runningAction.cuid);
|
|
1629
|
-
try {
|
|
1630
|
-
const rawResult = await handler(state.request);
|
|
1631
|
-
let result;
|
|
1632
|
-
if (rawResult instanceof ActionPayload_Result) result = rawResult;
|
|
1633
|
-
else if (rawResult != null && isActionPayload_Result_JsonObject(rawResult)) result = this.actionRouter.domainManager.getActionDomainOrThrow(state.request).hydrateResultPayload(rawResult);
|
|
1634
|
-
else result = state.request.successResult(rawResult);
|
|
1635
|
-
runningAction._completeWithResult(result);
|
|
1636
|
-
} finally {
|
|
1637
|
-
popHandlerCuid();
|
|
1638
|
-
}
|
|
1639
|
-
}
|
|
1640
|
-
async handlePayloadWireOrThrow(wire, config) {
|
|
1641
|
-
const hydratedAction = this.actionRouter.domainManager.hydrateActionPayload(wire);
|
|
1642
|
-
if (!(hydratedAction instanceof ActionPayload_Request)) throw err_nice_action.fromId("wire_action_not_payload", {
|
|
1643
|
-
domain: hydratedAction.domain,
|
|
1644
|
-
actionId: hydratedAction.id,
|
|
1645
|
-
actionState: hydratedAction.type ?? hydratedAction.form
|
|
1646
|
-
});
|
|
1647
|
-
return await this.handleActionRequest(hydratedAction, config);
|
|
1648
|
-
}
|
|
1649
|
-
toJsonObject() {
|
|
1650
|
-
return { type: this.handlerType };
|
|
1651
|
-
}
|
|
1652
|
-
toHandlerRouteItem() {
|
|
1653
|
-
return { type: this.handlerType };
|
|
1654
|
-
}
|
|
1655
|
-
};
|
|
1656
|
-
const createLocalHandler = () => {
|
|
1657
|
-
return new ActionLocalHandler();
|
|
1658
|
-
};
|
|
1659
|
-
//#endregion
|
|
1660
|
-
//#region src/utils/isAction_Context_JsonObject.ts
|
|
1661
|
-
const isAction_Context_JsonObject = (obj) => {
|
|
1662
|
-
return isAction_Base_JsonObject(obj) && obj.form === "context";
|
|
1663
|
-
};
|
|
1664
|
-
//#endregion
|
|
1665
|
-
//#region src/utils/isAction_Core_JsonObject.ts
|
|
1666
|
-
const isAction_Core_JsonObject = (obj) => {
|
|
1667
|
-
return isAction_Base_JsonObject(obj) && obj.form === "core";
|
|
1668
|
-
};
|
|
1669
|
-
//#endregion
|
|
1670
|
-
//#region src/utils/isAction_Any_JsonObject.ts
|
|
1671
|
-
function isAction_Any_JsonObject(obj) {
|
|
1672
|
-
return isActionPayload_Any_JsonObject(obj) || isAction_Context_JsonObject(obj) || isAction_Core_JsonObject(obj);
|
|
1673
|
-
}
|
|
1674
|
-
//#endregion
|
|
1675
|
-
//#region src/utils/assertIsActionJson.ts
|
|
1676
|
-
function assertIsActionJson(obj) {
|
|
1677
|
-
if (!isAction_Any_JsonObject(obj)) throw err_nice_action.fromId("wire_not_action_data");
|
|
1678
|
-
}
|
|
1679
|
-
//#endregion
|
|
1680
|
-
//#region src/utils/isAction_Any_Instance.ts
|
|
1681
|
-
function isAction_Any_Instance(value) {
|
|
1682
|
-
return value instanceof ActionCore || value instanceof ActionPayload || value instanceof ActionContext;
|
|
1683
|
-
}
|
|
1684
|
-
//#endregion
|
|
1685
|
-
//#region src/ActionDefinition/Domain/ActionDomainBase.ts
|
|
1686
|
-
var ActionDomainBase = class {
|
|
1687
|
-
domain;
|
|
1688
|
-
allDomains;
|
|
1689
|
-
actionSchema;
|
|
1690
|
-
_listeners = [];
|
|
1691
|
-
constructor(definition) {
|
|
1692
|
-
this.domain = definition.domain;
|
|
1693
|
-
this.allDomains = definition.allDomains;
|
|
1694
|
-
this.actionSchema = definition.actionSchema;
|
|
1695
|
-
}
|
|
1696
|
-
/**
|
|
1697
|
-
* Add an observer that is called after every action dispatched through this domain.
|
|
1698
|
-
* Returns an unsubscribe function — call it to remove the listener.
|
|
1699
|
-
*/
|
|
1700
|
-
addActionListener(listener) {
|
|
1701
|
-
this._listeners.push(listener);
|
|
1702
|
-
return () => {
|
|
1703
|
-
this._listeners = this._listeners.filter((l) => l !== listener);
|
|
1704
|
-
};
|
|
1705
|
-
}
|
|
1706
|
-
/**
|
|
1707
|
-
* @internal
|
|
1708
|
-
* Observers registered directly on this domain via {@link addActionListener}.
|
|
1709
|
-
* Used to wire observers (e.g. devtools) onto RunningActions that aren't created
|
|
1710
|
-
* through the local-dispatch path — notably inbound actions pushed from a backend
|
|
1711
|
-
* or another client over a bidirectional transport.
|
|
1712
|
-
*/
|
|
1713
|
-
_getActionObservers() {
|
|
1714
|
-
return this._listeners;
|
|
1715
|
-
}
|
|
1716
|
-
};
|
|
1717
|
-
//#endregion
|
|
1718
|
-
//#region src/ActionRuntime/ActionRuntimeManager.ts
|
|
1719
|
-
var ActionRuntimeManager = class {
|
|
1720
|
-
_runtimes = /* @__PURE__ */ new Map();
|
|
1721
|
-
_preferredRuntimeClientId = null;
|
|
1722
|
-
_context;
|
|
1723
|
-
constructor(context) {
|
|
1724
|
-
this._context = context ?? {};
|
|
1725
|
-
}
|
|
1726
|
-
registerRuntime(runtime) {
|
|
1727
|
-
const runtimeId = runtime.coordinate.stringId;
|
|
1728
|
-
if (this._runtimes.has(runtimeId)) throw err_nice_action.fromId("client_runtime_already_registered", {
|
|
1729
|
-
context: this._context,
|
|
1730
|
-
client: runtime.coordinate
|
|
1731
|
-
});
|
|
1732
|
-
for (const id of runtime.coordinate.toStringIds()) {
|
|
1733
|
-
if (this._runtimes.has(id)) continue;
|
|
1734
|
-
this._runtimes.set(id, runtime);
|
|
1735
|
-
}
|
|
1736
|
-
}
|
|
1737
|
-
getRuntimeAndHandlerForAction(action, options, throwOnIssue) {
|
|
1738
|
-
const localRuntime = options?.targetLocalRuntime;
|
|
1739
|
-
if (localRuntime != null) {
|
|
1740
|
-
const runtime = throwOnIssue ? this.getBestRuntimeOrThrow(options?.targetLocalRuntime?.coordinate) : this.getBestRuntime(options?.targetLocalRuntime?.coordinate);
|
|
1741
|
-
if (runtime == null) return;
|
|
1742
|
-
const handler = runtime._getHandlerForAction(action, options);
|
|
1743
|
-
if (handler != null) return {
|
|
1744
|
-
handler,
|
|
1745
|
-
runtime
|
|
1746
|
-
};
|
|
1747
|
-
if (throwOnIssue) throw err_nice_action.fromId("no_action_execution_handler", {
|
|
1748
|
-
domain: action.domain,
|
|
1749
|
-
actionId: action.id,
|
|
1750
|
-
specifiedClient: localRuntime.coordinate
|
|
1751
|
-
});
|
|
1752
|
-
}
|
|
1753
|
-
for (const runtime of this._runtimes.values()) {
|
|
1754
|
-
const handler = runtime._getHandlerForAction(action);
|
|
1755
|
-
if (handler) return {
|
|
1756
|
-
handler,
|
|
1757
|
-
runtime
|
|
1758
|
-
};
|
|
1759
|
-
}
|
|
1760
|
-
if (throwOnIssue) throw err_nice_action.fromId("no_action_execution_handler", {
|
|
1761
|
-
domain: action.domain,
|
|
1762
|
-
actionId: action.id,
|
|
1763
|
-
specifiedClient: options?.targetLocalRuntime?.coordinate
|
|
1764
|
-
});
|
|
1765
|
-
}
|
|
1766
|
-
getRuntimeAndHandlerForActionOrThrow(action, options) {
|
|
1767
|
-
return this.getRuntimeAndHandlerForAction(action, options, true);
|
|
1768
|
-
}
|
|
1769
|
-
setPreferredRuntime(runtime) {
|
|
1770
|
-
const runtimeId = runtime.coordinate.stringId;
|
|
1771
|
-
this._preferredRuntimeClientId = runtimeId;
|
|
1772
|
-
}
|
|
1773
|
-
getPreferredRuntime() {
|
|
1774
|
-
if (this._preferredRuntimeClientId) {
|
|
1775
|
-
const runtime = this._runtimes.get(this._preferredRuntimeClientId);
|
|
1776
|
-
if (runtime) return runtime;
|
|
1777
|
-
}
|
|
1778
|
-
return this._runtimes.values().next().value;
|
|
1779
|
-
}
|
|
1780
|
-
getBestRuntimeForSpecifier(clientSpecifier) {
|
|
1781
|
-
const ids = new RuntimeCoordinate(clientSpecifier).toStringIds();
|
|
1782
|
-
for (const id of ids) {
|
|
1783
|
-
const runtime = this._runtimes.get(id);
|
|
1784
|
-
if (runtime) return runtime;
|
|
1785
|
-
}
|
|
1786
|
-
}
|
|
1787
|
-
getBestRuntime(clientSpecifier) {
|
|
1788
|
-
return clientSpecifier != null ? this.getBestRuntimeForSpecifier(clientSpecifier) : this.getPreferredRuntime();
|
|
1789
|
-
}
|
|
1790
|
-
hasRuntime(runtime) {
|
|
1791
|
-
return this._runtimes.has(runtime.coordinate.stringId);
|
|
1792
|
-
}
|
|
1793
|
-
getBestRuntimeOrThrow(specifier) {
|
|
1794
|
-
const runtime = this.getBestRuntime(specifier);
|
|
1795
|
-
if (!runtime) {
|
|
1796
|
-
if (specifier == null) throw err_nice_action.fromId("no_client_runtimes_registered", { context: this._context });
|
|
1797
|
-
throw err_nice_action.fromId("client_runtime_not_registered", {
|
|
1798
|
-
context: this._context,
|
|
1799
|
-
clientStringId: runtimeCoordinateToStringIds(specifier)[0]
|
|
1800
|
-
});
|
|
1801
|
-
}
|
|
1802
|
-
return runtime;
|
|
1803
|
-
}
|
|
1804
|
-
};
|
|
1805
|
-
//#endregion
|
|
1806
|
-
//#region src/ActionDefinition/Domain/ActionRootDomain.ts
|
|
1807
|
-
var ActionRootDomain = class extends ActionDomainBase {
|
|
1808
|
-
domainDefinition;
|
|
1809
|
-
_actionRuntimeManager;
|
|
1810
|
-
constructor(domainDefinition) {
|
|
1811
|
-
const domainId = domainDefinition.domain;
|
|
1812
|
-
super({
|
|
1813
|
-
domain: domainId,
|
|
1814
|
-
allDomains: [domainId],
|
|
1815
|
-
actionSchema: {}
|
|
1816
|
-
});
|
|
1817
|
-
this.domainDefinition = domainDefinition;
|
|
1818
|
-
this._actionRuntimeManager = new ActionRuntimeManager({ domain: domainId });
|
|
1819
|
-
}
|
|
1820
|
-
createChildDomain(subDomainDef) {
|
|
1821
|
-
if (this.allDomains.includes(subDomainDef.domain)) throw err_nice_action.fromId("domain_already_exists_in_hierarchy", {
|
|
1822
|
-
domain: subDomainDef.domain,
|
|
1823
|
-
allParentDomains: this.allDomains,
|
|
1824
|
-
parentDomain: this.domain
|
|
1825
|
-
});
|
|
1826
|
-
return new ActionDomain({
|
|
1827
|
-
allDomains: [...this.allDomains, subDomainDef.domain],
|
|
1828
|
-
domain: subDomainDef.domain,
|
|
1829
|
-
actionSchema: subDomainDef.actions
|
|
1830
|
-
}, { rootDomain: this });
|
|
1831
|
-
}
|
|
1832
|
-
_registerRuntime(runtime) {
|
|
1833
|
-
this._actionRuntimeManager.registerRuntime(runtime);
|
|
1834
|
-
}
|
|
1835
|
-
_hasRuntime(runtime) {
|
|
1836
|
-
return this._actionRuntimeManager.hasRuntime(runtime);
|
|
1837
|
-
}
|
|
1838
|
-
getRuntime(clientSpecifier) {
|
|
1839
|
-
return this._actionRuntimeManager.getBestRuntimeForSpecifier(clientSpecifier);
|
|
1840
|
-
}
|
|
1841
|
-
async _runAction(actionPayload, options) {
|
|
1842
|
-
const allListeners = [...this._listeners, ...options?.listeners ?? []];
|
|
1843
|
-
let handlerAndRuntime;
|
|
1844
|
-
try {
|
|
1845
|
-
handlerAndRuntime = this._actionRuntimeManager.getRuntimeAndHandlerForActionOrThrow(actionPayload, options);
|
|
1846
|
-
} catch (err) {
|
|
1847
|
-
const runningAction = new RunningAction({
|
|
1848
|
-
context: actionPayload.context,
|
|
1849
|
-
request: actionPayload,
|
|
1850
|
-
callSite: actionPayload._callSite
|
|
1851
|
-
});
|
|
1852
|
-
runningAction.addUpdateListeners(allListeners);
|
|
1853
|
-
runningAction._failWithError(err);
|
|
1854
|
-
throw err;
|
|
1855
|
-
}
|
|
1856
|
-
const { handler, runtime } = handlerAndRuntime;
|
|
1857
|
-
actionPayload.context._setOriginClient(runtime.coordinate);
|
|
1858
|
-
const runningAction = await handler.handleActionRequest(actionPayload, { targetLocalRuntime: runtime });
|
|
1859
|
-
runningAction.addUpdateListeners(allListeners);
|
|
1860
|
-
return runningAction;
|
|
1861
|
-
}
|
|
1862
|
-
};
|
|
1863
|
-
//#endregion
|
|
1864
|
-
//#region src/ActionDefinition/Domain/ActionDomain.ts
|
|
1865
|
-
var ActionDomain = class ActionDomain extends ActionDomainBase {
|
|
1866
|
-
_rootDomain;
|
|
1867
|
-
_actionMap;
|
|
1868
|
-
constructor(definition, { rootDomain }) {
|
|
1869
|
-
super(definition);
|
|
1870
|
-
this._rootDomain = rootDomain;
|
|
1871
|
-
this._actionMap = this.createActionMap();
|
|
1872
|
-
}
|
|
1873
|
-
get rootDomain() {
|
|
1874
|
-
return this._rootDomain;
|
|
1875
|
-
}
|
|
1876
|
-
/**
|
|
1877
|
-
* @internal
|
|
1878
|
-
* All action observers that should see actions on this domain: the root domain's
|
|
1879
|
-
* observers plus this subdomain's own. Mirrors the listener set the local-dispatch
|
|
1880
|
-
* path assembles in `runAction`/`_runAction`, so inbound actions (pushed from a
|
|
1881
|
-
* backend or another client) can be wired up identically and surface in devtools.
|
|
1882
|
-
*/
|
|
1883
|
-
_collectActionObservers() {
|
|
1884
|
-
return [...this._rootDomain._getActionObservers(), ...this._getActionObservers()];
|
|
1885
|
-
}
|
|
1886
|
-
_registerRuntime(runtime) {
|
|
1887
|
-
this._rootDomain._registerRuntime(runtime);
|
|
1888
|
-
}
|
|
1889
|
-
createChildDomain(subDomainDef) {
|
|
1890
|
-
if (this.allDomains.includes(subDomainDef.domain)) throw err_nice_action.fromId("domain_already_exists_in_hierarchy", {
|
|
1891
|
-
domain: subDomainDef.domain,
|
|
1892
|
-
allParentDomains: this.allDomains,
|
|
1893
|
-
parentDomain: this.domain
|
|
1894
|
-
});
|
|
1895
|
-
return new ActionDomain({
|
|
1896
|
-
allDomains: [...this.allDomains, subDomainDef.domain],
|
|
1897
|
-
domain: subDomainDef.domain,
|
|
1898
|
-
actionSchema: subDomainDef.actions
|
|
1899
|
-
}, { rootDomain: this._rootDomain });
|
|
1900
|
-
}
|
|
1901
|
-
get action() {
|
|
1902
|
-
return this._actionMap;
|
|
1903
|
-
}
|
|
1904
|
-
actionsMap() {
|
|
1905
|
-
return this._actionMap;
|
|
1906
|
-
}
|
|
1907
|
-
actionForId(id) {
|
|
1908
|
-
if (!this.actionSchema[id]) throw err_nice_action.fromId("action_id_not_in_domain", {
|
|
1909
|
-
domain: this.domain,
|
|
1910
|
-
actionId: id
|
|
1911
|
-
});
|
|
1912
|
-
return new ActionCore(this, id);
|
|
1913
|
-
}
|
|
1914
|
-
wrapAsPartialLocalHandler(wrappedActionExecutor) {
|
|
1915
|
-
const _handler = new ActionLocalHandler();
|
|
1916
|
-
const executor = wrappedActionExecutor;
|
|
1917
|
-
for (const actionKey in wrappedActionExecutor) {
|
|
1918
|
-
if (!this.actionSchema[actionKey]) continue;
|
|
1919
|
-
_handler.forAction(this.actionForId(actionKey), (request) => executor[request.id](request.input));
|
|
1920
|
-
}
|
|
1921
|
-
return _handler;
|
|
1922
|
-
}
|
|
1923
|
-
wrapAsLocalHandler(wrappedActionExecutor) {
|
|
1924
|
-
const _handler = new ActionLocalHandler();
|
|
1925
|
-
const executor = wrappedActionExecutor;
|
|
1926
|
-
return _handler.forDomain(this, (request) => executor[request.id](request.input));
|
|
1927
|
-
}
|
|
1928
|
-
hydrateContext(id, contextData) {
|
|
1929
|
-
return new ActionContext(this, id, {
|
|
1930
|
-
timeCreated: contextData.timeCreated,
|
|
1931
|
-
cuid: contextData.cuid,
|
|
1932
|
-
routing: contextData.routing.map((item) => {
|
|
1933
|
-
return {
|
|
1934
|
-
runtime: new RuntimeCoordinate(item.runtime),
|
|
1935
|
-
handler: item.handler,
|
|
1936
|
-
time: item.time
|
|
1937
|
-
};
|
|
1938
|
-
}),
|
|
1939
|
-
originClient: contextData.originClient ? new RuntimeCoordinate(contextData.originClient) : RuntimeCoordinate.unknown
|
|
1940
|
-
});
|
|
1941
|
-
}
|
|
1942
|
-
isDomainAction(action) {
|
|
1943
|
-
return isAction_Any_Instance(action) && action.domain === this.domain;
|
|
1944
|
-
}
|
|
1945
|
-
hydrateRequestPayload(serialized) {
|
|
1946
|
-
if (serialized.type !== "request") throw err_nice_action.fromId("hydration_action_state_mismatch", {
|
|
1947
|
-
expected: "request",
|
|
1948
|
-
received: serialized.type
|
|
1949
|
-
});
|
|
1950
|
-
if (serialized.domain !== this.domain) throw err_nice_action.fromId("hydration_domain_mismatch", {
|
|
1951
|
-
expected: this.domain,
|
|
1952
|
-
received: serialized.domain
|
|
1953
|
-
});
|
|
1954
|
-
const id = serialized.id;
|
|
1955
|
-
if (!this.actionSchema[id]) throw err_nice_action.fromId("hydration_action_id_not_found", {
|
|
1956
|
-
domain: this.domain,
|
|
1957
|
-
actionId: serialized.id
|
|
1958
|
-
});
|
|
1959
|
-
const contextAction = this.hydrateContext(id, serialized.context);
|
|
1960
|
-
return new ActionPayload_Request({ context: contextAction }, contextAction.deserializeInput(serialized.input), { time: serialized.time });
|
|
1961
|
-
}
|
|
1962
|
-
hydrateResultPayload(serialized) {
|
|
1963
|
-
if (serialized.type !== "result") throw err_nice_action.fromId("hydration_action_state_mismatch", {
|
|
1964
|
-
expected: "result",
|
|
1965
|
-
received: serialized.type
|
|
1966
|
-
});
|
|
1967
|
-
if (serialized.domain !== this.domain) throw err_nice_action.fromId("hydration_domain_mismatch", {
|
|
1968
|
-
expected: this.domain,
|
|
1969
|
-
received: serialized.domain
|
|
1970
|
-
});
|
|
1971
|
-
const id = serialized.id;
|
|
1972
|
-
if (!this.actionSchema[id]) throw err_nice_action.fromId("hydration_action_id_not_found", {
|
|
1973
|
-
domain: this.domain,
|
|
1974
|
-
actionId: serialized.id
|
|
1975
|
-
});
|
|
1976
|
-
const contextAction = this.hydrateContext(id, serialized.context);
|
|
1977
|
-
const result = serialized.result.ok ? {
|
|
1978
|
-
ok: true,
|
|
1979
|
-
output: contextAction.schema.deserializeOutput(serialized.result.output)
|
|
1980
|
-
} : serialized.result;
|
|
1981
|
-
return new ActionPayload_Result({ context: contextAction }, result, { time: serialized.time });
|
|
1982
|
-
}
|
|
1983
|
-
hydrateAnyAction(actionJson) {
|
|
1984
|
-
assertIsActionJson(actionJson);
|
|
1985
|
-
if (actionJson.form === "data") {
|
|
1986
|
-
if (actionJson.type === "request") return this.hydrateRequestPayload(actionJson);
|
|
1987
|
-
if (actionJson.type === "result") return this.hydrateResultPayload(actionJson);
|
|
1988
|
-
}
|
|
1989
|
-
return this.actionForId(actionJson.id);
|
|
1990
|
-
}
|
|
1991
|
-
async runAction(request, options) {
|
|
1992
|
-
const allListeners = [...options?.listeners ?? [], ...this._listeners];
|
|
1993
|
-
return this._rootDomain._runAction(request, {
|
|
1994
|
-
...options,
|
|
1995
|
-
listeners: allListeners
|
|
1996
|
-
});
|
|
1997
|
-
}
|
|
1998
|
-
createActionMap() {
|
|
1999
|
-
const map = {};
|
|
2000
|
-
for (const id in this.actionSchema) map[id] = new ActionCore(this, id);
|
|
2001
|
-
return map;
|
|
2002
|
-
}
|
|
2003
|
-
};
|
|
2004
|
-
//#endregion
|
|
2005
|
-
//#region src/ActionDefinition/Domain/helpers/createRootActionDomain.ts
|
|
2006
|
-
const createActionRootDomain = (definition) => {
|
|
2007
|
-
return new ActionRootDomain(definition);
|
|
2008
|
-
};
|
|
2009
|
-
//#endregion
|
|
2010
|
-
//#region src/ActionRuntime/Transport/crypto/actionHandshake.ts
|
|
2011
|
-
/**
|
|
2012
|
-
* Authenticated handshake for the WebSocket channel. Run once per connection, before any action
|
|
2013
|
-
* frames flow, it:
|
|
2014
|
-
* - exchanges each side's {@link RuntimeCoordinate} + public keys,
|
|
2015
|
-
* - checks both ends share the same wire dictionary version (closes the positional-dictionary footgun),
|
|
2016
|
-
* - has the client prove control of its verify (Ed25519) key by signing a fresh challenge that binds
|
|
2017
|
-
* both nonces, both identities, the dictionary version, the level, and every exchanged public key,
|
|
2018
|
-
* - establishes a `ClientCryptoKeyLink` link on both sides (so the server can verify the signature and,
|
|
2019
|
-
* for the `encrypted` level, both can derive a shared AES-GCM key with the verify keys folded in).
|
|
2020
|
-
*
|
|
2021
|
-
* This module is transport-agnostic: it produces/consumes message objects. The transport + server
|
|
2022
|
-
* handler drive the I/O and the connection phase (Step 4). Keep `none`-level connections from ever
|
|
2023
|
-
* reaching here — they skip the handshake entirely.
|
|
2024
|
-
*/
|
|
2025
|
-
const HANDSHAKE_PROTOCOL = "nice-ws-hs/1";
|
|
2026
|
-
/** How much the channel protects after the handshake — chosen by the consumer (perf vs security). */
|
|
2027
|
-
let ESecurityLevel = /* @__PURE__ */ function(ESecurityLevel) {
|
|
2028
|
-
/** No handshake; identity is self-asserted (fastest, dev / trusted networks). */
|
|
2029
|
-
ESecurityLevel["none"] = "none";
|
|
2030
|
-
/** Handshake authenticates identity (sign/verify + key pin); frames stay plaintext over TLS. */
|
|
2031
|
-
ESecurityLevel["authenticated"] = "authenticated";
|
|
2032
|
-
/** Authenticated handshake + every frame AES-GCM encrypted with the derived shared key. */
|
|
2033
|
-
ESecurityLevel["encrypted"] = "encrypted";
|
|
2034
|
-
return ESecurityLevel;
|
|
2035
|
-
}({});
|
|
2036
|
-
let EHandshakeMessageType = /* @__PURE__ */ function(EHandshakeMessageType) {
|
|
2037
|
-
EHandshakeMessageType["hello"] = "hello";
|
|
2038
|
-
EHandshakeMessageType["welcome"] = "welcome";
|
|
2039
|
-
EHandshakeMessageType["prove"] = "prove";
|
|
2040
|
-
EHandshakeMessageType["accept"] = "accept";
|
|
2041
|
-
EHandshakeMessageType["reject"] = "reject";
|
|
2042
|
-
return EHandshakeMessageType;
|
|
2043
|
-
}({});
|
|
2044
|
-
const vEd25519Raw = v.custom((val) => typeof val === "string" && val.startsWith("ed25519::raw_base64::"));
|
|
2045
|
-
const vX25519Raw = v.custom((val) => typeof val === "string" && val.startsWith("x25519::raw_base64::"));
|
|
2046
|
-
const vCoordinate = v.object({
|
|
2047
|
-
envId: v.string(),
|
|
2048
|
-
perId: v.optional(v.string()),
|
|
2049
|
-
insId: v.optional(v.string())
|
|
2050
|
-
});
|
|
2051
|
-
const vSecurityLevel = v.picklist([
|
|
2052
|
-
"none",
|
|
2053
|
-
"authenticated",
|
|
2054
|
-
"encrypted"
|
|
2055
|
-
]);
|
|
2056
|
-
const vHsHello = v.object({
|
|
2057
|
-
t: v.literal("hello"),
|
|
2058
|
-
protocol: v.string(),
|
|
2059
|
-
securityLevel: vSecurityLevel,
|
|
2060
|
-
dictionaryVersion: v.string(),
|
|
2061
|
-
client: vCoordinate,
|
|
2062
|
-
clientNonce: v.string(),
|
|
2063
|
-
verifyPublicKey: vEd25519Raw,
|
|
2064
|
-
exchangePublicKey: v.optional(vX25519Raw)
|
|
2065
|
-
});
|
|
2066
|
-
const vHsWelcome = v.object({
|
|
2067
|
-
t: v.literal("welcome"),
|
|
2068
|
-
securityLevel: vSecurityLevel,
|
|
2069
|
-
dictionaryVersion: v.string(),
|
|
2070
|
-
server: vCoordinate,
|
|
2071
|
-
serverNonce: v.string(),
|
|
2072
|
-
verifyPublicKey: vEd25519Raw,
|
|
2073
|
-
exchangePublicKey: v.optional(vX25519Raw)
|
|
2074
|
-
});
|
|
2075
|
-
const vHsProve = v.object({
|
|
2076
|
-
t: v.literal("prove"),
|
|
2077
|
-
signatureBase64: v.string()
|
|
2078
|
-
});
|
|
2079
|
-
const vHsAccept = v.object({
|
|
2080
|
-
t: v.literal("accept"),
|
|
2081
|
-
signatureBase64: v.optional(v.string())
|
|
2082
|
-
});
|
|
2083
|
-
const vHsReject = v.object({
|
|
2084
|
-
t: v.literal("reject"),
|
|
2085
|
-
reason: v.string()
|
|
2086
|
-
});
|
|
2087
|
-
const vHandshakeMessage = v.variant("t", [
|
|
2088
|
-
vHsHello,
|
|
2089
|
-
vHsWelcome,
|
|
2090
|
-
vHsProve,
|
|
2091
|
-
vHsAccept,
|
|
2092
|
-
vHsReject
|
|
2093
|
-
]);
|
|
2094
|
-
/** Serialize a handshake message for the wire (handshake frames are JSON — they aren't the hot path). */
|
|
2095
|
-
function encodeHandshakeMessage(message) {
|
|
2096
|
-
return JSON.stringify(message);
|
|
2097
|
-
}
|
|
2098
|
-
/** Parse + structurally validate an incoming handshake frame; `undefined` if it isn't one. */
|
|
2099
|
-
function decodeHandshakeMessage(raw) {
|
|
2100
|
-
let parsed;
|
|
2101
|
-
try {
|
|
2102
|
-
parsed = JSON.parse(raw);
|
|
2103
|
-
} catch {
|
|
2104
|
-
return;
|
|
2105
|
-
}
|
|
2106
|
-
const result = v.safeParse(vHandshakeMessage, parsed);
|
|
2107
|
-
return result.success ? result.output : void 0;
|
|
2108
|
-
}
|
|
2109
|
-
/** Stable link id for a runtime coordinate — the key both the crypto link and the connection use. */
|
|
2110
|
-
function runtimeLinkId(coordinate) {
|
|
2111
|
-
return `runtime::${new RuntimeCoordinate(coordinate).stringId}`;
|
|
2112
|
-
}
|
|
2113
|
-
function coordId(coordinate) {
|
|
2114
|
-
return new RuntimeCoordinate(coordinate).stringId;
|
|
2115
|
-
}
|
|
2116
|
-
function sessionSalt(clientNonce, serverNonce) {
|
|
2117
|
-
return `${clientNonce}::${serverNonce}`;
|
|
2118
|
-
}
|
|
2119
|
-
function handshakeInfo(dictionaryVersion) {
|
|
2120
|
-
return `${HANDSHAKE_PROTOCOL}::${dictionaryVersion}`;
|
|
2121
|
-
}
|
|
2122
|
-
/**
|
|
2123
|
-
* The exact string both sides sign/verify. JSON-encoded ordered array so field boundaries are
|
|
2124
|
-
* unambiguous; binds identities, nonces (freshness), version, level, and all exchanged public keys
|
|
2125
|
-
* (authenticating the keys via the signature, complementing `bindVerifyKeysIntoDerivation`).
|
|
2126
|
-
*/
|
|
2127
|
-
function buildHandshakeChallenge(parts) {
|
|
2128
|
-
return JSON.stringify([
|
|
2129
|
-
HANDSHAKE_PROTOCOL,
|
|
2130
|
-
parts.securityLevel,
|
|
2131
|
-
parts.dictionaryVersion,
|
|
2132
|
-
parts.clientCoordId,
|
|
2133
|
-
parts.serverCoordId,
|
|
2134
|
-
parts.clientNonce,
|
|
2135
|
-
parts.serverNonce,
|
|
2136
|
-
parts.clientVerifyKey,
|
|
2137
|
-
parts.serverVerifyKey,
|
|
2138
|
-
parts.clientExchangeKey ?? "_",
|
|
2139
|
-
parts.serverExchangeKey ?? "_"
|
|
2140
|
-
]);
|
|
2141
|
-
}
|
|
2142
|
-
function reject(reason) {
|
|
2143
|
-
return {
|
|
2144
|
-
t: "reject",
|
|
2145
|
-
reason
|
|
2146
|
-
};
|
|
2147
|
-
}
|
|
2148
|
-
function tofuPinKey(client) {
|
|
2149
|
-
return `${client.envId}::${client.perId ?? client.insId ?? "_"}`;
|
|
2150
|
-
}
|
|
2151
|
-
/**
|
|
2152
|
-
* In-memory trust-on-first-use resolver: trusts (and pins) the first verify key seen for a client
|
|
2153
|
-
* identity, then rejects a different key for that identity. The default; replace with a storage-backed
|
|
2154
|
-
* resolver for cross-restart pinning (see Step 5).
|
|
2155
|
-
*/
|
|
2156
|
-
function createInMemoryTofuVerifyKeyResolver() {
|
|
2157
|
-
const pinned = /* @__PURE__ */ new Map();
|
|
2158
|
-
return { async resolve({ client, verifyPublicKey }) {
|
|
2159
|
-
const key = tofuPinKey(client);
|
|
2160
|
-
const existing = pinned.get(key);
|
|
2161
|
-
if (existing == null) {
|
|
2162
|
-
pinned.set(key, verifyPublicKey);
|
|
2163
|
-
return { trusted: true };
|
|
2164
|
-
}
|
|
2165
|
-
if (existing === verifyPublicKey) return { trusted: true };
|
|
2166
|
-
return {
|
|
2167
|
-
trusted: false,
|
|
2168
|
-
reason: "verify key changed for client identity (pin mismatch)"
|
|
2169
|
-
};
|
|
2170
|
-
} };
|
|
2171
|
-
}
|
|
2172
|
-
/**
|
|
2173
|
-
* Storage-backed trust-on-first-use resolver: pins survive process restarts / Durable Object eviction
|
|
2174
|
-
* (e.g. back it with `createDurableObjectStorageAdapter`). Same policy as the in-memory variant — trust
|
|
2175
|
-
* + pin the first verify key per client identity, reject a different one thereafter.
|
|
2176
|
-
*/
|
|
2177
|
-
function createStorageTofuVerifyKeyResolver(storageAdapter) {
|
|
2178
|
-
const storage = createTypedStorage({ storageAdapter });
|
|
2179
|
-
return { async resolve({ client, verifyPublicKey }) {
|
|
2180
|
-
const key = tofuPinKey(client);
|
|
2181
|
-
const existing = (await storage.getJson("pins"))?.[key];
|
|
2182
|
-
if (existing == null) {
|
|
2183
|
-
await storage.updateJsonWithDef("pins", {}, (current) => ({
|
|
2184
|
-
...current,
|
|
2185
|
-
[key]: verifyPublicKey
|
|
2186
|
-
}));
|
|
2187
|
-
return { trusted: true };
|
|
2188
|
-
}
|
|
2189
|
-
if (existing === verifyPublicKey) return { trusted: true };
|
|
2190
|
-
return {
|
|
2191
|
-
trusted: false,
|
|
2192
|
-
reason: "verify key changed for client identity (pin mismatch)"
|
|
2193
|
-
};
|
|
2194
|
-
} };
|
|
2195
|
-
}
|
|
2196
|
-
function createClientHandshake(config) {
|
|
2197
|
-
const { link, localCoordinate, dictionaryVersion, securityLevel } = config;
|
|
2198
|
-
const wantsEncryption = securityLevel === "encrypted";
|
|
2199
|
-
const clientNonce = nanoid();
|
|
2200
|
-
let pending;
|
|
2201
|
-
return {
|
|
2202
|
-
async createHello() {
|
|
2203
|
-
return {
|
|
2204
|
-
t: "hello",
|
|
2205
|
-
protocol: HANDSHAKE_PROTOCOL,
|
|
2206
|
-
securityLevel,
|
|
2207
|
-
dictionaryVersion,
|
|
2208
|
-
client: localCoordinate,
|
|
2209
|
-
clientNonce,
|
|
2210
|
-
verifyPublicKey: await link.getLocalVerifyPublicKey(),
|
|
2211
|
-
exchangePublicKey: wantsEncryption ? await link.getLocalExchangePublicKey() : void 0
|
|
2212
|
-
};
|
|
2213
|
-
},
|
|
2214
|
-
async onWelcome(welcome) {
|
|
2215
|
-
if (welcome.dictionaryVersion !== dictionaryVersion) throw new Error("[ws-handshake] server dictionary version mismatch");
|
|
2216
|
-
if (welcome.securityLevel !== securityLevel) throw new Error("[ws-handshake] server security level mismatch");
|
|
2217
|
-
if (wantsEncryption && welcome.exchangePublicKey == null) throw new Error("[ws-handshake] server did not provide an exchange key for encryption");
|
|
2218
|
-
const linkedServerId = runtimeLinkId(welcome.server);
|
|
2219
|
-
await link.linkClient({
|
|
2220
|
-
linkedClientId: linkedServerId,
|
|
2221
|
-
verifyPublicKey: welcome.verifyPublicKey,
|
|
2222
|
-
...wantsEncryption ? {
|
|
2223
|
-
exchangePublicKey: welcome.exchangePublicKey,
|
|
2224
|
-
saltString: sessionSalt(clientNonce, welcome.serverNonce),
|
|
2225
|
-
infoString: handshakeInfo(dictionaryVersion),
|
|
2226
|
-
bindVerifyKeysIntoDerivation: true
|
|
2227
|
-
} : {}
|
|
2228
|
-
});
|
|
2229
|
-
const challenge = buildHandshakeChallenge({
|
|
2230
|
-
securityLevel,
|
|
2231
|
-
dictionaryVersion,
|
|
2232
|
-
clientCoordId: coordId(localCoordinate),
|
|
2233
|
-
serverCoordId: coordId(welcome.server),
|
|
2234
|
-
clientNonce,
|
|
2235
|
-
serverNonce: welcome.serverNonce,
|
|
2236
|
-
clientVerifyKey: await link.getLocalVerifyPublicKey(),
|
|
2237
|
-
serverVerifyKey: welcome.verifyPublicKey,
|
|
2238
|
-
clientExchangeKey: wantsEncryption ? await link.getLocalExchangePublicKey() : void 0,
|
|
2239
|
-
serverExchangeKey: welcome.exchangePublicKey
|
|
2240
|
-
});
|
|
2241
|
-
pending = {
|
|
2242
|
-
linkedServerId,
|
|
2243
|
-
server: welcome.server,
|
|
2244
|
-
challenge
|
|
2245
|
-
};
|
|
2246
|
-
return {
|
|
2247
|
-
t: "prove",
|
|
2248
|
-
signatureBase64: (await link.signChallenge([challenge])).signatureBase64
|
|
2249
|
-
};
|
|
2250
|
-
},
|
|
2251
|
-
async onAccept(accept) {
|
|
2252
|
-
if (pending == null) throw new Error("[ws-handshake] accept before welcome");
|
|
2253
|
-
if (accept.signatureBase64 != null) {
|
|
2254
|
-
if (!await link.verifyChallengeFromLinkedClient({
|
|
2255
|
-
linkedClientId: pending.linkedServerId,
|
|
2256
|
-
challenge: pending.challenge,
|
|
2257
|
-
signatureBase64: accept.signatureBase64
|
|
2258
|
-
})) throw new Error("[ws-handshake] server signature invalid");
|
|
2259
|
-
}
|
|
2260
|
-
return {
|
|
2261
|
-
linkedClientId: pending.linkedServerId,
|
|
2262
|
-
remote: pending.server,
|
|
2263
|
-
securityLevel
|
|
2264
|
-
};
|
|
2265
|
-
}
|
|
2266
|
-
};
|
|
2267
|
-
}
|
|
2268
|
-
function createServerHandshake(config) {
|
|
2269
|
-
const { link, localCoordinate, dictionaryVersion } = config;
|
|
2270
|
-
const allowedLevels = Array.isArray(config.securityLevel) ? config.securityLevel : [config.securityLevel];
|
|
2271
|
-
const verifyKeyResolver = config.verifyKeyResolver ?? createInMemoryTofuVerifyKeyResolver();
|
|
2272
|
-
const serverNonce = nanoid();
|
|
2273
|
-
let pending;
|
|
2274
|
-
let result;
|
|
2275
|
-
return {
|
|
2276
|
-
async onHello(hello) {
|
|
2277
|
-
if (hello.protocol !== HANDSHAKE_PROTOCOL) return reject("unsupported handshake protocol");
|
|
2278
|
-
if (hello.dictionaryVersion !== dictionaryVersion) return reject("dictionary version mismatch");
|
|
2279
|
-
const negotiatedLevel = hello.securityLevel;
|
|
2280
|
-
if (negotiatedLevel === "none" || !allowedLevels.includes(negotiatedLevel)) return reject("security level not allowed");
|
|
2281
|
-
const wantsEncryption = negotiatedLevel === "encrypted";
|
|
2282
|
-
if (wantsEncryption && hello.exchangePublicKey == null) return reject("missing exchange key for encryption");
|
|
2283
|
-
const linkedClientId = runtimeLinkId(hello.client);
|
|
2284
|
-
await link.linkClient({
|
|
2285
|
-
linkedClientId,
|
|
2286
|
-
verifyPublicKey: hello.verifyPublicKey,
|
|
2287
|
-
...wantsEncryption ? {
|
|
2288
|
-
exchangePublicKey: hello.exchangePublicKey,
|
|
2289
|
-
saltString: sessionSalt(hello.clientNonce, serverNonce),
|
|
2290
|
-
infoString: handshakeInfo(dictionaryVersion),
|
|
2291
|
-
bindVerifyKeysIntoDerivation: true
|
|
2292
|
-
} : {}
|
|
2293
|
-
});
|
|
2294
|
-
const serverVerifyKey = await link.getLocalVerifyPublicKey();
|
|
2295
|
-
const serverExchangeKey = wantsEncryption ? await link.getLocalExchangePublicKey() : void 0;
|
|
2296
|
-
const keyMaterial = wantsEncryption && hello.exchangePublicKey != null ? {
|
|
2297
|
-
verifyPublicKey: hello.verifyPublicKey,
|
|
2298
|
-
exchangePublicKey: hello.exchangePublicKey,
|
|
2299
|
-
saltString: sessionSalt(hello.clientNonce, serverNonce),
|
|
2300
|
-
infoString: handshakeInfo(dictionaryVersion),
|
|
2301
|
-
bindVerifyKeysIntoDerivation: true
|
|
2302
|
-
} : void 0;
|
|
2303
|
-
pending = {
|
|
2304
|
-
client: hello.client,
|
|
2305
|
-
linkedClientId,
|
|
2306
|
-
clientVerifyKey: hello.verifyPublicKey,
|
|
2307
|
-
negotiatedLevel,
|
|
2308
|
-
keyMaterial,
|
|
2309
|
-
challenge: buildHandshakeChallenge({
|
|
2310
|
-
securityLevel: negotiatedLevel,
|
|
2311
|
-
dictionaryVersion,
|
|
2312
|
-
clientCoordId: coordId(hello.client),
|
|
2313
|
-
serverCoordId: coordId(localCoordinate),
|
|
2314
|
-
clientNonce: hello.clientNonce,
|
|
2315
|
-
serverNonce,
|
|
2316
|
-
clientVerifyKey: hello.verifyPublicKey,
|
|
2317
|
-
serverVerifyKey,
|
|
2318
|
-
clientExchangeKey: hello.exchangePublicKey,
|
|
2319
|
-
serverExchangeKey
|
|
2320
|
-
})
|
|
2321
|
-
};
|
|
2322
|
-
return {
|
|
2323
|
-
t: "welcome",
|
|
2324
|
-
securityLevel: negotiatedLevel,
|
|
2325
|
-
dictionaryVersion,
|
|
2326
|
-
server: localCoordinate,
|
|
2327
|
-
serverNonce,
|
|
2328
|
-
verifyPublicKey: serverVerifyKey,
|
|
2329
|
-
exchangePublicKey: serverExchangeKey
|
|
2330
|
-
};
|
|
2331
|
-
},
|
|
2332
|
-
async onProve(prove) {
|
|
2333
|
-
if (pending == null) return reject("prove before hello");
|
|
2334
|
-
if (!await link.verifyChallengeFromLinkedClient({
|
|
2335
|
-
linkedClientId: pending.linkedClientId,
|
|
2336
|
-
challenge: pending.challenge,
|
|
2337
|
-
signatureBase64: prove.signatureBase64
|
|
2338
|
-
})) return reject("invalid client signature");
|
|
2339
|
-
const trust = await verifyKeyResolver.resolve({
|
|
2340
|
-
client: pending.client,
|
|
2341
|
-
verifyPublicKey: pending.clientVerifyKey
|
|
2342
|
-
});
|
|
2343
|
-
if (!trust.trusted) return reject(trust.reason ?? "client verify key not trusted");
|
|
2344
|
-
result = {
|
|
2345
|
-
linkedClientId: pending.linkedClientId,
|
|
2346
|
-
remote: pending.client,
|
|
2347
|
-
securityLevel: pending.negotiatedLevel,
|
|
2348
|
-
encryptionKeyMaterial: pending.keyMaterial
|
|
2349
|
-
};
|
|
2350
|
-
return {
|
|
2351
|
-
t: "accept",
|
|
2352
|
-
signatureBase64: (await link.signChallenge([pending.challenge])).signatureBase64
|
|
2353
|
-
};
|
|
2354
|
-
},
|
|
2355
|
-
/** The completed handshake result once `onProve` has accepted, else `undefined`. */
|
|
2356
|
-
getResult() {
|
|
2357
|
-
return result;
|
|
2358
|
-
}
|
|
2359
|
-
};
|
|
2360
|
-
}
|
|
2361
|
-
//#endregion
|
|
2362
|
-
//#region src/utils/decodeActionFrame.ts
|
|
2363
|
-
/**
|
|
2364
|
-
* Decode a single inbound channel frame (text or binary) into validated action wire JSON, or
|
|
2365
|
-
* `undefined` if it isn't a recognisable action payload.
|
|
2366
|
-
*
|
|
2367
|
-
* Shared by the WebSocket transport's message listener and the server-side `AcceptorHandler` so
|
|
2368
|
-
* both decode identically: a binary `decoder.incoming` (e.g. msgpackr) takes precedence, and plain
|
|
2369
|
-
* text frames fall back to JSON — keeping binary and JSON clients interoperable on one channel.
|
|
2370
|
-
*/
|
|
2371
|
-
function decodeActionFrame(frame, decoder) {
|
|
2372
|
-
const decoded = decoder?.incoming?.(frame) ?? (typeof frame === "string" ? parseJsonActionFrame(frame) : void 0);
|
|
2373
|
-
return decoded != null && isActionPayload_Any_JsonObject(decoded) ? decoded : void 0;
|
|
2374
|
-
}
|
|
2375
|
-
function parseJsonActionFrame(message) {
|
|
2376
|
-
try {
|
|
2377
|
-
const json = JSON.parse(message);
|
|
2378
|
-
return isActionPayload_Any_JsonObject(json) ? json : void 0;
|
|
2379
|
-
} catch {
|
|
2380
|
-
return;
|
|
2381
|
-
}
|
|
2382
|
-
}
|
|
2383
|
-
//#endregion
|
|
2384
|
-
//#region src/ActionRuntime/Transport/crypto/actionFrameCrypto.ts
|
|
2385
|
-
const ENCRYPTED_ENVELOPE_LENGTH = 2;
|
|
2386
|
-
/**
|
|
2387
|
-
* Build the encrypt/decrypt transform for a connection whose handshake settled on the `encrypted`
|
|
2388
|
-
* level. Keyed by the link + `linkedClientId`, so it reuses the cached shared AES-GCM key.
|
|
2389
|
-
*/
|
|
2390
|
-
function createActionFrameCrypto({ link, linkedClientId }) {
|
|
2391
|
-
return {
|
|
2392
|
-
async encryptFrame(frame) {
|
|
2393
|
-
const { nonce, ciphertext } = await link.encryptBytesForLinkedClient({
|
|
2394
|
-
linkedClientId,
|
|
2395
|
-
dataToEncrypt: frame
|
|
2396
|
-
});
|
|
2397
|
-
return pack([nonce, ciphertext]);
|
|
2398
|
-
},
|
|
2399
|
-
async decryptFrame(frame) {
|
|
2400
|
-
if (typeof frame === "string") throw new Error("[ws-crypto] expected an encrypted binary frame, received text");
|
|
2401
|
-
const envelope = unpack(frame instanceof ArrayBuffer ? new Uint8Array(frame) : frame);
|
|
2402
|
-
if (!Array.isArray(envelope) || envelope.length !== ENCRYPTED_ENVELOPE_LENGTH) throw new Error("[ws-crypto] malformed encrypted frame envelope");
|
|
2403
|
-
const [nonce, ciphertext] = envelope;
|
|
2404
|
-
if (!(nonce instanceof Uint8Array) || !(ciphertext instanceof Uint8Array)) throw new Error("[ws-crypto] malformed encrypted frame fields");
|
|
2405
|
-
return await link.decryptBytesFromLinkedClient({
|
|
2406
|
-
linkedClientId,
|
|
2407
|
-
dataToDecrypt: {
|
|
2408
|
-
nonce,
|
|
2409
|
-
ciphertext
|
|
2410
|
-
}
|
|
2411
|
-
});
|
|
2412
|
-
}
|
|
2413
|
-
};
|
|
2414
|
-
}
|
|
2415
|
-
//#endregion
|
|
2416
|
-
//#region src/ActionRuntime/Transport/crypto/frameBytes.ts
|
|
2417
|
-
/** Normalize any frame form to bytes (for the AES-GCM layer, which works on `Uint8Array`). */
|
|
2418
|
-
function toFrameBytes(frame) {
|
|
2419
|
-
if (typeof frame === "string") return new TextEncoder().encode(frame);
|
|
2420
|
-
if (frame instanceof ArrayBuffer) return new Uint8Array(frame);
|
|
2421
|
-
return frame;
|
|
2422
|
-
}
|
|
2423
|
-
//#endregion
|
|
2424
|
-
//#region src/ActionRuntime/Transport/SecureSession/frameCryptoPipe.ts
|
|
2425
|
-
function createFrameCryptoPipe(config) {
|
|
2426
|
-
const { write, isOpen, crypto, label = "link" } = config;
|
|
2427
|
-
let sendChain = Promise.resolve();
|
|
2428
|
-
const send = (frame) => {
|
|
2429
|
-
if (isOpen != null && !isOpen()) return;
|
|
2430
|
-
if (crypto == null) {
|
|
2431
|
-
write(frame);
|
|
2432
|
-
return;
|
|
2433
|
-
}
|
|
2434
|
-
const bytes = toFrameBytes(frame);
|
|
2435
|
-
sendChain = sendChain.then(() => crypto).then((c) => c.encryptFrame(bytes)).then((encrypted) => {
|
|
2436
|
-
if (isOpen == null || isOpen()) write(encrypted);
|
|
2437
|
-
}).catch((err) => console.error(`[${label}] failed to encrypt/send frame`, err));
|
|
2438
|
-
};
|
|
2439
|
-
const decryptIncoming = async (frame) => {
|
|
2440
|
-
if (crypto == null) return frame;
|
|
2441
|
-
try {
|
|
2442
|
-
return await (await crypto).decryptFrame(frame);
|
|
2443
|
-
} catch (err) {
|
|
2444
|
-
console.error(`[${label}] failed to decrypt incoming frame`, err);
|
|
2445
|
-
return;
|
|
2446
|
-
}
|
|
2447
|
-
};
|
|
2448
|
-
return {
|
|
2449
|
-
send,
|
|
2450
|
-
decryptIncoming
|
|
2451
|
-
};
|
|
2452
|
-
}
|
|
2453
|
-
//#endregion
|
|
2454
|
-
//#region src/ActionRuntime/Transport/SecureSession/acceptorSecureSession.ts
|
|
2455
|
-
/**
|
|
2456
|
-
* The acceptor (accept-in) counterpart to the connector's `establishLinkSession`: one connection's
|
|
2457
|
-
* secure session — the server-side handshake driving, the ordered frame crypto, and the per-connection
|
|
2458
|
-
* phase (undecided → plain | authenticated). The crypto itself (ordered encrypt-send / decrypt) lives in
|
|
2459
|
-
* the shared {@link createFrameCryptoPipe}, the same primitive the connector uses — so the secure logic
|
|
2460
|
-
* lives once for both link roles.
|
|
2461
|
-
*
|
|
2462
|
-
* The handler owns one of these per accepted connection and feeds it inbound frames via {@link receive};
|
|
2463
|
-
* identity binding, persistence, codec, and return routing stay in the handler (driven through the
|
|
2464
|
-
* config callbacks). Inbound processing is serialized per connection because the handshake and decryption
|
|
2465
|
-
* are async — this keeps handshake ordering and frame order intact.
|
|
2466
|
-
*/
|
|
2467
|
-
var AcceptorSecureSession = class {
|
|
2468
|
-
config;
|
|
2469
|
-
_handshake;
|
|
2470
|
-
/** The ordered encrypt-send / decrypt pipe — present only for an `encrypted` connection. */
|
|
2471
|
-
_pipe;
|
|
2472
|
-
_authed = false;
|
|
2473
|
-
_plain = false;
|
|
2474
|
-
/** Serializes inbound processing (handshake + decryption are async). */
|
|
2475
|
-
_inboundChain = Promise.resolve();
|
|
2476
|
-
constructor(config) {
|
|
2477
|
-
this.config = config;
|
|
2478
|
-
}
|
|
2479
|
-
/** Feed one inbound frame. Serialized per connection; routes back through the config callbacks. */
|
|
2480
|
-
receive(frame) {
|
|
2481
|
-
this._inboundChain = this._inboundChain.then(() => this._receive(frame)).catch((err) => console.error("[ws-server] failed to process inbound frame", err));
|
|
2482
|
-
}
|
|
2483
|
-
async _receive(frame) {
|
|
2484
|
-
if (this._plain) {
|
|
2485
|
-
this.config.routePlain(frame);
|
|
2486
|
-
return;
|
|
2487
|
-
}
|
|
2488
|
-
if (!this._authed) {
|
|
2489
|
-
const message = typeof frame === "string" ? decodeHandshakeMessage(frame) : void 0;
|
|
2490
|
-
if (message == null) {
|
|
2491
|
-
if (this.config.noneAllowed) {
|
|
2492
|
-
this._plain = true;
|
|
2493
|
-
this.config.routePlain(frame);
|
|
2494
|
-
}
|
|
2495
|
-
return;
|
|
2496
|
-
}
|
|
2497
|
-
await this.config.link.initialize();
|
|
2498
|
-
if (this._handshake == null) this._handshake = createServerHandshake({
|
|
2499
|
-
link: this.config.link,
|
|
2500
|
-
localCoordinate: this.config.localCoordinate,
|
|
2501
|
-
dictionaryVersion: this.config.dictionaryVersion,
|
|
2502
|
-
securityLevel: this.config.securityLevel,
|
|
2503
|
-
verifyKeyResolver: this.config.verifyKeyResolver
|
|
2504
|
-
});
|
|
2505
|
-
if (message.t === "hello") this.config.send(encodeHandshakeMessage(await this._handshake.onHello(message)));
|
|
2506
|
-
else if (message.t === "prove") {
|
|
2507
|
-
const reply = await this._handshake.onProve(message);
|
|
2508
|
-
this.config.send(encodeHandshakeMessage(reply));
|
|
2509
|
-
const result = this._handshake.getResult();
|
|
2510
|
-
if (reply.t === "accept" && result != null) this._complete(result);
|
|
2511
|
-
}
|
|
2512
|
-
return;
|
|
2513
|
-
}
|
|
2514
|
-
const bytes = this._pipe != null ? await this._pipe.decryptIncoming(frame) : frame;
|
|
2515
|
-
if (bytes === void 0) return;
|
|
2516
|
-
this.config.routeAction(bytes);
|
|
2517
|
-
}
|
|
2518
|
-
_complete(result) {
|
|
2519
|
-
this._authed = true;
|
|
2520
|
-
this._handshake = void 0;
|
|
2521
|
-
if (result.securityLevel === "encrypted") this._pipe = this._buildPipe(createActionFrameCrypto({
|
|
2522
|
-
link: this.config.link,
|
|
2523
|
-
linkedClientId: result.linkedClientId
|
|
2524
|
-
}));
|
|
2525
|
-
this.config.onAuthenticated({
|
|
2526
|
-
client: new RuntimeCoordinate(result.remote),
|
|
2527
|
-
securityLevel: result.securityLevel,
|
|
2528
|
-
linkedClientId: result.linkedClientId,
|
|
2529
|
-
keyMaterial: result.encryptionKeyMaterial
|
|
2530
|
-
});
|
|
2531
|
-
}
|
|
2532
|
-
/**
|
|
2533
|
-
* Restore an already-authenticated session after eviction — no handshake. For an `encrypted`
|
|
2534
|
-
* connection the shared key is re-derived asynchronously (the acceptor re-links the client off its own
|
|
2535
|
-
* persisted identity); the pipe's crypto IS that promise, so the connection's first in/out frame
|
|
2536
|
-
* naturally waits for the key before encrypt/decrypt — no separate gate.
|
|
2537
|
-
*/
|
|
2538
|
-
rehydrate(state) {
|
|
2539
|
-
this._authed = true;
|
|
2540
|
-
if (state.securityLevel !== "encrypted" || state.keyMaterial == null) return;
|
|
2541
|
-
const { link } = this.config;
|
|
2542
|
-
const { linkedClientId, keyMaterial } = state;
|
|
2543
|
-
const cryptoReady = link.initialize().then(() => link.linkClient({
|
|
2544
|
-
linkedClientId,
|
|
2545
|
-
verifyPublicKey: keyMaterial.verifyPublicKey,
|
|
2546
|
-
exchangePublicKey: keyMaterial.exchangePublicKey,
|
|
2547
|
-
saltString: keyMaterial.saltString,
|
|
2548
|
-
infoString: keyMaterial.infoString,
|
|
2549
|
-
bindVerifyKeysIntoDerivation: keyMaterial.bindVerifyKeysIntoDerivation
|
|
2550
|
-
})).then(() => createActionFrameCrypto({
|
|
2551
|
-
link,
|
|
2552
|
-
linkedClientId
|
|
2553
|
-
}));
|
|
2554
|
-
cryptoReady.catch((err) => console.error("[ws-server] failed to restore encrypted session", err));
|
|
2555
|
-
this._pipe = this._buildPipe(cryptoReady);
|
|
2556
|
-
}
|
|
2557
|
-
/** Send an outbound frame: through the encrypt pipe when encrypted, otherwise raw. */
|
|
2558
|
-
send(frame) {
|
|
2559
|
-
if (this._pipe != null) {
|
|
2560
|
-
this._pipe.send(frame);
|
|
2561
|
-
return;
|
|
2562
|
-
}
|
|
2563
|
-
this.config.send(frame);
|
|
2564
|
-
}
|
|
2565
|
-
_buildPipe(crypto) {
|
|
2566
|
-
return createFrameCryptoPipe({
|
|
2567
|
-
write: this.config.send,
|
|
2568
|
-
crypto,
|
|
2569
|
-
label: "ws-server"
|
|
2570
|
-
});
|
|
2571
|
-
}
|
|
2572
|
-
};
|
|
2573
|
-
//#endregion
|
|
2574
|
-
//#region src/ActionRuntime/Handler/PeerLink/Acceptor/AcceptorHandler.ts
|
|
2575
|
-
/**
|
|
2576
|
-
* Server-side handler for backends that accept many client connections over a single open channel
|
|
2577
|
-
* (WebSockets, Durable Objects, …). It is transport-agnostic: you feed it inbound frames with
|
|
2578
|
-
* {@link receive} and tell it how to write outbound frames via the `send` option.
|
|
2579
|
-
*
|
|
2580
|
-
* Add it alongside your local execution handler:
|
|
2581
|
-
* ```ts
|
|
2582
|
-
* const serverHandler = createAcceptorHandler({ clientEnv, formatMessage, send: (ws, f) => ws.send(f) });
|
|
2583
|
-
* runtime.addHandlers([localHandler, serverHandler]);
|
|
2584
|
-
* // per inbound message (e.g. a Durable Object's webSocketMessage):
|
|
2585
|
-
* serverHandler.receive(ws, message);
|
|
2586
|
-
* ```
|
|
2587
|
-
*
|
|
2588
|
-
* Inbound requests route to your local handler; the runtime's return dispatch then calls this
|
|
2589
|
-
* handler back (it is an external handler keyed to `clientEnv`) to send the result to the originating
|
|
2590
|
-
* connection. The handler keeps a per-connection identity registry so each result lands on the right
|
|
2591
|
-
* socket, and remembers each connection's encoding so binary and JSON clients can share the channel.
|
|
2592
|
-
*
|
|
2593
|
-
* It registers an empty action router, so it is never chosen to *execute* an inbound request — only
|
|
2594
|
-
* to ferry results/pushes back out.
|
|
2595
|
-
*/
|
|
2596
|
-
var AcceptorHandler = class extends PeerLinkHandler {
|
|
2597
|
-
/** Accept-in over a live (duplex) connection registry — it pushes results/broadcasts to bound sockets. */
|
|
2598
|
-
canPush = true;
|
|
2599
|
-
_formatMessage;
|
|
2600
|
-
_createFormatMessage;
|
|
2601
|
-
_send;
|
|
2602
|
-
_runtime;
|
|
2603
|
-
_serverTimeout;
|
|
2604
|
-
_onConnectionBound;
|
|
2605
|
-
_security;
|
|
2606
|
-
/** Normalized accepted levels; whether `none` (plain) is allowed; whether any level needs a handshake. */
|
|
2607
|
-
_allowedLevels;
|
|
2608
|
-
_noneAllowed;
|
|
2609
|
-
_handshakeMode;
|
|
2610
|
-
_connByClient = /* @__PURE__ */ new Map();
|
|
2611
|
-
_clientByConn = /* @__PURE__ */ new Map();
|
|
2612
|
-
_connEncoding = /* @__PURE__ */ new Map();
|
|
2613
|
-
_codecByConn = /* @__PURE__ */ new Map();
|
|
2614
|
-
_sessionByConn = /* @__PURE__ */ new Map();
|
|
2615
|
-
constructor(options) {
|
|
2616
|
-
super(options.clientEnv);
|
|
2617
|
-
this._formatMessage = options.formatMessage;
|
|
2618
|
-
this._createFormatMessage = options.createFormatMessage;
|
|
2619
|
-
this._send = options.send;
|
|
2620
|
-
this._runtime = options.runtime;
|
|
2621
|
-
this._serverTimeout = options.defaultTimeout ?? 1e4;
|
|
2622
|
-
this._onConnectionBound = options.onConnectionBound;
|
|
2623
|
-
this._security = options.security;
|
|
2624
|
-
this._allowedLevels = options.security == null ? [] : Array.isArray(options.security.securityLevel) ? options.security.securityLevel : [options.security.securityLevel];
|
|
2625
|
-
this._noneAllowed = this._allowedLevels.includes("none");
|
|
2626
|
-
this._handshakeMode = this._allowedLevels.some((level) => level !== "none");
|
|
2627
|
-
}
|
|
2628
|
-
/**
|
|
2629
|
-
* The codec for a connection: a per-connection session (cached) when a factory was provided, else
|
|
2630
|
-
* the single shared `formatMessage`.
|
|
2631
|
-
*/
|
|
2632
|
-
_codecFor(connection) {
|
|
2633
|
-
if (this._createFormatMessage != null) {
|
|
2634
|
-
let codec = this._codecByConn.get(connection);
|
|
2635
|
-
if (codec == null) {
|
|
2636
|
-
codec = this._createFormatMessage();
|
|
2637
|
-
this._codecByConn.set(connection, codec);
|
|
2638
|
-
}
|
|
2639
|
-
return codec;
|
|
2640
|
-
}
|
|
2641
|
-
if (this._formatMessage != null) return this._formatMessage;
|
|
2642
|
-
throw err_nice_transport.fromId("not_found", { actionId: "server-handler-codec (provide formatMessage or createFormatMessage)" });
|
|
2643
|
-
}
|
|
2644
|
-
/**
|
|
2645
|
-
* Register (or replace) the connection-bound persistence callback after construction. Used by
|
|
2646
|
-
* lifecycle helpers like {@link createHibernatableWsServerAdapter} so persistence and replay are
|
|
2647
|
-
* owned by one place instead of being split across the constructor options.
|
|
2648
|
-
*/
|
|
2649
|
-
setOnConnectionBound(onConnectionBound) {
|
|
2650
|
-
this._onConnectionBound = onConnectionBound;
|
|
2651
|
-
}
|
|
2652
|
-
/**
|
|
2653
|
-
* Feed one inbound frame from a connection into the runtime. Decodes text or binary, binds the
|
|
2654
|
-
* connection to the requesting client's identity, then routes it (requests execute locally;
|
|
2655
|
-
* results/progress resolve pending server-initiated actions).
|
|
2656
|
-
*/
|
|
2657
|
-
receive(connection, frame) {
|
|
2658
|
-
const security = this._security;
|
|
2659
|
-
if (security == null || !this._handshakeMode) {
|
|
2660
|
-
this._receivePlain(connection, frame);
|
|
2661
|
-
return;
|
|
2662
|
-
}
|
|
2663
|
-
this._sessionFor(connection, security).receive(frame);
|
|
2664
|
-
}
|
|
2665
|
-
_receivePlain(connection, frame) {
|
|
2666
|
-
const wire = decodeActionFrame(frame, this._codecFor(connection));
|
|
2667
|
-
if (wire == null) return;
|
|
2668
|
-
const encoding = typeof frame === "string" ? "json" : "binary";
|
|
2669
|
-
this._connEncoding.set(connection, encoding);
|
|
2670
|
-
if (wire.type === "request") this._resolveRequestIdentity(connection, wire, encoding);
|
|
2671
|
-
this._emitIncoming(wire);
|
|
2672
|
-
}
|
|
2673
|
-
/**
|
|
2674
|
-
* The secure session for a connection (built lazily on its first secure-mode frame), with the
|
|
2675
|
-
* handler-owned effects — raw send, identity binding + persistence, and inbound routing — wired in as
|
|
2676
|
-
* callbacks. The session owns all crypto/handshake/chain state; the handler keeps only the registry.
|
|
2677
|
-
*/
|
|
2678
|
-
_sessionFor(connection, security) {
|
|
2679
|
-
let session = this._sessionByConn.get(connection);
|
|
2680
|
-
if (session == null) {
|
|
2681
|
-
session = new AcceptorSecureSession({
|
|
2682
|
-
link: security.link,
|
|
2683
|
-
localCoordinate: security.localCoordinate,
|
|
2684
|
-
dictionaryVersion: security.dictionaryVersion,
|
|
2685
|
-
securityLevel: security.securityLevel,
|
|
2686
|
-
verifyKeyResolver: security.verifyKeyResolver,
|
|
2687
|
-
noneAllowed: this._noneAllowed,
|
|
2688
|
-
send: (frame) => this._send(connection, frame),
|
|
2689
|
-
onAuthenticated: (auth) => this._onConnectionAuthenticated(connection, auth),
|
|
2690
|
-
routePlain: (frame) => this._receivePlain(connection, frame),
|
|
2691
|
-
routeAction: (bytes) => this._routeAuthedActionBytes(connection, bytes)
|
|
2692
|
-
});
|
|
2693
|
-
this._sessionByConn.set(connection, session);
|
|
2694
|
-
}
|
|
2695
|
-
return session;
|
|
2696
|
-
}
|
|
2697
|
-
/** Bind + persist a connection's authenticated identity once its handshake completes. */
|
|
2698
|
-
_onConnectionAuthenticated(connection, auth) {
|
|
2699
|
-
this._bindConnection(connection, auth.client);
|
|
2700
|
-
this._connEncoding.set(connection, "binary");
|
|
2701
|
-
this._onConnectionBound?.(connection, {
|
|
2702
|
-
client: auth.client.toJsonObject(),
|
|
2703
|
-
encoding: "binary",
|
|
2704
|
-
secure: {
|
|
2705
|
-
securityLevel: auth.securityLevel,
|
|
2706
|
-
linkedClientId: auth.linkedClientId,
|
|
2707
|
-
keyMaterial: auth.keyMaterial
|
|
2708
|
-
}
|
|
2709
|
-
});
|
|
2710
|
-
}
|
|
2711
|
-
/** Decode a decrypted authenticated frame, inject the *authenticated* identity, and route it. */
|
|
2712
|
-
_routeAuthedActionBytes(connection, bytes) {
|
|
2713
|
-
const wire = decodeActionFrame(bytes, this._codecFor(connection));
|
|
2714
|
-
if (wire == null) return;
|
|
2715
|
-
if (wire.type === "request") {
|
|
2716
|
-
const bound = this._clientByConn.get(connection);
|
|
2717
|
-
if (bound != null) wire.context.originClient = bound.toJsonObject();
|
|
2718
|
-
}
|
|
2719
|
-
this._emitIncoming(wire);
|
|
2720
|
-
}
|
|
2721
|
-
/**
|
|
2722
|
-
* Ensure an inbound request carries the client's identity and that this connection is bound to it,
|
|
2723
|
-
* so its result can be routed back. A session codec omits `originClient` after the first request, so
|
|
2724
|
-
* when it's missing we restore it from the (possibly rehydrated) binding instead. (Plain mode only;
|
|
2725
|
-
* secure mode binds the authenticated coordinate at handshake time.)
|
|
2726
|
-
*/
|
|
2727
|
-
_resolveRequestIdentity(connection, wire, encoding) {
|
|
2728
|
-
const wireOrigin = wire.context.originClient;
|
|
2729
|
-
if (wireOrigin != null && wireOrigin.envId !== "_unset_") {
|
|
2730
|
-
const clientCoord = new RuntimeCoordinate(wireOrigin);
|
|
2731
|
-
const isNewBinding = this._clientByConn.get(connection)?.stringId !== clientCoord.stringId;
|
|
2732
|
-
this._bindConnection(connection, clientCoord);
|
|
2733
|
-
if (isNewBinding) this._onConnectionBound?.(connection, {
|
|
2734
|
-
client: clientCoord.toJsonObject(),
|
|
2735
|
-
encoding
|
|
2736
|
-
});
|
|
2737
|
-
return;
|
|
2738
|
-
}
|
|
2739
|
-
const bound = this._clientByConn.get(connection);
|
|
2740
|
-
if (bound != null) wire.context.originClient = bound.toJsonObject();
|
|
2741
|
-
}
|
|
2742
|
-
/**
|
|
2743
|
-
* Restore a connection→client binding without an inbound frame — for transports that resume after
|
|
2744
|
-
* eviction. Pair it with the {@link IAcceptorHandlerOptions.onConnectionBound} hook: persist
|
|
2745
|
-
* the binding there, then replay each live connection here when the channel comes back (e.g. a
|
|
2746
|
-
* Durable Object iterating `ctx.getWebSockets()` as it wakes from hibernation).
|
|
2747
|
-
*/
|
|
2748
|
-
rehydrateConnection(connection, binding) {
|
|
2749
|
-
this._bindConnection(connection, new RuntimeCoordinate(binding.client));
|
|
2750
|
-
this._connEncoding.set(connection, binding.encoding);
|
|
2751
|
-
const secure = binding.secure;
|
|
2752
|
-
const security = this._security;
|
|
2753
|
-
if (secure == null || security == null) return;
|
|
2754
|
-
this._sessionFor(connection, security).rehydrate(secure);
|
|
2755
|
-
}
|
|
2756
|
-
toJsonObject() {
|
|
2757
|
-
return {
|
|
2758
|
-
type: this.handlerType,
|
|
2759
|
-
client: this.peerClient
|
|
2760
|
-
};
|
|
2761
|
-
}
|
|
2762
|
-
toHandlerRouteItem() {
|
|
2763
|
-
return {
|
|
2764
|
-
type: this.handlerType,
|
|
2765
|
-
client: this.peerClient,
|
|
2766
|
-
transShape: "duplex",
|
|
2767
|
-
transOrd: 0
|
|
2768
|
-
};
|
|
2769
|
-
}
|
|
2770
|
-
/** Forget a connection (call on socket close) so stale entries don't misroute later results. */
|
|
2771
|
-
dropConnection(connection) {
|
|
2772
|
-
const coord = this._clientByConn.get(connection);
|
|
2773
|
-
if (coord != null && this._connByClient.get(coord.stringId) === connection) this._connByClient.delete(coord.stringId);
|
|
2774
|
-
this._clientByConn.delete(connection);
|
|
2775
|
-
this._connEncoding.delete(connection);
|
|
2776
|
-
this._codecByConn.delete(connection);
|
|
2777
|
-
this._sessionByConn.delete(connection);
|
|
342
|
+
* All action observers that should see actions on this domain: the root domain's
|
|
343
|
+
* observers plus this subdomain's own. Mirrors the listener set the local-dispatch
|
|
344
|
+
* path assembles in `runAction`/`_runAction`, so inbound actions (pushed from a
|
|
345
|
+
* backend or another client) can be wired up identically and surface in devtools.
|
|
346
|
+
*/
|
|
347
|
+
_collectActionObservers() {
|
|
348
|
+
return [...this._rootDomain._getActionObservers(), ...this._getActionObservers()];
|
|
2778
349
|
}
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
return this._connByClient.get(client.stringId);
|
|
350
|
+
_registerRuntime(runtime) {
|
|
351
|
+
this._rootDomain._registerRuntime(runtime);
|
|
2782
352
|
}
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
353
|
+
createChildDomain(subDomainDef) {
|
|
354
|
+
if (this.allDomains.includes(subDomainDef.domain)) throw err_nice_action.fromId("domain_already_exists_in_hierarchy", {
|
|
355
|
+
domain: subDomainDef.domain,
|
|
356
|
+
allParentDomains: this.allDomains,
|
|
357
|
+
parentDomain: this.domain
|
|
358
|
+
});
|
|
359
|
+
return new ActionDomain({
|
|
360
|
+
allDomains: [...this.allDomains, subDomainDef.domain],
|
|
361
|
+
domain: subDomainDef.domain,
|
|
362
|
+
actionSchema: subDomainDef.actions
|
|
363
|
+
}, { rootDomain: this._rootDomain });
|
|
2786
364
|
}
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
return this._clientByConn.has(connection);
|
|
365
|
+
get action() {
|
|
366
|
+
return this._actionMap;
|
|
2790
367
|
}
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
* connection token directly (e.g. the `ws`) or a client `RuntimeCoordinate` to look one up.
|
|
2794
|
-
*/
|
|
2795
|
-
pushToClient(runtime, target, request, options) {
|
|
2796
|
-
const connection = this._resolveConnection(target);
|
|
2797
|
-
return this._dispatch(runtime, connection, request, options?.timeout);
|
|
368
|
+
actionsMap() {
|
|
369
|
+
return this._actionMap;
|
|
2798
370
|
}
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
* ```ts
|
|
2806
|
-
* runtime.addHandlers([serverHandler.forConnectionDomainCases(domain, { … }), serverHandler]);
|
|
2807
|
-
* ```
|
|
2808
|
-
*/
|
|
2809
|
-
forConnectionDomainCases(domain, cases) {
|
|
2810
|
-
return this.forConnectionDomainCasesMulti([domain], cases);
|
|
371
|
+
actionForId(id) {
|
|
372
|
+
if (!this.actionSchema[id]) throw err_nice_action.fromId("action_id_not_in_domain", {
|
|
373
|
+
domain: this.domain,
|
|
374
|
+
actionId: id
|
|
375
|
+
});
|
|
376
|
+
return new ActionCore(this, id);
|
|
2811
377
|
}
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
forConnectionDomainCasesMulti(domains, cases) {
|
|
2819
|
-
const handler = new ActionLocalHandler();
|
|
2820
|
-
for (const domain of domains) {
|
|
2821
|
-
const ownedIds = new Set(Object.keys(domain.actionsMap()));
|
|
2822
|
-
const wrapped = {};
|
|
2823
|
-
for (const id in cases) {
|
|
2824
|
-
if (!ownedIds.has(id)) continue;
|
|
2825
|
-
const caseFn = cases[id];
|
|
2826
|
-
if (caseFn == null) continue;
|
|
2827
|
-
wrapped[id] = (request) => caseFn(request, this.getConnectionForClient(request.context.originClient));
|
|
2828
|
-
}
|
|
2829
|
-
handler.forDomainActionCases(domain, wrapped);
|
|
378
|
+
wrapAsPartialLocalHandler(wrappedActionExecutor) {
|
|
379
|
+
const _handler = new ActionLocalHandler();
|
|
380
|
+
const executor = wrappedActionExecutor;
|
|
381
|
+
for (const actionKey in wrappedActionExecutor) {
|
|
382
|
+
if (!this.actionSchema[actionKey]) continue;
|
|
383
|
+
_handler.forAction(this.actionForId(actionKey), (request) => executor[request.id](request.input));
|
|
2830
384
|
}
|
|
2831
|
-
return
|
|
385
|
+
return _handler;
|
|
2832
386
|
}
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
* attachment for a role). Iterating bound connections (rather than every accepted socket) skips
|
|
2838
|
-
* sockets that are still mid-handshake and so can't yet receive a frame.
|
|
2839
|
-
*/
|
|
2840
|
-
broadcast(makeRequest, options) {
|
|
2841
|
-
const runtime = options?.runtime ?? this._runtime;
|
|
2842
|
-
if (runtime == null) throw err_nice_transport.fromId("not_found", { actionId: "server-handler-runtime (construct with `runtime` or pass `options.runtime`)" });
|
|
2843
|
-
for (const connection of this._clientByConn.keys()) {
|
|
2844
|
-
if (options?.except != null && connection === options.except) continue;
|
|
2845
|
-
if (options?.where != null && !options.where(connection)) continue;
|
|
2846
|
-
try {
|
|
2847
|
-
this.pushToClient(runtime, connection, makeRequest(), { timeout: options?.timeout });
|
|
2848
|
-
} catch (error) {
|
|
2849
|
-
if (options?.onError != null) options.onError(error, connection);
|
|
2850
|
-
else console.error("[ws-server] broadcast push failed", error);
|
|
2851
|
-
}
|
|
2852
|
-
}
|
|
387
|
+
wrapAsLocalHandler(wrappedActionExecutor) {
|
|
388
|
+
const _handler = new ActionLocalHandler();
|
|
389
|
+
const executor = wrappedActionExecutor;
|
|
390
|
+
return _handler.forDomain(this, (request) => executor[request.id](request.input));
|
|
2853
391
|
}
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
|
|
392
|
+
hydrateContext(id, contextData) {
|
|
393
|
+
return new ActionContext(this, id, {
|
|
394
|
+
timeCreated: contextData.timeCreated,
|
|
395
|
+
cuid: contextData.cuid,
|
|
396
|
+
routing: contextData.routing.map((item) => {
|
|
397
|
+
return {
|
|
398
|
+
runtime: new RuntimeCoordinate(item.runtime),
|
|
399
|
+
handler: item.handler,
|
|
400
|
+
time: item.time
|
|
401
|
+
};
|
|
402
|
+
}),
|
|
403
|
+
originClient: contextData.originClient ? new RuntimeCoordinate(contextData.originClient) : RuntimeCoordinate.unknown
|
|
404
|
+
});
|
|
2859
405
|
}
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
const connection = this._resolveSingleConnection();
|
|
2863
|
-
return this._dispatch(runtime, connection, action, config?.timeout);
|
|
406
|
+
isDomainAction(action) {
|
|
407
|
+
return isAction_Any_Instance(action) && action.domain === this.domain;
|
|
2864
408
|
}
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
runtime: runtime.coordinate,
|
|
2870
|
-
handler: this.toHandlerRouteItem(),
|
|
2871
|
-
time: Date.now()
|
|
409
|
+
hydrateRequestPayload(serialized) {
|
|
410
|
+
if (serialized.type !== "request") throw err_nice_action.fromId("hydration_action_state_mismatch", {
|
|
411
|
+
expected: "request",
|
|
412
|
+
received: serialized.type
|
|
2872
413
|
});
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
parentCuid: peekHandlerCuid(),
|
|
2877
|
-
callSite: action._callSite
|
|
414
|
+
if (serialized.domain !== this.domain) throw err_nice_action.fromId("hydration_domain_mismatch", {
|
|
415
|
+
expected: this.domain,
|
|
416
|
+
received: serialized.domain
|
|
2878
417
|
});
|
|
2879
|
-
|
|
2880
|
-
if (
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
runningAction._completeWithResult(action.successResult(void 0));
|
|
2884
|
-
} catch (err) {
|
|
2885
|
-
runningAction._abort(err);
|
|
2886
|
-
}
|
|
2887
|
-
return runningAction;
|
|
2888
|
-
}
|
|
2889
|
-
const timeoutId = setTimeout(() => {
|
|
2890
|
-
runningAction._abort(err_nice_transport.fromId("timeout", { timeout: timeoutMs }));
|
|
2891
|
-
}, timeoutMs);
|
|
2892
|
-
runningAction.addUpdateListeners([(update) => {
|
|
2893
|
-
if (update.type === "finished") clearTimeout(timeoutId);
|
|
2894
|
-
}]);
|
|
2895
|
-
try {
|
|
2896
|
-
this._sendPayload(connection, action, runtime.coordinate);
|
|
2897
|
-
} catch (err) {
|
|
2898
|
-
runningAction._abort(err);
|
|
2899
|
-
}
|
|
2900
|
-
return runningAction;
|
|
2901
|
-
}
|
|
2902
|
-
_sendPayload(connection, payload, localClient) {
|
|
2903
|
-
const frame = (this._connEncoding.get(connection) ?? "binary") === "json" ? JSON.stringify(payload.toJsonObject()) : this._codecFor(connection).outgoing({
|
|
2904
|
-
action: payload,
|
|
2905
|
-
localClient,
|
|
2906
|
-
externalClient: this.peerClient
|
|
418
|
+
const id = serialized.id;
|
|
419
|
+
if (!this.actionSchema[id]) throw err_nice_action.fromId("hydration_action_id_not_found", {
|
|
420
|
+
domain: this.domain,
|
|
421
|
+
actionId: serialized.id
|
|
2907
422
|
});
|
|
2908
|
-
const
|
|
2909
|
-
|
|
2910
|
-
this._send(connection, frame);
|
|
2911
|
-
return;
|
|
2912
|
-
}
|
|
2913
|
-
session.send(frame);
|
|
423
|
+
const contextAction = this.hydrateContext(id, serialized.context);
|
|
424
|
+
return new ActionPayload_Request({ context: contextAction }, contextAction.deserializeInput(serialized.input), { time: serialized.time });
|
|
2914
425
|
}
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
426
|
+
hydrateResultPayload(serialized) {
|
|
427
|
+
if (serialized.type !== "result") throw err_nice_action.fromId("hydration_action_state_mismatch", {
|
|
428
|
+
expected: "result",
|
|
429
|
+
received: serialized.type
|
|
430
|
+
});
|
|
431
|
+
if (serialized.domain !== this.domain) throw err_nice_action.fromId("hydration_domain_mismatch", {
|
|
432
|
+
expected: this.domain,
|
|
433
|
+
received: serialized.domain
|
|
434
|
+
});
|
|
435
|
+
const id = serialized.id;
|
|
436
|
+
if (!this.actionSchema[id]) throw err_nice_action.fromId("hydration_action_id_not_found", {
|
|
437
|
+
domain: this.domain,
|
|
438
|
+
actionId: serialized.id
|
|
439
|
+
});
|
|
440
|
+
const contextAction = this.hydrateContext(id, serialized.context);
|
|
441
|
+
const result = serialized.result.ok ? {
|
|
442
|
+
ok: true,
|
|
443
|
+
output: contextAction.schema.deserializeOutput(serialized.result.output)
|
|
444
|
+
} : serialized.result;
|
|
445
|
+
return new ActionPayload_Result({ context: contextAction }, result, { time: serialized.time });
|
|
2918
446
|
}
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
if (
|
|
2923
|
-
return
|
|
447
|
+
hydrateAnyAction(actionJson) {
|
|
448
|
+
assertIsActionJson(actionJson);
|
|
449
|
+
if (actionJson.form === "data") {
|
|
450
|
+
if (actionJson.type === "request") return this.hydrateRequestPayload(actionJson);
|
|
451
|
+
if (actionJson.type === "result") return this.hydrateResultPayload(actionJson);
|
|
2924
452
|
}
|
|
2925
|
-
return
|
|
453
|
+
return this.actionForId(actionJson.id);
|
|
2926
454
|
}
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
return this.
|
|
455
|
+
async runAction(request, options) {
|
|
456
|
+
const allListeners = [...options?.listeners ?? [], ...this._listeners];
|
|
457
|
+
return this._rootDomain._runAction(request, {
|
|
458
|
+
...options,
|
|
459
|
+
listeners: allListeners
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
createActionMap() {
|
|
463
|
+
const map = {};
|
|
464
|
+
for (const id in this.actionSchema) map[id] = new ActionCore(this, id);
|
|
465
|
+
return map;
|
|
2930
466
|
}
|
|
2931
467
|
};
|
|
2932
|
-
const createAcceptorHandler = (options) => {
|
|
2933
|
-
return new AcceptorHandler(options);
|
|
2934
|
-
};
|
|
2935
|
-
//#endregion
|
|
2936
|
-
//#region src/ActionRuntime/Handler/PeerLink/Acceptor/createSecureActionServer.ts
|
|
2937
|
-
/** Default accepted set: negotiate per connection to whatever the client picks. */
|
|
2938
|
-
const DEFAULT_SERVER_SECURITY_LEVELS$1 = [
|
|
2939
|
-
"none",
|
|
2940
|
-
"authenticated",
|
|
2941
|
-
"encrypted"
|
|
2942
|
-
];
|
|
2943
|
-
/**
|
|
2944
|
-
* Build an {@link AcceptorHandler} for the secure binary channel with the boilerplate folded in:
|
|
2945
|
-
* it creates the {@link ClientCryptoKeyLink} and the storage-backed TOFU resolver from a single
|
|
2946
|
-
* `storageAdapter`, installs the channel's per-connection codec, and assembles the `security` block
|
|
2947
|
-
* from the runtime coordinate + channel version (accepting all three levels by default).
|
|
2948
|
-
*
|
|
2949
|
-
* For a hibernatable transport (e.g. a Durable Object), pair it with
|
|
2950
|
-
* {@link createHibernatableWsServerAdapter} to wire persistence + replay.
|
|
2951
|
-
*/
|
|
2952
|
-
function createSecureAcceptorHandler(options) {
|
|
2953
|
-
const link = options.link ?? new ClientCryptoKeyLink({ storageAdapter: options.storageAdapter });
|
|
2954
|
-
return new AcceptorHandler({
|
|
2955
|
-
clientEnv: options.clientEnv,
|
|
2956
|
-
createFormatMessage: options.channel.createCodec,
|
|
2957
|
-
send: options.send,
|
|
2958
|
-
runtime: options.runtime,
|
|
2959
|
-
defaultTimeout: options.defaultTimeout,
|
|
2960
|
-
security: {
|
|
2961
|
-
securityLevel: options.securityLevel ?? DEFAULT_SERVER_SECURITY_LEVELS$1,
|
|
2962
|
-
link,
|
|
2963
|
-
localCoordinate: options.runtime.coordinate.toJsonObject(),
|
|
2964
|
-
dictionaryVersion: options.channel.dictionaryVersion,
|
|
2965
|
-
verifyKeyResolver: options.verifyKeyResolver ?? createStorageTofuVerifyKeyResolver(options.storageAdapter)
|
|
2966
|
-
}
|
|
2967
|
-
});
|
|
2968
|
-
}
|
|
2969
468
|
//#endregion
|
|
2970
|
-
//#region src/
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
* positionally (see `defineSecureChannel`) — add new domains to the end of their list. (`domains` is
|
|
2975
|
-
* accepted as a legacy alias for `toAcceptor`.)
|
|
2976
|
-
*/
|
|
2977
|
-
function defineChannel(options) {
|
|
2978
|
-
return {
|
|
2979
|
-
toAcceptorDomains: options.toAcceptor,
|
|
2980
|
-
toConnectorDomains: options.toConnector
|
|
2981
|
-
};
|
|
2982
|
-
}
|
|
2983
|
-
/**
|
|
2984
|
-
* Wire a connection to the acceptor straight from a channel: route the channel's `toAcceptor` domains to
|
|
2985
|
-
* the acceptor over `transports`, and register local handlers for its `toConnector` pushes from
|
|
2986
|
-
* `onPush`. The channel is the single source of truth for *what* is routed in each direction — the
|
|
2987
|
-
* caller only supplies the transport(s) and the push handlers, never restated domain lists. Pass several
|
|
2988
|
-
* transports to make the connector→acceptor path transport-agnostic (e.g. secure WS preferred, HTTP
|
|
2989
|
-
* fallback).
|
|
2990
|
-
*
|
|
2991
|
-
* Sugar over {@link ActionRuntime.connectTo}. Returns the acceptor handler so the caller can later
|
|
2992
|
-
* `clearTransportCache()` it.
|
|
2993
|
-
*/
|
|
2994
|
-
function connectChannel(runtime, acceptorCoordinate, options) {
|
|
2995
|
-
const pushHandlers = options.onPush != null ? options.channel.toConnectorDomains.map((domain) => domain.wrapAsPartialLocalHandler(options.onPush)) : [];
|
|
2996
|
-
return runtime.connectTo(acceptorCoordinate, {
|
|
2997
|
-
transports: options.transports,
|
|
2998
|
-
domains: [...options.channel.toAcceptorDomains],
|
|
2999
|
-
localHandlers: pushHandlers,
|
|
3000
|
-
defaultTimeout: options.defaultTimeout
|
|
3001
|
-
});
|
|
3002
|
-
}
|
|
3003
|
-
/**
|
|
3004
|
-
* Register an acceptor handler's execution for a channel straight from its definition: the channel's
|
|
3005
|
-
* `toAcceptor` domains are served together with one merged, connection-aware case map (each case gets
|
|
3006
|
-
* the primed request + the originating connection, as with
|
|
3007
|
-
* {@link AcceptorHandler.forConnectionDomainCases}). The domain list is taken from the channel,
|
|
3008
|
-
* never restated. Add the returned handler to the runtime alongside the acceptor handler:
|
|
3009
|
-
* ```ts
|
|
3010
|
-
* runtime.addHandlers([acceptChannelConnections(serverHandler, channel, { … }), serverHandler]);
|
|
3011
|
-
* ```
|
|
3012
|
-
*/
|
|
3013
|
-
function acceptChannelConnections(serverHandler, channel, cases) {
|
|
3014
|
-
return serverHandler.forConnectionDomainCasesMulti(channel.toAcceptorDomains, cases);
|
|
3015
|
-
}
|
|
3016
|
-
/**
|
|
3017
|
-
* Build the secure {@link AcceptorHandler} for a channel — the accept-in counterpart to
|
|
3018
|
-
* {@link connectChannel}. It folds in the same boilerplate as {@link createSecureAcceptorHandler} (the
|
|
3019
|
-
* `ClientCryptoKeyLink` + storage-backed TOFU resolver from one `storageAdapter`, the channel's codec +
|
|
3020
|
-
* dictionary version, the `security` block from the runtime coordinate) but takes the `(runtime, channel,
|
|
3021
|
-
* options)` shape of the channel family. Pair it with {@link acceptChannelConnections} for execution:
|
|
3022
|
-
* ```ts
|
|
3023
|
-
* const acceptor = acceptChannel(runtime, gameChannel, { clientEnv, storageAdapter, send });
|
|
3024
|
-
* runtime.addHandlers([acceptChannelConnections(acceptor, gameChannel, { … }), acceptor]);
|
|
3025
|
-
* ```
|
|
3026
|
-
*/
|
|
3027
|
-
function acceptChannel(runtime, channel, options) {
|
|
3028
|
-
return createSecureAcceptorHandler({
|
|
3029
|
-
channel,
|
|
3030
|
-
runtime,
|
|
3031
|
-
clientEnv: options.clientEnv,
|
|
3032
|
-
storageAdapter: options.storageAdapter,
|
|
3033
|
-
link: options.link,
|
|
3034
|
-
send: options.send,
|
|
3035
|
-
securityLevel: options.securityLevel,
|
|
3036
|
-
verifyKeyResolver: options.verifyKeyResolver,
|
|
3037
|
-
defaultTimeout: options.defaultTimeout
|
|
3038
|
-
});
|
|
3039
|
-
}
|
|
469
|
+
//#region src/ActionDefinition/Domain/helpers/createRootActionDomain.ts
|
|
470
|
+
const createActionRootDomain = (definition) => {
|
|
471
|
+
return new ActionRootDomain(definition);
|
|
472
|
+
};
|
|
3040
473
|
//#endregion
|
|
3041
474
|
//#region src/ActionRuntime/Transport/codec/actionWireCodec.ts
|
|
3042
475
|
/**
|
|
@@ -3252,547 +685,58 @@ function createBinaryWireSessionFactory(domains, options) {
|
|
|
3252
685
|
originClient = selfIdentity ?? unknownIdentity;
|
|
3253
686
|
}
|
|
3254
687
|
return assembleWireJson(routeMeta, payloadType, time, {
|
|
3255
|
-
cuid,
|
|
3256
|
-
timeCreated: time,
|
|
3257
|
-
routing: [],
|
|
3258
|
-
originClient
|
|
3259
|
-
}, envelope[ENVELOPE$1.payload]);
|
|
3260
|
-
} catch (e) {
|
|
3261
|
-
console.error("[binary-wire] Failed to unpack binary action session frame", e);
|
|
3262
|
-
return;
|
|
3263
|
-
}
|
|
3264
|
-
}
|
|
3265
|
-
};
|
|
3266
|
-
};
|
|
3267
|
-
}
|
|
3268
|
-
//#endregion
|
|
3269
|
-
//#region src/ActionRuntime/Channel/secureChannel.ts
|
|
3270
|
-
/**
|
|
3271
|
-
* Derive a stable wire-dictionary version from the ordered route list (FNV-1a over `domain:id,…`), so
|
|
3272
|
-
* the version moves automatically whenever the transported domains change — a stale peer is then
|
|
3273
|
-
* rejected by the handshake instead of silently misrouting a positionally-packed frame.
|
|
3274
|
-
*/
|
|
3275
|
-
function deriveDictionaryVersion(domains) {
|
|
3276
|
-
const { intToRoute } = buildActionRouteDictionary(domains);
|
|
3277
|
-
const signature = intToRoute.map((route) => `${route.domain}:${route.id}`).join(",");
|
|
3278
|
-
let hash = 2166136261;
|
|
3279
|
-
for (let i = 0; i < signature.length; i++) {
|
|
3280
|
-
hash ^= signature.charCodeAt(i);
|
|
3281
|
-
hash = Math.imul(hash, 16777619);
|
|
3282
|
-
}
|
|
3283
|
-
return `auto:${(hash >>> 0).toString(16).padStart(8, "0")}`;
|
|
3284
|
-
}
|
|
3285
|
-
/**
|
|
3286
|
-
* Bundle a secure channel's shared identity from its transported domains. Both ends MUST call this
|
|
3287
|
-
* with the same domains in the same order (the binary wire dictionary is positional). The
|
|
3288
|
-
* `dictionaryVersion` is derived from those domains unless you pin an explicit one.
|
|
3289
|
-
*
|
|
3290
|
-
* Declare the domains *by role* — `toAcceptor` (connector→acceptor requests) and `toConnector`
|
|
3291
|
-
* (acceptor→connector pushes) — so the routing for both ends is derived from the channel (see
|
|
3292
|
-
* {@link connectChannel} and `acceptChannelConnections`) instead of being restated at each end. The
|
|
3293
|
-
* wire dictionary spans `[...toAcceptor, ...toConnector]` in that order; add new domains to the end of
|
|
3294
|
-
* their list to keep older peers compatible. (`domains` is still accepted as a legacy alias for
|
|
3295
|
-
* `toAcceptor`.)
|
|
3296
|
-
*/
|
|
3297
|
-
function defineSecureChannel(options) {
|
|
3298
|
-
const base = defineChannel({
|
|
3299
|
-
toAcceptor: options.toAcceptor,
|
|
3300
|
-
toConnector: options.toConnector
|
|
3301
|
-
});
|
|
3302
|
-
const allDomains = [...base.toAcceptorDomains, ...base.toConnectorDomains];
|
|
3303
|
-
return {
|
|
3304
|
-
...base,
|
|
3305
|
-
dictionaryVersion: options.dictionaryVersion ?? deriveDictionaryVersion(allDomains),
|
|
3306
|
-
createCodec: createBinaryWireSessionFactory(allDomains, options.sessionOptions)
|
|
3307
|
-
};
|
|
3308
|
-
}
|
|
3309
|
-
//#endregion
|
|
3310
|
-
//#region src/ActionRuntime/Transport/SecureSession/exchangeProtocol.ts
|
|
3311
|
-
function encodeExchange(envelope) {
|
|
3312
|
-
return JSON.stringify(envelope);
|
|
3313
|
-
}
|
|
3314
|
-
function decodeExchangeRequest(raw) {
|
|
3315
|
-
return parse(raw);
|
|
3316
|
-
}
|
|
3317
|
-
function decodeExchangeReply(raw) {
|
|
3318
|
-
return parse(raw);
|
|
3319
|
-
}
|
|
3320
|
-
function parse(raw) {
|
|
3321
|
-
try {
|
|
3322
|
-
return JSON.parse(raw);
|
|
3323
|
-
} catch {
|
|
3324
|
-
return;
|
|
3325
|
-
}
|
|
3326
|
-
}
|
|
3327
|
-
function bytesToBase64(bytes) {
|
|
3328
|
-
let binary = "";
|
|
3329
|
-
for (let i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i]);
|
|
3330
|
-
return btoa(binary);
|
|
3331
|
-
}
|
|
3332
|
-
function base64ToBytes(base64) {
|
|
3333
|
-
const binary = atob(base64);
|
|
3334
|
-
const bytes = new Uint8Array(binary.length);
|
|
3335
|
-
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
|
|
3336
|
-
return bytes;
|
|
3337
|
-
}
|
|
3338
|
-
//#endregion
|
|
3339
|
-
//#region src/ActionRuntime/Transport/SecureSession/exchangeAcceptor.ts
|
|
3340
|
-
const textEncoder$1 = new TextEncoder();
|
|
3341
|
-
const textDecoder$1 = new TextDecoder();
|
|
3342
|
-
/**
|
|
3343
|
-
* Acceptor (accept-in) side of the secure exchange protocol — the HTTP counterpart to
|
|
3344
|
-
* {@link AcceptorSecureSession}. Each POST body is one {@link decodeExchangeRequest} envelope; the
|
|
3345
|
-
* acceptor drives the server handshake over the two `hs` POSTs (correlated by `hsid`, since stateless
|
|
3346
|
-
* requests can't rely on channel ordering), mints a session **token** on accept, and on every later `act`
|
|
3347
|
-
* POST resolves the session by token, decrypts the body (at `encrypted`), routes it through the runtime,
|
|
3348
|
-
* and returns the (encrypted) result inline as the reply.
|
|
3349
|
-
*
|
|
3350
|
-
* Sessions and in-flight handshakes are held in memory — fine for a single-instance server. (Surviving a
|
|
3351
|
-
* Durable-Object eviction would persist each token's `keyMaterial` and re-derive the key on a miss, the
|
|
3352
|
-
* same primitive `AcceptorSecureSession.rehydrate` uses; left as a follow-up.)
|
|
3353
|
-
*/
|
|
3354
|
-
var ExchangeAcceptor = class {
|
|
3355
|
-
_security;
|
|
3356
|
-
_runtime;
|
|
3357
|
-
_allowedLevels;
|
|
3358
|
-
_noneAllowed;
|
|
3359
|
-
_pendingHandshakes = /* @__PURE__ */ new Map();
|
|
3360
|
-
_sessions = /* @__PURE__ */ new Map();
|
|
3361
|
-
constructor(config) {
|
|
3362
|
-
this._security = config.security;
|
|
3363
|
-
this._runtime = config.runtime;
|
|
3364
|
-
this._allowedLevels = Array.isArray(config.security.securityLevel) ? config.security.securityLevel : [config.security.securityLevel];
|
|
3365
|
-
this._noneAllowed = this._allowedLevels.includes("none");
|
|
3366
|
-
}
|
|
3367
|
-
/** Process one POST body (an exchange envelope), returning the reply body to send back. */
|
|
3368
|
-
async handlePost(body) {
|
|
3369
|
-
const request = decodeExchangeRequest(body);
|
|
3370
|
-
if (request == null) return this._err("malformed exchange request");
|
|
3371
|
-
if (request.k === "hs") return encodeExchange(await this._handleHandshake(request));
|
|
3372
|
-
return encodeExchange(await this._handleAction(request));
|
|
3373
|
-
}
|
|
3374
|
-
async _handleHandshake(request) {
|
|
3375
|
-
const message = decodeHandshakeMessage(request.m);
|
|
3376
|
-
if (message == null) return {
|
|
3377
|
-
k: "err",
|
|
3378
|
-
message: "malformed handshake message"
|
|
3379
|
-
};
|
|
3380
|
-
const security = this._security;
|
|
3381
|
-
await security.link.initialize();
|
|
3382
|
-
let handshake = this._pendingHandshakes.get(request.hsid);
|
|
3383
|
-
if (handshake == null) {
|
|
3384
|
-
handshake = createServerHandshake({
|
|
3385
|
-
link: security.link,
|
|
3386
|
-
localCoordinate: security.localCoordinate,
|
|
3387
|
-
dictionaryVersion: security.dictionaryVersion,
|
|
3388
|
-
securityLevel: security.securityLevel,
|
|
3389
|
-
verifyKeyResolver: security.verifyKeyResolver
|
|
3390
|
-
});
|
|
3391
|
-
this._pendingHandshakes.set(request.hsid, handshake);
|
|
3392
|
-
}
|
|
3393
|
-
if (message.t === "hello") return {
|
|
3394
|
-
k: "hs",
|
|
3395
|
-
m: encodeHandshakeMessage(await handshake.onHello(message))
|
|
3396
|
-
};
|
|
3397
|
-
if (message.t === "prove") {
|
|
3398
|
-
const reply = await handshake.onProve(message);
|
|
3399
|
-
this._pendingHandshakes.delete(request.hsid);
|
|
3400
|
-
const result = handshake.getResult();
|
|
3401
|
-
if (reply.t === "accept" && result != null) {
|
|
3402
|
-
const token = nanoid();
|
|
3403
|
-
this._sessions.set(token, {
|
|
3404
|
-
client: new RuntimeCoordinate(result.remote),
|
|
3405
|
-
securityLevel: result.securityLevel,
|
|
3406
|
-
crypto: result.securityLevel === "encrypted" ? createActionFrameCrypto({
|
|
3407
|
-
link: security.link,
|
|
3408
|
-
linkedClientId: result.linkedClientId
|
|
3409
|
-
}) : void 0
|
|
3410
|
-
});
|
|
3411
|
-
return {
|
|
3412
|
-
k: "hs",
|
|
3413
|
-
m: encodeHandshakeMessage(reply),
|
|
3414
|
-
t: token
|
|
3415
|
-
};
|
|
3416
|
-
}
|
|
3417
|
-
return {
|
|
3418
|
-
k: "hs",
|
|
3419
|
-
m: encodeHandshakeMessage(reply)
|
|
3420
|
-
};
|
|
3421
|
-
}
|
|
3422
|
-
return {
|
|
3423
|
-
k: "err",
|
|
3424
|
-
message: `unexpected handshake message ${message.t}`
|
|
3425
|
-
};
|
|
3426
|
-
}
|
|
3427
|
-
async _handleAction(request) {
|
|
3428
|
-
let session;
|
|
3429
|
-
let candidate;
|
|
3430
|
-
if (request.t != null) {
|
|
3431
|
-
session = this._sessions.get(request.t);
|
|
3432
|
-
if (session == null) return {
|
|
3433
|
-
k: "err",
|
|
3434
|
-
message: "unknown or expired session token"
|
|
3435
|
-
};
|
|
3436
|
-
if ("c" in request) {
|
|
3437
|
-
if (session.crypto == null) return {
|
|
3438
|
-
k: "err",
|
|
3439
|
-
message: "session is not encrypted"
|
|
3440
|
-
};
|
|
3441
|
-
const plain = await session.crypto.decryptFrame(base64ToBytes(request.c));
|
|
3442
|
-
candidate = JSON.parse(textDecoder$1.decode(plain));
|
|
3443
|
-
} else candidate = request.w;
|
|
3444
|
-
} else {
|
|
3445
|
-
if (!this._noneAllowed || "c" in request) return {
|
|
3446
|
-
k: "err",
|
|
3447
|
-
message: "missing session token"
|
|
3448
|
-
};
|
|
3449
|
-
candidate = request.w;
|
|
3450
|
-
}
|
|
3451
|
-
if (!isActionPayload_Any_JsonObject(candidate)) return {
|
|
3452
|
-
k: "err",
|
|
3453
|
-
message: "malformed action wire"
|
|
3454
|
-
};
|
|
3455
|
-
const wire = candidate;
|
|
3456
|
-
if (session != null && wire.type === "request") wire.context.originClient = session.client.toJsonObject();
|
|
3457
|
-
const resultWire = (await (await this._runtime.handleActionPayloadWire(wire)).waitForResultPayload()).toJsonObject();
|
|
3458
|
-
if (session?.crypto != null) return {
|
|
3459
|
-
k: "act",
|
|
3460
|
-
c: bytesToBase64(await session.crypto.encryptFrame(textEncoder$1.encode(JSON.stringify(resultWire))))
|
|
3461
|
-
};
|
|
3462
|
-
return {
|
|
3463
|
-
k: "act",
|
|
3464
|
-
w: resultWire
|
|
3465
|
-
};
|
|
3466
|
-
}
|
|
3467
|
-
_err(message) {
|
|
3468
|
-
return encodeExchange({
|
|
3469
|
-
k: "err",
|
|
3470
|
-
message
|
|
3471
|
-
});
|
|
3472
|
-
}
|
|
3473
|
-
};
|
|
3474
|
-
//#endregion
|
|
3475
|
-
//#region src/ActionRuntime/Handler/PeerLink/Acceptor/createActionFetchHandler.ts
|
|
3476
|
-
/** Permissive defaults — fine for a public action endpoint; override (or disable) via `cors`. */
|
|
3477
|
-
const DEFAULT_CORS_HEADERS = {
|
|
3478
|
-
"Access-Control-Allow-Origin": "*",
|
|
3479
|
-
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
|
|
3480
|
-
"Access-Control-Allow-Headers": "Content-Type",
|
|
3481
|
-
"Access-Control-Max-Age": "86400"
|
|
3482
|
-
};
|
|
3483
|
-
/**
|
|
3484
|
-
* Build the `fetch` handler a server/Durable-Object exposes for action traffic, folding in the
|
|
3485
|
-
* boilerplate every endpoint repeats: CORS (incl. the `OPTIONS` preflight), routing the `/action`
|
|
3486
|
-
* `POST` body through the runtime (`handleActionPayloadWire` → `waitForResultPayload` →
|
|
3487
|
-
* `toHttpResponse`), an optional WebSocket-upgrade hook, and a `404` fallback.
|
|
3488
|
-
*
|
|
3489
|
-
* It only touches web-standard `Request`/`Response`, so it stays transport-agnostic — the one
|
|
3490
|
-
* environment-specific bit (the WS upgrade) is injected via {@link IActionFetchHandlerOptions.onWebSocketUpgrade}:
|
|
3491
|
-
* ```ts
|
|
3492
|
-
* this.fetchHandler = createActionFetchHandler(this.runtime, {
|
|
3493
|
-
* onWebSocketUpgrade: () => {
|
|
3494
|
-
* const pair = new WebSocketPair();
|
|
3495
|
-
* this.ctx.acceptWebSocket(pair[1]);
|
|
3496
|
-
* return new Response(null, { status: 101, webSocket: pair[0] });
|
|
3497
|
-
* },
|
|
3498
|
-
* });
|
|
3499
|
-
* // async fetch(request) { return this.fetchHandler(request); }
|
|
3500
|
-
* ```
|
|
3501
|
-
*/
|
|
3502
|
-
function createActionFetchHandler(runtime, options = {}) {
|
|
3503
|
-
const corsHeaders = options.cors === false ? {} : options.cors ?? DEFAULT_CORS_HEADERS;
|
|
3504
|
-
const isActionPath = options.isActionPath ?? ((url) => url.pathname.endsWith("/action"));
|
|
3505
|
-
const isWebSocketPath = options.isWebSocketPath ?? ((url) => url.pathname.endsWith("/ws"));
|
|
3506
|
-
const exchangeAcceptor = options.security != null ? new ExchangeAcceptor({
|
|
3507
|
-
runtime,
|
|
3508
|
-
security: options.security
|
|
3509
|
-
}) : void 0;
|
|
3510
|
-
const withCors = (response) => {
|
|
3511
|
-
if (options.cors === false) return response;
|
|
3512
|
-
const headers = new Headers(response.headers);
|
|
3513
|
-
for (const [key, value] of Object.entries(corsHeaders)) headers.set(key, value);
|
|
3514
|
-
return new Response(response.body, {
|
|
3515
|
-
status: response.status,
|
|
3516
|
-
headers
|
|
3517
|
-
});
|
|
3518
|
-
};
|
|
3519
|
-
return async (request) => {
|
|
3520
|
-
if (request.method === "OPTIONS") return withCors(new Response(null, { status: 204 }));
|
|
3521
|
-
const url = new URL(request.url);
|
|
3522
|
-
const isWebSocketUpgrade = options.isWebSocketUpgrade ?? ((req, u) => req.headers.get("Upgrade") === "websocket" && isWebSocketPath(u));
|
|
3523
|
-
if (options.onWebSocketUpgrade != null && isWebSocketUpgrade(request, url)) return options.onWebSocketUpgrade(request, url);
|
|
3524
|
-
if (request.method === "POST" && isActionPath(url)) {
|
|
3525
|
-
if (exchangeAcceptor != null) {
|
|
3526
|
-
const reply = await exchangeAcceptor.handlePost(await request.text());
|
|
3527
|
-
return withCors(new Response(reply, {
|
|
3528
|
-
status: 200,
|
|
3529
|
-
headers: { "Content-Type": "application/json" }
|
|
3530
|
-
}));
|
|
688
|
+
cuid,
|
|
689
|
+
timeCreated: time,
|
|
690
|
+
routing: [],
|
|
691
|
+
originClient
|
|
692
|
+
}, envelope[ENVELOPE$1.payload]);
|
|
693
|
+
} catch (e) {
|
|
694
|
+
console.error("[binary-wire] Failed to unpack binary action session frame", e);
|
|
695
|
+
return;
|
|
696
|
+
}
|
|
3531
697
|
}
|
|
3532
|
-
|
|
3533
|
-
}
|
|
3534
|
-
return withCors(new Response("Not found", { status: 404 }));
|
|
698
|
+
};
|
|
3535
699
|
};
|
|
3536
700
|
}
|
|
3537
701
|
//#endregion
|
|
3538
|
-
//#region src/ActionRuntime/
|
|
3539
|
-
/**
|
|
3540
|
-
* A typed per-connection state store that co-owns the app state and the acceptor handler's routing
|
|
3541
|
-
* binding in one attachment, so neither the consumer nor the handler has to hand-merge the two. Create
|
|
3542
|
-
* it through {@link createConnectionStateStore} (which also wires binding persistence and replays
|
|
3543
|
-
* surviving connections after a wake), then `get`/`set`/`clearApp` the app state directly.
|
|
3544
|
-
*
|
|
3545
|
-
* The mechanism is carrier-neutral — it only needs read/write/enumerate callbacks for the connection's
|
|
3546
|
-
* attachment — but it pays off on transports whose connections outlive process eviction (e.g. a
|
|
3547
|
-
* Durable Object's hibernatable WebSockets), which is why it lives beside the hibernation adapter.
|
|
3548
|
-
*
|
|
3549
|
-
* ```ts
|
|
3550
|
-
* const players = createConnectionStateStore(serverHandler, {
|
|
3551
|
-
* schema: vs_player,
|
|
3552
|
-
* read: (ws) => ws.deserializeAttachment(),
|
|
3553
|
-
* write: (ws, v) => ws.serializeAttachment(v),
|
|
3554
|
-
* getConnections: () => ctx.getWebSockets(),
|
|
3555
|
-
* });
|
|
3556
|
-
* players.set(ws, player); // binding is preserved automatically
|
|
3557
|
-
* const player = players.get(ws);
|
|
3558
|
-
* ```
|
|
3559
|
-
*/
|
|
3560
|
-
var ConnectionStateStore = class {
|
|
3561
|
-
options;
|
|
3562
|
-
constructor(options) {
|
|
3563
|
-
this.options = options;
|
|
3564
|
-
}
|
|
3565
|
-
/** The validated app state for a connection, or `null` if unset / invalid. */
|
|
3566
|
-
get(connection) {
|
|
3567
|
-
return this._readAttachment(connection).app ?? null;
|
|
3568
|
-
}
|
|
3569
|
-
/** Set the app state, preserving the runtime binding already pinned to the connection. */
|
|
3570
|
-
set(connection, app) {
|
|
3571
|
-
const existing = this._readAttachment(connection);
|
|
3572
|
-
this.options.write(connection, {
|
|
3573
|
-
app,
|
|
3574
|
-
binding: existing.binding
|
|
3575
|
-
});
|
|
3576
|
-
}
|
|
3577
|
-
/** Clear the app state but keep the binding (e.g. a spectator that stopped watching). */
|
|
3578
|
-
clearApp(connection) {
|
|
3579
|
-
const existing = this._readAttachment(connection);
|
|
3580
|
-
this.options.write(connection, { binding: existing.binding });
|
|
3581
|
-
}
|
|
3582
|
-
/** Every live connection paired with its (validated) app state — for rebuilding in-memory state after a wake. */
|
|
3583
|
-
entries() {
|
|
3584
|
-
return this.options.getConnections().map((connection) => [connection, this._readAttachment(connection).app ?? null]);
|
|
3585
|
-
}
|
|
3586
|
-
/** @internal Persist a freshly-bound connection's binding, preserving any app state already stored. */
|
|
3587
|
-
_persistBinding(connection, binding) {
|
|
3588
|
-
const existing = this._readAttachment(connection);
|
|
3589
|
-
this.options.write(connection, {
|
|
3590
|
-
app: existing.app,
|
|
3591
|
-
binding
|
|
3592
|
-
});
|
|
3593
|
-
}
|
|
3594
|
-
/** @internal The persisted binding for a connection, if any (used to replay routing after a wake). */
|
|
3595
|
-
_readBinding(connection) {
|
|
3596
|
-
return this._readAttachment(connection).binding;
|
|
3597
|
-
}
|
|
3598
|
-
_readAttachment(connection) {
|
|
3599
|
-
try {
|
|
3600
|
-
const raw = this.options.read(connection);
|
|
3601
|
-
if (typeof raw !== "object" || raw === null) return {};
|
|
3602
|
-
const attachment = raw;
|
|
3603
|
-
const result = {};
|
|
3604
|
-
if (attachment.binding != null) result.binding = attachment.binding;
|
|
3605
|
-
if (attachment.app !== void 0) {
|
|
3606
|
-
const app = this._validateApp(attachment.app);
|
|
3607
|
-
if (app !== void 0) result.app = app;
|
|
3608
|
-
}
|
|
3609
|
-
return result;
|
|
3610
|
-
} catch {
|
|
3611
|
-
return {};
|
|
3612
|
-
}
|
|
3613
|
-
}
|
|
3614
|
-
_validateApp(value) {
|
|
3615
|
-
const schema = this.options.schema;
|
|
3616
|
-
if (schema == null) return value;
|
|
3617
|
-
const result = schema["~standard"].validate(value);
|
|
3618
|
-
if (result instanceof Promise) return void 0;
|
|
3619
|
-
if (result.issues != null) return void 0;
|
|
3620
|
-
return result.value;
|
|
3621
|
-
}
|
|
3622
|
-
};
|
|
702
|
+
//#region src/ActionRuntime/Channel/secureChannel.ts
|
|
3623
703
|
/**
|
|
3624
|
-
*
|
|
3625
|
-
*
|
|
3626
|
-
*
|
|
3627
|
-
* {@link AcceptorHandler.rehydrateConnection} — so on a transport that resumes after eviction (e.g. a
|
|
3628
|
-
* Durable Object waking from hibernation) both the app identity and the action routing come back from a
|
|
3629
|
-
* single attachment, with no storage reads and no hand-rolled merge.
|
|
3630
|
-
*
|
|
3631
|
-
* Lives outside the handler so the generic {@link AcceptorHandler} stays free of any attachment/
|
|
3632
|
-
* hibernation concern — it exposes only the neutral `setOnConnectionBound` + `rehydrateConnection`
|
|
3633
|
-
* hooks this builder drives.
|
|
704
|
+
* Derive a stable wire-dictionary version from the ordered route list (FNV-1a over `domain:id,…`), so
|
|
705
|
+
* the version moves automatically whenever the transported domains change — a stale peer is then
|
|
706
|
+
* rejected by the handshake instead of silently misrouting a positionally-packed frame.
|
|
3634
707
|
*/
|
|
3635
|
-
function
|
|
3636
|
-
const
|
|
3637
|
-
|
|
3638
|
-
|
|
3639
|
-
|
|
3640
|
-
|
|
708
|
+
function deriveDictionaryVersion(domains) {
|
|
709
|
+
const { intToRoute } = buildActionRouteDictionary(domains);
|
|
710
|
+
const signature = intToRoute.map((route) => `${route.domain}:${route.id}`).join(",");
|
|
711
|
+
let hash = 2166136261;
|
|
712
|
+
for (let i = 0; i < signature.length; i++) {
|
|
713
|
+
hash ^= signature.charCodeAt(i);
|
|
714
|
+
hash = Math.imul(hash, 16777619);
|
|
3641
715
|
}
|
|
3642
|
-
return
|
|
716
|
+
return `auto:${(hash >>> 0).toString(16).padStart(8, "0")}`;
|
|
3643
717
|
}
|
|
3644
|
-
//#endregion
|
|
3645
|
-
//#region src/ActionRuntime/Handler/PeerLink/Acceptor/Hibernation/createHibernatableWsServerAdapter.ts
|
|
3646
718
|
/**
|
|
3647
|
-
*
|
|
3648
|
-
*
|
|
3649
|
-
*
|
|
3650
|
-
* live connection's stored binding via `getAttachment`, so results/pushes still route after a wake.
|
|
3651
|
-
*
|
|
3652
|
-
* Layered on top of the generic {@link AcceptorHandler} — it touches only the handler's neutral
|
|
3653
|
-
* `setOnConnectionBound` / `rehydrateConnection` / `receive` / `dropConnection` surface, so no
|
|
3654
|
-
* hibernation concern leaks into the handler itself.
|
|
719
|
+
* Bundle a secure channel's shared identity from its transported domains. Both ends MUST call this
|
|
720
|
+
* with the same domains in the same order (the binary wire dictionary is positional). The
|
|
721
|
+
* `dictionaryVersion` is derived from those domains unless you pin an explicit one.
|
|
3655
722
|
*
|
|
3656
|
-
*
|
|
3657
|
-
*
|
|
3658
|
-
*
|
|
3659
|
-
*
|
|
3660
|
-
*
|
|
3661
|
-
*
|
|
723
|
+
* Declare the domains *by role* — `toAcceptor` (connector→acceptor requests) and `toConnector`
|
|
724
|
+
* (acceptor→connector pushes) — so the routing for both ends is derived from the channel (see
|
|
725
|
+
* {@link connectChannel} and `acceptChannelConnections`) instead of being restated at each end. The
|
|
726
|
+
* wire dictionary spans `[...toAcceptor, ...toConnector]` in that order; add new domains to the end of
|
|
727
|
+
* their list to keep older peers compatible. (`domains` is still accepted as a legacy alias for
|
|
728
|
+
* `toAcceptor`.)
|
|
3662
729
|
*/
|
|
3663
|
-
function
|
|
3664
|
-
const
|
|
3665
|
-
|
|
3666
|
-
|
|
3667
|
-
const binding = getAttachment(connection);
|
|
3668
|
-
if (binding != null) handler.rehydrateConnection(connection, binding);
|
|
3669
|
-
}
|
|
3670
|
-
return {
|
|
3671
|
-
receive: (connection, frame) => handler.receive(connection, frame),
|
|
3672
|
-
drop: (connection) => handler.dropConnection(connection)
|
|
3673
|
-
};
|
|
3674
|
-
}
|
|
3675
|
-
//#endregion
|
|
3676
|
-
//#region src/ActionRuntime/Channel/serveChannel.ts
|
|
3677
|
-
/** Default accepted set, shared by every carrier: negotiate per connection to whatever the client picks. */
|
|
3678
|
-
const DEFAULT_SERVER_SECURITY_LEVELS = [
|
|
3679
|
-
"none",
|
|
3680
|
-
"authenticated",
|
|
3681
|
-
"encrypted"
|
|
3682
|
-
];
|
|
3683
|
-
function serveChannel(runtime, channel, options) {
|
|
3684
|
-
const duplexCarriers = options.carriers.filter((carrier) => !isExchangeAcceptorCarrier(carrier));
|
|
3685
|
-
const exchangeCarriers = options.carriers.filter(isExchangeAcceptorCarrier);
|
|
3686
|
-
if (exchangeCarriers.length > 1) throw new Error("serveChannel: at most one exchange carrier is supported");
|
|
3687
|
-
const exchangeCarrier = exchangeCarriers[0];
|
|
3688
|
-
const singleDuplex = duplexCarriers.length === 1;
|
|
3689
|
-
if (options.connectionState != null && !singleDuplex) throw new Error("serveChannel: `connectionState` requires exactly one duplex carrier");
|
|
3690
|
-
if (options.channelCases != null && !singleDuplex) throw new Error("serveChannel: `channelCases` requires exactly one duplex carrier");
|
|
3691
|
-
const exchangeSecure = exchangeCarrier != null && (exchangeCarrier.secure ?? true);
|
|
3692
|
-
const anyDuplexSecure = duplexCarriers.some((carrier) => carrier.secure ?? true);
|
|
3693
|
-
const securityLevel = options.securityLevel ?? DEFAULT_SERVER_SECURITY_LEVELS;
|
|
3694
|
-
let secure;
|
|
3695
|
-
if (anyDuplexSecure || exchangeSecure) {
|
|
3696
|
-
const storage = options.storage;
|
|
3697
|
-
if (storage == null) throw new Error("serveChannel: a secure carrier requires `storage`. Pass it, or set `secure: false` on the carrier for a plain endpoint.");
|
|
3698
|
-
secure = {
|
|
3699
|
-
storage,
|
|
3700
|
-
link: options.link ?? new ClientCryptoKeyLink({ storageAdapter: storage }),
|
|
3701
|
-
verifyKeyResolver: options.verifyKeyResolver ?? createStorageTofuVerifyKeyResolver(storage)
|
|
3702
|
-
};
|
|
3703
|
-
}
|
|
3704
|
-
const plainRouter = (handler) => ({
|
|
3705
|
-
receive: (connection, frame) => handler.receive(connection, frame),
|
|
3706
|
-
drop: (connection) => handler.dropConnection(connection)
|
|
3707
|
-
});
|
|
3708
|
-
const asObject = (value) => typeof value === "object" && value != null ? value : {};
|
|
3709
|
-
const handlers = [];
|
|
3710
|
-
let connections;
|
|
3711
|
-
for (const carrier of duplexCarriers) {
|
|
3712
|
-
const handler = (carrier.secure ?? true) && secure != null ? acceptChannel(runtime, channel, {
|
|
3713
|
-
clientEnv: options.clientEnv,
|
|
3714
|
-
storageAdapter: secure.storage,
|
|
3715
|
-
link: secure.link,
|
|
3716
|
-
verifyKeyResolver: secure.verifyKeyResolver,
|
|
3717
|
-
securityLevel,
|
|
3718
|
-
send: carrier.send,
|
|
3719
|
-
defaultTimeout: options.defaultTimeout
|
|
3720
|
-
}) : createAcceptorHandler({
|
|
3721
|
-
clientEnv: options.clientEnv,
|
|
3722
|
-
createFormatMessage: channel.createCodec,
|
|
3723
|
-
send: carrier.send,
|
|
3724
|
-
runtime,
|
|
3725
|
-
defaultTimeout: options.defaultTimeout
|
|
3726
|
-
});
|
|
3727
|
-
const attach = carrier.attachmentStore;
|
|
3728
|
-
let router;
|
|
3729
|
-
if (attach == null) router = plainRouter(handler);
|
|
3730
|
-
else if (options.connectionState != null) {
|
|
3731
|
-
connections = createConnectionStateStore(handler, {
|
|
3732
|
-
schema: options.connectionState.schema,
|
|
3733
|
-
getConnections: attach.getConnections,
|
|
3734
|
-
read: attach.read,
|
|
3735
|
-
write: attach.write
|
|
3736
|
-
});
|
|
3737
|
-
router = plainRouter(handler);
|
|
3738
|
-
} else router = createHibernatableWsServerAdapter({
|
|
3739
|
-
handler,
|
|
3740
|
-
getConnections: attach.getConnections,
|
|
3741
|
-
getAttachment: (connection) => attach.read(connection)?.binding,
|
|
3742
|
-
setAttachment: (connection, binding) => attach.write(connection, {
|
|
3743
|
-
...asObject(attach.read(connection)),
|
|
3744
|
-
binding
|
|
3745
|
-
})
|
|
3746
|
-
});
|
|
3747
|
-
carrier._activate(router);
|
|
3748
|
-
handlers.push(handler);
|
|
3749
|
-
}
|
|
3750
|
-
runtime.addHandlers([...options.handlers ?? [], ...handlers]);
|
|
3751
|
-
if (options.channelCases != null) runtime.addHandlers([acceptChannelConnections(handlers[0], channel, options.channelCases)]);
|
|
3752
|
-
const exchangeSecurity = exchangeSecure && secure != null ? {
|
|
3753
|
-
link: secure.link,
|
|
3754
|
-
verifyKeyResolver: secure.verifyKeyResolver,
|
|
3755
|
-
localCoordinate: runtime.coordinate.toJsonObject(),
|
|
3756
|
-
dictionaryVersion: channel.dictionaryVersion,
|
|
3757
|
-
securityLevel
|
|
3758
|
-
} : void 0;
|
|
3759
|
-
const defaultIsUpgrade = (request) => request.headers.get("Upgrade") === "websocket";
|
|
3760
|
-
const upgraders = [];
|
|
3761
|
-
for (const carrier of duplexCarriers) {
|
|
3762
|
-
if (carrier.upgrade == null) continue;
|
|
3763
|
-
upgraders.push({
|
|
3764
|
-
isUpgrade: carrier.isUpgrade ?? defaultIsUpgrade,
|
|
3765
|
-
upgrade: carrier.upgrade
|
|
3766
|
-
});
|
|
3767
|
-
}
|
|
3768
|
-
const fetch = createActionFetchHandler(runtime, {
|
|
3769
|
-
cors: exchangeCarrier?.cors,
|
|
3770
|
-
onWebSocketUpgrade: upgraders.length === 0 ? void 0 : (request, url) => (upgraders.find((u) => u.isUpgrade(request, url)) ?? upgraders[0]).upgrade(request, url),
|
|
3771
|
-
isWebSocketUpgrade: upgraders.length === 0 ? void 0 : (request, url) => upgraders.some((u) => u.isUpgrade(request, url)),
|
|
3772
|
-
isActionPath: exchangeCarrier != null ? exchangeCarrier.isActionPath ?? (() => true) : () => false,
|
|
3773
|
-
security: exchangeSecurity,
|
|
3774
|
-
useErrorStatus: exchangeCarrier?.useErrorStatus
|
|
730
|
+
function defineSecureChannel(options) {
|
|
731
|
+
const base = defineChannel({
|
|
732
|
+
toAcceptor: options.toAcceptor,
|
|
733
|
+
toConnector: options.toConnector
|
|
3775
734
|
});
|
|
3776
|
-
const
|
|
3777
|
-
const pushToClient = (target, request, pushOptions) => {
|
|
3778
|
-
const owner = target instanceof RuntimeCoordinate ? handlers.find((handler) => handler.ownsLiveConnectionFor(target)) : handlers.find((handler) => handler.hasConnection(target));
|
|
3779
|
-
if (owner == null) throw new Error("serveChannel: no duplex carrier holds a connection for the push target");
|
|
3780
|
-
return owner.pushToClient(runtime, target, request, pushOptions);
|
|
3781
|
-
};
|
|
3782
|
-
const broadcast = (makeRequest, broadcastOptions) => {
|
|
3783
|
-
if (!singleDuplex) throw new Error("serveChannel: broadcast requires exactly one duplex carrier — broadcast over a specific handlers[i] instead");
|
|
3784
|
-
handlers[0].broadcast(makeRequest, {
|
|
3785
|
-
runtime,
|
|
3786
|
-
...broadcastOptions
|
|
3787
|
-
});
|
|
3788
|
-
};
|
|
735
|
+
const allDomains = [...base.toAcceptorDomains, ...base.toConnectorDomains];
|
|
3789
736
|
return {
|
|
3790
|
-
|
|
3791
|
-
|
|
3792
|
-
|
|
3793
|
-
pushToClient,
|
|
3794
|
-
broadcast,
|
|
3795
|
-
connections
|
|
737
|
+
...base,
|
|
738
|
+
dictionaryVersion: options.dictionaryVersion ?? deriveDictionaryVersion(allDomains),
|
|
739
|
+
createCodec: createBinaryWireSessionFactory(allDomains, options.sessionOptions)
|
|
3796
740
|
};
|
|
3797
741
|
}
|
|
3798
742
|
//#endregion
|
|
@@ -4081,25 +1025,6 @@ function defaultWebSocket(url) {
|
|
|
4081
1025
|
return ws;
|
|
4082
1026
|
}
|
|
4083
1027
|
//#endregion
|
|
4084
|
-
//#region src/ActionRuntime/Transport/Carrier/exchange/http/httpAcceptorCarrier.ts
|
|
4085
|
-
/**
|
|
4086
|
-
* An HTTP {@link IExchangeAcceptorCarrier}: the accept-in dual of {@link httpCarrier}. It serves the
|
|
4087
|
-
* secure exchange protocol (handshake → token session → encrypted frames) over web-standard
|
|
4088
|
-
* `Request`/`Response`. The crypto identity, runtime coordinate, dictionary version, and accepted security
|
|
4089
|
-
* levels are all supplied centrally by `serveChannel`, so this only needs to say which requests carry an
|
|
4090
|
-
* action envelope and how to answer CORS.
|
|
4091
|
-
*/
|
|
4092
|
-
function httpAcceptorCarrier(options = {}) {
|
|
4093
|
-
return {
|
|
4094
|
-
shape: "exchange",
|
|
4095
|
-
carrierLabel: options.carrierLabel ?? "http",
|
|
4096
|
-
secure: options.secure,
|
|
4097
|
-
isActionPath: options.isActionPath,
|
|
4098
|
-
cors: options.cors,
|
|
4099
|
-
useErrorStatus: options.useErrorStatus
|
|
4100
|
-
};
|
|
4101
|
-
}
|
|
4102
|
-
//#endregion
|
|
4103
1028
|
//#region src/ActionRuntime/Transport/Carrier/exchange/http/httpCarrier.ts
|
|
4104
1029
|
function shortPath(url) {
|
|
4105
1030
|
try {
|
|
@@ -4235,658 +1160,6 @@ function createBinaryWireAdapter(domains) {
|
|
|
4235
1160
|
};
|
|
4236
1161
|
}
|
|
4237
1162
|
//#endregion
|
|
4238
|
-
|
|
4239
|
-
/**
|
|
4240
|
-
* Reusable transport definition. Devs construct these (`secureTransport({ carrier: wsCarrier(url) })`,
|
|
4241
|
-
* `plainTransport({ carrier: httpCarrier(...) })`, …) and pass them to a
|
|
4242
|
-
* `ConnectorHandler`. A single
|
|
4243
|
-
* definition can be shared across multiple handlers — each handler builds its own live
|
|
4244
|
-
* {@link TransportConnection} via {@link TransportConnection._createConnection}.
|
|
4245
|
-
*/
|
|
4246
|
-
var Transport = class {};
|
|
4247
|
-
//#endregion
|
|
4248
|
-
//#region src/ActionRuntime/Transport/SecureSession/establishExchangeSession.ts
|
|
4249
|
-
const textEncoder = new TextEncoder();
|
|
4250
|
-
const textDecoder = new TextDecoder();
|
|
4251
|
-
/** Plain path (no handshake/token): every action rides a bare `act` envelope, plaintext both ways. */
|
|
4252
|
-
function finalizePlainExchangeMethods(ctx) {
|
|
4253
|
-
return buildExchangeMethods(ctx, {});
|
|
4254
|
-
}
|
|
4255
|
-
/** Secure path: run the handshake (two exchanges) once at bring-up, then reuse the token + crypto. */
|
|
4256
|
-
async function finalizeSecureExchangeMethods(ctx) {
|
|
4257
|
-
return buildExchangeMethods(ctx, await runConnectorExchangeHandshake(ctx.carrier, ctx.secure));
|
|
4258
|
-
}
|
|
4259
|
-
function buildExchangeMethods(ctx, state) {
|
|
4260
|
-
const sendActionData = (inputs) => {
|
|
4261
|
-
runExchange(ctx.carrier, state, inputs).catch((err) => inputs.runningAction._abort(err));
|
|
4262
|
-
};
|
|
4263
|
-
return {
|
|
4264
|
-
sendActionData,
|
|
4265
|
-
updateRunConfig: ctx.updateRunConfig
|
|
4266
|
-
};
|
|
4267
|
-
}
|
|
4268
|
-
async function runExchange(carrier, state, inputs) {
|
|
4269
|
-
const { action, runningAction, timeout } = inputs;
|
|
4270
|
-
const ac = new AbortController();
|
|
4271
|
-
let timedOut = false;
|
|
4272
|
-
const timeoutId = setTimeout(() => {
|
|
4273
|
-
timedOut = true;
|
|
4274
|
-
ac.abort();
|
|
4275
|
-
}, timeout);
|
|
4276
|
-
const unsubscribe = runningAction.addUpdateListeners([(update) => {
|
|
4277
|
-
if (update.type === "finished") {
|
|
4278
|
-
clearTimeout(timeoutId);
|
|
4279
|
-
ac.abort();
|
|
4280
|
-
}
|
|
4281
|
-
}]);
|
|
4282
|
-
try {
|
|
4283
|
-
const request = await buildRequestEnvelope(state, action);
|
|
4284
|
-
const replyRaw = await carrier.exchange(encodeExchange(request), { signal: ac.signal });
|
|
4285
|
-
if (action.type !== "request") return;
|
|
4286
|
-
const reply = decodeExchangeReply(asString(replyRaw));
|
|
4287
|
-
if (reply == null) throw err_nice_transport.fromId("invalid_action_response", { actionId: action.id });
|
|
4288
|
-
if (reply.k === "err") throw err_nice_transport.fromId("send_failed", {
|
|
4289
|
-
actionState: action.type,
|
|
4290
|
-
actionId: action.id,
|
|
4291
|
-
message: reply.message
|
|
4292
|
-
});
|
|
4293
|
-
const wire = await extractReplyWire(state, reply);
|
|
4294
|
-
if (wire == null || !isActionPayload_Result_JsonObject(wire)) throw err_nice_transport.fromId("invalid_action_response", { actionId: action.id });
|
|
4295
|
-
runningAction._completeWithResult(action._domain.hydrateResultPayload(wire));
|
|
4296
|
-
} catch (err) {
|
|
4297
|
-
if (timedOut) throw err_nice_transport.fromId("timeout", { timeout });
|
|
4298
|
-
throw err;
|
|
4299
|
-
} finally {
|
|
4300
|
-
clearTimeout(timeoutId);
|
|
4301
|
-
unsubscribe();
|
|
4302
|
-
}
|
|
4303
|
-
}
|
|
4304
|
-
async function buildRequestEnvelope(state, action) {
|
|
4305
|
-
const wire = action.toJsonObject();
|
|
4306
|
-
if (state.crypto != null) {
|
|
4307
|
-
const ciphertext = await state.crypto.encryptFrame(textEncoder.encode(JSON.stringify(wire)));
|
|
4308
|
-
return {
|
|
4309
|
-
k: "act",
|
|
4310
|
-
t: state.token,
|
|
4311
|
-
c: bytesToBase64(ciphertext)
|
|
4312
|
-
};
|
|
4313
|
-
}
|
|
4314
|
-
return {
|
|
4315
|
-
k: "act",
|
|
4316
|
-
t: state.token,
|
|
4317
|
-
w: wire
|
|
4318
|
-
};
|
|
4319
|
-
}
|
|
4320
|
-
async function extractReplyWire(state, reply) {
|
|
4321
|
-
if (reply.k !== "act") return void 0;
|
|
4322
|
-
if ("c" in reply) {
|
|
4323
|
-
if (state.crypto == null) return void 0;
|
|
4324
|
-
const plain = await state.crypto.decryptFrame(base64ToBytes(reply.c));
|
|
4325
|
-
return JSON.parse(textDecoder.decode(plain));
|
|
4326
|
-
}
|
|
4327
|
-
return reply.w;
|
|
4328
|
-
}
|
|
4329
|
-
async function runConnectorExchangeHandshake(carrier, secure) {
|
|
4330
|
-
await secure.link.initialize();
|
|
4331
|
-
const handshake = createClientHandshake({
|
|
4332
|
-
link: secure.link,
|
|
4333
|
-
localCoordinate: secure.localCoordinate,
|
|
4334
|
-
dictionaryVersion: secure.dictionaryVersion,
|
|
4335
|
-
securityLevel: secure.securityLevel
|
|
4336
|
-
});
|
|
4337
|
-
const hsid = nanoid();
|
|
4338
|
-
const hello = await handshake.createHello();
|
|
4339
|
-
const welcomeReply = decodeExchangeReply(asString(await carrier.exchange(encodeExchange({
|
|
4340
|
-
k: "hs",
|
|
4341
|
-
hsid,
|
|
4342
|
-
m: encodeHandshakeMessage(hello)
|
|
4343
|
-
}))));
|
|
4344
|
-
if (welcomeReply?.k !== "hs") throw new Error("[exchange-handshake] expected a welcome reply");
|
|
4345
|
-
const welcome = decodeHandshakeMessage(welcomeReply.m);
|
|
4346
|
-
if (welcome == null) throw new Error("[exchange-handshake] malformed welcome");
|
|
4347
|
-
if (welcome.t === "reject") throw new Error(`[exchange-handshake] rejected by peer: ${welcome.reason}`);
|
|
4348
|
-
if (welcome.t !== "welcome") throw new Error(`[exchange-handshake] expected welcome, got ${welcome.t}`);
|
|
4349
|
-
const prove = await handshake.onWelcome(welcome);
|
|
4350
|
-
const acceptReply = decodeExchangeReply(asString(await carrier.exchange(encodeExchange({
|
|
4351
|
-
k: "hs",
|
|
4352
|
-
hsid,
|
|
4353
|
-
m: encodeHandshakeMessage(prove)
|
|
4354
|
-
}))));
|
|
4355
|
-
if (acceptReply?.k !== "hs") throw new Error("[exchange-handshake] expected an accept reply");
|
|
4356
|
-
const accept = decodeHandshakeMessage(acceptReply.m);
|
|
4357
|
-
if (accept == null) throw new Error("[exchange-handshake] malformed accept");
|
|
4358
|
-
if (accept.t === "reject") throw new Error(`[exchange-handshake] rejected by peer: ${accept.reason}`);
|
|
4359
|
-
if (accept.t !== "accept") throw new Error(`[exchange-handshake] expected accept, got ${accept.t}`);
|
|
4360
|
-
if (acceptReply.t == null) throw new Error("[exchange-handshake] accept missing session token");
|
|
4361
|
-
const result = await handshake.onAccept(accept);
|
|
4362
|
-
const crypto = result.securityLevel === "encrypted" ? createActionFrameCrypto({
|
|
4363
|
-
link: secure.link,
|
|
4364
|
-
linkedClientId: result.linkedClientId
|
|
4365
|
-
}) : void 0;
|
|
4366
|
-
return {
|
|
4367
|
-
token: acceptReply.t,
|
|
4368
|
-
crypto
|
|
4369
|
-
};
|
|
4370
|
-
}
|
|
4371
|
-
function asString(frame) {
|
|
4372
|
-
if (typeof frame === "string") return frame;
|
|
4373
|
-
return textDecoder.decode(frame instanceof ArrayBuffer ? new Uint8Array(frame) : frame);
|
|
4374
|
-
}
|
|
4375
|
-
//#endregion
|
|
4376
|
-
//#region src/ActionRuntime/Transport/helpers/addTransportStatusMetadata.ts
|
|
4377
|
-
function addTransportStatusMetadata(transportStatus) {
|
|
4378
|
-
if (transportStatus.status === "ready") return {
|
|
4379
|
-
status: "ready",
|
|
4380
|
-
readyData: transportStatus.readyData
|
|
4381
|
-
};
|
|
4382
|
-
if (transportStatus.status === "initializing") return {
|
|
4383
|
-
status: "initializing",
|
|
4384
|
-
initializationPromise: transportStatus.initializationPromise,
|
|
4385
|
-
timeStarted: Date.now()
|
|
4386
|
-
};
|
|
4387
|
-
if (transportStatus.status === "failed") return {
|
|
4388
|
-
status: "failed",
|
|
4389
|
-
error: transportStatus.error,
|
|
4390
|
-
timeFailed: Date.now()
|
|
4391
|
-
};
|
|
4392
|
-
if (transportStatus.status === "unsupported") return { status: "unsupported" };
|
|
4393
|
-
return { status: "uninitialized" };
|
|
4394
|
-
}
|
|
4395
|
-
//#endregion
|
|
4396
|
-
//#region src/ActionRuntime/Transport/TransportConnection.ts
|
|
4397
|
-
let transportOrd = 0;
|
|
4398
|
-
/**
|
|
4399
|
-
* Live, per-handler transport runtime built from a reusable {@link Transport} definition. Holds the
|
|
4400
|
-
* connection-scoped state (ordinal, initialized config, sockets / abort sets) that must not be shared
|
|
4401
|
-
* across handlers. Construct these via `definition._createConnection(...)`, never directly.
|
|
4402
|
-
*/
|
|
4403
|
-
var TransportConnection = class {
|
|
4404
|
-
def;
|
|
4405
|
-
transOrd = transportOrd++;
|
|
4406
|
-
type;
|
|
4407
|
-
initialized;
|
|
4408
|
-
/** Backref to the public definition that created this connection (used for devtools route info). */
|
|
4409
|
-
definition;
|
|
4410
|
-
constructor(def) {
|
|
4411
|
-
this.def = def;
|
|
4412
|
-
this.type = def.type;
|
|
4413
|
-
this.initialized = def.initialize();
|
|
4414
|
-
}
|
|
4415
|
-
/**
|
|
4416
|
-
* Devtools route info for an action routed through this live connection. Defaults to the stateless
|
|
4417
|
-
* {@link definition}'s info; connections override to enrich it from live state (e.g. the actual
|
|
4418
|
-
* resolved socket URL) when the definition couldn't resolve it on its own.
|
|
4419
|
-
*/
|
|
4420
|
-
getRouteInfo(input) {
|
|
4421
|
-
return this.definition?.getRouteInfo(input);
|
|
4422
|
-
}
|
|
4423
|
-
/**
|
|
4424
|
-
* Whether a `ready`-status transport still needs asynchronous bring-up before its methods exist —
|
|
4425
|
-
* awaiting the carrier to open and/or running a handshake. Default `false`: a stateless transport
|
|
4426
|
-
* (HTTP) is usable the instant `getTransport` reports `ready`, so it stays a terminal *synchronous*
|
|
4427
|
-
* fallback in {@link ConnectionTransportManager}. Stream carriers (Link/WS) override to `true`.
|
|
4428
|
-
*/
|
|
4429
|
-
_needsAsyncBringUp(_readyData) {
|
|
4430
|
-
return false;
|
|
4431
|
-
}
|
|
4432
|
-
/** Await the carrier becoming ready to send (e.g. a socket `open`). Default: nothing to await. */
|
|
4433
|
-
_awaitCarrierReady(_readyData) {
|
|
4434
|
-
return Promise.resolve();
|
|
4435
|
-
}
|
|
4436
|
-
/**
|
|
4437
|
-
* Finalize during async bring-up — may run a handshake, so it can be async. Defaults to the
|
|
4438
|
-
* synchronous {@link _finalizeTransportMethods}; secure stream carriers override to branch plain/secure.
|
|
4439
|
-
*/
|
|
4440
|
-
_finalizeReady(readyData) {
|
|
4441
|
-
return this._finalizeTransportMethods(readyData);
|
|
4442
|
-
}
|
|
4443
|
-
_getCacheKey(input) {
|
|
4444
|
-
const parts = this.initialized.getTransportCacheKey?.(input);
|
|
4445
|
-
if (parts == null) return null;
|
|
4446
|
-
return parts.join("\0");
|
|
4447
|
-
}
|
|
4448
|
-
getCacheKey(input) {
|
|
4449
|
-
const inner = this._getCacheKey(input);
|
|
4450
|
-
if (inner == null) return null;
|
|
4451
|
-
return `${this.transOrd}:${inner}`;
|
|
4452
|
-
}
|
|
4453
|
-
/**
|
|
4454
|
-
* Whether this transport can serve the given action right now. Consulted by the manager before
|
|
4455
|
-
* cache-key resolution and `getTransport`; a `false` result skips this transport (treated as
|
|
4456
|
-
* `unsupported`) and the manager falls through to the next in preference order. Defaults to `true`
|
|
4457
|
-
* when the transport declares no gate.
|
|
4458
|
-
*/
|
|
4459
|
-
isAvailable(input) {
|
|
4460
|
-
return this.initialized.isAvailable?.(input) ?? true;
|
|
4461
|
-
}
|
|
4462
|
-
getTransport(input) {
|
|
4463
|
-
return this._processTransportStatus(input);
|
|
4464
|
-
}
|
|
4465
|
-
_processTransportStatus(input) {
|
|
4466
|
-
const statusInfo = addTransportStatusMetadata(this.initialized.getTransport(input));
|
|
4467
|
-
if (statusInfo.status === "ready") {
|
|
4468
|
-
if (!this._needsAsyncBringUp(statusInfo.readyData)) return {
|
|
4469
|
-
status: "ready",
|
|
4470
|
-
readyData: this._finalizeTransportMethods(statusInfo.readyData)
|
|
4471
|
-
};
|
|
4472
|
-
return {
|
|
4473
|
-
status: "initializing",
|
|
4474
|
-
timeStarted: Date.now(),
|
|
4475
|
-
initializationPromise: this._bringUp(statusInfo.readyData)
|
|
4476
|
-
};
|
|
4477
|
-
}
|
|
4478
|
-
if (statusInfo.status === "initializing") {
|
|
4479
|
-
const initializationPromise = statusInfo.initializationPromise.then((result) => result.status === "ready" ? this._bringUp(result.readyData) : result);
|
|
4480
|
-
return {
|
|
4481
|
-
status: "initializing",
|
|
4482
|
-
timeStarted: statusInfo.timeStarted,
|
|
4483
|
-
initializationPromise
|
|
4484
|
-
};
|
|
4485
|
-
}
|
|
4486
|
-
return statusInfo;
|
|
4487
|
-
}
|
|
4488
|
-
/** Await carrier readiness, then finalize (possibly running a handshake) into the live methods. */
|
|
4489
|
-
async _bringUp(readyData) {
|
|
4490
|
-
await this._awaitCarrierReady(readyData);
|
|
4491
|
-
return {
|
|
4492
|
-
status: "ready",
|
|
4493
|
-
readyData: await this._finalizeReady(readyData)
|
|
4494
|
-
};
|
|
4495
|
-
}
|
|
4496
|
-
};
|
|
4497
|
-
//#endregion
|
|
4498
|
-
//#region src/ActionRuntime/Transport/Exchange/ExchangeConnection.ts
|
|
4499
|
-
/**
|
|
4500
|
-
* Carrier-agnostic live connection for the exchange (request → single reply) shape — the HTTP
|
|
4501
|
-
* counterpart to {@link LinkConnection}. It owns only the bring-up (run the secure handshake on first
|
|
4502
|
-
* use); the request/reply lifecycle + crypto live in the shared `establishExchangeSession`.
|
|
4503
|
-
*/
|
|
4504
|
-
var ExchangeConnection = class extends TransportConnection {
|
|
4505
|
-
constructor(def) {
|
|
4506
|
-
super({
|
|
4507
|
-
...def,
|
|
4508
|
-
type: "exchange"
|
|
4509
|
-
});
|
|
4510
|
-
}
|
|
4511
|
-
_getCacheKey(input) {
|
|
4512
|
-
return this.initialized.getTransportCacheKey?.(input).join("\0") ?? "";
|
|
4513
|
-
}
|
|
4514
|
-
_needsAsyncBringUp(data) {
|
|
4515
|
-
return data.secureChannel != null && data.secureChannel.securityLevel !== "none";
|
|
4516
|
-
}
|
|
4517
|
-
_finalizeReady(data) {
|
|
4518
|
-
const secure = data.secureChannel;
|
|
4519
|
-
if (secure != null && secure.securityLevel !== "none") return finalizeSecureExchangeMethods({
|
|
4520
|
-
...this._sessionContext(data),
|
|
4521
|
-
secure
|
|
4522
|
-
});
|
|
4523
|
-
return this._finalizeTransportMethods(data);
|
|
4524
|
-
}
|
|
4525
|
-
_finalizeTransportMethods(data) {
|
|
4526
|
-
return finalizePlainExchangeMethods(this._sessionContext(data));
|
|
4527
|
-
}
|
|
4528
|
-
_sessionContext(data) {
|
|
4529
|
-
return {
|
|
4530
|
-
carrier: data.carrier,
|
|
4531
|
-
updateRunConfig: data.updateRunConfig,
|
|
4532
|
-
secure: data.secureChannel
|
|
4533
|
-
};
|
|
4534
|
-
}
|
|
4535
|
-
};
|
|
4536
|
-
//#endregion
|
|
4537
|
-
//#region src/ActionRuntime/Transport/Exchange/ExchangeTransport.ts
|
|
4538
|
-
/**
|
|
4539
|
-
* A carrier-agnostic exchange (request → single reply) transport: it drives nice-action's secure session
|
|
4540
|
-
* over any {@link IExchangeCarrier} (HTTP being the one built-in). The duplex counterpart is
|
|
4541
|
-
* {@link LinkTransport}; this is the no-push half — its reply rides the response to its own request, so it
|
|
4542
|
-
* can't deliver an unsolicited frame (the runtime never picks it for the return path).
|
|
4543
|
-
*/
|
|
4544
|
-
var ExchangeTransport = class ExchangeTransport extends Transport {
|
|
4545
|
-
options;
|
|
4546
|
-
type = "exchange";
|
|
4547
|
-
constructor(options) {
|
|
4548
|
-
super();
|
|
4549
|
-
this.options = options;
|
|
4550
|
-
}
|
|
4551
|
-
static create(options) {
|
|
4552
|
-
return new ExchangeTransport(options);
|
|
4553
|
-
}
|
|
4554
|
-
_createConnection(_ctx) {
|
|
4555
|
-
const options = this.options;
|
|
4556
|
-
return new ExchangeConnection({ initialize: () => ({
|
|
4557
|
-
getTransportCacheKey: options.getTransportCacheKey,
|
|
4558
|
-
isAvailable: options.available,
|
|
4559
|
-
getTransport: (input) => ({
|
|
4560
|
-
status: "ready",
|
|
4561
|
-
readyData: {
|
|
4562
|
-
carrier: options.openCarrier(input),
|
|
4563
|
-
secureChannel: options.security,
|
|
4564
|
-
updateRunConfig: options.updateRunConfig
|
|
4565
|
-
}
|
|
4566
|
-
})
|
|
4567
|
-
}) });
|
|
4568
|
-
}
|
|
4569
|
-
getRouteInfo(input) {
|
|
4570
|
-
if (this.options.getRouteInfo != null) return this.options.getRouteInfo(input);
|
|
4571
|
-
return {
|
|
4572
|
-
carrierLabel: this.options.label ?? "exchange",
|
|
4573
|
-
summary: this.options.label ?? "exchange"
|
|
4574
|
-
};
|
|
4575
|
-
}
|
|
4576
|
-
};
|
|
4577
|
-
//#endregion
|
|
4578
|
-
//#region src/ActionRuntime/Transport/helpers/createUnsetTransportResolvers.ts
|
|
4579
|
-
const createUnsetTransportResolvers = (transportLabel) => ({ onIncomingActionDataJson: (json) => {
|
|
4580
|
-
console.warn(`Received incoming action JSON [${json.domain}:${json.id}] on Transport [${transportLabel}] but no incoming data listener has been set.`);
|
|
4581
|
-
} });
|
|
4582
|
-
//#endregion
|
|
4583
|
-
//#region src/ActionRuntime/Transport/SecureSession/establishLinkSession.ts
|
|
4584
|
-
const HANDSHAKE_TIMEOUT_MS = 15e3;
|
|
4585
|
-
/** Plain path (no handshake): route every inbound frame to the runtime; send without crypto. */
|
|
4586
|
-
function finalizePlainLinkMethods(ctx) {
|
|
4587
|
-
const disconnectListeners = [];
|
|
4588
|
-
const abortSet = /* @__PURE__ */ new Set();
|
|
4589
|
-
const pipe = makePipe(ctx, void 0);
|
|
4590
|
-
ctx.channel.attach({
|
|
4591
|
-
onMessage: (frame) => void handleIncomingActionFrame(ctx, pipe, frame),
|
|
4592
|
-
onClose: () => onChannelClosed(ctx, disconnectListeners, abortSet),
|
|
4593
|
-
onError: () => onChannelClosed(ctx, disconnectListeners, abortSet)
|
|
4594
|
-
});
|
|
4595
|
-
return buildSendMethods(ctx, pipe, disconnectListeners, abortSet);
|
|
4596
|
-
}
|
|
4597
|
-
/**
|
|
4598
|
-
* Secure path: a single message handler feeds the handshake until it completes, then routes action
|
|
4599
|
-
* frames (decrypting at the `encrypted` level). Frames that race ahead of activation are buffered and
|
|
4600
|
-
* flushed once the handshake lands, so nothing is lost.
|
|
4601
|
-
*/
|
|
4602
|
-
async function finalizeSecureLinkMethods(ctx) {
|
|
4603
|
-
const disconnectListeners = [];
|
|
4604
|
-
const abortSet = /* @__PURE__ */ new Set();
|
|
4605
|
-
const session = {};
|
|
4606
|
-
let active = false;
|
|
4607
|
-
const handshakeQueue = [];
|
|
4608
|
-
const handshakeWaiters = [];
|
|
4609
|
-
const pendingActionFrames = [];
|
|
4610
|
-
ctx.channel.attach({
|
|
4611
|
-
onMessage: (frame) => {
|
|
4612
|
-
if (active && session.pipe != null) {
|
|
4613
|
-
handleIncomingActionFrame(ctx, session.pipe, frame);
|
|
4614
|
-
return;
|
|
4615
|
-
}
|
|
4616
|
-
if (typeof frame === "string") {
|
|
4617
|
-
const message = decodeHandshakeMessage(frame);
|
|
4618
|
-
if (message != null) {
|
|
4619
|
-
const waiter = handshakeWaiters.shift();
|
|
4620
|
-
if (waiter != null) waiter(message);
|
|
4621
|
-
else handshakeQueue.push(message);
|
|
4622
|
-
return;
|
|
4623
|
-
}
|
|
4624
|
-
}
|
|
4625
|
-
pendingActionFrames.push(frame);
|
|
4626
|
-
},
|
|
4627
|
-
onClose: () => onChannelClosed(ctx, disconnectListeners, abortSet),
|
|
4628
|
-
onError: () => onChannelClosed(ctx, disconnectListeners, abortSet)
|
|
4629
|
-
});
|
|
4630
|
-
const nextHandshakeMessage = () => {
|
|
4631
|
-
const queued = handshakeQueue.shift();
|
|
4632
|
-
if (queued != null) return Promise.resolve(queued);
|
|
4633
|
-
return new Promise((resolve, reject) => {
|
|
4634
|
-
const timeout = setTimeout(() => reject(/* @__PURE__ */ new Error("[link-handshake] timed out waiting for peer reply")), HANDSHAKE_TIMEOUT_MS);
|
|
4635
|
-
handshakeWaiters.push((message) => {
|
|
4636
|
-
clearTimeout(timeout);
|
|
4637
|
-
resolve(message);
|
|
4638
|
-
});
|
|
4639
|
-
});
|
|
4640
|
-
};
|
|
4641
|
-
const pipe = makePipe(ctx, await runClientHandshake(ctx.channel, ctx.secure, nextHandshakeMessage));
|
|
4642
|
-
session.pipe = pipe;
|
|
4643
|
-
active = true;
|
|
4644
|
-
for (const frame of pendingActionFrames) await handleIncomingActionFrame(ctx, pipe, frame);
|
|
4645
|
-
pendingActionFrames.length = 0;
|
|
4646
|
-
return buildSendMethods(ctx, pipe, disconnectListeners, abortSet);
|
|
4647
|
-
}
|
|
4648
|
-
function makePipe(ctx, crypto) {
|
|
4649
|
-
return createFrameCryptoPipe({
|
|
4650
|
-
write: (frame) => ctx.channel.send(frame),
|
|
4651
|
-
isOpen: () => ctx.channel.isOpen(),
|
|
4652
|
-
crypto
|
|
4653
|
-
});
|
|
4654
|
-
}
|
|
4655
|
-
async function runClientHandshake(channel, secure, nextHandshakeMessage) {
|
|
4656
|
-
await secure.link.initialize();
|
|
4657
|
-
const handshake = createClientHandshake({
|
|
4658
|
-
link: secure.link,
|
|
4659
|
-
localCoordinate: secure.localCoordinate,
|
|
4660
|
-
dictionaryVersion: secure.dictionaryVersion,
|
|
4661
|
-
securityLevel: secure.securityLevel
|
|
4662
|
-
});
|
|
4663
|
-
channel.send(encodeHandshakeMessage(await handshake.createHello()));
|
|
4664
|
-
const welcome = await nextHandshakeMessage();
|
|
4665
|
-
if (welcome.t === "reject") throw new Error(`[link-handshake] rejected by peer: ${welcome.reason}`);
|
|
4666
|
-
if (welcome.t !== "welcome") throw new Error(`[link-handshake] expected welcome, got ${welcome.t}`);
|
|
4667
|
-
channel.send(encodeHandshakeMessage(await handshake.onWelcome(welcome)));
|
|
4668
|
-
const accept = await nextHandshakeMessage();
|
|
4669
|
-
if (accept.t === "reject") throw new Error(`[link-handshake] rejected by peer: ${accept.reason}`);
|
|
4670
|
-
if (accept.t !== "accept") throw new Error(`[link-handshake] expected accept, got ${accept.t}`);
|
|
4671
|
-
const result = await handshake.onAccept(accept);
|
|
4672
|
-
return result.securityLevel === "encrypted" ? createActionFrameCrypto({
|
|
4673
|
-
link: secure.link,
|
|
4674
|
-
linkedClientId: result.linkedClientId
|
|
4675
|
-
}) : void 0;
|
|
4676
|
-
}
|
|
4677
|
-
function buildSendMethods(ctx, pipe, disconnectListeners, abortSet) {
|
|
4678
|
-
const channel = ctx.channel;
|
|
4679
|
-
const sendActionData = (inputs) => {
|
|
4680
|
-
const { action, runningAction, timeout } = inputs;
|
|
4681
|
-
if (!channel.isOpen()) {
|
|
4682
|
-
if (action.type === "request") runningAction._abort(ctx.makeDisconnectError(action.id));
|
|
4683
|
-
return;
|
|
4684
|
-
}
|
|
4685
|
-
if (action.type === "request") {
|
|
4686
|
-
abortSet.add(runningAction);
|
|
4687
|
-
const timeoutId = setTimeout(() => {
|
|
4688
|
-
runningAction._abort(err_nice_transport.fromId("timeout", { timeout }));
|
|
4689
|
-
}, timeout);
|
|
4690
|
-
runningAction.addUpdateListeners([(update) => {
|
|
4691
|
-
if (update.type === "finished") {
|
|
4692
|
-
clearTimeout(timeoutId);
|
|
4693
|
-
abortSet.delete(runningAction);
|
|
4694
|
-
}
|
|
4695
|
-
}]);
|
|
4696
|
-
}
|
|
4697
|
-
pipe.send(ctx.formatMessage?.outgoing(inputs) ?? JSON.stringify(inputs.action.toJsonObject()));
|
|
4698
|
-
};
|
|
4699
|
-
return {
|
|
4700
|
-
sendActionData,
|
|
4701
|
-
updateRunConfig: ctx.updateRunConfig,
|
|
4702
|
-
addOnDisconnectListener: (cb) => {
|
|
4703
|
-
disconnectListeners.push(cb);
|
|
4704
|
-
},
|
|
4705
|
-
disconnect: () => {
|
|
4706
|
-
try {
|
|
4707
|
-
channel.close();
|
|
4708
|
-
} catch {}
|
|
4709
|
-
},
|
|
4710
|
-
sendReturnData: (payload, clients) => {
|
|
4711
|
-
const formatted = clients != null ? ctx.formatMessage?.outgoing({
|
|
4712
|
-
action: payload,
|
|
4713
|
-
...clients
|
|
4714
|
-
}) : void 0;
|
|
4715
|
-
pipe.send(formatted ?? JSON.stringify(payload.toJsonObject()));
|
|
4716
|
-
}
|
|
4717
|
-
};
|
|
4718
|
-
}
|
|
4719
|
-
async function handleIncomingActionFrame(ctx, pipe, frame) {
|
|
4720
|
-
const decoded = await pipe.decryptIncoming(frame);
|
|
4721
|
-
if (decoded === void 0) return;
|
|
4722
|
-
const rawJson = decodeActionFrame(decoded, ctx.formatMessage);
|
|
4723
|
-
if (rawJson != null) ctx.resolvers.onIncomingActionDataJson(rawJson);
|
|
4724
|
-
}
|
|
4725
|
-
function onChannelClosed(ctx, disconnectListeners, abortSet) {
|
|
4726
|
-
for (const cb of disconnectListeners) cb();
|
|
4727
|
-
const error = ctx.makeDisconnectError("—");
|
|
4728
|
-
for (const ra of [...abortSet]) ra._abort(error);
|
|
4729
|
-
}
|
|
4730
|
-
//#endregion
|
|
4731
|
-
//#region src/ActionRuntime/Transport/Link/LinkConnection.ts
|
|
4732
|
-
/** Abort error for a closed link channel (carrier-neutral — the carrier itself isn't named). */
|
|
4733
|
-
function linkDisconnectError(actionId) {
|
|
4734
|
-
return err_nice_transport.fromId("send_failed", {
|
|
4735
|
-
actionId,
|
|
4736
|
-
actionState: "request",
|
|
4737
|
-
message: "link channel disconnected"
|
|
4738
|
-
});
|
|
4739
|
-
}
|
|
4740
|
-
/**
|
|
4741
|
-
* Carrier-agnostic live connection. It owns only the *bring-up* (open the carrier, then run the secure
|
|
4742
|
-
* session); the session itself — handshake, frame crypto, codec, send/receive — lives in the shared
|
|
4743
|
-
* {@link finalizeSecureLinkMethods}/{@link finalizePlainLinkMethods}, so a WebSocket, a WebRTC data
|
|
4744
|
-
* channel, a Bluetooth characteristic, and an in-memory pipe all run the identical secure layer.
|
|
4745
|
-
*/
|
|
4746
|
-
var LinkConnection = class extends TransportConnection {
|
|
4747
|
-
resolvers;
|
|
4748
|
-
constructor(def, resolvers) {
|
|
4749
|
-
super({
|
|
4750
|
-
...def,
|
|
4751
|
-
type: "duplex"
|
|
4752
|
-
});
|
|
4753
|
-
this.resolvers = resolvers ?? createUnsetTransportResolvers("link");
|
|
4754
|
-
}
|
|
4755
|
-
_getCacheKey(input) {
|
|
4756
|
-
return this.initialized.getTransportCacheKey?.(input).join("\0") ?? "";
|
|
4757
|
-
}
|
|
4758
|
-
_needsAsyncBringUp() {
|
|
4759
|
-
return true;
|
|
4760
|
-
}
|
|
4761
|
-
_awaitCarrierReady(data) {
|
|
4762
|
-
return data.channel.ready;
|
|
4763
|
-
}
|
|
4764
|
-
_finalizeReady(data) {
|
|
4765
|
-
const secure = data.secureChannel;
|
|
4766
|
-
if (secure != null && secure.securityLevel !== "none") return finalizeSecureLinkMethods({
|
|
4767
|
-
...this._sessionContext(data),
|
|
4768
|
-
secure
|
|
4769
|
-
});
|
|
4770
|
-
return this._finalizeTransportMethods(data);
|
|
4771
|
-
}
|
|
4772
|
-
_sessionContext(data) {
|
|
4773
|
-
return {
|
|
4774
|
-
channel: data.channel,
|
|
4775
|
-
resolvers: this.resolvers,
|
|
4776
|
-
formatMessage: data.formatMessage,
|
|
4777
|
-
updateRunConfig: data.updateRunConfig,
|
|
4778
|
-
makeDisconnectError: linkDisconnectError
|
|
4779
|
-
};
|
|
4780
|
-
}
|
|
4781
|
-
_finalizeTransportMethods(data) {
|
|
4782
|
-
return finalizePlainLinkMethods(this._sessionContext(data));
|
|
4783
|
-
}
|
|
4784
|
-
};
|
|
4785
|
-
//#endregion
|
|
4786
|
-
//#region src/ActionRuntime/Transport/Link/LinkTransport.ts
|
|
4787
|
-
/**
|
|
4788
|
-
* A carrier-agnostic transport: it drives nice-action's secure session + action routing over any
|
|
4789
|
-
* {@link IDuplexCarrier}. The WebSocket transport is the special case that opens a `WebSocket`;
|
|
4790
|
-
* this opens whatever `openChannel` returns, so the identical secure layer works over WebRTC, Bluetooth,
|
|
4791
|
-
* or an in-memory pipe. Reported with an overridable carrier label in the devtools (defaults to "link").
|
|
4792
|
-
*/
|
|
4793
|
-
var LinkTransport = class LinkTransport extends Transport {
|
|
4794
|
-
options;
|
|
4795
|
-
type = "duplex";
|
|
4796
|
-
constructor(options) {
|
|
4797
|
-
super();
|
|
4798
|
-
this.options = options;
|
|
4799
|
-
}
|
|
4800
|
-
static create(options) {
|
|
4801
|
-
return new LinkTransport(options);
|
|
4802
|
-
}
|
|
4803
|
-
_createConnection(ctx) {
|
|
4804
|
-
const options = this.options;
|
|
4805
|
-
return new LinkConnection({ initialize: () => ({
|
|
4806
|
-
getTransportCacheKey: options.getTransportCacheKey,
|
|
4807
|
-
isAvailable: options.available,
|
|
4808
|
-
getTransport: (input) => ({
|
|
4809
|
-
status: "ready",
|
|
4810
|
-
readyData: {
|
|
4811
|
-
channel: options.openChannel(input),
|
|
4812
|
-
formatMessage: options.createFormatMessage?.() ?? options.formatMessage,
|
|
4813
|
-
updateRunConfig: options.updateRunConfig,
|
|
4814
|
-
secureChannel: options.security
|
|
4815
|
-
}
|
|
4816
|
-
})
|
|
4817
|
-
}) }, ctx.resolvers);
|
|
4818
|
-
}
|
|
4819
|
-
getRouteInfo(input) {
|
|
4820
|
-
if (this.options.getRouteInfo != null) return this.options.getRouteInfo(input);
|
|
4821
|
-
return {
|
|
4822
|
-
carrierLabel: this.options.label ?? "link",
|
|
4823
|
-
summary: this.options.label ?? "link"
|
|
4824
|
-
};
|
|
4825
|
-
}
|
|
4826
|
-
};
|
|
4827
|
-
//#endregion
|
|
4828
|
-
//#region src/ActionRuntime/Transport/Carrier/Carrier.types.ts
|
|
4829
|
-
/**
|
|
4830
|
-
* Narrow a carrier source to the exchange shape via its `shape` discriminant — the one branch the
|
|
4831
|
-
* transport factories ({@link secureTransport}, {@link plainTransport}) use to pick the duplex vs
|
|
4832
|
-
* exchange transport. A duplex source carries no `shape`, so the `else` branch is the duplex one.
|
|
4833
|
-
*/
|
|
4834
|
-
function isExchangeCarrierSource(carrier) {
|
|
4835
|
-
return "shape" in carrier && carrier.shape === "exchange";
|
|
4836
|
-
}
|
|
4837
|
-
//#endregion
|
|
4838
|
-
//#region src/ActionRuntime/Transport/plainTransport.ts
|
|
4839
|
-
function plainTransport(options) {
|
|
4840
|
-
const carrier = options.carrier;
|
|
4841
|
-
if (isExchangeCarrierSource(carrier)) return ExchangeTransport.create({
|
|
4842
|
-
openCarrier: carrier.open,
|
|
4843
|
-
getTransportCacheKey: carrier.getCacheKey,
|
|
4844
|
-
available: options.available,
|
|
4845
|
-
getRouteInfo: carrier.getRouteInfo,
|
|
4846
|
-
label: options.label ?? carrier.carrierLabel,
|
|
4847
|
-
updateRunConfig: options.updateRunConfig
|
|
4848
|
-
});
|
|
4849
|
-
return LinkTransport.create({
|
|
4850
|
-
openChannel: carrier.open,
|
|
4851
|
-
formatMessage: options.formatMessage,
|
|
4852
|
-
createFormatMessage: options.createFormatMessage,
|
|
4853
|
-
getTransportCacheKey: carrier.getCacheKey,
|
|
4854
|
-
available: options.available,
|
|
4855
|
-
getRouteInfo: carrier.getRouteInfo,
|
|
4856
|
-
label: options.label ?? carrier.carrierLabel,
|
|
4857
|
-
updateRunConfig: options.updateRunConfig
|
|
4858
|
-
});
|
|
4859
|
-
}
|
|
4860
|
-
//#endregion
|
|
4861
|
-
//#region src/ActionRuntime/Transport/secureTransport.ts
|
|
4862
|
-
function secureTransport(options) {
|
|
4863
|
-
const link = new ClientCryptoKeyLink({ storageAdapter: options.storageAdapter });
|
|
4864
|
-
const security = {
|
|
4865
|
-
securityLevel: options.securityLevel,
|
|
4866
|
-
link,
|
|
4867
|
-
localCoordinate: options.runtime.coordinate.toJsonObject(),
|
|
4868
|
-
dictionaryVersion: options.channel.dictionaryVersion
|
|
4869
|
-
};
|
|
4870
|
-
const carrier = options.carrier;
|
|
4871
|
-
if (isExchangeCarrierSource(carrier)) return ExchangeTransport.create({
|
|
4872
|
-
openCarrier: carrier.open,
|
|
4873
|
-
getTransportCacheKey: carrier.getCacheKey,
|
|
4874
|
-
available: options.available,
|
|
4875
|
-
getRouteInfo: carrier.getRouteInfo,
|
|
4876
|
-
label: carrier.carrierLabel,
|
|
4877
|
-
security
|
|
4878
|
-
});
|
|
4879
|
-
return LinkTransport.create({
|
|
4880
|
-
openChannel: carrier.open,
|
|
4881
|
-
createFormatMessage: options.channel.createCodec,
|
|
4882
|
-
getTransportCacheKey: carrier.getCacheKey,
|
|
4883
|
-
available: options.available,
|
|
4884
|
-
getRouteInfo: carrier.getRouteInfo,
|
|
4885
|
-
label: carrier.carrierLabel,
|
|
4886
|
-
security
|
|
4887
|
-
});
|
|
4888
|
-
}
|
|
4889
|
-
//#endregion
|
|
4890
|
-
export { AcceptorHandler, ActionCore, ActionDomain, ActionLocalHandler, ActionRootDomain, ActionRuntime, ActionSchema, ConnectionStateStore, ConnectorHandler, EActionPayloadType, EActionProgressType, EActionResponseMode, EErrId_NiceAction, EErrId_NiceTransport, EErrId_NiceTransport_WebSocket, EHandshakeMessageType, ERunningActionFinishedType, ERunningActionState, ERunningActionUpdateType, ESecurityLevel, ETransportShape, ETransportStatus, ExchangeAcceptor, ExchangeTransport, LinkTransport, PeerLinkHandler, RunningAction, RuntimeCoordinate, Transport, acceptChannel, acceptChannelConnections, actionSchema, connectChannel, createAcceptorHandler, createActionFetchHandler, createActionFrameCrypto, createActionRootDomain, createBinaryWireAdapter, createBinaryWireSessionFactory, createClientHandshake, createConnectionStateStore, createConnectorHandler, createHibernatableWsServerAdapter, createInMemoryChannelPair, createInMemoryTofuVerifyKeyResolver, createLocalHandler, createSecureAcceptorHandler, createServerHandshake, createStorageTofuVerifyKeyResolver, decodeActionFrame, decodeExchangeReply, decodeExchangeRequest, decodeHandshakeMessage, defineChannel, defineSecureChannel, encodeExchange, encodeHandshakeMessage, err_nice_action, err_nice_external_client, err_nice_transport, err_nice_transport_ws, httpAcceptorCarrier, httpCarrier, inMemoryCarrier, isActionPayload_Any_JsonObject, isActionPayload_Request_JsonObject, isActionPayload_Result_JsonObject, isExchangeAcceptorCarrier, plainTransport, rtcCarrier, rtcDataChannelByteChannel, runtimeLinkId, secureTransport, serveChannel, wsAcceptorCarrier, wsCarrier };
|
|
1163
|
+
export { AcceptorHandler, ActionCore, ActionDomain, ActionLocalHandler, ActionRootDomain, ActionRuntime, ActionSchema, ConnectionStateStore, ConnectorHandler, EActionPayloadType, EActionProgressType, EActionResponseMode, EErrId_NiceAction, EErrId_NiceTransport, EErrId_NiceTransport_WebSocket, EHandshakeMessageType, ERunningActionFinishedType, ERunningActionState, ERunningActionUpdateType, ESecurityLevel, ETransportShape, ETransportStatus, ExchangeAcceptor, ExchangeTransport, LinkTransport, PeerLinkHandler, RunningAction, RuntimeCoordinate, Transport, acceptChannel, acceptChannelConnections, actionSchema, connectChannel, createAcceptorHandler, createActionFetchHandler, createActionFrameCrypto, createActionRootDomain, createBinaryWireAdapter, createBinaryWireSessionFactory, createClientHandshake, createConnectionStateStore, createConnectorHandler, createHibernatableWsServerAdapter, createInMemoryChannelPair, createInMemoryTofuVerifyKeyResolver, createLocalHandler, createSecureAcceptorHandler, createServerHandshake, createStorageTofuVerifyKeyResolver, decodeActionFrame, decodeExchangeReply, decodeExchangeRequest, decodeHandshakeMessage, defineChannel, defineSecureChannel, encodeExchange, encodeHandshakeMessage, err_nice_action, err_nice_external_client, err_nice_transport, err_nice_transport_ws, httpAcceptorCarrier, httpCarrier, inMemoryCarrier, isActionPayload_Any_JsonObject, isActionPayload_Request_JsonObject, isActionPayload_Result_JsonObject, isExchangeAcceptorCarrier, rtcCarrier, rtcDataChannelByteChannel, runtimeLinkId, serveChannel, serveHost, wsAcceptorCarrier, wsCarrier };
|
|
4891
1164
|
|
|
4892
1165
|
//# sourceMappingURL=index.mjs.map
|