@camstack/system 1.1.19 → 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.js +16 -2
- package/dist/index.mjs +16 -2
- 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 +46 -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-Dt5XNSyd.js → manifest-python-deps-PbdHKDpj.js} +308 -115
- package/dist/{manifest-python-deps-C1Dd9MSr.mjs → manifest-python-deps-X00KVYHa.mjs} +308 -115
- package/package.json +1 -1
|
@@ -3360,6 +3360,133 @@ function createLocalTransport() {
|
|
|
3360
3360
|
};
|
|
3361
3361
|
}
|
|
3362
3362
|
//#endregion
|
|
3363
|
+
//#region src/kernel/moleculer/event-bus-core.ts
|
|
3364
|
+
/**
|
|
3365
|
+
* @param retainRecent - `true` ONLY for the hub main broker bus (it serves
|
|
3366
|
+
* `getRecent`). Defaults to `false` — the safe one-shot default for child
|
|
3367
|
+
* runners + agents, which retain nothing.
|
|
3368
|
+
*/
|
|
3369
|
+
function createSharedBusState(retainRecent = false) {
|
|
3370
|
+
return {
|
|
3371
|
+
handlers: /* @__PURE__ */ new Map(),
|
|
3372
|
+
recent: [],
|
|
3373
|
+
retainRecent
|
|
3374
|
+
};
|
|
3375
|
+
}
|
|
3376
|
+
function matchesPattern(pattern, category) {
|
|
3377
|
+
if (pattern === "*" || pattern === "**") return true;
|
|
3378
|
+
if (pattern.endsWith(".**")) return category.startsWith(pattern.slice(0, -3));
|
|
3379
|
+
if (pattern.endsWith(".*")) return category.startsWith(pattern.slice(0, -2));
|
|
3380
|
+
return pattern === category;
|
|
3381
|
+
}
|
|
3382
|
+
function extractCategoryPattern(filter) {
|
|
3383
|
+
if (typeof filter === "string") return filter;
|
|
3384
|
+
if (filter && typeof filter === "object" && "category" in filter) {
|
|
3385
|
+
const cat = filter.category;
|
|
3386
|
+
if (typeof cat === "string") return cat;
|
|
3387
|
+
if (Array.isArray(cat) && cat.length > 0) return String(cat[0]);
|
|
3388
|
+
}
|
|
3389
|
+
return "*";
|
|
3390
|
+
}
|
|
3391
|
+
/**
|
|
3392
|
+
* Match an event against the full `EventFilter`. The Map-keyed subscribe path
|
|
3393
|
+
* only handles the category pattern (deciding which handler set to fan out to);
|
|
3394
|
+
* this function applies the remaining scope dimensions (`agentId`, `addonId`,
|
|
3395
|
+
* `deviceId`, `source`, `since`) so per-device / per-addon subscribers don't
|
|
3396
|
+
* receive sibling events.
|
|
3397
|
+
*/
|
|
3398
|
+
function matchesEventFilter(event, filter) {
|
|
3399
|
+
if (!filter || typeof filter === "string") return true;
|
|
3400
|
+
if (filter.source) {
|
|
3401
|
+
if (event.source.type !== filter.source.type || event.source.id !== filter.source.id) return false;
|
|
3402
|
+
}
|
|
3403
|
+
if (filter.agentId) {
|
|
3404
|
+
const eventNodeId = event.source.nodeId;
|
|
3405
|
+
if (!eventNodeId || eventNodeId !== filter.agentId && !eventNodeId.startsWith(`${filter.agentId}/`)) return false;
|
|
3406
|
+
}
|
|
3407
|
+
if (filter.addonId) {
|
|
3408
|
+
if ((event.source.addonId ?? (event.source.type === "addon" ? String(event.source.id) : void 0)) !== filter.addonId) return false;
|
|
3409
|
+
}
|
|
3410
|
+
if (filter.deviceId !== void 0) {
|
|
3411
|
+
if ((event.source.deviceId ?? (event.source.type === "device" ? Number(event.source.id) : void 0)) !== filter.deviceId) return false;
|
|
3412
|
+
}
|
|
3413
|
+
if (filter.since && event.timestamp < filter.since) return false;
|
|
3414
|
+
return true;
|
|
3415
|
+
}
|
|
3416
|
+
/**
|
|
3417
|
+
* Categories filtered out of the `recent[]` ring buffer.
|
|
3418
|
+
*
|
|
3419
|
+
* High-frequency, low-value-for-the-operator categories — periodic metrics
|
|
3420
|
+
* snapshots, per-frame inference traces, raw motion-mask dumps, model-download
|
|
3421
|
+
* progress, etc. They still fan out to live subscribers (UI charts that
|
|
3422
|
+
* explicitly listen for them), but do NOT enter the recent-events ring so
|
|
3423
|
+
* meaningful events (motion / phase transitions / device lifecycle / addon
|
|
3424
|
+
* lifecycle / recording / detection.event) survive in the last-N window without
|
|
3425
|
+
* being displaced by per-frame noise.
|
|
3426
|
+
*/
|
|
3427
|
+
var RING_BUFFER_DENY_PATTERNS = [
|
|
3428
|
+
"pipeline.camera-metrics-snapshot",
|
|
3429
|
+
"pipeline.runner-load-snapshot",
|
|
3430
|
+
"pipeline.engine-metrics-snapshot",
|
|
3431
|
+
"pipeline.inference-result",
|
|
3432
|
+
"pipeline.trace",
|
|
3433
|
+
"pipeline.progress",
|
|
3434
|
+
"stream-broker.metrics-snapshot",
|
|
3435
|
+
"metrics.node-resources-snapshot",
|
|
3436
|
+
"metrics.node-processes-snapshot",
|
|
3437
|
+
"cluster.topology-snapshot",
|
|
3438
|
+
"detection.motion-analysis",
|
|
3439
|
+
"detection.motion-zones-raw",
|
|
3440
|
+
"detection.result",
|
|
3441
|
+
"pipeline.audio-inference-result",
|
|
3442
|
+
"platform-probe.phase",
|
|
3443
|
+
"benchmark.progress",
|
|
3444
|
+
"model.download.progress",
|
|
3445
|
+
"pipeline-analytics.frame-tracked",
|
|
3446
|
+
"pipeline-analytics.detection-event",
|
|
3447
|
+
"pipeline-analytics.track-started",
|
|
3448
|
+
"pipeline-analytics.track-ended",
|
|
3449
|
+
"motion.on-motion-changed",
|
|
3450
|
+
"capability.binding-changed"
|
|
3451
|
+
];
|
|
3452
|
+
/**
|
|
3453
|
+
* Hard cap on the `recent[]` buffer size. Even audit-grade categories that
|
|
3454
|
+
* are NOT on the deny-list (e.g. `device.state-changed`) must not accumulate
|
|
3455
|
+
* without bound — this is the last-N window, not a durable log. Without this
|
|
3456
|
+
* cap the buffer grew forever (~26 MB/h/process across every broker, agent,
|
|
3457
|
+
* and UDS-child bus), filling the hub to 16 GB.
|
|
3458
|
+
*/
|
|
3459
|
+
var MAX_RECENT_EVENTS = 1e3;
|
|
3460
|
+
function isHighFrequencyCategory(category) {
|
|
3461
|
+
for (const pattern of RING_BUFFER_DENY_PATTERNS) if (matchesPattern(pattern, category)) return true;
|
|
3462
|
+
return false;
|
|
3463
|
+
}
|
|
3464
|
+
/**
|
|
3465
|
+
* Deliver `event` to all matching local subscribers and — ONLY on a bus that
|
|
3466
|
+
* opted into retention (`state.retainRecent`, i.e. the hub main process) and
|
|
3467
|
+
* unless the event is high-frequency — append it to the bounded `recent[]`
|
|
3468
|
+
* ring.
|
|
3469
|
+
*
|
|
3470
|
+
* On a non-retaining bus (child runners, agents) the retention block is skipped
|
|
3471
|
+
* entirely: events are one-shot / fire-and-forget, still fanned out to local
|
|
3472
|
+
* subscribers but never accumulated. `getRecent` on such a bus returns `[]`.
|
|
3473
|
+
*/
|
|
3474
|
+
function deliverShared(state, event) {
|
|
3475
|
+
if (state.retainRecent && !isHighFrequencyCategory(event.category)) {
|
|
3476
|
+
state.recent.push(event);
|
|
3477
|
+
if (state.recent.length > 1e3) state.recent.splice(0, state.recent.length - MAX_RECENT_EVENTS);
|
|
3478
|
+
}
|
|
3479
|
+
for (const [pattern, set] of state.handlers) {
|
|
3480
|
+
if (!matchesPattern(pattern, event.category)) continue;
|
|
3481
|
+
for (const entry of set) {
|
|
3482
|
+
if (!matchesEventFilter(event, entry.filter)) continue;
|
|
3483
|
+
try {
|
|
3484
|
+
entry.handler(event);
|
|
3485
|
+
} catch (err) {}
|
|
3486
|
+
}
|
|
3487
|
+
}
|
|
3488
|
+
}
|
|
3489
|
+
//#endregion
|
|
3363
3490
|
//#region src/kernel/moleculer/resilient-cap-call.ts
|
|
3364
3491
|
/** Moleculer error `type` values meaning "the service is not (yet) routable". */
|
|
3365
3492
|
var DISCOVERY_ERROR_TYPES = new Set(["SERVICE_NOT_FOUND", "SERVICE_NOT_AVAILABLE"]);
|
|
@@ -3754,6 +3881,11 @@ var CapRouteResolver = class {
|
|
|
3754
3881
|
};
|
|
3755
3882
|
//#endregion
|
|
3756
3883
|
//#region src/kernel/transport/local-child-registry.ts
|
|
3884
|
+
function readFanoutMode() {
|
|
3885
|
+
const raw = process.env.CAMSTACK_UDS_EVENT_FANOUT;
|
|
3886
|
+
if (raw === "shadow" || raw === "broadcast") return raw;
|
|
3887
|
+
return "filter";
|
|
3888
|
+
}
|
|
3757
3889
|
/**
|
|
3758
3890
|
* Sentinel prefix used in the no-route error thrown when `cap-call-out` has no
|
|
3759
3891
|
* local sibling and no `onUnownedCall` fallback. `ipcParentLink` detects this
|
|
@@ -3781,6 +3913,12 @@ var LocalChildRegistry = class {
|
|
|
3781
3913
|
resolveChildIdForAddon;
|
|
3782
3914
|
/** Tracks capNames already logged as UDS-routed; one INFO line per capName per process. */
|
|
3783
3915
|
egressRoutedCaps = /* @__PURE__ */ new Set();
|
|
3916
|
+
/** Active event fan-out mode, read once from `CAMSTACK_UDS_EVENT_FANOUT`. */
|
|
3917
|
+
fanoutMode = readFanoutMode();
|
|
3918
|
+
/** Per-child event fan-out counters (sent / suppressed). */
|
|
3919
|
+
childEventStats = /* @__PURE__ */ new Map();
|
|
3920
|
+
/** Last pattern-set string logged per child, to dedup the INFO line. */
|
|
3921
|
+
loggedPatternSet = /* @__PURE__ */ new Map();
|
|
3784
3922
|
/**
|
|
3785
3923
|
* Accepts either a plain positional `server` argument (backward-compatible)
|
|
3786
3924
|
* or a full `LocalChildRegistryOptions` object.
|
|
@@ -3806,10 +3944,61 @@ var LocalChildRegistry = class {
|
|
|
3806
3944
|
}
|
|
3807
3945
|
}
|
|
3808
3946
|
async start() {
|
|
3947
|
+
this.logger?.info("UDS event fan-out mode", { mode: this.fanoutMode });
|
|
3809
3948
|
this.server.onConnection((channel) => this.onConnection(channel));
|
|
3810
3949
|
await this.server.listen();
|
|
3811
3950
|
}
|
|
3812
3951
|
/**
|
|
3952
|
+
* Per-child event fan-out counters (`{ sent, suppressed }`) or `null` if the
|
|
3953
|
+
* child has no recorded events yet. Exposed for the starvation-verification
|
|
3954
|
+
* surface (shadow burn-in + operator debug).
|
|
3955
|
+
*/
|
|
3956
|
+
getChildEventStats(childId) {
|
|
3957
|
+
const s = this.childEventStats.get(childId);
|
|
3958
|
+
return s === void 0 ? null : {
|
|
3959
|
+
sent: s.sent,
|
|
3960
|
+
suppressed: s.suppressed
|
|
3961
|
+
};
|
|
3962
|
+
}
|
|
3963
|
+
/**
|
|
3964
|
+
* Does `entry` want to receive an event in `category`? Honours the fan-out
|
|
3965
|
+
* mode: `broadcast` → always; undeclared patterns (`null`) → always
|
|
3966
|
+
* (fail-open); else the child's declared set must match via `matchesPattern`
|
|
3967
|
+
* — the SAME function the child's own `deliverShared` map-key gate uses,
|
|
3968
|
+
* giving provable delivery parity. `shadow` is handled by the caller (it
|
|
3969
|
+
* counts the would-suppress but still sends).
|
|
3970
|
+
*
|
|
3971
|
+
* NOTE: this gate must NOT consult `RING_BUFFER_DENY_PATTERNS` /
|
|
3972
|
+
* `isHighFrequencyCategory` — those are ring-buffer STORAGE policy; a child
|
|
3973
|
+
* that explicitly subscribes to a ring-denied category must still RECEIVE it.
|
|
3974
|
+
*/
|
|
3975
|
+
childWantsEvent(entry, category) {
|
|
3976
|
+
if (this.fanoutMode === "broadcast") return true;
|
|
3977
|
+
if (entry.eventPatterns === null) return true;
|
|
3978
|
+
return entry.eventPatterns.some((p) => matchesPattern(p, category));
|
|
3979
|
+
}
|
|
3980
|
+
statsFor(childId) {
|
|
3981
|
+
let s = this.childEventStats.get(childId);
|
|
3982
|
+
if (s === void 0) {
|
|
3983
|
+
s = {
|
|
3984
|
+
sent: 0,
|
|
3985
|
+
suppressed: 0
|
|
3986
|
+
};
|
|
3987
|
+
this.childEventStats.set(childId, s);
|
|
3988
|
+
}
|
|
3989
|
+
return s;
|
|
3990
|
+
}
|
|
3991
|
+
/** Deduped INFO line whenever a child's declared pattern set changes. */
|
|
3992
|
+
logPatternSet(childId, patterns) {
|
|
3993
|
+
const key = patterns === null ? "<undeclared>" : JSON.stringify([...patterns].toSorted());
|
|
3994
|
+
if (this.loggedPatternSet.get(childId) === key) return;
|
|
3995
|
+
this.loggedPatternSet.set(childId, key);
|
|
3996
|
+
this.logger?.info("child event patterns", {
|
|
3997
|
+
childId,
|
|
3998
|
+
patterns: patterns ?? null
|
|
3999
|
+
});
|
|
4000
|
+
}
|
|
4001
|
+
/**
|
|
3813
4002
|
* Child id that can service a call to `capName` (optionally addressing
|
|
3814
4003
|
* `deviceId`), or null.
|
|
3815
4004
|
*
|
|
@@ -3988,14 +4177,20 @@ var LocalChildRegistry = class {
|
|
|
3988
4177
|
* one (the originating child, to avoid echo). Fire-and-forget.
|
|
3989
4178
|
*/
|
|
3990
4179
|
broadcastEventToChildren(event, sourceNodeId, exceptChildId) {
|
|
4180
|
+
const msg = {
|
|
4181
|
+
kind: "event",
|
|
4182
|
+
event,
|
|
4183
|
+
sourceNodeId
|
|
4184
|
+
};
|
|
3991
4185
|
for (const entry of this.children.values()) {
|
|
3992
4186
|
if (entry.childId === exceptChildId) continue;
|
|
3993
|
-
const
|
|
3994
|
-
|
|
3995
|
-
|
|
3996
|
-
|
|
3997
|
-
|
|
3998
|
-
|
|
4187
|
+
const wants = this.childWantsEvent(entry, event.category);
|
|
4188
|
+
const stats = this.statsFor(entry.childId);
|
|
4189
|
+
if (!wants) stats.suppressed++;
|
|
4190
|
+
if (wants || this.fanoutMode === "shadow") {
|
|
4191
|
+
stats.sent++;
|
|
4192
|
+
entry.channel.emit(msg);
|
|
4193
|
+
}
|
|
3999
4194
|
}
|
|
4000
4195
|
}
|
|
4001
4196
|
/**
|
|
@@ -4031,17 +4226,31 @@ var LocalChildRegistry = class {
|
|
|
4031
4226
|
if (childId !== null) this.logHandler?.(childId, msg);
|
|
4032
4227
|
return;
|
|
4033
4228
|
}
|
|
4229
|
+
if (msg.kind === "event-sub") {
|
|
4230
|
+
if (childId === null) return;
|
|
4231
|
+
const entry = this.children.get(childId);
|
|
4232
|
+
if (entry === void 0) return;
|
|
4233
|
+
this.children.set(childId, {
|
|
4234
|
+
...entry,
|
|
4235
|
+
eventPatterns: msg.patterns
|
|
4236
|
+
});
|
|
4237
|
+
this.logPatternSet(childId, msg.patterns);
|
|
4238
|
+
return;
|
|
4239
|
+
}
|
|
4034
4240
|
});
|
|
4035
4241
|
channel.onRequest(async (body) => {
|
|
4036
4242
|
const msg = body;
|
|
4037
4243
|
if (msg.kind === "register") {
|
|
4038
4244
|
if (childId !== null && childId !== msg.childId) throw new Error(`child attempted to change identity from "${childId}" to "${msg.childId}"`);
|
|
4039
4245
|
childId = msg.childId;
|
|
4246
|
+
const eventPatterns = msg.eventPatterns ?? null;
|
|
4040
4247
|
this.children.set(msg.childId, {
|
|
4041
4248
|
childId: msg.childId,
|
|
4042
4249
|
channel,
|
|
4043
|
-
caps: msg.caps
|
|
4250
|
+
caps: msg.caps,
|
|
4251
|
+
eventPatterns
|
|
4044
4252
|
});
|
|
4253
|
+
this.logPatternSet(msg.childId, eventPatterns);
|
|
4045
4254
|
this.registeredHandler({
|
|
4046
4255
|
childId: msg.childId,
|
|
4047
4256
|
caps: msg.caps
|
|
@@ -4074,7 +4283,11 @@ var LocalChildRegistry = class {
|
|
|
4074
4283
|
throw new Error(`unknown child request kind: ${msg.kind}`);
|
|
4075
4284
|
});
|
|
4076
4285
|
channel.onClose(() => {
|
|
4077
|
-
if (childId !== null && this.children.delete(childId))
|
|
4286
|
+
if (childId !== null && this.children.delete(childId)) {
|
|
4287
|
+
this.childEventStats.delete(childId);
|
|
4288
|
+
this.loggedPatternSet.delete(childId);
|
|
4289
|
+
this.goneHandler(childId);
|
|
4290
|
+
}
|
|
4078
4291
|
});
|
|
4079
4292
|
}
|
|
4080
4293
|
};
|
|
@@ -4106,6 +4319,24 @@ var LocalChildClient = class {
|
|
|
4106
4319
|
* the latest set) instead of being lost or throwing.
|
|
4107
4320
|
*/
|
|
4108
4321
|
latestCaps;
|
|
4322
|
+
/**
|
|
4323
|
+
* Per-owner (addonId) category-pattern subscription sets, and their union.
|
|
4324
|
+
* The union is declared to the parent (in `RegisterMessage.eventPatterns`
|
|
4325
|
+
* pre/at-connect, or via an `event-sub` emit post-connect) so the parent can
|
|
4326
|
+
* subscription-filter its event fan-out. Per-owner keying keeps the union
|
|
4327
|
+
* correct if multiple event buses ever share one client (group-runner case).
|
|
4328
|
+
*/
|
|
4329
|
+
patternsByOwner = /* @__PURE__ */ new Map();
|
|
4330
|
+
latestEventPatterns = [];
|
|
4331
|
+
/**
|
|
4332
|
+
* Whether `updateEventPatterns` has ever been called. A client that never
|
|
4333
|
+
* declared a subscription set (no event bus wired — e.g. a pure cap-call
|
|
4334
|
+
* runner) omits `eventPatterns` from its register frame entirely, so the
|
|
4335
|
+
* parent treats it as UNDECLARED and fails OPEN (broadcast-all). Once any
|
|
4336
|
+
* owner declares (in production every addon context's framework
|
|
4337
|
+
* subscriptions do), the register carries the real union — even if empty.
|
|
4338
|
+
*/
|
|
4339
|
+
hasDeclaredEventPatterns = false;
|
|
4109
4340
|
/** Events and logs queued while the channel is not yet open. */
|
|
4110
4341
|
pendingEmits = [];
|
|
4111
4342
|
/** Handler for parent→child events. Registered via `onEvent`. */
|
|
@@ -4132,6 +4363,42 @@ var LocalChildClient = class {
|
|
|
4132
4363
|
this.latestCaps = options.caps;
|
|
4133
4364
|
}
|
|
4134
4365
|
/**
|
|
4366
|
+
* Declare an owner's live category-pattern subscription set. Stores it
|
|
4367
|
+
* per-owner (keyed by `ownerId` = addonId), recomputes the union, and — only
|
|
4368
|
+
* if the union changed — declares it to the parent:
|
|
4369
|
+
* - pre-connect: buffered only; `start()`'s register frame carries the set.
|
|
4370
|
+
* - post-connect: emitted as a fire-and-forget `event-sub` (full-set
|
|
4371
|
+
* replace).
|
|
4372
|
+
* Idempotent on an unchanged union (no redundant `event-sub` frames).
|
|
4373
|
+
*/
|
|
4374
|
+
updateEventPatterns(ownerId, patterns) {
|
|
4375
|
+
this.hasDeclaredEventPatterns = true;
|
|
4376
|
+
this.patternsByOwner.set(ownerId, patterns);
|
|
4377
|
+
const union = this.computePatternUnion();
|
|
4378
|
+
if (!this.patternsEqual(union, this.latestEventPatterns)) {
|
|
4379
|
+
this.latestEventPatterns = union;
|
|
4380
|
+
if (this.channel !== null) {
|
|
4381
|
+
const msg = {
|
|
4382
|
+
kind: "event-sub",
|
|
4383
|
+
patterns: union
|
|
4384
|
+
};
|
|
4385
|
+
this.channel.emit(msg);
|
|
4386
|
+
}
|
|
4387
|
+
}
|
|
4388
|
+
}
|
|
4389
|
+
/** Sorted, de-duplicated union across all owners' pattern sets. */
|
|
4390
|
+
computePatternUnion() {
|
|
4391
|
+
const all = /* @__PURE__ */ new Set();
|
|
4392
|
+
for (const patterns of this.patternsByOwner.values()) for (const p of patterns) all.add(p);
|
|
4393
|
+
return [...all].toSorted();
|
|
4394
|
+
}
|
|
4395
|
+
/** Order-insensitive equality — both inputs are already sorted unions here. */
|
|
4396
|
+
patternsEqual(a, b) {
|
|
4397
|
+
if (a.length !== b.length) return false;
|
|
4398
|
+
for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
|
|
4399
|
+
return true;
|
|
4400
|
+
}
|
|
4401
|
+
/**
|
|
4135
4402
|
* Register a callback that fires each time the client successfully connects
|
|
4136
4403
|
* (or reconnects) to its parent. Multiple handlers may be registered; all
|
|
4137
4404
|
* are called in registration order. Used by readiness-context in UDS mode
|
|
@@ -4210,7 +4477,8 @@ var LocalChildClient = class {
|
|
|
4210
4477
|
const register = {
|
|
4211
4478
|
kind: "register",
|
|
4212
4479
|
childId: this.options.childId,
|
|
4213
|
-
caps: this.latestCaps
|
|
4480
|
+
caps: this.latestCaps,
|
|
4481
|
+
...this.hasDeclaredEventPatterns ? { eventPatterns: this.latestEventPatterns } : {}
|
|
4214
4482
|
};
|
|
4215
4483
|
try {
|
|
4216
4484
|
await channel.request(register);
|
|
@@ -4243,7 +4511,8 @@ var LocalChildClient = class {
|
|
|
4243
4511
|
const register = {
|
|
4244
4512
|
kind: "register",
|
|
4245
4513
|
childId: this.options.childId,
|
|
4246
|
-
caps
|
|
4514
|
+
caps,
|
|
4515
|
+
...this.hasDeclaredEventPatterns ? { eventPatterns: this.latestEventPatterns } : {}
|
|
4247
4516
|
};
|
|
4248
4517
|
await this.channel.request(register);
|
|
4249
4518
|
}
|
|
@@ -4404,105 +4673,6 @@ function udsChildLogToWorkerEntry(childId, entry) {
|
|
|
4404
4673
|
};
|
|
4405
4674
|
}
|
|
4406
4675
|
//#endregion
|
|
4407
|
-
//#region src/kernel/moleculer/event-bus-core.ts
|
|
4408
|
-
function createSharedBusState() {
|
|
4409
|
-
return {
|
|
4410
|
-
handlers: /* @__PURE__ */ new Map(),
|
|
4411
|
-
recent: []
|
|
4412
|
-
};
|
|
4413
|
-
}
|
|
4414
|
-
function matchesPattern(pattern, category) {
|
|
4415
|
-
if (pattern === "*" || pattern === "**") return true;
|
|
4416
|
-
if (pattern.endsWith(".**")) return category.startsWith(pattern.slice(0, -3));
|
|
4417
|
-
if (pattern.endsWith(".*")) return category.startsWith(pattern.slice(0, -2));
|
|
4418
|
-
return pattern === category;
|
|
4419
|
-
}
|
|
4420
|
-
function extractCategoryPattern(filter) {
|
|
4421
|
-
if (typeof filter === "string") return filter;
|
|
4422
|
-
if (filter && typeof filter === "object" && "category" in filter) {
|
|
4423
|
-
const cat = filter.category;
|
|
4424
|
-
if (typeof cat === "string") return cat;
|
|
4425
|
-
if (Array.isArray(cat) && cat.length > 0) return String(cat[0]);
|
|
4426
|
-
}
|
|
4427
|
-
return "*";
|
|
4428
|
-
}
|
|
4429
|
-
/**
|
|
4430
|
-
* Match an event against the full `EventFilter`. The Map-keyed subscribe path
|
|
4431
|
-
* only handles the category pattern (deciding which handler set to fan out to);
|
|
4432
|
-
* this function applies the remaining scope dimensions (`agentId`, `addonId`,
|
|
4433
|
-
* `deviceId`, `source`, `since`) so per-device / per-addon subscribers don't
|
|
4434
|
-
* receive sibling events.
|
|
4435
|
-
*/
|
|
4436
|
-
function matchesEventFilter(event, filter) {
|
|
4437
|
-
if (!filter || typeof filter === "string") return true;
|
|
4438
|
-
if (filter.source) {
|
|
4439
|
-
if (event.source.type !== filter.source.type || event.source.id !== filter.source.id) return false;
|
|
4440
|
-
}
|
|
4441
|
-
if (filter.agentId) {
|
|
4442
|
-
const eventNodeId = event.source.nodeId;
|
|
4443
|
-
if (!eventNodeId || eventNodeId !== filter.agentId && !eventNodeId.startsWith(`${filter.agentId}/`)) return false;
|
|
4444
|
-
}
|
|
4445
|
-
if (filter.addonId) {
|
|
4446
|
-
if ((event.source.addonId ?? (event.source.type === "addon" ? String(event.source.id) : void 0)) !== filter.addonId) return false;
|
|
4447
|
-
}
|
|
4448
|
-
if (filter.deviceId !== void 0) {
|
|
4449
|
-
if ((event.source.deviceId ?? (event.source.type === "device" ? Number(event.source.id) : void 0)) !== filter.deviceId) return false;
|
|
4450
|
-
}
|
|
4451
|
-
if (filter.since && event.timestamp < filter.since) return false;
|
|
4452
|
-
return true;
|
|
4453
|
-
}
|
|
4454
|
-
/**
|
|
4455
|
-
* Categories filtered out of the `recent[]` ring buffer.
|
|
4456
|
-
*
|
|
4457
|
-
* High-frequency, low-value-for-the-operator categories — periodic metrics
|
|
4458
|
-
* snapshots, per-frame inference traces, raw motion-mask dumps, model-download
|
|
4459
|
-
* progress, etc. They still fan out to live subscribers (UI charts that
|
|
4460
|
-
* explicitly listen for them), but do NOT enter the recent-events ring so
|
|
4461
|
-
* meaningful events (motion / phase transitions / device lifecycle / addon
|
|
4462
|
-
* lifecycle / recording / detection.event) survive in the last-N window without
|
|
4463
|
-
* being displaced by per-frame noise.
|
|
4464
|
-
*/
|
|
4465
|
-
var RING_BUFFER_DENY_PATTERNS = [
|
|
4466
|
-
"pipeline.camera-metrics-snapshot",
|
|
4467
|
-
"pipeline.runner-load-snapshot",
|
|
4468
|
-
"pipeline.engine-metrics-snapshot",
|
|
4469
|
-
"pipeline.inference-result",
|
|
4470
|
-
"pipeline.trace",
|
|
4471
|
-
"pipeline.progress",
|
|
4472
|
-
"stream-broker.metrics-snapshot",
|
|
4473
|
-
"metrics.node-resources-snapshot",
|
|
4474
|
-
"metrics.node-processes-snapshot",
|
|
4475
|
-
"cluster.topology-snapshot",
|
|
4476
|
-
"detection.motion-analysis",
|
|
4477
|
-
"detection.motion-zones-raw",
|
|
4478
|
-
"detection.result",
|
|
4479
|
-
"pipeline.audio-inference-result",
|
|
4480
|
-
"platform-probe.phase",
|
|
4481
|
-
"benchmark.progress",
|
|
4482
|
-
"model.download.progress",
|
|
4483
|
-
"capability.binding-changed"
|
|
4484
|
-
];
|
|
4485
|
-
function isHighFrequencyCategory(category) {
|
|
4486
|
-
for (const pattern of RING_BUFFER_DENY_PATTERNS) if (matchesPattern(pattern, category)) return true;
|
|
4487
|
-
return false;
|
|
4488
|
-
}
|
|
4489
|
-
/**
|
|
4490
|
-
* Deliver `event` to all matching local subscribers and — unless it is
|
|
4491
|
-
* high-frequency — append it to the `recent[]` ring.
|
|
4492
|
-
*/
|
|
4493
|
-
function deliverShared(state, event) {
|
|
4494
|
-
if (!isHighFrequencyCategory(event.category)) state.recent.push(event);
|
|
4495
|
-
for (const [pattern, set] of state.handlers) {
|
|
4496
|
-
if (!matchesPattern(pattern, event.category)) continue;
|
|
4497
|
-
for (const entry of set) {
|
|
4498
|
-
if (!matchesEventFilter(event, entry.filter)) continue;
|
|
4499
|
-
try {
|
|
4500
|
-
entry.handler(event);
|
|
4501
|
-
} catch (err) {}
|
|
4502
|
-
}
|
|
4503
|
-
}
|
|
4504
|
-
}
|
|
4505
|
-
//#endregion
|
|
4506
4676
|
//#region src/kernel/transport/uds-event-bus.ts
|
|
4507
4677
|
/**
|
|
4508
4678
|
* Create a UDS-backed `IEventBus` for use inside a forked child process.
|
|
@@ -4517,6 +4687,9 @@ function createUdsEventBus(client, addonId) {
|
|
|
4517
4687
|
client.onEvent((event) => {
|
|
4518
4688
|
deliverShared(state, event);
|
|
4519
4689
|
});
|
|
4690
|
+
const reportPatterns = () => {
|
|
4691
|
+
client.updateEventPatterns(addonId, [...state.handlers.keys()]);
|
|
4692
|
+
};
|
|
4520
4693
|
return {
|
|
4521
4694
|
emit(event) {
|
|
4522
4695
|
const enriched = {
|
|
@@ -4536,8 +4709,13 @@ function createUdsEventBus(client, addonId) {
|
|
|
4536
4709
|
const set = state.handlers.get(pattern) ?? /* @__PURE__ */ new Set();
|
|
4537
4710
|
set.add(entry);
|
|
4538
4711
|
state.handlers.set(pattern, set);
|
|
4712
|
+
reportPatterns();
|
|
4539
4713
|
return () => {
|
|
4540
|
-
state.handlers.get(pattern)
|
|
4714
|
+
const current = state.handlers.get(pattern);
|
|
4715
|
+
if (current === void 0) return;
|
|
4716
|
+
current.delete(entry);
|
|
4717
|
+
if (current.size === 0) state.handlers.delete(pattern);
|
|
4718
|
+
reportPatterns();
|
|
4541
4719
|
};
|
|
4542
4720
|
},
|
|
4543
4721
|
getRecent(filter, limit) {
|
|
@@ -5665,12 +5843,20 @@ function clusterEventTopic(category) {
|
|
|
5665
5843
|
* brokers GC naturally.
|
|
5666
5844
|
*/
|
|
5667
5845
|
var brokerBusState = /* @__PURE__ */ new WeakMap();
|
|
5668
|
-
|
|
5846
|
+
/**
|
|
5847
|
+
* @param retainRecent - `true` from the hub-main opt-in call site
|
|
5848
|
+
* (`event-bus.service.ts`). If the bus was already created by a non-retaining
|
|
5849
|
+
* caller (e.g. `process-service`, the `$event-bus` handler, or the per-addon
|
|
5850
|
+
* wrapper) we UPGRADE it in place so the opt-in wins regardless of call
|
|
5851
|
+
* order. Only the hub main process ever passes `true`; agents + child runners
|
|
5852
|
+
* never do, so their bus stays one-shot.
|
|
5853
|
+
*/
|
|
5854
|
+
function getSharedBusState(broker, retainRecent = false) {
|
|
5669
5855
|
let state = brokerBusState.get(broker);
|
|
5670
5856
|
if (!state) {
|
|
5671
|
-
state = createSharedBusState();
|
|
5857
|
+
state = createSharedBusState(retainRecent);
|
|
5672
5858
|
brokerBusState.set(broker, state);
|
|
5673
|
-
}
|
|
5859
|
+
} else if (retainRecent && !state.retainRecent) state.retainRecent = true;
|
|
5674
5860
|
return state;
|
|
5675
5861
|
}
|
|
5676
5862
|
/** Brokers that already have the `$event-bus` service installed. */
|
|
@@ -5717,9 +5903,16 @@ function registerEventBusService(broker) {
|
|
|
5717
5903
|
} catch {}
|
|
5718
5904
|
}
|
|
5719
5905
|
}
|
|
5720
|
-
|
|
5906
|
+
/**
|
|
5907
|
+
* @param options.retainRecent - Pass `true` ONLY from the hub main process
|
|
5908
|
+
* (`EventBusService.attachBroker`), the single process that serves the
|
|
5909
|
+
* `getRecent`/audit capability. Every other caller (agents, child runners,
|
|
5910
|
+
* the per-addon wrapper, the `$event-bus` handler) omits it → the bus retains
|
|
5911
|
+
* nothing and `getRecent` returns `[]`. Events are one-shot everywhere else.
|
|
5912
|
+
*/
|
|
5913
|
+
function getBrokerEventBus(broker, options) {
|
|
5721
5914
|
const bkr = broker;
|
|
5722
|
-
const state = getSharedBusState(broker);
|
|
5915
|
+
const state = getSharedBusState(broker, options?.retainRecent ?? false);
|
|
5723
5916
|
registerEventBusService(broker);
|
|
5724
5917
|
const warnTimestamps = (() => {
|
|
5725
5918
|
let m = brokerBusWarnTimestamps.get(broker);
|