@camstack/system 1.2.313 → 1.2.315
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 +1 -1
- package/dist/addon-runner.mjs +1 -1
- 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/autotrack/index.js +1 -1
- package/dist/builtins/autotrack/index.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/camera-grid/index.js +1 -1
- package/dist/builtins/camera-grid/index.mjs +1 -1
- package/dist/builtins/composer/claim-gate.d.ts +32 -0
- package/dist/builtins/composer/composed-device-set.d.ts +38 -0
- package/dist/builtins/composer/composed-device.d.ts +4 -3
- package/dist/builtins/composer/composer-grafts.d.ts +54 -0
- package/dist/builtins/composer/composer-plan.d.ts +96 -0
- package/dist/builtins/composer/composer-verdict.d.ts +19 -0
- package/dist/builtins/composer/composer.addon.js +2717 -477
- package/dist/builtins/composer/composer.addon.mjs +2722 -482
- package/dist/builtins/composer/composer.d.ts +71 -52
- package/dist/builtins/composer/composition-runtime.d.ts +93 -9
- package/dist/builtins/composer/confirmed-seed.d.ts +32 -0
- package/dist/builtins/composer/existing-target.d.ts +96 -0
- package/dist/builtins/composer/field-record.d.ts +50 -0
- package/dist/builtins/composer/graft-host.d.ts +47 -0
- package/dist/builtins/composer/held-claims.d.ts +12 -0
- package/dist/builtins/composer/owned-fields-target.d.ts +55 -0
- package/dist/builtins/composer/slice-assembler.d.ts +10 -0
- package/dist/builtins/composer/source-readings.d.ts +122 -0
- package/dist/builtins/composer/source-tracker.d.ts +35 -42
- package/dist/builtins/console-logging/index.js +1 -1
- package/dist/builtins/console-logging/index.mjs +1 -1
- package/dist/builtins/core-blocks/composition-api.d.ts +7 -4
- package/dist/builtins/core-blocks/composition-peers.d.ts +9 -0
- package/dist/builtins/core-blocks/composition-sources.d.ts +16 -3
- package/dist/builtins/core-blocks/core-block-store.d.ts +8 -1
- package/dist/builtins/core-blocks/core-blocks.addon.d.ts +4 -3
- package/dist/builtins/core-blocks/core-blocks.addon.js +255 -79
- package/dist/builtins/core-blocks/core-blocks.addon.mjs +255 -79
- package/dist/builtins/device-manager/claimed-status-overlay.d.ts +12 -0
- package/dist/builtins/device-manager/device-manager.addon.d.ts +16 -0
- package/dist/builtins/device-manager/device-manager.addon.js +1825 -317
- package/dist/builtins/device-manager/device-manager.addon.mjs +1825 -317
- package/dist/builtins/device-manager/device-provider-context.d.ts +24 -2
- package/dist/builtins/device-manager/device-row-store.d.ts +2 -0
- package/dist/builtins/device-manager/device-state-claim-views.d.ts +77 -0
- package/dist/builtins/device-manager/device-state-claims.d.ts +43 -0
- package/dist/builtins/device-manager/device-state-mirror.d.ts +145 -61
- package/dist/builtins/device-manager/field-claims-index.d.ts +69 -0
- package/dist/builtins/device-manager/field-ownership.d.ts +53 -0
- package/dist/builtins/device-manager/migrate-device.d.ts +27 -0
- package/dist/builtins/device-manager/migrate-guard.d.ts +7 -0
- package/dist/builtins/device-manager/migrate-hardware-state.d.ts +55 -0
- package/dist/builtins/device-manager/migration-refused.d.ts +23 -0
- package/dist/builtins/device-manager/mirror-row-writer.d.ts +138 -0
- package/dist/builtins/device-manager/runtime-state-persist-gate.d.ts +4 -0
- package/dist/builtins/doorbell/binding-mirror.d.ts +1 -0
- package/dist/builtins/doorbell/doorbell-composition-migration.d.ts +59 -0
- package/dist/builtins/doorbell/virtual-doorbell.addon.d.ts +3 -0
- package/dist/builtins/doorbell/virtual-doorbell.addon.js +315 -10
- package/dist/builtins/doorbell/virtual-doorbell.addon.mjs +315 -10
- 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 +1 -1
- package/dist/builtins/local-network/local-network.addon.mjs +1 -1
- package/dist/builtins/loki-logging/index.js +1 -1
- package/dist/builtins/loki-logging/index.mjs +1 -1
- package/dist/builtins/native-metrics/native-metrics.addon.js +1 -1
- package/dist/builtins/native-metrics/native-metrics.addon.mjs +1 -1
- package/dist/builtins/platform-probe/index.js +1 -1
- package/dist/builtins/platform-probe/index.mjs +1 -1
- 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-settings.addon.js +0 -0
- package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +0 -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/{child-cap-dispatch-CHTiHbsv.mjs → child-cap-dispatch-AAltfBUH.mjs} +1484 -1417
- package/dist/{child-cap-dispatch-Dyn_7ezH.js → child-cap-dispatch-DzcUd_ct.js} +1503 -1424
- package/dist/composition-sources-DYqQLRzF.js +122 -0
- package/dist/composition-sources-yonsRneP.mjs +111 -0
- package/dist/{dist-CkRwnhbo.js → dist-DVKv5i-e.js} +5396 -4331
- package/dist/{dist-Cj7pJmXq.mjs → dist-MzYJVCLE.mjs} +5330 -4331
- package/dist/index.js +137 -8
- package/dist/index.mjs +133 -8
- package/dist/kernel/capability-registry.d.ts +43 -1
- package/dist/kernel/index.d.ts +3 -1
- package/dist/kernel/moleculer/device-cap-proxy.d.ts +0 -10
- package/dist/kernel/status-overlay.d.ts +18 -0
- package/dist/kernel/transport/claimed-get-status.d.ts +15 -0
- package/dist/kernel/transport/index.d.ts +2 -0
- package/dist/kernel/transport/local-child-registry.d.ts +23 -0
- package/dist/kernel/transport/parent-unowned-call.d.ts +25 -0
- package/dist/{retired-settings-keys-C1duPikd.mjs → retired-settings-keys-8yY2sTc-.mjs} +1 -1
- package/dist/{retired-settings-keys-CGSuXhP8.js → retired-settings-keys-iY5I0nKM.js} +1 -1
- package/package.json +1 -1
|
@@ -4634,31 +4634,6 @@ function buildNativeCapProxy(broker, addonId, capName, deviceId) {
|
|
|
4634
4634
|
};
|
|
4635
4635
|
} });
|
|
4636
4636
|
}
|
|
4637
|
-
/**
|
|
4638
|
-
* Hub-side proxy for a device-scoped native cap whose owning runner is a
|
|
4639
|
-
* HUB-LOCAL forked child reachable over UDS. Mirrors {@link buildNativeCapProxy}
|
|
4640
|
-
* but dispatches via `LocalChildRegistry.callCapOnChild` (UDS) instead of
|
|
4641
|
-
* `broker.call` — the child no longer hosts a Moleculer service after the UDS
|
|
4642
|
-
* cutover, so a broker call fails "is not found". The `deviceId` is merged into
|
|
4643
|
-
* the method args (provider convention) and carried as the routing field.
|
|
4644
|
-
*/
|
|
4645
|
-
function buildUdsNativeCapProxy(registry, childId, capName, deviceId) {
|
|
4646
|
-
return new Proxy({}, { get(_target, property) {
|
|
4647
|
-
if (typeof property !== "string") return void 0;
|
|
4648
|
-
return (input) => {
|
|
4649
|
-
const mergedInput = typeof input === "object" && input !== null ? {
|
|
4650
|
-
...input,
|
|
4651
|
-
deviceId
|
|
4652
|
-
} : { deviceId };
|
|
4653
|
-
return registry.callCapOnChild(childId, {
|
|
4654
|
-
capName,
|
|
4655
|
-
method: property,
|
|
4656
|
-
args: mergedInput,
|
|
4657
|
-
deviceId
|
|
4658
|
-
});
|
|
4659
|
-
};
|
|
4660
|
-
} });
|
|
4661
|
-
}
|
|
4662
4637
|
function buildDeviceOpsProvider(device) {
|
|
4663
4638
|
const assertMatch = (incoming) => {
|
|
4664
4639
|
if (incoming !== device.id) throw new Error(`device-ops: deviceId mismatch — expected '${device.id}', got '${incoming}'`);
|
|
@@ -4844,6 +4819,61 @@ function createBrokerDeviceManagerApi(opts) {
|
|
|
4844
4819
|
generation: nativeCapReadinessGeneration,
|
|
4845
4820
|
sourceNodeId: nodeId
|
|
4846
4821
|
});
|
|
4822
|
+
},
|
|
4823
|
+
unregisterNativeCap(capName) {
|
|
4824
|
+
const worker = opts.capabilityRegistry === void 0 && (opts.broker !== void 0 || opts.udsMode === true);
|
|
4825
|
+
if (!opts.capabilityRegistry && !worker) throw new Error(`DeviceContext.unregisterNativeCap('${capName}') called on '${stableId}' but none of capabilityRegistry / broker / udsMode was provided.`);
|
|
4826
|
+
const holder = opts.capabilityRegistry ? opts.capabilityRegistry.getNativeAddonId(capName, id) : workerNativeCapAddonIds.get(capName)?.get(id) ?? null;
|
|
4827
|
+
const staleRetract = worker && holder === null;
|
|
4828
|
+
if (holder !== addonId && !staleRetract) {
|
|
4829
|
+
opts.logger.debug("unregisterNativeCap: the cap is not registered by this addon — no-op", {
|
|
4830
|
+
tags: {
|
|
4831
|
+
deviceId: id,
|
|
4832
|
+
addonId
|
|
4833
|
+
},
|
|
4834
|
+
meta: { capName }
|
|
4835
|
+
});
|
|
4836
|
+
return;
|
|
4837
|
+
}
|
|
4838
|
+
if (staleRetract) opts.logger.info("unregisterNativeCap: retracting a binding this process never registered (a previous incarnation of the addon)", {
|
|
4839
|
+
tags: {
|
|
4840
|
+
deviceId: id,
|
|
4841
|
+
addonId
|
|
4842
|
+
},
|
|
4843
|
+
meta: { capName }
|
|
4844
|
+
});
|
|
4845
|
+
else if (opts.capabilityRegistry) opts.capabilityRegistry.unregisterNativeProvider(capName, id);
|
|
4846
|
+
else {
|
|
4847
|
+
workerNativeCaps.get(capName)?.delete(id);
|
|
4848
|
+
workerNativeCapAddonIds.get(capName)?.delete(id);
|
|
4849
|
+
onWorkerNativeCapsChanged?.();
|
|
4850
|
+
}
|
|
4851
|
+
eventBus.emit({
|
|
4852
|
+
id: (0, node_crypto.randomUUID)(),
|
|
4853
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
4854
|
+
source: {
|
|
4855
|
+
type: "device",
|
|
4856
|
+
id
|
|
4857
|
+
},
|
|
4858
|
+
category: _camstack_types_addon.EventCategory.DeviceBindingsChanged,
|
|
4859
|
+
data: {
|
|
4860
|
+
deviceId: id,
|
|
4861
|
+
capName,
|
|
4862
|
+
reason: "native-unregistered",
|
|
4863
|
+
addonId,
|
|
4864
|
+
nodeId
|
|
4865
|
+
}
|
|
4866
|
+
});
|
|
4867
|
+
(0, _camstack_types_addon.emitReadiness)(eventBus, {
|
|
4868
|
+
capName,
|
|
4869
|
+
scope: {
|
|
4870
|
+
type: "device",
|
|
4871
|
+
deviceId: id
|
|
4872
|
+
},
|
|
4873
|
+
state: "down",
|
|
4874
|
+
generation: nativeCapReadinessGeneration,
|
|
4875
|
+
sourceNodeId: nodeId
|
|
4876
|
+
});
|
|
4847
4877
|
}
|
|
4848
4878
|
};
|
|
4849
4879
|
};
|
|
@@ -7984,1536 +8014,1566 @@ function liftRoutingHints(args) {
|
|
|
7984
8014
|
...typeof deviceId === "number" && Number.isFinite(deviceId) ? { deviceId } : {}
|
|
7985
8015
|
};
|
|
7986
8016
|
}
|
|
7987
|
-
|
|
7988
|
-
|
|
7989
|
-
|
|
7990
|
-
|
|
8017
|
+
//#endregion
|
|
8018
|
+
//#region src/kernel/moleculer/resilient-cap-call.ts
|
|
8019
|
+
/** Moleculer error `type` values meaning "the service is not (yet) routable". */
|
|
8020
|
+
var DISCOVERY_ERROR_TYPES = new Set(["SERVICE_NOT_FOUND", "SERVICE_NOT_AVAILABLE"]);
|
|
8021
|
+
/** Default ceiling for the discovery wait — fail-fast past this. */
|
|
8022
|
+
var DEFAULT_DISCOVERY_TIMEOUT_MS$1 = 3e4;
|
|
8023
|
+
function isDiscoveryError(err) {
|
|
8024
|
+
if (typeof err !== "object" || err === null) return false;
|
|
8025
|
+
const type = err.type;
|
|
8026
|
+
return typeof type === "string" && DISCOVERY_ERROR_TYPES.has(type);
|
|
7991
8027
|
}
|
|
7992
8028
|
/**
|
|
7993
|
-
*
|
|
7994
|
-
*
|
|
7995
|
-
* prefix to distinguish routing failures (safe to retry via broker) from real
|
|
7996
|
-
* provider errors (must NOT retry to avoid double-executing side effects).
|
|
7997
|
-
*/
|
|
7998
|
-
var UDS_NO_ROUTE_PREFIX = "UDS_NO_ROUTE";
|
|
7999
|
-
/**
|
|
8000
|
-
* Parent-side authority for local addon-runners reachable over a
|
|
8001
|
-
* `LocalTransportServer` (UDS). Children register their cap manifest on
|
|
8002
|
-
* connect; the registry routes `(capName, deviceId?)` cap calls to the
|
|
8003
|
-
* owning child over the channel and drops a child's caps on disconnect.
|
|
8029
|
+
* Call a Moleculer action; on a service-discovery error, wait for the
|
|
8030
|
+
* named service to be discovered and retry the call exactly once.
|
|
8004
8031
|
*/
|
|
8005
|
-
|
|
8006
|
-
|
|
8007
|
-
|
|
8008
|
-
|
|
8009
|
-
|
|
8010
|
-
|
|
8011
|
-
|
|
8012
|
-
logHandler = null;
|
|
8013
|
-
readinessHandler = null;
|
|
8014
|
-
server;
|
|
8015
|
-
onUnownedCall;
|
|
8016
|
-
logger;
|
|
8017
|
-
getActiveSingletonAddonId;
|
|
8018
|
-
resolveChildIdForAddon;
|
|
8019
|
-
/** This node's own Moleculer nodeId (see {@link LocalChildRegistryOptions.ownNodeId}). */
|
|
8020
|
-
ownNodeId;
|
|
8021
|
-
/** See {@link LocalChildRegistryOptions.isAggregatedCollectionMethod}. */
|
|
8022
|
-
isAggregatedCollectionMethod;
|
|
8023
|
-
isAddonPinnedCall;
|
|
8024
|
-
/** See {@link ForwardCensus}. Mutable counters on the hot path, by design. */
|
|
8025
|
-
forwardedRaw = 0;
|
|
8026
|
-
forwardedDecoded = 0;
|
|
8027
|
-
/**
|
|
8028
|
-
* The per-ACTION byte census (D456). `ForwardCensus` above is the same
|
|
8029
|
-
* question asked of the whole plane; this one asks it per `<cap>.<method>`,
|
|
8030
|
-
* beside the bytes that method actually moved. Both are kept: `socketFwd=`
|
|
8031
|
-
* says whether the negotiation is working at all, and a `raw=0` on ONE
|
|
8032
|
-
* method with `raw>0` on its neighbours is a different fault.
|
|
8033
|
-
*/
|
|
8034
|
-
actionCensus = createCapActionCensus();
|
|
8035
|
-
/** See {@link LocalChildRegistryOptions.capTimeoutMs}. */
|
|
8036
|
-
capTimeoutMs;
|
|
8037
|
-
/** See {@link LocalChildRegistryOptions.capUsageObserver}. */
|
|
8038
|
-
capUsageObserver;
|
|
8039
|
-
/** Tracks capNames already logged as UDS-routed; one INFO line per capName per process. */
|
|
8040
|
-
egressRoutedCaps = /* @__PURE__ */ new Set();
|
|
8041
|
-
/** Active event fan-out mode, read once from `CAMSTACK_UDS_EVENT_FANOUT`. */
|
|
8042
|
-
fanoutMode = readFanoutMode();
|
|
8043
|
-
/** Per-child event fan-out counters (sent / suppressed). */
|
|
8044
|
-
childEventStats = /* @__PURE__ */ new Map();
|
|
8045
|
-
/** D467: per-category production census (cumulative). See {@link CategoryFanoutSample}. */
|
|
8046
|
-
categoryStats = /* @__PURE__ */ new Map();
|
|
8047
|
-
/** Broadcasts whose category did not fit in {@link CATEGORY_CENSUS_MAX}. */
|
|
8048
|
-
categoryOverflow = 0;
|
|
8049
|
-
/** One INFO line the first time the census cap trips, never per event. */
|
|
8050
|
-
categoryOverflowLogged = false;
|
|
8051
|
-
/** Last pattern-set string logged per child, to dedup the INFO line. */
|
|
8052
|
-
loggedPatternSet = /* @__PURE__ */ new Map();
|
|
8053
|
-
/**
|
|
8054
|
-
* Derived `(cap, device?) → child` index over {@link children}, or `null`
|
|
8055
|
-
* when it must be rebuilt. See {@link capIndex} for why it is a cache and
|
|
8056
|
-
* never a registry (D3, D457).
|
|
8057
|
-
*/
|
|
8058
|
-
capChildIndex = null;
|
|
8059
|
-
/**
|
|
8060
|
-
* Accepts either a plain positional `server` argument (backward-compatible)
|
|
8061
|
-
* or a full `LocalChildRegistryOptions` object.
|
|
8062
|
-
*
|
|
8063
|
-
* Positional overloads (existing call sites are unchanged):
|
|
8064
|
-
* new LocalChildRegistry(server)
|
|
8065
|
-
* new LocalChildRegistry(server, onUnownedCall)
|
|
8066
|
-
*
|
|
8067
|
-
* Options object (new call sites that pass a logger):
|
|
8068
|
-
* new LocalChildRegistry({ server, onUnownedCall, logger })
|
|
8069
|
-
*/
|
|
8070
|
-
constructor(serverOrOptions, onUnownedCallArg) {
|
|
8071
|
-
if (serverOrOptions && !("listen" in serverOrOptions)) {
|
|
8072
|
-
const opts = serverOrOptions;
|
|
8073
|
-
this.server = opts.server;
|
|
8074
|
-
this.onUnownedCall = opts.onUnownedCall;
|
|
8075
|
-
this.logger = opts.logger;
|
|
8076
|
-
this.getActiveSingletonAddonId = opts.getActiveSingletonAddonId;
|
|
8077
|
-
this.resolveChildIdForAddon = opts.resolveChildIdForAddon;
|
|
8078
|
-
this.ownNodeId = opts.ownNodeId;
|
|
8079
|
-
this.isAggregatedCollectionMethod = opts.isAggregatedCollectionMethod;
|
|
8080
|
-
this.isAddonPinnedCall = opts.isAddonPinnedCall;
|
|
8081
|
-
this.capTimeoutMs = opts.capTimeoutMs;
|
|
8082
|
-
this.capUsageObserver = opts.capUsageObserver;
|
|
8083
|
-
} else {
|
|
8084
|
-
this.server = serverOrOptions;
|
|
8085
|
-
this.onUnownedCall = onUnownedCallArg;
|
|
8086
|
-
}
|
|
8087
|
-
}
|
|
8088
|
-
async start() {
|
|
8089
|
-
this.logger?.info("UDS event fan-out mode", { mode: this.fanoutMode });
|
|
8090
|
-
this.server.onConnection((channel) => this.onConnection(channel));
|
|
8091
|
-
await this.server.listen();
|
|
8092
|
-
}
|
|
8093
|
-
/**
|
|
8094
|
-
* Per-child event fan-out counters (`{ sent, suppressed }`) or `null` if the
|
|
8095
|
-
* child has no recorded events yet. Exposed for the starvation-verification
|
|
8096
|
-
* surface (shadow burn-in + operator debug).
|
|
8097
|
-
*/
|
|
8098
|
-
getChildEventStats(childId) {
|
|
8099
|
-
const s = this.childEventStats.get(childId);
|
|
8100
|
-
return s === void 0 ? null : {
|
|
8101
|
-
sent: s.sent,
|
|
8102
|
-
suppressed: s.suppressed
|
|
8103
|
-
};
|
|
8104
|
-
}
|
|
8105
|
-
/**
|
|
8106
|
-
* D467: every category this node has broadcast, with its emit count and how
|
|
8107
|
-
* many child subscriptions matched. Cumulative for the life of the process —
|
|
8108
|
-
* the heartbeat differences it, the same discipline as the per-child
|
|
8109
|
-
* counters. Largest `broadcasts` first.
|
|
8110
|
-
*/
|
|
8111
|
-
categoryFanout() {
|
|
8112
|
-
const out = [];
|
|
8113
|
-
for (const [category, s] of this.categoryStats) out.push({
|
|
8114
|
-
category,
|
|
8115
|
-
broadcasts: s.broadcasts,
|
|
8116
|
-
wanted: s.wanted
|
|
8117
|
-
});
|
|
8118
|
-
return out.toSorted((a, b) => b.broadcasts - a.broadcasts || a.category.localeCompare(b.category));
|
|
8119
|
-
}
|
|
8120
|
-
/** Broadcasts the census could not attribute because it was full (cumulative). */
|
|
8121
|
-
categoryCensusOverflow() {
|
|
8122
|
-
return this.categoryOverflow;
|
|
8123
|
-
}
|
|
8124
|
-
/**
|
|
8125
|
-
* This child's socket traffic — bytes and messages, both directions, split by
|
|
8126
|
-
* frame kind — or `null` when the child is gone or its channel keeps no
|
|
8127
|
-
* counters (an in-process or test transport).
|
|
8128
|
-
*
|
|
8129
|
-
* The event counters above cover ONE kind of frame and, once surfaced on
|
|
8130
|
-
* 2026-08-29, accounted for 4% of hub-main's ~44 000 socket syscalls/s. This
|
|
8131
|
-
* is the same question asked of every frame the channel moves, and it is the
|
|
8132
|
-
* only per-peer attribution that exists: `/proc` reports one `rchar`/`wchar`
|
|
8133
|
-
* pair for the whole process and has no per-socket breakdown.
|
|
8134
|
-
*/
|
|
8135
|
-
getChildSocketTraffic(childId) {
|
|
8136
|
-
const entry = this.children.get(childId);
|
|
8137
|
-
if (entry === void 0) return null;
|
|
8138
|
-
return entry.channel.readTraffic?.() ?? null;
|
|
8032
|
+
async function callWithServiceDiscovery(broker, serviceName, action, params, opts, discoveryTimeoutMs = DEFAULT_DISCOVERY_TIMEOUT_MS$1) {
|
|
8033
|
+
try {
|
|
8034
|
+
return await broker.call(action, params, opts);
|
|
8035
|
+
} catch (err) {
|
|
8036
|
+
if (!isDiscoveryError(err)) throw err;
|
|
8037
|
+
await broker.waitForServices([serviceName], discoveryTimeoutMs);
|
|
8038
|
+
return await broker.call(action, params, opts);
|
|
8139
8039
|
}
|
|
8040
|
+
}
|
|
8041
|
+
//#endregion
|
|
8042
|
+
//#region src/kernel/transport/cap-route.ts
|
|
8043
|
+
function buildMessage(capName, method, detail) {
|
|
8044
|
+
const call = method !== void 0 ? `${capName}.${method}` : capName;
|
|
8045
|
+
const target = detail.nodeId !== void 0 ? ` to node ${detail.nodeId}` : "";
|
|
8046
|
+
const rejectedStr = detail.rejected.map((r) => `${r.kind}=${r.why}`).join("; ");
|
|
8047
|
+
const rejectedClause = rejectedStr.length > 0 ? ` (rejected: ${rejectedStr})` : "";
|
|
8048
|
+
return `${call} not routable${target}: ${detail.reason}${rejectedClause}`;
|
|
8049
|
+
}
|
|
8050
|
+
var CapRouteError = class extends Error {
|
|
8051
|
+
reason;
|
|
8052
|
+
nodeId;
|
|
8053
|
+
rejected;
|
|
8140
8054
|
/**
|
|
8141
|
-
*
|
|
8142
|
-
*
|
|
8143
|
-
*
|
|
8144
|
-
* of an outbound-queue episode, and the number lives on the channel.
|
|
8055
|
+
* @param cause Optional original error that triggered this routing failure.
|
|
8056
|
+
* Stored as `Error.cause` (TC39 standard option, Node 16.9+).
|
|
8057
|
+
* Dispatchers wrapping transport errors MUST pass the original.
|
|
8145
8058
|
*/
|
|
8146
|
-
|
|
8147
|
-
|
|
8148
|
-
|
|
8149
|
-
|
|
8059
|
+
constructor(capName, method, detail, cause) {
|
|
8060
|
+
super(buildMessage(capName, method, detail), cause !== void 0 ? { cause } : void 0);
|
|
8061
|
+
this.name = "CapRouteError";
|
|
8062
|
+
this.reason = detail.reason;
|
|
8063
|
+
this.nodeId = detail.nodeId;
|
|
8064
|
+
this.rejected = detail.rejected;
|
|
8150
8065
|
}
|
|
8151
|
-
|
|
8152
|
-
|
|
8153
|
-
|
|
8154
|
-
|
|
8155
|
-
|
|
8156
|
-
|
|
8157
|
-
|
|
8158
|
-
|
|
8159
|
-
|
|
8160
|
-
|
|
8161
|
-
|
|
8162
|
-
|
|
8163
|
-
|
|
8164
|
-
|
|
8165
|
-
|
|
8166
|
-
|
|
8167
|
-
|
|
8168
|
-
|
|
8169
|
-
|
|
8170
|
-
|
|
8171
|
-
|
|
8172
|
-
|
|
8173
|
-
|
|
8174
|
-
|
|
8175
|
-
|
|
8176
|
-
|
|
8177
|
-
|
|
8178
|
-
|
|
8179
|
-
|
|
8180
|
-
|
|
8181
|
-
|
|
8182
|
-
|
|
8183
|
-
|
|
8184
|
-
|
|
8185
|
-
|
|
8186
|
-
|
|
8187
|
-
|
|
8188
|
-
|
|
8189
|
-
|
|
8190
|
-
|
|
8191
|
-
|
|
8192
|
-
|
|
8193
|
-
|
|
8194
|
-
|
|
8195
|
-
|
|
8196
|
-
undeclaredChildCount() {
|
|
8197
|
-
let count = 0;
|
|
8198
|
-
for (const entry of this.children.values()) if (entry.eventPatterns === null) count += 1;
|
|
8199
|
-
return count;
|
|
8200
|
-
}
|
|
8201
|
-
/**
|
|
8202
|
-
* Does `entry` want to receive an event in `category`? Honours the fan-out
|
|
8203
|
-
* mode: `broadcast` → always; undeclared patterns (`null`) → always
|
|
8204
|
-
* (fail-open); else the child's declared set must match via `matchesPattern`
|
|
8205
|
-
* — the SAME function the child's own `deliverShared` map-key gate uses,
|
|
8206
|
-
* giving provable delivery parity. `shadow` is handled by the caller (it
|
|
8207
|
-
* counts the would-suppress but still sends).
|
|
8208
|
-
*
|
|
8209
|
-
* NOTE: this gate must NOT consult `RING_BUFFER_DENY_PATTERNS` /
|
|
8210
|
-
* `isHighFrequencyCategory` — those are ring-buffer STORAGE policy; a child
|
|
8211
|
-
* that explicitly subscribes to a ring-denied category must still RECEIVE it.
|
|
8212
|
-
*/
|
|
8213
|
-
childWantsEvent(entry, category) {
|
|
8214
|
-
if (this.fanoutMode === "broadcast") return true;
|
|
8215
|
-
return this.childSubscribesTo(entry, category);
|
|
8216
|
-
}
|
|
8217
|
-
/**
|
|
8218
|
-
* The SUBSCRIPTION answer alone, with no fan-out-mode override.
|
|
8219
|
-
*
|
|
8220
|
-
* {@link childWantsEvent} answers "will this child be sent the event", which
|
|
8221
|
-
* under `broadcast` is `true` for everything and therefore cannot say whether
|
|
8222
|
-
* anyone actually asked. The D467 census needs the second question, so the
|
|
8223
|
-
* two are separate functions and `broadcast` mode overrides only the first.
|
|
8224
|
-
*/
|
|
8225
|
-
childSubscribesTo(entry, category) {
|
|
8226
|
-
if (entry.eventPatterns === null) return true;
|
|
8227
|
-
return entry.eventPatterns.some((p) => matchesPattern(p, category));
|
|
8228
|
-
}
|
|
8229
|
-
/**
|
|
8230
|
-
* D2: this node's aggregate cross-node event interest — the UNION of every
|
|
8231
|
-
* connected child's declared category patterns. Consumed by the parent's
|
|
8232
|
-
* `$event-bus` inbound gate (via `setNodeEventInterest`) so the node only
|
|
8233
|
-
* accepts cross-node (Moleculer) categories at least one local child wants.
|
|
8234
|
-
*
|
|
8235
|
-
* Returns `null` (fail-open — accept everything) when:
|
|
8236
|
-
* - NO child is currently connected (boot window before the first forked
|
|
8237
|
-
* child completes its UDS handshake, or every child has disconnected): a
|
|
8238
|
-
* node MUST NOT drop 100% of inbound cross-node events during that window,
|
|
8239
|
-
* and a child may connect imminently. This is distinct from the
|
|
8240
|
-
* declared-empty case below.
|
|
8241
|
-
* - ANY connected child is UNDECLARED (`eventPatterns === null`, a legacy
|
|
8242
|
-
* runner): a node cannot safely narrow its inbound set while a child's
|
|
8243
|
-
* real interest is unknown. Mirrors the per-child fail-open in
|
|
8244
|
-
* {@link childWantsEvent}, aggregated conservatively.
|
|
8245
|
-
*
|
|
8246
|
-
* Returns an EMPTY array only when ≥1 child is connected and EVERY connected
|
|
8247
|
-
* child declared an explicit empty set — a genuine "wants nothing". Reads the
|
|
8248
|
-
* live `children` map, so a child registering / disconnecting / sending
|
|
8249
|
-
* `event-sub` is reflected on the next call with no extra bookkeeping.
|
|
8250
|
-
*/
|
|
8251
|
-
aggregateEventInterest() {
|
|
8252
|
-
if (this.children.size === 0) return null;
|
|
8253
|
-
const union = /* @__PURE__ */ new Set();
|
|
8254
|
-
for (const entry of this.children.values()) {
|
|
8255
|
-
if (entry.eventPatterns === null) return null;
|
|
8256
|
-
for (const p of entry.eventPatterns) union.add(p);
|
|
8066
|
+
};
|
|
8067
|
+
/**
|
|
8068
|
+
* Classifies a (capName, opts) pair into a typed CapRoute dispatch descriptor.
|
|
8069
|
+
*
|
|
8070
|
+
* Precedence (explicit nodeId path):
|
|
8071
|
+
* 1. hub-in-process — hub node + hubInProcessProvides
|
|
8072
|
+
* 2. hub-local-uds — hub node + hubLocalChildProvides
|
|
8073
|
+
* 3. node-offline → CapRouteError{reason:'node-offline'}
|
|
8074
|
+
* 4. agent-child-forward — node is an agent AND nodeKnowsCap
|
|
8075
|
+
* 5. remote-moleculer — any other online non-agent node
|
|
8076
|
+
*
|
|
8077
|
+
* Singleton path (no nodeId):
|
|
8078
|
+
* 1. hub-in-process (hub provides in-process)
|
|
8079
|
+
* 2. hub-local-uds (a hub-local UDS child provides it)
|
|
8080
|
+
* 3. remote-moleculer (any online, non-hub node that knows it)
|
|
8081
|
+
* 4. → CapRouteError{reason:'no-provider', rejected: all considered routes}
|
|
8082
|
+
*
|
|
8083
|
+
* PURE: no side effects, no async, no broker/registry imports.
|
|
8084
|
+
*/
|
|
8085
|
+
function classifyCapRoute(capName, opts, snapshot) {
|
|
8086
|
+
const rejected = [];
|
|
8087
|
+
if (opts.nodeId !== void 0) return classifyExplicitNode(capName, opts.nodeId, opts.deviceId, snapshot, rejected);
|
|
8088
|
+
return classifySingleton(capName, opts.deviceId, snapshot, rejected);
|
|
8089
|
+
}
|
|
8090
|
+
function classifyExplicitNode(capName, nodeId, deviceId, snap, rejected) {
|
|
8091
|
+
if (nodeId === snap.hubNodeId) {
|
|
8092
|
+
const residentProvides = snap.hubResidentProvides;
|
|
8093
|
+
const providesOnHub = residentProvides ?? snap.hubInProcessProvides;
|
|
8094
|
+
const getRef = residentProvides !== void 0 ? snap.getHubResidentProviderRef : snap.getInProcessProviderRef;
|
|
8095
|
+
if (providesOnHub(capName)) {
|
|
8096
|
+
const ref = getRef?.(capName) ?? null;
|
|
8097
|
+
if (ref !== null) return {
|
|
8098
|
+
kind: "hub-in-process",
|
|
8099
|
+
capName,
|
|
8100
|
+
ref
|
|
8101
|
+
};
|
|
8102
|
+
rejected.push({
|
|
8103
|
+
kind: "hub-in-process",
|
|
8104
|
+
why: "provider ref not available in snapshot"
|
|
8105
|
+
});
|
|
8106
|
+
throw new CapRouteError(capName, void 0, {
|
|
8107
|
+
reason: "no-provider",
|
|
8108
|
+
nodeId,
|
|
8109
|
+
rejected
|
|
8110
|
+
});
|
|
8257
8111
|
}
|
|
8258
|
-
|
|
8259
|
-
|
|
8260
|
-
|
|
8261
|
-
|
|
8262
|
-
|
|
8263
|
-
|
|
8264
|
-
sent: 0,
|
|
8265
|
-
suppressed: 0
|
|
8112
|
+
if (snap.hubLocalChildProvides(capName, deviceId)) {
|
|
8113
|
+
const childId = snap.getHubLocalChildId?.(capName, deviceId) ?? null;
|
|
8114
|
+
if (childId !== null) return {
|
|
8115
|
+
kind: "hub-local-uds",
|
|
8116
|
+
capName,
|
|
8117
|
+
childId
|
|
8266
8118
|
};
|
|
8267
|
-
|
|
8119
|
+
rejected.push({
|
|
8120
|
+
kind: "hub-local-uds",
|
|
8121
|
+
why: "child id not resolvable from snapshot"
|
|
8122
|
+
});
|
|
8123
|
+
throw new CapRouteError(capName, void 0, {
|
|
8124
|
+
reason: "no-provider",
|
|
8125
|
+
nodeId,
|
|
8126
|
+
rejected
|
|
8127
|
+
});
|
|
8268
8128
|
}
|
|
8269
|
-
|
|
8129
|
+
rejected.push({
|
|
8130
|
+
kind: "hub-in-process",
|
|
8131
|
+
why: "hub does not provide this cap in-process"
|
|
8132
|
+
});
|
|
8133
|
+
rejected.push({
|
|
8134
|
+
kind: "hub-local-uds",
|
|
8135
|
+
why: "no hub-local child provides this cap"
|
|
8136
|
+
});
|
|
8137
|
+
throw new CapRouteError(capName, void 0, {
|
|
8138
|
+
reason: "no-provider",
|
|
8139
|
+
nodeId,
|
|
8140
|
+
rejected
|
|
8141
|
+
});
|
|
8270
8142
|
}
|
|
8271
|
-
|
|
8272
|
-
|
|
8273
|
-
|
|
8274
|
-
|
|
8275
|
-
|
|
8276
|
-
|
|
8277
|
-
|
|
8278
|
-
|
|
8143
|
+
if (!snap.nodeOnline(nodeId)) {
|
|
8144
|
+
rejected.push({
|
|
8145
|
+
kind: "remote-moleculer",
|
|
8146
|
+
why: `node ${nodeId} is offline`
|
|
8147
|
+
});
|
|
8148
|
+
throw new CapRouteError(capName, void 0, {
|
|
8149
|
+
reason: "node-offline",
|
|
8150
|
+
nodeId,
|
|
8151
|
+
rejected
|
|
8279
8152
|
});
|
|
8280
8153
|
}
|
|
8281
|
-
|
|
8282
|
-
|
|
8283
|
-
|
|
8284
|
-
|
|
8285
|
-
|
|
8286
|
-
|
|
8287
|
-
|
|
8288
|
-
|
|
8289
|
-
|
|
8290
|
-
|
|
8291
|
-
|
|
8292
|
-
|
|
8293
|
-
|
|
8294
|
-
|
|
8295
|
-
|
|
8296
|
-
|
|
8297
|
-
resolveChildId(capName, deviceId) {
|
|
8298
|
-
const index = this.capIndex();
|
|
8299
|
-
if (deviceId !== void 0) {
|
|
8300
|
-
const deviceOwner = index.deviceOwner(capName, deviceId);
|
|
8301
|
-
if (deviceOwner !== null) return deviceOwner;
|
|
8302
|
-
}
|
|
8303
|
-
const candidates = index.singletonCandidates(capName);
|
|
8304
|
-
if (candidates.length === 0) return null;
|
|
8305
|
-
if (candidates.length === 1) return candidates[0];
|
|
8306
|
-
const preferredAddonId = this.getActiveSingletonAddonId?.(capName) ?? null;
|
|
8307
|
-
if (preferredAddonId !== null) {
|
|
8308
|
-
const preferredChildId = this.resolveChildIdForAddon?.(preferredAddonId) ?? preferredAddonId;
|
|
8309
|
-
if (candidates.includes(preferredChildId)) return preferredChildId;
|
|
8310
|
-
}
|
|
8311
|
-
return candidates[0];
|
|
8154
|
+
if (snap.nodeIsAgent(nodeId)) {
|
|
8155
|
+
if (snap.nodeKnowsCap(nodeId, capName)) return {
|
|
8156
|
+
kind: "agent-child-forward",
|
|
8157
|
+
capName,
|
|
8158
|
+
agentNodeId: nodeId,
|
|
8159
|
+
childId: snap.getAgentChildId?.(nodeId, capName) ?? void 0
|
|
8160
|
+
};
|
|
8161
|
+
rejected.push({
|
|
8162
|
+
kind: "agent-child-forward",
|
|
8163
|
+
why: `agent ${nodeId} does not know cap ${capName}`
|
|
8164
|
+
});
|
|
8165
|
+
throw new CapRouteError(capName, void 0, {
|
|
8166
|
+
reason: "no-provider",
|
|
8167
|
+
nodeId,
|
|
8168
|
+
rejected
|
|
8169
|
+
});
|
|
8312
8170
|
}
|
|
8313
|
-
|
|
8314
|
-
|
|
8315
|
-
|
|
8316
|
-
|
|
8317
|
-
|
|
8318
|
-
|
|
8319
|
-
|
|
8320
|
-
|
|
8321
|
-
|
|
8322
|
-
|
|
8323
|
-
|
|
8324
|
-
|
|
8325
|
-
|
|
8326
|
-
|
|
8327
|
-
|
|
8328
|
-
|
|
8329
|
-
|
|
8330
|
-
|
|
8331
|
-
|
|
8332
|
-
|
|
8333
|
-
|
|
8334
|
-
|
|
8171
|
+
return {
|
|
8172
|
+
kind: "remote-moleculer",
|
|
8173
|
+
capName,
|
|
8174
|
+
nodeId
|
|
8175
|
+
};
|
|
8176
|
+
}
|
|
8177
|
+
function classifySingleton(capName, deviceId, snap, rejected) {
|
|
8178
|
+
if (snap.hubInProcessProvides(capName)) {
|
|
8179
|
+
const ref = snap.getInProcessProviderRef?.(capName) ?? null;
|
|
8180
|
+
if (ref !== null) return {
|
|
8181
|
+
kind: "hub-in-process",
|
|
8182
|
+
capName,
|
|
8183
|
+
ref
|
|
8184
|
+
};
|
|
8185
|
+
rejected.push({
|
|
8186
|
+
kind: "hub-in-process",
|
|
8187
|
+
why: "provider ref not available in snapshot"
|
|
8188
|
+
});
|
|
8189
|
+
} else rejected.push({
|
|
8190
|
+
kind: "hub-in-process",
|
|
8191
|
+
why: "hub does not provide this cap in-process"
|
|
8192
|
+
});
|
|
8193
|
+
if (snap.hubLocalChildProvides(capName, deviceId)) {
|
|
8194
|
+
const childId = snap.getHubLocalChildId?.(capName, deviceId) ?? null;
|
|
8195
|
+
if (childId !== null) return {
|
|
8196
|
+
kind: "hub-local-uds",
|
|
8197
|
+
capName,
|
|
8198
|
+
childId
|
|
8199
|
+
};
|
|
8200
|
+
rejected.push({
|
|
8201
|
+
kind: "hub-local-uds",
|
|
8202
|
+
why: "child id not resolvable from snapshot"
|
|
8203
|
+
});
|
|
8204
|
+
} else rejected.push({
|
|
8205
|
+
kind: "hub-local-uds",
|
|
8206
|
+
why: "no hub-local child provides this cap"
|
|
8207
|
+
});
|
|
8208
|
+
const knownNodes = snap.listKnownNodeIds?.() ?? [];
|
|
8209
|
+
for (const nodeId of knownNodes) if (nodeId !== snap.hubNodeId && snap.nodeOnline(nodeId) && snap.nodeKnowsCap(nodeId, capName)) return {
|
|
8210
|
+
kind: "remote-moleculer",
|
|
8211
|
+
capName,
|
|
8212
|
+
nodeId
|
|
8213
|
+
};
|
|
8214
|
+
rejected.push({
|
|
8215
|
+
kind: "remote-moleculer",
|
|
8216
|
+
why: "no online remote node knows this cap"
|
|
8217
|
+
});
|
|
8218
|
+
throw new CapRouteError(capName, void 0, {
|
|
8219
|
+
reason: "no-provider",
|
|
8220
|
+
rejected
|
|
8221
|
+
});
|
|
8222
|
+
}
|
|
8223
|
+
//#endregion
|
|
8224
|
+
//#region src/kernel/transport/cap-route-resolver.ts
|
|
8225
|
+
/** The Moleculer service name that Task 6's agent registers. */
|
|
8226
|
+
var AGENT_CAP_FWD_SERVICE = "$agent-cap-fwd";
|
|
8227
|
+
/** The Moleculer action (service.action) for agent cap forwarding. */
|
|
8228
|
+
var AGENT_CAP_FWD_ACTION = `${AGENT_CAP_FWD_SERVICE}.forward`;
|
|
8229
|
+
/** Default timeout for remote Moleculer cap calls (ms). */
|
|
8230
|
+
var REMOTE_CALL_TIMEOUT_MS = 6e4;
|
|
8231
|
+
function extractDeviceId(args) {
|
|
8232
|
+
if (args === null || typeof args !== "object") return void 0;
|
|
8233
|
+
const raw = Reflect.get(args, "deviceId");
|
|
8234
|
+
return typeof raw === "number" ? raw : void 0;
|
|
8235
|
+
}
|
|
8236
|
+
/**
|
|
8237
|
+
* Extract an inline `nodeId` string from a cap call's args — the per-call node
|
|
8238
|
+
* pin a forked addon expresses by carrying `nodeId` in the input (the SAME way
|
|
8239
|
+
* device-scoped caps carry `deviceId`, and the way the generated cap-router on
|
|
8240
|
+
* the hub already honours an inline pin). Mirrors {@link extractDeviceId}.
|
|
8241
|
+
*
|
|
8242
|
+
* This is the routing source for hub→remote-node EXECUTION pinning (e.g. the
|
|
8243
|
+
* benchmark addon running a synthetic/decoder workload ON a chosen agent). The
|
|
8244
|
+
* out-of-band `nodePin(op.context)` path does NOT survive the forked addon →
|
|
8245
|
+
* hub UDS link chain, so `onUnownedCall` reads the inline pin instead. Provider
|
|
8246
|
+
* methods that don't declare `nodeId` simply ignore the extra field (they
|
|
8247
|
+
* destructure the fields they need); methods that DO declare it (e.g.
|
|
8248
|
+
* `getEngineProvisioning({nodeId})`) still receive it unchanged.
|
|
8249
|
+
*/
|
|
8250
|
+
function extractNodeId(args) {
|
|
8251
|
+
if (args === null || typeof args !== "object") return void 0;
|
|
8252
|
+
const raw = Reflect.get(args, "nodeId");
|
|
8253
|
+
return typeof raw === "string" && raw.length > 0 ? raw : void 0;
|
|
8254
|
+
}
|
|
8255
|
+
/**
|
|
8256
|
+
* Merge `callerAddonId` into `args` as an extra field — the SAME "provider
|
|
8257
|
+
* destructures what it needs and ignores the rest" convention
|
|
8258
|
+
* {@link extractNodeId} already relies on for the inline `nodeId` pin.
|
|
8259
|
+
* `undefined` (the overwhelmingly common case: no caller hint, or the census
|
|
8260
|
+
* consuming it is disarmed) returns `args` unchanged — no allocation. A
|
|
8261
|
+
* non-object `args` (or `null`/array) is returned unchanged too: there is
|
|
8262
|
+
* nowhere to attach an extra key without changing the payload's shape, and a
|
|
8263
|
+
* provider taking a bare value never reads `callerAddonId` off it anyway.
|
|
8264
|
+
*/
|
|
8265
|
+
function withCallerAddonId(args, callerAddonId) {
|
|
8266
|
+
if (callerAddonId === void 0) return args;
|
|
8267
|
+
if (args === null || typeof args !== "object" || Array.isArray(args)) return args;
|
|
8268
|
+
return {
|
|
8269
|
+
...args,
|
|
8270
|
+
callerAddonId
|
|
8271
|
+
};
|
|
8272
|
+
}
|
|
8273
|
+
var CapRouteResolver = class {
|
|
8274
|
+
hubNodeId;
|
|
8275
|
+
broker;
|
|
8276
|
+
hubLocalRegistry;
|
|
8277
|
+
nodeAuthority;
|
|
8278
|
+
inProcessProviders;
|
|
8279
|
+
hubResidentProviders;
|
|
8280
|
+
capTimeoutMs;
|
|
8281
|
+
snapshot;
|
|
8282
|
+
constructor(deps) {
|
|
8283
|
+
this.hubNodeId = deps.hubNodeId;
|
|
8284
|
+
this.broker = deps.broker;
|
|
8285
|
+
this.hubLocalRegistry = deps.hubLocalRegistry;
|
|
8286
|
+
this.nodeAuthority = deps.nodeAuthority;
|
|
8287
|
+
this.inProcessProviders = deps.inProcessProviders;
|
|
8288
|
+
this.hubResidentProviders = deps.hubResidentProviders;
|
|
8289
|
+
this.capTimeoutMs = deps.capTimeoutMs;
|
|
8290
|
+
this.snapshot = this.buildSnapshot();
|
|
8291
|
+
}
|
|
8292
|
+
/** Per-call RPC timeout: the cap method's declared override, else the default. */
|
|
8293
|
+
callTimeout(capName, method) {
|
|
8294
|
+
return this.capTimeoutMs?.(capName, method) ?? REMOTE_CALL_TIMEOUT_MS;
|
|
8295
|
+
}
|
|
8296
|
+
resolveCapRoute(capName, opts) {
|
|
8297
|
+
return classifyCapRoute(capName, opts, this.snapshot);
|
|
8335
8298
|
}
|
|
8336
8299
|
/**
|
|
8337
|
-
*
|
|
8338
|
-
*
|
|
8339
|
-
*
|
|
8340
|
-
* This replaced a linear walk of every child's cap manifest per
|
|
8341
|
-
* `cap-call-out`, measured at **7.1 % of hub-main's busy main thread**
|
|
8342
|
-
* across 39 children (D456). Why it did not exist before is the whole
|
|
8343
|
-
* design: what it indexes moves — a runner is replaced on every update, a
|
|
8344
|
-
* crash respawns one, a child re-registers with a replacement manifest after
|
|
8345
|
-
* init — and an index that names a dead child routes work into a socket
|
|
8346
|
-
* nobody is reading, which is strictly worse than a slow scan.
|
|
8300
|
+
* Resolve the hub-local-uds route for a cap owned by a forked hub-local
|
|
8301
|
+
* child, IGNORING any in-hub provider registered for the same cap name.
|
|
8347
8302
|
*
|
|
8348
|
-
*
|
|
8349
|
-
*
|
|
8350
|
-
*
|
|
8351
|
-
*
|
|
8352
|
-
*
|
|
8353
|
-
*
|
|
8354
|
-
* it
|
|
8303
|
+
* `resolveCapRoute` gives Priority 1 to `hub-in-process`: when an in-hub
|
|
8304
|
+
* provider (e.g. a wrapper) is registered for the cap, the route always
|
|
8305
|
+
* classifies as `hub-in-process` and the hub-local-uds NATIVE child is never
|
|
8306
|
+
* reached. That is correct for the generic dispatch path (the wrapper is the
|
|
8307
|
+
* active provider), but WRONG for the native-cap fallback
|
|
8308
|
+
* (`setNativeFallback`), whose contract is to reach the NATIVE provider in
|
|
8309
|
+
* the forked vendor child so a wrapper can delegate to it. A wrapper cap
|
|
8310
|
+
* with a forked native (today: `snapshot`) otherwise resolves to the wrapper
|
|
8311
|
+
* itself, the native is never invoked, and the wrapper silently falls
|
|
8312
|
+
* through to its secondary strategy.
|
|
8355
8313
|
*
|
|
8356
|
-
*
|
|
8357
|
-
*
|
|
8358
|
-
*
|
|
8314
|
+
* This method consults ONLY the hub-local-child authority
|
|
8315
|
+
* (`hubLocalChildProvides` + `getHubLocalChildId`, both deviceId-aware), so
|
|
8316
|
+
* it returns the native child route regardless of any in-process shadow.
|
|
8317
|
+
* Returns null when no hub-local child owns `(capName, deviceId)` — the
|
|
8318
|
+
* caller then falls through to its remote-resolution branch.
|
|
8359
8319
|
*/
|
|
8360
|
-
|
|
8361
|
-
if (this.
|
|
8362
|
-
|
|
8363
|
-
|
|
8364
|
-
|
|
8365
|
-
|
|
8366
|
-
|
|
8320
|
+
resolveHubLocalUdsRoute(capName, deviceId) {
|
|
8321
|
+
if (!this.snapshot.hubLocalChildProvides(capName, deviceId)) return null;
|
|
8322
|
+
const childId = this.snapshot.getHubLocalChildId?.(capName, deviceId) ?? null;
|
|
8323
|
+
if (childId === null) return null;
|
|
8324
|
+
return {
|
|
8325
|
+
kind: "hub-local-uds",
|
|
8326
|
+
capName,
|
|
8327
|
+
childId
|
|
8328
|
+
};
|
|
8367
8329
|
}
|
|
8368
8330
|
/**
|
|
8369
|
-
*
|
|
8370
|
-
*
|
|
8371
|
-
*
|
|
8372
|
-
*
|
|
8373
|
-
*
|
|
8374
|
-
*
|
|
8375
|
-
*
|
|
8376
|
-
*
|
|
8377
|
-
*
|
|
8378
|
-
*
|
|
8331
|
+
* @param addonId Optional out-of-band PROVIDER-selection hint (Task 7.5a):
|
|
8332
|
+
* the addonId of the provider the CALLER already resolved (e.g.
|
|
8333
|
+
* `buildCapCallFn`'s `deps.addonId` for a registered grouped-runner
|
|
8334
|
+
* provider). Only meaningful for `agent-child-forward` — a `remote-moleculer`
|
|
8335
|
+
* route already encodes the provider in its action name
|
|
8336
|
+
* (`${addonId}.${cap}.${method}`, via `NodeCapAuthority.getAddonId`), and a
|
|
8337
|
+
* `hub-local-uds` caller that already knows the addonId (`buildCapCallFn`)
|
|
8338
|
+
* talks to `LocalChildRegistry.callCapOnChild` directly rather than through
|
|
8339
|
+
* `dispatch`.
|
|
8340
|
+
* @param callerAddonId Optional out-of-band CALLER-identity hint — the
|
|
8341
|
+
* addon that ORIGINATED the call, distinct from `addonId` above (which
|
|
8342
|
+
* names the resolved PROVIDER, not the caller). Set by
|
|
8343
|
+
* `onUnownedCall` from `CapCallInput.callerAddonId`
|
|
8344
|
+
* (`LocalChildRegistry` stamped it off the registered `childId`). Only
|
|
8345
|
+
* the `hub-in-process` branch consumes it — merged into `args` so a
|
|
8346
|
+
* diagnostic tool deep in that provider (e.g. device-manager's
|
|
8347
|
+
* fleet-read census) can read it without every provider method changing
|
|
8348
|
+
* signature, the same "extra field the provider destructures or
|
|
8349
|
+
* ignores" convention `nodeId` already uses on this path.
|
|
8379
8350
|
*/
|
|
8351
|
+
async dispatch(route, method, args, addonId, callerAddonId) {
|
|
8352
|
+
try {
|
|
8353
|
+
return await this.dispatchInner(route, method, args, addonId, callerAddonId);
|
|
8354
|
+
} catch (err) {
|
|
8355
|
+
if (err instanceof CapRouteError) throw err;
|
|
8356
|
+
if (route.kind === "hub-in-process" || err instanceof PeerAnsweredError) throw err;
|
|
8357
|
+
const nodeId = this.routeNodeId(route);
|
|
8358
|
+
const cause = err instanceof Error ? err : new Error(String(err));
|
|
8359
|
+
throw new CapRouteError(route.capName, method, {
|
|
8360
|
+
reason: "transport-failed",
|
|
8361
|
+
nodeId,
|
|
8362
|
+
rejected: [{
|
|
8363
|
+
kind: route.kind,
|
|
8364
|
+
why: cause.message
|
|
8365
|
+
}]
|
|
8366
|
+
}, cause);
|
|
8367
|
+
}
|
|
8368
|
+
}
|
|
8380
8369
|
/**
|
|
8381
|
-
*
|
|
8382
|
-
*
|
|
8383
|
-
* over UDS at all", regardless of which caps it has announced yet. Used by
|
|
8384
|
-
* the route-mount fallback to decide between the handler-stripped
|
|
8385
|
-
* `callAddonOnChild(target:'routes')` path (child reachable) and awaiting a
|
|
8386
|
-
* cap proxy's `getRoutes()` (child not yet UDS-registered).
|
|
8370
|
+
* Inner dispatch: may throw CapRouteError (validation failures) or arbitrary
|
|
8371
|
+
* transport errors. The outer `dispatch` wraps non-CapRouteErrors.
|
|
8387
8372
|
*/
|
|
8388
|
-
|
|
8389
|
-
|
|
8390
|
-
|
|
8391
|
-
|
|
8392
|
-
|
|
8393
|
-
|
|
8394
|
-
|
|
8395
|
-
|
|
8396
|
-
|
|
8397
|
-
|
|
8398
|
-
|
|
8399
|
-
|
|
8400
|
-
|
|
8401
|
-
|
|
8402
|
-
|
|
8403
|
-
|
|
8404
|
-
|
|
8405
|
-
|
|
8406
|
-
|
|
8407
|
-
|
|
8408
|
-
|
|
8409
|
-
|
|
8410
|
-
|
|
8411
|
-
|
|
8412
|
-
|
|
8413
|
-
|
|
8414
|
-
|
|
8415
|
-
|
|
8416
|
-
|
|
8417
|
-
|
|
8418
|
-
|
|
8373
|
+
async dispatchInner(route, method, args, addonId, callerAddonId) {
|
|
8374
|
+
switch (route.kind) {
|
|
8375
|
+
case "hub-in-process": return route.ref.invoke(method, withCallerAddonId(args, callerAddonId));
|
|
8376
|
+
case "hub-local-uds": {
|
|
8377
|
+
const registry = this.hubLocalRegistry;
|
|
8378
|
+
if (registry === null) throw new CapRouteError(route.capName, method, {
|
|
8379
|
+
reason: "no-provider",
|
|
8380
|
+
rejected: [{
|
|
8381
|
+
kind: "hub-local-uds",
|
|
8382
|
+
why: "UDS registry not available"
|
|
8383
|
+
}]
|
|
8384
|
+
});
|
|
8385
|
+
const deviceId = extractDeviceId(args);
|
|
8386
|
+
const input = {
|
|
8387
|
+
capName: route.capName,
|
|
8388
|
+
method,
|
|
8389
|
+
args,
|
|
8390
|
+
...deviceId !== void 0 ? { deviceId } : {}
|
|
8391
|
+
};
|
|
8392
|
+
return registry.callCapOnChild(route.childId, input);
|
|
8393
|
+
}
|
|
8394
|
+
case "remote-moleculer": {
|
|
8395
|
+
const addonId = this.nodeAuthority.getAddonId(route.nodeId, route.capName);
|
|
8396
|
+
if (addonId === null) throw new CapRouteError(route.capName, method, {
|
|
8397
|
+
reason: "no-provider",
|
|
8398
|
+
nodeId: route.nodeId,
|
|
8399
|
+
rejected: [{
|
|
8400
|
+
kind: "remote-moleculer",
|
|
8401
|
+
why: `no addonId known for ${route.nodeId}/${route.capName}`
|
|
8402
|
+
}]
|
|
8403
|
+
});
|
|
8404
|
+
const deviceId = extractDeviceId(args);
|
|
8405
|
+
const isNative = this.nodeAuthority.isNativeCap(route.nodeId, route.capName, deviceId);
|
|
8406
|
+
const action = capActionName(addonId, route.capName, method, isNative);
|
|
8407
|
+
return callWithServiceDiscovery(this.broker, addonId, action, args, {
|
|
8408
|
+
nodeID: route.nodeId,
|
|
8409
|
+
timeout: this.callTimeout(route.capName, method)
|
|
8410
|
+
});
|
|
8411
|
+
}
|
|
8412
|
+
case "agent-child-forward": {
|
|
8413
|
+
const deviceId = extractDeviceId(args);
|
|
8414
|
+
const params = {
|
|
8415
|
+
capName: route.capName,
|
|
8416
|
+
method,
|
|
8417
|
+
args,
|
|
8418
|
+
...route.childId !== void 0 ? { childId: route.childId } : {},
|
|
8419
|
+
...deviceId !== void 0 ? { deviceId } : {},
|
|
8420
|
+
...addonId !== void 0 ? { addonId } : {}
|
|
8421
|
+
};
|
|
8422
|
+
return callWithServiceDiscovery(this.broker, AGENT_CAP_FWD_SERVICE, AGENT_CAP_FWD_ACTION, params, {
|
|
8423
|
+
nodeID: route.agentNodeId,
|
|
8424
|
+
timeout: this.callTimeout(route.capName, method)
|
|
8425
|
+
});
|
|
8426
|
+
}
|
|
8419
8427
|
}
|
|
8420
|
-
return entry.channel.request(this.toCapCall(input), this.capRequestOpts(input));
|
|
8421
|
-
}
|
|
8422
|
-
/**
|
|
8423
|
-
* Forward an ADDON-LEVEL call (routes / custom-action) to a SPECIFIC child
|
|
8424
|
-
* by id over UDS; rejects if that child is absent.
|
|
8425
|
-
*
|
|
8426
|
-
* The childId for a hub-local single-addon runner equals the addonId
|
|
8427
|
-
* (`resolveRunnerId` returns the addonId when no `execution.group` is
|
|
8428
|
-
* declared — no shipped addon declares one). Mirrors `callCapOnChild` for
|
|
8429
|
-
* the cap plane; carries the two surfaces the removed per-addon Moleculer
|
|
8430
|
-
* broker used to serve (`getRoutes` + `custom.<action>`).
|
|
8431
|
-
*/
|
|
8432
|
-
async callAddonOnChild(childId, input) {
|
|
8433
|
-
const entry = this.children.get(childId);
|
|
8434
|
-
if (!entry) throw new Error(`no local child "${childId}" for addon-call (${input.target})`);
|
|
8435
|
-
return entry.channel.request(this.toAddonCall(input));
|
|
8436
|
-
}
|
|
8437
|
-
/** Build the parent→child `addon-call` wire message from an addon-call input. */
|
|
8438
|
-
toAddonCall(input) {
|
|
8439
|
-
return {
|
|
8440
|
-
kind: "addon-call",
|
|
8441
|
-
addonId: input.addonId,
|
|
8442
|
-
target: input.target,
|
|
8443
|
-
...input.action !== void 0 ? { action: input.action } : {},
|
|
8444
|
-
...input.method !== void 0 ? { method: input.method } : {},
|
|
8445
|
-
...input.args !== void 0 ? { args: input.args } : {},
|
|
8446
|
-
...input.caller !== void 0 ? { caller: input.caller } : {}
|
|
8447
|
-
};
|
|
8448
8428
|
}
|
|
8449
|
-
|
|
8450
|
-
|
|
8429
|
+
buildSnapshot() {
|
|
8430
|
+
const hubLocalRegistry = this.hubLocalRegistry;
|
|
8431
|
+
const nodeAuthority = this.nodeAuthority;
|
|
8432
|
+
const inProcessProviders = this.inProcessProviders;
|
|
8433
|
+
const hubResidentProviders = this.hubResidentProviders;
|
|
8451
8434
|
return {
|
|
8452
|
-
|
|
8453
|
-
|
|
8454
|
-
|
|
8455
|
-
|
|
8456
|
-
|
|
8457
|
-
|
|
8435
|
+
hubNodeId: this.hubNodeId,
|
|
8436
|
+
hubInProcessProvides: (cap) => inProcessProviders(cap) !== null,
|
|
8437
|
+
...hubResidentProviders !== void 0 ? {
|
|
8438
|
+
hubResidentProvides: (cap) => hubResidentProviders(cap) !== null,
|
|
8439
|
+
getHubResidentProviderRef: (cap) => hubResidentProviders(cap)
|
|
8440
|
+
} : {},
|
|
8441
|
+
hubLocalChildProvides: (cap, deviceId) => hubLocalRegistry !== null && hubLocalRegistry.resolveChildId(cap, deviceId) !== null,
|
|
8442
|
+
nodeKnowsCap: (nodeId, cap) => nodeAuthority.nodeKnowsCap(nodeId, cap),
|
|
8443
|
+
nodeIsAgent: (nodeId) => nodeAuthority.nodeIsAgent(nodeId),
|
|
8444
|
+
nodeOnline: (nodeId) => nodeAuthority.nodeOnline(nodeId),
|
|
8445
|
+
listKnownNodeIds: () => nodeAuthority.listNodeIds(),
|
|
8446
|
+
getInProcessProviderRef: (cap) => inProcessProviders(cap),
|
|
8447
|
+
getHubLocalChildId: (cap, deviceId) => hubLocalRegistry !== null ? hubLocalRegistry.resolveChildId(cap, deviceId) : null,
|
|
8448
|
+
getAgentChildId: (agentNodeId, cap) => nodeAuthority.getAgentChildId(agentNodeId, cap)
|
|
8458
8449
|
};
|
|
8459
8450
|
}
|
|
8460
|
-
|
|
8461
|
-
|
|
8462
|
-
|
|
8463
|
-
|
|
8464
|
-
|
|
8465
|
-
|
|
8466
|
-
|
|
8467
|
-
|
|
8468
|
-
onChildRegistered(handler) {
|
|
8469
|
-
this.registeredHandler = handler;
|
|
8451
|
+
/** Extract a nodeId string from a route for error reporting, or undefined. */
|
|
8452
|
+
routeNodeId(route) {
|
|
8453
|
+
switch (route.kind) {
|
|
8454
|
+
case "hub-in-process": return this.hubNodeId;
|
|
8455
|
+
case "hub-local-uds": return `${this.hubNodeId}/${route.childId}`;
|
|
8456
|
+
case "remote-moleculer": return route.nodeId;
|
|
8457
|
+
case "agent-child-forward": return route.agentNodeId;
|
|
8458
|
+
}
|
|
8470
8459
|
}
|
|
8460
|
+
};
|
|
8461
|
+
//#endregion
|
|
8462
|
+
//#region src/kernel/transport/claimed-get-status.ts
|
|
8463
|
+
var GET_STATUS_METHOD = "getStatus";
|
|
8464
|
+
/** The device a `getStatus` is about, or `undefined` when the call is not one. */
|
|
8465
|
+
function deviceOfGetStatus(call) {
|
|
8466
|
+
if (call.method !== "getStatus") return void 0;
|
|
8467
|
+
return call.deviceId ?? extractDeviceId(call.args);
|
|
8468
|
+
}
|
|
8469
|
+
async function answerClaimedGetStatus(resolve, call) {
|
|
8470
|
+
if (resolve === void 0) return null;
|
|
8471
|
+
const deviceId = deviceOfGetStatus(call);
|
|
8472
|
+
if (deviceId === void 0) return null;
|
|
8473
|
+
const answer = await resolve(call.capName, deviceId);
|
|
8474
|
+
if (answer.kind === "unclaimed") return null;
|
|
8475
|
+
return { slice: answer.kind === "claimed" ? answer.slice : null };
|
|
8476
|
+
}
|
|
8477
|
+
function readFanoutMode() {
|
|
8478
|
+
const raw = process.env.CAMSTACK_UDS_EVENT_FANOUT;
|
|
8479
|
+
if (raw === "shadow" || raw === "broadcast") return raw;
|
|
8480
|
+
return "filter";
|
|
8481
|
+
}
|
|
8482
|
+
/**
|
|
8483
|
+
* Sentinel prefix used in the no-route error thrown when `cap-call-out` has no
|
|
8484
|
+
* local sibling and no `onUnownedCall` fallback. `ipcParentLink` detects this
|
|
8485
|
+
* prefix to distinguish routing failures (safe to retry via broker) from real
|
|
8486
|
+
* provider errors (must NOT retry to avoid double-executing side effects).
|
|
8487
|
+
*/
|
|
8488
|
+
var UDS_NO_ROUTE_PREFIX = "UDS_NO_ROUTE";
|
|
8489
|
+
/**
|
|
8490
|
+
* Parent-side authority for local addon-runners reachable over a
|
|
8491
|
+
* `LocalTransportServer` (UDS). Children register their cap manifest on
|
|
8492
|
+
* connect; the registry routes `(capName, deviceId?)` cap calls to the
|
|
8493
|
+
* owning child over the channel and drops a child's caps on disconnect.
|
|
8494
|
+
*/
|
|
8495
|
+
var LocalChildRegistry = class {
|
|
8496
|
+
children = /* @__PURE__ */ new Map();
|
|
8497
|
+
registeredHandler = () => {};
|
|
8498
|
+
goneHandler = () => {};
|
|
8499
|
+
/** Source of {@link RegisteredChild.incarnation}; one bump per accepted connection. */
|
|
8500
|
+
nextIncarnation = 1;
|
|
8501
|
+
eventHandler = null;
|
|
8502
|
+
logHandler = null;
|
|
8503
|
+
readinessHandler = null;
|
|
8504
|
+
server;
|
|
8505
|
+
onUnownedCall;
|
|
8506
|
+
logger;
|
|
8507
|
+
getActiveSingletonAddonId;
|
|
8508
|
+
resolveChildIdForAddon;
|
|
8509
|
+
/** This node's own Moleculer nodeId (see {@link LocalChildRegistryOptions.ownNodeId}). */
|
|
8510
|
+
ownNodeId;
|
|
8511
|
+
/** See {@link LocalChildRegistryOptions.isAggregatedCollectionMethod}. */
|
|
8512
|
+
isAggregatedCollectionMethod;
|
|
8513
|
+
isAddonPinnedCall;
|
|
8514
|
+
/** See {@link ForwardCensus}. Mutable counters on the hot path, by design. */
|
|
8515
|
+
forwardedRaw = 0;
|
|
8516
|
+
forwardedDecoded = 0;
|
|
8471
8517
|
/**
|
|
8472
|
-
*
|
|
8473
|
-
*
|
|
8474
|
-
*
|
|
8475
|
-
*
|
|
8518
|
+
* The per-ACTION byte census (D456). `ForwardCensus` above is the same
|
|
8519
|
+
* question asked of the whole plane; this one asks it per `<cap>.<method>`,
|
|
8520
|
+
* beside the bytes that method actually moved. Both are kept: `socketFwd=`
|
|
8521
|
+
* says whether the negotiation is working at all, and a `raw=0` on ONE
|
|
8522
|
+
* method with `raw>0` on its neighbours is a different fault.
|
|
8476
8523
|
*/
|
|
8477
|
-
|
|
8478
|
-
|
|
8479
|
-
|
|
8524
|
+
actionCensus = createCapActionCensus();
|
|
8525
|
+
/** See {@link LocalChildRegistryOptions.capTimeoutMs}. */
|
|
8526
|
+
capTimeoutMs;
|
|
8527
|
+
/** See {@link LocalChildRegistryOptions.claimedStatus}. */
|
|
8528
|
+
claimedStatus;
|
|
8529
|
+
/** See {@link LocalChildRegistryOptions.capUsageObserver}. */
|
|
8530
|
+
capUsageObserver;
|
|
8531
|
+
/** Tracks capNames already logged as UDS-routed; one INFO line per capName per process. */
|
|
8532
|
+
egressRoutedCaps = /* @__PURE__ */ new Set();
|
|
8533
|
+
/** Active event fan-out mode, read once from `CAMSTACK_UDS_EVENT_FANOUT`. */
|
|
8534
|
+
fanoutMode = readFanoutMode();
|
|
8535
|
+
/** Per-child event fan-out counters (sent / suppressed). */
|
|
8536
|
+
childEventStats = /* @__PURE__ */ new Map();
|
|
8537
|
+
/** D467: per-category production census (cumulative). See {@link CategoryFanoutSample}. */
|
|
8538
|
+
categoryStats = /* @__PURE__ */ new Map();
|
|
8539
|
+
/** Broadcasts whose category did not fit in {@link CATEGORY_CENSUS_MAX}. */
|
|
8540
|
+
categoryOverflow = 0;
|
|
8541
|
+
/** One INFO line the first time the census cap trips, never per event. */
|
|
8542
|
+
categoryOverflowLogged = false;
|
|
8543
|
+
/** Last pattern-set string logged per child, to dedup the INFO line. */
|
|
8544
|
+
loggedPatternSet = /* @__PURE__ */ new Map();
|
|
8480
8545
|
/**
|
|
8481
|
-
*
|
|
8482
|
-
*
|
|
8483
|
-
*
|
|
8484
|
-
* handler entirely (used by the UDS event bridge disposer on shutdown).
|
|
8546
|
+
* Derived `(cap, device?) → child` index over {@link children}, or `null`
|
|
8547
|
+
* when it must be rebuilt. See {@link capIndex} for why it is a cache and
|
|
8548
|
+
* never a registry (D3, D457).
|
|
8485
8549
|
*/
|
|
8486
|
-
|
|
8487
|
-
this.eventHandler = handler;
|
|
8488
|
-
}
|
|
8550
|
+
capChildIndex = null;
|
|
8489
8551
|
/**
|
|
8490
|
-
*
|
|
8491
|
-
*
|
|
8552
|
+
* Accepts either a plain positional `server` argument (backward-compatible)
|
|
8553
|
+
* or a full `LocalChildRegistryOptions` object.
|
|
8554
|
+
*
|
|
8555
|
+
* Positional overloads (existing call sites are unchanged):
|
|
8556
|
+
* new LocalChildRegistry(server)
|
|
8557
|
+
* new LocalChildRegistry(server, onUnownedCall)
|
|
8558
|
+
*
|
|
8559
|
+
* Options object (new call sites that pass a logger):
|
|
8560
|
+
* new LocalChildRegistry({ server, onUnownedCall, logger })
|
|
8492
8561
|
*/
|
|
8493
|
-
|
|
8494
|
-
|
|
8495
|
-
|
|
8496
|
-
|
|
8497
|
-
|
|
8498
|
-
|
|
8499
|
-
|
|
8500
|
-
|
|
8501
|
-
|
|
8562
|
+
constructor(serverOrOptions, onUnownedCallArg) {
|
|
8563
|
+
if (serverOrOptions && !("listen" in serverOrOptions)) {
|
|
8564
|
+
const opts = serverOrOptions;
|
|
8565
|
+
this.server = opts.server;
|
|
8566
|
+
this.onUnownedCall = opts.onUnownedCall;
|
|
8567
|
+
this.logger = opts.logger;
|
|
8568
|
+
this.getActiveSingletonAddonId = opts.getActiveSingletonAddonId;
|
|
8569
|
+
this.resolveChildIdForAddon = opts.resolveChildIdForAddon;
|
|
8570
|
+
this.ownNodeId = opts.ownNodeId;
|
|
8571
|
+
this.isAggregatedCollectionMethod = opts.isAggregatedCollectionMethod;
|
|
8572
|
+
this.isAddonPinnedCall = opts.isAddonPinnedCall;
|
|
8573
|
+
this.capTimeoutMs = opts.capTimeoutMs;
|
|
8574
|
+
this.claimedStatus = opts.claimedStatus;
|
|
8575
|
+
this.capUsageObserver = opts.capUsageObserver;
|
|
8576
|
+
} else {
|
|
8577
|
+
this.server = serverOrOptions;
|
|
8578
|
+
this.onUnownedCall = onUnownedCallArg;
|
|
8579
|
+
}
|
|
8580
|
+
}
|
|
8581
|
+
async start() {
|
|
8582
|
+
this.logger?.info("UDS event fan-out mode", { mode: this.fanoutMode });
|
|
8583
|
+
this.server.onConnection((channel) => this.onConnection(channel));
|
|
8584
|
+
await this.server.listen();
|
|
8502
8585
|
}
|
|
8503
8586
|
/**
|
|
8504
|
-
*
|
|
8505
|
-
*
|
|
8587
|
+
* Per-child event fan-out counters (`{ sent, suppressed }`) or `null` if the
|
|
8588
|
+
* child has no recorded events yet. Exposed for the starvation-verification
|
|
8589
|
+
* surface (shadow burn-in + operator debug).
|
|
8506
8590
|
*/
|
|
8507
|
-
|
|
8508
|
-
const
|
|
8509
|
-
|
|
8510
|
-
|
|
8511
|
-
|
|
8512
|
-
event,
|
|
8513
|
-
sourceNodeId
|
|
8591
|
+
getChildEventStats(childId) {
|
|
8592
|
+
const s = this.childEventStats.get(childId);
|
|
8593
|
+
return s === void 0 ? null : {
|
|
8594
|
+
sent: s.sent,
|
|
8595
|
+
suppressed: s.suppressed
|
|
8514
8596
|
};
|
|
8515
|
-
entry.channel.emit(msg);
|
|
8516
8597
|
}
|
|
8517
8598
|
/**
|
|
8518
|
-
*
|
|
8519
|
-
*
|
|
8599
|
+
* D467: every category this node has broadcast, with its emit count and how
|
|
8600
|
+
* many child subscriptions matched. Cumulative for the life of the process —
|
|
8601
|
+
* the heartbeat differences it, the same discipline as the per-child
|
|
8602
|
+
* counters. Largest `broadcasts` first.
|
|
8520
8603
|
*/
|
|
8521
|
-
|
|
8522
|
-
const
|
|
8523
|
-
|
|
8524
|
-
|
|
8525
|
-
|
|
8526
|
-
|
|
8527
|
-
|
|
8528
|
-
|
|
8529
|
-
|
|
8530
|
-
|
|
8531
|
-
|
|
8532
|
-
|
|
8533
|
-
if (!wants) stats.suppressed++;
|
|
8534
|
-
if (wants || this.fanoutMode === "shadow") {
|
|
8535
|
-
stats.sent++;
|
|
8536
|
-
entry.channel.emit(msg);
|
|
8537
|
-
}
|
|
8538
|
-
}
|
|
8539
|
-
this.recordCategoryBroadcast(event.category, subscribed);
|
|
8604
|
+
categoryFanout() {
|
|
8605
|
+
const out = [];
|
|
8606
|
+
for (const [category, s] of this.categoryStats) out.push({
|
|
8607
|
+
category,
|
|
8608
|
+
broadcasts: s.broadcasts,
|
|
8609
|
+
wanted: s.wanted
|
|
8610
|
+
});
|
|
8611
|
+
return out.toSorted((a, b) => b.broadcasts - a.broadcasts || a.category.localeCompare(b.category));
|
|
8612
|
+
}
|
|
8613
|
+
/** Broadcasts the census could not attribute because it was full (cumulative). */
|
|
8614
|
+
categoryCensusOverflow() {
|
|
8615
|
+
return this.categoryOverflow;
|
|
8540
8616
|
}
|
|
8541
8617
|
/**
|
|
8542
|
-
*
|
|
8543
|
-
*
|
|
8618
|
+
* This child's socket traffic — bytes and messages, both directions, split by
|
|
8619
|
+
* frame kind — or `null` when the child is gone or its channel keeps no
|
|
8620
|
+
* counters (an in-process or test transport).
|
|
8621
|
+
*
|
|
8622
|
+
* The event counters above cover ONE kind of frame and, once surfaced on
|
|
8623
|
+
* 2026-08-29, accounted for 4% of hub-main's ~44 000 socket syscalls/s. This
|
|
8624
|
+
* is the same question asked of every frame the channel moves, and it is the
|
|
8625
|
+
* only per-peer attribution that exists: `/proc` reports one `rchar`/`wchar`
|
|
8626
|
+
* pair for the whole process and has no per-socket breakdown.
|
|
8544
8627
|
*/
|
|
8545
|
-
|
|
8546
|
-
const
|
|
8547
|
-
if (
|
|
8548
|
-
|
|
8549
|
-
existing.wanted += subscribed;
|
|
8550
|
-
return;
|
|
8551
|
-
}
|
|
8552
|
-
if (this.categoryStats.size >= 256) {
|
|
8553
|
-
this.categoryOverflow += 1;
|
|
8554
|
-
if (!this.categoryOverflowLogged) {
|
|
8555
|
-
this.categoryOverflowLogged = true;
|
|
8556
|
-
this.logger?.info("event category census full — further new categories are unattributed", {
|
|
8557
|
-
max: 256,
|
|
8558
|
-
firstDropped: category
|
|
8559
|
-
});
|
|
8560
|
-
}
|
|
8561
|
-
return;
|
|
8562
|
-
}
|
|
8563
|
-
this.categoryStats.set(category, {
|
|
8564
|
-
broadcasts: 1,
|
|
8565
|
-
wanted: subscribed
|
|
8566
|
-
});
|
|
8628
|
+
getChildSocketTraffic(childId) {
|
|
8629
|
+
const entry = this.children.get(childId);
|
|
8630
|
+
if (entry === void 0) return null;
|
|
8631
|
+
return entry.channel.readTraffic?.() ?? null;
|
|
8567
8632
|
}
|
|
8568
8633
|
/**
|
|
8569
|
-
*
|
|
8570
|
-
*
|
|
8571
|
-
*
|
|
8572
|
-
*
|
|
8573
|
-
* back to the Moleculer `$node-mgmt.setLogLevel` action for the node.
|
|
8574
|
-
* Mirrors the `$node-mgmt.setLogLevel` Moleculer action for UDS children.
|
|
8634
|
+
* Bytes this process has written to a child that the child has not yet
|
|
8635
|
+
* taken, live, or `null` when the channel keeps no such reading. Read on
|
|
8636
|
+
* every heartbeat probe (D446) — a child that stops draining is the shape
|
|
8637
|
+
* of an outbound-queue episode, and the number lives on the channel.
|
|
8575
8638
|
*/
|
|
8576
|
-
|
|
8639
|
+
getChildQueuedBytes(childId) {
|
|
8577
8640
|
const entry = this.children.get(childId);
|
|
8578
|
-
if (entry === void 0) return
|
|
8579
|
-
|
|
8580
|
-
|
|
8581
|
-
|
|
8641
|
+
if (entry === void 0) return null;
|
|
8642
|
+
return entry.channel.queuedBytes?.() ?? null;
|
|
8643
|
+
}
|
|
8644
|
+
/** Cumulative {@link ForwardCensus} of this parent's `cap-call-out` routing. */
|
|
8645
|
+
readForwardCensus() {
|
|
8646
|
+
return {
|
|
8647
|
+
raw: this.forwardedRaw,
|
|
8648
|
+
decoded: this.forwardedDecoded
|
|
8582
8649
|
};
|
|
8583
|
-
entry.channel.emit(msg);
|
|
8584
|
-
return true;
|
|
8585
8650
|
}
|
|
8586
|
-
|
|
8587
|
-
|
|
8651
|
+
/**
|
|
8652
|
+
* Cumulative per-action byte census: peer → `<cap>.<method>` → bytes, calls
|
|
8653
|
+
* and the raw/decoded split. A COPY (`cap-action-census.ts` explains why it
|
|
8654
|
+
* must be one); the heartbeat subtracts the previous reading.
|
|
8655
|
+
*
|
|
8656
|
+
* This is the instrument D454 closed by asking for. `socketTopBytes=` names
|
|
8657
|
+
* the peer that moves the bytes and `txkB=` says they are responses; neither
|
|
8658
|
+
* can say WHICH method originates them, and after raw-forward the bytes that
|
|
8659
|
+
* cross are no longer the proxy for what hub-main pays — 85.7 % of sibling
|
|
8660
|
+
* calls cross unread. What costs is what hub-main LOOKS at, and that is the
|
|
8661
|
+
* `decoded` half of an entry, next to the bytes it carried.
|
|
8662
|
+
*/
|
|
8663
|
+
readActionCensus() {
|
|
8664
|
+
return this.actionCensus.read();
|
|
8588
8665
|
}
|
|
8589
|
-
onConnection(channel) {
|
|
8590
|
-
let childId = null;
|
|
8591
|
-
const incarnation = this.nextIncarnation++;
|
|
8592
|
-
channel.observeActions?.({
|
|
8593
|
-
label: capActionLabel,
|
|
8594
|
-
record: (label, reqBytes, resBytes) => {
|
|
8595
|
-
const peerId = childId ?? "(pre-register)";
|
|
8596
|
-
if (label === null) this.actionCensus.recordUnlabelled(peerId, reqBytes + resBytes);
|
|
8597
|
-
else this.actionCensus.record(peerId, label, reqBytes, resBytes);
|
|
8598
|
-
}
|
|
8599
|
-
});
|
|
8600
|
-
channel.onEvent((body) => {
|
|
8601
|
-
const msg = body;
|
|
8602
|
-
if (msg.kind === "event") {
|
|
8603
|
-
if (childId !== null) this.eventHandler?.(childId, msg.event);
|
|
8604
|
-
return;
|
|
8605
|
-
}
|
|
8606
|
-
if (msg.kind === "log") {
|
|
8607
|
-
if (childId !== null) this.logHandler?.(childId, msg);
|
|
8608
|
-
return;
|
|
8609
|
-
}
|
|
8610
|
-
if (msg.kind === "event-sub") {
|
|
8611
|
-
if (childId === null) return;
|
|
8612
|
-
const entry = this.children.get(childId);
|
|
8613
|
-
if (entry === void 0) return;
|
|
8614
|
-
this.children.set(childId, {
|
|
8615
|
-
...entry,
|
|
8616
|
-
eventPatterns: msg.patterns
|
|
8617
|
-
});
|
|
8618
|
-
this.logPatternSet(childId, msg.patterns);
|
|
8619
|
-
return;
|
|
8620
|
-
}
|
|
8621
|
-
});
|
|
8622
|
-
channel.onRequest(async (body, payload) => {
|
|
8623
|
-
const msg = body;
|
|
8624
|
-
if (msg.kind === "register") {
|
|
8625
|
-
if (childId !== null && childId !== msg.childId) throw new Error(`child attempted to change identity from "${childId}" to "${msg.childId}"`);
|
|
8626
|
-
childId = msg.childId;
|
|
8627
|
-
const eventPatterns = msg.eventPatterns ?? null;
|
|
8628
|
-
const superseded = this.children.get(msg.childId);
|
|
8629
|
-
if (superseded !== void 0 && superseded.channel !== channel) this.logger?.info("local child superseded by a new connection", {
|
|
8630
|
-
childId: msg.childId,
|
|
8631
|
-
supersededIncarnation: superseded.incarnation,
|
|
8632
|
-
incarnation
|
|
8633
|
-
});
|
|
8634
|
-
this.children.set(msg.childId, {
|
|
8635
|
-
childId: msg.childId,
|
|
8636
|
-
channel,
|
|
8637
|
-
caps: msg.caps,
|
|
8638
|
-
eventPatterns,
|
|
8639
|
-
incarnation,
|
|
8640
|
-
rawForward: msg.rawForward === true
|
|
8641
|
-
});
|
|
8642
|
-
this.invalidateCapIndex();
|
|
8643
|
-
this.logPatternSet(msg.childId, eventPatterns);
|
|
8644
|
-
this.registeredHandler({
|
|
8645
|
-
childId: msg.childId,
|
|
8646
|
-
caps: msg.caps,
|
|
8647
|
-
incarnation,
|
|
8648
|
-
...msg.customActions !== void 0 ? { customActions: msg.customActions } : {}
|
|
8649
|
-
});
|
|
8650
|
-
return {
|
|
8651
|
-
ok: true,
|
|
8652
|
-
rawForward: true
|
|
8653
|
-
};
|
|
8654
|
-
}
|
|
8655
|
-
if (msg.kind === "cap-call-out") {
|
|
8656
|
-
const out = msg;
|
|
8657
|
-
const hints = out.hints ?? liftRoutingHints(out.args);
|
|
8658
|
-
const materialise = () => payload !== void 0 ? unpackPayload(payload) : out.args;
|
|
8659
|
-
const buildInput = () => ({
|
|
8660
|
-
capName: out.capName,
|
|
8661
|
-
method: out.method,
|
|
8662
|
-
args: materialise(),
|
|
8663
|
-
...out.deviceId !== void 0 ? { deviceId: out.deviceId } : {},
|
|
8664
|
-
...out.nodeId !== void 0 ? { nodeId: out.nodeId } : {},
|
|
8665
|
-
...out.native === true ? { native: true } : {},
|
|
8666
|
-
...childId !== null ? { callerAddonId: childId } : {}
|
|
8667
|
-
});
|
|
8668
|
-
const pinnedNodeId = out.nodeId ?? hints.nodeId;
|
|
8669
|
-
const pinTargetsThisNode = pinnedNodeId !== void 0 && this.ownNodeId !== void 0 && pinnedNodeId === this.ownNodeId;
|
|
8670
|
-
const aggregated = pinnedNodeId === void 0 && this.isAggregatedCollectionMethod?.(out.capName, out.method) === true;
|
|
8671
|
-
const addonPinned = pinnedNodeId === void 0 && this.isAddonPinnedCall?.(out.capName, out.method, hints) === true;
|
|
8672
|
-
const target = !(out.native === true) && !aggregated && !addonPinned && (pinnedNodeId === void 0 || pinTargetsThisNode) ? this.resolveChildId(out.capName, out.deviceId) : null;
|
|
8673
|
-
this.recordCapUsage(childId, target, out.capName, out.method);
|
|
8674
|
-
const actionPeer = childId ?? "(pre-register)";
|
|
8675
|
-
const actionLabel = `${out.capName}.${out.method}`;
|
|
8676
|
-
if (target !== null) {
|
|
8677
|
-
if (!this.egressRoutedCaps.has(out.capName)) {
|
|
8678
|
-
this.egressRoutedCaps.add(out.capName);
|
|
8679
|
-
this.logger?.info("routed child egress over UDS", { capName: out.capName });
|
|
8680
|
-
}
|
|
8681
|
-
const entry = this.children.get(target);
|
|
8682
|
-
if (payload !== void 0 && entry !== void 0 && entry.rawForward) {
|
|
8683
|
-
this.forwardedRaw += 1;
|
|
8684
|
-
this.actionCensus.recordForward(actionPeer, actionLabel, "raw");
|
|
8685
|
-
const forward = {
|
|
8686
|
-
kind: "cap-call",
|
|
8687
|
-
capName: out.capName,
|
|
8688
|
-
method: out.method,
|
|
8689
|
-
args: void 0,
|
|
8690
|
-
...out.deviceId !== void 0 ? { deviceId: out.deviceId } : {}
|
|
8691
|
-
};
|
|
8692
|
-
const declared = this.capTimeoutMs?.(out.capName, out.method);
|
|
8693
|
-
return entry.channel.request(forward, {
|
|
8694
|
-
...typeof declared === "number" ? { timeoutMs: declared } : {},
|
|
8695
|
-
payload
|
|
8696
|
-
});
|
|
8697
|
-
}
|
|
8698
|
-
this.forwardedDecoded += 1;
|
|
8699
|
-
this.actionCensus.recordForward(actionPeer, actionLabel, "decoded");
|
|
8700
|
-
const input = buildInput();
|
|
8701
|
-
return entry !== void 0 ? this.callCapOnChild(target, input) : this.callCap(input);
|
|
8702
|
-
}
|
|
8703
|
-
this.forwardedDecoded += 1;
|
|
8704
|
-
this.actionCensus.recordForward(actionPeer, actionLabel, "decoded");
|
|
8705
|
-
if (this.onUnownedCall !== void 0) return this.onUnownedCall(buildInput());
|
|
8706
|
-
throw new Error(`${UDS_NO_ROUTE_PREFIX}: cap-call-out has no local provider for "${out.capName}" and no fallback`);
|
|
8707
|
-
}
|
|
8708
|
-
if (msg.kind === "readiness-request") return {
|
|
8709
|
-
kind: "readiness-snapshot",
|
|
8710
|
-
records: this.readinessHandler?.() ?? []
|
|
8711
|
-
};
|
|
8712
|
-
throw new Error(`unknown child request kind: ${msg.kind}`);
|
|
8713
|
-
});
|
|
8714
|
-
channel.onClose(() => {
|
|
8715
|
-
if (childId === null) return;
|
|
8716
|
-
const entry = this.children.get(childId);
|
|
8717
|
-
if (entry === void 0) return;
|
|
8718
|
-
if (entry.channel !== channel) {
|
|
8719
|
-
this.logger?.info("ignoring close of a superseded local child connection", {
|
|
8720
|
-
childId,
|
|
8721
|
-
incarnation,
|
|
8722
|
-
liveIncarnation: entry.incarnation
|
|
8723
|
-
});
|
|
8724
|
-
return;
|
|
8725
|
-
}
|
|
8726
|
-
this.children.delete(childId);
|
|
8727
|
-
this.invalidateCapIndex();
|
|
8728
|
-
this.childEventStats.delete(childId);
|
|
8729
|
-
this.actionCensus.forget(childId);
|
|
8730
|
-
this.loggedPatternSet.delete(childId);
|
|
8731
|
-
this.goneHandler(childId, incarnation);
|
|
8732
|
-
});
|
|
8733
|
-
}
|
|
8734
|
-
};
|
|
8735
|
-
//#endregion
|
|
8736
|
-
//#region src/kernel/transport/local-child-client.ts
|
|
8737
|
-
/** Did the parent's `register` answer say it accepts payload-borne args? */
|
|
8738
|
-
function ackAcceptsRawForward(ack) {
|
|
8739
|
-
if (ack === null || typeof ack !== "object") return false;
|
|
8740
|
-
return ack.rawForward === true;
|
|
8741
|
-
}
|
|
8742
|
-
/**
|
|
8743
|
-
* Child side of the local UDS transport. Connects to its parent (hub or
|
|
8744
|
-
* agent), registers its cap manifest, and serves parent→child cap calls by
|
|
8745
|
-
* delegating to `dispatch`. The provider implementation lives in the child;
|
|
8746
|
-
* only routing keys + call arguments cross the wire.
|
|
8747
|
-
*
|
|
8748
|
-
* Additional channels beyond cap-call:
|
|
8749
|
-
* - `emitEvent` fire-and-forget event toward the parent
|
|
8750
|
-
* - `sendLog` fire-and-forget log entry toward the parent
|
|
8751
|
-
* - `requestReadinessSnapshot` request/response snapshot of readiness records
|
|
8752
|
-
* - `onEvent` register a handler for parent→child events
|
|
8753
|
-
*
|
|
8754
|
-
* Events and logs emitted before `start()` resolves are buffered and flushed
|
|
8755
|
-
* on connect (mirrors the `updateCaps`/`latestCaps` pattern).
|
|
8756
|
-
*/
|
|
8757
|
-
var LocalChildClient = class {
|
|
8758
|
-
options;
|
|
8759
|
-
client = null;
|
|
8760
|
-
channel = null;
|
|
8761
|
-
/**
|
|
8762
|
-
* The cap set `start()` will register, kept current by `updateCaps`. Native
|
|
8763
|
-
* device caps register on device-restore, which can race AHEAD of the UDS
|
|
8764
|
-
* connect — buffering here lets a pre-start `updateCaps` survive (start sends
|
|
8765
|
-
* the latest set) instead of being lost or throwing.
|
|
8766
|
-
*/
|
|
8767
|
-
latestCaps;
|
|
8768
|
-
/**
|
|
8769
|
-
* Per-owner (addonId) category-pattern subscription sets, and their union.
|
|
8770
|
-
* The union is declared to the parent (in `RegisterMessage.eventPatterns`
|
|
8771
|
-
* pre/at-connect, or via an `event-sub` emit post-connect) so the parent can
|
|
8772
|
-
* subscription-filter its event fan-out. Per-owner keying keeps the union
|
|
8773
|
-
* correct if multiple event buses ever share one client (group-runner case).
|
|
8774
|
-
*/
|
|
8775
|
-
patternsByOwner = /* @__PURE__ */ new Map();
|
|
8776
|
-
latestEventPatterns = [];
|
|
8777
|
-
/**
|
|
8778
|
-
* Whether `updateEventPatterns` has ever been called. A client that never
|
|
8779
|
-
* declared a subscription set (no event bus wired — e.g. a pure cap-call
|
|
8780
|
-
* runner) omits `eventPatterns` from its register frame entirely, so the
|
|
8781
|
-
* parent treats it as UNDECLARED and fails OPEN (broadcast-all). Once any
|
|
8782
|
-
* owner declares (in production every addon context's framework
|
|
8783
|
-
* subscriptions do), the register carries the real union — even if empty.
|
|
8784
|
-
*/
|
|
8785
|
-
hasDeclaredEventPatterns = false;
|
|
8786
8666
|
/**
|
|
8787
|
-
* The
|
|
8788
|
-
*
|
|
8789
|
-
*
|
|
8790
|
-
*
|
|
8791
|
-
*
|
|
8792
|
-
*
|
|
8667
|
+
* The regime the counters above were produced under, read once from
|
|
8668
|
+
* `CAMSTACK_UDS_EVENT_FANOUT` at construction.
|
|
8669
|
+
*
|
|
8670
|
+
* A suppressed count means "nothing WAS dropped" under `filter` and "nothing
|
|
8671
|
+
* WOULD have been dropped" under `shadow`. Those are different claims, and
|
|
8672
|
+
* until this getter existed the mode was invisible at runtime — the counters
|
|
8673
|
+
* were readable and the regime that produced them was not.
|
|
8793
8674
|
*/
|
|
8794
|
-
|
|
8795
|
-
|
|
8796
|
-
|
|
8675
|
+
get eventFanoutMode() {
|
|
8676
|
+
return this.fanoutMode;
|
|
8677
|
+
}
|
|
8797
8678
|
/**
|
|
8798
|
-
*
|
|
8799
|
-
*
|
|
8800
|
-
*
|
|
8801
|
-
*
|
|
8679
|
+
* Connected children that never declared a subscription pattern set.
|
|
8680
|
+
*
|
|
8681
|
+
* Each one is a fail-open on BOTH planes: `childWantsEvent` returns true for
|
|
8682
|
+
* an undeclared child ({@link ChildEntry.eventPatterns} `=== null`), and
|
|
8683
|
+
* `aggregateEventInterest` returns `null` — accept everything — if ANY
|
|
8684
|
+
* connected child is undeclared, which disables the node's inbound cross-node
|
|
8685
|
+
* gate as well. **One legacy runner silently turns off the filtering for the
|
|
8686
|
+
* whole node**, and nothing reported it, so a zero here is as load-bearing as
|
|
8687
|
+
* a non-zero one.
|
|
8802
8688
|
*/
|
|
8803
|
-
|
|
8804
|
-
|
|
8805
|
-
|
|
8689
|
+
undeclaredChildCount() {
|
|
8690
|
+
let count = 0;
|
|
8691
|
+
for (const entry of this.children.values()) if (entry.eventPatterns === null) count += 1;
|
|
8692
|
+
return count;
|
|
8693
|
+
}
|
|
8806
8694
|
/**
|
|
8807
|
-
*
|
|
8808
|
-
*
|
|
8809
|
-
*
|
|
8810
|
-
*
|
|
8811
|
-
*
|
|
8695
|
+
* Does `entry` want to receive an event in `category`? Honours the fan-out
|
|
8696
|
+
* mode: `broadcast` → always; undeclared patterns (`null`) → always
|
|
8697
|
+
* (fail-open); else the child's declared set must match via `matchesPattern`
|
|
8698
|
+
* — the SAME function the child's own `deliverShared` map-key gate uses,
|
|
8699
|
+
* giving provable delivery parity. `shadow` is handled by the caller (it
|
|
8700
|
+
* counts the would-suppress but still sends).
|
|
8701
|
+
*
|
|
8702
|
+
* NOTE: this gate must NOT consult `RING_BUFFER_DENY_PATTERNS` /
|
|
8703
|
+
* `isHighFrequencyCategory` — those are ring-buffer STORAGE policy; a child
|
|
8704
|
+
* that explicitly subscribes to a ring-denied category must still RECEIVE it.
|
|
8812
8705
|
*/
|
|
8813
|
-
|
|
8706
|
+
childWantsEvent(entry, category) {
|
|
8707
|
+
if (this.fanoutMode === "broadcast") return true;
|
|
8708
|
+
return this.childSubscribesTo(entry, category);
|
|
8709
|
+
}
|
|
8814
8710
|
/**
|
|
8815
|
-
*
|
|
8816
|
-
*
|
|
8817
|
-
* child
|
|
8818
|
-
*
|
|
8711
|
+
* The SUBSCRIPTION answer alone, with no fan-out-mode override.
|
|
8712
|
+
*
|
|
8713
|
+
* {@link childWantsEvent} answers "will this child be sent the event", which
|
|
8714
|
+
* under `broadcast` is `true` for everything and therefore cannot say whether
|
|
8715
|
+
* anyone actually asked. The D467 census needs the second question, so the
|
|
8716
|
+
* two are separate functions and `broadcast` mode overrides only the first.
|
|
8819
8717
|
*/
|
|
8820
|
-
|
|
8821
|
-
|
|
8822
|
-
|
|
8823
|
-
constructor(options) {
|
|
8824
|
-
this.options = options;
|
|
8825
|
-
this.latestCaps = options.caps;
|
|
8718
|
+
childSubscribesTo(entry, category) {
|
|
8719
|
+
if (entry.eventPatterns === null) return true;
|
|
8720
|
+
return entry.eventPatterns.some((p) => matchesPattern(p, category));
|
|
8826
8721
|
}
|
|
8827
8722
|
/**
|
|
8828
|
-
*
|
|
8829
|
-
*
|
|
8830
|
-
*
|
|
8831
|
-
*
|
|
8832
|
-
*
|
|
8833
|
-
*
|
|
8834
|
-
*
|
|
8723
|
+
* D2: this node's aggregate cross-node event interest — the UNION of every
|
|
8724
|
+
* connected child's declared category patterns. Consumed by the parent's
|
|
8725
|
+
* `$event-bus` inbound gate (via `setNodeEventInterest`) so the node only
|
|
8726
|
+
* accepts cross-node (Moleculer) categories at least one local child wants.
|
|
8727
|
+
*
|
|
8728
|
+
* Returns `null` (fail-open — accept everything) when:
|
|
8729
|
+
* - NO child is currently connected (boot window before the first forked
|
|
8730
|
+
* child completes its UDS handshake, or every child has disconnected): a
|
|
8731
|
+
* node MUST NOT drop 100% of inbound cross-node events during that window,
|
|
8732
|
+
* and a child may connect imminently. This is distinct from the
|
|
8733
|
+
* declared-empty case below.
|
|
8734
|
+
* - ANY connected child is UNDECLARED (`eventPatterns === null`, a legacy
|
|
8735
|
+
* runner): a node cannot safely narrow its inbound set while a child's
|
|
8736
|
+
* real interest is unknown. Mirrors the per-child fail-open in
|
|
8737
|
+
* {@link childWantsEvent}, aggregated conservatively.
|
|
8738
|
+
*
|
|
8739
|
+
* Returns an EMPTY array only when ≥1 child is connected and EVERY connected
|
|
8740
|
+
* child declared an explicit empty set — a genuine "wants nothing". Reads the
|
|
8741
|
+
* live `children` map, so a child registering / disconnecting / sending
|
|
8742
|
+
* `event-sub` is reflected on the next call with no extra bookkeeping.
|
|
8835
8743
|
*/
|
|
8836
|
-
|
|
8837
|
-
this.
|
|
8838
|
-
|
|
8839
|
-
const
|
|
8840
|
-
|
|
8841
|
-
|
|
8842
|
-
if (this.channel !== null) {
|
|
8843
|
-
const msg = {
|
|
8844
|
-
kind: "event-sub",
|
|
8845
|
-
patterns: union
|
|
8846
|
-
};
|
|
8847
|
-
this.channel.emit(msg);
|
|
8848
|
-
}
|
|
8744
|
+
aggregateEventInterest() {
|
|
8745
|
+
if (this.children.size === 0) return null;
|
|
8746
|
+
const union = /* @__PURE__ */ new Set();
|
|
8747
|
+
for (const entry of this.children.values()) {
|
|
8748
|
+
if (entry.eventPatterns === null) return null;
|
|
8749
|
+
for (const p of entry.eventPatterns) union.add(p);
|
|
8849
8750
|
}
|
|
8751
|
+
return [...union];
|
|
8850
8752
|
}
|
|
8851
|
-
|
|
8852
|
-
|
|
8853
|
-
|
|
8854
|
-
|
|
8855
|
-
|
|
8753
|
+
statsFor(childId) {
|
|
8754
|
+
let s = this.childEventStats.get(childId);
|
|
8755
|
+
if (s === void 0) {
|
|
8756
|
+
s = {
|
|
8757
|
+
sent: 0,
|
|
8758
|
+
suppressed: 0
|
|
8759
|
+
};
|
|
8760
|
+
this.childEventStats.set(childId, s);
|
|
8761
|
+
}
|
|
8762
|
+
return s;
|
|
8856
8763
|
}
|
|
8857
|
-
/**
|
|
8858
|
-
|
|
8859
|
-
|
|
8860
|
-
|
|
8861
|
-
|
|
8764
|
+
/** Deduped INFO line whenever a child's declared pattern set changes. */
|
|
8765
|
+
logPatternSet(childId, patterns) {
|
|
8766
|
+
const key = patterns === null ? "<undeclared>" : JSON.stringify([...patterns].toSorted());
|
|
8767
|
+
if (this.loggedPatternSet.get(childId) === key) return;
|
|
8768
|
+
this.loggedPatternSet.set(childId, key);
|
|
8769
|
+
this.logger?.info("child event patterns", {
|
|
8770
|
+
childId,
|
|
8771
|
+
patterns: patterns ?? null
|
|
8772
|
+
});
|
|
8862
8773
|
}
|
|
8863
8774
|
/**
|
|
8864
|
-
*
|
|
8865
|
-
*
|
|
8866
|
-
*
|
|
8867
|
-
*
|
|
8775
|
+
* Child id that can service a call to `capName` (optionally addressing
|
|
8776
|
+
* `deviceId`), or null.
|
|
8777
|
+
*
|
|
8778
|
+
* `deviceId` is a routing HINT, not a hard filter. A singleton cap
|
|
8779
|
+
* (`pipeline-runner`, `stream-broker`, …) addresses devices through its
|
|
8780
|
+
* METHOD ARGUMENTS — `attachCamera({ deviceId })` is one provider serving
|
|
8781
|
+
* many cameras — so its descriptor carries no `deviceId`. Resolving on
|
|
8782
|
+
* `deviceId` alone would never match it and would force the call onto the
|
|
8783
|
+
* broker fallback (which then hangs in service discovery). Hence:
|
|
8784
|
+
* 1. exact device-scoped owner (native per-device caps where each child
|
|
8785
|
+
* owns a disjoint device subset) is preferred, then
|
|
8786
|
+
* 2. a singleton owner (deviceId-less descriptor) is the fallback.
|
|
8787
|
+
* A cap is globally singleton XOR device-scoped, so the two tiers never
|
|
8788
|
+
* compete for the same capName.
|
|
8868
8789
|
*/
|
|
8869
|
-
|
|
8870
|
-
this.
|
|
8790
|
+
resolveChildId(capName, deviceId) {
|
|
8791
|
+
const index = this.capIndex();
|
|
8792
|
+
if (deviceId !== void 0) {
|
|
8793
|
+
const deviceOwner = index.deviceOwner(capName, deviceId);
|
|
8794
|
+
if (deviceOwner !== null) return deviceOwner;
|
|
8795
|
+
}
|
|
8796
|
+
const candidates = index.singletonCandidates(capName);
|
|
8797
|
+
if (candidates.length === 0) return null;
|
|
8798
|
+
if (candidates.length === 1) return candidates[0];
|
|
8799
|
+
const preferredAddonId = this.getActiveSingletonAddonId?.(capName) ?? null;
|
|
8800
|
+
if (preferredAddonId !== null) {
|
|
8801
|
+
const preferredChildId = this.resolveChildIdForAddon?.(preferredAddonId) ?? preferredAddonId;
|
|
8802
|
+
if (candidates.includes(preferredChildId)) return preferredChildId;
|
|
8803
|
+
}
|
|
8804
|
+
return candidates[0];
|
|
8871
8805
|
}
|
|
8872
8806
|
/**
|
|
8873
|
-
*
|
|
8874
|
-
*
|
|
8875
|
-
*
|
|
8876
|
-
*
|
|
8807
|
+
* Publish one cap-usage observation, if a sink is wired.
|
|
8808
|
+
*
|
|
8809
|
+
* Cost discipline — this is on the hot path (D181: hub-main's event loop is
|
|
8810
|
+
* the cluster's only queue): no sink means one undefined check; with a sink
|
|
8811
|
+
* it is one object literal and one `Date.now()`, and the sink itself is O(1).
|
|
8812
|
+
* Wrapped so a broken observer can never fail the call it observes — the
|
|
8813
|
+
* registry swallows too, and BOTH matter: this catch also covers a sink that
|
|
8814
|
+
* is not the registry.
|
|
8877
8815
|
*/
|
|
8878
|
-
|
|
8879
|
-
|
|
8816
|
+
recordCapUsage(callerChildId, providerChildId, capName, methodName) {
|
|
8817
|
+
const sink = this.capUsageObserver;
|
|
8818
|
+
if (sink === void 0 || callerChildId === null) return;
|
|
8819
|
+
try {
|
|
8820
|
+
sink({
|
|
8821
|
+
callerAddonId: callerChildId,
|
|
8822
|
+
providerAddonId: providerChildId ?? "(unresolved: parent-routed)",
|
|
8823
|
+
capName,
|
|
8824
|
+
methodName,
|
|
8825
|
+
atMs: Date.now()
|
|
8826
|
+
});
|
|
8827
|
+
} catch {}
|
|
8880
8828
|
}
|
|
8881
8829
|
/**
|
|
8882
|
-
*
|
|
8883
|
-
*
|
|
8884
|
-
*
|
|
8885
|
-
*
|
|
8886
|
-
*
|
|
8830
|
+
* The cap index over the CURRENT children, rebuilt on first use after any
|
|
8831
|
+
* mutation of {@link children}.
|
|
8832
|
+
*
|
|
8833
|
+
* This replaced a linear walk of every child's cap manifest per
|
|
8834
|
+
* `cap-call-out`, measured at **7.1 % of hub-main's busy main thread**
|
|
8835
|
+
* across 39 children (D456). Why it did not exist before is the whole
|
|
8836
|
+
* design: what it indexes moves — a runner is replaced on every update, a
|
|
8837
|
+
* crash respawns one, a child re-registers with a replacement manifest after
|
|
8838
|
+
* init — and an index that names a dead child routes work into a socket
|
|
8839
|
+
* nobody is reading, which is strictly worse than a slow scan.
|
|
8840
|
+
*
|
|
8841
|
+
* So it is not maintained; it is DISCARDED. `buildCapChildIndex` is a pure
|
|
8842
|
+
* function of `children.values()`, the only two writers of that map
|
|
8843
|
+
* ({@link invalidateCapIndex}'s call sites) drop the index in the same
|
|
8844
|
+
* statement, and nothing else can construct one. There is no add-on-register
|
|
8845
|
+
* / remove-on-close path that could disagree with the map — i.e. no shadow
|
|
8846
|
+
* registry (D3): the map remains the single authority and this holds no fact
|
|
8847
|
+
* it does not.
|
|
8848
|
+
*
|
|
8849
|
+
* The operator's active-singleton preference is deliberately NOT indexed —
|
|
8850
|
+
* it changes without any child connecting or leaving, so `resolveChildId`
|
|
8851
|
+
* reads it live and the index only supplies the candidate list.
|
|
8887
8852
|
*/
|
|
8888
|
-
|
|
8889
|
-
this.
|
|
8853
|
+
capIndex() {
|
|
8854
|
+
if (this.capChildIndex === null) this.capChildIndex = buildCapChildIndex(this.children.values());
|
|
8855
|
+
return this.capChildIndex;
|
|
8856
|
+
}
|
|
8857
|
+
/** Drop the derived index. MUST follow every write to {@link children}. */
|
|
8858
|
+
invalidateCapIndex() {
|
|
8859
|
+
this.capChildIndex = null;
|
|
8890
8860
|
}
|
|
8891
8861
|
/**
|
|
8892
|
-
*
|
|
8893
|
-
*
|
|
8894
|
-
*
|
|
8895
|
-
*
|
|
8862
|
+
* Does the named child currently provide `(capName, deviceId?)`?
|
|
8863
|
+
*
|
|
8864
|
+
* Used by the hub proxy seam, which knows the EXACT addon (→ runner →
|
|
8865
|
+
* childId) a provider belongs to. Unlike `resolveChildId` (which picks the
|
|
8866
|
+
* first child owning `capName`), this targets one child — required for
|
|
8867
|
+
* COLLECTION caps (`addon-widgets-source`, …) where many children register
|
|
8868
|
+
* the same capName: routing by capName alone collapses every provider to
|
|
8869
|
+
* the first child. Same deviceId-as-hint semantics as `resolveChildId`:
|
|
8870
|
+
* a device-scoped descriptor matching `deviceId` OR a deviceId-less
|
|
8871
|
+
* (singleton/collection) descriptor counts.
|
|
8896
8872
|
*/
|
|
8897
|
-
|
|
8898
|
-
|
|
8873
|
+
/**
|
|
8874
|
+
* Is `childId` currently connected (has it completed its UDS handshake)?
|
|
8875
|
+
* Coarser than {@link childProvides}: it answers "is the child reachable
|
|
8876
|
+
* over UDS at all", regardless of which caps it has announced yet. Used by
|
|
8877
|
+
* the route-mount fallback to decide between the handler-stripped
|
|
8878
|
+
* `callAddonOnChild(target:'routes')` path (child reachable) and awaiting a
|
|
8879
|
+
* cap proxy's `getRoutes()` (child not yet UDS-registered).
|
|
8880
|
+
*/
|
|
8881
|
+
isChildKnown(childId) {
|
|
8882
|
+
return this.children.has(childId);
|
|
8883
|
+
}
|
|
8884
|
+
childProvides(childId, capName, deviceId) {
|
|
8885
|
+
const entry = this.children.get(childId);
|
|
8886
|
+
if (!entry) return false;
|
|
8887
|
+
if (deviceId !== void 0 && entry.caps.some((cap) => cap.capName === capName && cap.deviceId === deviceId)) return true;
|
|
8888
|
+
return entry.caps.some((cap) => cap.capName === capName && cap.deviceId === void 0);
|
|
8899
8889
|
}
|
|
8900
8890
|
/**
|
|
8901
|
-
*
|
|
8902
|
-
*
|
|
8891
|
+
* Per-request options for a cap call: the cap method's declared `timeoutMs`
|
|
8892
|
+
* when one exists, else `undefined` so the channel applies its default.
|
|
8893
|
+
* Mirrors `CapRouteResolver.callTimeout` for the remote plane.
|
|
8903
8894
|
*/
|
|
8904
|
-
|
|
8905
|
-
|
|
8895
|
+
capRequestOpts(input) {
|
|
8896
|
+
const declared = this.capTimeoutMs?.(input.capName, input.method);
|
|
8897
|
+
return typeof declared === "number" ? { timeoutMs: declared } : void 0;
|
|
8906
8898
|
}
|
|
8907
|
-
|
|
8908
|
-
|
|
8909
|
-
const
|
|
8910
|
-
|
|
8911
|
-
channel.
|
|
8912
|
-
const msg = body;
|
|
8913
|
-
if (msg.kind === "cap-call") {
|
|
8914
|
-
const args = payload !== void 0 ? unpackPayload(payload) : msg.args;
|
|
8915
|
-
const result = await this.options.dispatch({
|
|
8916
|
-
capName: msg.capName,
|
|
8917
|
-
method: msg.method,
|
|
8918
|
-
args,
|
|
8919
|
-
...msg.deviceId !== void 0 ? { deviceId: msg.deviceId } : {},
|
|
8920
|
-
...msg.addonId !== void 0 ? { addonId: msg.addonId } : {}
|
|
8921
|
-
});
|
|
8922
|
-
if (payload !== void 0 && result !== void 0) return new PayloadReply(void 0, packPayload(result));
|
|
8923
|
-
return result;
|
|
8924
|
-
}
|
|
8925
|
-
if (msg.kind === "addon-call") {
|
|
8926
|
-
if (this.addonCallHandler === null) throw new Error(`LocalChildClient: addon-call for "${msg.addonId}" arrived but no onAddonCall handler is registered`);
|
|
8927
|
-
return this.addonCallHandler({
|
|
8928
|
-
addonId: msg.addonId,
|
|
8929
|
-
target: msg.target,
|
|
8930
|
-
...msg.action !== void 0 ? { action: msg.action } : {},
|
|
8931
|
-
...msg.method !== void 0 ? { method: msg.method } : {},
|
|
8932
|
-
...msg.args !== void 0 ? { args: msg.args } : {},
|
|
8933
|
-
...msg.caller !== void 0 ? { caller: msg.caller } : {}
|
|
8934
|
-
});
|
|
8935
|
-
}
|
|
8936
|
-
throw new Error(`unknown parent request kind: ${msg.kind}`);
|
|
8937
|
-
});
|
|
8938
|
-
channel.onEvent((body) => {
|
|
8939
|
-
const msg = body;
|
|
8940
|
-
if (msg.kind === "event") {
|
|
8941
|
-
this.eventHandler?.(msg.event);
|
|
8942
|
-
return;
|
|
8943
|
-
}
|
|
8944
|
-
if (msg.kind === "set-log-level") this.setLogLevelHandler?.(msg.level);
|
|
8945
|
-
});
|
|
8946
|
-
const register = this.registerFrame(this.latestCaps);
|
|
8947
|
-
try {
|
|
8948
|
-
this.parentRawForward = ackAcceptsRawForward(await channel.request(register));
|
|
8949
|
-
} catch (err) {
|
|
8950
|
-
await client.close();
|
|
8951
|
-
throw err;
|
|
8952
|
-
}
|
|
8953
|
-
this.client = client;
|
|
8954
|
-
this.channel = channel;
|
|
8955
|
-
this.flushPending(channel);
|
|
8956
|
-
for (const cb of this.connectedHandlers) cb();
|
|
8899
|
+
/** Forward a cap method call to a SPECIFIC child by id over UDS; rejects if that child is absent. */
|
|
8900
|
+
async callCapOnChild(childId, input) {
|
|
8901
|
+
const entry = this.children.get(childId);
|
|
8902
|
+
if (!entry) throw new Error(`no local child "${childId}" for cap "${input.capName}"`);
|
|
8903
|
+
return entry.channel.request(this.toCapCall(input), this.capRequestOpts(input));
|
|
8957
8904
|
}
|
|
8958
|
-
/**
|
|
8959
|
-
|
|
8960
|
-
|
|
8961
|
-
|
|
8905
|
+
/** Forward a cap method call to the owning child over UDS; rejects if none. */
|
|
8906
|
+
async callCap(input) {
|
|
8907
|
+
const childId = this.resolveChildId(input.capName, input.deviceId);
|
|
8908
|
+
const entry = childId === null ? void 0 : this.children.get(childId);
|
|
8909
|
+
if (!entry) {
|
|
8910
|
+
const where = input.deviceId === void 0 ? "" : ` for device ${input.deviceId}`;
|
|
8911
|
+
throw new Error(`no local child provides cap "${input.capName}"${where}`);
|
|
8912
|
+
}
|
|
8913
|
+
return entry.channel.request(this.toCapCall(input), this.capRequestOpts(input));
|
|
8962
8914
|
}
|
|
8963
8915
|
/**
|
|
8964
|
-
*
|
|
8965
|
-
*
|
|
8966
|
-
* registered native (device-scoped) caps become routable over UDS.
|
|
8916
|
+
* Forward an ADDON-LEVEL call (routes / custom-action) to a SPECIFIC child
|
|
8917
|
+
* by id over UDS; rejects if that child is absent.
|
|
8967
8918
|
*
|
|
8968
|
-
*
|
|
8969
|
-
*
|
|
8970
|
-
*
|
|
8919
|
+
* The childId for a hub-local single-addon runner equals the addonId
|
|
8920
|
+
* (`resolveRunnerId` returns the addonId when no `execution.group` is
|
|
8921
|
+
* declared — no shipped addon declares one). Mirrors `callCapOnChild` for
|
|
8922
|
+
* the cap plane; carries the two surfaces the removed per-addon Moleculer
|
|
8923
|
+
* broker used to serve (`getRoutes` + `custom.<action>`).
|
|
8971
8924
|
*/
|
|
8972
|
-
async
|
|
8973
|
-
|
|
8974
|
-
if (
|
|
8975
|
-
|
|
8976
|
-
this.parentRawForward = ackAcceptsRawForward(await this.channel.request(this.registerFrame(caps)));
|
|
8925
|
+
async callAddonOnChild(childId, input) {
|
|
8926
|
+
const entry = this.children.get(childId);
|
|
8927
|
+
if (!entry) throw new Error(`no local child "${childId}" for addon-call (${input.target})`);
|
|
8928
|
+
return entry.channel.request(this.toAddonCall(input));
|
|
8977
8929
|
}
|
|
8978
|
-
/**
|
|
8979
|
-
|
|
8980
|
-
* re-carry so no separate re-sync can be missed — the subscription union
|
|
8981
|
-
* (omitted if nothing ever declared, which keeps the fail-open shape) and the
|
|
8982
|
-
* custom-action catalogs (omitted until the init loop produced them).
|
|
8983
|
-
*/
|
|
8984
|
-
registerFrame(caps) {
|
|
8930
|
+
/** Build the parent→child `addon-call` wire message from an addon-call input. */
|
|
8931
|
+
toAddonCall(input) {
|
|
8985
8932
|
return {
|
|
8986
|
-
kind: "
|
|
8987
|
-
|
|
8988
|
-
|
|
8989
|
-
...
|
|
8990
|
-
...
|
|
8991
|
-
|
|
8933
|
+
kind: "addon-call",
|
|
8934
|
+
addonId: input.addonId,
|
|
8935
|
+
target: input.target,
|
|
8936
|
+
...input.action !== void 0 ? { action: input.action } : {},
|
|
8937
|
+
...input.method !== void 0 ? { method: input.method } : {},
|
|
8938
|
+
...input.args !== void 0 ? { args: input.args } : {},
|
|
8939
|
+
...input.caller !== void 0 ? { caller: input.caller } : {}
|
|
8992
8940
|
};
|
|
8993
8941
|
}
|
|
8994
|
-
/**
|
|
8995
|
-
|
|
8996
|
-
|
|
8997
|
-
|
|
8998
|
-
|
|
8999
|
-
|
|
9000
|
-
|
|
9001
|
-
|
|
9002
|
-
|
|
8942
|
+
/** Build the parent→child `cap-call` wire message from a routing input. */
|
|
8943
|
+
toCapCall(input) {
|
|
8944
|
+
return {
|
|
8945
|
+
kind: "cap-call",
|
|
8946
|
+
capName: input.capName,
|
|
8947
|
+
method: input.method,
|
|
8948
|
+
args: input.args,
|
|
8949
|
+
...input.deviceId !== void 0 ? { deviceId: input.deviceId } : {},
|
|
8950
|
+
...input.addonId !== void 0 ? { addonId: input.addonId } : {}
|
|
9003
8951
|
};
|
|
9004
|
-
|
|
9005
|
-
|
|
9006
|
-
|
|
9007
|
-
|
|
9008
|
-
|
|
8952
|
+
}
|
|
8953
|
+
listChildren() {
|
|
8954
|
+
return [...this.children.values()].map((e) => ({
|
|
8955
|
+
childId: e.childId,
|
|
8956
|
+
caps: e.caps,
|
|
8957
|
+
incarnation: e.incarnation
|
|
8958
|
+
}));
|
|
8959
|
+
}
|
|
8960
|
+
/** Register the (single) child-registered handler. Only one handler is active at a time. */
|
|
8961
|
+
onChildRegistered(handler) {
|
|
8962
|
+
this.registeredHandler = handler;
|
|
9009
8963
|
}
|
|
9010
8964
|
/**
|
|
9011
|
-
*
|
|
9012
|
-
*
|
|
8965
|
+
* Register the (single) child-gone handler. Only one handler is active at a
|
|
8966
|
+
* time. The handler receives the {@link RegisteredChild.incarnation} of the
|
|
8967
|
+
* connection that closed — carry it into whatever teardown it triggers, so a
|
|
8968
|
+
* dead generation cannot tear down its successor's work.
|
|
9013
8969
|
*/
|
|
9014
|
-
|
|
9015
|
-
|
|
9016
|
-
kind: "log",
|
|
9017
|
-
...entry
|
|
9018
|
-
};
|
|
9019
|
-
if (this.channel !== null) this.channel.emit(msg);
|
|
9020
|
-
else this.pendingEmits.push({
|
|
9021
|
-
kind: "log",
|
|
9022
|
-
msg
|
|
9023
|
-
});
|
|
8970
|
+
onChildGone(handler) {
|
|
8971
|
+
this.goneHandler = handler;
|
|
9024
8972
|
}
|
|
9025
8973
|
/**
|
|
9026
|
-
*
|
|
9027
|
-
*
|
|
9028
|
-
*
|
|
8974
|
+
* Register the (single) child-event handler. Invoked when a child sends an
|
|
8975
|
+
* event via `LocalChildClient.emitEvent`. Only one handler is active at a
|
|
8976
|
+
* time; a new handler replaces the prior one. Pass `null` to clear the
|
|
8977
|
+
* handler entirely (used by the UDS event bridge disposer on shutdown).
|
|
9029
8978
|
*/
|
|
9030
|
-
|
|
9031
|
-
|
|
9032
|
-
return (await this.channel.request({ kind: "readiness-request" })).records;
|
|
8979
|
+
onChildEvent(handler) {
|
|
8980
|
+
this.eventHandler = handler;
|
|
9033
8981
|
}
|
|
9034
8982
|
/**
|
|
9035
|
-
*
|
|
9036
|
-
*
|
|
9037
|
-
* to its `onUnownedCall` fallback (the cluster CapabilityRegistry in
|
|
9038
|
-
* production). Throws if called before `start()`.
|
|
8983
|
+
* Register the (single) child-log handler. Invoked when a child sends a log
|
|
8984
|
+
* entry via `LocalChildClient.sendLog`. Only one handler is active.
|
|
9039
8985
|
*/
|
|
9040
|
-
|
|
9041
|
-
|
|
9042
|
-
|
|
9043
|
-
|
|
9044
|
-
|
|
9045
|
-
|
|
9046
|
-
|
|
9047
|
-
|
|
9048
|
-
|
|
9049
|
-
|
|
9050
|
-
|
|
9051
|
-
|
|
9052
|
-
|
|
9053
|
-
|
|
9054
|
-
|
|
9055
|
-
|
|
9056
|
-
|
|
9057
|
-
return reply instanceof PayloadReply ? unpackPayload(reply.payload) : reply;
|
|
9058
|
-
}
|
|
8986
|
+
onChildLog(handler) {
|
|
8987
|
+
this.logHandler = handler;
|
|
8988
|
+
}
|
|
8989
|
+
/**
|
|
8990
|
+
* Register the handler that supplies the authoritative readiness snapshot
|
|
8991
|
+
* when a child sends a `readiness-request`. Only one handler is active.
|
|
8992
|
+
*/
|
|
8993
|
+
onReadinessSnapshotRequest(handler) {
|
|
8994
|
+
this.readinessHandler = handler;
|
|
8995
|
+
}
|
|
8996
|
+
/**
|
|
8997
|
+
* Push a parent→child event to a specific child. Fire-and-forget (uses the
|
|
8998
|
+
* one-way `emit` path on the channel). No-op if the child is not connected.
|
|
8999
|
+
*/
|
|
9000
|
+
sendEventToChild(childId, event, sourceNodeId) {
|
|
9001
|
+
const entry = this.children.get(childId);
|
|
9002
|
+
if (entry === void 0) return;
|
|
9059
9003
|
const msg = {
|
|
9060
|
-
kind: "
|
|
9061
|
-
|
|
9062
|
-
|
|
9063
|
-
args: input.args,
|
|
9064
|
-
...routing
|
|
9004
|
+
kind: "event",
|
|
9005
|
+
event,
|
|
9006
|
+
sourceNodeId
|
|
9065
9007
|
};
|
|
9066
|
-
|
|
9067
|
-
}
|
|
9068
|
-
/** Disconnect from the parent. Safe to call before `start()` (no-op) and idempotent. */
|
|
9069
|
-
async close() {
|
|
9070
|
-
await this.client?.close();
|
|
9071
|
-
this.client = null;
|
|
9072
|
-
this.channel = null;
|
|
9008
|
+
entry.channel.emit(msg);
|
|
9073
9009
|
}
|
|
9074
|
-
};
|
|
9075
|
-
//#endregion
|
|
9076
|
-
//#region src/kernel/transport/cap-route.ts
|
|
9077
|
-
function buildMessage(capName, method, detail) {
|
|
9078
|
-
const call = method !== void 0 ? `${capName}.${method}` : capName;
|
|
9079
|
-
const target = detail.nodeId !== void 0 ? ` to node ${detail.nodeId}` : "";
|
|
9080
|
-
const rejectedStr = detail.rejected.map((r) => `${r.kind}=${r.why}`).join("; ");
|
|
9081
|
-
const rejectedClause = rejectedStr.length > 0 ? ` (rejected: ${rejectedStr})` : "";
|
|
9082
|
-
return `${call} not routable${target}: ${detail.reason}${rejectedClause}`;
|
|
9083
|
-
}
|
|
9084
|
-
var CapRouteError = class extends Error {
|
|
9085
|
-
reason;
|
|
9086
|
-
nodeId;
|
|
9087
|
-
rejected;
|
|
9088
9010
|
/**
|
|
9089
|
-
*
|
|
9090
|
-
*
|
|
9091
|
-
* Dispatchers wrapping transport errors MUST pass the original.
|
|
9011
|
+
* Push a parent→child event to every registered child, optionally skipping
|
|
9012
|
+
* one (the originating child, to avoid echo). Fire-and-forget.
|
|
9092
9013
|
*/
|
|
9093
|
-
|
|
9094
|
-
|
|
9095
|
-
|
|
9096
|
-
|
|
9097
|
-
|
|
9098
|
-
|
|
9014
|
+
broadcastEventToChildren(event, sourceNodeId, exceptChildId) {
|
|
9015
|
+
const msg = {
|
|
9016
|
+
kind: "event",
|
|
9017
|
+
event,
|
|
9018
|
+
sourceNodeId
|
|
9019
|
+
};
|
|
9020
|
+
let subscribed = 0;
|
|
9021
|
+
for (const entry of this.children.values()) {
|
|
9022
|
+
if (entry.childId === exceptChildId) continue;
|
|
9023
|
+
if (this.childSubscribesTo(entry, event.category)) subscribed += 1;
|
|
9024
|
+
const wants = this.childWantsEvent(entry, event.category);
|
|
9025
|
+
const stats = this.statsFor(entry.childId);
|
|
9026
|
+
if (!wants) stats.suppressed++;
|
|
9027
|
+
if (wants || this.fanoutMode === "shadow") {
|
|
9028
|
+
stats.sent++;
|
|
9029
|
+
entry.channel.emit(msg);
|
|
9030
|
+
}
|
|
9031
|
+
}
|
|
9032
|
+
this.recordCategoryBroadcast(event.category, subscribed);
|
|
9099
9033
|
}
|
|
9100
|
-
|
|
9101
|
-
|
|
9102
|
-
*
|
|
9103
|
-
|
|
9104
|
-
|
|
9105
|
-
|
|
9106
|
-
|
|
9107
|
-
|
|
9108
|
-
|
|
9109
|
-
|
|
9110
|
-
*
|
|
9111
|
-
* Singleton path (no nodeId):
|
|
9112
|
-
* 1. hub-in-process (hub provides in-process)
|
|
9113
|
-
* 2. hub-local-uds (a hub-local UDS child provides it)
|
|
9114
|
-
* 3. remote-moleculer (any online, non-hub node that knows it)
|
|
9115
|
-
* 4. → CapRouteError{reason:'no-provider', rejected: all considered routes}
|
|
9116
|
-
*
|
|
9117
|
-
* PURE: no side effects, no async, no broker/registry imports.
|
|
9118
|
-
*/
|
|
9119
|
-
function classifyCapRoute(capName, opts, snapshot) {
|
|
9120
|
-
const rejected = [];
|
|
9121
|
-
if (opts.nodeId !== void 0) return classifyExplicitNode(capName, opts.nodeId, opts.deviceId, snapshot, rejected);
|
|
9122
|
-
return classifySingleton(capName, opts.deviceId, snapshot, rejected);
|
|
9123
|
-
}
|
|
9124
|
-
function classifyExplicitNode(capName, nodeId, deviceId, snap, rejected) {
|
|
9125
|
-
if (nodeId === snap.hubNodeId) {
|
|
9126
|
-
const residentProvides = snap.hubResidentProvides;
|
|
9127
|
-
const providesOnHub = residentProvides ?? snap.hubInProcessProvides;
|
|
9128
|
-
const getRef = residentProvides !== void 0 ? snap.getHubResidentProviderRef : snap.getInProcessProviderRef;
|
|
9129
|
-
if (providesOnHub(capName)) {
|
|
9130
|
-
const ref = getRef?.(capName) ?? null;
|
|
9131
|
-
if (ref !== null) return {
|
|
9132
|
-
kind: "hub-in-process",
|
|
9133
|
-
capName,
|
|
9134
|
-
ref
|
|
9135
|
-
};
|
|
9136
|
-
rejected.push({
|
|
9137
|
-
kind: "hub-in-process",
|
|
9138
|
-
why: "provider ref not available in snapshot"
|
|
9139
|
-
});
|
|
9140
|
-
throw new CapRouteError(capName, void 0, {
|
|
9141
|
-
reason: "no-provider",
|
|
9142
|
-
nodeId,
|
|
9143
|
-
rejected
|
|
9144
|
-
});
|
|
9034
|
+
/**
|
|
9035
|
+
* Record one PRODUCED event against its category. Called once per broadcast,
|
|
9036
|
+
* outside the per-child loop, because production is what the census measures.
|
|
9037
|
+
*/
|
|
9038
|
+
recordCategoryBroadcast(category, subscribed) {
|
|
9039
|
+
const existing = this.categoryStats.get(category);
|
|
9040
|
+
if (existing !== void 0) {
|
|
9041
|
+
existing.broadcasts += 1;
|
|
9042
|
+
existing.wanted += subscribed;
|
|
9043
|
+
return;
|
|
9145
9044
|
}
|
|
9146
|
-
if (
|
|
9147
|
-
|
|
9148
|
-
if (
|
|
9149
|
-
|
|
9150
|
-
|
|
9151
|
-
|
|
9152
|
-
|
|
9153
|
-
|
|
9154
|
-
|
|
9155
|
-
|
|
9156
|
-
});
|
|
9157
|
-
throw new CapRouteError(capName, void 0, {
|
|
9158
|
-
reason: "no-provider",
|
|
9159
|
-
nodeId,
|
|
9160
|
-
rejected
|
|
9161
|
-
});
|
|
9045
|
+
if (this.categoryStats.size >= 256) {
|
|
9046
|
+
this.categoryOverflow += 1;
|
|
9047
|
+
if (!this.categoryOverflowLogged) {
|
|
9048
|
+
this.categoryOverflowLogged = true;
|
|
9049
|
+
this.logger?.info("event category census full — further new categories are unattributed", {
|
|
9050
|
+
max: 256,
|
|
9051
|
+
firstDropped: category
|
|
9052
|
+
});
|
|
9053
|
+
}
|
|
9054
|
+
return;
|
|
9162
9055
|
}
|
|
9163
|
-
|
|
9164
|
-
|
|
9165
|
-
|
|
9166
|
-
});
|
|
9167
|
-
rejected.push({
|
|
9168
|
-
kind: "hub-local-uds",
|
|
9169
|
-
why: "no hub-local child provides this cap"
|
|
9170
|
-
});
|
|
9171
|
-
throw new CapRouteError(capName, void 0, {
|
|
9172
|
-
reason: "no-provider",
|
|
9173
|
-
nodeId,
|
|
9174
|
-
rejected
|
|
9175
|
-
});
|
|
9176
|
-
}
|
|
9177
|
-
if (!snap.nodeOnline(nodeId)) {
|
|
9178
|
-
rejected.push({
|
|
9179
|
-
kind: "remote-moleculer",
|
|
9180
|
-
why: `node ${nodeId} is offline`
|
|
9181
|
-
});
|
|
9182
|
-
throw new CapRouteError(capName, void 0, {
|
|
9183
|
-
reason: "node-offline",
|
|
9184
|
-
nodeId,
|
|
9185
|
-
rejected
|
|
9056
|
+
this.categoryStats.set(category, {
|
|
9057
|
+
broadcasts: 1,
|
|
9058
|
+
wanted: subscribed
|
|
9186
9059
|
});
|
|
9187
9060
|
}
|
|
9188
|
-
|
|
9189
|
-
|
|
9190
|
-
|
|
9191
|
-
|
|
9192
|
-
|
|
9193
|
-
|
|
9061
|
+
/**
|
|
9062
|
+
* E2: Send a `set-log-level` control message to a specific child.
|
|
9063
|
+
* Returns `true` if the child is currently connected and the message was
|
|
9064
|
+
* emitted; `false` if the child is not connected (no-op). The `false`
|
|
9065
|
+
* return lets the caller (MoleculerService.setChildLogLevelByNodeId) fall
|
|
9066
|
+
* back to the Moleculer `$node-mgmt.setLogLevel` action for the node.
|
|
9067
|
+
* Mirrors the `$node-mgmt.setLogLevel` Moleculer action for UDS children.
|
|
9068
|
+
*/
|
|
9069
|
+
setChildLogLevel(childId, level) {
|
|
9070
|
+
const entry = this.children.get(childId);
|
|
9071
|
+
if (entry === void 0) return false;
|
|
9072
|
+
const msg = {
|
|
9073
|
+
kind: "set-log-level",
|
|
9074
|
+
level
|
|
9194
9075
|
};
|
|
9195
|
-
|
|
9196
|
-
|
|
9197
|
-
|
|
9076
|
+
entry.channel.emit(msg);
|
|
9077
|
+
return true;
|
|
9078
|
+
}
|
|
9079
|
+
async close() {
|
|
9080
|
+
await this.server.close();
|
|
9081
|
+
}
|
|
9082
|
+
onConnection(channel) {
|
|
9083
|
+
let childId = null;
|
|
9084
|
+
const incarnation = this.nextIncarnation++;
|
|
9085
|
+
channel.observeActions?.({
|
|
9086
|
+
label: capActionLabel,
|
|
9087
|
+
record: (label, reqBytes, resBytes) => {
|
|
9088
|
+
const peerId = childId ?? "(pre-register)";
|
|
9089
|
+
if (label === null) this.actionCensus.recordUnlabelled(peerId, reqBytes + resBytes);
|
|
9090
|
+
else this.actionCensus.record(peerId, label, reqBytes, resBytes);
|
|
9091
|
+
}
|
|
9198
9092
|
});
|
|
9199
|
-
|
|
9200
|
-
|
|
9201
|
-
|
|
9202
|
-
|
|
9093
|
+
channel.onEvent((body) => {
|
|
9094
|
+
const msg = body;
|
|
9095
|
+
if (msg.kind === "event") {
|
|
9096
|
+
if (childId !== null) this.eventHandler?.(childId, msg.event);
|
|
9097
|
+
return;
|
|
9098
|
+
}
|
|
9099
|
+
if (msg.kind === "log") {
|
|
9100
|
+
if (childId !== null) this.logHandler?.(childId, msg);
|
|
9101
|
+
return;
|
|
9102
|
+
}
|
|
9103
|
+
if (msg.kind === "event-sub") {
|
|
9104
|
+
if (childId === null) return;
|
|
9105
|
+
const entry = this.children.get(childId);
|
|
9106
|
+
if (entry === void 0) return;
|
|
9107
|
+
this.children.set(childId, {
|
|
9108
|
+
...entry,
|
|
9109
|
+
eventPatterns: msg.patterns
|
|
9110
|
+
});
|
|
9111
|
+
this.logPatternSet(childId, msg.patterns);
|
|
9112
|
+
return;
|
|
9113
|
+
}
|
|
9203
9114
|
});
|
|
9204
|
-
|
|
9205
|
-
|
|
9206
|
-
|
|
9207
|
-
|
|
9208
|
-
|
|
9209
|
-
|
|
9210
|
-
|
|
9211
|
-
|
|
9212
|
-
|
|
9213
|
-
|
|
9214
|
-
|
|
9215
|
-
|
|
9216
|
-
|
|
9217
|
-
|
|
9218
|
-
|
|
9219
|
-
|
|
9220
|
-
|
|
9221
|
-
|
|
9115
|
+
channel.onRequest(async (body, payload) => {
|
|
9116
|
+
const msg = body;
|
|
9117
|
+
if (msg.kind === "register") {
|
|
9118
|
+
if (childId !== null && childId !== msg.childId) throw new Error(`child attempted to change identity from "${childId}" to "${msg.childId}"`);
|
|
9119
|
+
childId = msg.childId;
|
|
9120
|
+
const eventPatterns = msg.eventPatterns ?? null;
|
|
9121
|
+
const superseded = this.children.get(msg.childId);
|
|
9122
|
+
if (superseded !== void 0 && superseded.channel !== channel) this.logger?.info("local child superseded by a new connection", {
|
|
9123
|
+
childId: msg.childId,
|
|
9124
|
+
supersededIncarnation: superseded.incarnation,
|
|
9125
|
+
incarnation
|
|
9126
|
+
});
|
|
9127
|
+
this.children.set(msg.childId, {
|
|
9128
|
+
childId: msg.childId,
|
|
9129
|
+
channel,
|
|
9130
|
+
caps: msg.caps,
|
|
9131
|
+
eventPatterns,
|
|
9132
|
+
incarnation,
|
|
9133
|
+
rawForward: msg.rawForward === true
|
|
9134
|
+
});
|
|
9135
|
+
this.invalidateCapIndex();
|
|
9136
|
+
this.logPatternSet(msg.childId, eventPatterns);
|
|
9137
|
+
this.registeredHandler({
|
|
9138
|
+
childId: msg.childId,
|
|
9139
|
+
caps: msg.caps,
|
|
9140
|
+
incarnation,
|
|
9141
|
+
...msg.customActions !== void 0 ? { customActions: msg.customActions } : {}
|
|
9142
|
+
});
|
|
9143
|
+
return {
|
|
9144
|
+
ok: true,
|
|
9145
|
+
rawForward: true
|
|
9146
|
+
};
|
|
9147
|
+
}
|
|
9148
|
+
if (msg.kind === "cap-call-out") {
|
|
9149
|
+
const out = msg;
|
|
9150
|
+
const hints = out.hints ?? liftRoutingHints(out.args);
|
|
9151
|
+
const materialise = () => payload !== void 0 ? unpackPayload(payload) : out.args;
|
|
9152
|
+
const buildInput = () => ({
|
|
9153
|
+
capName: out.capName,
|
|
9154
|
+
method: out.method,
|
|
9155
|
+
args: materialise(),
|
|
9156
|
+
...out.deviceId !== void 0 ? { deviceId: out.deviceId } : {},
|
|
9157
|
+
...out.nodeId !== void 0 ? { nodeId: out.nodeId } : {},
|
|
9158
|
+
...out.native === true ? { native: true } : {},
|
|
9159
|
+
...childId !== null ? { callerAddonId: childId } : {}
|
|
9160
|
+
});
|
|
9161
|
+
if (this.claimedStatus !== void 0 && out.method === "getStatus") {
|
|
9162
|
+
const claimed = await answerClaimedGetStatus(this.claimedStatus, {
|
|
9163
|
+
capName: out.capName,
|
|
9164
|
+
method: out.method,
|
|
9165
|
+
deviceId: out.deviceId ?? (payload === void 0 ? extractDeviceId(out.args) : void 0)
|
|
9166
|
+
});
|
|
9167
|
+
if (claimed !== null) {
|
|
9168
|
+
this.recordCapUsage(childId, null, out.capName, out.method);
|
|
9169
|
+
return claimed.slice;
|
|
9170
|
+
}
|
|
9171
|
+
}
|
|
9172
|
+
const pinnedNodeId = out.nodeId ?? hints.nodeId;
|
|
9173
|
+
const pinTargetsThisNode = pinnedNodeId !== void 0 && this.ownNodeId !== void 0 && pinnedNodeId === this.ownNodeId;
|
|
9174
|
+
const aggregated = pinnedNodeId === void 0 && this.isAggregatedCollectionMethod?.(out.capName, out.method) === true;
|
|
9175
|
+
const addonPinned = pinnedNodeId === void 0 && this.isAddonPinnedCall?.(out.capName, out.method, hints) === true;
|
|
9176
|
+
const target = !(out.native === true) && !aggregated && !addonPinned && (pinnedNodeId === void 0 || pinTargetsThisNode) ? this.resolveChildId(out.capName, out.deviceId) : null;
|
|
9177
|
+
this.recordCapUsage(childId, target, out.capName, out.method);
|
|
9178
|
+
const actionPeer = childId ?? "(pre-register)";
|
|
9179
|
+
const actionLabel = `${out.capName}.${out.method}`;
|
|
9180
|
+
if (target !== null) {
|
|
9181
|
+
if (!this.egressRoutedCaps.has(out.capName)) {
|
|
9182
|
+
this.egressRoutedCaps.add(out.capName);
|
|
9183
|
+
this.logger?.info("routed child egress over UDS", { capName: out.capName });
|
|
9184
|
+
}
|
|
9185
|
+
const entry = this.children.get(target);
|
|
9186
|
+
if (payload !== void 0 && entry !== void 0 && entry.rawForward) {
|
|
9187
|
+
this.forwardedRaw += 1;
|
|
9188
|
+
this.actionCensus.recordForward(actionPeer, actionLabel, "raw");
|
|
9189
|
+
const forward = {
|
|
9190
|
+
kind: "cap-call",
|
|
9191
|
+
capName: out.capName,
|
|
9192
|
+
method: out.method,
|
|
9193
|
+
args: void 0,
|
|
9194
|
+
...out.deviceId !== void 0 ? { deviceId: out.deviceId } : {}
|
|
9195
|
+
};
|
|
9196
|
+
const declared = this.capTimeoutMs?.(out.capName, out.method);
|
|
9197
|
+
return entry.channel.request(forward, {
|
|
9198
|
+
...typeof declared === "number" ? { timeoutMs: declared } : {},
|
|
9199
|
+
payload
|
|
9200
|
+
});
|
|
9201
|
+
}
|
|
9202
|
+
this.forwardedDecoded += 1;
|
|
9203
|
+
this.actionCensus.recordForward(actionPeer, actionLabel, "decoded");
|
|
9204
|
+
const input = buildInput();
|
|
9205
|
+
return entry !== void 0 ? this.callCapOnChild(target, input) : this.callCap(input);
|
|
9206
|
+
}
|
|
9207
|
+
this.forwardedDecoded += 1;
|
|
9208
|
+
this.actionCensus.recordForward(actionPeer, actionLabel, "decoded");
|
|
9209
|
+
if (this.onUnownedCall !== void 0) return this.onUnownedCall(buildInput());
|
|
9210
|
+
throw new Error(`${UDS_NO_ROUTE_PREFIX}: cap-call-out has no local provider for "${out.capName}" and no fallback`);
|
|
9211
|
+
}
|
|
9212
|
+
if (msg.kind === "readiness-request") return {
|
|
9213
|
+
kind: "readiness-snapshot",
|
|
9214
|
+
records: this.readinessHandler?.() ?? []
|
|
9215
|
+
};
|
|
9216
|
+
throw new Error(`unknown child request kind: ${msg.kind}`);
|
|
9222
9217
|
});
|
|
9223
|
-
|
|
9224
|
-
|
|
9225
|
-
|
|
9226
|
-
|
|
9227
|
-
|
|
9228
|
-
|
|
9229
|
-
|
|
9230
|
-
|
|
9231
|
-
|
|
9232
|
-
|
|
9233
|
-
|
|
9234
|
-
|
|
9235
|
-
|
|
9236
|
-
|
|
9218
|
+
channel.onClose(() => {
|
|
9219
|
+
if (childId === null) return;
|
|
9220
|
+
const entry = this.children.get(childId);
|
|
9221
|
+
if (entry === void 0) return;
|
|
9222
|
+
if (entry.channel !== channel) {
|
|
9223
|
+
this.logger?.info("ignoring close of a superseded local child connection", {
|
|
9224
|
+
childId,
|
|
9225
|
+
incarnation,
|
|
9226
|
+
liveIncarnation: entry.incarnation
|
|
9227
|
+
});
|
|
9228
|
+
return;
|
|
9229
|
+
}
|
|
9230
|
+
this.children.delete(childId);
|
|
9231
|
+
this.invalidateCapIndex();
|
|
9232
|
+
this.childEventStats.delete(childId);
|
|
9233
|
+
this.actionCensus.forget(childId);
|
|
9234
|
+
this.loggedPatternSet.delete(childId);
|
|
9235
|
+
this.goneHandler(childId, incarnation);
|
|
9237
9236
|
});
|
|
9238
|
-
}
|
|
9239
|
-
|
|
9240
|
-
why: "no hub-local child provides this cap"
|
|
9241
|
-
});
|
|
9242
|
-
const knownNodes = snap.listKnownNodeIds?.() ?? [];
|
|
9243
|
-
for (const nodeId of knownNodes) if (nodeId !== snap.hubNodeId && snap.nodeOnline(nodeId) && snap.nodeKnowsCap(nodeId, capName)) return {
|
|
9244
|
-
kind: "remote-moleculer",
|
|
9245
|
-
capName,
|
|
9246
|
-
nodeId
|
|
9247
|
-
};
|
|
9248
|
-
rejected.push({
|
|
9249
|
-
kind: "remote-moleculer",
|
|
9250
|
-
why: "no online remote node knows this cap"
|
|
9251
|
-
});
|
|
9252
|
-
throw new CapRouteError(capName, void 0, {
|
|
9253
|
-
reason: "no-provider",
|
|
9254
|
-
rejected
|
|
9255
|
-
});
|
|
9256
|
-
}
|
|
9237
|
+
}
|
|
9238
|
+
};
|
|
9257
9239
|
//#endregion
|
|
9258
|
-
//#region src/kernel/
|
|
9259
|
-
/**
|
|
9260
|
-
|
|
9261
|
-
|
|
9262
|
-
|
|
9263
|
-
function isDiscoveryError(err) {
|
|
9264
|
-
if (typeof err !== "object" || err === null) return false;
|
|
9265
|
-
const type = err.type;
|
|
9266
|
-
return typeof type === "string" && DISCOVERY_ERROR_TYPES.has(type);
|
|
9240
|
+
//#region src/kernel/transport/local-child-client.ts
|
|
9241
|
+
/** Did the parent's `register` answer say it accepts payload-borne args? */
|
|
9242
|
+
function ackAcceptsRawForward(ack) {
|
|
9243
|
+
if (ack === null || typeof ack !== "object") return false;
|
|
9244
|
+
return ack.rawForward === true;
|
|
9267
9245
|
}
|
|
9268
9246
|
/**
|
|
9269
|
-
*
|
|
9270
|
-
*
|
|
9271
|
-
|
|
9272
|
-
|
|
9273
|
-
try {
|
|
9274
|
-
return await broker.call(action, params, opts);
|
|
9275
|
-
} catch (err) {
|
|
9276
|
-
if (!isDiscoveryError(err)) throw err;
|
|
9277
|
-
await broker.waitForServices([serviceName], discoveryTimeoutMs);
|
|
9278
|
-
return await broker.call(action, params, opts);
|
|
9279
|
-
}
|
|
9280
|
-
}
|
|
9281
|
-
//#endregion
|
|
9282
|
-
//#region src/kernel/transport/cap-route-resolver.ts
|
|
9283
|
-
/** The Moleculer service name that Task 6's agent registers. */
|
|
9284
|
-
var AGENT_CAP_FWD_SERVICE = "$agent-cap-fwd";
|
|
9285
|
-
/** The Moleculer action (service.action) for agent cap forwarding. */
|
|
9286
|
-
var AGENT_CAP_FWD_ACTION = `${AGENT_CAP_FWD_SERVICE}.forward`;
|
|
9287
|
-
/** Default timeout for remote Moleculer cap calls (ms). */
|
|
9288
|
-
var REMOTE_CALL_TIMEOUT_MS = 6e4;
|
|
9289
|
-
function extractDeviceId(args) {
|
|
9290
|
-
if (args === null || typeof args !== "object") return void 0;
|
|
9291
|
-
const raw = Reflect.get(args, "deviceId");
|
|
9292
|
-
return typeof raw === "number" ? raw : void 0;
|
|
9293
|
-
}
|
|
9294
|
-
/**
|
|
9295
|
-
* Extract an inline `nodeId` string from a cap call's args — the per-call node
|
|
9296
|
-
* pin a forked addon expresses by carrying `nodeId` in the input (the SAME way
|
|
9297
|
-
* device-scoped caps carry `deviceId`, and the way the generated cap-router on
|
|
9298
|
-
* the hub already honours an inline pin). Mirrors {@link extractDeviceId}.
|
|
9247
|
+
* Child side of the local UDS transport. Connects to its parent (hub or
|
|
9248
|
+
* agent), registers its cap manifest, and serves parent→child cap calls by
|
|
9249
|
+
* delegating to `dispatch`. The provider implementation lives in the child;
|
|
9250
|
+
* only routing keys + call arguments cross the wire.
|
|
9299
9251
|
*
|
|
9300
|
-
*
|
|
9301
|
-
*
|
|
9302
|
-
*
|
|
9303
|
-
*
|
|
9304
|
-
*
|
|
9305
|
-
*
|
|
9306
|
-
* `
|
|
9307
|
-
|
|
9308
|
-
function extractNodeId(args) {
|
|
9309
|
-
if (args === null || typeof args !== "object") return void 0;
|
|
9310
|
-
const raw = Reflect.get(args, "nodeId");
|
|
9311
|
-
return typeof raw === "string" && raw.length > 0 ? raw : void 0;
|
|
9312
|
-
}
|
|
9313
|
-
/**
|
|
9314
|
-
* Merge `callerAddonId` into `args` as an extra field — the SAME "provider
|
|
9315
|
-
* destructures what it needs and ignores the rest" convention
|
|
9316
|
-
* {@link extractNodeId} already relies on for the inline `nodeId` pin.
|
|
9317
|
-
* `undefined` (the overwhelmingly common case: no caller hint, or the census
|
|
9318
|
-
* consuming it is disarmed) returns `args` unchanged — no allocation. A
|
|
9319
|
-
* non-object `args` (or `null`/array) is returned unchanged too: there is
|
|
9320
|
-
* nowhere to attach an extra key without changing the payload's shape, and a
|
|
9321
|
-
* provider taking a bare value never reads `callerAddonId` off it anyway.
|
|
9252
|
+
* Additional channels beyond cap-call:
|
|
9253
|
+
* - `emitEvent` fire-and-forget event toward the parent
|
|
9254
|
+
* - `sendLog` fire-and-forget log entry toward the parent
|
|
9255
|
+
* - `requestReadinessSnapshot` request/response snapshot of readiness records
|
|
9256
|
+
* - `onEvent` register a handler for parent→child events
|
|
9257
|
+
*
|
|
9258
|
+
* Events and logs emitted before `start()` resolves are buffered and flushed
|
|
9259
|
+
* on connect (mirrors the `updateCaps`/`latestCaps` pattern).
|
|
9322
9260
|
*/
|
|
9323
|
-
|
|
9324
|
-
|
|
9325
|
-
|
|
9326
|
-
|
|
9327
|
-
|
|
9328
|
-
|
|
9329
|
-
|
|
9330
|
-
|
|
9331
|
-
|
|
9332
|
-
|
|
9333
|
-
|
|
9334
|
-
|
|
9335
|
-
|
|
9336
|
-
|
|
9337
|
-
|
|
9338
|
-
|
|
9339
|
-
|
|
9340
|
-
|
|
9341
|
-
|
|
9342
|
-
|
|
9343
|
-
|
|
9344
|
-
|
|
9345
|
-
|
|
9346
|
-
|
|
9347
|
-
|
|
9348
|
-
|
|
9261
|
+
var LocalChildClient = class {
|
|
9262
|
+
options;
|
|
9263
|
+
client = null;
|
|
9264
|
+
channel = null;
|
|
9265
|
+
/**
|
|
9266
|
+
* The cap set `start()` will register, kept current by `updateCaps`. Native
|
|
9267
|
+
* device caps register on device-restore, which can race AHEAD of the UDS
|
|
9268
|
+
* connect — buffering here lets a pre-start `updateCaps` survive (start sends
|
|
9269
|
+
* the latest set) instead of being lost or throwing.
|
|
9270
|
+
*/
|
|
9271
|
+
latestCaps;
|
|
9272
|
+
/**
|
|
9273
|
+
* Per-owner (addonId) category-pattern subscription sets, and their union.
|
|
9274
|
+
* The union is declared to the parent (in `RegisterMessage.eventPatterns`
|
|
9275
|
+
* pre/at-connect, or via an `event-sub` emit post-connect) so the parent can
|
|
9276
|
+
* subscription-filter its event fan-out. Per-owner keying keeps the union
|
|
9277
|
+
* correct if multiple event buses ever share one client (group-runner case).
|
|
9278
|
+
*/
|
|
9279
|
+
patternsByOwner = /* @__PURE__ */ new Map();
|
|
9280
|
+
latestEventPatterns = [];
|
|
9281
|
+
/**
|
|
9282
|
+
* Whether `updateEventPatterns` has ever been called. A client that never
|
|
9283
|
+
* declared a subscription set (no event bus wired — e.g. a pure cap-call
|
|
9284
|
+
* runner) omits `eventPatterns` from its register frame entirely, so the
|
|
9285
|
+
* parent treats it as UNDECLARED and fails OPEN (broadcast-all). Once any
|
|
9286
|
+
* owner declares (in production every addon context's framework
|
|
9287
|
+
* subscriptions do), the register carries the real union — even if empty.
|
|
9288
|
+
*/
|
|
9289
|
+
hasDeclaredEventPatterns = false;
|
|
9290
|
+
/**
|
|
9291
|
+
* The custom-action catalogs of every hosted addon, once the runner's init
|
|
9292
|
+
* loop has produced them (`updateCaps(caps, catalogs)` at post-init). `null`
|
|
9293
|
+
* until then, and OMITTED from the register frame while null: the parent
|
|
9294
|
+
* reads absence as "not described yet", and a pre-init register must never
|
|
9295
|
+
* look like "this child declares no actions". Kept on the client so every
|
|
9296
|
+
* later re-register (native-cap change, reconnect) re-carries it unchanged.
|
|
9297
|
+
*/
|
|
9298
|
+
latestCustomActions = null;
|
|
9299
|
+
/** Events and logs queued while the channel is not yet open. */
|
|
9300
|
+
pendingEmits = [];
|
|
9301
|
+
/**
|
|
9302
|
+
* Whether the parent acknowledged raw-forward at `register`. Until it has,
|
|
9303
|
+
* `callOut` sends args inline exactly as every child always did — a legacy
|
|
9304
|
+
* parent would decode a payload-borne call to `args: undefined` and run the
|
|
9305
|
+
* provider on nothing, silently. Re-read on every re-register.
|
|
9306
|
+
*/
|
|
9307
|
+
parentRawForward = false;
|
|
9308
|
+
/** Handler for parent→child events. Registered via `onEvent`. */
|
|
9309
|
+
eventHandler = null;
|
|
9310
|
+
/**
|
|
9311
|
+
* Handler for parent→child addon-level calls (`routes` / `custom`).
|
|
9312
|
+
* Registered via `onAddonCall`. Resolves the loaded addon instance and
|
|
9313
|
+
* invokes its `addon-routes.getRoutes()` or its custom-action handler.
|
|
9314
|
+
* Replaces the per-addon Moleculer `getRoutes` / `custom.<action>` actions
|
|
9315
|
+
* removed in F1/F2. Only one handler is active at a time.
|
|
9316
|
+
*/
|
|
9317
|
+
addonCallHandler = null;
|
|
9318
|
+
/**
|
|
9319
|
+
* Handler for `set-log-level` messages pushed by the parent.
|
|
9320
|
+
* Registered via `onSetLogLevel`. The handler applies the new level to the
|
|
9321
|
+
* child's local Moleculer logger (mirrors `$node-mgmt.setLogLevel`).
|
|
9322
|
+
* E2: wired in `addon-runner.ts` to forward to the broker logger.
|
|
9323
|
+
*/
|
|
9324
|
+
setLogLevelHandler = null;
|
|
9325
|
+
/** Callbacks registered via `onConnected`. Fired on every (re)connect. */
|
|
9326
|
+
connectedHandlers = [];
|
|
9327
|
+
constructor(options) {
|
|
9328
|
+
this.options = options;
|
|
9329
|
+
this.latestCaps = options.caps;
|
|
9330
|
+
}
|
|
9331
|
+
/**
|
|
9332
|
+
* Declare an owner's live category-pattern subscription set. Stores it
|
|
9333
|
+
* per-owner (keyed by `ownerId` = addonId), recomputes the union, and — only
|
|
9334
|
+
* if the union changed — declares it to the parent:
|
|
9335
|
+
* - pre-connect: buffered only; `start()`'s register frame carries the set.
|
|
9336
|
+
* - post-connect: emitted as a fire-and-forget `event-sub` (full-set
|
|
9337
|
+
* replace).
|
|
9338
|
+
* Idempotent on an unchanged union (no redundant `event-sub` frames).
|
|
9339
|
+
*/
|
|
9340
|
+
updateEventPatterns(ownerId, patterns) {
|
|
9341
|
+
this.hasDeclaredEventPatterns = true;
|
|
9342
|
+
this.patternsByOwner.set(ownerId, patterns);
|
|
9343
|
+
const union = this.computePatternUnion();
|
|
9344
|
+
if (!this.patternsEqual(union, this.latestEventPatterns)) {
|
|
9345
|
+
this.latestEventPatterns = union;
|
|
9346
|
+
if (this.channel !== null) {
|
|
9347
|
+
const msg = {
|
|
9348
|
+
kind: "event-sub",
|
|
9349
|
+
patterns: union
|
|
9350
|
+
};
|
|
9351
|
+
this.channel.emit(msg);
|
|
9352
|
+
}
|
|
9353
|
+
}
|
|
9354
|
+
}
|
|
9355
|
+
/** Sorted, de-duplicated union across all owners' pattern sets. */
|
|
9356
|
+
computePatternUnion() {
|
|
9357
|
+
const all = /* @__PURE__ */ new Set();
|
|
9358
|
+
for (const patterns of this.patternsByOwner.values()) for (const p of patterns) all.add(p);
|
|
9359
|
+
return [...all].toSorted();
|
|
9360
|
+
}
|
|
9361
|
+
/** Order-insensitive equality — both inputs are already sorted unions here. */
|
|
9362
|
+
patternsEqual(a, b) {
|
|
9363
|
+
if (a.length !== b.length) return false;
|
|
9364
|
+
for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
|
|
9365
|
+
return true;
|
|
9366
|
+
}
|
|
9367
|
+
/**
|
|
9368
|
+
* Register a callback that fires each time the client successfully connects
|
|
9369
|
+
* (or reconnects) to its parent. Multiple handlers may be registered; all
|
|
9370
|
+
* are called in registration order. Used by readiness-context in UDS mode
|
|
9371
|
+
* to trigger a snapshot hydrate on connect/reconnect.
|
|
9372
|
+
*/
|
|
9373
|
+
onConnected(handler) {
|
|
9374
|
+
this.connectedHandlers.push(handler);
|
|
9375
|
+
}
|
|
9376
|
+
/**
|
|
9377
|
+
* Register a handler for events pushed from the parent to this child.
|
|
9378
|
+
* Must be called before `start()` to avoid missing early events (though
|
|
9379
|
+
* registration after start is also safe for events not yet delivered).
|
|
9380
|
+
* Replaces any previously registered handler.
|
|
9381
|
+
*/
|
|
9382
|
+
onEvent(handler) {
|
|
9383
|
+
this.eventHandler = handler;
|
|
9384
|
+
}
|
|
9385
|
+
/**
|
|
9386
|
+
* Register the handler invoked when the parent sends an `addon-call` (the
|
|
9387
|
+
* addon-level routes / custom-action plane). The addon-runner wires this to
|
|
9388
|
+
* resolve the loaded addon by id and dispatch to its `getRoutes()` or its
|
|
9389
|
+
* custom-action handler. Safe to register before or after `start()`;
|
|
9390
|
+
* replaces any prior handler.
|
|
9391
|
+
*/
|
|
9392
|
+
onAddonCall(handler) {
|
|
9393
|
+
this.addonCallHandler = handler;
|
|
9394
|
+
}
|
|
9395
|
+
/**
|
|
9396
|
+
* E2: Register a handler invoked when the parent sends a `set-log-level`
|
|
9397
|
+
* message to this child. The handler should forward the new level to the
|
|
9398
|
+
* child's local Moleculer broker logger (mirrors `$node-mgmt.setLogLevel`).
|
|
9399
|
+
* Safe to register before or after `start()`. Replaces any prior handler.
|
|
9400
|
+
*/
|
|
9401
|
+
onSetLogLevel(handler) {
|
|
9402
|
+
this.setLogLevelHandler = handler;
|
|
9403
|
+
}
|
|
9404
|
+
/**
|
|
9405
|
+
* True once `start()` has successfully connected and registered. Used by
|
|
9406
|
+
* callers (event-bus/logger bridges) to know the channel is live.
|
|
9407
|
+
*/
|
|
9408
|
+
get isConnected() {
|
|
9409
|
+
return this.channel !== null;
|
|
9410
|
+
}
|
|
9411
|
+
async start() {
|
|
9412
|
+
if (this.client !== null) throw new Error("LocalChildClient: already started — call close() first");
|
|
9413
|
+
const client = createLocalTransport().createClient(this.options.nodeId);
|
|
9414
|
+
const channel = await client.connect();
|
|
9415
|
+
channel.onRequest(async (body, payload) => {
|
|
9416
|
+
const msg = body;
|
|
9417
|
+
if (msg.kind === "cap-call") {
|
|
9418
|
+
const args = payload !== void 0 ? unpackPayload(payload) : msg.args;
|
|
9419
|
+
const result = await this.options.dispatch({
|
|
9420
|
+
capName: msg.capName,
|
|
9421
|
+
method: msg.method,
|
|
9422
|
+
args,
|
|
9423
|
+
...msg.deviceId !== void 0 ? { deviceId: msg.deviceId } : {},
|
|
9424
|
+
...msg.addonId !== void 0 ? { addonId: msg.addonId } : {}
|
|
9425
|
+
});
|
|
9426
|
+
if (payload !== void 0 && result !== void 0) return new PayloadReply(void 0, packPayload(result));
|
|
9427
|
+
return result;
|
|
9428
|
+
}
|
|
9429
|
+
if (msg.kind === "addon-call") {
|
|
9430
|
+
if (this.addonCallHandler === null) throw new Error(`LocalChildClient: addon-call for "${msg.addonId}" arrived but no onAddonCall handler is registered`);
|
|
9431
|
+
return this.addonCallHandler({
|
|
9432
|
+
addonId: msg.addonId,
|
|
9433
|
+
target: msg.target,
|
|
9434
|
+
...msg.action !== void 0 ? { action: msg.action } : {},
|
|
9435
|
+
...msg.method !== void 0 ? { method: msg.method } : {},
|
|
9436
|
+
...msg.args !== void 0 ? { args: msg.args } : {},
|
|
9437
|
+
...msg.caller !== void 0 ? { caller: msg.caller } : {}
|
|
9438
|
+
});
|
|
9439
|
+
}
|
|
9440
|
+
throw new Error(`unknown parent request kind: ${msg.kind}`);
|
|
9441
|
+
});
|
|
9442
|
+
channel.onEvent((body) => {
|
|
9443
|
+
const msg = body;
|
|
9444
|
+
if (msg.kind === "event") {
|
|
9445
|
+
this.eventHandler?.(msg.event);
|
|
9446
|
+
return;
|
|
9447
|
+
}
|
|
9448
|
+
if (msg.kind === "set-log-level") this.setLogLevelHandler?.(msg.level);
|
|
9449
|
+
});
|
|
9450
|
+
const register = this.registerFrame(this.latestCaps);
|
|
9451
|
+
try {
|
|
9452
|
+
this.parentRawForward = ackAcceptsRawForward(await channel.request(register));
|
|
9453
|
+
} catch (err) {
|
|
9454
|
+
await client.close();
|
|
9455
|
+
throw err;
|
|
9456
|
+
}
|
|
9457
|
+
this.client = client;
|
|
9458
|
+
this.channel = channel;
|
|
9459
|
+
this.flushPending(channel);
|
|
9460
|
+
for (const cb of this.connectedHandlers) cb();
|
|
9461
|
+
}
|
|
9462
|
+
/** Flush buffered pre-start emits over the now-open channel. */
|
|
9463
|
+
flushPending(channel) {
|
|
9464
|
+
for (const item of this.pendingEmits) channel.emit(item.msg);
|
|
9465
|
+
this.pendingEmits.length = 0;
|
|
9466
|
+
}
|
|
9467
|
+
/**
|
|
9468
|
+
* Re-send the cap manifest to the parent, atomically replacing the child's
|
|
9469
|
+
* registered descriptor set. Call this after device-restore so newly
|
|
9470
|
+
* registered native (device-scoped) caps become routable over UDS.
|
|
9471
|
+
*
|
|
9472
|
+
* Safe to call before `start()`: the new set is buffered and `start()` sends
|
|
9473
|
+
* it (device-restore can race ahead of the UDS connect). After `start()`,
|
|
9474
|
+
* the set is sent immediately.
|
|
9475
|
+
*/
|
|
9476
|
+
async updateCaps(caps, customActions) {
|
|
9477
|
+
this.latestCaps = caps;
|
|
9478
|
+
if (customActions !== void 0) this.latestCustomActions = customActions;
|
|
9479
|
+
if (this.channel === null) return;
|
|
9480
|
+
this.parentRawForward = ackAcceptsRawForward(await this.channel.request(this.registerFrame(caps)));
|
|
9349
9481
|
}
|
|
9350
|
-
/**
|
|
9351
|
-
|
|
9352
|
-
|
|
9482
|
+
/**
|
|
9483
|
+
* The register frame: caps, plus everything a re-register must atomically
|
|
9484
|
+
* re-carry so no separate re-sync can be missed — the subscription union
|
|
9485
|
+
* (omitted if nothing ever declared, which keeps the fail-open shape) and the
|
|
9486
|
+
* custom-action catalogs (omitted until the init loop produced them).
|
|
9487
|
+
*/
|
|
9488
|
+
registerFrame(caps) {
|
|
9489
|
+
return {
|
|
9490
|
+
kind: "register",
|
|
9491
|
+
childId: this.options.childId,
|
|
9492
|
+
caps,
|
|
9493
|
+
...this.hasDeclaredEventPatterns ? { eventPatterns: this.latestEventPatterns } : {},
|
|
9494
|
+
...this.latestCustomActions !== null ? { customActions: this.latestCustomActions } : {},
|
|
9495
|
+
rawForward: true
|
|
9496
|
+
};
|
|
9353
9497
|
}
|
|
9354
|
-
|
|
9355
|
-
|
|
9498
|
+
/**
|
|
9499
|
+
* Fire-and-forget: send a system event to the parent for forwarding to the
|
|
9500
|
+
* hub event bus. Safe to call before `start()` — events are buffered and
|
|
9501
|
+
* flushed on connect.
|
|
9502
|
+
*/
|
|
9503
|
+
emitEvent(event) {
|
|
9504
|
+
const msg = {
|
|
9505
|
+
kind: "event",
|
|
9506
|
+
event
|
|
9507
|
+
};
|
|
9508
|
+
if (this.channel !== null) this.channel.emit(msg);
|
|
9509
|
+
else this.pendingEmits.push({
|
|
9510
|
+
kind: "event",
|
|
9511
|
+
msg
|
|
9512
|
+
});
|
|
9356
9513
|
}
|
|
9357
9514
|
/**
|
|
9358
|
-
*
|
|
9359
|
-
*
|
|
9360
|
-
*
|
|
9361
|
-
* `resolveCapRoute` gives Priority 1 to `hub-in-process`: when an in-hub
|
|
9362
|
-
* provider (e.g. a wrapper) is registered for the cap, the route always
|
|
9363
|
-
* classifies as `hub-in-process` and the hub-local-uds NATIVE child is never
|
|
9364
|
-
* reached. That is correct for the generic dispatch path (the wrapper is the
|
|
9365
|
-
* active provider), but WRONG for the native-cap fallback
|
|
9366
|
-
* (`setNativeFallback`), whose contract is to reach the NATIVE provider in
|
|
9367
|
-
* the forked vendor child so a wrapper can delegate to it. A wrapper cap
|
|
9368
|
-
* with a forked native (today: `snapshot`) otherwise resolves to the wrapper
|
|
9369
|
-
* itself, the native is never invoked, and the wrapper silently falls
|
|
9370
|
-
* through to its secondary strategy.
|
|
9371
|
-
*
|
|
9372
|
-
* This method consults ONLY the hub-local-child authority
|
|
9373
|
-
* (`hubLocalChildProvides` + `getHubLocalChildId`, both deviceId-aware), so
|
|
9374
|
-
* it returns the native child route regardless of any in-process shadow.
|
|
9375
|
-
* Returns null when no hub-local child owns `(capName, deviceId)` — the
|
|
9376
|
-
* caller then falls through to its remote-resolution branch.
|
|
9515
|
+
* Fire-and-forget: send a structured log entry to the parent. Safe to call
|
|
9516
|
+
* before `start()` — log entries are buffered and flushed on connect.
|
|
9377
9517
|
*/
|
|
9378
|
-
|
|
9379
|
-
|
|
9380
|
-
|
|
9381
|
-
|
|
9382
|
-
return {
|
|
9383
|
-
kind: "hub-local-uds",
|
|
9384
|
-
capName,
|
|
9385
|
-
childId
|
|
9518
|
+
sendLog(entry) {
|
|
9519
|
+
const msg = {
|
|
9520
|
+
kind: "log",
|
|
9521
|
+
...entry
|
|
9386
9522
|
};
|
|
9523
|
+
if (this.channel !== null) this.channel.emit(msg);
|
|
9524
|
+
else this.pendingEmits.push({
|
|
9525
|
+
kind: "log",
|
|
9526
|
+
msg
|
|
9527
|
+
});
|
|
9387
9528
|
}
|
|
9388
9529
|
/**
|
|
9389
|
-
*
|
|
9390
|
-
*
|
|
9391
|
-
*
|
|
9392
|
-
* provider). Only meaningful for `agent-child-forward` — a `remote-moleculer`
|
|
9393
|
-
* route already encodes the provider in its action name
|
|
9394
|
-
* (`${addonId}.${cap}.${method}`, via `NodeCapAuthority.getAddonId`), and a
|
|
9395
|
-
* `hub-local-uds` caller that already knows the addonId (`buildCapCallFn`)
|
|
9396
|
-
* talks to `LocalChildRegistry.callCapOnChild` directly rather than through
|
|
9397
|
-
* `dispatch`.
|
|
9398
|
-
* @param callerAddonId Optional out-of-band CALLER-identity hint — the
|
|
9399
|
-
* addon that ORIGINATED the call, distinct from `addonId` above (which
|
|
9400
|
-
* names the resolved PROVIDER, not the caller). Set by
|
|
9401
|
-
* `onUnownedCall` from `CapCallInput.callerAddonId`
|
|
9402
|
-
* (`LocalChildRegistry` stamped it off the registered `childId`). Only
|
|
9403
|
-
* the `hub-in-process` branch consumes it — merged into `args` so a
|
|
9404
|
-
* diagnostic tool deep in that provider (e.g. device-manager's
|
|
9405
|
-
* fleet-read census) can read it without every provider method changing
|
|
9406
|
-
* signature, the same "extra field the provider destructures or
|
|
9407
|
-
* ignores" convention `nodeId` already uses on this path.
|
|
9530
|
+
* Request the current readiness snapshot from the parent. Returns the
|
|
9531
|
+
* authoritative set of `IReadinessRegistryRecord` entries the hub holds.
|
|
9532
|
+
* Requires `start()` to have been called; throws with a clear message if not.
|
|
9408
9533
|
*/
|
|
9409
|
-
async
|
|
9410
|
-
|
|
9411
|
-
|
|
9412
|
-
} catch (err) {
|
|
9413
|
-
if (err instanceof CapRouteError) throw err;
|
|
9414
|
-
if (route.kind === "hub-in-process" || err instanceof PeerAnsweredError) throw err;
|
|
9415
|
-
const nodeId = this.routeNodeId(route);
|
|
9416
|
-
const cause = err instanceof Error ? err : new Error(String(err));
|
|
9417
|
-
throw new CapRouteError(route.capName, method, {
|
|
9418
|
-
reason: "transport-failed",
|
|
9419
|
-
nodeId,
|
|
9420
|
-
rejected: [{
|
|
9421
|
-
kind: route.kind,
|
|
9422
|
-
why: cause.message
|
|
9423
|
-
}]
|
|
9424
|
-
}, cause);
|
|
9425
|
-
}
|
|
9534
|
+
async requestReadinessSnapshot() {
|
|
9535
|
+
if (this.channel === null) throw new Error("LocalChildClient: requestReadinessSnapshot called before start()");
|
|
9536
|
+
return (await this.channel.request({ kind: "readiness-request" })).records;
|
|
9426
9537
|
}
|
|
9427
9538
|
/**
|
|
9428
|
-
*
|
|
9429
|
-
*
|
|
9539
|
+
* Ask the parent to execute a cap call this child does NOT own. The parent
|
|
9540
|
+
* routes to the owning local sibling over UDS, or — if no sibling owns it —
|
|
9541
|
+
* to its `onUnownedCall` fallback (the cluster CapabilityRegistry in
|
|
9542
|
+
* production). Throws if called before `start()`.
|
|
9430
9543
|
*/
|
|
9431
|
-
async
|
|
9432
|
-
|
|
9433
|
-
|
|
9434
|
-
|
|
9435
|
-
|
|
9436
|
-
|
|
9437
|
-
|
|
9438
|
-
|
|
9439
|
-
|
|
9440
|
-
|
|
9441
|
-
|
|
9442
|
-
|
|
9443
|
-
|
|
9444
|
-
|
|
9445
|
-
|
|
9446
|
-
|
|
9447
|
-
|
|
9448
|
-
|
|
9449
|
-
};
|
|
9450
|
-
return registry.callCapOnChild(route.childId, input);
|
|
9451
|
-
}
|
|
9452
|
-
case "remote-moleculer": {
|
|
9453
|
-
const addonId = this.nodeAuthority.getAddonId(route.nodeId, route.capName);
|
|
9454
|
-
if (addonId === null) throw new CapRouteError(route.capName, method, {
|
|
9455
|
-
reason: "no-provider",
|
|
9456
|
-
nodeId: route.nodeId,
|
|
9457
|
-
rejected: [{
|
|
9458
|
-
kind: "remote-moleculer",
|
|
9459
|
-
why: `no addonId known for ${route.nodeId}/${route.capName}`
|
|
9460
|
-
}]
|
|
9461
|
-
});
|
|
9462
|
-
const deviceId = extractDeviceId(args);
|
|
9463
|
-
const isNative = this.nodeAuthority.isNativeCap(route.nodeId, route.capName, deviceId);
|
|
9464
|
-
const action = capActionName(addonId, route.capName, method, isNative);
|
|
9465
|
-
return callWithServiceDiscovery(this.broker, addonId, action, args, {
|
|
9466
|
-
nodeID: route.nodeId,
|
|
9467
|
-
timeout: this.callTimeout(route.capName, method)
|
|
9468
|
-
});
|
|
9469
|
-
}
|
|
9470
|
-
case "agent-child-forward": {
|
|
9471
|
-
const deviceId = extractDeviceId(args);
|
|
9472
|
-
const params = {
|
|
9473
|
-
capName: route.capName,
|
|
9474
|
-
method,
|
|
9475
|
-
args,
|
|
9476
|
-
...route.childId !== void 0 ? { childId: route.childId } : {},
|
|
9477
|
-
...deviceId !== void 0 ? { deviceId } : {},
|
|
9478
|
-
...addonId !== void 0 ? { addonId } : {}
|
|
9479
|
-
};
|
|
9480
|
-
return callWithServiceDiscovery(this.broker, AGENT_CAP_FWD_SERVICE, AGENT_CAP_FWD_ACTION, params, {
|
|
9481
|
-
nodeID: route.agentNodeId,
|
|
9482
|
-
timeout: this.callTimeout(route.capName, method)
|
|
9483
|
-
});
|
|
9484
|
-
}
|
|
9544
|
+
async callOut(input) {
|
|
9545
|
+
if (this.channel === null) throw new Error("LocalChildClient: callOut before start");
|
|
9546
|
+
const routing = {
|
|
9547
|
+
...input.deviceId !== void 0 ? { deviceId: input.deviceId } : {},
|
|
9548
|
+
...input.nodeId !== void 0 ? { nodeId: input.nodeId } : {},
|
|
9549
|
+
...input.native === true ? { native: true } : {}
|
|
9550
|
+
};
|
|
9551
|
+
if (this.parentRawForward && input.args !== void 0) {
|
|
9552
|
+
const hints = liftRoutingHints(input.args);
|
|
9553
|
+
const msg = {
|
|
9554
|
+
kind: "cap-call-out",
|
|
9555
|
+
capName: input.capName,
|
|
9556
|
+
method: input.method,
|
|
9557
|
+
...Object.keys(hints).length > 0 ? { hints } : {},
|
|
9558
|
+
...routing
|
|
9559
|
+
};
|
|
9560
|
+
const reply = await this.channel.request(msg, { payload: packPayload(input.args) });
|
|
9561
|
+
return reply instanceof PayloadReply ? unpackPayload(reply.payload) : reply;
|
|
9485
9562
|
}
|
|
9486
|
-
|
|
9487
|
-
|
|
9488
|
-
|
|
9489
|
-
|
|
9490
|
-
|
|
9491
|
-
|
|
9492
|
-
return {
|
|
9493
|
-
hubNodeId: this.hubNodeId,
|
|
9494
|
-
hubInProcessProvides: (cap) => inProcessProviders(cap) !== null,
|
|
9495
|
-
...hubResidentProviders !== void 0 ? {
|
|
9496
|
-
hubResidentProvides: (cap) => hubResidentProviders(cap) !== null,
|
|
9497
|
-
getHubResidentProviderRef: (cap) => hubResidentProviders(cap)
|
|
9498
|
-
} : {},
|
|
9499
|
-
hubLocalChildProvides: (cap, deviceId) => hubLocalRegistry !== null && hubLocalRegistry.resolveChildId(cap, deviceId) !== null,
|
|
9500
|
-
nodeKnowsCap: (nodeId, cap) => nodeAuthority.nodeKnowsCap(nodeId, cap),
|
|
9501
|
-
nodeIsAgent: (nodeId) => nodeAuthority.nodeIsAgent(nodeId),
|
|
9502
|
-
nodeOnline: (nodeId) => nodeAuthority.nodeOnline(nodeId),
|
|
9503
|
-
listKnownNodeIds: () => nodeAuthority.listNodeIds(),
|
|
9504
|
-
getInProcessProviderRef: (cap) => inProcessProviders(cap),
|
|
9505
|
-
getHubLocalChildId: (cap, deviceId) => hubLocalRegistry !== null ? hubLocalRegistry.resolveChildId(cap, deviceId) : null,
|
|
9506
|
-
getAgentChildId: (agentNodeId, cap) => nodeAuthority.getAgentChildId(agentNodeId, cap)
|
|
9563
|
+
const msg = {
|
|
9564
|
+
kind: "cap-call-out",
|
|
9565
|
+
capName: input.capName,
|
|
9566
|
+
method: input.method,
|
|
9567
|
+
args: input.args,
|
|
9568
|
+
...routing
|
|
9507
9569
|
};
|
|
9570
|
+
return this.channel.request(msg);
|
|
9508
9571
|
}
|
|
9509
|
-
/**
|
|
9510
|
-
|
|
9511
|
-
|
|
9512
|
-
|
|
9513
|
-
|
|
9514
|
-
case "remote-moleculer": return route.nodeId;
|
|
9515
|
-
case "agent-child-forward": return route.agentNodeId;
|
|
9516
|
-
}
|
|
9572
|
+
/** Disconnect from the parent. Safe to call before `start()` (no-op) and idempotent. */
|
|
9573
|
+
async close() {
|
|
9574
|
+
await this.client?.close();
|
|
9575
|
+
this.client = null;
|
|
9576
|
+
this.channel = null;
|
|
9517
9577
|
}
|
|
9518
9578
|
};
|
|
9519
9579
|
//#endregion
|
|
@@ -9775,6 +9835,13 @@ function createParentUnownedCallHandler(deps) {
|
|
|
9775
9835
|
});
|
|
9776
9836
|
throw new Error(`no native provider for capability "${input.capName}" on device ${String(deviceId)}`);
|
|
9777
9837
|
}
|
|
9838
|
+
const claimed = await answerClaimedGetStatus(deps.claimedStatus, {
|
|
9839
|
+
capName: input.capName,
|
|
9840
|
+
method: input.method,
|
|
9841
|
+
...deviceId !== void 0 ? { deviceId } : {},
|
|
9842
|
+
args: input.args
|
|
9843
|
+
});
|
|
9844
|
+
if (claimed !== null) return claimed.slice;
|
|
9778
9845
|
const nodeId = deps.isDataNodeIdCap?.(input.capName) === true ? void 0 : input.nodeId ?? extractNodeId(input.args);
|
|
9779
9846
|
const pinnedToOtherNode = nodeId !== void 0 && nodeId !== deps.broker.nodeID;
|
|
9780
9847
|
if (nodeId === void 0) {
|
|
@@ -12776,6 +12843,12 @@ Object.defineProperty(exports, "FrameDecoder", {
|
|
|
12776
12843
|
return FrameDecoder;
|
|
12777
12844
|
}
|
|
12778
12845
|
});
|
|
12846
|
+
Object.defineProperty(exports, "GET_STATUS_METHOD", {
|
|
12847
|
+
enumerable: true,
|
|
12848
|
+
get: function() {
|
|
12849
|
+
return GET_STATUS_METHOD;
|
|
12850
|
+
}
|
|
12851
|
+
});
|
|
12779
12852
|
Object.defineProperty(exports, "HEAP_RECLAIM_MIN_INTERVAL_MS", {
|
|
12780
12853
|
enumerable: true,
|
|
12781
12854
|
get: function() {
|
|
@@ -12968,6 +13041,12 @@ Object.defineProperty(exports, "adaptBrokerToCluster", {
|
|
|
12968
13041
|
return adaptBrokerToCluster;
|
|
12969
13042
|
}
|
|
12970
13043
|
});
|
|
13044
|
+
Object.defineProperty(exports, "answerClaimedGetStatus", {
|
|
13045
|
+
enumerable: true,
|
|
13046
|
+
get: function() {
|
|
13047
|
+
return answerClaimedGetStatus;
|
|
13048
|
+
}
|
|
13049
|
+
});
|
|
12971
13050
|
Object.defineProperty(exports, "brokerCallForCap", {
|
|
12972
13051
|
enumerable: true,
|
|
12973
13052
|
get: function() {
|
|
@@ -13004,12 +13083,6 @@ Object.defineProperty(exports, "buildNpmChildEnv", {
|
|
|
13004
13083
|
return buildNpmChildEnv;
|
|
13005
13084
|
}
|
|
13006
13085
|
});
|
|
13007
|
-
Object.defineProperty(exports, "buildUdsNativeCapProxy", {
|
|
13008
|
-
enumerable: true,
|
|
13009
|
-
get: function() {
|
|
13010
|
-
return buildUdsNativeCapProxy;
|
|
13011
|
-
}
|
|
13012
|
-
});
|
|
13013
13086
|
Object.defineProperty(exports, "callWithServiceDiscovery", {
|
|
13014
13087
|
enumerable: true,
|
|
13015
13088
|
get: function() {
|
|
@@ -13250,6 +13323,12 @@ Object.defineProperty(exports, "deviceCapResolverLink", {
|
|
|
13250
13323
|
return deviceCapResolverLink;
|
|
13251
13324
|
}
|
|
13252
13325
|
});
|
|
13326
|
+
Object.defineProperty(exports, "deviceOfGetStatus", {
|
|
13327
|
+
enumerable: true,
|
|
13328
|
+
get: function() {
|
|
13329
|
+
return deviceOfGetStatus;
|
|
13330
|
+
}
|
|
13331
|
+
});
|
|
13253
13332
|
Object.defineProperty(exports, "diffCapActionCensus", {
|
|
13254
13333
|
enumerable: true,
|
|
13255
13334
|
get: function() {
|