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