@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.
@@ -1,5 +1,5 @@
1
1
  const require_chunk = require("./chunk-Cek0wNdY.js");
2
- const require_manifest_python_deps = require("./manifest-python-deps-Dt5XNSyd.js");
2
+ const require_manifest_python_deps = require("./manifest-python-deps-PbdHKDpj.js");
3
3
  const require_custom_action_registry = require("./custom-action-registry-vLYEFTtv.js");
4
4
  let node_fs = require("node:fs");
5
5
  node_fs = require_chunk.__toESM(node_fs);
@@ -1,4 +1,4 @@
1
- import { $ as getWorkerNativeCapProvider, N as createUdsLoggerWithControl, P as LocalChildClient, _t as resolveAddonClass, et as getWorkerNativeCapSnapshot, gt as installManifestNativeDeps, i as createUdsAddonContext, it as validateProviderRegistrations, nt as setWorkerNativeCapsChangeListener, t as installManifestPythonDeps } from "./manifest-python-deps-C1Dd9MSr.mjs";
1
+ import { $ as getWorkerNativeCapProvider, N as createUdsLoggerWithControl, P as LocalChildClient, _t as resolveAddonClass, et as getWorkerNativeCapSnapshot, gt as installManifestNativeDeps, i as createUdsAddonContext, it as validateProviderRegistrations, nt as setWorkerNativeCapsChangeListener, t as installManifestPythonDeps } from "./manifest-python-deps-X00KVYHa.mjs";
2
2
  import { t as CustomActionRegistry } from "./custom-action-registry-BEXwC-oo.mjs";
3
3
  import { register } from "node:module";
4
4
  import * as fs from "node:fs";
package/dist/index.js CHANGED
@@ -20,7 +20,7 @@ const require_builtins_local_auth_local_auth_addon = require("./builtins/local-a
20
20
  require("./builtins/local-auth/index.js");
21
21
  const require_builtins_device_manager_device_manager_addon = require("./builtins/device-manager/device-manager.addon.js");
22
22
  require("./builtins/device-manager/index.js");
23
- const require_manifest_python_deps = require("./manifest-python-deps-Dt5XNSyd.js");
23
+ const require_manifest_python_deps = require("./manifest-python-deps-PbdHKDpj.js");
24
24
  const require_custom_action_registry = require("./custom-action-registry-vLYEFTtv.js");
25
25
  let _camstack_types_node = require("@camstack/types/node");
26
26
  let node_http = require("node:http");
@@ -92358,6 +92358,16 @@ function readAddonHeapProfile(spec) {
92358
92358
  heapProfileCache.set(cacheKey, profile);
92359
92359
  return profile;
92360
92360
  }
92361
+ /**
92362
+ * Drop the cached heap profile for each addon so the NEXT `readAddonHeapProfile`
92363
+ * re-reads the manifest from disk. Called on operator-driven spawns / restarts
92364
+ * (a deploy may have changed the addon's `execution.heapProfile`); the
92365
+ * crash-respawn fast path deliberately does NOT invalidate — a crashing runner
92366
+ * has the same on-disk manifest, so re-reading it is pure overhead.
92367
+ */
92368
+ function invalidateHeapProfiles(addons) {
92369
+ for (const spec of addons) heapProfileCache.delete(`${spec.addonDir}::${spec.addonId}`);
92370
+ }
92361
92371
  function manifestHasAddon(parsed, addonId) {
92362
92372
  return readManifestAddons(parsed).some((a) => a.id === addonId);
92363
92373
  }
@@ -92478,12 +92488,13 @@ function createProcessService(parentNodeId, dataDir, deps, parentTcpPort, parent
92478
92488
  CAMSTACK_RUNNER_ADDONS: JSON.stringify(addons),
92479
92489
  CAMSTACK_PARENT_NODE_ID: parentNodeId,
92480
92490
  CAMSTACK_DATA_DIR: dataDir,
92481
- CAMSTACK_LOG_LEVEL: "debug",
92491
+ CAMSTACK_LOG_LEVEL: process.env["CAMSTACK_LOG_LEVEL"] ?? "info",
92482
92492
  ...parentTcpPort !== void 0 ? { CAMSTACK_PARENT_TCP_PORT: String(parentTcpPort) } : {},
92483
92493
  ...parentUdsPath !== void 0 ? { CAMSTACK_PARENT_UDS_PATH: parentUdsPath } : {},
92484
92494
  ...env
92485
92495
  };
92486
92496
  const heapFlags = runnerHeapFlags(addons);
92497
+ capturedBroker?.logger.info(`[${runnerId}] heap profile: ${isHeavyRunner(addons) ? "heavy" : "light"} flags=[${heapFlags.join(" ")}]`);
92487
92498
  const child = spawnFn(process.execPath, [...heapFlags, runnerPath], {
92488
92499
  env: childEnv,
92489
92500
  stdio: [
@@ -92558,6 +92569,7 @@ function createProcessService(parentNodeId, dataDir, deps, parentTcpPort, parent
92558
92569
  const { runnerId, addons, env } = ctx.params;
92559
92570
  if (processes.has(runnerId)) throw new Error(`Runner "${runnerId}" already running`);
92560
92571
  if (!Array.isArray(addons) || addons.length === 0) throw new Error(`spawnRunner requires non-empty addons array`);
92572
+ invalidateHeapProfiles(addons);
92561
92573
  const entry = spawnRunner(runnerId, addons, env);
92562
92574
  return {
92563
92575
  pid: entry.pid,
@@ -92632,6 +92644,7 @@ function createProcessService(parentNodeId, dataDir, deps, parentTcpPort, parent
92632
92644
  processes.delete(name);
92633
92645
  crashSupervisor.reset(name);
92634
92646
  forceMoleculerDisconnect(ctx.broker, deadNodeId);
92647
+ invalidateHeapProfiles(runnerAddons);
92635
92648
  const restarted = spawnRunner(name, runnerAddons);
92636
92649
  restarted.restartCount = prevRestartCount + 1;
92637
92650
  return {
@@ -92689,6 +92702,7 @@ function createProcessService(parentNodeId, dataDir, deps, parentTcpPort, parent
92689
92702
  }
92690
92703
  processes.delete(name);
92691
92704
  crashSupervisor.reset(name);
92705
+ invalidateHeapProfiles(runnerAddons);
92692
92706
  const respawned = spawnRunner(name, runnerAddons);
92693
92707
  respawned.restartCount = prevRestartCount + 1;
92694
92708
  });
package/dist/index.mjs CHANGED
@@ -18,7 +18,7 @@ import { LocalAuthAddon, a as require_ms, c as __esmMin, d as __toCommonJS, f as
18
18
  import "./builtins/local-auth/index.mjs";
19
19
  import { DeviceManagerAddon } from "./builtins/device-manager/device-manager.addon.mjs";
20
20
  import "./builtins/device-manager/index.mjs";
21
- import { A as createUdsEventBus, B as CapRouteError, C as ipcParentLink, D as createHubCapForwardService, E as HUB_CAP_FWD_SERVICE, F as LocalChildRegistry, G as UdsLocalTransportServer, H as callWithServiceDiscovery, I as UDS_NO_ROUTE_PREFIX, J as FrameDecoder, K as SocketChannel, L as AGENT_CAP_FWD_ACTION, M as createUdsLogger, N as createUdsLoggerWithControl, O as createParentUnownedCallHandler, P as LocalChildClient, Q as createBrokerDeviceManagerApi, R as AGENT_CAP_FWD_SERVICE, S as ipcChildLink, T as HUB_CAP_FWD_ACTION, U as createLocalTransport, V as classifyCapRoute, W as UdsLocalTransportClient, X as buildNativeCapProxy, Y as encodeFrame, Z as buildUdsNativeCapProxy, _ as __resetCapUsageRegistryForTests, _t as resolveAddonClass, a as getWorkerDeviceRegistry, at as NATIVE_PROVIDER_SERVICE_INFIX, b as brokerTransportLink, c as setHubConnected, ct as capBareAction, d as registerEventBusService, dt as deserializeTypedArrays, f as AddonDepsManager, ft as serializeTypedArrays, g as CapUsageRegistry, gt as installManifestNativeDeps, h as createHwAccelService, ht as CapabilityUnavailableError, i as createUdsAddonContext, it as validateProviderRegistrations, j as udsChildLogToWorkerEntry, k as createUdsEventBridge, l as EVENT_TOPIC_PREFIX, lt as capServiceName, m as resolveHwAccel, mt as CapabilityHandle, n as adaptBrokerToCluster, o as getOrInitReadinessRegistry, ot as capActionName, p as createKernelHwAccel, pt as DeviceRegistry, q as localEndpointPath, r as createAddonContext, rt as createAddonService, s as getOrInitReadinessRegistryForClient, st as capActionSuffix, t as installManifestPythonDeps, tt as mountNativeCapService, u as getBrokerEventBus, ut as parseCapAction, v as getCapUsageRegistry, w as localProviderLink, x as buildLinkChain, y as brokerCallForCap, z as CapRouteResolver } from "./manifest-python-deps-C1Dd9MSr.mjs";
21
+ import { A as createUdsEventBus, B as CapRouteError, C as ipcParentLink, D as createHubCapForwardService, E as HUB_CAP_FWD_SERVICE, F as LocalChildRegistry, G as UdsLocalTransportServer, H as callWithServiceDiscovery, I as UDS_NO_ROUTE_PREFIX, J as FrameDecoder, K as SocketChannel, L as AGENT_CAP_FWD_ACTION, M as createUdsLogger, N as createUdsLoggerWithControl, O as createParentUnownedCallHandler, P as LocalChildClient, Q as createBrokerDeviceManagerApi, R as AGENT_CAP_FWD_SERVICE, S as ipcChildLink, T as HUB_CAP_FWD_ACTION, U as createLocalTransport, V as classifyCapRoute, W as UdsLocalTransportClient, X as buildNativeCapProxy, Y as encodeFrame, Z as buildUdsNativeCapProxy, _ as __resetCapUsageRegistryForTests, _t as resolveAddonClass, a as getWorkerDeviceRegistry, at as NATIVE_PROVIDER_SERVICE_INFIX, b as brokerTransportLink, c as setHubConnected, ct as capBareAction, d as registerEventBusService, dt as deserializeTypedArrays, f as AddonDepsManager, ft as serializeTypedArrays, g as CapUsageRegistry, gt as installManifestNativeDeps, h as createHwAccelService, ht as CapabilityUnavailableError, i as createUdsAddonContext, it as validateProviderRegistrations, j as udsChildLogToWorkerEntry, k as createUdsEventBridge, l as EVENT_TOPIC_PREFIX, lt as capServiceName, m as resolveHwAccel, mt as CapabilityHandle, n as adaptBrokerToCluster, o as getOrInitReadinessRegistry, ot as capActionName, p as createKernelHwAccel, pt as DeviceRegistry, q as localEndpointPath, r as createAddonContext, rt as createAddonService, s as getOrInitReadinessRegistryForClient, st as capActionSuffix, t as installManifestPythonDeps, tt as mountNativeCapService, u as getBrokerEventBus, ut as parseCapAction, v as getCapUsageRegistry, w as localProviderLink, x as buildLinkChain, y as brokerCallForCap, z as CapRouteResolver } from "./manifest-python-deps-X00KVYHa.mjs";
22
22
  import { t as CustomActionRegistry } from "./custom-action-registry-BEXwC-oo.mjs";
23
23
  import { PYTHON_VERSION, buildBinaryPath, downloadBinary, ensureBinary, ensureFfmpeg, ensurePython, findInPath, getFfmpegDownloadUrl, getPlatformInfo, getPythonDownloadUrl, installPythonPackages, installPythonRequirements } from "@camstack/types/node";
24
24
  import { request } from "node:http";
@@ -92350,6 +92350,16 @@ function readAddonHeapProfile(spec) {
92350
92350
  heapProfileCache.set(cacheKey, profile);
92351
92351
  return profile;
92352
92352
  }
92353
+ /**
92354
+ * Drop the cached heap profile for each addon so the NEXT `readAddonHeapProfile`
92355
+ * re-reads the manifest from disk. Called on operator-driven spawns / restarts
92356
+ * (a deploy may have changed the addon's `execution.heapProfile`); the
92357
+ * crash-respawn fast path deliberately does NOT invalidate — a crashing runner
92358
+ * has the same on-disk manifest, so re-reading it is pure overhead.
92359
+ */
92360
+ function invalidateHeapProfiles(addons) {
92361
+ for (const spec of addons) heapProfileCache.delete(`${spec.addonDir}::${spec.addonId}`);
92362
+ }
92353
92363
  function manifestHasAddon(parsed, addonId) {
92354
92364
  return readManifestAddons(parsed).some((a) => a.id === addonId);
92355
92365
  }
@@ -92470,12 +92480,13 @@ function createProcessService(parentNodeId, dataDir, deps, parentTcpPort, parent
92470
92480
  CAMSTACK_RUNNER_ADDONS: JSON.stringify(addons),
92471
92481
  CAMSTACK_PARENT_NODE_ID: parentNodeId,
92472
92482
  CAMSTACK_DATA_DIR: dataDir,
92473
- CAMSTACK_LOG_LEVEL: "debug",
92483
+ CAMSTACK_LOG_LEVEL: process.env["CAMSTACK_LOG_LEVEL"] ?? "info",
92474
92484
  ...parentTcpPort !== void 0 ? { CAMSTACK_PARENT_TCP_PORT: String(parentTcpPort) } : {},
92475
92485
  ...parentUdsPath !== void 0 ? { CAMSTACK_PARENT_UDS_PATH: parentUdsPath } : {},
92476
92486
  ...env
92477
92487
  };
92478
92488
  const heapFlags = runnerHeapFlags(addons);
92489
+ capturedBroker?.logger.info(`[${runnerId}] heap profile: ${isHeavyRunner(addons) ? "heavy" : "light"} flags=[${heapFlags.join(" ")}]`);
92479
92490
  const child = spawnFn(process.execPath, [...heapFlags, runnerPath], {
92480
92491
  env: childEnv,
92481
92492
  stdio: [
@@ -92550,6 +92561,7 @@ function createProcessService(parentNodeId, dataDir, deps, parentTcpPort, parent
92550
92561
  const { runnerId, addons, env } = ctx.params;
92551
92562
  if (processes.has(runnerId)) throw new Error(`Runner "${runnerId}" already running`);
92552
92563
  if (!Array.isArray(addons) || addons.length === 0) throw new Error(`spawnRunner requires non-empty addons array`);
92564
+ invalidateHeapProfiles(addons);
92553
92565
  const entry = spawnRunner(runnerId, addons, env);
92554
92566
  return {
92555
92567
  pid: entry.pid,
@@ -92624,6 +92636,7 @@ function createProcessService(parentNodeId, dataDir, deps, parentTcpPort, parent
92624
92636
  processes.delete(name);
92625
92637
  crashSupervisor.reset(name);
92626
92638
  forceMoleculerDisconnect(ctx.broker, deadNodeId);
92639
+ invalidateHeapProfiles(runnerAddons);
92627
92640
  const restarted = spawnRunner(name, runnerAddons);
92628
92641
  restarted.restartCount = prevRestartCount + 1;
92629
92642
  return {
@@ -92681,6 +92694,7 @@ function createProcessService(parentNodeId, dataDir, deps, parentTcpPort, parent
92681
92694
  }
92682
92695
  processes.delete(name);
92683
92696
  crashSupervisor.reset(name);
92697
+ invalidateHeapProfiles(runnerAddons);
92684
92698
  const respawned = spawnRunner(name, runnerAddons);
92685
92699
  respawned.restartCount = prevRestartCount + 1;
92686
92700
  });
@@ -1,4 +1,4 @@
1
- import { SystemEvent, EventFilter } from '@camstack/types';
1
+ import { EventFilter, SystemEvent } from '@camstack/types';
2
2
  export interface SubscriberEntry {
3
3
  /** Original filter — kept whole so non-category dimensions are honoured per-event. */
4
4
  readonly filter: EventFilter | string | undefined;
@@ -18,11 +18,34 @@ export interface SharedBusState {
18
18
  * high-frequency categories so the buffer only accumulates audit-grade
19
19
  * lifecycle events.
20
20
  *
21
+ * ONLY populated when `retainRecent` is true (the hub main process). On every
22
+ * other bus this array stays empty for the process's whole lifetime.
23
+ *
21
24
  * Cleared on server restart (intentional — events are volatile by design).
22
25
  */
23
26
  readonly recent: SystemEvent[];
27
+ /**
28
+ * Whether this bus retains events in `recent[]` to back the
29
+ * `getRecent`/audit capability.
30
+ *
31
+ * Events are one-shot (fire-and-forget): ONLY the hub main process — the
32
+ * single process that serves `getRecent` — needs the registry, so ONLY its
33
+ * broker bus sets this true. Child addon runners and remote agents leave it
34
+ * false: they fan every event out to their local subscribers but retain
35
+ * nothing, so their heap can never accumulate an unbounded recent[] backlog.
36
+ *
37
+ * Mutable so the authoritative opt-in (the hub-main `getBrokerEventBus`
38
+ * caller) can upgrade a bus that a non-retaining caller happened to create
39
+ * first — see `getSharedBusState`. Never downgraded from true → false.
40
+ */
41
+ retainRecent: boolean;
24
42
  }
25
- export declare function createSharedBusState(): SharedBusState;
43
+ /**
44
+ * @param retainRecent - `true` ONLY for the hub main broker bus (it serves
45
+ * `getRecent`). Defaults to `false` — the safe one-shot default for child
46
+ * runners + agents, which retain nothing.
47
+ */
48
+ export declare function createSharedBusState(retainRecent?: boolean): SharedBusState;
26
49
  export declare function matchesPattern(pattern: string, category: string): boolean;
27
50
  export declare function extractCategoryPattern(filter: EventFilter | string): string;
28
51
  /**
@@ -45,9 +68,23 @@ export declare function matchesEventFilter(event: SystemEvent, filter: EventFilt
45
68
  * being displaced by per-frame noise.
46
69
  */
47
70
  export declare const RING_BUFFER_DENY_PATTERNS: readonly string[];
71
+ /**
72
+ * Hard cap on the `recent[]` buffer size. Even audit-grade categories that
73
+ * are NOT on the deny-list (e.g. `device.state-changed`) must not accumulate
74
+ * without bound — this is the last-N window, not a durable log. Without this
75
+ * cap the buffer grew forever (~26 MB/h/process across every broker, agent,
76
+ * and UDS-child bus), filling the hub to 16 GB.
77
+ */
78
+ export declare const MAX_RECENT_EVENTS = 1000;
48
79
  export declare function isHighFrequencyCategory(category: string): boolean;
49
80
  /**
50
- * Deliver `event` to all matching local subscribers and — unless it is
51
- * high-frequency append it to the `recent[]` ring.
81
+ * Deliver `event` to all matching local subscribers and — ONLY on a bus that
82
+ * opted into retention (`state.retainRecent`, i.e. the hub main process) and
83
+ * unless the event is high-frequency — append it to the bounded `recent[]`
84
+ * ring.
85
+ *
86
+ * On a non-retaining bus (child runners, agents) the retention block is skipped
87
+ * entirely: events are one-shot / fire-and-forget, still fanned out to local
88
+ * subscribers but never accumulated. `getRecent` on such a bus returns `[]`.
52
89
  */
53
90
  export declare function deliverShared(state: SharedBusState, event: SystemEvent): void;
@@ -1,5 +1,5 @@
1
- import { ServiceBroker } from 'moleculer';
2
1
  import { IEventBus } from '@camstack/types';
2
+ import { ServiceBroker } from 'moleculer';
3
3
  /**
4
4
  * Narrow interface for the Moleculer ServiceBroker surface used in this file.
5
5
  * moleculer's index.d.ts chains through eventemitter2 whose package.json has
@@ -49,5 +49,14 @@ export declare function clusterEventTopic(category: string): string;
49
49
  * - forkable process / group runner, before `broker.start()`
50
50
  */
51
51
  export declare function registerEventBusService(broker: ServiceBroker): void;
52
- export declare function getBrokerEventBus(broker: ServiceBroker): IEventBus;
52
+ /**
53
+ * @param options.retainRecent - Pass `true` ONLY from the hub main process
54
+ * (`EventBusService.attachBroker`), the single process that serves the
55
+ * `getRecent`/audit capability. Every other caller (agents, child runners,
56
+ * the per-addon wrapper, the `$event-bus` handler) omits it → the bus retains
57
+ * nothing and `getRecent` returns `[]`. Events are one-shot everywhere else.
58
+ */
59
+ export declare function getBrokerEventBus(broker: ServiceBroker, options?: {
60
+ retainRecent?: boolean;
61
+ }): IEventBus;
53
62
  export declare function createBrokerEventBus(broker: ServiceBroker, addonId: string): IEventBus;
@@ -68,5 +68,5 @@ interface ProcessServiceDeps {
68
68
  readonly spawnFn?: SpawnFn;
69
69
  }
70
70
  declare function createProcessService(parentNodeId: string, dataDir: string, deps?: ProcessServiceDeps, parentTcpPort?: number, parentUdsPath?: string): ServiceSchema;
71
- export { createProcessService };
72
71
  export type { ProcessInfo, SpawnedProcess };
72
+ export { createProcessService };
@@ -1,4 +1,4 @@
1
- import { SystemEvent, IReadinessRegistryRecord, LogLevel, LogTags } from '@camstack/types';
1
+ import { IReadinessRegistryRecord, LogLevel, LogTags, SystemEvent } from '@camstack/types';
2
2
  /**
3
3
  * Routing descriptor a child sends so the parent can route cap calls to it.
4
4
  * The provider implementation never crosses the wire — only these keys do.
@@ -15,6 +15,27 @@ export interface RegisterMessage {
15
15
  readonly kind: 'register';
16
16
  readonly childId: string;
17
17
  readonly caps: readonly ChildCapDescriptor[];
18
+ /**
19
+ * Snapshot of the child's live category-pattern subscriptions (the keys of
20
+ * its local `SharedBusState.handlers` map). Carried IN the register frame so
21
+ * the parent learns the child's subscriptions race-free at boot — no separate
22
+ * post-connect sync can be missed. OPTIONAL: a legacy child bundling an older
23
+ * `@camstack/system` omits it; the parent treats an absent field as
24
+ * "undeclared" and fails OPEN (broadcast every event to that child). See
25
+ * `EventSubUpdateMessage` for post-boot updates.
26
+ */
27
+ readonly eventPatterns?: readonly string[];
28
+ }
29
+ /**
30
+ * Child → parent, fire-and-forget: the child's category-pattern subscription
31
+ * set changed after registration. FULL-SET REPLACE semantics — `patterns` is
32
+ * the complete current set, not a delta. Idempotent and order-safe on the
33
+ * in-order UDS socket (last write wins is correct). Sent via `channel.emit`
34
+ * like {@link ChildEventMessage}; carries no response.
35
+ */
36
+ export interface EventSubUpdateMessage {
37
+ readonly kind: 'event-sub';
38
+ readonly patterns: readonly string[];
18
39
  }
19
40
  /** Child → parent outbound cap invocation — the child asks the parent to route a cap call it does not own. */
20
41
  export interface CapCallOutMessage {
@@ -56,10 +77,10 @@ export interface ReadinessSnapshotRequest {
56
77
  * Child → parent message bodies.
57
78
  * `RegisterMessage`, `CapCallOutMessage`, and `ReadinessSnapshotRequest` travel
58
79
  * as correlated request frames (`channel.request`) and carry a response.
59
- * `ChildEventMessage` and `ChildLogMessage` are sent fire-and-forget via
60
- * `channel.emit()` and do NOT carry a response.
80
+ * `ChildEventMessage`, `ChildLogMessage`, and `EventSubUpdateMessage` are sent
81
+ * fire-and-forget via `channel.emit()` and do NOT carry a response.
61
82
  */
62
- export type ChildToParentRequest = RegisterMessage | CapCallOutMessage | ChildEventMessage | ChildLogMessage | ReadinessSnapshotRequest;
83
+ export type ChildToParentRequest = RegisterMessage | CapCallOutMessage | ChildEventMessage | ChildLogMessage | EventSubUpdateMessage | ReadinessSnapshotRequest;
63
84
  /** Parent → child cap invocation. */
64
85
  export interface CapCallMessage {
65
86
  readonly kind: 'cap-call';
@@ -1,5 +1,5 @@
1
- import { ChildCapDescriptor, CapCallInput, ChildLogMessage, AddonCallInput } from './child-cap-protocol.js';
2
- import { SystemEvent, IReadinessRegistryRecord } from '@camstack/types';
1
+ import { IReadinessRegistryRecord, SystemEvent } from '@camstack/types';
2
+ import { AddonCallInput, CapCallInput, ChildCapDescriptor, ChildLogMessage } from './child-cap-protocol.js';
3
3
  export interface LocalChildClientOptions {
4
4
  /** Parent node id — selects the UDS endpoint this child connects to. */
5
5
  readonly nodeId: string;
@@ -38,6 +38,24 @@ export declare class LocalChildClient {
38
38
  * the latest set) instead of being lost or throwing.
39
39
  */
40
40
  private latestCaps;
41
+ /**
42
+ * Per-owner (addonId) category-pattern subscription sets, and their union.
43
+ * The union is declared to the parent (in `RegisterMessage.eventPatterns`
44
+ * pre/at-connect, or via an `event-sub` emit post-connect) so the parent can
45
+ * subscription-filter its event fan-out. Per-owner keying keeps the union
46
+ * correct if multiple event buses ever share one client (group-runner case).
47
+ */
48
+ private readonly patternsByOwner;
49
+ private latestEventPatterns;
50
+ /**
51
+ * Whether `updateEventPatterns` has ever been called. A client that never
52
+ * declared a subscription set (no event bus wired — e.g. a pure cap-call
53
+ * runner) omits `eventPatterns` from its register frame entirely, so the
54
+ * parent treats it as UNDECLARED and fails OPEN (broadcast-all). Once any
55
+ * owner declares (in production every addon context's framework
56
+ * subscriptions do), the register carries the real union — even if empty.
57
+ */
58
+ private hasDeclaredEventPatterns;
41
59
  /** Events and logs queued while the channel is not yet open. */
42
60
  private readonly pendingEmits;
43
61
  /** Handler for parent→child events. Registered via `onEvent`. */
@@ -60,6 +78,20 @@ export declare class LocalChildClient {
60
78
  /** Callbacks registered via `onConnected`. Fired on every (re)connect. */
61
79
  private readonly connectedHandlers;
62
80
  constructor(options: LocalChildClientOptions);
81
+ /**
82
+ * Declare an owner's live category-pattern subscription set. Stores it
83
+ * per-owner (keyed by `ownerId` = addonId), recomputes the union, and — only
84
+ * if the union changed — declares it to the parent:
85
+ * - pre-connect: buffered only; `start()`'s register frame carries the set.
86
+ * - post-connect: emitted as a fire-and-forget `event-sub` (full-set
87
+ * replace).
88
+ * Idempotent on an unchanged union (no redundant `event-sub` frames).
89
+ */
90
+ updateEventPatterns(ownerId: string, patterns: readonly string[]): void;
91
+ /** Sorted, de-duplicated union across all owners' pattern sets. */
92
+ private computePatternUnion;
93
+ /** Order-insensitive equality — both inputs are already sorted unions here. */
94
+ private patternsEqual;
63
95
  /**
64
96
  * Register a callback that fires each time the client successfully connects
65
97
  * (or reconnects) to its parent. Multiple handlers may be registered; all
@@ -1,6 +1,22 @@
1
+ import { IReadinessRegistryRecord, SystemEvent } from '@camstack/types';
2
+ import { AddonCallInput, CapCallInput, ChildLogMessage, RegisteredChild } from './child-cap-protocol.js';
1
3
  import { LocalTransportServer } from './local-transport.js';
2
- import { ChildLogMessage, CapCallInput, AddonCallInput, RegisteredChild } from './child-cap-protocol.js';
3
- import { SystemEvent, IReadinessRegistryRecord } from '@camstack/types';
4
+ /**
5
+ * Fan-out mode for parent→child event delivery, from the
6
+ * `CAMSTACK_UDS_EVENT_FANOUT` env (read once at registry construction):
7
+ * - `filter` (default) — apply the subscription gate; a child receives a
8
+ * category only if one of its declared patterns matches.
9
+ * - `shadow` — compute the gate decision + count would-suppresses, but
10
+ * STILL send every event (burn-in: prove zero would-be-starved deliveries).
11
+ * - `broadcast` — ignore the gate entirely (today's behaviour); the operator's
12
+ * one-env rollback without redeploying runners.
13
+ */
14
+ export type UdsEventFanoutMode = 'filter' | 'shadow' | 'broadcast';
15
+ /** Per-child event fan-out counters (sent = delivered, suppressed = gated/would-gate). */
16
+ export interface ChildEventStats {
17
+ sent: number;
18
+ suppressed: number;
19
+ }
4
20
  /**
5
21
  * Sentinel prefix used in the no-route error thrown when `cap-call-out` has no
6
22
  * local sibling and no `onUnownedCall` fallback. `ipcParentLink` detects this
@@ -69,6 +85,12 @@ export declare class LocalChildRegistry {
69
85
  private readonly resolveChildIdForAddon?;
70
86
  /** Tracks capNames already logged as UDS-routed; one INFO line per capName per process. */
71
87
  private readonly egressRoutedCaps;
88
+ /** Active event fan-out mode, read once from `CAMSTACK_UDS_EVENT_FANOUT`. */
89
+ private readonly fanoutMode;
90
+ /** Per-child event fan-out counters (sent / suppressed). */
91
+ private readonly childEventStats;
92
+ /** Last pattern-set string logged per child, to dedup the INFO line. */
93
+ private readonly loggedPatternSet;
72
94
  /**
73
95
  * Accepts either a plain positional `server` argument (backward-compatible)
74
96
  * or a full `LocalChildRegistryOptions` object.
@@ -82,6 +104,28 @@ export declare class LocalChildRegistry {
82
104
  */
83
105
  constructor(serverOrOptions: LocalTransportServer | LocalChildRegistryOptions, onUnownedCallArg?: (input: CapCallInput) => Promise<unknown>);
84
106
  start(): Promise<void>;
107
+ /**
108
+ * Per-child event fan-out counters (`{ sent, suppressed }`) or `null` if the
109
+ * child has no recorded events yet. Exposed for the starvation-verification
110
+ * surface (shadow burn-in + operator debug).
111
+ */
112
+ getChildEventStats(childId: string): ChildEventStats | null;
113
+ /**
114
+ * Does `entry` want to receive an event in `category`? Honours the fan-out
115
+ * mode: `broadcast` → always; undeclared patterns (`null`) → always
116
+ * (fail-open); else the child's declared set must match via `matchesPattern`
117
+ * — the SAME function the child's own `deliverShared` map-key gate uses,
118
+ * giving provable delivery parity. `shadow` is handled by the caller (it
119
+ * counts the would-suppress but still sends).
120
+ *
121
+ * NOTE: this gate must NOT consult `RING_BUFFER_DENY_PATTERNS` /
122
+ * `isHighFrequencyCategory` — those are ring-buffer STORAGE policy; a child
123
+ * that explicitly subscribes to a ring-denied category must still RECEIVE it.
124
+ */
125
+ private childWantsEvent;
126
+ private statsFor;
127
+ /** Deduped INFO line whenever a child's declared pattern set changes. */
128
+ private logPatternSet;
85
129
  /**
86
130
  * Child id that can service a call to `capName` (optionally addressing
87
131
  * `deviceId`), or null.
@@ -2,6 +2,16 @@ import { IEventBus, SystemEvent } from '@camstack/types';
2
2
  /**
3
3
  * Minimal surface the bridge requires from `LocalChildRegistry`.
4
4
  * Structurally typed so tests can provide a fake without any casts.
5
+ *
6
+ * CONTRACT: `broadcastEventToChildren` is SUBSCRIPTION-FILTERED. The registry
7
+ * fans an event only to children whose declared category-pattern set matches
8
+ * `event.category` (mirror of each child's local `SharedBusState.handlers`
9
+ * keys). A child that never declared a set (legacy/undeclared) fails OPEN and
10
+ * receives everything. The `CAMSTACK_UDS_EVENT_FANOUT` env (`filter` default /
11
+ * `shadow` / `broadcast`) is the registry-side kill-switch. The bridge itself
12
+ * needs no per-event logic change — both fan-out call sites here go through the
13
+ * filtered `broadcastEventToChildren`. `sendEventToChild` (targeted push) is
14
+ * NOT filtered — an explicit single-child send expresses intent.
5
15
  */
6
16
  export interface ChildEventBroadcaster {
7
17
  /**
@@ -6,10 +6,16 @@ import { IEventBus, SystemEvent } from '@camstack/types';
6
6
  * - `emitEvent` — fire-and-forget: send a system event to the parent.
7
7
  * - `onEvent` — register the single handler for parent→child events.
8
8
  * (Only one handler is registered; calling again replaces the previous one.)
9
+ * - `updateEventPatterns` — declare this owner's live category-pattern set to
10
+ * the parent so it can subscription-filter the event fan-out. Called after
11
+ * every subscribe/unsubscribe with the CURRENT key set (full-set replace).
12
+ * Keyed by `ownerId` (the addonId) so multiple buses sharing one client
13
+ * union correctly.
9
14
  */
10
15
  export interface UdsEventChannel {
11
16
  emitEvent(event: SystemEvent): void;
12
17
  onEvent(handler: (event: SystemEvent) => void): void;
18
+ updateEventPatterns(ownerId: string, patterns: readonly string[]): void;
13
19
  }
14
20
  /**
15
21
  * Create a UDS-backed `IEventBus` for use inside a forked child process.