@camstack/system 1.1.18 → 1.1.20
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/index.d.ts +1 -0
- package/dist/index.js +57 -51
- package/dist/index.mjs +57 -51
- package/dist/kernel/moleculer/event-bus-core.d.ts +41 -4
- package/dist/kernel/moleculer/event-bus.d.ts +11 -2
- package/dist/kernel/moleculer/process-service.d.ts +1 -1
- package/dist/kernel/transport/child-cap-protocol.d.ts +25 -4
- package/dist/kernel/transport/local-child-client.d.ts +34 -2
- package/dist/kernel/transport/local-child-registry.d.ts +57 -2
- package/dist/kernel/transport/uds-event-bridge.d.ts +10 -0
- package/dist/kernel/transport/uds-event-bus.d.ts +6 -0
- package/dist/{manifest-python-deps-lZd5B4FS.js → manifest-python-deps-PbdHKDpj.js} +315 -117
- package/dist/{manifest-python-deps-DOAh19rY.mjs → manifest-python-deps-X00KVYHa.mjs} +315 -117
- package/dist/notification/notification-service.d.ts +29 -23
- package/package.json +1 -1
|
@@ -3358,6 +3358,133 @@ function createLocalTransport() {
|
|
|
3358
3358
|
};
|
|
3359
3359
|
}
|
|
3360
3360
|
//#endregion
|
|
3361
|
+
//#region src/kernel/moleculer/event-bus-core.ts
|
|
3362
|
+
/**
|
|
3363
|
+
* @param retainRecent - `true` ONLY for the hub main broker bus (it serves
|
|
3364
|
+
* `getRecent`). Defaults to `false` — the safe one-shot default for child
|
|
3365
|
+
* runners + agents, which retain nothing.
|
|
3366
|
+
*/
|
|
3367
|
+
function createSharedBusState(retainRecent = false) {
|
|
3368
|
+
return {
|
|
3369
|
+
handlers: /* @__PURE__ */ new Map(),
|
|
3370
|
+
recent: [],
|
|
3371
|
+
retainRecent
|
|
3372
|
+
};
|
|
3373
|
+
}
|
|
3374
|
+
function matchesPattern(pattern, category) {
|
|
3375
|
+
if (pattern === "*" || pattern === "**") return true;
|
|
3376
|
+
if (pattern.endsWith(".**")) return category.startsWith(pattern.slice(0, -3));
|
|
3377
|
+
if (pattern.endsWith(".*")) return category.startsWith(pattern.slice(0, -2));
|
|
3378
|
+
return pattern === category;
|
|
3379
|
+
}
|
|
3380
|
+
function extractCategoryPattern(filter) {
|
|
3381
|
+
if (typeof filter === "string") return filter;
|
|
3382
|
+
if (filter && typeof filter === "object" && "category" in filter) {
|
|
3383
|
+
const cat = filter.category;
|
|
3384
|
+
if (typeof cat === "string") return cat;
|
|
3385
|
+
if (Array.isArray(cat) && cat.length > 0) return String(cat[0]);
|
|
3386
|
+
}
|
|
3387
|
+
return "*";
|
|
3388
|
+
}
|
|
3389
|
+
/**
|
|
3390
|
+
* Match an event against the full `EventFilter`. The Map-keyed subscribe path
|
|
3391
|
+
* only handles the category pattern (deciding which handler set to fan out to);
|
|
3392
|
+
* this function applies the remaining scope dimensions (`agentId`, `addonId`,
|
|
3393
|
+
* `deviceId`, `source`, `since`) so per-device / per-addon subscribers don't
|
|
3394
|
+
* receive sibling events.
|
|
3395
|
+
*/
|
|
3396
|
+
function matchesEventFilter(event, filter) {
|
|
3397
|
+
if (!filter || typeof filter === "string") return true;
|
|
3398
|
+
if (filter.source) {
|
|
3399
|
+
if (event.source.type !== filter.source.type || event.source.id !== filter.source.id) return false;
|
|
3400
|
+
}
|
|
3401
|
+
if (filter.agentId) {
|
|
3402
|
+
const eventNodeId = event.source.nodeId;
|
|
3403
|
+
if (!eventNodeId || eventNodeId !== filter.agentId && !eventNodeId.startsWith(`${filter.agentId}/`)) return false;
|
|
3404
|
+
}
|
|
3405
|
+
if (filter.addonId) {
|
|
3406
|
+
if ((event.source.addonId ?? (event.source.type === "addon" ? String(event.source.id) : void 0)) !== filter.addonId) return false;
|
|
3407
|
+
}
|
|
3408
|
+
if (filter.deviceId !== void 0) {
|
|
3409
|
+
if ((event.source.deviceId ?? (event.source.type === "device" ? Number(event.source.id) : void 0)) !== filter.deviceId) return false;
|
|
3410
|
+
}
|
|
3411
|
+
if (filter.since && event.timestamp < filter.since) return false;
|
|
3412
|
+
return true;
|
|
3413
|
+
}
|
|
3414
|
+
/**
|
|
3415
|
+
* Categories filtered out of the `recent[]` ring buffer.
|
|
3416
|
+
*
|
|
3417
|
+
* High-frequency, low-value-for-the-operator categories — periodic metrics
|
|
3418
|
+
* snapshots, per-frame inference traces, raw motion-mask dumps, model-download
|
|
3419
|
+
* progress, etc. They still fan out to live subscribers (UI charts that
|
|
3420
|
+
* explicitly listen for them), but do NOT enter the recent-events ring so
|
|
3421
|
+
* meaningful events (motion / phase transitions / device lifecycle / addon
|
|
3422
|
+
* lifecycle / recording / detection.event) survive in the last-N window without
|
|
3423
|
+
* being displaced by per-frame noise.
|
|
3424
|
+
*/
|
|
3425
|
+
var RING_BUFFER_DENY_PATTERNS = [
|
|
3426
|
+
"pipeline.camera-metrics-snapshot",
|
|
3427
|
+
"pipeline.runner-load-snapshot",
|
|
3428
|
+
"pipeline.engine-metrics-snapshot",
|
|
3429
|
+
"pipeline.inference-result",
|
|
3430
|
+
"pipeline.trace",
|
|
3431
|
+
"pipeline.progress",
|
|
3432
|
+
"stream-broker.metrics-snapshot",
|
|
3433
|
+
"metrics.node-resources-snapshot",
|
|
3434
|
+
"metrics.node-processes-snapshot",
|
|
3435
|
+
"cluster.topology-snapshot",
|
|
3436
|
+
"detection.motion-analysis",
|
|
3437
|
+
"detection.motion-zones-raw",
|
|
3438
|
+
"detection.result",
|
|
3439
|
+
"pipeline.audio-inference-result",
|
|
3440
|
+
"platform-probe.phase",
|
|
3441
|
+
"benchmark.progress",
|
|
3442
|
+
"model.download.progress",
|
|
3443
|
+
"pipeline-analytics.frame-tracked",
|
|
3444
|
+
"pipeline-analytics.detection-event",
|
|
3445
|
+
"pipeline-analytics.track-started",
|
|
3446
|
+
"pipeline-analytics.track-ended",
|
|
3447
|
+
"motion.on-motion-changed",
|
|
3448
|
+
"capability.binding-changed"
|
|
3449
|
+
];
|
|
3450
|
+
/**
|
|
3451
|
+
* Hard cap on the `recent[]` buffer size. Even audit-grade categories that
|
|
3452
|
+
* are NOT on the deny-list (e.g. `device.state-changed`) must not accumulate
|
|
3453
|
+
* without bound — this is the last-N window, not a durable log. Without this
|
|
3454
|
+
* cap the buffer grew forever (~26 MB/h/process across every broker, agent,
|
|
3455
|
+
* and UDS-child bus), filling the hub to 16 GB.
|
|
3456
|
+
*/
|
|
3457
|
+
var MAX_RECENT_EVENTS = 1e3;
|
|
3458
|
+
function isHighFrequencyCategory(category) {
|
|
3459
|
+
for (const pattern of RING_BUFFER_DENY_PATTERNS) if (matchesPattern(pattern, category)) return true;
|
|
3460
|
+
return false;
|
|
3461
|
+
}
|
|
3462
|
+
/**
|
|
3463
|
+
* Deliver `event` to all matching local subscribers and — ONLY on a bus that
|
|
3464
|
+
* opted into retention (`state.retainRecent`, i.e. the hub main process) and
|
|
3465
|
+
* unless the event is high-frequency — append it to the bounded `recent[]`
|
|
3466
|
+
* ring.
|
|
3467
|
+
*
|
|
3468
|
+
* On a non-retaining bus (child runners, agents) the retention block is skipped
|
|
3469
|
+
* entirely: events are one-shot / fire-and-forget, still fanned out to local
|
|
3470
|
+
* subscribers but never accumulated. `getRecent` on such a bus returns `[]`.
|
|
3471
|
+
*/
|
|
3472
|
+
function deliverShared(state, event) {
|
|
3473
|
+
if (state.retainRecent && !isHighFrequencyCategory(event.category)) {
|
|
3474
|
+
state.recent.push(event);
|
|
3475
|
+
if (state.recent.length > 1e3) state.recent.splice(0, state.recent.length - MAX_RECENT_EVENTS);
|
|
3476
|
+
}
|
|
3477
|
+
for (const [pattern, set] of state.handlers) {
|
|
3478
|
+
if (!matchesPattern(pattern, event.category)) continue;
|
|
3479
|
+
for (const entry of set) {
|
|
3480
|
+
if (!matchesEventFilter(event, entry.filter)) continue;
|
|
3481
|
+
try {
|
|
3482
|
+
entry.handler(event);
|
|
3483
|
+
} catch (err) {}
|
|
3484
|
+
}
|
|
3485
|
+
}
|
|
3486
|
+
}
|
|
3487
|
+
//#endregion
|
|
3361
3488
|
//#region src/kernel/moleculer/resilient-cap-call.ts
|
|
3362
3489
|
/** Moleculer error `type` values meaning "the service is not (yet) routable". */
|
|
3363
3490
|
var DISCOVERY_ERROR_TYPES = new Set(["SERVICE_NOT_FOUND", "SERVICE_NOT_AVAILABLE"]);
|
|
@@ -3752,6 +3879,11 @@ var CapRouteResolver = class {
|
|
|
3752
3879
|
};
|
|
3753
3880
|
//#endregion
|
|
3754
3881
|
//#region src/kernel/transport/local-child-registry.ts
|
|
3882
|
+
function readFanoutMode() {
|
|
3883
|
+
const raw = process.env.CAMSTACK_UDS_EVENT_FANOUT;
|
|
3884
|
+
if (raw === "shadow" || raw === "broadcast") return raw;
|
|
3885
|
+
return "filter";
|
|
3886
|
+
}
|
|
3755
3887
|
/**
|
|
3756
3888
|
* Sentinel prefix used in the no-route error thrown when `cap-call-out` has no
|
|
3757
3889
|
* local sibling and no `onUnownedCall` fallback. `ipcParentLink` detects this
|
|
@@ -3776,8 +3908,15 @@ var LocalChildRegistry = class {
|
|
|
3776
3908
|
onUnownedCall;
|
|
3777
3909
|
logger;
|
|
3778
3910
|
getActiveSingletonAddonId;
|
|
3911
|
+
resolveChildIdForAddon;
|
|
3779
3912
|
/** Tracks capNames already logged as UDS-routed; one INFO line per capName per process. */
|
|
3780
3913
|
egressRoutedCaps = /* @__PURE__ */ new Set();
|
|
3914
|
+
/** Active event fan-out mode, read once from `CAMSTACK_UDS_EVENT_FANOUT`. */
|
|
3915
|
+
fanoutMode = readFanoutMode();
|
|
3916
|
+
/** Per-child event fan-out counters (sent / suppressed). */
|
|
3917
|
+
childEventStats = /* @__PURE__ */ new Map();
|
|
3918
|
+
/** Last pattern-set string logged per child, to dedup the INFO line. */
|
|
3919
|
+
loggedPatternSet = /* @__PURE__ */ new Map();
|
|
3781
3920
|
/**
|
|
3782
3921
|
* Accepts either a plain positional `server` argument (backward-compatible)
|
|
3783
3922
|
* or a full `LocalChildRegistryOptions` object.
|
|
@@ -3796,16 +3935,68 @@ var LocalChildRegistry = class {
|
|
|
3796
3935
|
this.onUnownedCall = opts.onUnownedCall;
|
|
3797
3936
|
this.logger = opts.logger;
|
|
3798
3937
|
this.getActiveSingletonAddonId = opts.getActiveSingletonAddonId;
|
|
3938
|
+
this.resolveChildIdForAddon = opts.resolveChildIdForAddon;
|
|
3799
3939
|
} else {
|
|
3800
3940
|
this.server = serverOrOptions;
|
|
3801
3941
|
this.onUnownedCall = onUnownedCallArg;
|
|
3802
3942
|
}
|
|
3803
3943
|
}
|
|
3804
3944
|
async start() {
|
|
3945
|
+
this.logger?.info("UDS event fan-out mode", { mode: this.fanoutMode });
|
|
3805
3946
|
this.server.onConnection((channel) => this.onConnection(channel));
|
|
3806
3947
|
await this.server.listen();
|
|
3807
3948
|
}
|
|
3808
3949
|
/**
|
|
3950
|
+
* Per-child event fan-out counters (`{ sent, suppressed }`) or `null` if the
|
|
3951
|
+
* child has no recorded events yet. Exposed for the starvation-verification
|
|
3952
|
+
* surface (shadow burn-in + operator debug).
|
|
3953
|
+
*/
|
|
3954
|
+
getChildEventStats(childId) {
|
|
3955
|
+
const s = this.childEventStats.get(childId);
|
|
3956
|
+
return s === void 0 ? null : {
|
|
3957
|
+
sent: s.sent,
|
|
3958
|
+
suppressed: s.suppressed
|
|
3959
|
+
};
|
|
3960
|
+
}
|
|
3961
|
+
/**
|
|
3962
|
+
* Does `entry` want to receive an event in `category`? Honours the fan-out
|
|
3963
|
+
* mode: `broadcast` → always; undeclared patterns (`null`) → always
|
|
3964
|
+
* (fail-open); else the child's declared set must match via `matchesPattern`
|
|
3965
|
+
* — the SAME function the child's own `deliverShared` map-key gate uses,
|
|
3966
|
+
* giving provable delivery parity. `shadow` is handled by the caller (it
|
|
3967
|
+
* counts the would-suppress but still sends).
|
|
3968
|
+
*
|
|
3969
|
+
* NOTE: this gate must NOT consult `RING_BUFFER_DENY_PATTERNS` /
|
|
3970
|
+
* `isHighFrequencyCategory` — those are ring-buffer STORAGE policy; a child
|
|
3971
|
+
* that explicitly subscribes to a ring-denied category must still RECEIVE it.
|
|
3972
|
+
*/
|
|
3973
|
+
childWantsEvent(entry, category) {
|
|
3974
|
+
if (this.fanoutMode === "broadcast") return true;
|
|
3975
|
+
if (entry.eventPatterns === null) return true;
|
|
3976
|
+
return entry.eventPatterns.some((p) => matchesPattern(p, category));
|
|
3977
|
+
}
|
|
3978
|
+
statsFor(childId) {
|
|
3979
|
+
let s = this.childEventStats.get(childId);
|
|
3980
|
+
if (s === void 0) {
|
|
3981
|
+
s = {
|
|
3982
|
+
sent: 0,
|
|
3983
|
+
suppressed: 0
|
|
3984
|
+
};
|
|
3985
|
+
this.childEventStats.set(childId, s);
|
|
3986
|
+
}
|
|
3987
|
+
return s;
|
|
3988
|
+
}
|
|
3989
|
+
/** Deduped INFO line whenever a child's declared pattern set changes. */
|
|
3990
|
+
logPatternSet(childId, patterns) {
|
|
3991
|
+
const key = patterns === null ? "<undeclared>" : JSON.stringify([...patterns].toSorted());
|
|
3992
|
+
if (this.loggedPatternSet.get(childId) === key) return;
|
|
3993
|
+
this.loggedPatternSet.set(childId, key);
|
|
3994
|
+
this.logger?.info("child event patterns", {
|
|
3995
|
+
childId,
|
|
3996
|
+
patterns: patterns ?? null
|
|
3997
|
+
});
|
|
3998
|
+
}
|
|
3999
|
+
/**
|
|
3809
4000
|
* Child id that can service a call to `capName` (optionally addressing
|
|
3810
4001
|
* `deviceId`), or null.
|
|
3811
4002
|
*
|
|
@@ -3829,8 +4020,11 @@ var LocalChildRegistry = class {
|
|
|
3829
4020
|
const candidates = this.findAllChildIds((cap) => cap.capName === capName && cap.deviceId === void 0);
|
|
3830
4021
|
if (candidates.length === 0) return null;
|
|
3831
4022
|
if (candidates.length === 1) return candidates[0];
|
|
3832
|
-
const
|
|
3833
|
-
if (
|
|
4023
|
+
const preferredAddonId = this.getActiveSingletonAddonId?.(capName) ?? null;
|
|
4024
|
+
if (preferredAddonId !== null) {
|
|
4025
|
+
const preferredChildId = this.resolveChildIdForAddon?.(preferredAddonId) ?? preferredAddonId;
|
|
4026
|
+
if (candidates.includes(preferredChildId)) return preferredChildId;
|
|
4027
|
+
}
|
|
3834
4028
|
return candidates[0];
|
|
3835
4029
|
}
|
|
3836
4030
|
/** First child whose cap manifest contains a descriptor matching `predicate`. */
|
|
@@ -3981,14 +4175,20 @@ var LocalChildRegistry = class {
|
|
|
3981
4175
|
* one (the originating child, to avoid echo). Fire-and-forget.
|
|
3982
4176
|
*/
|
|
3983
4177
|
broadcastEventToChildren(event, sourceNodeId, exceptChildId) {
|
|
4178
|
+
const msg = {
|
|
4179
|
+
kind: "event",
|
|
4180
|
+
event,
|
|
4181
|
+
sourceNodeId
|
|
4182
|
+
};
|
|
3984
4183
|
for (const entry of this.children.values()) {
|
|
3985
4184
|
if (entry.childId === exceptChildId) continue;
|
|
3986
|
-
const
|
|
3987
|
-
|
|
3988
|
-
|
|
3989
|
-
|
|
3990
|
-
|
|
3991
|
-
|
|
4185
|
+
const wants = this.childWantsEvent(entry, event.category);
|
|
4186
|
+
const stats = this.statsFor(entry.childId);
|
|
4187
|
+
if (!wants) stats.suppressed++;
|
|
4188
|
+
if (wants || this.fanoutMode === "shadow") {
|
|
4189
|
+
stats.sent++;
|
|
4190
|
+
entry.channel.emit(msg);
|
|
4191
|
+
}
|
|
3992
4192
|
}
|
|
3993
4193
|
}
|
|
3994
4194
|
/**
|
|
@@ -4024,17 +4224,31 @@ var LocalChildRegistry = class {
|
|
|
4024
4224
|
if (childId !== null) this.logHandler?.(childId, msg);
|
|
4025
4225
|
return;
|
|
4026
4226
|
}
|
|
4227
|
+
if (msg.kind === "event-sub") {
|
|
4228
|
+
if (childId === null) return;
|
|
4229
|
+
const entry = this.children.get(childId);
|
|
4230
|
+
if (entry === void 0) return;
|
|
4231
|
+
this.children.set(childId, {
|
|
4232
|
+
...entry,
|
|
4233
|
+
eventPatterns: msg.patterns
|
|
4234
|
+
});
|
|
4235
|
+
this.logPatternSet(childId, msg.patterns);
|
|
4236
|
+
return;
|
|
4237
|
+
}
|
|
4027
4238
|
});
|
|
4028
4239
|
channel.onRequest(async (body) => {
|
|
4029
4240
|
const msg = body;
|
|
4030
4241
|
if (msg.kind === "register") {
|
|
4031
4242
|
if (childId !== null && childId !== msg.childId) throw new Error(`child attempted to change identity from "${childId}" to "${msg.childId}"`);
|
|
4032
4243
|
childId = msg.childId;
|
|
4244
|
+
const eventPatterns = msg.eventPatterns ?? null;
|
|
4033
4245
|
this.children.set(msg.childId, {
|
|
4034
4246
|
childId: msg.childId,
|
|
4035
4247
|
channel,
|
|
4036
|
-
caps: msg.caps
|
|
4248
|
+
caps: msg.caps,
|
|
4249
|
+
eventPatterns
|
|
4037
4250
|
});
|
|
4251
|
+
this.logPatternSet(msg.childId, eventPatterns);
|
|
4038
4252
|
this.registeredHandler({
|
|
4039
4253
|
childId: msg.childId,
|
|
4040
4254
|
caps: msg.caps
|
|
@@ -4067,7 +4281,11 @@ var LocalChildRegistry = class {
|
|
|
4067
4281
|
throw new Error(`unknown child request kind: ${msg.kind}`);
|
|
4068
4282
|
});
|
|
4069
4283
|
channel.onClose(() => {
|
|
4070
|
-
if (childId !== null && this.children.delete(childId))
|
|
4284
|
+
if (childId !== null && this.children.delete(childId)) {
|
|
4285
|
+
this.childEventStats.delete(childId);
|
|
4286
|
+
this.loggedPatternSet.delete(childId);
|
|
4287
|
+
this.goneHandler(childId);
|
|
4288
|
+
}
|
|
4071
4289
|
});
|
|
4072
4290
|
}
|
|
4073
4291
|
};
|
|
@@ -4099,6 +4317,24 @@ var LocalChildClient = class {
|
|
|
4099
4317
|
* the latest set) instead of being lost or throwing.
|
|
4100
4318
|
*/
|
|
4101
4319
|
latestCaps;
|
|
4320
|
+
/**
|
|
4321
|
+
* Per-owner (addonId) category-pattern subscription sets, and their union.
|
|
4322
|
+
* The union is declared to the parent (in `RegisterMessage.eventPatterns`
|
|
4323
|
+
* pre/at-connect, or via an `event-sub` emit post-connect) so the parent can
|
|
4324
|
+
* subscription-filter its event fan-out. Per-owner keying keeps the union
|
|
4325
|
+
* correct if multiple event buses ever share one client (group-runner case).
|
|
4326
|
+
*/
|
|
4327
|
+
patternsByOwner = /* @__PURE__ */ new Map();
|
|
4328
|
+
latestEventPatterns = [];
|
|
4329
|
+
/**
|
|
4330
|
+
* Whether `updateEventPatterns` has ever been called. A client that never
|
|
4331
|
+
* declared a subscription set (no event bus wired — e.g. a pure cap-call
|
|
4332
|
+
* runner) omits `eventPatterns` from its register frame entirely, so the
|
|
4333
|
+
* parent treats it as UNDECLARED and fails OPEN (broadcast-all). Once any
|
|
4334
|
+
* owner declares (in production every addon context's framework
|
|
4335
|
+
* subscriptions do), the register carries the real union — even if empty.
|
|
4336
|
+
*/
|
|
4337
|
+
hasDeclaredEventPatterns = false;
|
|
4102
4338
|
/** Events and logs queued while the channel is not yet open. */
|
|
4103
4339
|
pendingEmits = [];
|
|
4104
4340
|
/** Handler for parent→child events. Registered via `onEvent`. */
|
|
@@ -4125,6 +4361,42 @@ var LocalChildClient = class {
|
|
|
4125
4361
|
this.latestCaps = options.caps;
|
|
4126
4362
|
}
|
|
4127
4363
|
/**
|
|
4364
|
+
* Declare an owner's live category-pattern subscription set. Stores it
|
|
4365
|
+
* per-owner (keyed by `ownerId` = addonId), recomputes the union, and — only
|
|
4366
|
+
* if the union changed — declares it to the parent:
|
|
4367
|
+
* - pre-connect: buffered only; `start()`'s register frame carries the set.
|
|
4368
|
+
* - post-connect: emitted as a fire-and-forget `event-sub` (full-set
|
|
4369
|
+
* replace).
|
|
4370
|
+
* Idempotent on an unchanged union (no redundant `event-sub` frames).
|
|
4371
|
+
*/
|
|
4372
|
+
updateEventPatterns(ownerId, patterns) {
|
|
4373
|
+
this.hasDeclaredEventPatterns = true;
|
|
4374
|
+
this.patternsByOwner.set(ownerId, patterns);
|
|
4375
|
+
const union = this.computePatternUnion();
|
|
4376
|
+
if (!this.patternsEqual(union, this.latestEventPatterns)) {
|
|
4377
|
+
this.latestEventPatterns = union;
|
|
4378
|
+
if (this.channel !== null) {
|
|
4379
|
+
const msg = {
|
|
4380
|
+
kind: "event-sub",
|
|
4381
|
+
patterns: union
|
|
4382
|
+
};
|
|
4383
|
+
this.channel.emit(msg);
|
|
4384
|
+
}
|
|
4385
|
+
}
|
|
4386
|
+
}
|
|
4387
|
+
/** Sorted, de-duplicated union across all owners' pattern sets. */
|
|
4388
|
+
computePatternUnion() {
|
|
4389
|
+
const all = /* @__PURE__ */ new Set();
|
|
4390
|
+
for (const patterns of this.patternsByOwner.values()) for (const p of patterns) all.add(p);
|
|
4391
|
+
return [...all].toSorted();
|
|
4392
|
+
}
|
|
4393
|
+
/** Order-insensitive equality — both inputs are already sorted unions here. */
|
|
4394
|
+
patternsEqual(a, b) {
|
|
4395
|
+
if (a.length !== b.length) return false;
|
|
4396
|
+
for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
|
|
4397
|
+
return true;
|
|
4398
|
+
}
|
|
4399
|
+
/**
|
|
4128
4400
|
* Register a callback that fires each time the client successfully connects
|
|
4129
4401
|
* (or reconnects) to its parent. Multiple handlers may be registered; all
|
|
4130
4402
|
* are called in registration order. Used by readiness-context in UDS mode
|
|
@@ -4203,7 +4475,8 @@ var LocalChildClient = class {
|
|
|
4203
4475
|
const register = {
|
|
4204
4476
|
kind: "register",
|
|
4205
4477
|
childId: this.options.childId,
|
|
4206
|
-
caps: this.latestCaps
|
|
4478
|
+
caps: this.latestCaps,
|
|
4479
|
+
...this.hasDeclaredEventPatterns ? { eventPatterns: this.latestEventPatterns } : {}
|
|
4207
4480
|
};
|
|
4208
4481
|
try {
|
|
4209
4482
|
await channel.request(register);
|
|
@@ -4236,7 +4509,8 @@ var LocalChildClient = class {
|
|
|
4236
4509
|
const register = {
|
|
4237
4510
|
kind: "register",
|
|
4238
4511
|
childId: this.options.childId,
|
|
4239
|
-
caps
|
|
4512
|
+
caps,
|
|
4513
|
+
...this.hasDeclaredEventPatterns ? { eventPatterns: this.latestEventPatterns } : {}
|
|
4240
4514
|
};
|
|
4241
4515
|
await this.channel.request(register);
|
|
4242
4516
|
}
|
|
@@ -4397,105 +4671,6 @@ function udsChildLogToWorkerEntry(childId, entry) {
|
|
|
4397
4671
|
};
|
|
4398
4672
|
}
|
|
4399
4673
|
//#endregion
|
|
4400
|
-
//#region src/kernel/moleculer/event-bus-core.ts
|
|
4401
|
-
function createSharedBusState() {
|
|
4402
|
-
return {
|
|
4403
|
-
handlers: /* @__PURE__ */ new Map(),
|
|
4404
|
-
recent: []
|
|
4405
|
-
};
|
|
4406
|
-
}
|
|
4407
|
-
function matchesPattern(pattern, category) {
|
|
4408
|
-
if (pattern === "*" || pattern === "**") return true;
|
|
4409
|
-
if (pattern.endsWith(".**")) return category.startsWith(pattern.slice(0, -3));
|
|
4410
|
-
if (pattern.endsWith(".*")) return category.startsWith(pattern.slice(0, -2));
|
|
4411
|
-
return pattern === category;
|
|
4412
|
-
}
|
|
4413
|
-
function extractCategoryPattern(filter) {
|
|
4414
|
-
if (typeof filter === "string") return filter;
|
|
4415
|
-
if (filter && typeof filter === "object" && "category" in filter) {
|
|
4416
|
-
const cat = filter.category;
|
|
4417
|
-
if (typeof cat === "string") return cat;
|
|
4418
|
-
if (Array.isArray(cat) && cat.length > 0) return String(cat[0]);
|
|
4419
|
-
}
|
|
4420
|
-
return "*";
|
|
4421
|
-
}
|
|
4422
|
-
/**
|
|
4423
|
-
* Match an event against the full `EventFilter`. The Map-keyed subscribe path
|
|
4424
|
-
* only handles the category pattern (deciding which handler set to fan out to);
|
|
4425
|
-
* this function applies the remaining scope dimensions (`agentId`, `addonId`,
|
|
4426
|
-
* `deviceId`, `source`, `since`) so per-device / per-addon subscribers don't
|
|
4427
|
-
* receive sibling events.
|
|
4428
|
-
*/
|
|
4429
|
-
function matchesEventFilter(event, filter) {
|
|
4430
|
-
if (!filter || typeof filter === "string") return true;
|
|
4431
|
-
if (filter.source) {
|
|
4432
|
-
if (event.source.type !== filter.source.type || event.source.id !== filter.source.id) return false;
|
|
4433
|
-
}
|
|
4434
|
-
if (filter.agentId) {
|
|
4435
|
-
const eventNodeId = event.source.nodeId;
|
|
4436
|
-
if (!eventNodeId || eventNodeId !== filter.agentId && !eventNodeId.startsWith(`${filter.agentId}/`)) return false;
|
|
4437
|
-
}
|
|
4438
|
-
if (filter.addonId) {
|
|
4439
|
-
if ((event.source.addonId ?? (event.source.type === "addon" ? String(event.source.id) : void 0)) !== filter.addonId) return false;
|
|
4440
|
-
}
|
|
4441
|
-
if (filter.deviceId !== void 0) {
|
|
4442
|
-
if ((event.source.deviceId ?? (event.source.type === "device" ? Number(event.source.id) : void 0)) !== filter.deviceId) return false;
|
|
4443
|
-
}
|
|
4444
|
-
if (filter.since && event.timestamp < filter.since) return false;
|
|
4445
|
-
return true;
|
|
4446
|
-
}
|
|
4447
|
-
/**
|
|
4448
|
-
* Categories filtered out of the `recent[]` ring buffer.
|
|
4449
|
-
*
|
|
4450
|
-
* High-frequency, low-value-for-the-operator categories — periodic metrics
|
|
4451
|
-
* snapshots, per-frame inference traces, raw motion-mask dumps, model-download
|
|
4452
|
-
* progress, etc. They still fan out to live subscribers (UI charts that
|
|
4453
|
-
* explicitly listen for them), but do NOT enter the recent-events ring so
|
|
4454
|
-
* meaningful events (motion / phase transitions / device lifecycle / addon
|
|
4455
|
-
* lifecycle / recording / detection.event) survive in the last-N window without
|
|
4456
|
-
* being displaced by per-frame noise.
|
|
4457
|
-
*/
|
|
4458
|
-
var RING_BUFFER_DENY_PATTERNS = [
|
|
4459
|
-
"pipeline.camera-metrics-snapshot",
|
|
4460
|
-
"pipeline.runner-load-snapshot",
|
|
4461
|
-
"pipeline.engine-metrics-snapshot",
|
|
4462
|
-
"pipeline.inference-result",
|
|
4463
|
-
"pipeline.trace",
|
|
4464
|
-
"pipeline.progress",
|
|
4465
|
-
"stream-broker.metrics-snapshot",
|
|
4466
|
-
"metrics.node-resources-snapshot",
|
|
4467
|
-
"metrics.node-processes-snapshot",
|
|
4468
|
-
"cluster.topology-snapshot",
|
|
4469
|
-
"detection.motion-analysis",
|
|
4470
|
-
"detection.motion-zones-raw",
|
|
4471
|
-
"detection.result",
|
|
4472
|
-
"pipeline.audio-inference-result",
|
|
4473
|
-
"platform-probe.phase",
|
|
4474
|
-
"benchmark.progress",
|
|
4475
|
-
"model.download.progress",
|
|
4476
|
-
"capability.binding-changed"
|
|
4477
|
-
];
|
|
4478
|
-
function isHighFrequencyCategory(category) {
|
|
4479
|
-
for (const pattern of RING_BUFFER_DENY_PATTERNS) if (matchesPattern(pattern, category)) return true;
|
|
4480
|
-
return false;
|
|
4481
|
-
}
|
|
4482
|
-
/**
|
|
4483
|
-
* Deliver `event` to all matching local subscribers and — unless it is
|
|
4484
|
-
* high-frequency — append it to the `recent[]` ring.
|
|
4485
|
-
*/
|
|
4486
|
-
function deliverShared(state, event) {
|
|
4487
|
-
if (!isHighFrequencyCategory(event.category)) state.recent.push(event);
|
|
4488
|
-
for (const [pattern, set] of state.handlers) {
|
|
4489
|
-
if (!matchesPattern(pattern, event.category)) continue;
|
|
4490
|
-
for (const entry of set) {
|
|
4491
|
-
if (!matchesEventFilter(event, entry.filter)) continue;
|
|
4492
|
-
try {
|
|
4493
|
-
entry.handler(event);
|
|
4494
|
-
} catch (err) {}
|
|
4495
|
-
}
|
|
4496
|
-
}
|
|
4497
|
-
}
|
|
4498
|
-
//#endregion
|
|
4499
4674
|
//#region src/kernel/transport/uds-event-bus.ts
|
|
4500
4675
|
/**
|
|
4501
4676
|
* Create a UDS-backed `IEventBus` for use inside a forked child process.
|
|
@@ -4510,6 +4685,9 @@ function createUdsEventBus(client, addonId) {
|
|
|
4510
4685
|
client.onEvent((event) => {
|
|
4511
4686
|
deliverShared(state, event);
|
|
4512
4687
|
});
|
|
4688
|
+
const reportPatterns = () => {
|
|
4689
|
+
client.updateEventPatterns(addonId, [...state.handlers.keys()]);
|
|
4690
|
+
};
|
|
4513
4691
|
return {
|
|
4514
4692
|
emit(event) {
|
|
4515
4693
|
const enriched = {
|
|
@@ -4529,8 +4707,13 @@ function createUdsEventBus(client, addonId) {
|
|
|
4529
4707
|
const set = state.handlers.get(pattern) ?? /* @__PURE__ */ new Set();
|
|
4530
4708
|
set.add(entry);
|
|
4531
4709
|
state.handlers.set(pattern, set);
|
|
4710
|
+
reportPatterns();
|
|
4532
4711
|
return () => {
|
|
4533
|
-
state.handlers.get(pattern)
|
|
4712
|
+
const current = state.handlers.get(pattern);
|
|
4713
|
+
if (current === void 0) return;
|
|
4714
|
+
current.delete(entry);
|
|
4715
|
+
if (current.size === 0) state.handlers.delete(pattern);
|
|
4716
|
+
reportPatterns();
|
|
4534
4717
|
};
|
|
4535
4718
|
},
|
|
4536
4719
|
getRecent(filter, limit) {
|
|
@@ -5658,12 +5841,20 @@ function clusterEventTopic(category) {
|
|
|
5658
5841
|
* brokers GC naturally.
|
|
5659
5842
|
*/
|
|
5660
5843
|
var brokerBusState = /* @__PURE__ */ new WeakMap();
|
|
5661
|
-
|
|
5844
|
+
/**
|
|
5845
|
+
* @param retainRecent - `true` from the hub-main opt-in call site
|
|
5846
|
+
* (`event-bus.service.ts`). If the bus was already created by a non-retaining
|
|
5847
|
+
* caller (e.g. `process-service`, the `$event-bus` handler, or the per-addon
|
|
5848
|
+
* wrapper) we UPGRADE it in place so the opt-in wins regardless of call
|
|
5849
|
+
* order. Only the hub main process ever passes `true`; agents + child runners
|
|
5850
|
+
* never do, so their bus stays one-shot.
|
|
5851
|
+
*/
|
|
5852
|
+
function getSharedBusState(broker, retainRecent = false) {
|
|
5662
5853
|
let state = brokerBusState.get(broker);
|
|
5663
5854
|
if (!state) {
|
|
5664
|
-
state = createSharedBusState();
|
|
5855
|
+
state = createSharedBusState(retainRecent);
|
|
5665
5856
|
brokerBusState.set(broker, state);
|
|
5666
|
-
}
|
|
5857
|
+
} else if (retainRecent && !state.retainRecent) state.retainRecent = true;
|
|
5667
5858
|
return state;
|
|
5668
5859
|
}
|
|
5669
5860
|
/** Brokers that already have the `$event-bus` service installed. */
|
|
@@ -5710,9 +5901,16 @@ function registerEventBusService(broker) {
|
|
|
5710
5901
|
} catch {}
|
|
5711
5902
|
}
|
|
5712
5903
|
}
|
|
5713
|
-
|
|
5904
|
+
/**
|
|
5905
|
+
* @param options.retainRecent - Pass `true` ONLY from the hub main process
|
|
5906
|
+
* (`EventBusService.attachBroker`), the single process that serves the
|
|
5907
|
+
* `getRecent`/audit capability. Every other caller (agents, child runners,
|
|
5908
|
+
* the per-addon wrapper, the `$event-bus` handler) omits it → the bus retains
|
|
5909
|
+
* nothing and `getRecent` returns `[]`. Events are one-shot everywhere else.
|
|
5910
|
+
*/
|
|
5911
|
+
function getBrokerEventBus(broker, options) {
|
|
5714
5912
|
const bkr = broker;
|
|
5715
|
-
const state = getSharedBusState(broker);
|
|
5913
|
+
const state = getSharedBusState(broker, options?.retainRecent ?? false);
|
|
5716
5914
|
registerEventBusService(broker);
|
|
5717
5915
|
const warnTimestamps = (() => {
|
|
5718
5916
|
let m = brokerBusWarnTimestamps.get(broker);
|
|
@@ -1,37 +1,43 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { INotificationOutputProvider, RoutedNotification, ICapabilityRegistry } from '@camstack/types';
|
|
2
2
|
import { IScopedLogger } from '../logging/scoped-logger.js';
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* A routing destination: one target (`targetId`) owned by one provider
|
|
5
|
+
* (`addonId`). The notifiers addon and the HA addon each register a
|
|
6
|
+
* `notification-output` collection provider; the service resolves the
|
|
7
|
+
* provider by `addonId` and dispatches to the specific target.
|
|
8
|
+
*/
|
|
9
|
+
export interface NotificationTargetRef {
|
|
10
|
+
readonly addonId: string;
|
|
11
|
+
readonly targetId: string;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Central notification service that routes canonical notifications to
|
|
15
|
+
* configured `(addonId, targetId)` targets. Framework-agnostic —
|
|
16
|
+
* dependencies injected via constructor.
|
|
6
17
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
18
|
+
* Providers are resolved from the ICapabilityRegistry by `addonId` on each
|
|
19
|
+
* call (proxy pattern). Falls back to a local map when no registry is
|
|
20
|
+
* provided (tests / backward compat).
|
|
10
21
|
*/
|
|
11
22
|
export declare class NotificationService {
|
|
12
23
|
private readonly logger;
|
|
13
|
-
private readonly
|
|
24
|
+
private readonly localProviders;
|
|
14
25
|
private readonly routing;
|
|
15
26
|
private readonly rateLimits;
|
|
16
27
|
private readonly lastSent;
|
|
17
28
|
private registry;
|
|
18
29
|
constructor(logger: IScopedLogger);
|
|
19
|
-
/** Set the registry for live
|
|
30
|
+
/** Set the registry for live provider lookup. Called once during boot. */
|
|
20
31
|
setRegistry(registry: ICapabilityRegistry): void;
|
|
21
|
-
/** Resolve
|
|
22
|
-
private
|
|
23
|
-
/** Register
|
|
24
|
-
|
|
25
|
-
/** Remove
|
|
26
|
-
|
|
27
|
-
|
|
32
|
+
/** Resolve the collection provider for an addon — registry first, then local map. */
|
|
33
|
+
private resolveProvider;
|
|
34
|
+
/** Register a provider in the local fallback map (used when no registry is set). */
|
|
35
|
+
registerLocalProvider(addonId: string, provider: INotificationOutputProvider): void;
|
|
36
|
+
/** Remove a provider from the local fallback map. */
|
|
37
|
+
unregisterLocalProvider(addonId: string): void;
|
|
38
|
+
/** Route a category to a set of `(addonId, targetId)` targets. */
|
|
39
|
+
setRouting(category: string, targets: readonly NotificationTargetRef[]): void;
|
|
28
40
|
setRateLimit(category: string, minIntervalMs: number): void;
|
|
29
|
-
notify(notification:
|
|
30
|
-
|
|
31
|
-
id: string;
|
|
32
|
-
name: string;
|
|
33
|
-
icon: string;
|
|
34
|
-
}>;
|
|
35
|
-
getRouting(): ReadonlyMap<string, string[]>;
|
|
36
|
-
getOutput(id: string): INotificationOutput | undefined;
|
|
41
|
+
notify(notification: RoutedNotification): Promise<void>;
|
|
42
|
+
getRouting(): ReadonlyMap<string, NotificationTargetRef[]>;
|
|
37
43
|
}
|