@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.
- package/package.json +9 -7
- package/src/__tests__/addon-install-e2e.test.ts +0 -1
- package/src/__tests__/addon-pages-e2e.test.ts +40 -18
- package/src/__tests__/addon-settings-router.spec.ts +6 -1
- package/src/__tests__/addon-upload.spec.ts +91 -29
- package/src/__tests__/agent-registry.spec.ts +26 -9
- package/src/__tests__/agent-status-page.spec.ts +1 -3
- package/src/__tests__/auth-session-cookie.test.ts +28 -1
- package/src/__tests__/bulk-update-coordinator.spec.ts +48 -31
- package/src/__tests__/cap-ownership-authority.spec.ts +39 -8
- package/src/__tests__/cap-providers/cap-providers-location-import.spec.ts +24 -4
- package/src/__tests__/cap-providers/cap-usage-graph.spec.ts +17 -3
- package/src/__tests__/cap-providers/compute-topology-categories.spec.ts +57 -11
- package/src/__tests__/cap-providers/integrations-delete-cascade.spec.ts +64 -15
- package/src/__tests__/cap-providers-bulk-update.spec.ts +27 -7
- package/src/__tests__/cap-route-adapter.spec.ts +28 -15
- package/src/__tests__/cap-routers/_meta.spec.ts +6 -7
- package/src/__tests__/cap-routers/addon-settings.router.spec.ts +19 -10
- package/src/__tests__/cap-routers/broker-routing.router.spec.ts +14 -6
- package/src/__tests__/cap-routers/cap-route-error-formatter.spec.ts +3 -1
- package/src/__tests__/cap-routers/capabilities-node.spec.ts +18 -5
- package/src/__tests__/cap-routers/device-link-overlay.spec.ts +11 -6
- package/src/__tests__/cap-routers/device-manager-aggregate.router.spec.ts +72 -20
- package/src/__tests__/cap-routers/harness.ts +11 -7
- package/src/__tests__/cap-routers/metrics-provider.router.spec.ts +17 -3
- package/src/__tests__/cap-routers/null-provider-guard.spec.ts +5 -7
- package/src/__tests__/cap-routers/pipeline-executor.router.spec.ts +35 -11
- package/src/__tests__/cap-routers/settings-store.router.spec.ts +59 -15
- package/src/__tests__/capability-e2e.test.ts +9 -11
- package/src/__tests__/cli-e2e.test.ts +80 -59
- package/src/__tests__/core-cap-bridge.spec.ts +3 -1
- package/src/__tests__/dev-bootstrap-shm-ring.spec.ts +12 -2
- package/src/__tests__/device-settings-contribution-dispatch.spec.ts +61 -30
- package/src/__tests__/embedded-deps-e2e.test.ts +35 -19
- package/src/__tests__/event-bus-proxy-router.spec.ts +3 -0
- package/src/__tests__/framework-allowlist.spec.ts +5 -4
- package/src/__tests__/https-e2e.test.ts +12 -6
- package/src/__tests__/lifecycle-e2e.test.ts +60 -11
- package/src/__tests__/live-events-subscription.spec.ts +17 -18
- package/src/__tests__/moleculer/uds-readiness.spec.ts +11 -4
- package/src/__tests__/moleculer/uds-topology.spec.ts +39 -11
- package/src/__tests__/moleculer/uds-unowned-call.spec.ts +71 -17
- package/src/__tests__/moleculer-register-node-idempotency.spec.ts +16 -7
- package/src/__tests__/native-cap-route.spec.ts +42 -19
- package/src/__tests__/oauth2-account-linking.spec.ts +63 -17
- package/src/__tests__/singleton-contention.test.ts +23 -11
- package/src/__tests__/streaming-diagnostic.test.ts +156 -53
- package/src/__tests__/streaming-scale.test.ts +69 -35
- package/src/__tests__/uds-addon-call-wiring.spec.ts +6 -1
- package/src/agent-status-page.ts +4 -3
- package/src/api/__tests__/addons-custom.spec.ts +22 -8
- package/src/api/__tests__/capabilities.router.test.ts +18 -9
- package/src/api/addon-upload.ts +46 -15
- package/src/api/addons-custom.router.ts +7 -6
- package/src/api/auth-whoami.ts +3 -1
- package/src/api/bridge-addons.router.ts +3 -1
- package/src/api/capabilities.router.ts +117 -78
- package/src/api/core/__tests__/auth-router-totp.spec.ts +57 -16
- package/src/api/core/addon-settings.router.ts +4 -1
- package/src/api/core/agents.router.ts +52 -53
- package/src/api/core/auth.router.ts +55 -36
- package/src/api/core/bulk-update-coordinator.ts +25 -22
- package/src/api/core/cap-providers.ts +346 -202
- package/src/api/core/capabilities.router.ts +30 -23
- package/src/api/core/hwaccel.router.ts +37 -10
- package/src/api/core/live-events.router.ts +16 -9
- package/src/api/core/logs.router.ts +54 -25
- package/src/api/core/notifications.router.ts +2 -1
- package/src/api/core/repl.router.ts +1 -3
- package/src/api/core/settings-backend.router.ts +68 -70
- package/src/api/core/system-events.router.ts +41 -32
- package/src/api/health/health.routes.ts +7 -13
- package/src/api/oauth2/__tests__/oauth2-routes.spec.ts +12 -2
- package/src/api/oauth2/consent-page.ts +4 -3
- package/src/api/oauth2/oauth2-routes.ts +41 -12
- package/src/api/trpc/__tests__/scope-access-device.spec.ts +68 -23
- package/src/api/trpc/__tests__/scope-access.spec.ts +8 -13
- package/src/api/trpc/__tests__/webrtc-session-ua-enrich.spec.ts +10 -2
- package/src/api/trpc/cap-mount-helpers.ts +64 -55
- package/src/api/trpc/cap-route-error-formatter.ts +17 -9
- package/src/api/trpc/core-cap-bridge.ts +3 -1
- package/src/api/trpc/generated-cap-mounts.ts +593 -351
- package/src/api/trpc/generated-cap-routers.ts +3680 -579
- package/src/api/trpc/scope-access.ts +7 -7
- package/src/api/trpc/trpc.context.ts +7 -4
- package/src/api/trpc/trpc.middleware.ts +4 -2
- package/src/api/trpc/trpc.router.ts +79 -46
- package/src/auth/session-cookie.ts +10 -0
- package/src/boot/__tests__/integration-id-backfill.spec.ts +21 -6
- package/src/boot/boot-config.ts +103 -122
- package/src/boot/post-boot.service.ts +5 -3
- package/src/core/addon/__tests__/addon-registry-capability.test.ts +12 -3
- package/src/core/addon/addon-call-gateway.ts +20 -6
- package/src/core/addon/addon-package.service.ts +183 -89
- package/src/core/addon/addon-registry.service.ts +1163 -1305
- package/src/core/addon/addon-search.service.ts +2 -1
- package/src/core/addon/addon-settings-provider.ts +27 -7
- package/src/core/addon-bridge/addon-bridge.service.ts +11 -6
- package/src/core/addon-pages/addon-pages.service.ts +3 -1
- package/src/core/addon-widgets/addon-widgets.service.ts +5 -2
- package/src/core/agent/agent-registry.service.ts +60 -38
- package/src/core/auth/auth.service.spec.ts +6 -8
- package/src/core/config/config.service.spec.ts +1 -1
- package/src/core/events/event-bus.service.spec.ts +44 -21
- package/src/core/events/event-bus.service.ts +5 -1
- package/src/core/feature/feature.service.spec.ts +4 -1
- package/src/core/lifecycle/lifecycle-state-machine.spec.ts +8 -10
- package/src/core/logging/logging.service.spec.ts +61 -21
- package/src/core/logging/logging.service.ts +12 -3
- package/src/core/moleculer/cap-call-fn.spec.ts +17 -10
- package/src/core/moleculer/cap-call-fn.ts +5 -1
- package/src/core/moleculer/cap-route-authority.ts +18 -6
- package/src/core/moleculer/moleculer.service.ts +120 -32
- package/src/core/network/network-quality.service.spec.ts +6 -1
- package/src/core/notification/notification-wrapper.service.ts +1 -3
- package/src/core/notification/toast-wrapper.service.ts +1 -5
- package/src/core/repl/repl-engine.service.spec.ts +66 -39
- package/src/core/repl/repl-engine.service.ts +11 -12
- package/src/core/storage/storage-location-manager.spec.ts +12 -3
- package/src/core/streaming/stream-probe.service.ts +22 -13
- package/src/core/topology/topology-emitter.service.ts +5 -1
- package/src/launcher.ts +14 -9
- package/src/main.ts +602 -531
- package/src/manual-boot.ts +133 -154
- 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 (
|
|
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: {
|
|
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
|
-
...
|
|
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
|
-
{
|
|
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([
|
|
129
|
-
|
|
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([
|
|
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 () => ({
|
|
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: (
|
|
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(
|
|
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 {
|
|
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 (
|
|
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(
|
|
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
|
-
|
|
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 {
|
|
20
|
-
|
|
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 {
|
|
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 =
|
|
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
|
|
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) =>
|
|
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) =>
|
|
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 (
|
|
281
|
-
//
|
|
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', {
|
|
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(
|
|
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(
|
|
367
|
-
|
|
368
|
-
|
|
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) ??
|
|
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 =
|
|
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 (
|
|
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')
|
|
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: [
|
|
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(
|
|
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 = (
|
|
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
|
|
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(
|
|
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 (
|
|
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
|
-
|
|
880
|
-
|
|
881
|
-
|
|
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(
|
|
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, {
|
|
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
|
}
|