@camstack/server 1.1.26 → 1.1.27

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.
@@ -143,10 +143,11 @@ function buildCapabilityRouters(services) {
143
143
  live: (0, live_events_router_js_1.createLiveEventsRouter)(services.eventBus, services.addonRegistry),
144
144
  // stream-probe — fixed core API (ffprobe wrapper), not a cap.
145
145
  streamProbe: (0, stream_probe_router_js_1.createStreamProbeRouter)(services.streamProbe),
146
- // hwaccel — fixed core API, wraps the per-node `$hwaccel` Moleculer
147
- // service. UI pipeline / NodeDetail pages query per-node to show
148
- // which hardware backend each agent will use.
149
- hwaccel: (0, hwaccel_router_js_1.createHwAccelRouter)(services.moleculer?.broker ?? null),
146
+ // hwaccel — fixed core API, repointed onto the `platform-probe` cap
147
+ // (the sole cross-node-pinnable hwaccel surface). UI pipeline /
148
+ // NodeDetail pages query per-node to show which hardware backend each
149
+ // agent will use.
150
+ hwaccel: (0, hwaccel_router_js_1.createHwAccelRouter)(services.capabilityRegistry, services.moleculer),
150
151
  auth: (0, auth_router_js_1.createAuthRouter)(services.authService, services.capabilityRegistry),
151
152
  // ── Cap overrides: `mount: { kind: 'custom' }` ──────────────────
152
153
  // `snapshot-provider.supportsDevice` is an OR across providers;
@@ -34,6 +34,7 @@ var __importStar = (this && this.__importStar) || (function () {
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.AddonRegistryService = void 0;
37
+ exports.shouldEmitProviderRegisteredReady = shouldEmitProviderRegisteredReady;
37
38
  /* eslint-disable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-member-access -- pre-existing lint debt across this 2200-line orchestration class. The flagged sites (StorageService.setLocationManager / setSettingsBackend, LoggingService.addDestination, RouteRegistry, etc.) are typed as `unknown` by their owning services to break circular construction-order dependencies; runtime contracts are validated structurally. Tracked separately; do not amend in unrelated edits. */
38
39
  const os = __importStar(require("node:os"));
39
40
  const addon_row_manifest_1 = require("./addon-row-manifest");
@@ -159,6 +160,19 @@ function parseDataPlaneEndpoints(raw) {
159
160
  // shim that consumed it were deleted. All settings flow through the
160
161
  // new `getAddonSettings / getGlobalSettings / getDeviceSettings`
161
162
  // endpoints on the `addon-settings` singleton capability.
163
+ /**
164
+ * Whether the `capability:provider-registered` hook may emit `ready` for a
165
+ * capability. `platform-probe` OWNS its readiness protocol: its provider
166
+ * registers synchronously from `onInitialize` while the REAL probe
167
+ * (hardware + embedded-Python EP scoring) is still running async — the
168
+ * registration-time emit would flip it `ready` prematurely and probe-gated
169
+ * consumers (detection-pipeline engine auto-pick) would read
170
+ * accelerator-blind results. The addon emits `starting`/`ready`/`down`
171
+ * itself (`autoEmitReadiness=false`, ready on probe-done).
172
+ */
173
+ function shouldEmitProviderRegisteredReady(capability) {
174
+ return capability !== 'platform-probe';
175
+ }
162
176
  class AddonRegistryService {
163
177
  loggingService;
164
178
  eventBusService;
@@ -2048,13 +2062,18 @@ class AddonRegistryService {
2048
2062
  // loops). The node-scoped emit is sufficient: the hydrate
2049
2063
  // path on subprocess brokers replays whichever records the
2050
2064
  // hub's `$readiness.getSnapshot` returns, scope and all.
2051
- try {
2052
- this.moleculer.readinessRegistry.emitReady(capability, { type: 'node', nodeId: 'hub' });
2053
- }
2054
- catch (err) {
2055
- this.logger.warn('emitReady failed', {
2056
- meta: { capability, addonId, error: (0, types_1.errMsg)(err) },
2057
- });
2065
+ // Caps that OWN their readiness (platform-probe: registration
2066
+ // happens while its async probe still runs) are excluded —
2067
+ // this emit would flip them `ready` prematurely.
2068
+ if (shouldEmitProviderRegisteredReady(capability)) {
2069
+ try {
2070
+ this.moleculer.readinessRegistry.emitReady(capability, { type: 'node', nodeId: 'hub' });
2071
+ }
2072
+ catch (err) {
2073
+ this.logger.warn('emitReady failed', {
2074
+ meta: { capability, addonId, error: (0, types_1.errMsg)(err) },
2075
+ });
2076
+ }
2058
2077
  }
2059
2078
  switch (capability) {
2060
2079
  case 'storage': {
@@ -407,12 +407,27 @@ class AgentRegistryService {
407
407
  const hubEntry = await this.buildHubEntry(hubProcesses);
408
408
  const remoteEntries = [];
409
409
  const registry = this.moleculer.broker.registry;
410
- const nodes = registry?.getNodeList?.({ onlyAvailable: true }) ?? [];
410
+ // `onlyAvailable: false` intentionally includes nodes Moleculer still
411
+ // remembers as disconnected (`available: false`) — the same registry
412
+ // read `listNodeLiveness()` uses for `/health`. This surfaces recently
413
+ // offline agents inline instead of silently omitting them; it is NOT a
414
+ // new shadow map (no persistence, no timestamp field added anywhere) —
415
+ // once Moleculer's own `cleanOfflineNodesTimeout` purges the entry, the
416
+ // row disappears again. A hub-restart-surviving offline history is a
417
+ // separate, deliberately deferred piece of work.
418
+ const nodes = registry?.getNodeList?.({ onlyAvailable: false }) ?? [];
411
419
  for (const node of nodes) {
412
420
  const nodeId = node.id;
413
421
  // Skip hub (already included) and child processes (contain '/')
414
422
  if (nodeId === 'hub' || nodeId.includes('/'))
415
423
  continue;
424
+ if (!node.available) {
425
+ // Offline: skip the `$agent.status`/`$process.list` RPC fan-out
426
+ // entirely (the node is unreachable — those calls would just time
427
+ // out) and surface a minimal, honestly-degraded row instead.
428
+ remoteEntries.push(this.buildOfflineEntry(node));
429
+ continue;
430
+ }
416
431
  try {
417
432
  const status = (await this.broker.call('$agent.status', {}, {
418
433
  nodeID: nodeId,
@@ -486,12 +501,50 @@ class AgentRegistryService {
486
501
  // Skip nodes without $agent service
487
502
  }
488
503
  }
489
- // TODO(D3 follow-up): offline-agent history dropped with knownAgents.
490
- // Previously, agents that disconnected were kept in a shadow map and
491
- // surfaced here as offline rows. listNodes now reflects only live
492
- // broker.registry nodes.
504
+ // Offline agents ARE included above (see the `!node.available` branch)
505
+ // as long as Moleculer's own registry still remembers them. That window
506
+ // is transient it does not survive a hub restart and disappears once
507
+ // `cleanOfflineNodesTimeout` purges the entry. A durable, restart-proof
508
+ // offline-node history is a separate, deliberately deferred piece of
509
+ // work (would need its own routing-blind store — never a `knownAgents`-
510
+ // style addition to `HubNodeRegistry`).
493
511
  return [hubEntry, ...remoteEntries];
494
512
  }
513
+ /**
514
+ * Minimal, honestly-degraded entry for a node Moleculer's registry still
515
+ * remembers but marks `available: false`. No RPC round-trip is attempted
516
+ * (the node is unreachable by definition) so every live-only field is
517
+ * zeroed/defaulted rather than guessed. `connectedSince: Date.now()`
518
+ * keeps the downstream `uptime = Date.now() - connectedSince` topology
519
+ * projection at ~0 instead of asserting a stale/undefined value.
520
+ */
521
+ buildOfflineEntry(node) {
522
+ return {
523
+ info: {
524
+ id: node.id,
525
+ name: node.id,
526
+ hostname: node.hostname ?? node.id,
527
+ capabilities: [],
528
+ platform: 'unknown',
529
+ arch: 'unknown',
530
+ cpuCores: 0,
531
+ memoryMB: 0,
532
+ },
533
+ localIps: [],
534
+ status: {
535
+ activeCameras: 0,
536
+ cpuPercent: 0,
537
+ memoryPercent: 0,
538
+ fps: {},
539
+ errors: [],
540
+ },
541
+ connectedSince: Date.now(),
542
+ isHub: false,
543
+ isOnline: false,
544
+ subProcesses: [],
545
+ agentAddons: [],
546
+ };
547
+ }
495
548
  async buildHubEntry(subProcesses = []) {
496
549
  const cpus = os.cpus();
497
550
  // Get live metrics from the metrics-provider capability (NativeMetricsProvider).
@@ -251,6 +251,14 @@ class MoleculerService {
251
251
  // handler retries the hub-local route then throws a precise error rather
252
252
  // than the unroutable broker fallback (`switch.switch.getStatus`).
253
253
  isDeviceNativeCap: (capName) => this.capabilityService.getRegistry()?.getDefinition(capName)?.deviceNative === true,
254
+ // `nodeIdMode: 'data'` signal: for these caps (`addon-settings`,
255
+ // `addons`, `nodes`, `pipeline-orchestrator`) an inline `args.nodeId`
256
+ // is provider DATA — the hub singleton dispatches internally — never a
257
+ // routing pin. Without this a forked child's call carrying `nodeId`
258
+ // gets pinned to a node with no provider (dead broker fallback →
259
+ // ServiceNotFoundError). The kernel layer has no cap registry, so feed
260
+ // it the flag from the cap definition.
261
+ isDataNodeIdCap: (capName) => this.capabilityService.getRegistry()?.getDefinition(capName)?.nodeIdMode === 'data',
254
262
  // Empty-collection fast-path: a `collection` cap's array-output method
255
263
  // with ZERO providers registered cluster-wide IS the empty aggregate —
256
264
  // return [] instead of routing (resolver miss → broker fallback → ~30s
@@ -388,12 +396,6 @@ class MoleculerService {
388
396
  probe: (url, options) => this.streamProbe.probe(url, options),
389
397
  probeField: (key, value) => this.streamProbe.probeField(key, value),
390
398
  }));
391
- // Register `$hwaccel` on hub — every node in the cluster does the
392
- // same so `broker.call('$hwaccel.resolve', params, { nodeID })`
393
- // returns the backend list for whichever host the caller targets.
394
- // Admin UI uses this to show per-agent hwaccel info on the
395
- // pipeline / NodeDetail pages.
396
- this.brokerSafe.createService((0, system_1.createHwAccelService)((0, system_1.createKernelHwAccel)()));
397
399
  await this.brokerSafe.start();
398
400
  logger.info('Moleculer broker started (TCP transport)');
399
401
  // Construct the CapRouteResolver now that both the broker and
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/server",
3
- "version": "1.1.26",
3
+ "version": "1.1.27",
4
4
  "private": false,
5
5
  "files": [
6
6
  "dist",