@camstack/server 1.0.0 → 1.0.1
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/{src/agent-status-page.ts → dist/agent-status-page.js} +30 -45
- package/dist/api/addon-upload.js +441 -0
- package/dist/api/addons-custom.router.js +91 -0
- package/dist/api/auth-whoami.js +55 -0
- package/dist/api/bridge-addons.router.js +109 -0
- package/dist/api/capabilities.router.js +229 -0
- package/dist/api/core/addon-settings.router.js +117 -0
- package/dist/api/core/agents.router.js +73 -0
- package/dist/api/core/auth.router.js +286 -0
- package/dist/api/core/bulk-update-coordinator.js +229 -0
- package/dist/api/core/cap-providers.js +1124 -0
- package/dist/api/core/capabilities.router.js +138 -0
- package/dist/api/core/collection-preference.js +17 -0
- package/dist/api/core/event-bus-proxy.router.js +45 -0
- package/dist/api/core/hwaccel.router.js +91 -0
- package/dist/api/core/live-events.router.js +61 -0
- package/dist/api/core/logs.router.js +172 -0
- package/dist/api/core/notifications.router.js +67 -0
- package/dist/api/core/repl.router.js +35 -0
- package/dist/api/core/settings-backend.router.js +121 -0
- package/dist/api/core/stream-probe.router.js +58 -0
- package/dist/api/core/system-events.router.js +100 -0
- package/dist/api/health/health.routes.js +68 -0
- package/{src/api/oauth2/consent-page.ts → dist/api/oauth2/consent-page.js} +11 -20
- package/dist/api/oauth2/oauth2-routes.js +219 -0
- package/dist/api/trpc/cap-mount-helpers.js +194 -0
- package/dist/api/trpc/cap-route-error-formatter.js +133 -0
- package/dist/api/trpc/client-ip.js +147 -0
- package/dist/api/trpc/core-cap-bridge.js +115 -0
- package/dist/api/trpc/generated-cap-mounts.js +388 -0
- package/dist/api/trpc/generated-cap-routers.js +7635 -0
- package/dist/api/trpc/scope-access.js +93 -0
- package/dist/api/trpc/trpc.context.js +184 -0
- package/dist/api/trpc/trpc.middleware.js +139 -0
- package/dist/api/trpc/trpc.router.js +188 -0
- package/dist/auth/session-cookie.js +47 -0
- package/dist/boot/boot-config.js +241 -0
- package/dist/boot/integration-id-backfill.js +76 -0
- package/dist/boot/post-boot.service.js +85 -0
- package/dist/core/addon/addon-call-gateway.js +99 -0
- package/dist/core/addon/addon-package.service.js +1560 -0
- package/dist/core/addon/addon-registry.service.js +2739 -0
- package/{src/core/addon/addon-row-manifest.ts → dist/core/addon/addon-row-manifest.js} +5 -5
- package/dist/core/addon/addon-search.service.js +62 -0
- package/dist/core/addon/addon-settings-provider.js +102 -0
- package/dist/core/addon/addon.tokens.js +5 -0
- package/dist/core/addon-bridge/addon-bridge.service.js +145 -0
- package/dist/core/addon-pages/addon-pages.service.js +107 -0
- package/dist/core/addon-widgets/addon-widgets.service.js +120 -0
- package/dist/core/agent/agent-registry.service.js +477 -0
- package/dist/core/auth/auth.service.js +10 -0
- package/dist/core/capability/capability.service.js +58 -0
- package/dist/core/config/config.schema.js +7 -0
- package/dist/core/config/config.service.js +10 -0
- package/dist/core/events/event-bus.service.js +83 -0
- package/dist/core/feature/feature.service.js +10 -0
- package/dist/core/lifecycle/lifecycle-state-machine.js +6 -0
- package/dist/core/logging/log-ring-buffer.js +6 -0
- package/dist/core/logging/logging.service.js +130 -0
- package/dist/core/logging/scoped-logger.js +6 -0
- package/dist/core/moleculer/cap-call-fn.js +50 -0
- package/dist/core/moleculer/cap-route-authority.js +122 -0
- package/dist/core/moleculer/moleculer.service.js +898 -0
- package/dist/core/network/network-quality.service.js +7 -0
- package/dist/core/notification/notification-wrapper.service.js +33 -0
- package/dist/core/notification/toast-wrapper.service.js +25 -0
- package/dist/core/provider/provider.tokens.js +4 -0
- package/dist/core/repl/repl-engine.service.js +140 -0
- package/dist/core/storage/fs-storage-backend.js +6 -0
- package/dist/core/storage/storage-location-manager.js +6 -0
- package/dist/core/storage/storage.service.js +7 -0
- package/dist/core/streaming/stream-probe.service.js +209 -0
- package/dist/core/topology/topology-emitter.service.js +106 -0
- package/dist/launcher.js +325 -0
- package/dist/main.js +1098 -0
- package/dist/manual-boot.js +227 -0
- package/package.json +5 -1
- package/src/__tests__/addon-install-e2e.test.ts +0 -74
- package/src/__tests__/addon-pages-e2e.test.ts +0 -200
- package/src/__tests__/addon-route-session.test.ts +0 -17
- package/src/__tests__/addon-settings-router.spec.ts +0 -67
- package/src/__tests__/addon-upload.spec.ts +0 -475
- package/src/__tests__/agent-registry.spec.ts +0 -179
- package/src/__tests__/agent-status-page.spec.ts +0 -82
- package/src/__tests__/auth-session-cookie.test.ts +0 -48
- package/src/__tests__/bulk-update-coordinator.spec.ts +0 -303
- package/src/__tests__/cap-ownership-authority.spec.ts +0 -431
- package/src/__tests__/cap-providers/cap-providers-location-import.spec.ts +0 -206
- package/src/__tests__/cap-providers/cap-usage-graph.spec.ts +0 -37
- package/src/__tests__/cap-providers/compute-topology-categories.spec.ts +0 -110
- package/src/__tests__/cap-providers/integrations-delete-cascade.spec.ts +0 -292
- package/src/__tests__/cap-providers-bulk-update.spec.ts +0 -408
- package/src/__tests__/cap-route-adapter.spec.ts +0 -302
- package/src/__tests__/cap-routers/_meta.spec.ts +0 -199
- package/src/__tests__/cap-routers/addon-settings.router.spec.ts +0 -115
- package/src/__tests__/cap-routers/broker-routing.router.spec.ts +0 -177
- package/src/__tests__/cap-routers/cap-route-error-formatter.spec.ts +0 -125
- package/src/__tests__/cap-routers/capabilities-node.spec.ts +0 -68
- package/src/__tests__/cap-routers/device-link-overlay.spec.ts +0 -137
- package/src/__tests__/cap-routers/device-manager-aggregate.router.spec.ts +0 -194
- package/src/__tests__/cap-routers/harness.ts +0 -163
- package/src/__tests__/cap-routers/metrics-provider.router.spec.ts +0 -133
- package/src/__tests__/cap-routers/null-provider-guard.spec.ts +0 -64
- package/src/__tests__/cap-routers/pipeline-executor.router.spec.ts +0 -159
- package/src/__tests__/cap-routers/settings-store.router.spec.ts +0 -291
- package/src/__tests__/capability-e2e.test.ts +0 -384
- package/src/__tests__/cli-e2e.test.ts +0 -150
- package/src/__tests__/core-cap-bridge.spec.ts +0 -91
- package/src/__tests__/dev-bootstrap-shm-ring.spec.ts +0 -40
- package/src/__tests__/device-settings-contribution-dispatch.spec.ts +0 -280
- package/src/__tests__/embedded-deps-e2e.test.ts +0 -125
- package/src/__tests__/event-bus-proxy-router.spec.ts +0 -75
- package/src/__tests__/fixtures/mock-analysis-addon-a.ts +0 -37
- package/src/__tests__/fixtures/mock-analysis-addon-b.ts +0 -37
- package/src/__tests__/fixtures/mock-log-addon.ts +0 -37
- package/src/__tests__/fixtures/mock-storage-addon.ts +0 -40
- package/src/__tests__/framework-allowlist.spec.ts +0 -96
- package/src/__tests__/framework-installer-defer-restart.spec.ts +0 -165
- package/src/__tests__/https-e2e.test.ts +0 -124
- package/src/__tests__/lifecycle-e2e.test.ts +0 -189
- package/src/__tests__/live-events-subscription.spec.ts +0 -149
- package/src/__tests__/moleculer/uds-readiness.spec.ts +0 -150
- package/src/__tests__/moleculer/uds-topology.spec.ts +0 -418
- package/src/__tests__/moleculer/uds-unowned-call.spec.ts +0 -383
- package/src/__tests__/moleculer-register-node-idempotency.spec.ts +0 -273
- package/src/__tests__/native-cap-route.spec.ts +0 -427
- package/src/__tests__/oauth2-account-linking.spec.ts +0 -867
- package/src/__tests__/post-boot-restart.spec.ts +0 -161
- package/src/__tests__/singleton-contention.test.ts +0 -499
- package/src/__tests__/streaming-diagnostic.test.ts +0 -615
- package/src/__tests__/streaming-scale.test.ts +0 -314
- package/src/__tests__/uds-addon-call-wiring.spec.ts +0 -242
- package/src/__tests__/uds-log-ingest.spec.ts +0 -183
- package/src/api/__tests__/addons-custom.spec.ts +0 -148
- package/src/api/__tests__/capabilities.router.test.ts +0 -56
- package/src/api/addon-upload.ts +0 -529
- package/src/api/addons-custom.router.ts +0 -101
- package/src/api/auth-whoami.ts +0 -101
- package/src/api/bridge-addons.router.ts +0 -122
- package/src/api/capabilities.router.ts +0 -265
- package/src/api/core/__tests__/auth-router-totp.spec.ts +0 -297
- package/src/api/core/__tests__/integration-markers.spec.ts +0 -10
- package/src/api/core/addon-settings.router.ts +0 -127
- package/src/api/core/agents.router.ts +0 -86
- package/src/api/core/auth.router.ts +0 -322
- package/src/api/core/bulk-update-coordinator.ts +0 -305
- package/src/api/core/cap-providers.ts +0 -1339
- package/src/api/core/capabilities.router.ts +0 -149
- package/src/api/core/collection-preference.ts +0 -40
- package/src/api/core/event-bus-proxy.router.ts +0 -45
- package/src/api/core/hwaccel.router.ts +0 -108
- package/src/api/core/live-events.router.ts +0 -67
- package/src/api/core/logs.router.ts +0 -195
- package/src/api/core/notifications.router.ts +0 -66
- package/src/api/core/repl.router.ts +0 -39
- package/src/api/core/settings-backend.router.ts +0 -140
- package/src/api/core/stream-probe.router.ts +0 -57
- package/src/api/core/system-events.router.ts +0 -125
- package/src/api/health/health.routes.ts +0 -117
- package/src/api/oauth2/__tests__/oauth2-routes.spec.ts +0 -62
- package/src/api/oauth2/oauth2-routes.ts +0 -281
- package/src/api/trpc/__tests__/client-ip.spec.ts +0 -146
- package/src/api/trpc/__tests__/scope-access-device.spec.ts +0 -268
- package/src/api/trpc/__tests__/scope-access.spec.ts +0 -102
- package/src/api/trpc/__tests__/webrtc-session-ua-enrich.spec.ts +0 -136
- package/src/api/trpc/cap-mount-helpers.ts +0 -245
- package/src/api/trpc/cap-route-error-formatter.ts +0 -171
- package/src/api/trpc/client-ip.ts +0 -147
- package/src/api/trpc/core-cap-bridge.ts +0 -154
- package/src/api/trpc/generated-cap-mounts.ts +0 -1240
- package/src/api/trpc/generated-cap-routers.ts +0 -11523
- package/src/api/trpc/scope-access.ts +0 -110
- package/src/api/trpc/trpc.context.ts +0 -258
- package/src/api/trpc/trpc.middleware.ts +0 -146
- package/src/api/trpc/trpc.router.ts +0 -389
- package/src/auth/session-cookie.ts +0 -54
- package/src/boot/__tests__/integration-id-backfill.spec.ts +0 -131
- package/src/boot/boot-config.ts +0 -259
- package/src/boot/integration-id-backfill.ts +0 -109
- package/src/boot/post-boot.service.ts +0 -105
- package/src/core/addon/__tests__/addon-registry-capability.test.ts +0 -62
- package/src/core/addon/__tests__/addon-row-manifest.spec.ts +0 -62
- package/src/core/addon/addon-call-gateway.ts +0 -171
- package/src/core/addon/addon-package.service.ts +0 -1787
- package/src/core/addon/addon-registry.service.ts +0 -3130
- package/src/core/addon/addon-search.service.ts +0 -91
- package/src/core/addon/addon-settings-provider.ts +0 -220
- package/src/core/addon/addon.tokens.ts +0 -2
- package/src/core/addon-bridge/addon-bridge.service.ts +0 -130
- package/src/core/addon-pages/addon-pages.service.spec.ts +0 -117
- package/src/core/addon-pages/addon-pages.service.ts +0 -82
- package/src/core/addon-widgets/addon-widgets.service.ts +0 -95
- package/src/core/agent/agent-registry.service.ts +0 -529
- package/src/core/auth/auth.service.spec.ts +0 -86
- package/src/core/auth/auth.service.ts +0 -8
- package/src/core/capability/capability.service.ts +0 -66
- package/src/core/config/config.schema.ts +0 -3
- package/src/core/config/config.service.spec.ts +0 -175
- package/src/core/config/config.service.ts +0 -7
- package/src/core/events/event-bus.service.spec.ts +0 -235
- package/src/core/events/event-bus.service.ts +0 -89
- package/src/core/feature/feature.service.spec.ts +0 -99
- package/src/core/feature/feature.service.ts +0 -8
- package/src/core/lifecycle/lifecycle-state-machine.spec.ts +0 -166
- package/src/core/lifecycle/lifecycle-state-machine.ts +0 -3
- package/src/core/logging/log-ring-buffer.ts +0 -3
- package/src/core/logging/logging.service.spec.ts +0 -287
- package/src/core/logging/logging.service.ts +0 -143
- package/src/core/logging/scoped-logger.ts +0 -3
- package/src/core/moleculer/cap-call-fn.spec.ts +0 -173
- package/src/core/moleculer/cap-call-fn.ts +0 -107
- package/src/core/moleculer/cap-route-authority.ts +0 -194
- package/src/core/moleculer/moleculer.service.ts +0 -1072
- package/src/core/network/network-quality.service.spec.ts +0 -53
- package/src/core/network/network-quality.service.ts +0 -5
- package/src/core/notification/notification-wrapper.service.ts +0 -34
- package/src/core/notification/toast-wrapper.service.ts +0 -27
- package/src/core/provider/provider.tokens.ts +0 -1
- package/src/core/repl/repl-engine.service.spec.ts +0 -444
- package/src/core/repl/repl-engine.service.ts +0 -155
- package/src/core/storage/fs-storage-backend.spec.ts +0 -70
- package/src/core/storage/fs-storage-backend.ts +0 -3
- package/src/core/storage/storage-location-manager.spec.ts +0 -130
- package/src/core/storage/storage-location-manager.ts +0 -3
- package/src/core/storage/storage.service.spec.ts +0 -73
- package/src/core/storage/storage.service.ts +0 -3
- package/src/core/streaming/stream-probe.service.ts +0 -221
- package/src/core/topology/topology-emitter.service.ts +0 -105
- package/src/launcher.ts +0 -314
- package/src/main.ts +0 -1245
- package/src/manual-boot.ts +0 -301
- package/tsconfig.build.json +0 -8
- package/tsconfig.json +0 -33
- package/vitest.config.ts +0 -26
|
@@ -1,1072 +0,0 @@
|
|
|
1
|
-
import { ServiceBroker, type Service, type ServiceSchema } from 'moleculer'
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Narrow-typed view of the Moleculer surface this file actually uses.
|
|
5
|
-
* Moleculer's published `.d.ts` chains through `eventemitter2` whose
|
|
6
|
-
* package.json has no `types` field; typescript-eslint's `no-unsafe-*`
|
|
7
|
-
* rules flag every `broker.X` access. Casting once at the boundary
|
|
8
|
-
* collapses the rule violations into one documented narrowing.
|
|
9
|
-
*/
|
|
10
|
-
interface BrokerLike {
|
|
11
|
-
readonly nodeID: string
|
|
12
|
-
readonly logger: Record<string, unknown>
|
|
13
|
-
start(): Promise<void>
|
|
14
|
-
stop(): Promise<void>
|
|
15
|
-
createService(svc: Service | unknown): unknown
|
|
16
|
-
call(action: string, params?: unknown, opts?: unknown): Promise<unknown>
|
|
17
|
-
waitForServices(services: string[], timeout?: number): Promise<unknown>
|
|
18
|
-
}
|
|
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'
|
|
50
|
-
import { buildCapCallFn } from './cap-call-fn.js'
|
|
51
|
-
import { createNodeCapAuthority, createInProcessProviderLookup } from './cap-route-authority.js'
|
|
52
|
-
import { EventCategory, expandCapMethods, ReadinessRegistry, emitReadiness } from '@camstack/types'
|
|
53
|
-
import type { CapabilityDefinition } from '@camstack/types'
|
|
54
|
-
import { randomUUID } from 'node:crypto'
|
|
55
|
-
import { EventBusService } from '../events/event-bus.service'
|
|
56
|
-
import { ConfigService } from '../config/config.service'
|
|
57
|
-
import { LoggingService } from '../logging/logging.service'
|
|
58
|
-
import { CapabilityService } from '../capability/capability.service'
|
|
59
|
-
import { StreamProbeService } from '../streaming/stream-probe.service'
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Narrow-typed view of the Moleculer broker surface used by the
|
|
63
|
-
* `$node.disconnected` listener — extends `BrokerLike` with `localBus`
|
|
64
|
-
* so the handler can be fully typed (same pattern as agent-registry.service.ts).
|
|
65
|
-
*/
|
|
66
|
-
interface BrokerWithLocalBus {
|
|
67
|
-
localBus: {
|
|
68
|
-
on(event: string, handler: (payload: { node: { id: string } }) => void): void
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* One `(addon, capability)` pair that a node manifest applies onto the
|
|
74
|
-
* CapabilityRegistry, with its resolved `CapabilityDefinition`. Built by
|
|
75
|
-
* `applyNodeManifest`'s diff so the register loop never re-resolves the
|
|
76
|
-
* cap def. Keyed in the diff map by `` `${registryKey}::${capName}` ``.
|
|
77
|
-
*/
|
|
78
|
-
interface AppliedCapEntry {
|
|
79
|
-
readonly addonId: string
|
|
80
|
-
readonly capName: string
|
|
81
|
-
readonly capDef: CapabilityDefinition
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
export class MoleculerService {
|
|
85
|
-
readonly broker: ServiceBroker
|
|
86
|
-
/** Narrow-typed view of `this.broker` — see `BrokerLike` doc above. */
|
|
87
|
-
private get brokerSafe(): BrokerLike {
|
|
88
|
-
return this.broker as unknown as BrokerLike
|
|
89
|
-
}
|
|
90
|
-
private readonly logger: ReturnType<LoggingService['createLogger']>
|
|
91
|
-
/**
|
|
92
|
-
* D3 authority: union of every node's manifest delivered via
|
|
93
|
-
* `$hub.registerNode`. Populated by `onRegisterNode` in `hubDeps`.
|
|
94
|
-
*/
|
|
95
|
-
private readonly nodeRegistry = new HubNodeRegistry()
|
|
96
|
-
private readonly nodeCallFns = new Map<string, CallFn>()
|
|
97
|
-
/**
|
|
98
|
-
* D3: callback invoked whenever a bare-ID agent node completes the
|
|
99
|
-
* `$hub.registerNode` handshake. Registered by `AgentRegistryService`
|
|
100
|
-
* via `setOnAgentRegistered()`. The handshake is the authoritative
|
|
101
|
-
* completeness signal — the hub has the full manifest at this point
|
|
102
|
-
* and reconciliation can run immediately (no grace delay needed).
|
|
103
|
-
*/
|
|
104
|
-
private onAgentRegisteredCb: ((nodeId: string) => void) | null = null
|
|
105
|
-
/**
|
|
106
|
-
* Hub-side authoritative readiness registry. Subscribed to the
|
|
107
|
-
* shared `EventBusService` so it ingests both hub-local emits and
|
|
108
|
-
* remote emits forwarded via `$hub.event`. Exposed to:
|
|
109
|
-
* - the `$readiness.getSnapshot` Moleculer action (consumed by
|
|
110
|
-
* workers / agents on boot)
|
|
111
|
-
* - `ctx.kernel.readinessRegistry` on every hub addon context so
|
|
112
|
-
* hub consumers share the same snapshot.
|
|
113
|
-
*/
|
|
114
|
-
readonly readinessRegistry: ReadinessRegistry
|
|
115
|
-
/**
|
|
116
|
-
* Resolved cluster secret (`CAMSTACK_CLUSTER_SECRET` env, else
|
|
117
|
-
* `cluster.secret` config), or `undefined` when none is configured.
|
|
118
|
-
* Threaded both into the broker factory and the hub's
|
|
119
|
-
* `expectedClusterSecretHash` so `$hub.registerNode` can gate on it.
|
|
120
|
-
*/
|
|
121
|
-
private readonly clusterSecret: string | undefined
|
|
122
|
-
/**
|
|
123
|
-
* UDS server that listens for addon-runners spawned by this hub node.
|
|
124
|
-
* `null` when the UDS server failed to start (children run broker-only).
|
|
125
|
-
*/
|
|
126
|
-
private localChildRegistry: LocalChildRegistry | null = null
|
|
127
|
-
/**
|
|
128
|
-
* Tracks cap names already logged as UDS-routed to avoid log spam.
|
|
129
|
-
* Cleared on no external event — one INFO line per distinct capName
|
|
130
|
-
* across the lifetime of the process.
|
|
131
|
-
*/
|
|
132
|
-
private readonly udsRoutedCaps = new Set<string>()
|
|
133
|
-
/**
|
|
134
|
-
* CapRouteResolver — the single authority for cap dispatch routing.
|
|
135
|
-
* Constructed at the end of onModuleInit, once `localChildRegistry` is
|
|
136
|
-
* available and the broker has started. All cap dispatch flows through this.
|
|
137
|
-
*/
|
|
138
|
-
private resolver: CapRouteResolver | null = null
|
|
139
|
-
/**
|
|
140
|
-
* Disposer returned by `createUdsEventBridge`. Called in `onModuleDestroy`
|
|
141
|
-
* to unsubscribe the bridge from the parent bus and clear the child-event
|
|
142
|
-
* handler, preventing subscriber leaks on shutdown.
|
|
143
|
-
*/
|
|
144
|
-
private udsEventBridgeDispose: (() => void) | null = null
|
|
145
|
-
|
|
146
|
-
get childRegistry(): LocalChildRegistry | null {
|
|
147
|
-
return this.localChildRegistry
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
/** The CapRouteResolver once onModuleInit has completed; null before that. */
|
|
151
|
-
get capRouteResolver(): CapRouteResolver | null {
|
|
152
|
-
return this.resolver
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
/** This hub's Moleculer node id (e.g. `hub`). Hub-local forked children
|
|
156
|
-
* register under `${nodeId}/${runnerId}`. */
|
|
157
|
-
get nodeId(): string {
|
|
158
|
-
return this.brokerSafe.nodeID
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
constructor(
|
|
162
|
-
private readonly eventBus: EventBusService,
|
|
163
|
-
private readonly config: ConfigService,
|
|
164
|
-
private readonly logging: LoggingService,
|
|
165
|
-
private readonly capabilityService: CapabilityService,
|
|
166
|
-
private readonly streamProbe: StreamProbeService,
|
|
167
|
-
) {
|
|
168
|
-
this.logger = this.logging.createLogger('moleculer')
|
|
169
|
-
// Optional port overrides. Live primarily for the e2e harness: when
|
|
170
|
-
// a developer's dev:full is up on the default 6000/4445 ports, an
|
|
171
|
-
// isolated test hub can't reuse them. Production keeps the defaults
|
|
172
|
-
// so cluster discovery + agent-to-hub connections keep working
|
|
173
|
-
// without per-deploy config.
|
|
174
|
-
const tcpPortEnv = process.env['CAMSTACK_HUB_TCP_PORT']
|
|
175
|
-
const udpPortEnv = process.env['CAMSTACK_HUB_UDP_PORT']
|
|
176
|
-
const tcpPort = tcpPortEnv ? Number(tcpPortEnv) : undefined
|
|
177
|
-
const udpPort = udpPortEnv ? Number(udpPortEnv) : undefined
|
|
178
|
-
// Two-step cast: createBroker's dist `.d.ts` chains through
|
|
179
|
-
// moleculer→eventemitter2 whose types are unresolvable at this
|
|
180
|
-
// boundary, so the inference falls to `error` and trips
|
|
181
|
-
// `no-unsafe-assignment`. Going via `unknown` documents the boundary.
|
|
182
|
-
this.clusterSecret =
|
|
183
|
-
process.env['CAMSTACK_CLUSTER_SECRET'] ?? this.config.get<string>('cluster.secret')
|
|
184
|
-
const broker = createBroker({
|
|
185
|
-
nodeID: 'hub',
|
|
186
|
-
mode: 'hub',
|
|
187
|
-
logLevel: this.config.get<string>('moleculer.logLevel') ?? 'warn',
|
|
188
|
-
secret: this.clusterSecret,
|
|
189
|
-
...(tcpPort && !Number.isNaN(tcpPort) ? { tcpPort } : {}),
|
|
190
|
-
...(udpPort && !Number.isNaN(udpPort) ? { udpPort } : {}),
|
|
191
|
-
}) as unknown
|
|
192
|
-
// `ServiceBroker` itself surfaces as `error`-typed at this boundary
|
|
193
|
-
// (eventemitter2 chain unresolvable). Documented + single-site cast.
|
|
194
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
|
195
|
-
this.broker = broker as ServiceBroker
|
|
196
|
-
this.readinessRegistry = new ReadinessRegistry({
|
|
197
|
-
eventBus: this.eventBus,
|
|
198
|
-
sourceNodeId: this.brokerSafe.nodeID,
|
|
199
|
-
logger: this.logging.createLogger('readiness'),
|
|
200
|
-
})
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
/**
|
|
204
|
-
* D3: register the callback that fires when an agent node completes the
|
|
205
|
-
* `$hub.registerNode` handshake. Called by `AgentRegistryService` during
|
|
206
|
-
* its own `onModuleInit` — after `MoleculerService.onModuleInit` has
|
|
207
|
-
* returned, so the broker is already live. Option (b) direct-callback
|
|
208
|
-
* wiring: no event-bus round-trip needed for internal core wiring.
|
|
209
|
-
*/
|
|
210
|
-
setOnAgentRegistered(cb: (nodeId: string) => void): void {
|
|
211
|
-
this.onAgentRegisteredCb = cb
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
async onModuleInit(): Promise<void> {
|
|
215
|
-
const logger = this.logging.createLogger('moleculer')
|
|
216
|
-
|
|
217
|
-
const hubDeps: HubServiceDeps = {
|
|
218
|
-
getAddonConfig: (addonId) => {
|
|
219
|
-
return this.config.getAddonConfig(addonId)
|
|
220
|
-
},
|
|
221
|
-
getSettings: (scope, key) => {
|
|
222
|
-
return this.config.get(key ? `${scope}.${key}` : scope)
|
|
223
|
-
},
|
|
224
|
-
getRecentEvents: (category, limit) => {
|
|
225
|
-
return this.eventBus.getRecent(category ? { category } : undefined, limit)
|
|
226
|
-
},
|
|
227
|
-
onLog: (entry) => {
|
|
228
|
-
this.logging.writeFromWorker({
|
|
229
|
-
addonId: entry.addonId,
|
|
230
|
-
nodeId: entry.nodeId,
|
|
231
|
-
level: entry.level,
|
|
232
|
-
message: entry.message,
|
|
233
|
-
...(entry.scope !== undefined ? { scope: entry.scope } : {}),
|
|
234
|
-
...(entry.tags ? { tags: entry.tags } : {}),
|
|
235
|
-
...(entry.meta ? { meta: entry.meta } : {}),
|
|
236
|
-
})
|
|
237
|
-
},
|
|
238
|
-
onSetLogLevel: (level) => {
|
|
239
|
-
const factory = this.brokerSafe.logger
|
|
240
|
-
const appenders = factory['appenders'] as Array<{ opts: { level: string } }> | undefined
|
|
241
|
-
if (appenders) {
|
|
242
|
-
for (const appender of appenders) {
|
|
243
|
-
appender.opts.level = level
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
const cache = factory['cache'] as Map<string, unknown> | undefined
|
|
247
|
-
if (cache) {
|
|
248
|
-
cache.clear()
|
|
249
|
-
}
|
|
250
|
-
logger.info('Moleculer log level changed', { meta: { level } })
|
|
251
|
-
this.brokerSafe.call('$process.setLogLevel', { level }).catch(() => {})
|
|
252
|
-
},
|
|
253
|
-
// D3: registration-handshake path. Nodes send $hub.registerNode with
|
|
254
|
-
// their complete capability manifest; the hub applies it immediately.
|
|
255
|
-
onRegisterNode: (params) => {
|
|
256
|
-
this.onRegisterNode(params)
|
|
257
|
-
},
|
|
258
|
-
onUnregisterNode: (nodeId) => {
|
|
259
|
-
this.removeNodeFromRegistry(nodeId)
|
|
260
|
-
},
|
|
261
|
-
expectedClusterSecretHash: this.clusterSecret
|
|
262
|
-
? hashClusterSecret(this.clusterSecret)
|
|
263
|
-
: undefined,
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
const hubService: unknown = createHubService(hubDeps)
|
|
267
|
-
this.brokerSafe.createService(hubService)
|
|
268
|
-
|
|
269
|
-
const dataDir = this.config.get<string>('dataDir') ?? 'camstack-data'
|
|
270
|
-
|
|
271
|
-
// UDS local transport: the hub hosts a LocalChildRegistry so its
|
|
272
|
-
// forked addon-runners route cap calls directly over a Unix-domain
|
|
273
|
-
// socket instead of through Moleculer. The broker stays available as
|
|
274
|
-
// the no-route fallback (remote agents + caps no local child owns).
|
|
275
|
-
// If the registry fails to start, children transparently fall back to
|
|
276
|
-
// broker-only — no parentUdsPath is propagated.
|
|
277
|
-
let parentUdsPath: string | undefined
|
|
278
|
-
try {
|
|
279
|
-
const nodeId = this.brokerSafe.nodeID
|
|
280
|
-
// F0 (slice-5 outbound): when a forked child issues `ctx.api.<cap>` for a
|
|
281
|
-
// cap NO local sibling owns, route it from the PARENT and return the
|
|
282
|
-
// result over UDS — resolver-first, broker-fallback. Closes over `this`
|
|
283
|
-
// so it reads `this.resolver` at CALL time (the resolver is constructed
|
|
284
|
-
// later in onModuleInit, after broker.start()). The broker fallback
|
|
285
|
-
// reaches the hub's core `$`-infra services (`$core-caps`, `$stream-probe`,
|
|
286
|
-
// settings-store, …) that are Moleculer services, NOT registered
|
|
287
|
-
// capabilities the resolver can see. Before F0 this fell through
|
|
288
|
-
// UDS_NO_ROUTE → the child's own brokerTransportLink; F1+F2 removes that
|
|
289
|
-
// child broker, so the parent must own this path.
|
|
290
|
-
const onUnownedCall = createParentUnownedCallHandler({
|
|
291
|
-
getResolver: () => this.resolver,
|
|
292
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- moleculer types unresolvable; see `broker` getter docstring
|
|
293
|
-
broker: this.broker,
|
|
294
|
-
// Single capability authority — lets the broker fallback pin a
|
|
295
|
-
// device-scoped call to its owning node instead of load-balancing it.
|
|
296
|
-
nodeRegistry: this.nodeRegistry,
|
|
297
|
-
// Hub-local UDS child dispatcher — routes a device-scoped native cap
|
|
298
|
-
// owned by a hub-local child (reolink/hikvision cameras) directly over
|
|
299
|
-
// UDS before any broker fallback. Getter: `this.localChildRegistry` is
|
|
300
|
-
// assigned later in this method, after the handler is constructed.
|
|
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,
|
|
311
|
-
logger: {
|
|
312
|
-
warn: (msg, meta) =>
|
|
313
|
-
logger.warn(
|
|
314
|
-
msg,
|
|
315
|
-
meta !== null && meta !== undefined
|
|
316
|
-
? { meta: meta as Record<string, unknown> }
|
|
317
|
-
: undefined,
|
|
318
|
-
),
|
|
319
|
-
},
|
|
320
|
-
})
|
|
321
|
-
const registry = new LocalChildRegistry({
|
|
322
|
-
server: createLocalTransport().createServer(nodeId),
|
|
323
|
-
onUnownedCall,
|
|
324
|
-
logger: {
|
|
325
|
-
info: (msg, meta) =>
|
|
326
|
-
logger.info(
|
|
327
|
-
msg,
|
|
328
|
-
meta !== null && meta !== undefined
|
|
329
|
-
? { meta: meta as Record<string, unknown> }
|
|
330
|
-
: undefined,
|
|
331
|
-
),
|
|
332
|
-
},
|
|
333
|
-
// Hand the UDS-routing layer a view into the operator's
|
|
334
|
-
// active-singleton preference. Without this, when two local
|
|
335
|
-
// children own the same singleton cap (e.g. two `webrtc-session`
|
|
336
|
-
// providers), routing returns
|
|
337
|
-
// the first-registered child by insertion order — silently
|
|
338
|
-
// bypassing `setActiveSingleton`. The closure reads the live
|
|
339
|
-
// registry on every call so a runtime swap takes effect
|
|
340
|
-
// immediately without rebuilding the resolver snapshot.
|
|
341
|
-
getActiveSingletonAddonId: (capName: string): string | null =>
|
|
342
|
-
this.capabilityService.getRegistry()?.getSingletonAddonId(capName) ?? null,
|
|
343
|
-
})
|
|
344
|
-
await registry.start()
|
|
345
|
-
// E1: apply child manifest + cleanup from the UDS lifecycle (hub-local children).
|
|
346
|
-
// When a runner connects over UDS, apply its cap descriptors to the
|
|
347
|
-
// CapabilityRegistry — same effect as the `$hub.registerNode` RPC for
|
|
348
|
-
// `hub/<runner>` nodes. This runs in PARALLEL with the RPC path until
|
|
349
|
-
// Phase F removes the child broker; `onRegisterNode`'s diff logic ensures
|
|
350
|
-
// double-apply is idempotent (same nodeId + same caps → no-op on the second call).
|
|
351
|
-
registry.onChildRegistered((child) => {
|
|
352
|
-
const hubNodeId = this.brokerSafe.nodeID
|
|
353
|
-
const nodeId = `${hubNodeId}/${child.childId}`
|
|
354
|
-
const params = buildChildUdsManifest(nodeId, child.childId, child.caps)
|
|
355
|
-
this.onRegisterNode(params)
|
|
356
|
-
logger.info('UDS child registered — manifest applied', { meta: { nodeId } })
|
|
357
|
-
})
|
|
358
|
-
// E1: cleanup on child disconnect — same effect as `$node.disconnected`
|
|
359
|
-
// for hub-local children. The Moleculer path stays for AGENT nodes.
|
|
360
|
-
registry.onChildGone((childId) => {
|
|
361
|
-
const hubNodeId = this.brokerSafe.nodeID
|
|
362
|
-
const nodeId = `${hubNodeId}/${childId}`
|
|
363
|
-
logger.info('UDS child gone — removing from registry', { meta: { childId } })
|
|
364
|
-
this.removeNodeFromRegistry(nodeId)
|
|
365
|
-
})
|
|
366
|
-
// B2: ingest UDS child logs into the hub's LoggingService so they appear
|
|
367
|
-
// in the LogManager / admin-UI log stream alongside broker-forwarded logs.
|
|
368
|
-
// This runs in PARALLEL with the existing $hub.log / onLog broker path —
|
|
369
|
-
// both stay active until Phase F removes the broker path.
|
|
370
|
-
registry.onChildLog((childId, entry) => {
|
|
371
|
-
this.logging.writeFromWorker(udsChildLogToWorkerEntry(childId, entry))
|
|
372
|
-
})
|
|
373
|
-
// D1: answer readiness-snapshot requests from UDS children so they can
|
|
374
|
-
// hydrate without a `$readiness.getSnapshot` Moleculer call.
|
|
375
|
-
// `this.readinessRegistry` is the hub-authoritative instance subscribed
|
|
376
|
-
// to the shared EventBusService — same source `$readiness.getSnapshot` uses.
|
|
377
|
-
// The handler is a live closure (calls `getSnapshotForTransport()` on each
|
|
378
|
-
// request) so children always receive the current snapshot, not a stale copy.
|
|
379
|
-
// Keep `$readiness.getSnapshot` intact — Phase F removes it.
|
|
380
|
-
registry.onReadinessSnapshotRequest(() => this.readinessRegistry.getSnapshotForTransport())
|
|
381
|
-
this.localChildRegistry = registry
|
|
382
|
-
parentUdsPath = localEndpointPath(nodeId)
|
|
383
|
-
logger.info('UDS child registry listening', { meta: { path: parentUdsPath } })
|
|
384
|
-
} catch (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
|
-
})
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
const processService: unknown = createProcessService(
|
|
391
|
-
this.brokerSafe.nodeID,
|
|
392
|
-
dataDir,
|
|
393
|
-
undefined,
|
|
394
|
-
undefined,
|
|
395
|
-
parentUdsPath,
|
|
396
|
-
)
|
|
397
|
-
this.brokerSafe.createService(processService)
|
|
398
|
-
|
|
399
|
-
// $addonHost — REMOVED (Sprint 6). Three-level settings are now
|
|
400
|
-
// served by the `addon-settings` singleton capability. Per-addon
|
|
401
|
-
// Moleculer services expose `settings.*` actions for remote agents.
|
|
402
|
-
|
|
403
|
-
// D3: mirror $node.disconnected onto the registry path so nodes that
|
|
404
|
-
// sent a $hub.registerNode manifest get cleaned up on disconnect.
|
|
405
|
-
const bridgeBus = this.broker as unknown as BrokerWithLocalBus
|
|
406
|
-
bridgeBus.localBus.on('$node.disconnected', ({ node }: { node: { id: string } }) => {
|
|
407
|
-
this.removeNodeFromRegistry(node.id)
|
|
408
|
-
})
|
|
409
|
-
|
|
410
|
-
// Register the $event-bus service BEFORE broker.start(). Moleculer
|
|
411
|
-
// announces service subscriptions to remote nodes only during discovery
|
|
412
|
-
// handshake, not dynamically after post-start `createService()`.
|
|
413
|
-
// Without this, cross-node broadcasts (camstack.evt.<category>) would
|
|
414
|
-
// arrive unreliably for the first ~10s after each node joins.
|
|
415
|
-
registerEventBusService(this.broker)
|
|
416
|
-
|
|
417
|
-
// Register the hub-authoritative `$readiness.getSnapshot` service
|
|
418
|
-
// BEFORE broker.start() so workers / agents see it in their initial
|
|
419
|
-
// INFO packet — post-start `createService` calls propagate via
|
|
420
|
-
// heartbeat (several seconds) and would force workers to poll.
|
|
421
|
-
this.brokerSafe.createService(createReadinessServiceForRegistry(this.readinessRegistry))
|
|
422
|
-
|
|
423
|
-
// Register the hub-authoritative `$stream-probe` service —
|
|
424
|
-
// workers route RTSP probe + field-probe through this action when
|
|
425
|
-
// the tRPC WSS link isn't available (default path for forked
|
|
426
|
-
// workers). Keeps ffprobe + HTTP-reachability as a single
|
|
427
|
-
// hub-side implementation; see `createStreamProbeBrokerService`
|
|
428
|
-
// for the shape.
|
|
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
|
-
)
|
|
435
|
-
|
|
436
|
-
// Register `$hwaccel` on hub — every node in the cluster does the
|
|
437
|
-
// same so `broker.call('$hwaccel.resolve', params, { nodeID })`
|
|
438
|
-
// returns the backend list for whichever host the caller targets.
|
|
439
|
-
// Admin UI uses this to show per-agent hwaccel info on the
|
|
440
|
-
// pipeline / NodeDetail pages.
|
|
441
|
-
this.brokerSafe.createService(createHwAccelService(createKernelHwAccel()))
|
|
442
|
-
|
|
443
|
-
await this.brokerSafe.start()
|
|
444
|
-
logger.info('Moleculer broker started (TCP transport)')
|
|
445
|
-
|
|
446
|
-
// Construct the CapRouteResolver now that both the broker and
|
|
447
|
-
// localChildRegistry are ready. The resolver reads live registry state
|
|
448
|
-
// via closure accessors on every call (not a frozen snapshot), so new
|
|
449
|
-
// children connecting/disconnecting after this point are picked up
|
|
450
|
-
// correctly. The localChildRegistry reference is captured and may be
|
|
451
|
-
// null if UDS failed to start.
|
|
452
|
-
this.resolver = new CapRouteResolver({
|
|
453
|
-
hubNodeId: this.brokerSafe.nodeID,
|
|
454
|
-
broker: this.brokerSafe,
|
|
455
|
-
hubLocalRegistry: this.localChildRegistry,
|
|
456
|
-
nodeAuthority: createNodeCapAuthority(this.nodeRegistry, {
|
|
457
|
-
resolveSingleton: (capName, nodeId) =>
|
|
458
|
-
this.capabilityService.getRegistry()?.resolveSingletonAddonIdForNode(capName, nodeId) ??
|
|
459
|
-
null,
|
|
460
|
-
}),
|
|
461
|
-
inProcessProviders: createInProcessProviderLookup(this.capabilityService),
|
|
462
|
-
})
|
|
463
|
-
|
|
464
|
-
// Wire the hub's EventBusService into the broker so hub-addon
|
|
465
|
-
// emissions fan out to every remote process via
|
|
466
|
-
// `camstack.evt.<category>`, and incoming $event-bus events land on
|
|
467
|
-
// the same local bus that subscribers already use. The
|
|
468
|
-
// EventBusService's `emit` override handles the "only broadcast
|
|
469
|
-
// locally-originated events" guard, and id-based dedup absorbs the
|
|
470
|
-
// duplicate delivery when `createBrokerEventBus` on a remote uses
|
|
471
|
-
// both broadcast + `$hub.event` for back-compat.
|
|
472
|
-
this.eventBus.attachBroker(this.broker)
|
|
473
|
-
|
|
474
|
-
// C2: wire the UDS ↔ Moleculer event bridge so events emitted by UDS
|
|
475
|
-
// children fan to siblings and reach the cluster, and cluster / parent-
|
|
476
|
-
// local events propagate to every UDS child. Inert when no children
|
|
477
|
-
// are connected (bridge just adds a no-op bus subscriber). The bridge
|
|
478
|
-
// is wired after attachBroker so the parentBus is backed by the real
|
|
479
|
-
// shared broker bus and broker.broadcast is live.
|
|
480
|
-
if (this.localChildRegistry !== null) {
|
|
481
|
-
const hubNodeId = this.brokerSafe.nodeID
|
|
482
|
-
this.udsEventBridgeDispose = createUdsEventBridge({
|
|
483
|
-
registry: this.localChildRegistry,
|
|
484
|
-
parentBus: this.eventBus,
|
|
485
|
-
parentNodeId: hubNodeId,
|
|
486
|
-
})
|
|
487
|
-
}
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
/**
|
|
491
|
-
* Register the log-receiver service for agent log forwarding.
|
|
492
|
-
* Must be called AFTER app.init() so Moleculer re-advertises
|
|
493
|
-
* the updated service list to the network.
|
|
494
|
-
*/
|
|
495
|
-
registerLogReceiver(): void {
|
|
496
|
-
this.brokerSafe.createService({
|
|
497
|
-
name: 'log-receiver',
|
|
498
|
-
actions: {
|
|
499
|
-
ingest: {
|
|
500
|
-
handler: (ctx: {
|
|
501
|
-
params: {
|
|
502
|
-
level: string
|
|
503
|
-
message: string
|
|
504
|
-
addonId: string
|
|
505
|
-
nodeId: string
|
|
506
|
-
scope?: string
|
|
507
|
-
tags?: import('@camstack/types').LogTags
|
|
508
|
-
meta?: Record<string, unknown>
|
|
509
|
-
}
|
|
510
|
-
}) => {
|
|
511
|
-
this.logging.writeFromWorker({
|
|
512
|
-
addonId: ctx.params.addonId,
|
|
513
|
-
nodeId: ctx.params.nodeId,
|
|
514
|
-
level: ctx.params.level,
|
|
515
|
-
message: ctx.params.message,
|
|
516
|
-
...(ctx.params.scope !== undefined ? { scope: ctx.params.scope } : {}),
|
|
517
|
-
...(ctx.params.tags ? { tags: ctx.params.tags } : {}),
|
|
518
|
-
...(ctx.params.meta ? { meta: ctx.params.meta } : {}),
|
|
519
|
-
})
|
|
520
|
-
return true
|
|
521
|
-
},
|
|
522
|
-
},
|
|
523
|
-
},
|
|
524
|
-
})
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
/**
|
|
528
|
-
* Register the `$core-caps` Moleculer service that bridges the hub's
|
|
529
|
-
* core (non-addon) tRPC routers onto the cluster mesh.
|
|
530
|
-
*
|
|
531
|
-
* Called from `main.ts` after the appRouter is built — that happens
|
|
532
|
-
* after `app.init()`, so the broker is already started. Post-start
|
|
533
|
-
* `createService` is fine: the service propagates to remote nodes via
|
|
534
|
-
* heartbeat and `brokerTransportLink` polls discovery, so forked
|
|
535
|
-
* addons and late-joining agents still resolve `ctx.api.<coreCap>`.
|
|
536
|
-
* `registerLogReceiver` relies on the same post-init registration.
|
|
537
|
-
*/
|
|
538
|
-
registerCoreCapService(service: ServiceSchema): void {
|
|
539
|
-
this.brokerSafe.createService(service)
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
/**
|
|
543
|
-
* Call a capability method on a specific node.
|
|
544
|
-
*
|
|
545
|
-
* Delegates all routing decisions to the CapRouteResolver, which classifies
|
|
546
|
-
* the (capName, nodeId) pair into a typed CapRoute and dispatches to the
|
|
547
|
-
* appropriate transport (hub-in-process, hub-local-uds, remote-moleculer,
|
|
548
|
-
* agent-child-forward). A genuinely-absent cap throws CapRouteError (typed,
|
|
549
|
-
* with reason + rejected routes) instead of the old opaque error string.
|
|
550
|
-
*
|
|
551
|
-
* Falls back to the legacy findCallFn path when the resolver is not yet
|
|
552
|
-
* constructed (before onModuleInit completes) or when the resolver throws a
|
|
553
|
-
* no-provider / node-offline error for a node that IS in nodeCallFns — this
|
|
554
|
-
* handles the window between registerNode applying a callFn and the resolver
|
|
555
|
-
* seeing the new node (the resolver reads live registry state via closure
|
|
556
|
-
* accessors, but nodeCallFns is populated by applyNodeManifest which may
|
|
557
|
-
* have run before the resolver was constructed).
|
|
558
|
-
*/
|
|
559
|
-
async callCapabilityOnNode(
|
|
560
|
-
nodeId: string,
|
|
561
|
-
capabilityName: string,
|
|
562
|
-
methodName: string,
|
|
563
|
-
params: unknown,
|
|
564
|
-
): Promise<unknown> {
|
|
565
|
-
const resolver = this.resolver
|
|
566
|
-
if (resolver !== null) {
|
|
567
|
-
// Extract deviceId from params so device-scoped native caps (ptz, motion-zones, …)
|
|
568
|
-
// resolve through the resolver's deviceId-aware snapshot instead of falling back to
|
|
569
|
-
// the legacy callFn store. The deviceId hint is a number extracted from the method args.
|
|
570
|
-
const rawDeviceId: unknown =
|
|
571
|
-
params !== null && typeof params === 'object' ? Reflect.get(params, 'deviceId') : undefined
|
|
572
|
-
const routeDeviceId: number | undefined =
|
|
573
|
-
typeof rawDeviceId === 'number' ? rawDeviceId : undefined
|
|
574
|
-
try {
|
|
575
|
-
const route = resolver.resolveCapRoute(capabilityName, { nodeId, deviceId: routeDeviceId })
|
|
576
|
-
return await resolver.dispatch(route, methodName, params)
|
|
577
|
-
} catch (err) {
|
|
578
|
-
if (
|
|
579
|
-
err instanceof CapRouteError &&
|
|
580
|
-
(err.reason === 'no-provider' || err.reason === 'node-offline')
|
|
581
|
-
) {
|
|
582
|
-
// Resolver couldn't find the cap — try the legacy callFn store as a
|
|
583
|
-
// fallback. This covers caps registered in nodeCallFns (e.g. agent
|
|
584
|
-
// nodes that registered before the resolver's snapshot was built or
|
|
585
|
-
// caps that the resolver's nodeAuthority doesn't see yet because the
|
|
586
|
-
// resolver reads live registry state via closure accessors).
|
|
587
|
-
// Device-scoped native caps now resolve via the resolver (M1/M5 thread deviceId),
|
|
588
|
-
// so this fallback only handles genuinely-transitional stale-snapshot windows.
|
|
589
|
-
const callFn = this.findCallFn(nodeId, capabilityName)
|
|
590
|
-
if (callFn !== undefined) {
|
|
591
|
-
return callFn(methodName, params)
|
|
592
|
-
}
|
|
593
|
-
}
|
|
594
|
-
// Rethrow — includes transport-failed and all other errors
|
|
595
|
-
throw err
|
|
596
|
-
}
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
// Pre-init fallback (resolver not yet constructed — before onModuleInit).
|
|
600
|
-
// This path is only reachable in tests that drive onRegisterNode without
|
|
601
|
-
// calling onModuleInit first.
|
|
602
|
-
if (nodeId === 'hub' || nodeId === this.brokerSafe.nodeID) {
|
|
603
|
-
const registry = this.capabilityService.getRegistry()
|
|
604
|
-
const provider = registry?.getSingleton<Record<string, unknown>>(capabilityName) ?? null
|
|
605
|
-
if (provider !== null) {
|
|
606
|
-
const fn = provider[methodName]
|
|
607
|
-
if (typeof fn !== 'function')
|
|
608
|
-
throw new Error(`Method "${methodName}" not found on "${capabilityName}"`)
|
|
609
|
-
return fn.call(provider, params)
|
|
610
|
-
}
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
const callFn = this.findCallFn(nodeId, capabilityName)
|
|
614
|
-
if (callFn) {
|
|
615
|
-
return callFn(methodName, params)
|
|
616
|
-
}
|
|
617
|
-
|
|
618
|
-
throw new CapRouteError(capabilityName, methodName, {
|
|
619
|
-
reason: 'no-provider',
|
|
620
|
-
nodeId,
|
|
621
|
-
rejected: [
|
|
622
|
-
{ kind: 'hub-in-process', why: 'CapRouteResolver not initialised (pre-onModuleInit)' },
|
|
623
|
-
],
|
|
624
|
-
})
|
|
625
|
-
}
|
|
626
|
-
|
|
627
|
-
/**
|
|
628
|
-
* D3 registration-handshake entrypoint — invoked by the `$hub.registerNode`
|
|
629
|
-
* Moleculer action (wired through `hubDeps.onRegisterNode`).
|
|
630
|
-
*
|
|
631
|
-
* Captures the node's PREVIOUS manifest before `nodeRegistry.registerNode`
|
|
632
|
-
* overwrites it, then hands both manifests to `applyNodeManifest` so the
|
|
633
|
-
* CapabilityRegistry update is a diff (atomic replace) rather than an
|
|
634
|
-
* unconditional re-register — see `applyNodeManifest` for the rationale.
|
|
635
|
-
*/
|
|
636
|
-
private onRegisterNode(params: RegisterNodeParams): void {
|
|
637
|
-
const previousManifest = this.nodeRegistry.getNodeManifest(params.nodeId)
|
|
638
|
-
this.nodeRegistry.registerNode(params)
|
|
639
|
-
this.applyNodeManifest(params, previousManifest)
|
|
640
|
-
// Notify AgentRegistryService to reconcile placement for bare-ID
|
|
641
|
-
// agent nodes (no '/' = not a hub child worker, not the hub itself).
|
|
642
|
-
// The handshake is the authoritative completeness signal — the full
|
|
643
|
-
// manifest is available here so reconciliation runs without delay.
|
|
644
|
-
const { nodeId } = params
|
|
645
|
-
if (nodeId !== 'hub' && !nodeId.includes('/') && this.onAgentRegisteredCb) {
|
|
646
|
-
this.onAgentRegisteredCb(nodeId)
|
|
647
|
-
}
|
|
648
|
-
}
|
|
649
|
-
|
|
650
|
-
/**
|
|
651
|
-
* D3: apply a node's registered manifest onto the CapabilityRegistry.
|
|
652
|
-
* Builds a method proxy for each capability — same registryKey rule
|
|
653
|
-
* (local child → bare addonId, remote agent → addonId@nodeId), same
|
|
654
|
-
* `broker.call` routing shape, same `expandCapMethods` method surface.
|
|
655
|
-
*
|
|
656
|
-
* Called from `onRegisterNode` whenever a node handshakes.
|
|
657
|
-
*
|
|
658
|
-
* Diff-based / idempotent: the D3 protocol legitimately re-handshakes (a
|
|
659
|
-
* node re-sends its COMPLETE manifest — e.g. the post-device-restore
|
|
660
|
-
* `nativeCaps` re-handshake). `registerProvider` throws on a duplicate
|
|
661
|
-
* `(cap, addonId)` pair, so a blind re-register would throw on every
|
|
662
|
-
* re-handshake and trip the registering node's retry loop into a storm.
|
|
663
|
-
* Instead this diffs the NEW manifest against `previousManifest`:
|
|
664
|
-
* unchanged caps are left untouched, dropped caps are unregistered, new
|
|
665
|
-
* caps are registered. This honours the invariant "`registerNode`
|
|
666
|
-
* replaces the node's entire cap set atomically".
|
|
667
|
-
*
|
|
668
|
-
* NOTE: `params.nativeCaps` is stored by `nodeRegistry.registerNode()`
|
|
669
|
-
* already; this method handles only `params.addons` (system caps).
|
|
670
|
-
* Native-cap wiring into device-manager is done in a later task.
|
|
671
|
-
*/
|
|
672
|
-
private applyNodeManifest(
|
|
673
|
-
params: RegisterNodeParams,
|
|
674
|
-
previousManifest?: readonly RegisteredAddonManifest[],
|
|
675
|
-
): void {
|
|
676
|
-
const { nodeId, addons } = params
|
|
677
|
-
const hubNodeId = this.brokerSafe.nodeID
|
|
678
|
-
const isLocalChild = nodeId.startsWith(hubNodeId + '/')
|
|
679
|
-
const isHubInProcess = nodeId === hubNodeId
|
|
680
|
-
|
|
681
|
-
// Hub in-process addons register themselves during initialize() — skip.
|
|
682
|
-
if (isHubInProcess) return
|
|
683
|
-
|
|
684
|
-
const registry = this.capabilityService.getRegistry()
|
|
685
|
-
if (!registry) return
|
|
686
|
-
|
|
687
|
-
// Same registryKey rule as CapabilityBridge / onProviderConnected:
|
|
688
|
-
// local child → bare addonId (one instance per forked child)
|
|
689
|
-
// remote agent → addonId@nodeId (unique per agent node)
|
|
690
|
-
// `nodeId` is identical for the previous and new manifest (same node
|
|
691
|
-
// re-handshaking), so the rule resolves the same key on both sides.
|
|
692
|
-
const registryKeyFor = (addonId: string): string =>
|
|
693
|
-
isLocalChild ? addonId : `${addonId}@${nodeId}`
|
|
694
|
-
|
|
695
|
-
// Collect the `(registryKey, capName)` pairs a manifest would APPLY —
|
|
696
|
-
// applying the SAME `isInfraCapability` skip and `capDef` existence
|
|
697
|
-
// check the register block below uses, so the set reflects exactly
|
|
698
|
-
// what is (or would have been) registered. Keyed `${registryKey}::${capName}`.
|
|
699
|
-
// The resolved `capDef` is carried through so the register loop never
|
|
700
|
-
// re-looks it up (and never needs a non-null assertion).
|
|
701
|
-
const appliedKeys = (
|
|
702
|
-
manifest: readonly RegisteredAddonManifest[],
|
|
703
|
-
): Map<string, AppliedCapEntry> => {
|
|
704
|
-
const keys = new Map<string, AppliedCapEntry>()
|
|
705
|
-
for (const addon of manifest) {
|
|
706
|
-
const registryKey = registryKeyFor(addon.addonId)
|
|
707
|
-
for (const capName of addon.capabilities) {
|
|
708
|
-
if (isInfraCapability(capName)) continue
|
|
709
|
-
const capDef = registry.getDefinition(capName)
|
|
710
|
-
if (!capDef) continue
|
|
711
|
-
keys.set(`${registryKey}::${capName}`, { addonId: addon.addonId, capName, capDef })
|
|
712
|
-
}
|
|
713
|
-
}
|
|
714
|
-
return keys
|
|
715
|
-
}
|
|
716
|
-
|
|
717
|
-
const desired = appliedKeys(addons)
|
|
718
|
-
const previous = appliedKeys(previousManifest ?? [])
|
|
719
|
-
|
|
720
|
-
// ── UNREGISTER ── caps the previous manifest applied but the new one
|
|
721
|
-
// does not. Quiet — readiness is driven by `$node.connected/disconnected`,
|
|
722
|
-
// not by a re-handshake, so no readiness events here.
|
|
723
|
-
for (const [key, { addonId, capName }] of previous) {
|
|
724
|
-
if (desired.has(key)) continue
|
|
725
|
-
registry.unregisterProvider(capName, registryKeyFor(addonId))
|
|
726
|
-
this.nodeCallFns.delete(`${nodeId}::${capName}`)
|
|
727
|
-
}
|
|
728
|
-
|
|
729
|
-
// ── REGISTER ── caps the new manifest applies that the previous one
|
|
730
|
-
// did not. Caps present in BOTH sets are left untouched — zero churn,
|
|
731
|
-
// no duplicate `registerProvider`, no spurious page/widget re-emit.
|
|
732
|
-
for (const [key, { addonId, capName, capDef }] of desired) {
|
|
733
|
-
if (previous.has(key)) continue
|
|
734
|
-
|
|
735
|
-
const registryKey = registryKeyFor(addonId)
|
|
736
|
-
|
|
737
|
-
// The runner id (= UDS childId) for a hub-local forked child is the
|
|
738
|
-
// trailing segment of its nodeId `${hubNodeId}/${runnerId}`. Only
|
|
739
|
-
// hub-local children are reachable over UDS; agent-hosted providers
|
|
740
|
-
// (`<agent>/<runner>`) fall through to Moleculer.
|
|
741
|
-
const udsChildId = isLocalChild ? nodeId.slice(hubNodeId.length + 1) : null
|
|
742
|
-
|
|
743
|
-
// Per-(cap,node) dispatcher. Routing lives in the unit-tested
|
|
744
|
-
// `buildCapCallFn` (see cap-call-fn.ts):
|
|
745
|
-
// - hub-local child → per-child UDS (collection-safe; keyed by runner
|
|
746
|
-
// id, never by capName which would collapse a COLLECTION cap onto
|
|
747
|
-
// the first child). Fails fast if the child isn't providing — NEVER
|
|
748
|
-
// a Moleculer fallback, since a hub-local child is not a Moleculer
|
|
749
|
-
// service (a broker call would hang the full discovery timeout).
|
|
750
|
-
// - agent-hosted / remote → the unified `CapRouteResolver`, which
|
|
751
|
-
// classifies an agent node as `agent-child-forward` (hub→agent→UDS
|
|
752
|
-
// child) and a direct remote as `remote-moleculer`. This closes the
|
|
753
|
-
// UDS-migration gap where this dispatcher hand-rolled a `broker.call`
|
|
754
|
-
// to an agent that exposes no Moleculer service for the cap.
|
|
755
|
-
// - resolver not yet built (pre-init) → legacy Moleculer call.
|
|
756
|
-
const callFn: CallFn = buildCapCallFn({
|
|
757
|
-
capName,
|
|
758
|
-
nodeId,
|
|
759
|
-
udsChildId,
|
|
760
|
-
getLocalChildRegistry: () => this.localChildRegistry,
|
|
761
|
-
getResolver: () => this.resolver,
|
|
762
|
-
legacyBrokerCall: (method, methodParams, targetNode) =>
|
|
763
|
-
callWithServiceDiscovery(
|
|
764
|
-
this.brokerSafe,
|
|
765
|
-
addonId,
|
|
766
|
-
capActionName(addonId, capName, method, false),
|
|
767
|
-
serializeTypedArrays(methodParams),
|
|
768
|
-
{ nodeID: targetNode, timeout: 60_000 },
|
|
769
|
-
),
|
|
770
|
-
onUdsRoute: (cap) => {
|
|
771
|
-
if (!this.udsRoutedCaps.has(cap)) {
|
|
772
|
-
this.udsRoutedCaps.add(cap)
|
|
773
|
-
this.logger.info('routing cap over UDS', { meta: { capName: cap } })
|
|
774
|
-
}
|
|
775
|
-
},
|
|
776
|
-
})
|
|
777
|
-
|
|
778
|
-
const proxy: Record<string, unknown> = { id: addonId, nodeId }
|
|
779
|
-
for (const methodName of Object.keys(expandCapMethods(capDef))) {
|
|
780
|
-
proxy[methodName] = (methodParams: unknown) => callFn(methodName, methodParams)
|
|
781
|
-
}
|
|
782
|
-
registry.registerProvider(capName, registryKey, proxy)
|
|
783
|
-
|
|
784
|
-
// Local-first singleton preference (UDS regression fix). A
|
|
785
|
-
// `placement: 'any-node'` singleton (e.g. `pipeline-executor`) can
|
|
786
|
-
// register on BOTH the hub-local forked child and a remote agent.
|
|
787
|
-
// `CapabilityRegistry` keeps the FIRST-registered provider active, so a
|
|
788
|
-
// race could leave the REMOTE agent proxy active — and its callFn routes
|
|
789
|
-
// over Moleculer to a UDS-only agent runner that no longer hosts the
|
|
790
|
-
// Moleculer service ("not found on <agent>"). The hub-local provider is
|
|
791
|
-
// reachable over UDS, so prefer it whenever the current active is absent
|
|
792
|
-
// or remote (`@`-keyed). Never steals from another local provider, so an
|
|
793
|
-
// operator's binding choice (a bare-key local provider) is preserved.
|
|
794
|
-
if (capDef.mode === 'singleton' && isLocalChild) {
|
|
795
|
-
const activeKey = registry.getSingletonAddonId(capName)
|
|
796
|
-
if (activeKey === null || activeKey.includes('@')) {
|
|
797
|
-
registry.setSingletonActiveAddon(capName, registryKey)
|
|
798
|
-
}
|
|
799
|
-
}
|
|
800
|
-
|
|
801
|
-
// Emit AddonPageReady / AddonWidgetReady so the admin-UI sidebar
|
|
802
|
-
// refreshes its page/widget registry for cross-process addons.
|
|
803
|
-
if (capName === 'addon-pages-source') {
|
|
804
|
-
this.eventBus.emit({
|
|
805
|
-
id: randomUUID(),
|
|
806
|
-
timestamp: new Date(),
|
|
807
|
-
source: { type: 'addon', id: addonId },
|
|
808
|
-
category: EventCategory.AddonPageReady,
|
|
809
|
-
data: { addonId, nodeId },
|
|
810
|
-
})
|
|
811
|
-
}
|
|
812
|
-
if (capName === 'addon-widgets-source') {
|
|
813
|
-
this.eventBus.emit({
|
|
814
|
-
id: randomUUID(),
|
|
815
|
-
timestamp: new Date(),
|
|
816
|
-
source: { type: 'addon', id: addonId },
|
|
817
|
-
category: EventCategory.AddonWidgetReady,
|
|
818
|
-
data: { addonId, nodeId },
|
|
819
|
-
})
|
|
820
|
-
}
|
|
821
|
-
|
|
822
|
-
// Store callFn so `callCapabilityOnNode` and `createCapabilityProxy`
|
|
823
|
-
// can reach manifest-registered nodes.
|
|
824
|
-
this.nodeCallFns.set(`${nodeId}::${capName}`, callFn)
|
|
825
|
-
}
|
|
826
|
-
}
|
|
827
|
-
|
|
828
|
-
/**
|
|
829
|
-
* D3: remove a node's manifest from the CapabilityRegistry on disconnect.
|
|
830
|
-
* Unregisters every cap the node's last manifest declared and emits
|
|
831
|
-
* synthetic readiness-down events for each.
|
|
832
|
-
*/
|
|
833
|
-
private removeNodeFromRegistry(nodeId: string): void {
|
|
834
|
-
const manifest = this.nodeRegistry.getNodeManifest(nodeId)
|
|
835
|
-
if (!manifest) return // node never sent a handshake — nothing to do
|
|
836
|
-
|
|
837
|
-
const hubNodeId = this.brokerSafe.nodeID
|
|
838
|
-
const isLocalChild = nodeId.startsWith(hubNodeId + '/')
|
|
839
|
-
const disconnectGen = `disconnect-${nodeId}-${randomUUID()}`
|
|
840
|
-
const agentNodeId = nodeId.includes('/') ? nodeId.split('/')[0]! : nodeId
|
|
841
|
-
|
|
842
|
-
const registry = this.capabilityService.getRegistry()
|
|
843
|
-
|
|
844
|
-
for (const addon of manifest) {
|
|
845
|
-
const { addonId, capabilities } = addon
|
|
846
|
-
const registryKey = isLocalChild ? addonId : `${addonId}@${nodeId}`
|
|
847
|
-
|
|
848
|
-
if (registry) {
|
|
849
|
-
for (const capName of capabilities) {
|
|
850
|
-
registry.unregisterProvider(capName, registryKey)
|
|
851
|
-
}
|
|
852
|
-
}
|
|
853
|
-
|
|
854
|
-
for (const capName of capabilities) {
|
|
855
|
-
this.nodeCallFns.delete(`${nodeId}::${capName}`)
|
|
856
|
-
|
|
857
|
-
try {
|
|
858
|
-
emitReadiness(this.eventBus, {
|
|
859
|
-
capName,
|
|
860
|
-
scope: { type: 'node', nodeId: agentNodeId },
|
|
861
|
-
state: 'down',
|
|
862
|
-
generation: disconnectGen,
|
|
863
|
-
sourceNodeId: hubNodeId,
|
|
864
|
-
})
|
|
865
|
-
} catch (err) {
|
|
866
|
-
this.logger.warn('Failed to emit synthetic readiness down', {
|
|
867
|
-
tags: { addonId, nodeId },
|
|
868
|
-
meta: { capName, err: err instanceof Error ? err.message : String(err) },
|
|
869
|
-
})
|
|
870
|
-
}
|
|
871
|
-
}
|
|
872
|
-
}
|
|
873
|
-
|
|
874
|
-
this.nodeRegistry.removeNode(nodeId)
|
|
875
|
-
}
|
|
876
|
-
|
|
877
|
-
private findCallFn(nodeId: string, capabilityName: string): CallFn | undefined {
|
|
878
|
-
// Exact match first — direct hit when the caller knows the full
|
|
879
|
-
// nodeId (e.g. `hub/detection-pipeline`) or when the cap is hosted
|
|
880
|
-
// on a bare top-level node (remote agent with in-process addons).
|
|
881
|
-
const direct = this.nodeCallFns.get(`${nodeId}::${capabilityName}`)
|
|
882
|
-
if (direct) return direct
|
|
883
|
-
// Prefix fallback: forkable addons register under
|
|
884
|
-
// `<parent>/<processName>` (e.g. `dev-agent-0/detection-pipeline`).
|
|
885
|
-
// UI callers typically pass the bare parent nodeId (`dev-agent-0`)
|
|
886
|
-
// because that's what they get from AgentOnline events and the
|
|
887
|
-
// orchestrator assignments. Resolve by finding any registered node
|
|
888
|
-
// whose id starts with `<nodeId>/` and hosts the cap.
|
|
889
|
-
const prefix = `${nodeId}/`
|
|
890
|
-
for (const key of this.nodeCallFns.keys()) {
|
|
891
|
-
const sep = key.lastIndexOf('::')
|
|
892
|
-
if (sep < 0) continue
|
|
893
|
-
const keyNode = key.slice(0, sep)
|
|
894
|
-
const keyCap = key.slice(sep + 2)
|
|
895
|
-
if (keyCap !== capabilityName) continue
|
|
896
|
-
if (keyNode.startsWith(prefix)) return this.nodeCallFns.get(key)
|
|
897
|
-
}
|
|
898
|
-
return undefined
|
|
899
|
-
}
|
|
900
|
-
|
|
901
|
-
/**
|
|
902
|
-
* Returns true when a (nodeId, capabilityName) pair is reachable via the
|
|
903
|
-
* legacy fallback paths: either a stored callFn in nodeCallFns, or a
|
|
904
|
-
* hub-local forked child that is reachable over UDS (even without a
|
|
905
|
-
* manifest callFn — e.g. device-scoped native caps). Used by
|
|
906
|
-
* createCapabilityProxy to decide whether to build a proxy when the
|
|
907
|
-
* CapRouteResolver cannot find a route.
|
|
908
|
-
*/
|
|
909
|
-
private isReachableViaLegacy(nodeId: string, capabilityName: string): boolean {
|
|
910
|
-
if (this.findCallFn(nodeId, capabilityName) !== undefined) return true
|
|
911
|
-
return this.localChildRegistry !== null && nodeId.startsWith(`${this.brokerSafe.nodeID}/`)
|
|
912
|
-
}
|
|
913
|
-
|
|
914
|
-
/**
|
|
915
|
-
* Build a proxy object that forwards every method call on a capability
|
|
916
|
-
* to the correct transport via CapRouteResolver. Returns null if the
|
|
917
|
-
* capability is provably not reachable on that node (resolver says no-provider
|
|
918
|
-
* AND no legacy callFn exists AND it is not a hub-local child).
|
|
919
|
-
*
|
|
920
|
-
* Used by the generated cap routers when a request includes a `nodeId` field
|
|
921
|
-
* for transparent node routing.
|
|
922
|
-
*
|
|
923
|
-
* Hub-local forked children (e.g. `hub/provider-reolink`) are reachable over
|
|
924
|
-
* UDS even when no manifest callFn exists — device-scoped NATIVE caps (ptz,
|
|
925
|
-
* motion-zones) aren't in `applyNodeManifest`'s callFn store. The proxy is
|
|
926
|
-
* built unconditionally for hub-local children so `callCapabilityOnNode` can
|
|
927
|
-
* route the actual method call via the resolver's hub-local-uds branch.
|
|
928
|
-
*/
|
|
929
|
-
createCapabilityProxy(
|
|
930
|
-
capabilityName: string,
|
|
931
|
-
nodeId: string,
|
|
932
|
-
): Record<string, (params: unknown) => Promise<unknown>> | null {
|
|
933
|
-
const resolver = this.resolver
|
|
934
|
-
if (resolver !== null) {
|
|
935
|
-
// Use the resolver to determine reachability. If it resolves a route, we
|
|
936
|
-
// can build a proxy. If it throws no-provider but a legacy callFn exists
|
|
937
|
-
// or the node is a hub-local child (for native caps), build the proxy anyway
|
|
938
|
-
// because callCapabilityOnNode's fallback will handle it at dispatch time.
|
|
939
|
-
try {
|
|
940
|
-
resolver.resolveCapRoute(capabilityName, { nodeId })
|
|
941
|
-
// Resolver found a route — proxy is reachable.
|
|
942
|
-
} catch (err) {
|
|
943
|
-
if (
|
|
944
|
-
err instanceof CapRouteError &&
|
|
945
|
-
(err.reason === 'no-provider' || err.reason === 'node-offline')
|
|
946
|
-
) {
|
|
947
|
-
// Check legacy callFn store and hub-local child fallbacks.
|
|
948
|
-
if (!this.isReachableViaLegacy(nodeId, capabilityName)) return null
|
|
949
|
-
// Proxy reachable via fallback paths — fall through to build it.
|
|
950
|
-
} else {
|
|
951
|
-
throw err
|
|
952
|
-
}
|
|
953
|
-
}
|
|
954
|
-
} else {
|
|
955
|
-
// Pre-init: use legacy reachability check.
|
|
956
|
-
if (!this.isReachableViaLegacy(nodeId, capabilityName)) return null
|
|
957
|
-
}
|
|
958
|
-
|
|
959
|
-
// Build a dynamic proxy: every property access returns a function that
|
|
960
|
-
// routes the call through callCapabilityOnNode (which delegates to the resolver).
|
|
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
|
-
},
|
|
968
|
-
},
|
|
969
|
-
)
|
|
970
|
-
}
|
|
971
|
-
|
|
972
|
-
/**
|
|
973
|
-
* D3 handshake-fed native-cap view of the whole cluster.
|
|
974
|
-
* Returns every `(nodeId, addonId, capName, deviceId)` tuple stored by
|
|
975
|
-
* `onRegisterNode` — updated atomically each time a node re-handshakes
|
|
976
|
-
* (e.g. after device restore completes). Used by `device-manager.addon.ts`
|
|
977
|
-
* as a reliable fallback when push-based `DeviceBindingsChanged` events
|
|
978
|
-
* were lost in the Moleculer transport handshake window.
|
|
979
|
-
*
|
|
980
|
-
* NOT a Moleculer action — only the hub process calls this directly
|
|
981
|
-
* through the `ctx.kernel.listClusterNativeCaps` injection.
|
|
982
|
-
*/
|
|
983
|
-
listClusterNativeCaps(): readonly import('@camstack/kernel').NodeNativeCapEntry[] {
|
|
984
|
-
return this.nodeRegistry.listNativeCapEntries()
|
|
985
|
-
}
|
|
986
|
-
|
|
987
|
-
/**
|
|
988
|
-
* Per-device slice of {@link listClusterNativeCaps}, served from the
|
|
989
|
-
* registry's `deviceId → entries` index — O(caps-for-device). Used by the
|
|
990
|
-
* per-device `getBindings` hot path so `getAllBindings` doesn't flatten the
|
|
991
|
-
* whole cluster once per device.
|
|
992
|
-
*/
|
|
993
|
-
listClusterNativeCapsForDevice(
|
|
994
|
-
deviceId: number,
|
|
995
|
-
): readonly import('@camstack/kernel').NodeNativeCapEntry[] {
|
|
996
|
-
return this.nodeRegistry.listNativeCapEntriesForDevice(deviceId)
|
|
997
|
-
}
|
|
998
|
-
|
|
999
|
-
/**
|
|
1000
|
-
* E2: Send a `set-log-level` UDS message to a hub-local child identified by
|
|
1001
|
-
* `nodeId` (e.g. `hub/provider-reolink`). Extracts the `childId` from the
|
|
1002
|
-
* nodeId and delegates to `LocalChildRegistry.setChildLogLevel`.
|
|
1003
|
-
*
|
|
1004
|
-
* Returns `true` only if the nodeId is a hub-local child (`hub/<childId}`) AND
|
|
1005
|
-
* the child is currently connected to the LocalChildRegistry (the UDS message
|
|
1006
|
-
* was emitted). Returns `false` when the nodeId is not a hub-local child, the
|
|
1007
|
-
* registry is absent, or the child is not yet/no longer connected — in all
|
|
1008
|
-
* three cases the caller (setProcessLogLevel in cap-providers.ts) falls back
|
|
1009
|
-
* to the Moleculer `$node-mgmt.setLogLevel` action.
|
|
1010
|
-
*/
|
|
1011
|
-
setChildLogLevelByNodeId(nodeId: string, level: string): boolean {
|
|
1012
|
-
const hubNodeId = this.brokerSafe.nodeID
|
|
1013
|
-
if (!nodeId.startsWith(`${hubNodeId}/`)) return false
|
|
1014
|
-
const childId = nodeId.slice(hubNodeId.length + 1)
|
|
1015
|
-
const registry = this.localChildRegistry
|
|
1016
|
-
if (registry === null) return false
|
|
1017
|
-
return registry.setChildLogLevel(childId, level)
|
|
1018
|
-
}
|
|
1019
|
-
|
|
1020
|
-
async onModuleDestroy(): Promise<void> {
|
|
1021
|
-
this.udsEventBridgeDispose?.()
|
|
1022
|
-
this.udsEventBridgeDispose = null
|
|
1023
|
-
await this.brokerSafe.stop()
|
|
1024
|
-
await this.localChildRegistry?.close()
|
|
1025
|
-
}
|
|
1026
|
-
}
|
|
1027
|
-
|
|
1028
|
-
// ---------------------------------------------------------------------------
|
|
1029
|
-
// Module-level helpers
|
|
1030
|
-
// ---------------------------------------------------------------------------
|
|
1031
|
-
|
|
1032
|
-
/**
|
|
1033
|
-
* E1: Adapt a child's UDS `ChildCapDescriptor[]` (which has no `addonId`) into
|
|
1034
|
-
* a `RegisterNodeParams` that `onRegisterNode` / `applyNodeManifest` can consume.
|
|
1035
|
-
*
|
|
1036
|
-
* Strategy: use `childId` as the synthetic `addonId`. For currently-shipped addons
|
|
1037
|
-
* `childId = runnerId = addonId` (one-addon-one-process, no shared group), so the
|
|
1038
|
-
* `registryKey = childId` produced here matches what the Moleculer `$hub.registerNode`
|
|
1039
|
-
* path uses — making double-apply via both paths fully idempotent.
|
|
1040
|
-
*
|
|
1041
|
-
* Singleton vs. device-scoped caps: `ChildCapDescriptor.deviceId` is present only
|
|
1042
|
-
* for device-scoped native caps. The `addons` array carries system (singleton/collection)
|
|
1043
|
-
* cap names; device-scoped native caps are handled separately via `nativeCaps` in the
|
|
1044
|
-
* full `RegisterNodeParams`. For the parallel-window phase (E1), we populate only
|
|
1045
|
-
* the `addons` portion — the Moleculer path carries `nativeCaps` on the re-handshake.
|
|
1046
|
-
*
|
|
1047
|
-
* TODO(co-location): This function synthesises ONE manifest entry with `addonId = childId`
|
|
1048
|
-
* (the runner id). This is correct under the current one-addon-one-process invariant
|
|
1049
|
-
* where `childId = runnerId = addonId`. If `execution.group` co-location is ever
|
|
1050
|
-
* activated (multiple addons sharing one runner), a single runner would host multiple
|
|
1051
|
-
* addonIds but this function would register all their caps under one synthetic addonId —
|
|
1052
|
-
* collapsing distinct provider registryKeys into one and breaking per-addon routing.
|
|
1053
|
-
* Multi-addon manifest support (splitting the `ChildCapDescriptor[]` by addonId once the
|
|
1054
|
-
* protocol carries addonId) would be needed here before enabling co-location post-Phase-F.
|
|
1055
|
-
*/
|
|
1056
|
-
export function buildChildUdsManifest(
|
|
1057
|
-
nodeId: string,
|
|
1058
|
-
childId: string,
|
|
1059
|
-
caps: readonly ChildCapDescriptor[],
|
|
1060
|
-
): RegisterNodeParams {
|
|
1061
|
-
// Collect unique system (non-device-scoped) cap names.
|
|
1062
|
-
const systemCapNames = new Set<string>()
|
|
1063
|
-
for (const cap of caps) {
|
|
1064
|
-
if (cap.deviceId === undefined) {
|
|
1065
|
-
systemCapNames.add(cap.capName)
|
|
1066
|
-
}
|
|
1067
|
-
}
|
|
1068
|
-
const addons: readonly RegisteredAddonManifest[] = [
|
|
1069
|
-
{ addonId: childId, capabilities: [...systemCapNames] },
|
|
1070
|
-
]
|
|
1071
|
-
return { nodeId, addons }
|
|
1072
|
-
}
|