@camstack/system 1.2.218 → 1.2.220
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/addon-runner.js +2 -2
- package/dist/addon-runner.mjs +2 -2
- package/dist/addon-utils.js +2 -2
- package/dist/addon-utils.mjs +2 -2
- package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +1 -1
- package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +1 -1
- package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +1 -1
- package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +1 -1
- package/dist/builtins/alerts/alerts.addon.js +1 -1
- package/dist/builtins/alerts/alerts.addon.mjs +1 -1
- package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +1 -1
- package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +1 -1
- package/dist/builtins/console-logging/index.js +1 -1
- package/dist/builtins/console-logging/index.mjs +1 -1
- package/dist/builtins/core-blocks/core-blocks.addon.js +1 -1
- package/dist/builtins/core-blocks/core-blocks.addon.mjs +1 -1
- package/dist/builtins/device-manager/device-manager.addon.js +2 -2
- package/dist/builtins/device-manager/device-manager.addon.mjs +2 -2
- package/dist/builtins/doorbell/virtual-doorbell.addon.js +1 -1
- package/dist/builtins/doorbell/virtual-doorbell.addon.mjs +1 -1
- package/dist/builtins/hub-forwarder/index.js +1 -1
- package/dist/builtins/hub-forwarder/index.mjs +1 -1
- package/dist/builtins/liveness-monitor/liveness-monitor.addon.js +1 -1
- package/dist/builtins/liveness-monitor/liveness-monitor.addon.mjs +1 -1
- package/dist/builtins/local-auth/local-auth.addon.js +1 -1
- package/dist/builtins/local-auth/local-auth.addon.mjs +1 -1
- package/dist/builtins/local-network/local-network.addon.js +2 -2
- package/dist/builtins/local-network/local-network.addon.mjs +2 -2
- package/dist/builtins/loki-logging/index.js +1 -1
- package/dist/builtins/loki-logging/index.mjs +1 -1
- package/dist/builtins/native-metrics/gpu-probe.d.ts +18 -0
- package/dist/builtins/native-metrics/native-metrics-provider.d.ts +1 -1
- package/dist/builtins/native-metrics/native-metrics.addon.d.ts +26 -3
- package/dist/builtins/native-metrics/native-metrics.addon.js +1320 -1093
- package/dist/builtins/native-metrics/native-metrics.addon.mjs +1320 -1096
- package/dist/builtins/native-metrics/proc-process-table.d.ts +25 -0
- package/dist/builtins/platform-probe/index.js +3 -3
- package/dist/builtins/platform-probe/index.mjs +3 -3
- package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
- package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
- package/dist/builtins/snapshot/index.js +1 -1
- package/dist/builtins/snapshot/index.mjs +1 -1
- package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +1 -1
- package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +1 -1
- package/dist/builtins/sqlite-storage/sqlite-pragmas.d.ts +23 -0
- package/dist/builtins/sqlite-storage/sqlite-settings-backend.d.ts +1 -1
- package/dist/builtins/sqlite-storage/sqlite-settings.addon.d.ts +1 -0
- package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +0 -0
- package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +0 -0
- package/dist/builtins/sqlite-storage/wal-checkpoint-policy.d.ts +115 -0
- package/dist/builtins/sqlite-storage/wal-checkpoint-worker.d.ts +1 -0
- package/dist/builtins/sqlite-storage/wal-checkpointer.d.ts +70 -0
- package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +1 -1
- package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +1 -1
- package/dist/builtins/system-config/system-config.addon.js +1 -1
- package/dist/builtins/system-config/system-config.addon.mjs +1 -1
- package/dist/builtins/winston-logging/index.js +1 -1
- package/dist/builtins/winston-logging/index.mjs +1 -1
- package/dist/{custom-action-registry-BDwM5qAK.js → custom-action-registry-B42MC_Pv.js} +1 -1
- package/dist/{custom-action-registry-CsONUlhC.mjs → custom-action-registry-CpR58DXT.mjs} +1 -1
- package/dist/{dist-DigmGsQS.mjs → dist-Ca_jaxXs.mjs} +232 -4
- package/dist/{dist-BOlMCOeT.js → dist-CllKpcTd.js} +232 -4
- package/dist/index.d.ts +6 -1
- package/dist/index.js +99 -65
- package/dist/index.mjs +84 -67
- package/dist/kernel/gc-census.d.ts +144 -0
- package/dist/kernel/heap-watch.d.ts +32 -1
- package/dist/kernel/moleculer/mesh-queue-report.d.ts +51 -0
- package/dist/kernel/outbound-queue-watch.d.ts +94 -0
- package/dist/kernel/socket-plane-report.d.ts +112 -4
- package/dist/kernel/transport/cap-action-census.d.ts +218 -0
- package/dist/kernel/transport/cap-child-index.d.ts +49 -0
- package/dist/kernel/transport/cap-routing-hints.d.ts +21 -0
- package/dist/kernel/transport/child-cap-protocol.d.ts +28 -2
- package/dist/kernel/transport/frame-codec.d.ts +49 -0
- package/dist/kernel/transport/index.d.ts +2 -0
- package/dist/kernel/transport/local-child-client.d.ts +7 -0
- package/dist/kernel/transport/local-child-registry.d.ts +99 -5
- package/dist/kernel/transport/local-transport.d.ts +54 -2
- package/dist/kernel/transport/socket-channel.d.ts +40 -1
- package/dist/{manifest-system-deps-Bo92lg5u.js → manifest-system-deps-DfpwaXkh.js} +1725 -614
- package/dist/{manifest-system-deps-D--wyMD9.mjs → manifest-system-deps-iadzxSBr.mjs} +1850 -841
- package/dist/process/proc-stat.d.ts +75 -0
- package/dist/process/resource-monitor.d.ts +9 -1
- package/dist/{resource-monitor-CdnzxBLP.js → resource-monitor-LVE1BoLs.js} +42 -23
- package/dist/{resource-monitor-BWmQ5i-o.mjs → resource-monitor-Vp0bPV3U.mjs} +42 -22
- package/dist/{retired-settings-keys-xEAlYnv7.js → retired-settings-keys-7iEsSzmD.js} +1 -1
- package/dist/{retired-settings-keys-vK_U-CG2.mjs → retired-settings-keys-DQnfr__p.mjs} +1 -1
- package/dist/wal-checkpoint-policy-CAcg63o-.mjs +92 -0
- package/dist/wal-checkpoint-policy-CCJZngds.js +127 -0
- package/dist/wal-checkpoint-worker.js +138 -0
- package/dist/wal-checkpoint-worker.mjs +137 -0
- package/package.json +1 -1
- package/dist/{event-loop-stall-monitor-DM9OAzy8.js → event-loop-stall-monitor-CAsHdMwP.js} +1 -1
- package/dist/{event-loop-stall-monitor-B3CFd9tM.mjs → event-loop-stall-monitor-GPrVvx14.mjs} +1 -1
- package/dist/{tls-DOTmtLCW.mjs → tls-2xbgg78V.mjs} +2 -2
- package/dist/{tls-BxQlomxd.js → tls-BFs4PzoW.js} +2 -2
|
@@ -6,12 +6,12 @@ let _camstack_types_node = require("@camstack/types/node");
|
|
|
6
6
|
let node_fs_promises = require("node:fs/promises");
|
|
7
7
|
let node_path = require("node:path");
|
|
8
8
|
node_path = require_chunk.__toESM(node_path);
|
|
9
|
-
let node_child_process = require("node:child_process");
|
|
10
|
-
let node_util = require("node:util");
|
|
11
9
|
let node_fs = require("node:fs");
|
|
12
10
|
node_fs = require_chunk.__toESM(node_fs);
|
|
11
|
+
let node_child_process = require("node:child_process");
|
|
13
12
|
let node_os = require("node:os");
|
|
14
13
|
node_os = require_chunk.__toESM(node_os);
|
|
14
|
+
let node_util = require("node:util");
|
|
15
15
|
let node_http = require("node:http");
|
|
16
16
|
let node_v8 = require("node:v8");
|
|
17
17
|
node_v8 = require_chunk.__toESM(node_v8);
|
|
@@ -155,6 +155,166 @@ function formatEventPlane(window) {
|
|
|
155
155
|
return ` fanoutMode=uds:${window.udsMode}/cross-node:${window.crossNodeMode} children=${window.childrenConnected} childrenUndeclared=${window.childrenUndeclared} fanoutSent=${window.fanoutSent} fanoutSuppressed=${window.fanoutSuppressed} crossNodeDelivered=${window.crossNodeDelivered} crossNodeSuppressed=${window.crossNodeSuppressed}` + (top.length === 0 ? "" : ` fanoutTop=${top}`);
|
|
156
156
|
}
|
|
157
157
|
//#endregion
|
|
158
|
+
//#region src/kernel/transport/cap-action-census.ts
|
|
159
|
+
/** A process that has never read the census. */
|
|
160
|
+
var EMPTY_CAP_ACTION_READING = /* @__PURE__ */ new Map();
|
|
161
|
+
/**
|
|
162
|
+
* Bytes that crossed on a frame carrying no cap envelope.
|
|
163
|
+
*
|
|
164
|
+
* Named rather than omitted so the census's coverage is readable off its own
|
|
165
|
+
* output: `register`, `addon-call`, log frames and the event fan-out all land
|
|
166
|
+
* here. A census that reported only what it could name would look like a
|
|
167
|
+
* complete attribution of whatever share it happened to cover.
|
|
168
|
+
*/
|
|
169
|
+
var CAP_ACTION_UNLABELLED = "(unlabelled)";
|
|
170
|
+
/** Bytes of actions past {@link MAX_CAP_ACTIONS_PER_PEER} on one peer. */
|
|
171
|
+
var CAP_ACTION_OVERFLOW_LABEL = "(overflow)";
|
|
172
|
+
/**
|
|
173
|
+
* Distinct named actions kept per peer before the overflow bucket takes over.
|
|
174
|
+
*
|
|
175
|
+
* Sixty-four against a measured worst case of 14 (`pipeline-analytics`, the
|
|
176
|
+
* heaviest peer on this hub, 2026-09-11) — four times the observed shape, so a
|
|
177
|
+
* peer has to change character before it overflows, and a bound that is never
|
|
178
|
+
* reached in practice still forbids the unbounded growth a wire-supplied key
|
|
179
|
+
* makes possible in principle.
|
|
180
|
+
*/
|
|
181
|
+
var MAX_CAP_ACTIONS_PER_PEER = 64;
|
|
182
|
+
/**
|
|
183
|
+
* `<cap>.<method>` for a cap envelope, `null` for anything else.
|
|
184
|
+
*
|
|
185
|
+
* Both directions are labelled by the same function: a child's `cap-call-out`
|
|
186
|
+
* and the parent's `cap-call` carry `capName` + `method` in the same two keys,
|
|
187
|
+
* so one predicate covers the whole plane. Anything else — a `register`, an
|
|
188
|
+
* `addon-call`, a log or an event — is deliberately unlabelled; see
|
|
189
|
+
* {@link CAP_ACTION_UNLABELLED} for where its bytes go.
|
|
190
|
+
*
|
|
191
|
+
* Cost: two property reads, two `typeof`s and one concatenation of two short
|
|
192
|
+
* strings, called once per `req` frame (not per frame). Against the
|
|
193
|
+
* `encodeFrame` it sits beside — 1 948–1 975 ns on a 0.33 kB body — it is
|
|
194
|
+
* noise, and unlike `recordSocketFrame` it is not on the `evt` path at all.
|
|
195
|
+
*/
|
|
196
|
+
function capActionLabel(body) {
|
|
197
|
+
if (typeof body !== "object" || body === null) return null;
|
|
198
|
+
const env = body;
|
|
199
|
+
if (env.kind !== "cap-call" && env.kind !== "cap-call-out") return null;
|
|
200
|
+
if (typeof env.capName !== "string" || typeof env.method !== "string") return null;
|
|
201
|
+
return `${env.capName}.${env.method}`;
|
|
202
|
+
}
|
|
203
|
+
function emptyCounters() {
|
|
204
|
+
return {
|
|
205
|
+
calls: 0,
|
|
206
|
+
reqBytes: 0,
|
|
207
|
+
resBytes: 0,
|
|
208
|
+
raw: 0,
|
|
209
|
+
decoded: 0
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
function createCapActionCensus() {
|
|
213
|
+
const peers = /* @__PURE__ */ new Map();
|
|
214
|
+
/**
|
|
215
|
+
* The per-peer slot for `action`, creating it under the bound.
|
|
216
|
+
*
|
|
217
|
+
* Past {@link MAX_CAP_ACTIONS_PER_PEER} a NEW label is folded into the
|
|
218
|
+
* overflow bucket; labels already present keep their own slot, so a peer
|
|
219
|
+
* that overflows does not lose the attribution it had built.
|
|
220
|
+
*/
|
|
221
|
+
const slot = (peerId, action) => {
|
|
222
|
+
let byAction = peers.get(peerId);
|
|
223
|
+
if (byAction === void 0) {
|
|
224
|
+
byAction = /* @__PURE__ */ new Map();
|
|
225
|
+
peers.set(peerId, byAction);
|
|
226
|
+
}
|
|
227
|
+
const existing = byAction.get(action);
|
|
228
|
+
if (existing !== void 0) return existing;
|
|
229
|
+
if (byAction.size >= 64) {
|
|
230
|
+
let overflow = byAction.get(CAP_ACTION_OVERFLOW_LABEL);
|
|
231
|
+
if (overflow === void 0) {
|
|
232
|
+
overflow = emptyCounters();
|
|
233
|
+
byAction.set(CAP_ACTION_OVERFLOW_LABEL, overflow);
|
|
234
|
+
}
|
|
235
|
+
return overflow;
|
|
236
|
+
}
|
|
237
|
+
const fresh = emptyCounters();
|
|
238
|
+
byAction.set(action, fresh);
|
|
239
|
+
return fresh;
|
|
240
|
+
};
|
|
241
|
+
return {
|
|
242
|
+
record(peerId, action, reqBytes, resBytes) {
|
|
243
|
+
const c = slot(peerId, action);
|
|
244
|
+
c.calls += 1;
|
|
245
|
+
c.reqBytes += reqBytes;
|
|
246
|
+
c.resBytes += resBytes;
|
|
247
|
+
},
|
|
248
|
+
recordForward(peerId, action, kind) {
|
|
249
|
+
const c = slot(peerId, action);
|
|
250
|
+
if (kind === "raw") c.raw += 1;
|
|
251
|
+
else c.decoded += 1;
|
|
252
|
+
},
|
|
253
|
+
recordUnlabelled(peerId, bytes) {
|
|
254
|
+
const c = slot(peerId, CAP_ACTION_UNLABELLED);
|
|
255
|
+
c.resBytes += bytes;
|
|
256
|
+
},
|
|
257
|
+
forget(peerId) {
|
|
258
|
+
peers.delete(peerId);
|
|
259
|
+
},
|
|
260
|
+
read() {
|
|
261
|
+
const out = /* @__PURE__ */ new Map();
|
|
262
|
+
for (const [peerId, byAction] of peers) {
|
|
263
|
+
const copy = /* @__PURE__ */ new Map();
|
|
264
|
+
for (const [action, c] of byAction) copy.set(action, {
|
|
265
|
+
calls: c.calls,
|
|
266
|
+
reqBytes: c.reqBytes,
|
|
267
|
+
resBytes: c.resBytes,
|
|
268
|
+
raw: c.raw,
|
|
269
|
+
decoded: c.decoded
|
|
270
|
+
});
|
|
271
|
+
out.set(peerId, copy);
|
|
272
|
+
}
|
|
273
|
+
return out;
|
|
274
|
+
}
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
function sampleOf(reading, peerId, action) {
|
|
278
|
+
return reading.get(peerId)?.get(action);
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* Subtract the previous reading from the current one.
|
|
282
|
+
*
|
|
283
|
+
* A peer whose runner RESPAWNED reconnects under the same id with counters
|
|
284
|
+
* that went backwards. Clamped at 0, exactly as `diffSocketPlane` clamps for
|
|
285
|
+
* the same reason: its traffic in that window is genuinely unknown, and a
|
|
286
|
+
* negative number on a rate reads as a broken meter.
|
|
287
|
+
*/
|
|
288
|
+
function diffCapActionCensus(before, current, topN) {
|
|
289
|
+
const deltas = [];
|
|
290
|
+
let labelledBytes = 0;
|
|
291
|
+
let totalBytes = 0;
|
|
292
|
+
for (const [peerId, byAction] of current) for (const [action, now] of byAction) {
|
|
293
|
+
const then = sampleOf(before, peerId, action);
|
|
294
|
+
const reqBytes = Math.max(0, now.reqBytes - (then?.reqBytes ?? 0));
|
|
295
|
+
const resBytes = Math.max(0, now.resBytes - (then?.resBytes ?? 0));
|
|
296
|
+
const bytes = reqBytes + resBytes;
|
|
297
|
+
totalBytes += bytes;
|
|
298
|
+
if (action === "(unlabelled)" || action === "(overflow)") continue;
|
|
299
|
+
labelledBytes += bytes;
|
|
300
|
+
if (bytes === 0) continue;
|
|
301
|
+
deltas.push({
|
|
302
|
+
peerId,
|
|
303
|
+
action,
|
|
304
|
+
calls: Math.max(0, now.calls - (then?.calls ?? 0)),
|
|
305
|
+
reqBytes,
|
|
306
|
+
resBytes,
|
|
307
|
+
raw: Math.max(0, now.raw - (then?.raw ?? 0)),
|
|
308
|
+
decoded: Math.max(0, now.decoded - (then?.decoded ?? 0))
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
return {
|
|
312
|
+
top: deltas.toSorted((a, b) => b.reqBytes + b.resBytes - (a.reqBytes + a.resBytes) || a.peerId.localeCompare(b.peerId) || a.action.localeCompare(b.action)).slice(0, topN),
|
|
313
|
+
labelledBytes,
|
|
314
|
+
totalBytes
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
//#endregion
|
|
158
318
|
//#region src/kernel/transport/socket-traffic.ts
|
|
159
319
|
function createSocketDirectionCounters() {
|
|
160
320
|
return {
|
|
@@ -311,12 +471,17 @@ function createSocketPlaneReader(sources) {
|
|
|
311
471
|
peers.push({
|
|
312
472
|
peerId: child.childId,
|
|
313
473
|
tx: traffic.tx,
|
|
314
|
-
rx: traffic.rx
|
|
474
|
+
rx: traffic.rx,
|
|
475
|
+
queuedBytes: registry.getChildQueuedBytes(child.childId)
|
|
315
476
|
});
|
|
316
477
|
}
|
|
478
|
+
const forward = registry.readForwardCensus?.();
|
|
479
|
+
const actions = registry.readActionCensus?.();
|
|
317
480
|
return {
|
|
318
481
|
peers,
|
|
319
|
-
peersConnected: listed.length
|
|
482
|
+
peersConnected: listed.length,
|
|
483
|
+
...forward === void 0 ? {} : { forward },
|
|
484
|
+
...actions === void 0 ? {} : { actions }
|
|
320
485
|
};
|
|
321
486
|
} catch {
|
|
322
487
|
return;
|
|
@@ -334,8 +499,41 @@ function createSocketPlaneReader(sources) {
|
|
|
334
499
|
* stops being read.
|
|
335
500
|
*/
|
|
336
501
|
var SOCKET_PLANE_TOP_N = 5;
|
|
502
|
+
/** How many queue-holding peers get named. Three: a queue is an exception,
|
|
503
|
+
* and the line already carries five names for traffic. */
|
|
504
|
+
var SOCKET_QUEUE_TOP_N = 3;
|
|
505
|
+
/** The queue reading over the current counters — a level, so no baseline. */
|
|
506
|
+
function readSocketQueues(current) {
|
|
507
|
+
let totalBytes = 0;
|
|
508
|
+
let unmeasured = 0;
|
|
509
|
+
const holding = [];
|
|
510
|
+
for (const peer of current.peers) {
|
|
511
|
+
if (peer.queuedBytes === null) {
|
|
512
|
+
unmeasured += 1;
|
|
513
|
+
continue;
|
|
514
|
+
}
|
|
515
|
+
totalBytes += peer.queuedBytes;
|
|
516
|
+
if (peer.queuedBytes > 0) holding.push({
|
|
517
|
+
peerId: peer.peerId,
|
|
518
|
+
bytes: peer.queuedBytes
|
|
519
|
+
});
|
|
520
|
+
}
|
|
521
|
+
return {
|
|
522
|
+
totalBytes,
|
|
523
|
+
unmeasured,
|
|
524
|
+
top: holding.toSorted((a, b) => b.bytes - a.bytes || a.peerId.localeCompare(b.peerId)).slice(0, 3)
|
|
525
|
+
};
|
|
526
|
+
}
|
|
337
527
|
/** A process that has never read the plane. */
|
|
338
528
|
var EMPTY_SOCKET_PLANE_BASELINE = { peers: /* @__PURE__ */ new Map() };
|
|
529
|
+
/** The census over the window; a counter that went backwards reads as quiet. */
|
|
530
|
+
function diffForward(current, before) {
|
|
531
|
+
if (current === void 0) return void 0;
|
|
532
|
+
return {
|
|
533
|
+
raw: Math.max(0, current.raw - (before?.raw ?? 0)),
|
|
534
|
+
decoded: Math.max(0, current.decoded - (before?.decoded ?? 0))
|
|
535
|
+
};
|
|
536
|
+
}
|
|
339
537
|
function diffDirection(current, before) {
|
|
340
538
|
const previous = before ?? EMPTY_SOCKET_DIRECTION;
|
|
341
539
|
return {
|
|
@@ -361,6 +559,14 @@ function addDirection(into, add) {
|
|
|
361
559
|
function peerMessages(delta) {
|
|
362
560
|
return socketDirectionMessages(delta.tx) + socketDirectionMessages(delta.rx);
|
|
363
561
|
}
|
|
562
|
+
/**
|
|
563
|
+
* Total bytes a peer moved in the window, both directions — the OTHER
|
|
564
|
+
* ranking, and the one that names a peer the message ranking structurally
|
|
565
|
+
* cannot see (D454).
|
|
566
|
+
*/
|
|
567
|
+
function peerBytes(delta) {
|
|
568
|
+
return socketDirectionBytes(delta.tx) + socketDirectionBytes(delta.rx);
|
|
569
|
+
}
|
|
364
570
|
/** Subtract the previous cumulative reading from the current one. */
|
|
365
571
|
function diffSocketPlane(baseline, current, topN = 5) {
|
|
366
572
|
const deltas = [];
|
|
@@ -377,12 +583,18 @@ function diffSocketPlane(baseline, current, topN = 5) {
|
|
|
377
583
|
rx = addDirection(rx, delta.rx);
|
|
378
584
|
if (peerMessages(delta) > 0) deltas.push(delta);
|
|
379
585
|
}
|
|
586
|
+
const forward = diffForward(current.forward, baseline.forward);
|
|
587
|
+
const actions = current.actions === void 0 ? void 0 : diffCapActionCensus(baseline.actions ?? EMPTY_CAP_ACTION_READING, current.actions, 5);
|
|
380
588
|
return {
|
|
381
589
|
peersConnected: current.peersConnected,
|
|
382
590
|
peersMeasured: current.peers.length,
|
|
383
591
|
tx,
|
|
384
592
|
rx,
|
|
385
|
-
top: deltas.toSorted((a, b) => peerMessages(b) - peerMessages(a) || a.peerId.localeCompare(b.peerId)).slice(0, topN)
|
|
593
|
+
top: deltas.toSorted((a, b) => peerMessages(b) - peerMessages(a) || a.peerId.localeCompare(b.peerId)).slice(0, topN),
|
|
594
|
+
topBytes: deltas.toSorted((a, b) => peerBytes(b) - peerBytes(a) || a.peerId.localeCompare(b.peerId)).slice(0, topN),
|
|
595
|
+
queued: readSocketQueues(current),
|
|
596
|
+
...forward === void 0 ? {} : { forward },
|
|
597
|
+
...actions === void 0 ? {} : { actions }
|
|
386
598
|
};
|
|
387
599
|
}
|
|
388
600
|
function createSocketPlaneMeter(reader, topN = 5) {
|
|
@@ -391,24 +603,62 @@ function createSocketPlaneMeter(reader, topN = 5) {
|
|
|
391
603
|
const current = reader();
|
|
392
604
|
if (current === void 0) return void 0;
|
|
393
605
|
const window = diffSocketPlane(baseline, current, topN);
|
|
394
|
-
baseline = {
|
|
606
|
+
baseline = {
|
|
607
|
+
peers: new Map(current.peers.map((p) => [p.peerId, p])),
|
|
608
|
+
...current.forward === void 0 ? {} : { forward: current.forward },
|
|
609
|
+
...current.actions === void 0 ? {} : { actions: current.actions }
|
|
610
|
+
};
|
|
395
611
|
return window;
|
|
396
612
|
} };
|
|
397
613
|
}
|
|
398
614
|
/** Bytes → whole kB. The plane moves 180–330 MB/min; raw bytes would put
|
|
399
615
|
* nine-digit numbers on a line that has to stay readable. */
|
|
400
|
-
function kb(bytes) {
|
|
616
|
+
function kb$1(bytes) {
|
|
401
617
|
return Math.round(bytes / 1024);
|
|
402
618
|
}
|
|
403
619
|
/** `req:<msgs>/<kB>,res:<msgs>/<kB>,evt:<msgs>/<kB>` — every kind, always,
|
|
404
620
|
* including at zero: a kind that went quiet is a reading, not a blank. */
|
|
405
621
|
function formatDirection(sample) {
|
|
406
|
-
return `req:${sample.reqMessages}/${kb(sample.reqBytes)}kB,res:${sample.resMessages}/${kb(sample.resBytes)}kB,evt:${sample.evtMessages}/${kb(sample.evtBytes)}kB`;
|
|
622
|
+
return `req:${sample.reqMessages}/${kb$1(sample.reqBytes)}kB,res:${sample.resMessages}/${kb$1(sample.resBytes)}kB,evt:${sample.evtMessages}/${kb$1(sample.evtBytes)}kB`;
|
|
623
|
+
}
|
|
624
|
+
/** `<req>/<res>/<evt>` in kB — the byte triple, in the same order as the
|
|
625
|
+
* message triple next to it, so the two read as one table. */
|
|
626
|
+
function formatPeerBytes(sample) {
|
|
627
|
+
return `${kb$1(sample.reqBytes)}/${kb$1(sample.resBytes)}/${kb$1(sample.evtBytes)}`;
|
|
407
628
|
}
|
|
408
|
-
/**
|
|
409
|
-
*
|
|
629
|
+
/**
|
|
630
|
+
* `<peer>:tx=<req>/<res>/<evt>:rx=…:txkB=<req>/<res>/<evt>:rxkB=…` — the whole
|
|
631
|
+
* peer×kind cross for one peer, messages and bytes, which is the question in
|
|
632
|
+
* one token.
|
|
633
|
+
*
|
|
634
|
+
* The bytes were a per-direction TOTAL until D454, and the total could not
|
|
635
|
+
* answer the question `socket-traffic.ts` says this instrument exists to
|
|
636
|
+
* settle: whether a peer's bytes are RESPONSES (media, and D9/D18 re-opens) or
|
|
637
|
+
* fan-out (`evt`, which scales with runner count and nothing an operator
|
|
638
|
+
* asked for). On 2026-09-11 hub-main wrote 118 MB in one minute to
|
|
639
|
+
* `hub/pipeline-analytics` and the line could only say "118 MB" — separating
|
|
640
|
+
* `res` from `evt` took an hour of arithmetic against the plane-wide totals.
|
|
641
|
+
*/
|
|
410
642
|
function formatPeer(delta) {
|
|
411
|
-
return `${delta.peerId}:tx=${delta.tx.reqMessages}/${delta.tx.resMessages}/${delta.tx.evtMessages}:rx=${delta.rx.reqMessages}/${delta.rx.resMessages}/${delta.rx.evtMessages}:
|
|
643
|
+
return `${delta.peerId}:tx=${delta.tx.reqMessages}/${delta.tx.resMessages}/${delta.tx.evtMessages}:rx=${delta.rx.reqMessages}/${delta.rx.resMessages}/${delta.rx.evtMessages}:txkB=${formatPeerBytes(delta.tx)}:rxkB=${formatPeerBytes(delta.rx)}`;
|
|
644
|
+
}
|
|
645
|
+
/**
|
|
646
|
+
* One named action: `<peer>!<cap>.<method>:<kB>kB/<calls>:r<raw>/d<decoded>`.
|
|
647
|
+
*
|
|
648
|
+
* `!` separates the peer from the action because `.` already separates cap
|
|
649
|
+
* from method and a single delimiter for both would make the token ambiguous
|
|
650
|
+
* to split.
|
|
651
|
+
*
|
|
652
|
+
* The raw/decoded pair is the half that did not exist anywhere before D456.
|
|
653
|
+
* `socketFwd=` answers it for the whole plane — 30 349/5 051 on this hub — and
|
|
654
|
+
* a plane-wide ratio cannot tell a method that is entirely forwarded from one
|
|
655
|
+
* that is entirely decoded, which is the difference between bytes hub-main
|
|
656
|
+
* copies and bytes hub-main reads. The census pays for the two counters it
|
|
657
|
+
* takes; the conclusion they buy is which of the line's megabytes are actually
|
|
658
|
+
* expensive.
|
|
659
|
+
*/
|
|
660
|
+
function formatAction(entry) {
|
|
661
|
+
return `${entry.peerId}!${entry.action}:${kb$1(entry.reqBytes + entry.resBytes)}kB/${entry.calls}:r${entry.raw}/d${entry.decoded}`;
|
|
412
662
|
}
|
|
413
663
|
/**
|
|
414
664
|
* The suffix appended to a `[mem]` line — leading space included, empty when
|
|
@@ -421,14 +671,234 @@ function formatPeer(delta) {
|
|
|
421
671
|
* is the only way to see an unmeasured channel, and the per-kind splits because
|
|
422
672
|
* "the plane went quiet for a minute" is a reading.
|
|
423
673
|
*
|
|
424
|
-
* `socketTop` is
|
|
425
|
-
*
|
|
426
|
-
*
|
|
674
|
+
* `socketTop` is a conditional field — a ranked list of nothing is noise — and
|
|
675
|
+
* it is a single comma-separated value so the field COUNT does not change from
|
|
676
|
+
* line to line.
|
|
677
|
+
*
|
|
678
|
+
* `socketTopBytes` is the other one, and its PRESENCE is the reading: it is
|
|
679
|
+
* printed only when ranking by bytes names a peer that ranking by messages
|
|
680
|
+
* does not. A line without it says the two rankings agree; a line with it says
|
|
681
|
+
* a peer is moving few, heavy frames and names it (D454).
|
|
427
682
|
*/
|
|
428
683
|
function formatSocketPlane(window) {
|
|
429
684
|
if (window === void 0) return "";
|
|
430
685
|
const top = window.top.map(formatPeer).join(",");
|
|
431
|
-
|
|
686
|
+
const named = new Set(window.top.map((p) => p.peerId));
|
|
687
|
+
const topBytes = window.topBytes.filter((p) => !named.has(p.peerId)).map(formatPeer).join(",");
|
|
688
|
+
const queuedTop = window.queued.top.map((p) => `${p.peerId}:${kb$1(p.bytes)}`).join(",");
|
|
689
|
+
return ` socketPeers=${window.peersMeasured}/${window.peersConnected} socketTx=${formatDirection(window.tx)} socketRx=${formatDirection(window.rx)}` + (top.length === 0 ? "" : ` socketTop=${top}`) + (topBytes.length === 0 ? "" : ` socketTopBytes=${topBytes}`) + ` socketQueued=${kb$1(window.queued.totalBytes)}kB` + (window.queued.unmeasured === 0 ? "" : ` socketQueuedUnmeasured=${window.queued.unmeasured}`) + (queuedTop.length === 0 ? "" : ` socketQueuedTop=${queuedTop}`) + (window.forward === void 0 ? "" : ` socketFwd=${window.forward.raw}/${window.forward.decoded}`) + (window.actions === void 0 ? "" : ` socketActions=${kb$1(window.actions.labelledBytes)}/${kb$1(window.actions.totalBytes)}kB` + (window.actions.top.length === 0 ? "" : ` socketTopActions=${window.actions.top.map(formatAction).join(",")}`));
|
|
690
|
+
}
|
|
691
|
+
//#endregion
|
|
692
|
+
//#region src/kernel/moleculer/mesh-queue-report.ts
|
|
693
|
+
function isRecord$1(value) {
|
|
694
|
+
return typeof value === "object" && value !== null;
|
|
695
|
+
}
|
|
696
|
+
/** The shape `TcpWriter` keeps its sockets in. Anything else is not a mesh. */
|
|
697
|
+
function writerSocketsOf(broker) {
|
|
698
|
+
if (!isRecord$1(broker)) return void 0;
|
|
699
|
+
const transit = broker["transit"];
|
|
700
|
+
if (!isRecord$1(transit)) return void 0;
|
|
701
|
+
const transporter = transit["tx"];
|
|
702
|
+
if (!isRecord$1(transporter)) return void 0;
|
|
703
|
+
const writer = transporter["writer"];
|
|
704
|
+
if (!isRecord$1(writer)) return void 0;
|
|
705
|
+
const sockets = writer["sockets"];
|
|
706
|
+
return sockets instanceof Map ? sockets : void 0;
|
|
707
|
+
}
|
|
708
|
+
function readSocket(nodeId, socket) {
|
|
709
|
+
if (typeof nodeId !== "string" || !isRecord$1(socket)) return void 0;
|
|
710
|
+
const queuedBytes = socket["writableLength"];
|
|
711
|
+
const bytesWritten = socket["bytesWritten"];
|
|
712
|
+
if (typeof queuedBytes !== "number" || typeof bytesWritten !== "number") return void 0;
|
|
713
|
+
return {
|
|
714
|
+
nodeId,
|
|
715
|
+
queuedBytes,
|
|
716
|
+
bytesWritten
|
|
717
|
+
};
|
|
718
|
+
}
|
|
719
|
+
/**
|
|
720
|
+
* Build a reader over a lazily-resolved broker. Wrapped: a broker mid-teardown
|
|
721
|
+
* may throw, and a diagnostic that can crash the process it watches is worse
|
|
722
|
+
* than none.
|
|
723
|
+
*/
|
|
724
|
+
function createMeshQueueReader(getBroker) {
|
|
725
|
+
return () => {
|
|
726
|
+
try {
|
|
727
|
+
const sockets = writerSocketsOf(getBroker());
|
|
728
|
+
if (sockets === void 0) return void 0;
|
|
729
|
+
const peers = [];
|
|
730
|
+
for (const [nodeId, socket] of sockets) {
|
|
731
|
+
const peer = readSocket(nodeId, socket);
|
|
732
|
+
if (peer !== void 0) peers.push(peer);
|
|
733
|
+
}
|
|
734
|
+
return peers;
|
|
735
|
+
} catch {
|
|
736
|
+
return;
|
|
737
|
+
}
|
|
738
|
+
};
|
|
739
|
+
}
|
|
740
|
+
/** At most this many nodes named. A cluster has a handful. */
|
|
741
|
+
var MESH_QUEUE_TOP_N = 3;
|
|
742
|
+
function kb(bytes) {
|
|
743
|
+
return Math.round(bytes / 1024);
|
|
744
|
+
}
|
|
745
|
+
/**
|
|
746
|
+
* The suffix appended to a `[mem]` line — leading space included, empty when
|
|
747
|
+
* this process has no mesh writer. `meshQueued` is printed always, at zero
|
|
748
|
+
* included: a quiet mesh is a reading. `meshQueuedTop` names only nodes with
|
|
749
|
+
* a queue, largest first — a ranked list of zeros is noise.
|
|
750
|
+
*/
|
|
751
|
+
function formatMeshQueue(peers) {
|
|
752
|
+
if (peers === void 0) return "";
|
|
753
|
+
const total = peers.reduce((sum, p) => sum + p.queuedBytes, 0);
|
|
754
|
+
const top = peers.filter((p) => p.queuedBytes > 0).toSorted((a, b) => b.queuedBytes - a.queuedBytes || a.nodeId.localeCompare(b.nodeId)).slice(0, 3).map((p) => `${p.nodeId}:${kb(p.queuedBytes)}`).join(",");
|
|
755
|
+
return ` meshQueued=${kb(total)}kB` + (top.length === 0 ? "" : ` meshQueuedTop=${top}`);
|
|
756
|
+
}
|
|
757
|
+
//#endregion
|
|
758
|
+
//#region src/kernel/outbound-queue-watch.ts
|
|
759
|
+
/**
|
|
760
|
+
* outbound-queue-watch — an outbound socket queue that HOLDS is named, and its
|
|
761
|
+
* release is timestamped (D446).
|
|
762
|
+
*
|
|
763
|
+
* ## The blind spot this closes
|
|
764
|
+
*
|
|
765
|
+
* On 2026-09-10 hub-main's `arrayBuffers` climbed twice at a constant rate
|
|
766
|
+
* (20 MB/min to 1 264 MB; 63 MB/min for 77 min to 4 968 MB), LIVE across a
|
|
767
|
+
* forced compaction, with ~4 % of JS heap alongside, and each time released in
|
|
768
|
+
* one instant with no line from any owner. The investigation excluded "every
|
|
769
|
+
* socket write queue" by summing `writableLength` over
|
|
770
|
+
* `process._getActiveHandles()` — 84 MB, all of it the phone's WebSocket.
|
|
771
|
+
*
|
|
772
|
+
* `_getActiveHandles()` lists only handles that keep the loop alive. An
|
|
773
|
+
* UNREF'D socket is not one of them, and Moleculer's `TcpWriter.connect`
|
|
774
|
+
* unrefs every socket it opens to a peer node (`transporters/tcp/tcp-writer.js`,
|
|
775
|
+
* `socket.unref()`). So the one outbound plane that writes to another MACHINE
|
|
776
|
+
* — where a reader that stops draining is the ordinary failure — was never in
|
|
777
|
+
* the sum. The UDS children were in it, but only as a total, at one instant,
|
|
778
|
+
* by hand.
|
|
779
|
+
*
|
|
780
|
+
* ## What this is
|
|
781
|
+
*
|
|
782
|
+
* A pure gate over per-peer queue depths the heartbeat reads on every probe:
|
|
783
|
+
* the UDS children's `socket.writableLength` (`SocketChannel.queuedBytes`) and
|
|
784
|
+
* the mesh writer sockets' (`mesh-queue-report.ts`). It says three things and
|
|
785
|
+
* nothing else:
|
|
786
|
+
*
|
|
787
|
+
* - a peer whose queue has been at or above {@link OUTBOUND_QUEUE_THRESHOLD_BYTES}
|
|
788
|
+
* for {@link OUTBOUND_QUEUE_SUSTAIN_MS} is NAMED, with its current and peak
|
|
789
|
+
* depth and how long it has held — at most once per
|
|
790
|
+
* {@link OUTBOUND_QUEUE_COOLDOWN_MS};
|
|
791
|
+
* - a named peer whose queue then falls under half the threshold, or that
|
|
792
|
+
* vanishes from the readings (its socket died — which is how a queue is
|
|
793
|
+
* dropped in one instant), is reported as RELEASED with its peak and the
|
|
794
|
+
* hold, so the release the episodes never logged has a line and a peer;
|
|
795
|
+
* - a burst that drains inside the sustain window says nothing. Backpressure
|
|
796
|
+
* measures LIVENESS, not identity (D444): a peer momentarily behind is
|
|
797
|
+
* coming back.
|
|
798
|
+
*
|
|
799
|
+
* ## What it is NOT
|
|
800
|
+
*
|
|
801
|
+
* Not a bound. It terminates nothing: a UDS child whose channel is destroyed
|
|
802
|
+
* loses every in-flight call, and a mesh socket Moleculer reconnects on its
|
|
803
|
+
* own terms. The WS guard (`ws-slow-consumer-guard.ts`) bounds the one plane
|
|
804
|
+
* whose client is expendable. Here the line is the deliverable; what to do
|
|
805
|
+
* about a peer that holds is decided with the peer named, not before.
|
|
806
|
+
*/
|
|
807
|
+
/** 64 MiB. Below this a queue is a burst on a busy peer (the orchestrator's
|
|
808
|
+
* channel moves ~1.5 MB/s of audio responses); above it, and held, it is
|
|
809
|
+
* retention with a name. */
|
|
810
|
+
var OUTBOUND_QUEUE_THRESHOLD_BYTES = 64 * 1024 * 1024;
|
|
811
|
+
/** Matches the ArrayBuffer census sustain: a minute above the bound. */
|
|
812
|
+
var OUTBOUND_QUEUE_SUSTAIN_MS = 6e4;
|
|
813
|
+
/** A held queue is re-named every ten minutes, not every probe. */
|
|
814
|
+
var OUTBOUND_QUEUE_COOLDOWN_MS = 10 * 6e4;
|
|
815
|
+
/** A release is a fall under this fraction of the threshold. */
|
|
816
|
+
var RELEASE_RATIO = .5;
|
|
817
|
+
/**
|
|
818
|
+
* The pure decision — threshold, sustain, cooldown, release, gone — testable
|
|
819
|
+
* without a socket.
|
|
820
|
+
*/
|
|
821
|
+
function createOutboundQueueGate(options = {}) {
|
|
822
|
+
const thresholdBytes = options.thresholdBytes ?? 67108864;
|
|
823
|
+
const sustainMs = options.sustainMs ?? 6e4;
|
|
824
|
+
const cooldownMs = options.cooldownMs ?? 6e5;
|
|
825
|
+
const tracked = /* @__PURE__ */ new Map();
|
|
826
|
+
const judgeOne = (peerId, bytes, at) => {
|
|
827
|
+
const state = tracked.get(peerId);
|
|
828
|
+
if (bytes >= thresholdBytes) {
|
|
829
|
+
const next = state === void 0 ? {
|
|
830
|
+
aboveSince: at,
|
|
831
|
+
peakBytes: bytes,
|
|
832
|
+
lastNamedAt: null
|
|
833
|
+
} : {
|
|
834
|
+
...state,
|
|
835
|
+
peakBytes: Math.max(state.peakBytes, bytes)
|
|
836
|
+
};
|
|
837
|
+
const heldForMs = at - next.aboveSince;
|
|
838
|
+
const due = heldForMs >= sustainMs && (next.lastNamedAt === null || at - next.lastNamedAt >= cooldownMs);
|
|
839
|
+
tracked.set(peerId, due ? {
|
|
840
|
+
...next,
|
|
841
|
+
lastNamedAt: at
|
|
842
|
+
} : next);
|
|
843
|
+
return due ? {
|
|
844
|
+
kind: "sustained",
|
|
845
|
+
peerId,
|
|
846
|
+
bytes,
|
|
847
|
+
peakBytes: next.peakBytes,
|
|
848
|
+
heldForMs
|
|
849
|
+
} : void 0;
|
|
850
|
+
}
|
|
851
|
+
if (state === void 0) return void 0;
|
|
852
|
+
if (bytes >= thresholdBytes * RELEASE_RATIO) return void 0;
|
|
853
|
+
tracked.delete(peerId);
|
|
854
|
+
if (state.lastNamedAt === null) return void 0;
|
|
855
|
+
return {
|
|
856
|
+
kind: "released",
|
|
857
|
+
peerId,
|
|
858
|
+
peakBytes: state.peakBytes,
|
|
859
|
+
toBytes: bytes,
|
|
860
|
+
heldForMs: at - state.aboveSince,
|
|
861
|
+
gone: false
|
|
862
|
+
};
|
|
863
|
+
};
|
|
864
|
+
return { observe(readings, at) {
|
|
865
|
+
const verdicts = [];
|
|
866
|
+
const seen = /* @__PURE__ */ new Set();
|
|
867
|
+
for (const reading of readings) {
|
|
868
|
+
seen.add(reading.peerId);
|
|
869
|
+
const verdict = judgeOne(reading.peerId, reading.bytes, at);
|
|
870
|
+
if (verdict !== void 0) verdicts.push(verdict);
|
|
871
|
+
}
|
|
872
|
+
for (const [peerId, state] of tracked) {
|
|
873
|
+
if (seen.has(peerId)) continue;
|
|
874
|
+
tracked.delete(peerId);
|
|
875
|
+
if (state.lastNamedAt === null) continue;
|
|
876
|
+
verdicts.push({
|
|
877
|
+
kind: "released",
|
|
878
|
+
peerId,
|
|
879
|
+
peakBytes: state.peakBytes,
|
|
880
|
+
toBytes: 0,
|
|
881
|
+
heldForMs: at - state.aboveSince,
|
|
882
|
+
gone: true
|
|
883
|
+
});
|
|
884
|
+
}
|
|
885
|
+
return verdicts;
|
|
886
|
+
} };
|
|
887
|
+
}
|
|
888
|
+
var BYTES_PER_MB$1 = 1048576;
|
|
889
|
+
function mb$2(bytes) {
|
|
890
|
+
return Math.round(bytes / BYTES_PER_MB$1);
|
|
891
|
+
}
|
|
892
|
+
/** `60s` under two minutes, `77m` from there — the hold is read at a glance. */
|
|
893
|
+
function duration(ms) {
|
|
894
|
+
const seconds = Math.round(ms / 1e3);
|
|
895
|
+
return seconds < 120 ? `${seconds}s` : `${Math.round(seconds / 60)}m`;
|
|
896
|
+
}
|
|
897
|
+
/** The line for one verdict. Both carry the peer, the bytes and the hold;
|
|
898
|
+
* the sustained one also says why the older census could not see it. */
|
|
899
|
+
function formatOutboundQueueVerdict(label, verdict) {
|
|
900
|
+
if (verdict.kind === "sustained") return `[mem] ${label} outbound queue to ${verdict.peerId} has held ${mb$2(verdict.bytes)}MB (peak ${mb$2(verdict.peakBytes)}MB) for ${duration(verdict.heldForMs)} — the peer is not draining what this process writes to it. This is socket.writableLength on the channel itself; a sum over _getActiveHandles() does not list an unref'd socket and would miss it.`;
|
|
901
|
+
return `[mem] ${label} outbound queue to ${verdict.peerId} RELEASED — ${mb$2(verdict.peakBytes)}MB at peak, ${mb$2(verdict.toBytes)}MB now, held ${duration(verdict.heldForMs)}` + (verdict.gone ? " (peer gone: the socket closed and dropped its queue)" : " (drained)");
|
|
432
902
|
}
|
|
433
903
|
//#endregion
|
|
434
904
|
//#region src/kernel/heap-spaces.ts
|
|
@@ -852,6 +1322,125 @@ function formatArrayBufferCensus(label, report, context) {
|
|
|
852
1322
|
return `[mem] ${label} arrayBuffer census: live=${live}MB in ${report.count} ArrayBuffers (raw arrayBuffers=${context.rawMb}MB at the trigger → ${verdict}) top sizes: ${report.topSizes.map(formatExact).join(", ") || "(none)"}; buckets: ${report.buckets.map(formatBucket).join(", ") || "(none)"}; detached=${report.detached} took=${context.tookMs}ms — cost: ONE forced full GC (the same pause as a reclaim pass), walked in memory, nothing serialised, nothing written; trigger: ${context.reason}`;
|
|
853
1323
|
}
|
|
854
1324
|
//#endregion
|
|
1325
|
+
//#region src/kernel/gc-census.ts
|
|
1326
|
+
function countsOf(entries, kinds) {
|
|
1327
|
+
let majors = 0;
|
|
1328
|
+
let minors = 0;
|
|
1329
|
+
let incrementals = 0;
|
|
1330
|
+
let pauseMs = 0;
|
|
1331
|
+
for (const e of entries) {
|
|
1332
|
+
if (e.kind === kinds.major) majors++;
|
|
1333
|
+
else if (e.kind === kinds.minor) minors++;
|
|
1334
|
+
else if (e.kind === kinds.incremental) incrementals++;
|
|
1335
|
+
pauseMs += e.durationMs;
|
|
1336
|
+
}
|
|
1337
|
+
return {
|
|
1338
|
+
majors,
|
|
1339
|
+
minors,
|
|
1340
|
+
incrementals,
|
|
1341
|
+
pauseMs,
|
|
1342
|
+
windowMs: 0
|
|
1343
|
+
};
|
|
1344
|
+
}
|
|
1345
|
+
/**
|
|
1346
|
+
* Build a census over an injected observer.
|
|
1347
|
+
*
|
|
1348
|
+
* Returns `undefined` when the observer cannot be installed — the caller then
|
|
1349
|
+
* prints nothing, which is the honest reading. It must never degrade into a
|
|
1350
|
+
* census that reports zero collections: a process whose GC is not being
|
|
1351
|
+
* observed and a process that is not collecting look identical on the line, and
|
|
1352
|
+
* they are the two answers furthest apart.
|
|
1353
|
+
*/
|
|
1354
|
+
function createGcCensus(install, kinds, startedAt) {
|
|
1355
|
+
let pending = [];
|
|
1356
|
+
const stopObserving = install((entry) => {
|
|
1357
|
+
pending.push(entry);
|
|
1358
|
+
});
|
|
1359
|
+
if (stopObserving === void 0) return void 0;
|
|
1360
|
+
let windowStart = startedAt;
|
|
1361
|
+
return {
|
|
1362
|
+
read: (at) => {
|
|
1363
|
+
const windowMs = at - windowStart;
|
|
1364
|
+
if (windowMs < 1e3) return null;
|
|
1365
|
+
const taken = pending;
|
|
1366
|
+
pending = [];
|
|
1367
|
+
windowStart = at;
|
|
1368
|
+
return {
|
|
1369
|
+
...countsOf(taken, kinds),
|
|
1370
|
+
windowMs
|
|
1371
|
+
};
|
|
1372
|
+
},
|
|
1373
|
+
stop: () => {
|
|
1374
|
+
stopObserving();
|
|
1375
|
+
pending = [];
|
|
1376
|
+
}
|
|
1377
|
+
};
|
|
1378
|
+
}
|
|
1379
|
+
/**
|
|
1380
|
+
* The observer this process actually gets.
|
|
1381
|
+
*
|
|
1382
|
+
* Everything that can fail is inside the try: a runtime without
|
|
1383
|
+
* `perf_hooks`, without the `gc` entry type, or without the kind constants
|
|
1384
|
+
* yields `undefined` from `install`, and {@link createGcCensus} then declines
|
|
1385
|
+
* to exist rather than reporting zeros.
|
|
1386
|
+
*/
|
|
1387
|
+
function nodeGcObserver() {
|
|
1388
|
+
return {
|
|
1389
|
+
kinds: readKinds({
|
|
1390
|
+
major: 4,
|
|
1391
|
+
minor: 1,
|
|
1392
|
+
incremental: 8
|
|
1393
|
+
}),
|
|
1394
|
+
install: (observe) => {
|
|
1395
|
+
try {
|
|
1396
|
+
const observer = new (process.getBuiltinModule("node:perf_hooks")).PerformanceObserver((list) => {
|
|
1397
|
+
for (const entry of list.getEntries()) {
|
|
1398
|
+
const detail = "detail" in entry ? entry.detail : void 0;
|
|
1399
|
+
observe({
|
|
1400
|
+
kind: typeof detail === "object" && detail !== null && "kind" in detail && typeof detail.kind === "number" ? detail.kind : -1,
|
|
1401
|
+
durationMs: entry.duration
|
|
1402
|
+
});
|
|
1403
|
+
}
|
|
1404
|
+
});
|
|
1405
|
+
observer.observe({ entryTypes: ["gc"] });
|
|
1406
|
+
return () => {
|
|
1407
|
+
observer.disconnect();
|
|
1408
|
+
};
|
|
1409
|
+
} catch {
|
|
1410
|
+
return;
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1413
|
+
};
|
|
1414
|
+
}
|
|
1415
|
+
function readKinds(fallback) {
|
|
1416
|
+
try {
|
|
1417
|
+
const c = process.getBuiltinModule("node:perf_hooks").constants;
|
|
1418
|
+
return {
|
|
1419
|
+
major: c.NODE_PERFORMANCE_GC_MAJOR,
|
|
1420
|
+
minor: c.NODE_PERFORMANCE_GC_MINOR,
|
|
1421
|
+
incremental: c.NODE_PERFORMANCE_GC_INCREMENTAL
|
|
1422
|
+
};
|
|
1423
|
+
} catch {
|
|
1424
|
+
return fallback;
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
/**
|
|
1428
|
+
* The two fields, APPENDED LAST on the `[mem]` line.
|
|
1429
|
+
*
|
|
1430
|
+
* D446's rule: no existing field moves, so every Loki prefix written since
|
|
1431
|
+
* 2026-08-29 still matches. `gc=` prints even when nothing was collected — a
|
|
1432
|
+
* window with no collections is a reading, and it is the reading that says a
|
|
1433
|
+
* process is idle rather than unobserved. `gcPauseMs=` is the MAIN-THREAD
|
|
1434
|
+
* pause only; the concurrent marking that cost hub-main 70 % of its CPU is on
|
|
1435
|
+
* the platform workers and no in-process counter sees it, which is why the
|
|
1436
|
+
* field is named for the thread it describes (D456, part 4).
|
|
1437
|
+
*/
|
|
1438
|
+
function formatGcCensus(window) {
|
|
1439
|
+
if (window === null || window === void 0) return "";
|
|
1440
|
+
const perMin = (n) => window.windowMs <= 0 ? "?" : (n / (window.windowMs / 6e4)).toFixed(1);
|
|
1441
|
+
return ` gc=maj${window.majors}/min${window.minors}/inc${window.incrementals}@${Math.round(window.windowMs / 1e3)}s gcMajPerMin=${perMin(window.majors)} gcPauseMs=${Math.round(window.pauseMs)}`;
|
|
1442
|
+
}
|
|
1443
|
+
//#endregion
|
|
855
1444
|
//#region src/kernel/heap-watch.ts
|
|
856
1445
|
/**
|
|
857
1446
|
* heap-watch — hub-main's own memory, on the record, and returned to the OS.
|
|
@@ -1416,6 +2005,27 @@ function format(label, s, loop, budgetMb, suffix) {
|
|
|
1416
2005
|
return `${line} loopP50=${loop.p50Ms}ms loopP99=${loop.p99Ms}ms loopMax=${loop.maxMs}ms${suffix}`;
|
|
1417
2006
|
}
|
|
1418
2007
|
/**
|
|
2008
|
+
* Every outbound queue the heartbeat can reach, as the gate wants them: the
|
|
2009
|
+
* UDS children (only those whose channel keeps a reading — an unmeasured
|
|
2010
|
+
* peer is not a zero) and the mesh writer sockets, each prefixed with its
|
|
2011
|
+
* plane.
|
|
2012
|
+
*/
|
|
2013
|
+
function outboundQueueReadings(uds, mesh) {
|
|
2014
|
+
const readings = [];
|
|
2015
|
+
for (const peer of uds?.peers ?? []) {
|
|
2016
|
+
if (peer.queuedBytes === null) continue;
|
|
2017
|
+
readings.push({
|
|
2018
|
+
peerId: `uds:${peer.peerId}`,
|
|
2019
|
+
bytes: peer.queuedBytes
|
|
2020
|
+
});
|
|
2021
|
+
}
|
|
2022
|
+
for (const node of mesh ?? []) readings.push({
|
|
2023
|
+
peerId: `mesh:${node.nodeId}`,
|
|
2024
|
+
bytes: node.queuedBytes
|
|
2025
|
+
});
|
|
2026
|
+
return readings;
|
|
2027
|
+
}
|
|
2028
|
+
/**
|
|
1419
2029
|
* Start the heartbeat. Returns a stop function.
|
|
1420
2030
|
*
|
|
1421
2031
|
* `unref`ed so it can never hold the process open, and every read is wrapped:
|
|
@@ -1427,6 +2037,8 @@ function startHeapWatch(label = HUB_MAIN_HEAP_WATCH_LABEL, sink = consoleSink, i
|
|
|
1427
2037
|
const socketPlane = probes?.socketPlane === void 0 ? void 0 : createSocketPlaneMeter(probes.socketPlane);
|
|
1428
2038
|
const readMemory = reclaimOptions?.readMemory ?? (() => process.memoryUsage());
|
|
1429
2039
|
const now = reclaimOptions?.now ?? (() => Date.now());
|
|
2040
|
+
const gcObserver = probes?.gcObserver ?? nodeGcObserver();
|
|
2041
|
+
const gcCensus = createGcCensus(gcObserver.install, gcObserver.kinds, now());
|
|
1430
2042
|
const triggerMb = reclaimOptions?.triggerMb ?? 1024;
|
|
1431
2043
|
const minIntervalMs = reclaimOptions?.minIntervalMs ?? 12e4;
|
|
1432
2044
|
const steadyStateIntervalMs = reclaimOptions?.steadyStateIntervalMs ?? 6e5;
|
|
@@ -1529,7 +2141,7 @@ function startHeapWatch(label = HUB_MAIN_HEAP_WATCH_LABEL, sink = consoleSink, i
|
|
|
1529
2141
|
const due = at - lastLoggedAt >= intervalMs;
|
|
1530
2142
|
if (mode === "escalated" || due) {
|
|
1531
2143
|
lastLoggedAt = at;
|
|
1532
|
-
const line = format(label, sample, loopDelay?.read(), budgetMb, `${formatHeapSpaces(readSpaces())}${formatEventPlane(eventPlane?.read())}` + formatSocketPlane(socketPlane?.read()));
|
|
2144
|
+
const line = format(label, sample, loopDelay?.read(), budgetMb, `${formatHeapSpaces(readSpaces())}${formatEventPlane(eventPlane?.read())}` + formatSocketPlane(socketPlane?.read()) + formatMeshQueue(meshQueue?.()) + formatGcCensus(gcCensus?.read(at)));
|
|
1533
2145
|
if (sample.nearLimit) sink.warn(`${line} — APPROACHING HEAP LIMIT`);
|
|
1534
2146
|
else if (mode === "escalated") sink.warn(`${line} — heap elevated, sampling every ${probeIntervalMs}ms`);
|
|
1535
2147
|
else sink.info(line);
|
|
@@ -1538,8 +2150,17 @@ function startHeapWatch(label = HUB_MAIN_HEAP_WATCH_LABEL, sink = consoleSink, i
|
|
|
1538
2150
|
checkRssBudget(sample, at);
|
|
1539
2151
|
maybeReclaim(sample);
|
|
1540
2152
|
judgeArrayBuffers(sample, at);
|
|
2153
|
+
judgeOutboundQueues(at);
|
|
1541
2154
|
} catch {}
|
|
1542
2155
|
};
|
|
2156
|
+
const meshQueue = probes?.meshQueue;
|
|
2157
|
+
const queueReader = probes?.socketPlane;
|
|
2158
|
+
const queueGate = queueReader === void 0 && meshQueue === void 0 ? void 0 : createOutboundQueueGate(probes?.outboundQueue);
|
|
2159
|
+
const judgeOutboundQueues = (at) => {
|
|
2160
|
+
if (queueGate === void 0) return;
|
|
2161
|
+
const readings = outboundQueueReadings(queueReader?.(), meshQueue?.());
|
|
2162
|
+
for (const verdict of queueGate.observe(readings, at)) sink.warn(formatOutboundQueueVerdict(label, verdict));
|
|
2163
|
+
};
|
|
1543
2164
|
const censusProbe = probes?.arrayBufferCensus;
|
|
1544
2165
|
const censusGate = censusProbe === void 0 ? void 0 : createArrayBufferCensusGate(censusProbe);
|
|
1545
2166
|
let censusInFlight = false;
|
|
@@ -1577,6 +2198,7 @@ function startHeapWatch(label = HUB_MAIN_HEAP_WATCH_LABEL, sink = consoleSink, i
|
|
|
1577
2198
|
stopped = true;
|
|
1578
2199
|
clearInterval(timer);
|
|
1579
2200
|
loopDelay?.stop();
|
|
2201
|
+
gcCensus?.stop();
|
|
1580
2202
|
};
|
|
1581
2203
|
}
|
|
1582
2204
|
/**
|
|
@@ -5336,6 +5958,74 @@ function encodeFrame(value) {
|
|
|
5336
5958
|
Buffer.from(body.buffer, body.byteOffset, body.byteLength).copy(out, 4);
|
|
5337
5959
|
return out;
|
|
5338
5960
|
}
|
|
5961
|
+
/** msgpack `fixstr` of {@link PAYLOAD_KEY}: `0xa0 | length`, then the bytes. */
|
|
5962
|
+
var PAYLOAD_KEY_BYTES = Buffer.from([162, ...Buffer.from("pl")]);
|
|
5963
|
+
/** msgpack `bin 32` marker: one byte, then a 4-byte big-endian length. */
|
|
5964
|
+
var BIN32_MARKER = 198;
|
|
5965
|
+
/** msgpack `fixmap` range: `0x80 | count`, count in `0..15`. */
|
|
5966
|
+
var FIXMAP_BASE = 128;
|
|
5967
|
+
var FIXMAP_MAX_COUNT = 15;
|
|
5968
|
+
/**
|
|
5969
|
+
* Encode the HEAD of a frame that carries an opaque payload: the length
|
|
5970
|
+
* prefix, the msgpack envelope with one extra key ({@link PAYLOAD_KEY}), and
|
|
5971
|
+
* the `bin 32` header for the payload — everything but the payload bytes
|
|
5972
|
+
* themselves, which the caller writes straight after it.
|
|
5973
|
+
*
|
|
5974
|
+
* ## Why the payload is the frame's TAIL and not a field
|
|
5975
|
+
*
|
|
5976
|
+
* hub-main routes every sibling↔sibling call. Until this existed it decoded
|
|
5977
|
+
* the whole request (args included), built a new message, and re-encoded it
|
|
5978
|
+
* — walking a 13.5 KB PCM chunk or a 2 000-row settings answer twice on the
|
|
5979
|
+
* one thread that serves every tRPC request, and allocating an `Encoder`
|
|
5980
|
+
* buffer that doubles from 2 KiB up to the body's size on every write
|
|
5981
|
+
* (D441: ~27 % of hub-main's busy time in msgpack decode, ~6 % in encode,
|
|
5982
|
+
* and the churn that fed a 29.5 % GC share).
|
|
5983
|
+
*
|
|
5984
|
+
* With the payload appended as the LAST key of the map, the receiver's
|
|
5985
|
+
* ordinary msgpack decode yields it as a zero-copy VIEW of the frame
|
|
5986
|
+
* (`@msgpack/msgpack` decodes `bin` as `subarray`), the router reads only the
|
|
5987
|
+
* envelope, and forwarding is: encode a ~60-byte head, write it, write the
|
|
5988
|
+
* view. The wire stays plain msgpack — a peer that never heard of payloads
|
|
5989
|
+
* decodes `{ …frame, pl: <bytes> }` and can be told, at register, not to be
|
|
5990
|
+
* sent one.
|
|
5991
|
+
*
|
|
5992
|
+
* ## The one hand-written msgpack in this repository
|
|
5993
|
+
*
|
|
5994
|
+
* `@msgpack/msgpack` has no "encode a map, then append a key" API, so the
|
|
5995
|
+
* envelope is encoded normally and its `fixmap` count byte is bumped by one
|
|
5996
|
+
* before the key and the `bin 32` header are appended. That patch is only
|
|
5997
|
+
* valid for a `fixmap` (≤ 15 keys); a `Frame` has at most four, and anything
|
|
5998
|
+
* else is refused rather than silently corrupted.
|
|
5999
|
+
*/
|
|
6000
|
+
function encodeFrameHead(frame, payload) {
|
|
6001
|
+
const envelope = encode(frame, ENCODE_OPTIONS);
|
|
6002
|
+
const first = envelope[0];
|
|
6003
|
+
if (envelope.byteLength === 0 || first === void 0 || first < FIXMAP_BASE || first > 142) throw new Error(`encodeFrameHead: envelope must be a msgpack fixmap with at most ${FIXMAP_MAX_COUNT - 1} keys (got leading byte 0x${(first ?? 0).toString(16)})`);
|
|
6004
|
+
const bodyLength = envelope.byteLength + PAYLOAD_KEY_BYTES.byteLength + 5 + payload.byteLength;
|
|
6005
|
+
const head = Buffer.allocUnsafe(HEADER_BYTES + envelope.byteLength + PAYLOAD_KEY_BYTES.byteLength + 5);
|
|
6006
|
+
head.writeUInt32BE(bodyLength, 0);
|
|
6007
|
+
Buffer.from(envelope.buffer, envelope.byteOffset, envelope.byteLength).copy(head, HEADER_BYTES);
|
|
6008
|
+
head[HEADER_BYTES] = first + 1;
|
|
6009
|
+
let at = HEADER_BYTES + envelope.byteLength;
|
|
6010
|
+
PAYLOAD_KEY_BYTES.copy(head, at);
|
|
6011
|
+
at += PAYLOAD_KEY_BYTES.byteLength;
|
|
6012
|
+
head[at] = BIN32_MARKER;
|
|
6013
|
+
head.writeUInt32BE(payload.byteLength, at + 1);
|
|
6014
|
+
return head;
|
|
6015
|
+
}
|
|
6016
|
+
/**
|
|
6017
|
+
* Serialise a value into the bytes a payload carries. The SAME options as the
|
|
6018
|
+
* envelope codec, so `undefined` properties are omitted rather than turned
|
|
6019
|
+
* into `null` — a `z.void()` cap output must read back as `undefined` on the
|
|
6020
|
+
* far side exactly as it did when it travelled inline.
|
|
6021
|
+
*/
|
|
6022
|
+
function packPayload(value) {
|
|
6023
|
+
return encode(value, ENCODE_OPTIONS);
|
|
6024
|
+
}
|
|
6025
|
+
/** Inverse of {@link packPayload}. A `bin` inside decodes as a view of `bytes`. */
|
|
6026
|
+
function unpackPayload(bytes) {
|
|
6027
|
+
return decode(bytes);
|
|
6028
|
+
}
|
|
5339
6029
|
/** Bytes in the big-endian length prefix that opens every frame. */
|
|
5340
6030
|
var HEADER_BYTES = 4;
|
|
5341
6031
|
/**
|
|
@@ -5494,6 +6184,25 @@ function localEndpointPath(nodeId) {
|
|
|
5494
6184
|
if (process.platform === "win32") return `\\\\.\\pipe\\camstack-${safe}`;
|
|
5495
6185
|
return (0, node_path.join)((0, node_os.tmpdir)(), `camstack-${safe}.sock`);
|
|
5496
6186
|
}
|
|
6187
|
+
//#endregion
|
|
6188
|
+
//#region src/kernel/transport/local-transport.ts
|
|
6189
|
+
/**
|
|
6190
|
+
* An answer that carries an opaque payload beside its body.
|
|
6191
|
+
*
|
|
6192
|
+
* A CLASS rather than a shape so the channel can tell it from a provider's
|
|
6193
|
+
* ordinary `{ body, payload }` return value by identity, never by duck-typing:
|
|
6194
|
+
* a cap method is free to return an object with those two keys and it must
|
|
6195
|
+
* travel inline like any other. `request()` resolves with one of these only
|
|
6196
|
+
* when the peer's `res` frame carried a payload.
|
|
6197
|
+
*/
|
|
6198
|
+
var PayloadReply = class {
|
|
6199
|
+
body;
|
|
6200
|
+
payload;
|
|
6201
|
+
constructor(body, payload) {
|
|
6202
|
+
this.body = body;
|
|
6203
|
+
this.payload = payload;
|
|
6204
|
+
}
|
|
6205
|
+
};
|
|
5497
6206
|
var SocketChannel = class {
|
|
5498
6207
|
socket;
|
|
5499
6208
|
nextId = 1;
|
|
@@ -5511,6 +6220,8 @@ var SocketChannel = class {
|
|
|
5511
6220
|
txCounters = createSocketDirectionCounters();
|
|
5512
6221
|
/** Frames read from the peer, cumulative, split by kind. */
|
|
5513
6222
|
rxCounters = createSocketDirectionCounters();
|
|
6223
|
+
/** See {@link CapActionChannelObserver}. Absent = no per-action census. */
|
|
6224
|
+
actionObserver = null;
|
|
5514
6225
|
constructor(socket) {
|
|
5515
6226
|
this.socket = socket;
|
|
5516
6227
|
socket.on("data", (chunk) => this.onData(chunk));
|
|
@@ -5541,15 +6252,25 @@ var SocketChannel = class {
|
|
|
5541
6252
|
}, timeoutMs);
|
|
5542
6253
|
timer.unref?.();
|
|
5543
6254
|
}
|
|
6255
|
+
const label = this.labelSafely(body);
|
|
5544
6256
|
this.pending.set(id, {
|
|
5545
6257
|
resolve,
|
|
5546
6258
|
reject,
|
|
5547
|
-
timer
|
|
6259
|
+
timer,
|
|
6260
|
+
action: null
|
|
5548
6261
|
});
|
|
5549
|
-
this.send({
|
|
6262
|
+
const reqBytes = this.send({
|
|
5550
6263
|
k: "req",
|
|
5551
6264
|
id,
|
|
5552
6265
|
body
|
|
6266
|
+
}, opts?.payload);
|
|
6267
|
+
const entry = this.pending.get(id);
|
|
6268
|
+
if (entry !== void 0 && label !== void 0) this.pending.set(id, {
|
|
6269
|
+
...entry,
|
|
6270
|
+
action: {
|
|
6271
|
+
label,
|
|
6272
|
+
reqBytes
|
|
6273
|
+
}
|
|
5553
6274
|
});
|
|
5554
6275
|
});
|
|
5555
6276
|
}
|
|
@@ -5560,6 +6281,36 @@ var SocketChannel = class {
|
|
|
5560
6281
|
body
|
|
5561
6282
|
});
|
|
5562
6283
|
}
|
|
6284
|
+
/**
|
|
6285
|
+
* Wire the per-action census. Only one observer is active at a time; an
|
|
6286
|
+
* unwired channel records nothing at all (never zeros).
|
|
6287
|
+
*/
|
|
6288
|
+
observeActions(observer) {
|
|
6289
|
+
this.actionObserver = observer;
|
|
6290
|
+
}
|
|
6291
|
+
/**
|
|
6292
|
+
* The label for one envelope, or `undefined` when there is nothing to
|
|
6293
|
+
* record. Wrapped: a broken labeller must never fail the call it watches —
|
|
6294
|
+
* the same discipline `LocalChildRegistry.recordCapUsage` applies, and for
|
|
6295
|
+
* the same reason.
|
|
6296
|
+
*/
|
|
6297
|
+
labelSafely(body) {
|
|
6298
|
+
const obs = this.actionObserver;
|
|
6299
|
+
if (obs === null) return void 0;
|
|
6300
|
+
try {
|
|
6301
|
+
return obs.label(body);
|
|
6302
|
+
} catch {
|
|
6303
|
+
return null;
|
|
6304
|
+
}
|
|
6305
|
+
}
|
|
6306
|
+
/** Settle one request/response pair on the census. Never throws. */
|
|
6307
|
+
recordAction(label, reqBytes, resBytes) {
|
|
6308
|
+
const obs = this.actionObserver;
|
|
6309
|
+
if (obs === null) return;
|
|
6310
|
+
try {
|
|
6311
|
+
obs.record(label, reqBytes, resBytes);
|
|
6312
|
+
} catch {}
|
|
6313
|
+
}
|
|
5563
6314
|
/** Replace the request handler. Only one handler is active at a time. */
|
|
5564
6315
|
onRequest(handler) {
|
|
5565
6316
|
this.requestHandler = handler;
|
|
@@ -5590,11 +6341,44 @@ var SocketChannel = class {
|
|
|
5590
6341
|
rx: sampleSocketDirection(this.rxCounters)
|
|
5591
6342
|
};
|
|
5592
6343
|
}
|
|
5593
|
-
|
|
5594
|
-
|
|
5595
|
-
|
|
5596
|
-
|
|
5597
|
-
|
|
6344
|
+
/**
|
|
6345
|
+
* Bytes handed to the socket that the kernel has not taken yet — the
|
|
6346
|
+
* `Writable` buffer plus the write in flight. Read LIVE, never cached: this
|
|
6347
|
+
* is the number that says whether the peer is draining what we write, and
|
|
6348
|
+
* it is read on every heartbeat probe (D446). An unref'd socket keeps it
|
|
6349
|
+
* exactly like a ref'd one, which is why it is read here and not summed
|
|
6350
|
+
* over `_getActiveHandles()`.
|
|
6351
|
+
*/
|
|
6352
|
+
queuedBytes() {
|
|
6353
|
+
return this.socket.writableLength;
|
|
6354
|
+
}
|
|
6355
|
+
/**
|
|
6356
|
+
* Write one frame. With a payload the head and the tail are two `write`
|
|
6357
|
+
* calls under `cork()` — the kernel sees one `writev`, and the payload bytes
|
|
6358
|
+
* are never copied into the head. A forwarded payload is a view into the
|
|
6359
|
+
* frame it arrived on, and that frame's buffer is never mutated, so handing
|
|
6360
|
+
* the view to the socket is safe.
|
|
6361
|
+
*
|
|
6362
|
+
* Returns the on-the-wire bytes it charged — the same number the per-kind
|
|
6363
|
+
* counter got, so the per-action census can never disagree with the per-peer
|
|
6364
|
+
* one. `0` when the channel is closed and nothing was written.
|
|
6365
|
+
*/
|
|
6366
|
+
send(frame, payload) {
|
|
6367
|
+
if (this.closed) return 0;
|
|
6368
|
+
if (payload === void 0) {
|
|
6369
|
+
const encoded = encodeFrame(frame);
|
|
6370
|
+
recordSocketFrame(this.txCounters, frame.k, encoded.byteLength);
|
|
6371
|
+
this.socket.write(encoded);
|
|
6372
|
+
return encoded.byteLength;
|
|
6373
|
+
}
|
|
6374
|
+
const head = encodeFrameHead(frame, payload);
|
|
6375
|
+
const bytes = head.byteLength + payload.byteLength;
|
|
6376
|
+
recordSocketFrame(this.txCounters, frame.k, bytes);
|
|
6377
|
+
this.socket.cork();
|
|
6378
|
+
this.socket.write(head);
|
|
6379
|
+
this.socket.write(payload);
|
|
6380
|
+
this.socket.uncork();
|
|
6381
|
+
return bytes;
|
|
5598
6382
|
}
|
|
5599
6383
|
onData(chunk) {
|
|
5600
6384
|
const frames = this.decoder.push(chunk);
|
|
@@ -5613,22 +6397,31 @@ var SocketChannel = class {
|
|
|
5613
6397
|
async handleFrame(frame, wireBytes) {
|
|
5614
6398
|
recordSocketFrame(this.rxCounters, frame.k, wireBytes);
|
|
5615
6399
|
if (frame.k === "req") {
|
|
6400
|
+
const label = this.labelSafely(frame.body);
|
|
6401
|
+
let resBytes = 0;
|
|
5616
6402
|
try {
|
|
5617
|
-
const result = await this.requestHandler(frame.body);
|
|
5618
|
-
this.send({
|
|
6403
|
+
const result = await this.requestHandler(frame.body, frame.pl);
|
|
6404
|
+
if (result instanceof PayloadReply) resBytes = this.send({
|
|
6405
|
+
k: "res",
|
|
6406
|
+
id: frame.id,
|
|
6407
|
+
ok: true,
|
|
6408
|
+
body: result.body
|
|
6409
|
+
}, result.payload);
|
|
6410
|
+
else resBytes = this.send({
|
|
5619
6411
|
k: "res",
|
|
5620
6412
|
id: frame.id,
|
|
5621
6413
|
ok: true,
|
|
5622
6414
|
body: result
|
|
5623
6415
|
});
|
|
5624
6416
|
} catch (err) {
|
|
5625
|
-
this.send({
|
|
6417
|
+
resBytes = this.send({
|
|
5626
6418
|
k: "res",
|
|
5627
6419
|
id: frame.id,
|
|
5628
6420
|
ok: false,
|
|
5629
6421
|
err: err instanceof Error ? err.message : String(err)
|
|
5630
6422
|
});
|
|
5631
6423
|
}
|
|
6424
|
+
if (label !== void 0) this.recordAction(label, wireBytes, resBytes);
|
|
5632
6425
|
return;
|
|
5633
6426
|
}
|
|
5634
6427
|
if (frame.k === "res") {
|
|
@@ -5636,8 +6429,10 @@ var SocketChannel = class {
|
|
|
5636
6429
|
if (!p) return;
|
|
5637
6430
|
this.pending.delete(frame.id);
|
|
5638
6431
|
if (p.timer !== null) clearTimeout(p.timer);
|
|
5639
|
-
if (
|
|
5640
|
-
|
|
6432
|
+
if (p.action !== null) this.recordAction(p.action.label, p.action.reqBytes, wireBytes);
|
|
6433
|
+
if (!frame.ok) p.reject(new Error(frame.err));
|
|
6434
|
+
else if (frame.pl !== void 0) p.resolve(new PayloadReply(frame.body, frame.pl));
|
|
6435
|
+
else p.resolve(frame.body);
|
|
5641
6436
|
return;
|
|
5642
6437
|
}
|
|
5643
6438
|
this.eventHandler(frame.body);
|
|
@@ -6188,454 +6983,79 @@ function deliverShared(state, event) {
|
|
|
6188
6983
|
}
|
|
6189
6984
|
}
|
|
6190
6985
|
//#endregion
|
|
6191
|
-
//#region src/kernel/
|
|
6192
|
-
/**
|
|
6193
|
-
var
|
|
6194
|
-
|
|
6195
|
-
var
|
|
6196
|
-
|
|
6197
|
-
|
|
6198
|
-
|
|
6199
|
-
|
|
6200
|
-
|
|
6986
|
+
//#region src/kernel/transport/cap-child-index.ts
|
|
6987
|
+
/** Separator that cannot occur in a kebab cap name, so the key is unambiguous. */
|
|
6988
|
+
var DEVICE_KEY_SEPARATOR = "\0";
|
|
6989
|
+
var NO_CANDIDATES = Object.freeze([]);
|
|
6990
|
+
var MapCapChildIndex = class {
|
|
6991
|
+
deviceOwners;
|
|
6992
|
+
singletons;
|
|
6993
|
+
constructor(deviceOwners, singletons) {
|
|
6994
|
+
this.deviceOwners = deviceOwners;
|
|
6995
|
+
this.singletons = singletons;
|
|
6996
|
+
}
|
|
6997
|
+
deviceOwner(capName, deviceId) {
|
|
6998
|
+
return this.deviceOwners.get(`${capName}${DEVICE_KEY_SEPARATOR}${deviceId}`) ?? null;
|
|
6999
|
+
}
|
|
7000
|
+
singletonCandidates(capName) {
|
|
7001
|
+
return this.singletons.get(capName) ?? NO_CANDIDATES;
|
|
7002
|
+
}
|
|
7003
|
+
};
|
|
6201
7004
|
/**
|
|
6202
|
-
*
|
|
6203
|
-
*
|
|
6204
|
-
|
|
6205
|
-
|
|
6206
|
-
|
|
6207
|
-
|
|
6208
|
-
|
|
6209
|
-
|
|
6210
|
-
|
|
6211
|
-
|
|
7005
|
+
* Build the `(cap, device?) → child` index from the connected children.
|
|
7006
|
+
*
|
|
7007
|
+
* A PURE function of what it is handed — it captures nothing, subscribes to
|
|
7008
|
+
* nothing and cannot be updated in place. That is the whole safety argument
|
|
7009
|
+
* (D3, D457): the only way to change what it says is to build a new one from
|
|
7010
|
+
* the `children` map, so an index can never name a child that map has already
|
|
7011
|
+
* dropped. An incremental index — add on register, remove on close — would be
|
|
7012
|
+
* a second registry that can disagree with the first, and a stale one routes
|
|
7013
|
+
* to a dead runner, which is strictly worse than a slow scan.
|
|
7014
|
+
*
|
|
7015
|
+
* Cost: O(children × descriptors), paid once per mutation of the children map
|
|
7016
|
+
* (a register, a re-register, a disconnect) rather than once per cap call.
|
|
7017
|
+
*
|
|
7018
|
+
* The two tiers stay separate, exactly as the scan had them: a device lookup
|
|
7019
|
+
* is never answered from a deviceId-less descriptor. `resolveChildId` owns
|
|
7020
|
+
* the fallback from tier 1 to tier 2, and folding it in here would make it
|
|
7021
|
+
* unreachable.
|
|
7022
|
+
*/
|
|
7023
|
+
function buildCapChildIndex(entries) {
|
|
7024
|
+
const deviceOwners = /* @__PURE__ */ new Map();
|
|
7025
|
+
const singletons = /* @__PURE__ */ new Map();
|
|
7026
|
+
for (const entry of entries) {
|
|
7027
|
+
const namedForThisChild = /* @__PURE__ */ new Set();
|
|
7028
|
+
for (const cap of entry.caps) {
|
|
7029
|
+
if (cap.deviceId === void 0) {
|
|
7030
|
+
if (namedForThisChild.has(cap.capName)) continue;
|
|
7031
|
+
namedForThisChild.add(cap.capName);
|
|
7032
|
+
const candidates = singletons.get(cap.capName);
|
|
7033
|
+
if (candidates === void 0) singletons.set(cap.capName, [entry.childId]);
|
|
7034
|
+
else candidates.push(entry.childId);
|
|
7035
|
+
continue;
|
|
7036
|
+
}
|
|
7037
|
+
const key = `${cap.capName}${DEVICE_KEY_SEPARATOR}${cap.deviceId}`;
|
|
7038
|
+
if (!deviceOwners.has(key)) deviceOwners.set(key, entry.childId);
|
|
7039
|
+
}
|
|
6212
7040
|
}
|
|
7041
|
+
return new MapCapChildIndex(deviceOwners, singletons);
|
|
6213
7042
|
}
|
|
6214
7043
|
//#endregion
|
|
6215
|
-
//#region src/kernel/transport/cap-
|
|
6216
|
-
|
|
6217
|
-
|
|
6218
|
-
|
|
6219
|
-
const
|
|
6220
|
-
const
|
|
6221
|
-
return
|
|
6222
|
-
}
|
|
6223
|
-
|
|
6224
|
-
|
|
6225
|
-
|
|
6226
|
-
|
|
6227
|
-
|
|
6228
|
-
|
|
6229
|
-
|
|
6230
|
-
* Dispatchers wrapping transport errors MUST pass the original.
|
|
6231
|
-
*/
|
|
6232
|
-
constructor(capName, method, detail, cause) {
|
|
6233
|
-
super(buildMessage(capName, method, detail), cause !== void 0 ? { cause } : void 0);
|
|
6234
|
-
this.name = "CapRouteError";
|
|
6235
|
-
this.reason = detail.reason;
|
|
6236
|
-
this.nodeId = detail.nodeId;
|
|
6237
|
-
this.rejected = detail.rejected;
|
|
6238
|
-
}
|
|
6239
|
-
};
|
|
6240
|
-
/**
|
|
6241
|
-
* Classifies a (capName, opts) pair into a typed CapRoute dispatch descriptor.
|
|
6242
|
-
*
|
|
6243
|
-
* Precedence (explicit nodeId path):
|
|
6244
|
-
* 1. hub-in-process — hub node + hubInProcessProvides
|
|
6245
|
-
* 2. hub-local-uds — hub node + hubLocalChildProvides
|
|
6246
|
-
* 3. node-offline → CapRouteError{reason:'node-offline'}
|
|
6247
|
-
* 4. agent-child-forward — node is an agent AND nodeKnowsCap
|
|
6248
|
-
* 5. remote-moleculer — any other online non-agent node
|
|
6249
|
-
*
|
|
6250
|
-
* Singleton path (no nodeId):
|
|
6251
|
-
* 1. hub-in-process (hub provides in-process)
|
|
6252
|
-
* 2. hub-local-uds (a hub-local UDS child provides it)
|
|
6253
|
-
* 3. remote-moleculer (any online, non-hub node that knows it)
|
|
6254
|
-
* 4. → CapRouteError{reason:'no-provider', rejected: all considered routes}
|
|
6255
|
-
*
|
|
6256
|
-
* PURE: no side effects, no async, no broker/registry imports.
|
|
6257
|
-
*/
|
|
6258
|
-
function classifyCapRoute(capName, opts, snapshot) {
|
|
6259
|
-
const rejected = [];
|
|
6260
|
-
if (opts.nodeId !== void 0) return classifyExplicitNode(capName, opts.nodeId, opts.deviceId, snapshot, rejected);
|
|
6261
|
-
return classifySingleton(capName, opts.deviceId, snapshot, rejected);
|
|
6262
|
-
}
|
|
6263
|
-
function classifyExplicitNode(capName, nodeId, deviceId, snap, rejected) {
|
|
6264
|
-
if (nodeId === snap.hubNodeId) {
|
|
6265
|
-
const residentProvides = snap.hubResidentProvides;
|
|
6266
|
-
const providesOnHub = residentProvides ?? snap.hubInProcessProvides;
|
|
6267
|
-
const getRef = residentProvides !== void 0 ? snap.getHubResidentProviderRef : snap.getInProcessProviderRef;
|
|
6268
|
-
if (providesOnHub(capName)) {
|
|
6269
|
-
const ref = getRef?.(capName) ?? null;
|
|
6270
|
-
if (ref !== null) return {
|
|
6271
|
-
kind: "hub-in-process",
|
|
6272
|
-
capName,
|
|
6273
|
-
ref
|
|
6274
|
-
};
|
|
6275
|
-
rejected.push({
|
|
6276
|
-
kind: "hub-in-process",
|
|
6277
|
-
why: "provider ref not available in snapshot"
|
|
6278
|
-
});
|
|
6279
|
-
throw new CapRouteError(capName, void 0, {
|
|
6280
|
-
reason: "no-provider",
|
|
6281
|
-
nodeId,
|
|
6282
|
-
rejected
|
|
6283
|
-
});
|
|
6284
|
-
}
|
|
6285
|
-
if (snap.hubLocalChildProvides(capName, deviceId)) {
|
|
6286
|
-
const childId = snap.getHubLocalChildId?.(capName, deviceId) ?? null;
|
|
6287
|
-
if (childId !== null) return {
|
|
6288
|
-
kind: "hub-local-uds",
|
|
6289
|
-
capName,
|
|
6290
|
-
childId
|
|
6291
|
-
};
|
|
6292
|
-
rejected.push({
|
|
6293
|
-
kind: "hub-local-uds",
|
|
6294
|
-
why: "child id not resolvable from snapshot"
|
|
6295
|
-
});
|
|
6296
|
-
throw new CapRouteError(capName, void 0, {
|
|
6297
|
-
reason: "no-provider",
|
|
6298
|
-
nodeId,
|
|
6299
|
-
rejected
|
|
6300
|
-
});
|
|
6301
|
-
}
|
|
6302
|
-
rejected.push({
|
|
6303
|
-
kind: "hub-in-process",
|
|
6304
|
-
why: "hub does not provide this cap in-process"
|
|
6305
|
-
});
|
|
6306
|
-
rejected.push({
|
|
6307
|
-
kind: "hub-local-uds",
|
|
6308
|
-
why: "no hub-local child provides this cap"
|
|
6309
|
-
});
|
|
6310
|
-
throw new CapRouteError(capName, void 0, {
|
|
6311
|
-
reason: "no-provider",
|
|
6312
|
-
nodeId,
|
|
6313
|
-
rejected
|
|
6314
|
-
});
|
|
6315
|
-
}
|
|
6316
|
-
if (!snap.nodeOnline(nodeId)) {
|
|
6317
|
-
rejected.push({
|
|
6318
|
-
kind: "remote-moleculer",
|
|
6319
|
-
why: `node ${nodeId} is offline`
|
|
6320
|
-
});
|
|
6321
|
-
throw new CapRouteError(capName, void 0, {
|
|
6322
|
-
reason: "node-offline",
|
|
6323
|
-
nodeId,
|
|
6324
|
-
rejected
|
|
6325
|
-
});
|
|
6326
|
-
}
|
|
6327
|
-
if (snap.nodeIsAgent(nodeId)) {
|
|
6328
|
-
if (snap.nodeKnowsCap(nodeId, capName)) return {
|
|
6329
|
-
kind: "agent-child-forward",
|
|
6330
|
-
capName,
|
|
6331
|
-
agentNodeId: nodeId,
|
|
6332
|
-
childId: snap.getAgentChildId?.(nodeId, capName) ?? void 0
|
|
6333
|
-
};
|
|
6334
|
-
rejected.push({
|
|
6335
|
-
kind: "agent-child-forward",
|
|
6336
|
-
why: `agent ${nodeId} does not know cap ${capName}`
|
|
6337
|
-
});
|
|
6338
|
-
throw new CapRouteError(capName, void 0, {
|
|
6339
|
-
reason: "no-provider",
|
|
6340
|
-
nodeId,
|
|
6341
|
-
rejected
|
|
6342
|
-
});
|
|
6343
|
-
}
|
|
6344
|
-
return {
|
|
6345
|
-
kind: "remote-moleculer",
|
|
6346
|
-
capName,
|
|
6347
|
-
nodeId
|
|
6348
|
-
};
|
|
6349
|
-
}
|
|
6350
|
-
function classifySingleton(capName, deviceId, snap, rejected) {
|
|
6351
|
-
if (snap.hubInProcessProvides(capName)) {
|
|
6352
|
-
const ref = snap.getInProcessProviderRef?.(capName) ?? null;
|
|
6353
|
-
if (ref !== null) return {
|
|
6354
|
-
kind: "hub-in-process",
|
|
6355
|
-
capName,
|
|
6356
|
-
ref
|
|
6357
|
-
};
|
|
6358
|
-
rejected.push({
|
|
6359
|
-
kind: "hub-in-process",
|
|
6360
|
-
why: "provider ref not available in snapshot"
|
|
6361
|
-
});
|
|
6362
|
-
} else rejected.push({
|
|
6363
|
-
kind: "hub-in-process",
|
|
6364
|
-
why: "hub does not provide this cap in-process"
|
|
6365
|
-
});
|
|
6366
|
-
if (snap.hubLocalChildProvides(capName, deviceId)) {
|
|
6367
|
-
const childId = snap.getHubLocalChildId?.(capName, deviceId) ?? null;
|
|
6368
|
-
if (childId !== null) return {
|
|
6369
|
-
kind: "hub-local-uds",
|
|
6370
|
-
capName,
|
|
6371
|
-
childId
|
|
6372
|
-
};
|
|
6373
|
-
rejected.push({
|
|
6374
|
-
kind: "hub-local-uds",
|
|
6375
|
-
why: "child id not resolvable from snapshot"
|
|
6376
|
-
});
|
|
6377
|
-
} else rejected.push({
|
|
6378
|
-
kind: "hub-local-uds",
|
|
6379
|
-
why: "no hub-local child provides this cap"
|
|
6380
|
-
});
|
|
6381
|
-
const knownNodes = snap.listKnownNodeIds?.() ?? [];
|
|
6382
|
-
for (const nodeId of knownNodes) if (nodeId !== snap.hubNodeId && snap.nodeOnline(nodeId) && snap.nodeKnowsCap(nodeId, capName)) return {
|
|
6383
|
-
kind: "remote-moleculer",
|
|
6384
|
-
capName,
|
|
6385
|
-
nodeId
|
|
6386
|
-
};
|
|
6387
|
-
rejected.push({
|
|
6388
|
-
kind: "remote-moleculer",
|
|
6389
|
-
why: "no online remote node knows this cap"
|
|
6390
|
-
});
|
|
6391
|
-
throw new CapRouteError(capName, void 0, {
|
|
6392
|
-
reason: "no-provider",
|
|
6393
|
-
rejected
|
|
6394
|
-
});
|
|
6395
|
-
}
|
|
6396
|
-
//#endregion
|
|
6397
|
-
//#region src/kernel/transport/cap-route-resolver.ts
|
|
6398
|
-
/** The Moleculer service name that Task 6's agent registers. */
|
|
6399
|
-
var AGENT_CAP_FWD_SERVICE = "$agent-cap-fwd";
|
|
6400
|
-
/** The Moleculer action (service.action) for agent cap forwarding. */
|
|
6401
|
-
var AGENT_CAP_FWD_ACTION = `${AGENT_CAP_FWD_SERVICE}.forward`;
|
|
6402
|
-
/** Default timeout for remote Moleculer cap calls (ms). */
|
|
6403
|
-
var REMOTE_CALL_TIMEOUT_MS = 6e4;
|
|
6404
|
-
function extractDeviceId(args) {
|
|
6405
|
-
if (args === null || typeof args !== "object") return void 0;
|
|
6406
|
-
const raw = Reflect.get(args, "deviceId");
|
|
6407
|
-
return typeof raw === "number" ? raw : void 0;
|
|
6408
|
-
}
|
|
6409
|
-
/**
|
|
6410
|
-
* Extract an inline `nodeId` string from a cap call's args — the per-call node
|
|
6411
|
-
* pin a forked addon expresses by carrying `nodeId` in the input (the SAME way
|
|
6412
|
-
* device-scoped caps carry `deviceId`, and the way the generated cap-router on
|
|
6413
|
-
* the hub already honours an inline pin). Mirrors {@link extractDeviceId}.
|
|
6414
|
-
*
|
|
6415
|
-
* This is the routing source for hub→remote-node EXECUTION pinning (e.g. the
|
|
6416
|
-
* benchmark addon running a synthetic/decoder workload ON a chosen agent). The
|
|
6417
|
-
* out-of-band `nodePin(op.context)` path does NOT survive the forked addon →
|
|
6418
|
-
* hub UDS link chain, so `onUnownedCall` reads the inline pin instead. Provider
|
|
6419
|
-
* methods that don't declare `nodeId` simply ignore the extra field (they
|
|
6420
|
-
* destructure the fields they need); methods that DO declare it (e.g.
|
|
6421
|
-
* `getEngineProvisioning({nodeId})`) still receive it unchanged.
|
|
6422
|
-
*/
|
|
6423
|
-
function extractNodeId(args) {
|
|
6424
|
-
if (args === null || typeof args !== "object") return void 0;
|
|
6425
|
-
const raw = Reflect.get(args, "nodeId");
|
|
6426
|
-
return typeof raw === "string" && raw.length > 0 ? raw : void 0;
|
|
6427
|
-
}
|
|
6428
|
-
/**
|
|
6429
|
-
* Merge `callerAddonId` into `args` as an extra field — the SAME "provider
|
|
6430
|
-
* destructures what it needs and ignores the rest" convention
|
|
6431
|
-
* {@link extractNodeId} already relies on for the inline `nodeId` pin.
|
|
6432
|
-
* `undefined` (the overwhelmingly common case: no caller hint, or the census
|
|
6433
|
-
* consuming it is disarmed) returns `args` unchanged — no allocation. A
|
|
6434
|
-
* non-object `args` (or `null`/array) is returned unchanged too: there is
|
|
6435
|
-
* nowhere to attach an extra key without changing the payload's shape, and a
|
|
6436
|
-
* provider taking a bare value never reads `callerAddonId` off it anyway.
|
|
6437
|
-
*/
|
|
6438
|
-
function withCallerAddonId(args, callerAddonId) {
|
|
6439
|
-
if (callerAddonId === void 0) return args;
|
|
6440
|
-
if (args === null || typeof args !== "object" || Array.isArray(args)) return args;
|
|
6441
|
-
return {
|
|
6442
|
-
...args,
|
|
6443
|
-
callerAddonId
|
|
6444
|
-
};
|
|
6445
|
-
}
|
|
6446
|
-
var CapRouteResolver = class {
|
|
6447
|
-
hubNodeId;
|
|
6448
|
-
broker;
|
|
6449
|
-
hubLocalRegistry;
|
|
6450
|
-
nodeAuthority;
|
|
6451
|
-
inProcessProviders;
|
|
6452
|
-
hubResidentProviders;
|
|
6453
|
-
capTimeoutMs;
|
|
6454
|
-
snapshot;
|
|
6455
|
-
constructor(deps) {
|
|
6456
|
-
this.hubNodeId = deps.hubNodeId;
|
|
6457
|
-
this.broker = deps.broker;
|
|
6458
|
-
this.hubLocalRegistry = deps.hubLocalRegistry;
|
|
6459
|
-
this.nodeAuthority = deps.nodeAuthority;
|
|
6460
|
-
this.inProcessProviders = deps.inProcessProviders;
|
|
6461
|
-
this.hubResidentProviders = deps.hubResidentProviders;
|
|
6462
|
-
this.capTimeoutMs = deps.capTimeoutMs;
|
|
6463
|
-
this.snapshot = this.buildSnapshot();
|
|
6464
|
-
}
|
|
6465
|
-
/** Per-call RPC timeout: the cap method's declared override, else the default. */
|
|
6466
|
-
callTimeout(capName, method) {
|
|
6467
|
-
return this.capTimeoutMs?.(capName, method) ?? REMOTE_CALL_TIMEOUT_MS;
|
|
6468
|
-
}
|
|
6469
|
-
resolveCapRoute(capName, opts) {
|
|
6470
|
-
return classifyCapRoute(capName, opts, this.snapshot);
|
|
6471
|
-
}
|
|
6472
|
-
/**
|
|
6473
|
-
* Resolve the hub-local-uds route for a cap owned by a forked hub-local
|
|
6474
|
-
* child, IGNORING any in-hub provider registered for the same cap name.
|
|
6475
|
-
*
|
|
6476
|
-
* `resolveCapRoute` gives Priority 1 to `hub-in-process`: when an in-hub
|
|
6477
|
-
* provider (e.g. a wrapper) is registered for the cap, the route always
|
|
6478
|
-
* classifies as `hub-in-process` and the hub-local-uds NATIVE child is never
|
|
6479
|
-
* reached. That is correct for the generic dispatch path (the wrapper is the
|
|
6480
|
-
* active provider), but WRONG for the native-cap fallback
|
|
6481
|
-
* (`setNativeFallback`), whose contract is to reach the NATIVE provider in
|
|
6482
|
-
* the forked vendor child so a wrapper can delegate to it. A wrapper cap
|
|
6483
|
-
* with a forked native (today: `snapshot`) otherwise resolves to the wrapper
|
|
6484
|
-
* itself, the native is never invoked, and the wrapper silently falls
|
|
6485
|
-
* through to its secondary strategy.
|
|
6486
|
-
*
|
|
6487
|
-
* This method consults ONLY the hub-local-child authority
|
|
6488
|
-
* (`hubLocalChildProvides` + `getHubLocalChildId`, both deviceId-aware), so
|
|
6489
|
-
* it returns the native child route regardless of any in-process shadow.
|
|
6490
|
-
* Returns null when no hub-local child owns `(capName, deviceId)` — the
|
|
6491
|
-
* caller then falls through to its remote-resolution branch.
|
|
6492
|
-
*/
|
|
6493
|
-
resolveHubLocalUdsRoute(capName, deviceId) {
|
|
6494
|
-
if (!this.snapshot.hubLocalChildProvides(capName, deviceId)) return null;
|
|
6495
|
-
const childId = this.snapshot.getHubLocalChildId?.(capName, deviceId) ?? null;
|
|
6496
|
-
if (childId === null) return null;
|
|
6497
|
-
return {
|
|
6498
|
-
kind: "hub-local-uds",
|
|
6499
|
-
capName,
|
|
6500
|
-
childId
|
|
6501
|
-
};
|
|
6502
|
-
}
|
|
6503
|
-
/**
|
|
6504
|
-
* @param addonId Optional out-of-band PROVIDER-selection hint (Task 7.5a):
|
|
6505
|
-
* the addonId of the provider the CALLER already resolved (e.g.
|
|
6506
|
-
* `buildCapCallFn`'s `deps.addonId` for a registered grouped-runner
|
|
6507
|
-
* provider). Only meaningful for `agent-child-forward` — a `remote-moleculer`
|
|
6508
|
-
* route already encodes the provider in its action name
|
|
6509
|
-
* (`${addonId}.${cap}.${method}`, via `NodeCapAuthority.getAddonId`), and a
|
|
6510
|
-
* `hub-local-uds` caller that already knows the addonId (`buildCapCallFn`)
|
|
6511
|
-
* talks to `LocalChildRegistry.callCapOnChild` directly rather than through
|
|
6512
|
-
* `dispatch`.
|
|
6513
|
-
* @param callerAddonId Optional out-of-band CALLER-identity hint — the
|
|
6514
|
-
* addon that ORIGINATED the call, distinct from `addonId` above (which
|
|
6515
|
-
* names the resolved PROVIDER, not the caller). Set by
|
|
6516
|
-
* `onUnownedCall` from `CapCallInput.callerAddonId`
|
|
6517
|
-
* (`LocalChildRegistry` stamped it off the registered `childId`). Only
|
|
6518
|
-
* the `hub-in-process` branch consumes it — merged into `args` so a
|
|
6519
|
-
* diagnostic tool deep in that provider (e.g. device-manager's
|
|
6520
|
-
* fleet-read census) can read it without every provider method changing
|
|
6521
|
-
* signature, the same "extra field the provider destructures or
|
|
6522
|
-
* ignores" convention `nodeId` already uses on this path.
|
|
6523
|
-
*/
|
|
6524
|
-
async dispatch(route, method, args, addonId, callerAddonId) {
|
|
6525
|
-
try {
|
|
6526
|
-
return await this.dispatchInner(route, method, args, addonId, callerAddonId);
|
|
6527
|
-
} catch (err) {
|
|
6528
|
-
if (err instanceof CapRouteError) throw err;
|
|
6529
|
-
const nodeId = this.routeNodeId(route);
|
|
6530
|
-
const cause = err instanceof Error ? err : new Error(String(err));
|
|
6531
|
-
throw new CapRouteError(route.capName, method, {
|
|
6532
|
-
reason: "transport-failed",
|
|
6533
|
-
nodeId,
|
|
6534
|
-
rejected: [{
|
|
6535
|
-
kind: route.kind,
|
|
6536
|
-
why: cause.message
|
|
6537
|
-
}]
|
|
6538
|
-
}, cause);
|
|
6539
|
-
}
|
|
6540
|
-
}
|
|
6541
|
-
/**
|
|
6542
|
-
* Inner dispatch: may throw CapRouteError (validation failures) or arbitrary
|
|
6543
|
-
* transport errors. The outer `dispatch` wraps non-CapRouteErrors.
|
|
6544
|
-
*/
|
|
6545
|
-
async dispatchInner(route, method, args, addonId, callerAddonId) {
|
|
6546
|
-
switch (route.kind) {
|
|
6547
|
-
case "hub-in-process": return route.ref.invoke(method, withCallerAddonId(args, callerAddonId));
|
|
6548
|
-
case "hub-local-uds": {
|
|
6549
|
-
const registry = this.hubLocalRegistry;
|
|
6550
|
-
if (registry === null) throw new CapRouteError(route.capName, method, {
|
|
6551
|
-
reason: "no-provider",
|
|
6552
|
-
rejected: [{
|
|
6553
|
-
kind: "hub-local-uds",
|
|
6554
|
-
why: "UDS registry not available"
|
|
6555
|
-
}]
|
|
6556
|
-
});
|
|
6557
|
-
const deviceId = extractDeviceId(args);
|
|
6558
|
-
const input = {
|
|
6559
|
-
capName: route.capName,
|
|
6560
|
-
method,
|
|
6561
|
-
args,
|
|
6562
|
-
...deviceId !== void 0 ? { deviceId } : {}
|
|
6563
|
-
};
|
|
6564
|
-
return registry.callCapOnChild(route.childId, input);
|
|
6565
|
-
}
|
|
6566
|
-
case "remote-moleculer": {
|
|
6567
|
-
const addonId = this.nodeAuthority.getAddonId(route.nodeId, route.capName);
|
|
6568
|
-
if (addonId === null) throw new CapRouteError(route.capName, method, {
|
|
6569
|
-
reason: "no-provider",
|
|
6570
|
-
nodeId: route.nodeId,
|
|
6571
|
-
rejected: [{
|
|
6572
|
-
kind: "remote-moleculer",
|
|
6573
|
-
why: `no addonId known for ${route.nodeId}/${route.capName}`
|
|
6574
|
-
}]
|
|
6575
|
-
});
|
|
6576
|
-
const deviceId = extractDeviceId(args);
|
|
6577
|
-
const isNative = this.nodeAuthority.isNativeCap(route.nodeId, route.capName, deviceId);
|
|
6578
|
-
const action = capActionName(addonId, route.capName, method, isNative);
|
|
6579
|
-
return callWithServiceDiscovery(this.broker, addonId, action, args, {
|
|
6580
|
-
nodeID: route.nodeId,
|
|
6581
|
-
timeout: this.callTimeout(route.capName, method)
|
|
6582
|
-
});
|
|
6583
|
-
}
|
|
6584
|
-
case "agent-child-forward": {
|
|
6585
|
-
const deviceId = extractDeviceId(args);
|
|
6586
|
-
const params = {
|
|
6587
|
-
capName: route.capName,
|
|
6588
|
-
method,
|
|
6589
|
-
args,
|
|
6590
|
-
...route.childId !== void 0 ? { childId: route.childId } : {},
|
|
6591
|
-
...deviceId !== void 0 ? { deviceId } : {},
|
|
6592
|
-
...addonId !== void 0 ? { addonId } : {}
|
|
6593
|
-
};
|
|
6594
|
-
return callWithServiceDiscovery(this.broker, AGENT_CAP_FWD_SERVICE, AGENT_CAP_FWD_ACTION, params, {
|
|
6595
|
-
nodeID: route.agentNodeId,
|
|
6596
|
-
timeout: this.callTimeout(route.capName, method)
|
|
6597
|
-
});
|
|
6598
|
-
}
|
|
6599
|
-
}
|
|
6600
|
-
}
|
|
6601
|
-
buildSnapshot() {
|
|
6602
|
-
const hubLocalRegistry = this.hubLocalRegistry;
|
|
6603
|
-
const nodeAuthority = this.nodeAuthority;
|
|
6604
|
-
const inProcessProviders = this.inProcessProviders;
|
|
6605
|
-
const hubResidentProviders = this.hubResidentProviders;
|
|
6606
|
-
return {
|
|
6607
|
-
hubNodeId: this.hubNodeId,
|
|
6608
|
-
hubInProcessProvides: (cap) => inProcessProviders(cap) !== null,
|
|
6609
|
-
...hubResidentProviders !== void 0 ? {
|
|
6610
|
-
hubResidentProvides: (cap) => hubResidentProviders(cap) !== null,
|
|
6611
|
-
getHubResidentProviderRef: (cap) => hubResidentProviders(cap)
|
|
6612
|
-
} : {},
|
|
6613
|
-
hubLocalChildProvides: (cap, deviceId) => hubLocalRegistry !== null && hubLocalRegistry.resolveChildId(cap, deviceId) !== null,
|
|
6614
|
-
nodeKnowsCap: (nodeId, cap) => nodeAuthority.nodeKnowsCap(nodeId, cap),
|
|
6615
|
-
nodeIsAgent: (nodeId) => nodeAuthority.nodeIsAgent(nodeId),
|
|
6616
|
-
nodeOnline: (nodeId) => nodeAuthority.nodeOnline(nodeId),
|
|
6617
|
-
listKnownNodeIds: () => nodeAuthority.listNodeIds(),
|
|
6618
|
-
getInProcessProviderRef: (cap) => inProcessProviders(cap),
|
|
6619
|
-
getHubLocalChildId: (cap, deviceId) => hubLocalRegistry !== null ? hubLocalRegistry.resolveChildId(cap, deviceId) : null,
|
|
6620
|
-
getAgentChildId: (agentNodeId, cap) => nodeAuthority.getAgentChildId(agentNodeId, cap)
|
|
6621
|
-
};
|
|
6622
|
-
}
|
|
6623
|
-
/** Extract a nodeId string from a route for error reporting, or undefined. */
|
|
6624
|
-
routeNodeId(route) {
|
|
6625
|
-
switch (route.kind) {
|
|
6626
|
-
case "hub-in-process": return this.hubNodeId;
|
|
6627
|
-
case "hub-local-uds": return `${this.hubNodeId}/${route.childId}`;
|
|
6628
|
-
case "remote-moleculer": return route.nodeId;
|
|
6629
|
-
case "agent-child-forward": return route.agentNodeId;
|
|
6630
|
-
}
|
|
6631
|
-
}
|
|
6632
|
-
};
|
|
6633
|
-
//#endregion
|
|
6634
|
-
//#region src/kernel/transport/local-child-registry.ts
|
|
6635
|
-
function readFanoutMode() {
|
|
6636
|
-
const raw = process.env.CAMSTACK_UDS_EVENT_FANOUT;
|
|
6637
|
-
if (raw === "shadow" || raw === "broadcast") return raw;
|
|
6638
|
-
return "filter";
|
|
7044
|
+
//#region src/kernel/transport/cap-routing-hints.ts
|
|
7045
|
+
/** Lift {@link CapRoutingHints} from a call's args. Never throws. */
|
|
7046
|
+
function liftRoutingHints(args) {
|
|
7047
|
+
if (args === null || typeof args !== "object" || Array.isArray(args)) return {};
|
|
7048
|
+
const nodeId = Reflect.get(args, "nodeId");
|
|
7049
|
+
const addonId = Reflect.get(args, "addonId");
|
|
7050
|
+
return {
|
|
7051
|
+
...typeof nodeId === "string" && nodeId.length > 0 ? { nodeId } : {},
|
|
7052
|
+
...typeof addonId === "string" && addonId.length > 0 ? { addonId } : {}
|
|
7053
|
+
};
|
|
7054
|
+
}
|
|
7055
|
+
function readFanoutMode() {
|
|
7056
|
+
const raw = process.env.CAMSTACK_UDS_EVENT_FANOUT;
|
|
7057
|
+
if (raw === "shadow" || raw === "broadcast") return raw;
|
|
7058
|
+
return "filter";
|
|
6639
7059
|
}
|
|
6640
7060
|
/**
|
|
6641
7061
|
* Sentinel prefix used in the no-route error thrown when `cap-call-out` has no
|
|
@@ -6669,6 +7089,17 @@ var LocalChildRegistry = class {
|
|
|
6669
7089
|
/** See {@link LocalChildRegistryOptions.isAggregatedCollectionMethod}. */
|
|
6670
7090
|
isAggregatedCollectionMethod;
|
|
6671
7091
|
isAddonPinnedCall;
|
|
7092
|
+
/** See {@link ForwardCensus}. Mutable counters on the hot path, by design. */
|
|
7093
|
+
forwardedRaw = 0;
|
|
7094
|
+
forwardedDecoded = 0;
|
|
7095
|
+
/**
|
|
7096
|
+
* The per-ACTION byte census (D456). `ForwardCensus` above is the same
|
|
7097
|
+
* question asked of the whole plane; this one asks it per `<cap>.<method>`,
|
|
7098
|
+
* beside the bytes that method actually moved. Both are kept: `socketFwd=`
|
|
7099
|
+
* says whether the negotiation is working at all, and a `raw=0` on ONE
|
|
7100
|
+
* method with `raw>0` on its neighbours is a different fault.
|
|
7101
|
+
*/
|
|
7102
|
+
actionCensus = createCapActionCensus();
|
|
6672
7103
|
/** See {@link LocalChildRegistryOptions.capTimeoutMs}. */
|
|
6673
7104
|
capTimeoutMs;
|
|
6674
7105
|
/** See {@link LocalChildRegistryOptions.capUsageObserver}. */
|
|
@@ -6682,6 +7113,12 @@ var LocalChildRegistry = class {
|
|
|
6682
7113
|
/** Last pattern-set string logged per child, to dedup the INFO line. */
|
|
6683
7114
|
loggedPatternSet = /* @__PURE__ */ new Map();
|
|
6684
7115
|
/**
|
|
7116
|
+
* Derived `(cap, device?) → child` index over {@link children}, or `null`
|
|
7117
|
+
* when it must be rebuilt. See {@link capIndex} for why it is a cache and
|
|
7118
|
+
* never a registry (D3, D457).
|
|
7119
|
+
*/
|
|
7120
|
+
capChildIndex = null;
|
|
7121
|
+
/**
|
|
6685
7122
|
* Accepts either a plain positional `server` argument (backward-compatible)
|
|
6686
7123
|
* or a full `LocalChildRegistryOptions` object.
|
|
6687
7124
|
*
|
|
@@ -6738,10 +7175,43 @@ var LocalChildRegistry = class {
|
|
|
6738
7175
|
* only per-peer attribution that exists: `/proc` reports one `rchar`/`wchar`
|
|
6739
7176
|
* pair for the whole process and has no per-socket breakdown.
|
|
6740
7177
|
*/
|
|
6741
|
-
getChildSocketTraffic(childId) {
|
|
7178
|
+
getChildSocketTraffic(childId) {
|
|
7179
|
+
const entry = this.children.get(childId);
|
|
7180
|
+
if (entry === void 0) return null;
|
|
7181
|
+
return entry.channel.readTraffic?.() ?? null;
|
|
7182
|
+
}
|
|
7183
|
+
/**
|
|
7184
|
+
* Bytes this process has written to a child that the child has not yet
|
|
7185
|
+
* taken, live, or `null` when the channel keeps no such reading. Read on
|
|
7186
|
+
* every heartbeat probe (D446) — a child that stops draining is the shape
|
|
7187
|
+
* of an outbound-queue episode, and the number lives on the channel.
|
|
7188
|
+
*/
|
|
7189
|
+
getChildQueuedBytes(childId) {
|
|
6742
7190
|
const entry = this.children.get(childId);
|
|
6743
7191
|
if (entry === void 0) return null;
|
|
6744
|
-
return entry.channel.
|
|
7192
|
+
return entry.channel.queuedBytes?.() ?? null;
|
|
7193
|
+
}
|
|
7194
|
+
/** Cumulative {@link ForwardCensus} of this parent's `cap-call-out` routing. */
|
|
7195
|
+
readForwardCensus() {
|
|
7196
|
+
return {
|
|
7197
|
+
raw: this.forwardedRaw,
|
|
7198
|
+
decoded: this.forwardedDecoded
|
|
7199
|
+
};
|
|
7200
|
+
}
|
|
7201
|
+
/**
|
|
7202
|
+
* Cumulative per-action byte census: peer → `<cap>.<method>` → bytes, calls
|
|
7203
|
+
* and the raw/decoded split. A COPY (`cap-action-census.ts` explains why it
|
|
7204
|
+
* must be one); the heartbeat subtracts the previous reading.
|
|
7205
|
+
*
|
|
7206
|
+
* This is the instrument D454 closed by asking for. `socketTopBytes=` names
|
|
7207
|
+
* the peer that moves the bytes and `txkB=` says they are responses; neither
|
|
7208
|
+
* can say WHICH method originates them, and after raw-forward the bytes that
|
|
7209
|
+
* cross are no longer the proxy for what hub-main pays — 85.7 % of sibling
|
|
7210
|
+
* calls cross unread. What costs is what hub-main LOOKS at, and that is the
|
|
7211
|
+
* `decoded` half of an entry, next to the bytes it carried.
|
|
7212
|
+
*/
|
|
7213
|
+
readActionCensus() {
|
|
7214
|
+
return this.actionCensus.read();
|
|
6745
7215
|
}
|
|
6746
7216
|
/**
|
|
6747
7217
|
* The regime the counters above were produced under, read once from
|
|
@@ -6857,11 +7327,12 @@ var LocalChildRegistry = class {
|
|
|
6857
7327
|
* compete for the same capName.
|
|
6858
7328
|
*/
|
|
6859
7329
|
resolveChildId(capName, deviceId) {
|
|
7330
|
+
const index = this.capIndex();
|
|
6860
7331
|
if (deviceId !== void 0) {
|
|
6861
|
-
const deviceOwner =
|
|
7332
|
+
const deviceOwner = index.deviceOwner(capName, deviceId);
|
|
6862
7333
|
if (deviceOwner !== null) return deviceOwner;
|
|
6863
7334
|
}
|
|
6864
|
-
const candidates =
|
|
7335
|
+
const candidates = index.singletonCandidates(capName);
|
|
6865
7336
|
if (candidates.length === 0) return null;
|
|
6866
7337
|
if (candidates.length === 1) return candidates[0];
|
|
6867
7338
|
const preferredAddonId = this.getActiveSingletonAddonId?.(capName) ?? null;
|
|
@@ -6894,16 +7365,37 @@ var LocalChildRegistry = class {
|
|
|
6894
7365
|
});
|
|
6895
7366
|
} catch {}
|
|
6896
7367
|
}
|
|
6897
|
-
/**
|
|
6898
|
-
|
|
6899
|
-
|
|
6900
|
-
|
|
7368
|
+
/**
|
|
7369
|
+
* The cap index over the CURRENT children, rebuilt on first use after any
|
|
7370
|
+
* mutation of {@link children}.
|
|
7371
|
+
*
|
|
7372
|
+
* This replaced a linear walk of every child's cap manifest per
|
|
7373
|
+
* `cap-call-out`, measured at **7.1 % of hub-main's busy main thread**
|
|
7374
|
+
* across 39 children (D456). Why it did not exist before is the whole
|
|
7375
|
+
* design: what it indexes moves — a runner is replaced on every update, a
|
|
7376
|
+
* crash respawns one, a child re-registers with a replacement manifest after
|
|
7377
|
+
* init — and an index that names a dead child routes work into a socket
|
|
7378
|
+
* nobody is reading, which is strictly worse than a slow scan.
|
|
7379
|
+
*
|
|
7380
|
+
* So it is not maintained; it is DISCARDED. `buildCapChildIndex` is a pure
|
|
7381
|
+
* function of `children.values()`, the only two writers of that map
|
|
7382
|
+
* ({@link invalidateCapIndex}'s call sites) drop the index in the same
|
|
7383
|
+
* statement, and nothing else can construct one. There is no add-on-register
|
|
7384
|
+
* / remove-on-close path that could disagree with the map — i.e. no shadow
|
|
7385
|
+
* registry (D3): the map remains the single authority and this holds no fact
|
|
7386
|
+
* it does not.
|
|
7387
|
+
*
|
|
7388
|
+
* The operator's active-singleton preference is deliberately NOT indexed —
|
|
7389
|
+
* it changes without any child connecting or leaving, so `resolveChildId`
|
|
7390
|
+
* reads it live and the index only supplies the candidate list.
|
|
7391
|
+
*/
|
|
7392
|
+
capIndex() {
|
|
7393
|
+
if (this.capChildIndex === null) this.capChildIndex = buildCapChildIndex(this.children.values());
|
|
7394
|
+
return this.capChildIndex;
|
|
6901
7395
|
}
|
|
6902
|
-
/**
|
|
6903
|
-
|
|
6904
|
-
|
|
6905
|
-
for (const entry of this.children.values()) if (entry.caps.some(predicate)) out.push(entry.childId);
|
|
6906
|
-
return out;
|
|
7396
|
+
/** Drop the derived index. MUST follow every write to {@link children}. */
|
|
7397
|
+
invalidateCapIndex() {
|
|
7398
|
+
this.capChildIndex = null;
|
|
6907
7399
|
}
|
|
6908
7400
|
/**
|
|
6909
7401
|
* Does the named child currently provide `(capName, deviceId?)`?
|
|
@@ -7099,6 +7591,14 @@ var LocalChildRegistry = class {
|
|
|
7099
7591
|
onConnection(channel) {
|
|
7100
7592
|
let childId = null;
|
|
7101
7593
|
const incarnation = this.nextIncarnation++;
|
|
7594
|
+
channel.observeActions?.({
|
|
7595
|
+
label: capActionLabel,
|
|
7596
|
+
record: (label, reqBytes, resBytes) => {
|
|
7597
|
+
const peerId = childId ?? "(pre-register)";
|
|
7598
|
+
if (label === null) this.actionCensus.recordUnlabelled(peerId, reqBytes + resBytes);
|
|
7599
|
+
else this.actionCensus.record(peerId, label, reqBytes, resBytes);
|
|
7600
|
+
}
|
|
7601
|
+
});
|
|
7102
7602
|
channel.onEvent((body) => {
|
|
7103
7603
|
const msg = body;
|
|
7104
7604
|
if (msg.kind === "event") {
|
|
@@ -7121,7 +7621,7 @@ var LocalChildRegistry = class {
|
|
|
7121
7621
|
return;
|
|
7122
7622
|
}
|
|
7123
7623
|
});
|
|
7124
|
-
channel.onRequest(async (body) => {
|
|
7624
|
+
channel.onRequest(async (body, payload) => {
|
|
7125
7625
|
const msg = body;
|
|
7126
7626
|
if (msg.kind === "register") {
|
|
7127
7627
|
if (childId !== null && childId !== msg.childId) throw new Error(`child attempted to change identity from "${childId}" to "${msg.childId}"`);
|
|
@@ -7138,8 +7638,10 @@ var LocalChildRegistry = class {
|
|
|
7138
7638
|
channel,
|
|
7139
7639
|
caps: msg.caps,
|
|
7140
7640
|
eventPatterns,
|
|
7141
|
-
incarnation
|
|
7641
|
+
incarnation,
|
|
7642
|
+
rawForward: msg.rawForward === true
|
|
7142
7643
|
});
|
|
7644
|
+
this.invalidateCapIndex();
|
|
7143
7645
|
this.logPatternSet(msg.childId, eventPatterns);
|
|
7144
7646
|
this.registeredHandler({
|
|
7145
7647
|
childId: msg.childId,
|
|
@@ -7147,33 +7649,62 @@ var LocalChildRegistry = class {
|
|
|
7147
7649
|
incarnation,
|
|
7148
7650
|
...msg.customActions !== void 0 ? { customActions: msg.customActions } : {}
|
|
7149
7651
|
});
|
|
7150
|
-
return {
|
|
7652
|
+
return {
|
|
7653
|
+
ok: true,
|
|
7654
|
+
rawForward: true
|
|
7655
|
+
};
|
|
7151
7656
|
}
|
|
7152
7657
|
if (msg.kind === "cap-call-out") {
|
|
7153
7658
|
const out = msg;
|
|
7154
|
-
const
|
|
7659
|
+
const hints = out.hints ?? liftRoutingHints(out.args);
|
|
7660
|
+
const materialise = () => payload !== void 0 ? unpackPayload(payload) : out.args;
|
|
7661
|
+
const buildInput = () => ({
|
|
7155
7662
|
capName: out.capName,
|
|
7156
7663
|
method: out.method,
|
|
7157
|
-
args:
|
|
7664
|
+
args: materialise(),
|
|
7158
7665
|
...out.deviceId !== void 0 ? { deviceId: out.deviceId } : {},
|
|
7159
7666
|
...out.nodeId !== void 0 ? { nodeId: out.nodeId } : {},
|
|
7160
7667
|
...out.native === true ? { native: true } : {},
|
|
7161
7668
|
...childId !== null ? { callerAddonId: childId } : {}
|
|
7162
|
-
};
|
|
7163
|
-
const pinnedNodeId = out.nodeId ??
|
|
7669
|
+
});
|
|
7670
|
+
const pinnedNodeId = out.nodeId ?? hints.nodeId;
|
|
7164
7671
|
const pinTargetsThisNode = pinnedNodeId !== void 0 && this.ownNodeId !== void 0 && pinnedNodeId === this.ownNodeId;
|
|
7165
7672
|
const aggregated = pinnedNodeId === void 0 && this.isAggregatedCollectionMethod?.(out.capName, out.method) === true;
|
|
7166
|
-
const addonPinned = pinnedNodeId === void 0 && this.isAddonPinnedCall?.(out.capName, out.method,
|
|
7673
|
+
const addonPinned = pinnedNodeId === void 0 && this.isAddonPinnedCall?.(out.capName, out.method, hints) === true;
|
|
7167
7674
|
const target = !(out.native === true) && !aggregated && !addonPinned && (pinnedNodeId === void 0 || pinTargetsThisNode) ? this.resolveChildId(out.capName, out.deviceId) : null;
|
|
7168
7675
|
this.recordCapUsage(childId, target, out.capName, out.method);
|
|
7676
|
+
const actionPeer = childId ?? "(pre-register)";
|
|
7677
|
+
const actionLabel = `${out.capName}.${out.method}`;
|
|
7169
7678
|
if (target !== null) {
|
|
7170
7679
|
if (!this.egressRoutedCaps.has(out.capName)) {
|
|
7171
7680
|
this.egressRoutedCaps.add(out.capName);
|
|
7172
7681
|
this.logger?.info("routed child egress over UDS", { capName: out.capName });
|
|
7173
7682
|
}
|
|
7174
|
-
|
|
7683
|
+
const entry = this.children.get(target);
|
|
7684
|
+
if (payload !== void 0 && entry !== void 0 && entry.rawForward) {
|
|
7685
|
+
this.forwardedRaw += 1;
|
|
7686
|
+
this.actionCensus.recordForward(actionPeer, actionLabel, "raw");
|
|
7687
|
+
const forward = {
|
|
7688
|
+
kind: "cap-call",
|
|
7689
|
+
capName: out.capName,
|
|
7690
|
+
method: out.method,
|
|
7691
|
+
args: void 0,
|
|
7692
|
+
...out.deviceId !== void 0 ? { deviceId: out.deviceId } : {}
|
|
7693
|
+
};
|
|
7694
|
+
const declared = this.capTimeoutMs?.(out.capName, out.method);
|
|
7695
|
+
return entry.channel.request(forward, {
|
|
7696
|
+
...typeof declared === "number" ? { timeoutMs: declared } : {},
|
|
7697
|
+
payload
|
|
7698
|
+
});
|
|
7699
|
+
}
|
|
7700
|
+
this.forwardedDecoded += 1;
|
|
7701
|
+
this.actionCensus.recordForward(actionPeer, actionLabel, "decoded");
|
|
7702
|
+
const input = buildInput();
|
|
7703
|
+
return entry !== void 0 ? this.callCapOnChild(target, input) : this.callCap(input);
|
|
7175
7704
|
}
|
|
7176
|
-
|
|
7705
|
+
this.forwardedDecoded += 1;
|
|
7706
|
+
this.actionCensus.recordForward(actionPeer, actionLabel, "decoded");
|
|
7707
|
+
if (this.onUnownedCall !== void 0) return this.onUnownedCall(buildInput());
|
|
7177
7708
|
throw new Error(`${UDS_NO_ROUTE_PREFIX}: cap-call-out has no local provider for "${out.capName}" and no fallback`);
|
|
7178
7709
|
}
|
|
7179
7710
|
if (msg.kind === "readiness-request") return {
|
|
@@ -7195,7 +7726,9 @@ var LocalChildRegistry = class {
|
|
|
7195
7726
|
return;
|
|
7196
7727
|
}
|
|
7197
7728
|
this.children.delete(childId);
|
|
7729
|
+
this.invalidateCapIndex();
|
|
7198
7730
|
this.childEventStats.delete(childId);
|
|
7731
|
+
this.actionCensus.forget(childId);
|
|
7199
7732
|
this.loggedPatternSet.delete(childId);
|
|
7200
7733
|
this.goneHandler(childId, incarnation);
|
|
7201
7734
|
});
|
|
@@ -7203,6 +7736,11 @@ var LocalChildRegistry = class {
|
|
|
7203
7736
|
};
|
|
7204
7737
|
//#endregion
|
|
7205
7738
|
//#region src/kernel/transport/local-child-client.ts
|
|
7739
|
+
/** Did the parent's `register` answer say it accepts payload-borne args? */
|
|
7740
|
+
function ackAcceptsRawForward(ack) {
|
|
7741
|
+
if (ack === null || typeof ack !== "object") return false;
|
|
7742
|
+
return ack.rawForward === true;
|
|
7743
|
+
}
|
|
7206
7744
|
/**
|
|
7207
7745
|
* Child side of the local UDS transport. Connects to its parent (hub or
|
|
7208
7746
|
* agent), registers its cap manifest, and serves parent→child cap calls by
|
|
@@ -7258,6 +7796,13 @@ var LocalChildClient = class {
|
|
|
7258
7796
|
latestCustomActions = null;
|
|
7259
7797
|
/** Events and logs queued while the channel is not yet open. */
|
|
7260
7798
|
pendingEmits = [];
|
|
7799
|
+
/**
|
|
7800
|
+
* Whether the parent acknowledged raw-forward at `register`. Until it has,
|
|
7801
|
+
* `callOut` sends args inline exactly as every child always did — a legacy
|
|
7802
|
+
* parent would decode a payload-borne call to `args: undefined` and run the
|
|
7803
|
+
* provider on nothing, silently. Re-read on every re-register.
|
|
7804
|
+
*/
|
|
7805
|
+
parentRawForward = false;
|
|
7261
7806
|
/** Handler for parent→child events. Registered via `onEvent`. */
|
|
7262
7807
|
eventHandler = null;
|
|
7263
7808
|
/**
|
|
@@ -7365,147 +7910,611 @@ var LocalChildClient = class {
|
|
|
7365
7910
|
if (this.client !== null) throw new Error("LocalChildClient: already started — call close() first");
|
|
7366
7911
|
const client = createLocalTransport().createClient(this.options.nodeId);
|
|
7367
7912
|
const channel = await client.connect();
|
|
7368
|
-
channel.onRequest(async (body) => {
|
|
7913
|
+
channel.onRequest(async (body, payload) => {
|
|
7369
7914
|
const msg = body;
|
|
7370
|
-
if (msg.kind === "cap-call")
|
|
7371
|
-
|
|
7372
|
-
|
|
7373
|
-
|
|
7374
|
-
|
|
7375
|
-
|
|
7915
|
+
if (msg.kind === "cap-call") {
|
|
7916
|
+
const args = payload !== void 0 ? unpackPayload(payload) : msg.args;
|
|
7917
|
+
const result = await this.options.dispatch({
|
|
7918
|
+
capName: msg.capName,
|
|
7919
|
+
method: msg.method,
|
|
7920
|
+
args,
|
|
7921
|
+
...msg.deviceId !== void 0 ? { deviceId: msg.deviceId } : {},
|
|
7922
|
+
...msg.addonId !== void 0 ? { addonId: msg.addonId } : {}
|
|
7923
|
+
});
|
|
7924
|
+
if (payload !== void 0 && result !== void 0) return new PayloadReply(void 0, packPayload(result));
|
|
7925
|
+
return result;
|
|
7926
|
+
}
|
|
7927
|
+
if (msg.kind === "addon-call") {
|
|
7928
|
+
if (this.addonCallHandler === null) throw new Error(`LocalChildClient: addon-call for "${msg.addonId}" arrived but no onAddonCall handler is registered`);
|
|
7929
|
+
return this.addonCallHandler({
|
|
7930
|
+
addonId: msg.addonId,
|
|
7931
|
+
target: msg.target,
|
|
7932
|
+
...msg.action !== void 0 ? { action: msg.action } : {},
|
|
7933
|
+
...msg.method !== void 0 ? { method: msg.method } : {},
|
|
7934
|
+
...msg.args !== void 0 ? { args: msg.args } : {},
|
|
7935
|
+
...msg.caller !== void 0 ? { caller: msg.caller } : {}
|
|
7936
|
+
});
|
|
7937
|
+
}
|
|
7938
|
+
throw new Error(`unknown parent request kind: ${msg.kind}`);
|
|
7939
|
+
});
|
|
7940
|
+
channel.onEvent((body) => {
|
|
7941
|
+
const msg = body;
|
|
7942
|
+
if (msg.kind === "event") {
|
|
7943
|
+
this.eventHandler?.(msg.event);
|
|
7944
|
+
return;
|
|
7945
|
+
}
|
|
7946
|
+
if (msg.kind === "set-log-level") this.setLogLevelHandler?.(msg.level);
|
|
7947
|
+
});
|
|
7948
|
+
const register = this.registerFrame(this.latestCaps);
|
|
7949
|
+
try {
|
|
7950
|
+
this.parentRawForward = ackAcceptsRawForward(await channel.request(register));
|
|
7951
|
+
} catch (err) {
|
|
7952
|
+
await client.close();
|
|
7953
|
+
throw err;
|
|
7954
|
+
}
|
|
7955
|
+
this.client = client;
|
|
7956
|
+
this.channel = channel;
|
|
7957
|
+
this.flushPending(channel);
|
|
7958
|
+
for (const cb of this.connectedHandlers) cb();
|
|
7959
|
+
}
|
|
7960
|
+
/** Flush buffered pre-start emits over the now-open channel. */
|
|
7961
|
+
flushPending(channel) {
|
|
7962
|
+
for (const item of this.pendingEmits) channel.emit(item.msg);
|
|
7963
|
+
this.pendingEmits.length = 0;
|
|
7964
|
+
}
|
|
7965
|
+
/**
|
|
7966
|
+
* Re-send the cap manifest to the parent, atomically replacing the child's
|
|
7967
|
+
* registered descriptor set. Call this after device-restore so newly
|
|
7968
|
+
* registered native (device-scoped) caps become routable over UDS.
|
|
7969
|
+
*
|
|
7970
|
+
* Safe to call before `start()`: the new set is buffered and `start()` sends
|
|
7971
|
+
* it (device-restore can race ahead of the UDS connect). After `start()`,
|
|
7972
|
+
* the set is sent immediately.
|
|
7973
|
+
*/
|
|
7974
|
+
async updateCaps(caps, customActions) {
|
|
7975
|
+
this.latestCaps = caps;
|
|
7976
|
+
if (customActions !== void 0) this.latestCustomActions = customActions;
|
|
7977
|
+
if (this.channel === null) return;
|
|
7978
|
+
this.parentRawForward = ackAcceptsRawForward(await this.channel.request(this.registerFrame(caps)));
|
|
7979
|
+
}
|
|
7980
|
+
/**
|
|
7981
|
+
* The register frame: caps, plus everything a re-register must atomically
|
|
7982
|
+
* re-carry so no separate re-sync can be missed — the subscription union
|
|
7983
|
+
* (omitted if nothing ever declared, which keeps the fail-open shape) and the
|
|
7984
|
+
* custom-action catalogs (omitted until the init loop produced them).
|
|
7985
|
+
*/
|
|
7986
|
+
registerFrame(caps) {
|
|
7987
|
+
return {
|
|
7988
|
+
kind: "register",
|
|
7989
|
+
childId: this.options.childId,
|
|
7990
|
+
caps,
|
|
7991
|
+
...this.hasDeclaredEventPatterns ? { eventPatterns: this.latestEventPatterns } : {},
|
|
7992
|
+
...this.latestCustomActions !== null ? { customActions: this.latestCustomActions } : {},
|
|
7993
|
+
rawForward: true
|
|
7994
|
+
};
|
|
7995
|
+
}
|
|
7996
|
+
/**
|
|
7997
|
+
* Fire-and-forget: send a system event to the parent for forwarding to the
|
|
7998
|
+
* hub event bus. Safe to call before `start()` — events are buffered and
|
|
7999
|
+
* flushed on connect.
|
|
8000
|
+
*/
|
|
8001
|
+
emitEvent(event) {
|
|
8002
|
+
const msg = {
|
|
8003
|
+
kind: "event",
|
|
8004
|
+
event
|
|
8005
|
+
};
|
|
8006
|
+
if (this.channel !== null) this.channel.emit(msg);
|
|
8007
|
+
else this.pendingEmits.push({
|
|
8008
|
+
kind: "event",
|
|
8009
|
+
msg
|
|
8010
|
+
});
|
|
8011
|
+
}
|
|
8012
|
+
/**
|
|
8013
|
+
* Fire-and-forget: send a structured log entry to the parent. Safe to call
|
|
8014
|
+
* before `start()` — log entries are buffered and flushed on connect.
|
|
8015
|
+
*/
|
|
8016
|
+
sendLog(entry) {
|
|
8017
|
+
const msg = {
|
|
8018
|
+
kind: "log",
|
|
8019
|
+
...entry
|
|
8020
|
+
};
|
|
8021
|
+
if (this.channel !== null) this.channel.emit(msg);
|
|
8022
|
+
else this.pendingEmits.push({
|
|
8023
|
+
kind: "log",
|
|
8024
|
+
msg
|
|
8025
|
+
});
|
|
8026
|
+
}
|
|
8027
|
+
/**
|
|
8028
|
+
* Request the current readiness snapshot from the parent. Returns the
|
|
8029
|
+
* authoritative set of `IReadinessRegistryRecord` entries the hub holds.
|
|
8030
|
+
* Requires `start()` to have been called; throws with a clear message if not.
|
|
8031
|
+
*/
|
|
8032
|
+
async requestReadinessSnapshot() {
|
|
8033
|
+
if (this.channel === null) throw new Error("LocalChildClient: requestReadinessSnapshot called before start()");
|
|
8034
|
+
return (await this.channel.request({ kind: "readiness-request" })).records;
|
|
8035
|
+
}
|
|
8036
|
+
/**
|
|
8037
|
+
* Ask the parent to execute a cap call this child does NOT own. The parent
|
|
8038
|
+
* routes to the owning local sibling over UDS, or — if no sibling owns it —
|
|
8039
|
+
* to its `onUnownedCall` fallback (the cluster CapabilityRegistry in
|
|
8040
|
+
* production). Throws if called before `start()`.
|
|
8041
|
+
*/
|
|
8042
|
+
async callOut(input) {
|
|
8043
|
+
if (this.channel === null) throw new Error("LocalChildClient: callOut before start");
|
|
8044
|
+
const routing = {
|
|
8045
|
+
...input.deviceId !== void 0 ? { deviceId: input.deviceId } : {},
|
|
8046
|
+
...input.nodeId !== void 0 ? { nodeId: input.nodeId } : {},
|
|
8047
|
+
...input.native === true ? { native: true } : {}
|
|
8048
|
+
};
|
|
8049
|
+
if (this.parentRawForward && input.args !== void 0) {
|
|
8050
|
+
const hints = liftRoutingHints(input.args);
|
|
8051
|
+
const msg = {
|
|
8052
|
+
kind: "cap-call-out",
|
|
8053
|
+
capName: input.capName,
|
|
8054
|
+
method: input.method,
|
|
8055
|
+
...Object.keys(hints).length > 0 ? { hints } : {},
|
|
8056
|
+
...routing
|
|
8057
|
+
};
|
|
8058
|
+
const reply = await this.channel.request(msg, { payload: packPayload(input.args) });
|
|
8059
|
+
return reply instanceof PayloadReply ? unpackPayload(reply.payload) : reply;
|
|
8060
|
+
}
|
|
8061
|
+
const msg = {
|
|
8062
|
+
kind: "cap-call-out",
|
|
8063
|
+
capName: input.capName,
|
|
8064
|
+
method: input.method,
|
|
8065
|
+
args: input.args,
|
|
8066
|
+
...routing
|
|
8067
|
+
};
|
|
8068
|
+
return this.channel.request(msg);
|
|
8069
|
+
}
|
|
8070
|
+
/** Disconnect from the parent. Safe to call before `start()` (no-op) and idempotent. */
|
|
8071
|
+
async close() {
|
|
8072
|
+
await this.client?.close();
|
|
8073
|
+
this.client = null;
|
|
8074
|
+
this.channel = null;
|
|
8075
|
+
}
|
|
8076
|
+
};
|
|
8077
|
+
//#endregion
|
|
8078
|
+
//#region src/kernel/transport/cap-route.ts
|
|
8079
|
+
function buildMessage(capName, method, detail) {
|
|
8080
|
+
const call = method !== void 0 ? `${capName}.${method}` : capName;
|
|
8081
|
+
const target = detail.nodeId !== void 0 ? ` to node ${detail.nodeId}` : "";
|
|
8082
|
+
const rejectedStr = detail.rejected.map((r) => `${r.kind}=${r.why}`).join("; ");
|
|
8083
|
+
const rejectedClause = rejectedStr.length > 0 ? ` (rejected: ${rejectedStr})` : "";
|
|
8084
|
+
return `${call} not routable${target}: ${detail.reason}${rejectedClause}`;
|
|
8085
|
+
}
|
|
8086
|
+
var CapRouteError = class extends Error {
|
|
8087
|
+
reason;
|
|
8088
|
+
nodeId;
|
|
8089
|
+
rejected;
|
|
8090
|
+
/**
|
|
8091
|
+
* @param cause Optional original error that triggered this routing failure.
|
|
8092
|
+
* Stored as `Error.cause` (TC39 standard option, Node 16.9+).
|
|
8093
|
+
* Dispatchers wrapping transport errors MUST pass the original.
|
|
8094
|
+
*/
|
|
8095
|
+
constructor(capName, method, detail, cause) {
|
|
8096
|
+
super(buildMessage(capName, method, detail), cause !== void 0 ? { cause } : void 0);
|
|
8097
|
+
this.name = "CapRouteError";
|
|
8098
|
+
this.reason = detail.reason;
|
|
8099
|
+
this.nodeId = detail.nodeId;
|
|
8100
|
+
this.rejected = detail.rejected;
|
|
8101
|
+
}
|
|
8102
|
+
};
|
|
8103
|
+
/**
|
|
8104
|
+
* Classifies a (capName, opts) pair into a typed CapRoute dispatch descriptor.
|
|
8105
|
+
*
|
|
8106
|
+
* Precedence (explicit nodeId path):
|
|
8107
|
+
* 1. hub-in-process — hub node + hubInProcessProvides
|
|
8108
|
+
* 2. hub-local-uds — hub node + hubLocalChildProvides
|
|
8109
|
+
* 3. node-offline → CapRouteError{reason:'node-offline'}
|
|
8110
|
+
* 4. agent-child-forward — node is an agent AND nodeKnowsCap
|
|
8111
|
+
* 5. remote-moleculer — any other online non-agent node
|
|
8112
|
+
*
|
|
8113
|
+
* Singleton path (no nodeId):
|
|
8114
|
+
* 1. hub-in-process (hub provides in-process)
|
|
8115
|
+
* 2. hub-local-uds (a hub-local UDS child provides it)
|
|
8116
|
+
* 3. remote-moleculer (any online, non-hub node that knows it)
|
|
8117
|
+
* 4. → CapRouteError{reason:'no-provider', rejected: all considered routes}
|
|
8118
|
+
*
|
|
8119
|
+
* PURE: no side effects, no async, no broker/registry imports.
|
|
8120
|
+
*/
|
|
8121
|
+
function classifyCapRoute(capName, opts, snapshot) {
|
|
8122
|
+
const rejected = [];
|
|
8123
|
+
if (opts.nodeId !== void 0) return classifyExplicitNode(capName, opts.nodeId, opts.deviceId, snapshot, rejected);
|
|
8124
|
+
return classifySingleton(capName, opts.deviceId, snapshot, rejected);
|
|
8125
|
+
}
|
|
8126
|
+
function classifyExplicitNode(capName, nodeId, deviceId, snap, rejected) {
|
|
8127
|
+
if (nodeId === snap.hubNodeId) {
|
|
8128
|
+
const residentProvides = snap.hubResidentProvides;
|
|
8129
|
+
const providesOnHub = residentProvides ?? snap.hubInProcessProvides;
|
|
8130
|
+
const getRef = residentProvides !== void 0 ? snap.getHubResidentProviderRef : snap.getInProcessProviderRef;
|
|
8131
|
+
if (providesOnHub(capName)) {
|
|
8132
|
+
const ref = getRef?.(capName) ?? null;
|
|
8133
|
+
if (ref !== null) return {
|
|
8134
|
+
kind: "hub-in-process",
|
|
8135
|
+
capName,
|
|
8136
|
+
ref
|
|
8137
|
+
};
|
|
8138
|
+
rejected.push({
|
|
8139
|
+
kind: "hub-in-process",
|
|
8140
|
+
why: "provider ref not available in snapshot"
|
|
8141
|
+
});
|
|
8142
|
+
throw new CapRouteError(capName, void 0, {
|
|
8143
|
+
reason: "no-provider",
|
|
8144
|
+
nodeId,
|
|
8145
|
+
rejected
|
|
8146
|
+
});
|
|
8147
|
+
}
|
|
8148
|
+
if (snap.hubLocalChildProvides(capName, deviceId)) {
|
|
8149
|
+
const childId = snap.getHubLocalChildId?.(capName, deviceId) ?? null;
|
|
8150
|
+
if (childId !== null) return {
|
|
8151
|
+
kind: "hub-local-uds",
|
|
8152
|
+
capName,
|
|
8153
|
+
childId
|
|
8154
|
+
};
|
|
8155
|
+
rejected.push({
|
|
8156
|
+
kind: "hub-local-uds",
|
|
8157
|
+
why: "child id not resolvable from snapshot"
|
|
8158
|
+
});
|
|
8159
|
+
throw new CapRouteError(capName, void 0, {
|
|
8160
|
+
reason: "no-provider",
|
|
8161
|
+
nodeId,
|
|
8162
|
+
rejected
|
|
7376
8163
|
});
|
|
7377
|
-
|
|
7378
|
-
|
|
7379
|
-
|
|
7380
|
-
|
|
7381
|
-
target: msg.target,
|
|
7382
|
-
...msg.action !== void 0 ? { action: msg.action } : {},
|
|
7383
|
-
...msg.method !== void 0 ? { method: msg.method } : {},
|
|
7384
|
-
...msg.args !== void 0 ? { args: msg.args } : {},
|
|
7385
|
-
...msg.caller !== void 0 ? { caller: msg.caller } : {}
|
|
7386
|
-
});
|
|
7387
|
-
}
|
|
7388
|
-
throw new Error(`unknown parent request kind: ${msg.kind}`);
|
|
8164
|
+
}
|
|
8165
|
+
rejected.push({
|
|
8166
|
+
kind: "hub-in-process",
|
|
8167
|
+
why: "hub does not provide this cap in-process"
|
|
7389
8168
|
});
|
|
7390
|
-
|
|
7391
|
-
|
|
7392
|
-
|
|
7393
|
-
|
|
7394
|
-
|
|
7395
|
-
|
|
7396
|
-
|
|
8169
|
+
rejected.push({
|
|
8170
|
+
kind: "hub-local-uds",
|
|
8171
|
+
why: "no hub-local child provides this cap"
|
|
8172
|
+
});
|
|
8173
|
+
throw new CapRouteError(capName, void 0, {
|
|
8174
|
+
reason: "no-provider",
|
|
8175
|
+
nodeId,
|
|
8176
|
+
rejected
|
|
7397
8177
|
});
|
|
7398
|
-
const register = this.registerFrame(this.latestCaps);
|
|
7399
|
-
try {
|
|
7400
|
-
await channel.request(register);
|
|
7401
|
-
} catch (err) {
|
|
7402
|
-
await client.close();
|
|
7403
|
-
throw err;
|
|
7404
|
-
}
|
|
7405
|
-
this.client = client;
|
|
7406
|
-
this.channel = channel;
|
|
7407
|
-
this.flushPending(channel);
|
|
7408
|
-
for (const cb of this.connectedHandlers) cb();
|
|
7409
|
-
}
|
|
7410
|
-
/** Flush buffered pre-start emits over the now-open channel. */
|
|
7411
|
-
flushPending(channel) {
|
|
7412
|
-
for (const item of this.pendingEmits) channel.emit(item.msg);
|
|
7413
|
-
this.pendingEmits.length = 0;
|
|
7414
8178
|
}
|
|
7415
|
-
|
|
7416
|
-
|
|
7417
|
-
|
|
7418
|
-
|
|
7419
|
-
|
|
7420
|
-
|
|
7421
|
-
|
|
7422
|
-
|
|
7423
|
-
|
|
7424
|
-
|
|
7425
|
-
this.latestCaps = caps;
|
|
7426
|
-
if (customActions !== void 0) this.latestCustomActions = customActions;
|
|
7427
|
-
if (this.channel === null) return;
|
|
7428
|
-
await this.channel.request(this.registerFrame(caps));
|
|
8179
|
+
if (!snap.nodeOnline(nodeId)) {
|
|
8180
|
+
rejected.push({
|
|
8181
|
+
kind: "remote-moleculer",
|
|
8182
|
+
why: `node ${nodeId} is offline`
|
|
8183
|
+
});
|
|
8184
|
+
throw new CapRouteError(capName, void 0, {
|
|
8185
|
+
reason: "node-offline",
|
|
8186
|
+
nodeId,
|
|
8187
|
+
rejected
|
|
8188
|
+
});
|
|
7429
8189
|
}
|
|
7430
|
-
|
|
7431
|
-
|
|
7432
|
-
|
|
7433
|
-
|
|
7434
|
-
|
|
7435
|
-
|
|
7436
|
-
registerFrame(caps) {
|
|
7437
|
-
return {
|
|
7438
|
-
kind: "register",
|
|
7439
|
-
childId: this.options.childId,
|
|
7440
|
-
caps,
|
|
7441
|
-
...this.hasDeclaredEventPatterns ? { eventPatterns: this.latestEventPatterns } : {},
|
|
7442
|
-
...this.latestCustomActions !== null ? { customActions: this.latestCustomActions } : {}
|
|
8190
|
+
if (snap.nodeIsAgent(nodeId)) {
|
|
8191
|
+
if (snap.nodeKnowsCap(nodeId, capName)) return {
|
|
8192
|
+
kind: "agent-child-forward",
|
|
8193
|
+
capName,
|
|
8194
|
+
agentNodeId: nodeId,
|
|
8195
|
+
childId: snap.getAgentChildId?.(nodeId, capName) ?? void 0
|
|
7443
8196
|
};
|
|
8197
|
+
rejected.push({
|
|
8198
|
+
kind: "agent-child-forward",
|
|
8199
|
+
why: `agent ${nodeId} does not know cap ${capName}`
|
|
8200
|
+
});
|
|
8201
|
+
throw new CapRouteError(capName, void 0, {
|
|
8202
|
+
reason: "no-provider",
|
|
8203
|
+
nodeId,
|
|
8204
|
+
rejected
|
|
8205
|
+
});
|
|
7444
8206
|
}
|
|
7445
|
-
|
|
7446
|
-
|
|
7447
|
-
|
|
7448
|
-
|
|
7449
|
-
|
|
7450
|
-
|
|
7451
|
-
|
|
7452
|
-
|
|
7453
|
-
|
|
8207
|
+
return {
|
|
8208
|
+
kind: "remote-moleculer",
|
|
8209
|
+
capName,
|
|
8210
|
+
nodeId
|
|
8211
|
+
};
|
|
8212
|
+
}
|
|
8213
|
+
function classifySingleton(capName, deviceId, snap, rejected) {
|
|
8214
|
+
if (snap.hubInProcessProvides(capName)) {
|
|
8215
|
+
const ref = snap.getInProcessProviderRef?.(capName) ?? null;
|
|
8216
|
+
if (ref !== null) return {
|
|
8217
|
+
kind: "hub-in-process",
|
|
8218
|
+
capName,
|
|
8219
|
+
ref
|
|
7454
8220
|
};
|
|
7455
|
-
|
|
7456
|
-
|
|
7457
|
-
|
|
7458
|
-
|
|
8221
|
+
rejected.push({
|
|
8222
|
+
kind: "hub-in-process",
|
|
8223
|
+
why: "provider ref not available in snapshot"
|
|
8224
|
+
});
|
|
8225
|
+
} else rejected.push({
|
|
8226
|
+
kind: "hub-in-process",
|
|
8227
|
+
why: "hub does not provide this cap in-process"
|
|
8228
|
+
});
|
|
8229
|
+
if (snap.hubLocalChildProvides(capName, deviceId)) {
|
|
8230
|
+
const childId = snap.getHubLocalChildId?.(capName, deviceId) ?? null;
|
|
8231
|
+
if (childId !== null) return {
|
|
8232
|
+
kind: "hub-local-uds",
|
|
8233
|
+
capName,
|
|
8234
|
+
childId
|
|
8235
|
+
};
|
|
8236
|
+
rejected.push({
|
|
8237
|
+
kind: "hub-local-uds",
|
|
8238
|
+
why: "child id not resolvable from snapshot"
|
|
7459
8239
|
});
|
|
8240
|
+
} else rejected.push({
|
|
8241
|
+
kind: "hub-local-uds",
|
|
8242
|
+
why: "no hub-local child provides this cap"
|
|
8243
|
+
});
|
|
8244
|
+
const knownNodes = snap.listKnownNodeIds?.() ?? [];
|
|
8245
|
+
for (const nodeId of knownNodes) if (nodeId !== snap.hubNodeId && snap.nodeOnline(nodeId) && snap.nodeKnowsCap(nodeId, capName)) return {
|
|
8246
|
+
kind: "remote-moleculer",
|
|
8247
|
+
capName,
|
|
8248
|
+
nodeId
|
|
8249
|
+
};
|
|
8250
|
+
rejected.push({
|
|
8251
|
+
kind: "remote-moleculer",
|
|
8252
|
+
why: "no online remote node knows this cap"
|
|
8253
|
+
});
|
|
8254
|
+
throw new CapRouteError(capName, void 0, {
|
|
8255
|
+
reason: "no-provider",
|
|
8256
|
+
rejected
|
|
8257
|
+
});
|
|
8258
|
+
}
|
|
8259
|
+
//#endregion
|
|
8260
|
+
//#region src/kernel/moleculer/resilient-cap-call.ts
|
|
8261
|
+
/** Moleculer error `type` values meaning "the service is not (yet) routable". */
|
|
8262
|
+
var DISCOVERY_ERROR_TYPES = new Set(["SERVICE_NOT_FOUND", "SERVICE_NOT_AVAILABLE"]);
|
|
8263
|
+
/** Default ceiling for the discovery wait — fail-fast past this. */
|
|
8264
|
+
var DEFAULT_DISCOVERY_TIMEOUT_MS$1 = 3e4;
|
|
8265
|
+
function isDiscoveryError(err) {
|
|
8266
|
+
if (typeof err !== "object" || err === null) return false;
|
|
8267
|
+
const type = err.type;
|
|
8268
|
+
return typeof type === "string" && DISCOVERY_ERROR_TYPES.has(type);
|
|
8269
|
+
}
|
|
8270
|
+
/**
|
|
8271
|
+
* Call a Moleculer action; on a service-discovery error, wait for the
|
|
8272
|
+
* named service to be discovered and retry the call exactly once.
|
|
8273
|
+
*/
|
|
8274
|
+
async function callWithServiceDiscovery(broker, serviceName, action, params, opts, discoveryTimeoutMs = DEFAULT_DISCOVERY_TIMEOUT_MS$1) {
|
|
8275
|
+
try {
|
|
8276
|
+
return await broker.call(action, params, opts);
|
|
8277
|
+
} catch (err) {
|
|
8278
|
+
if (!isDiscoveryError(err)) throw err;
|
|
8279
|
+
await broker.waitForServices([serviceName], discoveryTimeoutMs);
|
|
8280
|
+
return await broker.call(action, params, opts);
|
|
8281
|
+
}
|
|
8282
|
+
}
|
|
8283
|
+
//#endregion
|
|
8284
|
+
//#region src/kernel/transport/cap-route-resolver.ts
|
|
8285
|
+
/** The Moleculer service name that Task 6's agent registers. */
|
|
8286
|
+
var AGENT_CAP_FWD_SERVICE = "$agent-cap-fwd";
|
|
8287
|
+
/** The Moleculer action (service.action) for agent cap forwarding. */
|
|
8288
|
+
var AGENT_CAP_FWD_ACTION = `${AGENT_CAP_FWD_SERVICE}.forward`;
|
|
8289
|
+
/** Default timeout for remote Moleculer cap calls (ms). */
|
|
8290
|
+
var REMOTE_CALL_TIMEOUT_MS = 6e4;
|
|
8291
|
+
function extractDeviceId(args) {
|
|
8292
|
+
if (args === null || typeof args !== "object") return void 0;
|
|
8293
|
+
const raw = Reflect.get(args, "deviceId");
|
|
8294
|
+
return typeof raw === "number" ? raw : void 0;
|
|
8295
|
+
}
|
|
8296
|
+
/**
|
|
8297
|
+
* Extract an inline `nodeId` string from a cap call's args — the per-call node
|
|
8298
|
+
* pin a forked addon expresses by carrying `nodeId` in the input (the SAME way
|
|
8299
|
+
* device-scoped caps carry `deviceId`, and the way the generated cap-router on
|
|
8300
|
+
* the hub already honours an inline pin). Mirrors {@link extractDeviceId}.
|
|
8301
|
+
*
|
|
8302
|
+
* This is the routing source for hub→remote-node EXECUTION pinning (e.g. the
|
|
8303
|
+
* benchmark addon running a synthetic/decoder workload ON a chosen agent). The
|
|
8304
|
+
* out-of-band `nodePin(op.context)` path does NOT survive the forked addon →
|
|
8305
|
+
* hub UDS link chain, so `onUnownedCall` reads the inline pin instead. Provider
|
|
8306
|
+
* methods that don't declare `nodeId` simply ignore the extra field (they
|
|
8307
|
+
* destructure the fields they need); methods that DO declare it (e.g.
|
|
8308
|
+
* `getEngineProvisioning({nodeId})`) still receive it unchanged.
|
|
8309
|
+
*/
|
|
8310
|
+
function extractNodeId(args) {
|
|
8311
|
+
if (args === null || typeof args !== "object") return void 0;
|
|
8312
|
+
const raw = Reflect.get(args, "nodeId");
|
|
8313
|
+
return typeof raw === "string" && raw.length > 0 ? raw : void 0;
|
|
8314
|
+
}
|
|
8315
|
+
/**
|
|
8316
|
+
* Merge `callerAddonId` into `args` as an extra field — the SAME "provider
|
|
8317
|
+
* destructures what it needs and ignores the rest" convention
|
|
8318
|
+
* {@link extractNodeId} already relies on for the inline `nodeId` pin.
|
|
8319
|
+
* `undefined` (the overwhelmingly common case: no caller hint, or the census
|
|
8320
|
+
* consuming it is disarmed) returns `args` unchanged — no allocation. A
|
|
8321
|
+
* non-object `args` (or `null`/array) is returned unchanged too: there is
|
|
8322
|
+
* nowhere to attach an extra key without changing the payload's shape, and a
|
|
8323
|
+
* provider taking a bare value never reads `callerAddonId` off it anyway.
|
|
8324
|
+
*/
|
|
8325
|
+
function withCallerAddonId(args, callerAddonId) {
|
|
8326
|
+
if (callerAddonId === void 0) return args;
|
|
8327
|
+
if (args === null || typeof args !== "object" || Array.isArray(args)) return args;
|
|
8328
|
+
return {
|
|
8329
|
+
...args,
|
|
8330
|
+
callerAddonId
|
|
8331
|
+
};
|
|
8332
|
+
}
|
|
8333
|
+
var CapRouteResolver = class {
|
|
8334
|
+
hubNodeId;
|
|
8335
|
+
broker;
|
|
8336
|
+
hubLocalRegistry;
|
|
8337
|
+
nodeAuthority;
|
|
8338
|
+
inProcessProviders;
|
|
8339
|
+
hubResidentProviders;
|
|
8340
|
+
capTimeoutMs;
|
|
8341
|
+
snapshot;
|
|
8342
|
+
constructor(deps) {
|
|
8343
|
+
this.hubNodeId = deps.hubNodeId;
|
|
8344
|
+
this.broker = deps.broker;
|
|
8345
|
+
this.hubLocalRegistry = deps.hubLocalRegistry;
|
|
8346
|
+
this.nodeAuthority = deps.nodeAuthority;
|
|
8347
|
+
this.inProcessProviders = deps.inProcessProviders;
|
|
8348
|
+
this.hubResidentProviders = deps.hubResidentProviders;
|
|
8349
|
+
this.capTimeoutMs = deps.capTimeoutMs;
|
|
8350
|
+
this.snapshot = this.buildSnapshot();
|
|
8351
|
+
}
|
|
8352
|
+
/** Per-call RPC timeout: the cap method's declared override, else the default. */
|
|
8353
|
+
callTimeout(capName, method) {
|
|
8354
|
+
return this.capTimeoutMs?.(capName, method) ?? REMOTE_CALL_TIMEOUT_MS;
|
|
8355
|
+
}
|
|
8356
|
+
resolveCapRoute(capName, opts) {
|
|
8357
|
+
return classifyCapRoute(capName, opts, this.snapshot);
|
|
7460
8358
|
}
|
|
7461
8359
|
/**
|
|
7462
|
-
*
|
|
7463
|
-
*
|
|
8360
|
+
* Resolve the hub-local-uds route for a cap owned by a forked hub-local
|
|
8361
|
+
* child, IGNORING any in-hub provider registered for the same cap name.
|
|
8362
|
+
*
|
|
8363
|
+
* `resolveCapRoute` gives Priority 1 to `hub-in-process`: when an in-hub
|
|
8364
|
+
* provider (e.g. a wrapper) is registered for the cap, the route always
|
|
8365
|
+
* classifies as `hub-in-process` and the hub-local-uds NATIVE child is never
|
|
8366
|
+
* reached. That is correct for the generic dispatch path (the wrapper is the
|
|
8367
|
+
* active provider), but WRONG for the native-cap fallback
|
|
8368
|
+
* (`setNativeFallback`), whose contract is to reach the NATIVE provider in
|
|
8369
|
+
* the forked vendor child so a wrapper can delegate to it. A wrapper cap
|
|
8370
|
+
* with a forked native (today: `snapshot`) otherwise resolves to the wrapper
|
|
8371
|
+
* itself, the native is never invoked, and the wrapper silently falls
|
|
8372
|
+
* through to its secondary strategy.
|
|
8373
|
+
*
|
|
8374
|
+
* This method consults ONLY the hub-local-child authority
|
|
8375
|
+
* (`hubLocalChildProvides` + `getHubLocalChildId`, both deviceId-aware), so
|
|
8376
|
+
* it returns the native child route regardless of any in-process shadow.
|
|
8377
|
+
* Returns null when no hub-local child owns `(capName, deviceId)` — the
|
|
8378
|
+
* caller then falls through to its remote-resolution branch.
|
|
7464
8379
|
*/
|
|
7465
|
-
|
|
7466
|
-
|
|
7467
|
-
|
|
7468
|
-
|
|
8380
|
+
resolveHubLocalUdsRoute(capName, deviceId) {
|
|
8381
|
+
if (!this.snapshot.hubLocalChildProvides(capName, deviceId)) return null;
|
|
8382
|
+
const childId = this.snapshot.getHubLocalChildId?.(capName, deviceId) ?? null;
|
|
8383
|
+
if (childId === null) return null;
|
|
8384
|
+
return {
|
|
8385
|
+
kind: "hub-local-uds",
|
|
8386
|
+
capName,
|
|
8387
|
+
childId
|
|
7469
8388
|
};
|
|
7470
|
-
if (this.channel !== null) this.channel.emit(msg);
|
|
7471
|
-
else this.pendingEmits.push({
|
|
7472
|
-
kind: "log",
|
|
7473
|
-
msg
|
|
7474
|
-
});
|
|
7475
8389
|
}
|
|
7476
8390
|
/**
|
|
7477
|
-
*
|
|
7478
|
-
*
|
|
7479
|
-
*
|
|
8391
|
+
* @param addonId Optional out-of-band PROVIDER-selection hint (Task 7.5a):
|
|
8392
|
+
* the addonId of the provider the CALLER already resolved (e.g.
|
|
8393
|
+
* `buildCapCallFn`'s `deps.addonId` for a registered grouped-runner
|
|
8394
|
+
* provider). Only meaningful for `agent-child-forward` — a `remote-moleculer`
|
|
8395
|
+
* route already encodes the provider in its action name
|
|
8396
|
+
* (`${addonId}.${cap}.${method}`, via `NodeCapAuthority.getAddonId`), and a
|
|
8397
|
+
* `hub-local-uds` caller that already knows the addonId (`buildCapCallFn`)
|
|
8398
|
+
* talks to `LocalChildRegistry.callCapOnChild` directly rather than through
|
|
8399
|
+
* `dispatch`.
|
|
8400
|
+
* @param callerAddonId Optional out-of-band CALLER-identity hint — the
|
|
8401
|
+
* addon that ORIGINATED the call, distinct from `addonId` above (which
|
|
8402
|
+
* names the resolved PROVIDER, not the caller). Set by
|
|
8403
|
+
* `onUnownedCall` from `CapCallInput.callerAddonId`
|
|
8404
|
+
* (`LocalChildRegistry` stamped it off the registered `childId`). Only
|
|
8405
|
+
* the `hub-in-process` branch consumes it — merged into `args` so a
|
|
8406
|
+
* diagnostic tool deep in that provider (e.g. device-manager's
|
|
8407
|
+
* fleet-read census) can read it without every provider method changing
|
|
8408
|
+
* signature, the same "extra field the provider destructures or
|
|
8409
|
+
* ignores" convention `nodeId` already uses on this path.
|
|
7480
8410
|
*/
|
|
7481
|
-
async
|
|
7482
|
-
|
|
7483
|
-
|
|
8411
|
+
async dispatch(route, method, args, addonId, callerAddonId) {
|
|
8412
|
+
try {
|
|
8413
|
+
return await this.dispatchInner(route, method, args, addonId, callerAddonId);
|
|
8414
|
+
} catch (err) {
|
|
8415
|
+
if (err instanceof CapRouteError) throw err;
|
|
8416
|
+
const nodeId = this.routeNodeId(route);
|
|
8417
|
+
const cause = err instanceof Error ? err : new Error(String(err));
|
|
8418
|
+
throw new CapRouteError(route.capName, method, {
|
|
8419
|
+
reason: "transport-failed",
|
|
8420
|
+
nodeId,
|
|
8421
|
+
rejected: [{
|
|
8422
|
+
kind: route.kind,
|
|
8423
|
+
why: cause.message
|
|
8424
|
+
}]
|
|
8425
|
+
}, cause);
|
|
8426
|
+
}
|
|
7484
8427
|
}
|
|
7485
8428
|
/**
|
|
7486
|
-
*
|
|
7487
|
-
*
|
|
7488
|
-
* to its `onUnownedCall` fallback (the cluster CapabilityRegistry in
|
|
7489
|
-
* production). Throws if called before `start()`.
|
|
8429
|
+
* Inner dispatch: may throw CapRouteError (validation failures) or arbitrary
|
|
8430
|
+
* transport errors. The outer `dispatch` wraps non-CapRouteErrors.
|
|
7490
8431
|
*/
|
|
7491
|
-
async
|
|
7492
|
-
|
|
7493
|
-
|
|
7494
|
-
|
|
7495
|
-
|
|
7496
|
-
|
|
7497
|
-
|
|
7498
|
-
|
|
7499
|
-
|
|
7500
|
-
|
|
8432
|
+
async dispatchInner(route, method, args, addonId, callerAddonId) {
|
|
8433
|
+
switch (route.kind) {
|
|
8434
|
+
case "hub-in-process": return route.ref.invoke(method, withCallerAddonId(args, callerAddonId));
|
|
8435
|
+
case "hub-local-uds": {
|
|
8436
|
+
const registry = this.hubLocalRegistry;
|
|
8437
|
+
if (registry === null) throw new CapRouteError(route.capName, method, {
|
|
8438
|
+
reason: "no-provider",
|
|
8439
|
+
rejected: [{
|
|
8440
|
+
kind: "hub-local-uds",
|
|
8441
|
+
why: "UDS registry not available"
|
|
8442
|
+
}]
|
|
8443
|
+
});
|
|
8444
|
+
const deviceId = extractDeviceId(args);
|
|
8445
|
+
const input = {
|
|
8446
|
+
capName: route.capName,
|
|
8447
|
+
method,
|
|
8448
|
+
args,
|
|
8449
|
+
...deviceId !== void 0 ? { deviceId } : {}
|
|
8450
|
+
};
|
|
8451
|
+
return registry.callCapOnChild(route.childId, input);
|
|
8452
|
+
}
|
|
8453
|
+
case "remote-moleculer": {
|
|
8454
|
+
const addonId = this.nodeAuthority.getAddonId(route.nodeId, route.capName);
|
|
8455
|
+
if (addonId === null) throw new CapRouteError(route.capName, method, {
|
|
8456
|
+
reason: "no-provider",
|
|
8457
|
+
nodeId: route.nodeId,
|
|
8458
|
+
rejected: [{
|
|
8459
|
+
kind: "remote-moleculer",
|
|
8460
|
+
why: `no addonId known for ${route.nodeId}/${route.capName}`
|
|
8461
|
+
}]
|
|
8462
|
+
});
|
|
8463
|
+
const deviceId = extractDeviceId(args);
|
|
8464
|
+
const isNative = this.nodeAuthority.isNativeCap(route.nodeId, route.capName, deviceId);
|
|
8465
|
+
const action = capActionName(addonId, route.capName, method, isNative);
|
|
8466
|
+
return callWithServiceDiscovery(this.broker, addonId, action, args, {
|
|
8467
|
+
nodeID: route.nodeId,
|
|
8468
|
+
timeout: this.callTimeout(route.capName, method)
|
|
8469
|
+
});
|
|
8470
|
+
}
|
|
8471
|
+
case "agent-child-forward": {
|
|
8472
|
+
const deviceId = extractDeviceId(args);
|
|
8473
|
+
const params = {
|
|
8474
|
+
capName: route.capName,
|
|
8475
|
+
method,
|
|
8476
|
+
args,
|
|
8477
|
+
...route.childId !== void 0 ? { childId: route.childId } : {},
|
|
8478
|
+
...deviceId !== void 0 ? { deviceId } : {},
|
|
8479
|
+
...addonId !== void 0 ? { addonId } : {}
|
|
8480
|
+
};
|
|
8481
|
+
return callWithServiceDiscovery(this.broker, AGENT_CAP_FWD_SERVICE, AGENT_CAP_FWD_ACTION, params, {
|
|
8482
|
+
nodeID: route.agentNodeId,
|
|
8483
|
+
timeout: this.callTimeout(route.capName, method)
|
|
8484
|
+
});
|
|
8485
|
+
}
|
|
8486
|
+
}
|
|
8487
|
+
}
|
|
8488
|
+
buildSnapshot() {
|
|
8489
|
+
const hubLocalRegistry = this.hubLocalRegistry;
|
|
8490
|
+
const nodeAuthority = this.nodeAuthority;
|
|
8491
|
+
const inProcessProviders = this.inProcessProviders;
|
|
8492
|
+
const hubResidentProviders = this.hubResidentProviders;
|
|
8493
|
+
return {
|
|
8494
|
+
hubNodeId: this.hubNodeId,
|
|
8495
|
+
hubInProcessProvides: (cap) => inProcessProviders(cap) !== null,
|
|
8496
|
+
...hubResidentProviders !== void 0 ? {
|
|
8497
|
+
hubResidentProvides: (cap) => hubResidentProviders(cap) !== null,
|
|
8498
|
+
getHubResidentProviderRef: (cap) => hubResidentProviders(cap)
|
|
8499
|
+
} : {},
|
|
8500
|
+
hubLocalChildProvides: (cap, deviceId) => hubLocalRegistry !== null && hubLocalRegistry.resolveChildId(cap, deviceId) !== null,
|
|
8501
|
+
nodeKnowsCap: (nodeId, cap) => nodeAuthority.nodeKnowsCap(nodeId, cap),
|
|
8502
|
+
nodeIsAgent: (nodeId) => nodeAuthority.nodeIsAgent(nodeId),
|
|
8503
|
+
nodeOnline: (nodeId) => nodeAuthority.nodeOnline(nodeId),
|
|
8504
|
+
listKnownNodeIds: () => nodeAuthority.listNodeIds(),
|
|
8505
|
+
getInProcessProviderRef: (cap) => inProcessProviders(cap),
|
|
8506
|
+
getHubLocalChildId: (cap, deviceId) => hubLocalRegistry !== null ? hubLocalRegistry.resolveChildId(cap, deviceId) : null,
|
|
8507
|
+
getAgentChildId: (agentNodeId, cap) => nodeAuthority.getAgentChildId(agentNodeId, cap)
|
|
7501
8508
|
};
|
|
7502
|
-
return this.channel.request(msg);
|
|
7503
8509
|
}
|
|
7504
|
-
/**
|
|
7505
|
-
|
|
7506
|
-
|
|
7507
|
-
|
|
7508
|
-
|
|
8510
|
+
/** Extract a nodeId string from a route for error reporting, or undefined. */
|
|
8511
|
+
routeNodeId(route) {
|
|
8512
|
+
switch (route.kind) {
|
|
8513
|
+
case "hub-in-process": return this.hubNodeId;
|
|
8514
|
+
case "hub-local-uds": return `${this.hubNodeId}/${route.childId}`;
|
|
8515
|
+
case "remote-moleculer": return route.nodeId;
|
|
8516
|
+
case "agent-child-forward": return route.agentNodeId;
|
|
8517
|
+
}
|
|
7509
8518
|
}
|
|
7510
8519
|
};
|
|
7511
8520
|
//#endregion
|
|
@@ -10219,6 +11228,18 @@ Object.defineProperty(exports, "AddonDepsManager", {
|
|
|
10219
11228
|
return AddonDepsManager;
|
|
10220
11229
|
}
|
|
10221
11230
|
});
|
|
11231
|
+
Object.defineProperty(exports, "CAP_ACTION_OVERFLOW_LABEL", {
|
|
11232
|
+
enumerable: true,
|
|
11233
|
+
get: function() {
|
|
11234
|
+
return CAP_ACTION_OVERFLOW_LABEL;
|
|
11235
|
+
}
|
|
11236
|
+
});
|
|
11237
|
+
Object.defineProperty(exports, "CAP_ACTION_UNLABELLED", {
|
|
11238
|
+
enumerable: true,
|
|
11239
|
+
get: function() {
|
|
11240
|
+
return CAP_ACTION_UNLABELLED;
|
|
11241
|
+
}
|
|
11242
|
+
});
|
|
10222
11243
|
Object.defineProperty(exports, "CapRouteError", {
|
|
10223
11244
|
enumerable: true,
|
|
10224
11245
|
get: function() {
|
|
@@ -10261,6 +11282,12 @@ Object.defineProperty(exports, "DeviceRegistry", {
|
|
|
10261
11282
|
return DeviceRegistry;
|
|
10262
11283
|
}
|
|
10263
11284
|
});
|
|
11285
|
+
Object.defineProperty(exports, "EMPTY_CAP_ACTION_READING", {
|
|
11286
|
+
enumerable: true,
|
|
11287
|
+
get: function() {
|
|
11288
|
+
return EMPTY_CAP_ACTION_READING;
|
|
11289
|
+
}
|
|
11290
|
+
});
|
|
10264
11291
|
Object.defineProperty(exports, "EMPTY_SOCKET_DIRECTION", {
|
|
10265
11292
|
enumerable: true,
|
|
10266
11293
|
get: function() {
|
|
@@ -10357,12 +11384,42 @@ Object.defineProperty(exports, "LocalChildRegistry", {
|
|
|
10357
11384
|
return LocalChildRegistry;
|
|
10358
11385
|
}
|
|
10359
11386
|
});
|
|
11387
|
+
Object.defineProperty(exports, "MAX_CAP_ACTIONS_PER_PEER", {
|
|
11388
|
+
enumerable: true,
|
|
11389
|
+
get: function() {
|
|
11390
|
+
return MAX_CAP_ACTIONS_PER_PEER;
|
|
11391
|
+
}
|
|
11392
|
+
});
|
|
11393
|
+
Object.defineProperty(exports, "MESH_QUEUE_TOP_N", {
|
|
11394
|
+
enumerable: true,
|
|
11395
|
+
get: function() {
|
|
11396
|
+
return MESH_QUEUE_TOP_N;
|
|
11397
|
+
}
|
|
11398
|
+
});
|
|
10360
11399
|
Object.defineProperty(exports, "NATIVE_PROVIDER_SERVICE_INFIX", {
|
|
10361
11400
|
enumerable: true,
|
|
10362
11401
|
get: function() {
|
|
10363
11402
|
return NATIVE_PROVIDER_SERVICE_INFIX;
|
|
10364
11403
|
}
|
|
10365
11404
|
});
|
|
11405
|
+
Object.defineProperty(exports, "OUTBOUND_QUEUE_COOLDOWN_MS", {
|
|
11406
|
+
enumerable: true,
|
|
11407
|
+
get: function() {
|
|
11408
|
+
return OUTBOUND_QUEUE_COOLDOWN_MS;
|
|
11409
|
+
}
|
|
11410
|
+
});
|
|
11411
|
+
Object.defineProperty(exports, "OUTBOUND_QUEUE_SUSTAIN_MS", {
|
|
11412
|
+
enumerable: true,
|
|
11413
|
+
get: function() {
|
|
11414
|
+
return OUTBOUND_QUEUE_SUSTAIN_MS;
|
|
11415
|
+
}
|
|
11416
|
+
});
|
|
11417
|
+
Object.defineProperty(exports, "OUTBOUND_QUEUE_THRESHOLD_BYTES", {
|
|
11418
|
+
enumerable: true,
|
|
11419
|
+
get: function() {
|
|
11420
|
+
return OUTBOUND_QUEUE_THRESHOLD_BYTES;
|
|
11421
|
+
}
|
|
11422
|
+
});
|
|
10366
11423
|
Object.defineProperty(exports, "PARENT_ROUTED_PROVIDER_ADDON_ID", {
|
|
10367
11424
|
enumerable: true,
|
|
10368
11425
|
get: function() {
|
|
@@ -10399,6 +11456,12 @@ Object.defineProperty(exports, "SOCKET_PLANE_TOP_N", {
|
|
|
10399
11456
|
return SOCKET_PLANE_TOP_N;
|
|
10400
11457
|
}
|
|
10401
11458
|
});
|
|
11459
|
+
Object.defineProperty(exports, "SOCKET_QUEUE_TOP_N", {
|
|
11460
|
+
enumerable: true,
|
|
11461
|
+
get: function() {
|
|
11462
|
+
return SOCKET_QUEUE_TOP_N;
|
|
11463
|
+
}
|
|
11464
|
+
});
|
|
10402
11465
|
Object.defineProperty(exports, "SocketChannel", {
|
|
10403
11466
|
enumerable: true,
|
|
10404
11467
|
get: function() {
|
|
@@ -10483,6 +11546,12 @@ Object.defineProperty(exports, "callWithServiceDiscovery", {
|
|
|
10483
11546
|
return callWithServiceDiscovery;
|
|
10484
11547
|
}
|
|
10485
11548
|
});
|
|
11549
|
+
Object.defineProperty(exports, "capActionLabel", {
|
|
11550
|
+
enumerable: true,
|
|
11551
|
+
get: function() {
|
|
11552
|
+
return capActionLabel;
|
|
11553
|
+
}
|
|
11554
|
+
});
|
|
10486
11555
|
Object.defineProperty(exports, "capActionName", {
|
|
10487
11556
|
enumerable: true,
|
|
10488
11557
|
get: function() {
|
|
@@ -10555,6 +11624,12 @@ Object.defineProperty(exports, "createBrokerDeviceManagerApi", {
|
|
|
10555
11624
|
return createBrokerDeviceManagerApi;
|
|
10556
11625
|
}
|
|
10557
11626
|
});
|
|
11627
|
+
Object.defineProperty(exports, "createCapActionCensus", {
|
|
11628
|
+
enumerable: true,
|
|
11629
|
+
get: function() {
|
|
11630
|
+
return createCapActionCensus;
|
|
11631
|
+
}
|
|
11632
|
+
});
|
|
10558
11633
|
Object.defineProperty(exports, "createDeferredHeapWatchSink", {
|
|
10559
11634
|
enumerable: true,
|
|
10560
11635
|
get: function() {
|
|
@@ -10597,6 +11672,18 @@ Object.defineProperty(exports, "createLoopDelayMeter", {
|
|
|
10597
11672
|
return createLoopDelayMeter;
|
|
10598
11673
|
}
|
|
10599
11674
|
});
|
|
11675
|
+
Object.defineProperty(exports, "createMeshQueueReader", {
|
|
11676
|
+
enumerable: true,
|
|
11677
|
+
get: function() {
|
|
11678
|
+
return createMeshQueueReader;
|
|
11679
|
+
}
|
|
11680
|
+
});
|
|
11681
|
+
Object.defineProperty(exports, "createOutboundQueueGate", {
|
|
11682
|
+
enumerable: true,
|
|
11683
|
+
get: function() {
|
|
11684
|
+
return createOutboundQueueGate;
|
|
11685
|
+
}
|
|
11686
|
+
});
|
|
10600
11687
|
Object.defineProperty(exports, "createParentUnownedCallHandler", {
|
|
10601
11688
|
enumerable: true,
|
|
10602
11689
|
get: function() {
|
|
@@ -10669,6 +11756,12 @@ Object.defineProperty(exports, "deserializeTypedArrays", {
|
|
|
10669
11756
|
return deserializeTypedArrays;
|
|
10670
11757
|
}
|
|
10671
11758
|
});
|
|
11759
|
+
Object.defineProperty(exports, "diffCapActionCensus", {
|
|
11760
|
+
enumerable: true,
|
|
11761
|
+
get: function() {
|
|
11762
|
+
return diffCapActionCensus;
|
|
11763
|
+
}
|
|
11764
|
+
});
|
|
10672
11765
|
Object.defineProperty(exports, "diffEventPlane", {
|
|
10673
11766
|
enumerable: true,
|
|
10674
11767
|
get: function() {
|
|
@@ -10711,6 +11804,18 @@ Object.defineProperty(exports, "formatHeapSpaces", {
|
|
|
10711
11804
|
return formatHeapSpaces;
|
|
10712
11805
|
}
|
|
10713
11806
|
});
|
|
11807
|
+
Object.defineProperty(exports, "formatMeshQueue", {
|
|
11808
|
+
enumerable: true,
|
|
11809
|
+
get: function() {
|
|
11810
|
+
return formatMeshQueue;
|
|
11811
|
+
}
|
|
11812
|
+
});
|
|
11813
|
+
Object.defineProperty(exports, "formatOutboundQueueVerdict", {
|
|
11814
|
+
enumerable: true,
|
|
11815
|
+
get: function() {
|
|
11816
|
+
return formatOutboundQueueVerdict;
|
|
11817
|
+
}
|
|
11818
|
+
});
|
|
10714
11819
|
Object.defineProperty(exports, "formatSocketPlane", {
|
|
10715
11820
|
enumerable: true,
|
|
10716
11821
|
get: function() {
|
|
@@ -10855,6 +11960,12 @@ Object.defineProperty(exports, "readMoleculerFanoutMode", {
|
|
|
10855
11960
|
return readMoleculerFanoutMode;
|
|
10856
11961
|
}
|
|
10857
11962
|
});
|
|
11963
|
+
Object.defineProperty(exports, "readSocketQueues", {
|
|
11964
|
+
enumerable: true,
|
|
11965
|
+
get: function() {
|
|
11966
|
+
return readSocketQueues;
|
|
11967
|
+
}
|
|
11968
|
+
});
|
|
10858
11969
|
Object.defineProperty(exports, "reclaimIntervalMs", {
|
|
10859
11970
|
enumerable: true,
|
|
10860
11971
|
get: function() {
|