@camstack/server 1.1.22 → 1.1.24

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,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.buildHubHealth = buildHubHealth;
3
4
  exports.registerHealthRoutes = registerHealthRoutes;
4
5
  const AGENT_HEALTH_TIMEOUT_MS = 3_000;
5
6
  function nowIso() {
@@ -8,8 +9,9 @@ function nowIso() {
8
9
  async function buildHubHealth(deps, proc = process) {
9
10
  const nodes = await deps.agentRegistry.listNodes();
10
11
  const remote = nodes.filter((n) => !n.isHub);
11
- const online = remote.filter((n) => n.isOnline !== false).length;
12
+ const offlineNodes = remote.filter((n) => n.isOnline === false);
12
13
  const total = remote.length;
14
+ const online = total - offlineNodes.length;
13
15
  const memUsage = proc.memoryUsage();
14
16
  const totalMem = memUsage.heapTotal + memUsage.external + memUsage.arrayBuffers;
15
17
  const memoryPercent = totalMem > 0 ? Math.round((memUsage.heapUsed / totalMem) * 100) : 0;
@@ -19,7 +21,14 @@ async function buildHubHealth(deps, proc = process) {
19
21
  version: deps.hubVersion,
20
22
  uptimeSeconds: Math.round(proc.uptime()),
21
23
  pid: proc.pid,
22
- agents: { total, online, offline: total - online },
24
+ agents: {
25
+ total,
26
+ online,
27
+ offline: offlineNodes.length,
28
+ // Named so health consumers (admin-ui connection banner) can say
29
+ // WHICH node is degraded, not only how many.
30
+ offlineIds: offlineNodes.map((n) => n.info.id),
31
+ },
23
32
  cpuPercent: 0,
24
33
  memoryPercent,
25
34
  checkedAt: nowIso(),
@@ -55,6 +55,21 @@ function requireDeviceScoped(registry, capName) {
55
55
  }
56
56
  const native = registry.getNativeProvider(capName, deviceId);
57
57
  if (!native) {
58
+ // No native provider. For `getStatus`, the device may still have a
59
+ // SYNTHESIZED status from device-links (battery/consumables wired
60
+ // from sibling sensors). Consult the resolver with a null base
61
+ // (the synthesize path) before failing — it returns null when the
62
+ // device has no links for this cap, preserving today's error.
63
+ if (prop === 'getStatus') {
64
+ const deviceManager = registry.getSingleton('device-manager');
65
+ const synthesized = await deviceManager?.resolveLinkedStatus?.({
66
+ deviceId,
67
+ cap: String(capName),
68
+ baseStatus: null,
69
+ });
70
+ if (synthesized != null)
71
+ return synthesized;
72
+ }
58
73
  throw new server_1.TRPCError({
59
74
  code: 'PRECONDITION_FAILED',
60
75
  message: `Capability "${String(capName)}" not registered for device ${deviceId}`,
@@ -16,6 +16,7 @@
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
17
  exports.createNodeCapAuthority = createNodeCapAuthority;
18
18
  exports.createInProcessProviderLookup = createInProcessProviderLookup;
19
+ exports.createHubResidentProviderLookup = createHubResidentProviderLookup;
19
20
  /**
20
21
  * Build a NodeCapAuthority backed by a HubNodeRegistry.
21
22
  *
@@ -107,16 +108,42 @@ function createInProcessProviderLookup(capabilityService) {
107
108
  capabilityService.getSingleton(capName);
108
109
  if (provider === null || provider === undefined)
109
110
  return null;
110
- const ref = {
111
- invoke: (method, args) => {
112
- const fn = provider[method];
113
- if (typeof fn !== 'function') {
114
- return Promise.reject(new Error(`method "${method}" not found on cap "${capName}"`));
115
- }
116
- const result = fn.call(provider, args);
117
- return Promise.resolve(result);
118
- },
119
- };
120
- return ref;
111
+ return buildProviderRef(capName, provider);
112
+ };
113
+ }
114
+ /**
115
+ * NODE-ACCURATE variant of {@link createInProcessProviderLookup}: resolves a
116
+ * provider ONLY when one is physically resident on the hub node
117
+ * (`getSingletonForNode(cap, 'hub')`) — no fallback to the cluster-elected
118
+ * ACTIVE singleton.
119
+ *
120
+ * Rationale: `getSingleton(cap)` returns the elected provider OBJECT, which
121
+ * for a remote election is a Moleculer-proxy registered in the hub registry.
122
+ * That fallback is correct for UNPINNED singleton dispatch (election
123
+ * adherence) but must never satisfy an explicit `nodePin('hub')` — the
124
+ * "hub-in-process" route would silently execute on another node (the shm
125
+ * frame-plane cross-node decoder-session storm: a `decoder` call pinned to
126
+ * the hub landed on a remote agent whenever the hub's decoder runner was
127
+ * circuit-broken). Wired into `CapRouteResolver.hubResidentProviders`.
128
+ */
129
+ function createHubResidentProviderLookup(capabilityService) {
130
+ return (capName) => {
131
+ const provider = capabilityService.getSingletonForNode?.(capName, 'hub');
132
+ if (provider === null || provider === undefined)
133
+ return null;
134
+ return buildProviderRef(capName, provider);
135
+ };
136
+ }
137
+ /** Shared cast-free InProcessProviderRef builder over a provider object. */
138
+ function buildProviderRef(capName, provider) {
139
+ return {
140
+ invoke: (method, args) => {
141
+ const fn = provider[method];
142
+ if (typeof fn !== 'function') {
143
+ return Promise.reject(new Error(`method "${method}" not found on cap "${capName}"`));
144
+ }
145
+ const result = fn.call(provider, args);
146
+ return Promise.resolve(result);
147
+ },
121
148
  };
122
149
  }
@@ -2,12 +2,12 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.MoleculerService = void 0;
4
4
  exports.buildChildUdsManifest = buildChildUdsManifest;
5
+ const node_crypto_1 = require("node:crypto");
5
6
  const system_1 = require("@camstack/system");
7
+ const types_1 = require("@camstack/types");
6
8
  const cap_router_runtime_js_1 = require("../../api/trpc/cap-router-runtime.js");
7
9
  const cap_call_fn_js_1 = require("./cap-call-fn.js");
8
10
  const cap_route_authority_js_1 = require("./cap-route-authority.js");
9
- const types_1 = require("@camstack/types");
10
- const node_crypto_1 = require("node:crypto");
11
11
  class MoleculerService {
12
12
  eventBus;
13
13
  config;
@@ -411,6 +411,12 @@ class MoleculerService {
411
411
  null,
412
412
  }),
413
413
  inProcessProviders: (0, cap_route_authority_js_1.createInProcessProviderLookup)(this.capabilityService),
414
+ // Node-accurate lookup for EXPLICITLY hub-pinned calls: unlike the
415
+ // lookup above (which falls back to the cluster-elected ACTIVE
416
+ // singleton — possibly a remote proxy), this resolves only providers
417
+ // physically resident on the hub, so a `nodePin('hub')` fails precisely
418
+ // instead of silently executing on another node.
419
+ hubResidentProviders: (0, cap_route_authority_js_1.createHubResidentProviderLookup)(this.capabilityService),
414
420
  // Per-cap-method RPC timeout override (e.g. model-convert.convert, which
415
421
  // runs for minutes) — read from the cap definition's declared timeoutMs.
416
422
  capTimeoutMs: (capName, method) => this.capabilityService.getRegistry()?.getDefinition(capName)?.methods?.[method]?.timeoutMs,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/server",
3
- "version": "1.1.22",
3
+ "version": "1.1.24",
4
4
  "private": false,
5
5
  "files": [
6
6
  "dist",