@camstack/server 0.1.8 → 0.2.0

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.
Files changed (125) hide show
  1. package/package.json +9 -7
  2. package/src/__tests__/addon-install-e2e.test.ts +0 -1
  3. package/src/__tests__/addon-pages-e2e.test.ts +40 -18
  4. package/src/__tests__/addon-settings-router.spec.ts +6 -1
  5. package/src/__tests__/addon-upload.spec.ts +91 -29
  6. package/src/__tests__/agent-registry.spec.ts +26 -9
  7. package/src/__tests__/agent-status-page.spec.ts +1 -3
  8. package/src/__tests__/auth-session-cookie.test.ts +28 -1
  9. package/src/__tests__/bulk-update-coordinator.spec.ts +48 -31
  10. package/src/__tests__/cap-ownership-authority.spec.ts +39 -8
  11. package/src/__tests__/cap-providers/cap-providers-location-import.spec.ts +24 -4
  12. package/src/__tests__/cap-providers/cap-usage-graph.spec.ts +17 -3
  13. package/src/__tests__/cap-providers/compute-topology-categories.spec.ts +57 -11
  14. package/src/__tests__/cap-providers/integrations-delete-cascade.spec.ts +64 -15
  15. package/src/__tests__/cap-providers-bulk-update.spec.ts +27 -7
  16. package/src/__tests__/cap-route-adapter.spec.ts +28 -15
  17. package/src/__tests__/cap-routers/_meta.spec.ts +6 -7
  18. package/src/__tests__/cap-routers/addon-settings.router.spec.ts +19 -10
  19. package/src/__tests__/cap-routers/broker-routing.router.spec.ts +14 -6
  20. package/src/__tests__/cap-routers/cap-route-error-formatter.spec.ts +3 -1
  21. package/src/__tests__/cap-routers/capabilities-node.spec.ts +18 -5
  22. package/src/__tests__/cap-routers/device-link-overlay.spec.ts +11 -6
  23. package/src/__tests__/cap-routers/device-manager-aggregate.router.spec.ts +72 -20
  24. package/src/__tests__/cap-routers/harness.ts +11 -7
  25. package/src/__tests__/cap-routers/metrics-provider.router.spec.ts +17 -3
  26. package/src/__tests__/cap-routers/null-provider-guard.spec.ts +5 -7
  27. package/src/__tests__/cap-routers/pipeline-executor.router.spec.ts +35 -11
  28. package/src/__tests__/cap-routers/settings-store.router.spec.ts +59 -15
  29. package/src/__tests__/capability-e2e.test.ts +9 -11
  30. package/src/__tests__/cli-e2e.test.ts +80 -59
  31. package/src/__tests__/core-cap-bridge.spec.ts +3 -1
  32. package/src/__tests__/dev-bootstrap-shm-ring.spec.ts +12 -2
  33. package/src/__tests__/device-settings-contribution-dispatch.spec.ts +61 -30
  34. package/src/__tests__/embedded-deps-e2e.test.ts +35 -19
  35. package/src/__tests__/event-bus-proxy-router.spec.ts +3 -0
  36. package/src/__tests__/framework-allowlist.spec.ts +5 -4
  37. package/src/__tests__/https-e2e.test.ts +12 -6
  38. package/src/__tests__/lifecycle-e2e.test.ts +60 -11
  39. package/src/__tests__/live-events-subscription.spec.ts +17 -18
  40. package/src/__tests__/moleculer/uds-readiness.spec.ts +11 -4
  41. package/src/__tests__/moleculer/uds-topology.spec.ts +39 -11
  42. package/src/__tests__/moleculer/uds-unowned-call.spec.ts +71 -17
  43. package/src/__tests__/moleculer-register-node-idempotency.spec.ts +16 -7
  44. package/src/__tests__/native-cap-route.spec.ts +42 -19
  45. package/src/__tests__/oauth2-account-linking.spec.ts +63 -17
  46. package/src/__tests__/singleton-contention.test.ts +23 -11
  47. package/src/__tests__/streaming-diagnostic.test.ts +156 -53
  48. package/src/__tests__/streaming-scale.test.ts +69 -35
  49. package/src/__tests__/uds-addon-call-wiring.spec.ts +6 -1
  50. package/src/agent-status-page.ts +4 -3
  51. package/src/api/__tests__/addons-custom.spec.ts +22 -8
  52. package/src/api/__tests__/capabilities.router.test.ts +18 -9
  53. package/src/api/addon-upload.ts +46 -15
  54. package/src/api/addons-custom.router.ts +7 -6
  55. package/src/api/auth-whoami.ts +3 -1
  56. package/src/api/bridge-addons.router.ts +3 -1
  57. package/src/api/capabilities.router.ts +117 -78
  58. package/src/api/core/__tests__/auth-router-totp.spec.ts +57 -16
  59. package/src/api/core/addon-settings.router.ts +4 -1
  60. package/src/api/core/agents.router.ts +52 -53
  61. package/src/api/core/auth.router.ts +55 -36
  62. package/src/api/core/bulk-update-coordinator.ts +25 -22
  63. package/src/api/core/cap-providers.ts +346 -202
  64. package/src/api/core/capabilities.router.ts +30 -23
  65. package/src/api/core/hwaccel.router.ts +37 -10
  66. package/src/api/core/live-events.router.ts +16 -9
  67. package/src/api/core/logs.router.ts +54 -25
  68. package/src/api/core/notifications.router.ts +2 -1
  69. package/src/api/core/repl.router.ts +1 -3
  70. package/src/api/core/settings-backend.router.ts +68 -70
  71. package/src/api/core/system-events.router.ts +41 -32
  72. package/src/api/health/health.routes.ts +7 -13
  73. package/src/api/oauth2/__tests__/oauth2-routes.spec.ts +12 -2
  74. package/src/api/oauth2/consent-page.ts +4 -3
  75. package/src/api/oauth2/oauth2-routes.ts +41 -12
  76. package/src/api/trpc/__tests__/scope-access-device.spec.ts +68 -23
  77. package/src/api/trpc/__tests__/scope-access.spec.ts +8 -13
  78. package/src/api/trpc/__tests__/webrtc-session-ua-enrich.spec.ts +10 -2
  79. package/src/api/trpc/cap-mount-helpers.ts +64 -55
  80. package/src/api/trpc/cap-route-error-formatter.ts +17 -9
  81. package/src/api/trpc/core-cap-bridge.ts +3 -1
  82. package/src/api/trpc/generated-cap-mounts.ts +593 -351
  83. package/src/api/trpc/generated-cap-routers.ts +3680 -579
  84. package/src/api/trpc/scope-access.ts +7 -7
  85. package/src/api/trpc/trpc.context.ts +7 -4
  86. package/src/api/trpc/trpc.middleware.ts +4 -2
  87. package/src/api/trpc/trpc.router.ts +79 -46
  88. package/src/auth/session-cookie.ts +10 -0
  89. package/src/boot/__tests__/integration-id-backfill.spec.ts +21 -6
  90. package/src/boot/boot-config.ts +103 -122
  91. package/src/boot/post-boot.service.ts +5 -3
  92. package/src/core/addon/__tests__/addon-registry-capability.test.ts +12 -3
  93. package/src/core/addon/addon-call-gateway.ts +20 -6
  94. package/src/core/addon/addon-package.service.ts +183 -89
  95. package/src/core/addon/addon-registry.service.ts +1163 -1305
  96. package/src/core/addon/addon-search.service.ts +2 -1
  97. package/src/core/addon/addon-settings-provider.ts +27 -7
  98. package/src/core/addon-bridge/addon-bridge.service.ts +11 -6
  99. package/src/core/addon-pages/addon-pages.service.ts +3 -1
  100. package/src/core/addon-widgets/addon-widgets.service.ts +5 -2
  101. package/src/core/agent/agent-registry.service.ts +60 -38
  102. package/src/core/auth/auth.service.spec.ts +6 -8
  103. package/src/core/config/config.service.spec.ts +1 -1
  104. package/src/core/events/event-bus.service.spec.ts +44 -21
  105. package/src/core/events/event-bus.service.ts +5 -1
  106. package/src/core/feature/feature.service.spec.ts +4 -1
  107. package/src/core/lifecycle/lifecycle-state-machine.spec.ts +8 -10
  108. package/src/core/logging/logging.service.spec.ts +61 -21
  109. package/src/core/logging/logging.service.ts +12 -3
  110. package/src/core/moleculer/cap-call-fn.spec.ts +17 -10
  111. package/src/core/moleculer/cap-call-fn.ts +5 -1
  112. package/src/core/moleculer/cap-route-authority.ts +18 -6
  113. package/src/core/moleculer/moleculer.service.ts +120 -32
  114. package/src/core/network/network-quality.service.spec.ts +6 -1
  115. package/src/core/notification/notification-wrapper.service.ts +1 -3
  116. package/src/core/notification/toast-wrapper.service.ts +1 -5
  117. package/src/core/repl/repl-engine.service.spec.ts +66 -39
  118. package/src/core/repl/repl-engine.service.ts +11 -12
  119. package/src/core/storage/storage-location-manager.spec.ts +12 -3
  120. package/src/core/streaming/stream-probe.service.ts +22 -13
  121. package/src/core/topology/topology-emitter.service.ts +5 -1
  122. package/src/launcher.ts +14 -9
  123. package/src/main.ts +602 -531
  124. package/src/manual-boot.ts +133 -154
  125. package/tsconfig.json +20 -8
@@ -43,7 +43,12 @@ export class LoggingService extends LogManager {
43
43
  setDeviceNames(entries: ReadonlyArray<{ id: number; name: string }>): void {
44
44
  this.deviceNames.clear()
45
45
  for (const { id, name } of entries) {
46
- if (typeof id === 'number' && Number.isFinite(id) && typeof name === 'string' && name.length > 0) {
46
+ if (
47
+ typeof id === 'number' &&
48
+ Number.isFinite(id) &&
49
+ typeof name === 'string' &&
50
+ name.length > 0
51
+ ) {
47
52
  this.deviceNames.set(id, name)
48
53
  }
49
54
  }
@@ -73,7 +78,11 @@ export class LoggingService extends LogManager {
73
78
  if (data && typeof data.deviceId === 'number') {
74
79
  this.upsertDeviceName(data.deviceId, data.name)
75
80
  selfLogger.info('device-name cache upserted', {
76
- meta: { deviceId: data.deviceId, name: data.name ?? null, cacheSize: this.deviceNames.size },
81
+ meta: {
82
+ deviceId: data.deviceId,
83
+ name: data.name ?? null,
84
+ cacheSize: this.deviceNames.size,
85
+ },
77
86
  })
78
87
  }
79
88
  })
@@ -117,7 +126,7 @@ export class LoggingService extends LogManager {
117
126
  const nodeId = entry.nodeId
118
127
  const agentId = nodeId?.includes('/') ? nodeId.split('/')[0]! : nodeId
119
128
  const mergedTags: LogTags = {
120
- ...(entry.tags ?? {}),
129
+ ...entry.tags,
121
130
  addonId: entry.addonId,
122
131
  ...(nodeId !== undefined ? { nodeId } : {}),
123
132
  ...(agentId !== undefined ? { agentId } : {}),
@@ -1,10 +1,6 @@
1
1
  import { describe, it, expect, vi } from 'vitest'
2
2
  import type { CapRoute, CapCallInput } from '@camstack/kernel'
3
- import {
4
- buildCapCallFn,
5
- type CapCallFnLocalChild,
6
- type CapCallFnResolver,
7
- } from './cap-call-fn.js'
3
+ import { buildCapCallFn, type CapCallFnLocalChild, type CapCallFnResolver } from './cap-call-fn.js'
8
4
 
9
5
  /**
10
6
  * buildCapCallFn — the per-(cap,node) dispatcher behind every CapabilityRegistry
@@ -84,7 +80,10 @@ describe('buildCapCallFn', () => {
84
80
 
85
81
  expect(result).toEqual({ from: 'uds' })
86
82
  expect(child.callCapOnChildCalls).toEqual([
87
- { childId: 'benchmark', input: { capName: 'cap-x', method: 'listPages', args: { deviceId: 7 }, deviceId: 7 } },
83
+ {
84
+ childId: 'benchmark',
85
+ input: { capName: 'cap-x', method: 'listPages', args: { deviceId: 7 }, deviceId: 7 },
86
+ },
88
87
  ])
89
88
  expect(resolver.resolveCalls).toEqual([]) // resolver never consulted
90
89
  expect(legacy).not.toHaveBeenCalled()
@@ -125,8 +124,12 @@ describe('buildCapCallFn', () => {
125
124
  const result = await fn('listPages', { deviceId: 3 })
126
125
 
127
126
  expect(result).toEqual({ from: 'resolver' })
128
- expect(resolver.resolveCalls).toEqual([{ capName: 'cap-x', nodeId: 'dev-agent-0', deviceId: 3 }])
129
- expect(resolver.dispatchCalls).toEqual([{ route: REMOTE_ROUTE, method: 'listPages', args: { deviceId: 3 } }])
127
+ expect(resolver.resolveCalls).toEqual([
128
+ { capName: 'cap-x', nodeId: 'dev-agent-0', deviceId: 3 },
129
+ ])
130
+ expect(resolver.dispatchCalls).toEqual([
131
+ { route: REMOTE_ROUTE, method: 'listPages', args: { deviceId: 3 } },
132
+ ])
130
133
  expect(legacy).not.toHaveBeenCalled()
131
134
  })
132
135
 
@@ -144,11 +147,15 @@ describe('buildCapCallFn', () => {
144
147
 
145
148
  await fn('listPages', undefined, 'dev-agent-1')
146
149
 
147
- expect(resolver.resolveCalls).toEqual([{ capName: 'cap-x', nodeId: 'dev-agent-1', deviceId: undefined }])
150
+ expect(resolver.resolveCalls).toEqual([
151
+ { capName: 'cap-x', nodeId: 'dev-agent-1', deviceId: undefined },
152
+ ])
148
153
  })
149
154
 
150
155
  it('resolver not yet built → falls back to the legacy broker call', async () => {
151
- const legacy = vi.fn<(m: string, p: unknown, n: string) => Promise<unknown>>(async () => ({ from: 'legacy' }))
156
+ const legacy = vi.fn<(m: string, p: unknown, n: string) => Promise<unknown>>(async () => ({
157
+ from: 'legacy',
158
+ }))
152
159
  const fn = buildCapCallFn({
153
160
  capName: 'cap-x',
154
161
  nodeId: 'dev-agent-0',
@@ -44,7 +44,11 @@ export interface CapCallFnDeps {
44
44
  /** Live getter for the resolver (`null` before `onModuleInit` builds it). */
45
45
  readonly getResolver: () => CapCallFnResolver | null
46
46
  /** Legacy Moleculer call — used ONLY in the pre-init window (no resolver yet). */
47
- readonly legacyBrokerCall: (method: string, params: unknown, targetNodeId: string) => Promise<unknown>
47
+ readonly legacyBrokerCall: (
48
+ method: string,
49
+ params: unknown,
50
+ targetNodeId: string,
51
+ ) => Promise<unknown>
48
52
  /** Optional diagnostic hook fired the first time this cap routes over UDS. */
49
53
  readonly onUdsRoute?: (capName: string) => void
50
54
  }
@@ -26,14 +26,21 @@ import type { InProcessProviderRef } from '@camstack/kernel'
26
26
  * is required. HubNodeRegistry satisfies this interface structurally.
27
27
  */
28
28
  export interface NodeRegistryLike {
29
- getNodeManifest(nodeId: string): readonly { readonly addonId: string; readonly capabilities: readonly string[] }[] | undefined
29
+ getNodeManifest(
30
+ nodeId: string,
31
+ ): readonly { readonly addonId: string; readonly capabilities: readonly string[] }[] | undefined
30
32
  listNodeIds(): readonly string[]
31
33
  /**
32
34
  * Optional: returns flat (nodeId, addonId, capName, deviceId) native-cap tuples.
33
35
  * When provided, `nodeKnowsCap` and `isNativeCap` also consult native caps so
34
36
  * device-scoped native caps (ptz, motion-zones, …) are visible to the resolver.
35
37
  */
36
- listNativeCapEntries?(): readonly { readonly nodeId: string; readonly addonId: string; readonly capName: string; readonly deviceId: number }[]
38
+ listNativeCapEntries?(): readonly {
39
+ readonly nodeId: string
40
+ readonly addonId: string
41
+ readonly capName: string
42
+ readonly deviceId: number
43
+ }[]
37
44
  }
38
45
 
39
46
  // ---------------------------------------------------------------------------
@@ -87,7 +94,10 @@ export function createNodeCapAuthority(
87
94
  nodeKnowsCap(nodeId: string, capName: string): boolean {
88
95
  // Check system (manifest) caps first
89
96
  const manifest = nodeRegistry.getNodeManifest(nodeId)
90
- if (manifest !== undefined && manifest.some((addon) => addon.capabilities.includes(capName))) {
97
+ if (
98
+ manifest !== undefined &&
99
+ manifest.some((addon) => addon.capabilities.includes(capName))
100
+ ) {
91
101
  return true
92
102
  }
93
103
  // Also check device-scoped native caps — these are NOT in the addon manifest
@@ -139,7 +149,9 @@ export function createNodeCapAuthority(
139
149
  isNativeCap(nodeId: string, capName: string, deviceId?: number): boolean {
140
150
  const nativeEntries = nodeRegistry.listNativeCapEntries?.() ?? []
141
151
  if (deviceId !== undefined) {
142
- return nativeEntries.some((n) => n.nodeId === nodeId && n.capName === capName && n.deviceId === deviceId)
152
+ return nativeEntries.some(
153
+ (n) => n.nodeId === nodeId && n.capName === capName && n.deviceId === deviceId,
154
+ )
143
155
  }
144
156
  return nativeEntries.some((n) => n.nodeId === nodeId && n.capName === capName)
145
157
  },
@@ -163,8 +175,8 @@ export function createInProcessProviderLookup(
163
175
  ): InProcessProviderLookup {
164
176
  return (capName: string): InProcessProviderRef | null => {
165
177
  const provider =
166
- capabilityService.getSingletonForNode?.(capName, 'hub')
167
- ?? capabilityService.getSingleton(capName)
178
+ capabilityService.getSingletonForNode?.(capName, 'hub') ??
179
+ capabilityService.getSingleton(capName)
168
180
  if (provider === null || provider === undefined) return null
169
181
 
170
182
  const ref: InProcessProviderRef = {
@@ -16,8 +16,37 @@ interface BrokerLike {
16
16
  call(action: string, params?: unknown, opts?: unknown): Promise<unknown>
17
17
  waitForServices(services: string[], timeout?: number): Promise<unknown>
18
18
  }
19
- import { createBroker, createHubService, createProcessService, isInfraCapability, registerEventBusService, createReadinessServiceForRegistry, createStreamProbeBrokerService, createHwAccelService, createKernelHwAccel, HubNodeRegistry, serializeTypedArrays, callWithServiceDiscovery, hashClusterSecret, LocalChildRegistry, createLocalTransport, localEndpointPath, CapRouteResolver, CapRouteError, capActionName, udsChildLogToWorkerEntry, createUdsEventBridge, createParentUnownedCallHandler } from '@camstack/kernel'
20
- import type { HubServiceDeps, CallFn, RegisterNodeParams, RegisteredAddonManifest, ChildCapDescriptor } from '@camstack/kernel'
19
+ import {
20
+ createBroker,
21
+ createHubService,
22
+ createProcessService,
23
+ isInfraCapability,
24
+ registerEventBusService,
25
+ createReadinessServiceForRegistry,
26
+ createStreamProbeBrokerService,
27
+ createHwAccelService,
28
+ createKernelHwAccel,
29
+ HubNodeRegistry,
30
+ serializeTypedArrays,
31
+ callWithServiceDiscovery,
32
+ hashClusterSecret,
33
+ LocalChildRegistry,
34
+ createLocalTransport,
35
+ localEndpointPath,
36
+ CapRouteResolver,
37
+ CapRouteError,
38
+ capActionName,
39
+ udsChildLogToWorkerEntry,
40
+ createUdsEventBridge,
41
+ createParentUnownedCallHandler,
42
+ } from '@camstack/kernel'
43
+ import type {
44
+ HubServiceDeps,
45
+ CallFn,
46
+ RegisterNodeParams,
47
+ RegisteredAddonManifest,
48
+ ChildCapDescriptor,
49
+ } from '@camstack/kernel'
21
50
  import { buildCapCallFn } from './cap-call-fn.js'
22
51
  import { createNodeCapAuthority, createInProcessProviderLookup } from './cap-route-authority.js'
23
52
  import { EventCategory, expandCapMethods, ReadinessRegistry, emitReadiness } from '@camstack/types'
@@ -55,7 +84,9 @@ interface AppliedCapEntry {
55
84
  export class MoleculerService {
56
85
  readonly broker: ServiceBroker
57
86
  /** Narrow-typed view of `this.broker` — see `BrokerLike` doc above. */
58
- private get brokerSafe(): BrokerLike { return this.broker as unknown as BrokerLike }
87
+ private get brokerSafe(): BrokerLike {
88
+ return this.broker as unknown as BrokerLike
89
+ }
59
90
  private readonly logger: ReturnType<LoggingService['createLogger']>
60
91
  /**
61
92
  * D3 authority: union of every node's manifest delivered via
@@ -148,7 +179,8 @@ export class MoleculerService {
148
179
  // moleculer→eventemitter2 whose types are unresolvable at this
149
180
  // boundary, so the inference falls to `error` and trips
150
181
  // `no-unsafe-assignment`. Going via `unknown` documents the boundary.
151
- this.clusterSecret = process.env['CAMSTACK_CLUSTER_SECRET'] ?? this.config.get<string>('cluster.secret')
182
+ this.clusterSecret =
183
+ process.env['CAMSTACK_CLUSTER_SECRET'] ?? this.config.get<string>('cluster.secret')
152
184
  const broker = createBroker({
153
185
  nodeID: 'hub',
154
186
  mode: 'hub',
@@ -226,7 +258,9 @@ export class MoleculerService {
226
258
  onUnregisterNode: (nodeId) => {
227
259
  this.removeNodeFromRegistry(nodeId)
228
260
  },
229
- expectedClusterSecretHash: this.clusterSecret ? hashClusterSecret(this.clusterSecret) : undefined,
261
+ expectedClusterSecretHash: this.clusterSecret
262
+ ? hashClusterSecret(this.clusterSecret)
263
+ : undefined,
230
264
  }
231
265
 
232
266
  const hubService: unknown = createHubService(hubDeps)
@@ -265,20 +299,41 @@ export class MoleculerService {
265
299
  // UDS before any broker fallback. Getter: `this.localChildRegistry` is
266
300
  // assigned later in this method, after the handler is constructed.
267
301
  getLocalDispatcher: () => this.localChildRegistry,
302
+ // Device-native signal for the registration-race recovery: a forked
303
+ // child's device-native cap (e.g. `switch` on an export target) can be
304
+ // briefly absent from the LocalChildRegistry just after a respawn. The
305
+ // kernel layer has no cap registry, so feed it the `deviceNative` flag
306
+ // from the cap definition. When true + binding-not-yet-registered, the
307
+ // handler retries the hub-local route then throws a precise error rather
308
+ // than the unroutable broker fallback (`switch.switch.getStatus`).
309
+ isDeviceNativeCap: (capName) =>
310
+ this.capabilityService.getRegistry()?.getDefinition(capName)?.deviceNative === true,
268
311
  logger: {
269
- warn: (msg, meta) => logger.warn(msg, meta !== null && meta !== undefined ? { meta: meta as Record<string, unknown> } : undefined),
312
+ warn: (msg, meta) =>
313
+ logger.warn(
314
+ msg,
315
+ meta !== null && meta !== undefined
316
+ ? { meta: meta as Record<string, unknown> }
317
+ : undefined,
318
+ ),
270
319
  },
271
320
  })
272
321
  const registry = new LocalChildRegistry({
273
322
  server: createLocalTransport().createServer(nodeId),
274
323
  onUnownedCall,
275
324
  logger: {
276
- info: (msg, meta) => logger.info(msg, meta !== null && meta !== undefined ? { meta: meta as Record<string, unknown> } : undefined),
325
+ info: (msg, meta) =>
326
+ logger.info(
327
+ msg,
328
+ meta !== null && meta !== undefined
329
+ ? { meta: meta as Record<string, unknown> }
330
+ : undefined,
331
+ ),
277
332
  },
278
333
  // Hand the UDS-routing layer a view into the operator's
279
334
  // active-singleton preference. Without this, when two local
280
- // children own the same singleton cap (today: `webrtc-session`
281
- // → `stream-broker` + `addon-webrtc-native`), routing returns
335
+ // children own the same singleton cap (e.g. two `webrtc-session`
336
+ // providers), routing returns
282
337
  // the first-registered child by insertion order — silently
283
338
  // bypassing `setActiveSingleton`. The closure reads the live
284
339
  // registry on every call so a runtime swap takes effect
@@ -327,10 +382,18 @@ export class MoleculerService {
327
382
  parentUdsPath = localEndpointPath(nodeId)
328
383
  logger.info('UDS child registry listening', { meta: { path: parentUdsPath } })
329
384
  } catch (err) {
330
- logger.warn('UDS child registry failed to start; children stay broker-only', { meta: { err: err instanceof Error ? err.message : String(err) } })
385
+ logger.warn('UDS child registry failed to start; children stay broker-only', {
386
+ meta: { err: err instanceof Error ? err.message : String(err) },
387
+ })
331
388
  }
332
389
 
333
- const processService: unknown = createProcessService(this.brokerSafe.nodeID, dataDir, undefined, undefined, parentUdsPath)
390
+ const processService: unknown = createProcessService(
391
+ this.brokerSafe.nodeID,
392
+ dataDir,
393
+ undefined,
394
+ undefined,
395
+ parentUdsPath,
396
+ )
334
397
  this.brokerSafe.createService(processService)
335
398
 
336
399
  // $addonHost — REMOVED (Sprint 6). Three-level settings are now
@@ -363,10 +426,12 @@ export class MoleculerService {
363
426
  // workers). Keeps ffprobe + HTTP-reachability as a single
364
427
  // hub-side implementation; see `createStreamProbeBrokerService`
365
428
  // for the shape.
366
- this.brokerSafe.createService(createStreamProbeBrokerService({
367
- probe: (url, options) => this.streamProbe.probe(url, options),
368
- probeField: (key, value) => this.streamProbe.probeField(key, value),
369
- }))
429
+ this.brokerSafe.createService(
430
+ createStreamProbeBrokerService({
431
+ probe: (url, options) => this.streamProbe.probe(url, options),
432
+ probeField: (key, value) => this.streamProbe.probeField(key, value),
433
+ }),
434
+ )
370
435
 
371
436
  // Register `$hwaccel` on hub — every node in the cluster does the
372
437
  // same so `broker.call('$hwaccel.resolve', params, { nodeID })`
@@ -390,7 +455,8 @@ export class MoleculerService {
390
455
  hubLocalRegistry: this.localChildRegistry,
391
456
  nodeAuthority: createNodeCapAuthority(this.nodeRegistry, {
392
457
  resolveSingleton: (capName, nodeId) =>
393
- this.capabilityService.getRegistry()?.resolveSingletonAddonIdForNode(capName, nodeId) ?? null,
458
+ this.capabilityService.getRegistry()?.resolveSingletonAddonIdForNode(capName, nodeId) ??
459
+ null,
394
460
  }),
395
461
  inProcessProviders: createInProcessProviderLookup(this.capabilityService),
396
462
  })
@@ -503,12 +569,16 @@ export class MoleculerService {
503
569
  // the legacy callFn store. The deviceId hint is a number extracted from the method args.
504
570
  const rawDeviceId: unknown =
505
571
  params !== null && typeof params === 'object' ? Reflect.get(params, 'deviceId') : undefined
506
- const routeDeviceId: number | undefined = typeof rawDeviceId === 'number' ? rawDeviceId : undefined
572
+ const routeDeviceId: number | undefined =
573
+ typeof rawDeviceId === 'number' ? rawDeviceId : undefined
507
574
  try {
508
575
  const route = resolver.resolveCapRoute(capabilityName, { nodeId, deviceId: routeDeviceId })
509
576
  return await resolver.dispatch(route, methodName, params)
510
577
  } catch (err) {
511
- if (err instanceof CapRouteError && (err.reason === 'no-provider' || err.reason === 'node-offline')) {
578
+ if (
579
+ err instanceof CapRouteError &&
580
+ (err.reason === 'no-provider' || err.reason === 'node-offline')
581
+ ) {
512
582
  // Resolver couldn't find the cap — try the legacy callFn store as a
513
583
  // fallback. This covers caps registered in nodeCallFns (e.g. agent
514
584
  // nodes that registered before the resolver's snapshot was built or
@@ -534,7 +604,8 @@ export class MoleculerService {
534
604
  const provider = registry?.getSingleton<Record<string, unknown>>(capabilityName) ?? null
535
605
  if (provider !== null) {
536
606
  const fn = provider[methodName]
537
- if (typeof fn !== 'function') throw new Error(`Method "${methodName}" not found on "${capabilityName}"`)
607
+ if (typeof fn !== 'function')
608
+ throw new Error(`Method "${methodName}" not found on "${capabilityName}"`)
538
609
  return fn.call(provider, params)
539
610
  }
540
611
  }
@@ -547,7 +618,9 @@ export class MoleculerService {
547
618
  throw new CapRouteError(capabilityName, methodName, {
548
619
  reason: 'no-provider',
549
620
  nodeId,
550
- rejected: [{ kind: 'hub-in-process', why: 'CapRouteResolver not initialised (pre-onModuleInit)' }],
621
+ rejected: [
622
+ { kind: 'hub-in-process', why: 'CapRouteResolver not initialised (pre-onModuleInit)' },
623
+ ],
551
624
  })
552
625
  }
553
626
 
@@ -596,7 +669,10 @@ export class MoleculerService {
596
669
  * already; this method handles only `params.addons` (system caps).
597
670
  * Native-cap wiring into device-manager is done in a later task.
598
671
  */
599
- private applyNodeManifest(params: RegisterNodeParams, previousManifest?: readonly RegisteredAddonManifest[]): void {
672
+ private applyNodeManifest(
673
+ params: RegisterNodeParams,
674
+ previousManifest?: readonly RegisteredAddonManifest[],
675
+ ): void {
600
676
  const { nodeId, addons } = params
601
677
  const hubNodeId = this.brokerSafe.nodeID
602
678
  const isLocalChild = nodeId.startsWith(hubNodeId + '/')
@@ -622,7 +698,9 @@ export class MoleculerService {
622
698
  // what is (or would have been) registered. Keyed `${registryKey}::${capName}`.
623
699
  // The resolved `capDef` is carried through so the register loop never
624
700
  // re-looks it up (and never needs a non-null assertion).
625
- const appliedKeys = (manifest: readonly RegisteredAddonManifest[]): Map<string, AppliedCapEntry> => {
701
+ const appliedKeys = (
702
+ manifest: readonly RegisteredAddonManifest[],
703
+ ): Map<string, AppliedCapEntry> => {
626
704
  const keys = new Map<string, AppliedCapEntry>()
627
705
  for (const addon of manifest) {
628
706
  const registryKey = registryKeyFor(addon.addonId)
@@ -754,7 +832,7 @@ export class MoleculerService {
754
832
  */
755
833
  private removeNodeFromRegistry(nodeId: string): void {
756
834
  const manifest = this.nodeRegistry.getNodeManifest(nodeId)
757
- if (!manifest) return // node never sent a handshake — nothing to do
835
+ if (!manifest) return // node never sent a handshake — nothing to do
758
836
 
759
837
  const hubNodeId = this.brokerSafe.nodeID
760
838
  const isLocalChild = nodeId.startsWith(hubNodeId + '/')
@@ -820,7 +898,6 @@ export class MoleculerService {
820
898
  return undefined
821
899
  }
822
900
 
823
-
824
901
  /**
825
902
  * Returns true when a (nodeId, capabilityName) pair is reachable via the
826
903
  * legacy fallback paths: either a stored callFn in nodeCallFns, or a
@@ -849,7 +926,10 @@ export class MoleculerService {
849
926
  * built unconditionally for hub-local children so `callCapabilityOnNode` can
850
927
  * route the actual method call via the resolver's hub-local-uds branch.
851
928
  */
852
- createCapabilityProxy(capabilityName: string, nodeId: string): Record<string, (params: unknown) => Promise<unknown>> | null {
929
+ createCapabilityProxy(
930
+ capabilityName: string,
931
+ nodeId: string,
932
+ ): Record<string, (params: unknown) => Promise<unknown>> | null {
853
933
  const resolver = this.resolver
854
934
  if (resolver !== null) {
855
935
  // Use the resolver to determine reachability. If it resolves a route, we
@@ -860,7 +940,10 @@ export class MoleculerService {
860
940
  resolver.resolveCapRoute(capabilityName, { nodeId })
861
941
  // Resolver found a route — proxy is reachable.
862
942
  } catch (err) {
863
- if (err instanceof CapRouteError && (err.reason === 'no-provider' || err.reason === 'node-offline')) {
943
+ if (
944
+ err instanceof CapRouteError &&
945
+ (err.reason === 'no-provider' || err.reason === 'node-offline')
946
+ ) {
864
947
  // Check legacy callFn store and hub-local child fallbacks.
865
948
  if (!this.isReachableViaLegacy(nodeId, capabilityName)) return null
866
949
  // Proxy reachable via fallback paths — fall through to build it.
@@ -875,12 +958,15 @@ export class MoleculerService {
875
958
 
876
959
  // Build a dynamic proxy: every property access returns a function that
877
960
  // routes the call through callCapabilityOnNode (which delegates to the resolver).
878
- return new Proxy<Record<string, (params: unknown) => Promise<unknown>>>({}, {
879
- get: (_target, methodName: string) => {
880
- return (params: unknown): Promise<unknown> =>
881
- this.callCapabilityOnNode(nodeId, capabilityName, methodName, params)
961
+ return new Proxy<Record<string, (params: unknown) => Promise<unknown>>>(
962
+ {},
963
+ {
964
+ get: (_target, methodName: string) => {
965
+ return (params: unknown): Promise<unknown> =>
966
+ this.callCapabilityOnNode(nodeId, capabilityName, methodName, params)
967
+ },
882
968
  },
883
- })
969
+ )
884
970
  }
885
971
 
886
972
  /**
@@ -904,7 +990,9 @@ export class MoleculerService {
904
990
  * per-device `getBindings` hot path so `getAllBindings` doesn't flatten the
905
991
  * whole cluster once per device.
906
992
  */
907
- listClusterNativeCapsForDevice(deviceId: number): readonly import('@camstack/kernel').NodeNativeCapEntry[] {
993
+ listClusterNativeCapsForDevice(
994
+ deviceId: number,
995
+ ): readonly import('@camstack/kernel').NodeNativeCapEntry[] {
908
996
  return this.nodeRegistry.listNativeCapEntriesForDevice(deviceId)
909
997
  }
910
998
 
@@ -32,7 +32,12 @@ describe('NetworkQualityService', () => {
32
32
  })
33
33
 
34
34
  it('should track client stats', () => {
35
- service.reportClientStats(1, { rttMs: 50, jitterMs: 5, estimatedBandwidthKbps: 20000, packetLossPercent: 3 })
35
+ service.reportClientStats(1, {
36
+ rttMs: 50,
37
+ jitterMs: 5,
38
+ estimatedBandwidthKbps: 20000,
39
+ packetLossPercent: 3,
40
+ })
36
41
  const stats = service.getDeviceStats(1)
37
42
  expect(stats!.client?.rttMs).toBe(50)
38
43
  expect(stats!.client?.estimatedBandwidthKbps).toBe(20000)
@@ -19,9 +19,7 @@ export class NotificationServiceWrapper {
19
19
 
20
20
  get service(): NotificationService {
21
21
  if (!this._service) {
22
- this._service = new NotificationService(
23
- this.logging.createLogger('notifications'),
24
- )
22
+ this._service = new NotificationService(this.logging.createLogger('notifications'))
25
23
  const registry = this.caps.getRegistry()
26
24
  if (registry) {
27
25
  this._service.setRegistry(registry)
@@ -21,11 +21,7 @@ export class ToastServiceWrapper {
21
21
  this._service.sendToUser(userId, toast)
22
22
  }
23
23
 
24
- subscribe(
25
- connectionId: string,
26
- userId: string,
27
- callback: (toast: Toast) => void,
28
- ): () => void {
24
+ subscribe(connectionId: string, userId: string, callback: (toast: Toast) => void): () => void {
29
25
  return this._service.subscribe(connectionId, userId, callback)
30
26
  }
31
27
  }