@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,143 +0,0 @@
|
|
|
1
|
-
import { LogManager } from '@camstack/core'
|
|
2
|
-
import type { IScopedLogger, LogTags } from '@camstack/types'
|
|
3
|
-
import { EventCategory } from '@camstack/types'
|
|
4
|
-
import { ConfigService } from '../config/config.service'
|
|
5
|
-
import type { EventBusService } from '../events/event-bus.service'
|
|
6
|
-
|
|
7
|
-
/** Baseline tags every hub-local logger gets, so console output never
|
|
8
|
-
* falls back to `?` for the agent slot. Hub-local addons already
|
|
9
|
-
* re-tag with their addonId; this ensures non-addon scopes (core
|
|
10
|
-
* services like `AddonRegistry`, `StreamProbeService`, `moleculer`)
|
|
11
|
-
* render as `hub` in the agent column.
|
|
12
|
-
*
|
|
13
|
-
* `pid` is the hub renderer's own OS pid. Entries forwarded from
|
|
14
|
-
* forked workers via `$hub.log` carry their own `tags.pid` which
|
|
15
|
-
* wins over this baseline (right-biased merge in `writeFromWorker`). */
|
|
16
|
-
const HUB_BASELINE_TAGS: LogTags = { agentId: 'hub', nodeId: 'hub', pid: process.pid }
|
|
17
|
-
|
|
18
|
-
export class LoggingService extends LogManager {
|
|
19
|
-
/**
|
|
20
|
-
* Device-name cache consulted by the formatter when a log line
|
|
21
|
-
* carries `tags.deviceId` but no explicit `deviceName`. Populated
|
|
22
|
-
* by `setDeviceNames` whenever device-manager emits registered /
|
|
23
|
-
* updated / removed events. Missing entries fall back to `#<id>`.
|
|
24
|
-
*/
|
|
25
|
-
private readonly deviceNames = new Map<number, string>()
|
|
26
|
-
|
|
27
|
-
constructor(configService: ConfigService) {
|
|
28
|
-
// The log buffer is now partitioned per addonId (see PartitionedLogBuffer):
|
|
29
|
-
// this value caps EACH addon's bucket, not the total. A chatty addon evicts
|
|
30
|
-
// only its own lines, so quiet addons (e.g. a HomeAssistant `image` entity)
|
|
31
|
-
// keep their sparse history. `eventBus.ringBufferSize` still sizes the
|
|
32
|
-
// separate system-event ring; logs get their own per-addon cap.
|
|
33
|
-
const perAddonCapacity = configService.get<number>('eventBus.perAddonLogBufferSize') ?? 5000
|
|
34
|
-
super(perAddonCapacity)
|
|
35
|
-
// Enriches every emitted LogEntry with `tags.deviceName` before
|
|
36
|
-
// destinations / subscribers see it — works across bundled copies
|
|
37
|
-
// of `@camstack/core` (addon packages) because the mutation
|
|
38
|
-
// happens upstream of their formatters.
|
|
39
|
-
this.setDeviceNameLookup((id) => this.deviceNames.get(id) ?? null)
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/** Bulk-refresh from device-manager snapshot. Replaces every entry. */
|
|
43
|
-
setDeviceNames(entries: ReadonlyArray<{ id: number; name: string }>): void {
|
|
44
|
-
this.deviceNames.clear()
|
|
45
|
-
for (const { id, name } of entries) {
|
|
46
|
-
if (
|
|
47
|
-
typeof id === 'number' &&
|
|
48
|
-
Number.isFinite(id) &&
|
|
49
|
-
typeof name === 'string' &&
|
|
50
|
-
name.length > 0
|
|
51
|
-
) {
|
|
52
|
-
this.deviceNames.set(id, name)
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/** Incremental update — called from DeviceRegistered / DeviceUpdated. */
|
|
58
|
-
upsertDeviceName(id: number, name: string | undefined): void {
|
|
59
|
-
if (!Number.isFinite(id)) return
|
|
60
|
-
if (typeof name === 'string' && name.length > 0) this.deviceNames.set(id, name)
|
|
61
|
-
else this.deviceNames.delete(id)
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/** Drop on DeviceRemoved. */
|
|
65
|
-
removeDeviceName(id: number): void {
|
|
66
|
-
this.deviceNames.delete(id)
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* Subscribe to `device.*` events so the cache stays live: every
|
|
71
|
-
* DeviceRegistered / updated emit carries `{deviceId, name}` — we
|
|
72
|
-
* upsert, and DeviceUnregistered clears. Call once at boot.
|
|
73
|
-
*/
|
|
74
|
-
attachDeviceNameStream(eventBus: EventBusService): void {
|
|
75
|
-
const selfLogger = this.createLogger('logging').withTags({ addonId: 'logging' })
|
|
76
|
-
eventBus.subscribe({ category: EventCategory.DeviceRegistered }, (event) => {
|
|
77
|
-
const data = event.data as { deviceId?: number; name?: string } | undefined
|
|
78
|
-
if (data && typeof data.deviceId === 'number') {
|
|
79
|
-
this.upsertDeviceName(data.deviceId, data.name)
|
|
80
|
-
selfLogger.info('device-name cache upserted', {
|
|
81
|
-
meta: {
|
|
82
|
-
deviceId: data.deviceId,
|
|
83
|
-
name: data.name ?? null,
|
|
84
|
-
cacheSize: this.deviceNames.size,
|
|
85
|
-
},
|
|
86
|
-
})
|
|
87
|
-
}
|
|
88
|
-
})
|
|
89
|
-
eventBus.subscribe({ category: EventCategory.DeviceUnregistered }, (event) => {
|
|
90
|
-
const data = event.data as { deviceId?: number } | undefined
|
|
91
|
-
if (data && typeof data.deviceId === 'number') this.removeDeviceName(data.deviceId)
|
|
92
|
-
})
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
* Override — every logger created hub-side comes pre-tagged with
|
|
97
|
-
* `agentId: 'hub'`. Callers that need per-addon identity chain
|
|
98
|
-
* `.withTags({ addonId })` on top (the merge is right-biased, so
|
|
99
|
-
* the explicit tag wins).
|
|
100
|
-
*/
|
|
101
|
-
override createLogger(scope?: string): IScopedLogger {
|
|
102
|
-
return super.createLogger(scope).withTags(HUB_BASELINE_TAGS)
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/**
|
|
106
|
-
* Write a log entry received from a forked worker / remote agent.
|
|
107
|
-
* Called by the `$hub.log` action and `log-receiver.ingest` action.
|
|
108
|
-
*
|
|
109
|
-
* Tags propagated by the worker (including `deviceId`, `deviceName`,
|
|
110
|
-
* `integrationId`, `streamId`, etc.) are preserved via `withTags`;
|
|
111
|
-
* baseline tags (`addonId`, `nodeId`, `agentId`) are always ensured
|
|
112
|
-
* even if the worker didn't set them explicitly.
|
|
113
|
-
*/
|
|
114
|
-
writeFromWorker(entry: {
|
|
115
|
-
addonId: string
|
|
116
|
-
nodeId?: string
|
|
117
|
-
level: string
|
|
118
|
-
message: string
|
|
119
|
-
scope?: string
|
|
120
|
-
tags?: LogTags
|
|
121
|
-
meta?: Record<string, unknown>
|
|
122
|
-
}): void {
|
|
123
|
-
// Scope is a single optional sub-component label; addon/node identity
|
|
124
|
-
// lives in tags, not in scope. Pass through whatever the worker set.
|
|
125
|
-
let logger = this.createLogger(entry.scope)
|
|
126
|
-
const nodeId = entry.nodeId
|
|
127
|
-
const agentId = nodeId?.includes('/') ? nodeId.split('/')[0]! : nodeId
|
|
128
|
-
const mergedTags: LogTags = {
|
|
129
|
-
...entry.tags,
|
|
130
|
-
addonId: entry.addonId,
|
|
131
|
-
...(nodeId !== undefined ? { nodeId } : {}),
|
|
132
|
-
...(agentId !== undefined ? { agentId } : {}),
|
|
133
|
-
}
|
|
134
|
-
logger = logger.withTags(mergedTags)
|
|
135
|
-
const level = entry.level as 'info' | 'warn' | 'error' | 'debug'
|
|
136
|
-
const extras = entry.meta !== undefined ? { meta: entry.meta } : undefined
|
|
137
|
-
if (typeof logger[level] === 'function') {
|
|
138
|
-
logger[level](entry.message, extras)
|
|
139
|
-
} else {
|
|
140
|
-
logger.info(entry.message, extras)
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
}
|
|
@@ -1,173 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi } from 'vitest'
|
|
2
|
-
import type { CapRoute, CapCallInput } from '@camstack/kernel'
|
|
3
|
-
import { buildCapCallFn, type CapCallFnLocalChild, type CapCallFnResolver } from './cap-call-fn.js'
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* buildCapCallFn — the per-(cap,node) dispatcher behind every CapabilityRegistry
|
|
7
|
-
* provider proxy. These specs lock every routing branch so the UDS-migration
|
|
8
|
-
* gap (an agent-hosted provider's cap call falling onto a `broker.call` to a
|
|
9
|
-
* Moleculer node that does not exist → 30s `waitForServices` timeout) cannot
|
|
10
|
-
* regress:
|
|
11
|
-
* - hub-local child that provides → per-child UDS (collection-safe)
|
|
12
|
-
* - hub-local child that does NOT provide → fail fast (NEVER Moleculer)
|
|
13
|
-
* - agent-hosted / remote → delegate to the unified CapRouteResolver
|
|
14
|
-
* - resolver not built yet → legacy broker fallback
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
const REMOTE_ROUTE: CapRoute = { kind: 'remote-moleculer', capName: 'cap-x', nodeId: 'dev-agent-0' }
|
|
18
|
-
|
|
19
|
-
function recordingLocalChild(provides: boolean): {
|
|
20
|
-
fake: CapCallFnLocalChild
|
|
21
|
-
childProvidesCalls: Array<{ childId: string; capName: string; deviceId?: number }>
|
|
22
|
-
callCapOnChildCalls: Array<{ childId: string; input: CapCallInput }>
|
|
23
|
-
} {
|
|
24
|
-
const childProvidesCalls: Array<{ childId: string; capName: string; deviceId?: number }> = []
|
|
25
|
-
const callCapOnChildCalls: Array<{ childId: string; input: CapCallInput }> = []
|
|
26
|
-
return {
|
|
27
|
-
childProvidesCalls,
|
|
28
|
-
callCapOnChildCalls,
|
|
29
|
-
fake: {
|
|
30
|
-
childProvides: (childId, capName, deviceId) => {
|
|
31
|
-
childProvidesCalls.push({ childId, capName, deviceId })
|
|
32
|
-
return provides
|
|
33
|
-
},
|
|
34
|
-
callCapOnChild: async (childId, input) => {
|
|
35
|
-
callCapOnChildCalls.push({ childId, input })
|
|
36
|
-
return { from: 'uds' }
|
|
37
|
-
},
|
|
38
|
-
},
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function recordingResolver(): {
|
|
43
|
-
fake: CapCallFnResolver
|
|
44
|
-
resolveCalls: Array<{ capName: string; nodeId?: string; deviceId?: number }>
|
|
45
|
-
dispatchCalls: Array<{ route: CapRoute; method: string; args: unknown }>
|
|
46
|
-
} {
|
|
47
|
-
const resolveCalls: Array<{ capName: string; nodeId?: string; deviceId?: number }> = []
|
|
48
|
-
const dispatchCalls: Array<{ route: CapRoute; method: string; args: unknown }> = []
|
|
49
|
-
return {
|
|
50
|
-
resolveCalls,
|
|
51
|
-
dispatchCalls,
|
|
52
|
-
fake: {
|
|
53
|
-
resolveCapRoute: (capName, opts) => {
|
|
54
|
-
resolveCalls.push({ capName, nodeId: opts.nodeId, deviceId: opts.deviceId })
|
|
55
|
-
return REMOTE_ROUTE
|
|
56
|
-
},
|
|
57
|
-
dispatch: async (route, method, args) => {
|
|
58
|
-
dispatchCalls.push({ route, method, args })
|
|
59
|
-
return { from: 'resolver' }
|
|
60
|
-
},
|
|
61
|
-
},
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
describe('buildCapCallFn', () => {
|
|
66
|
-
it('hub-local child that provides the cap → routes per-child over UDS', async () => {
|
|
67
|
-
const child = recordingLocalChild(true)
|
|
68
|
-
const resolver = recordingResolver()
|
|
69
|
-
const legacy = vi.fn<(m: string, p: unknown, n: string) => Promise<unknown>>()
|
|
70
|
-
const fn = buildCapCallFn({
|
|
71
|
-
capName: 'cap-x',
|
|
72
|
-
nodeId: 'hub/benchmark',
|
|
73
|
-
udsChildId: 'benchmark',
|
|
74
|
-
getLocalChildRegistry: () => child.fake,
|
|
75
|
-
getResolver: () => resolver.fake,
|
|
76
|
-
legacyBrokerCall: legacy,
|
|
77
|
-
})
|
|
78
|
-
|
|
79
|
-
const result = await fn('listPages', { deviceId: 7 })
|
|
80
|
-
|
|
81
|
-
expect(result).toEqual({ from: 'uds' })
|
|
82
|
-
expect(child.callCapOnChildCalls).toEqual([
|
|
83
|
-
{
|
|
84
|
-
childId: 'benchmark',
|
|
85
|
-
input: { capName: 'cap-x', method: 'listPages', args: { deviceId: 7 }, deviceId: 7 },
|
|
86
|
-
},
|
|
87
|
-
])
|
|
88
|
-
expect(resolver.resolveCalls).toEqual([]) // resolver never consulted
|
|
89
|
-
expect(legacy).not.toHaveBeenCalled()
|
|
90
|
-
})
|
|
91
|
-
|
|
92
|
-
it('hub-local child that does NOT provide → fails fast, never touches Moleculer', async () => {
|
|
93
|
-
const child = recordingLocalChild(false)
|
|
94
|
-
const resolver = recordingResolver()
|
|
95
|
-
const legacy = vi.fn<(m: string, p: unknown, n: string) => Promise<unknown>>()
|
|
96
|
-
const fn = buildCapCallFn({
|
|
97
|
-
capName: 'cap-x',
|
|
98
|
-
nodeId: 'hub/benchmark',
|
|
99
|
-
udsChildId: 'benchmark',
|
|
100
|
-
getLocalChildRegistry: () => child.fake,
|
|
101
|
-
getResolver: () => resolver.fake,
|
|
102
|
-
legacyBrokerCall: legacy,
|
|
103
|
-
})
|
|
104
|
-
|
|
105
|
-
await expect(fn('listPages', undefined)).rejects.toThrow(/does not currently provide/)
|
|
106
|
-
expect(child.callCapOnChildCalls).toEqual([])
|
|
107
|
-
expect(resolver.resolveCalls).toEqual([])
|
|
108
|
-
expect(resolver.dispatchCalls).toEqual([])
|
|
109
|
-
expect(legacy).not.toHaveBeenCalled()
|
|
110
|
-
})
|
|
111
|
-
|
|
112
|
-
it('agent-hosted provider → delegates to the resolver (agent-child-forward)', async () => {
|
|
113
|
-
const resolver = recordingResolver()
|
|
114
|
-
const legacy = vi.fn<(m: string, p: unknown, n: string) => Promise<unknown>>()
|
|
115
|
-
const fn = buildCapCallFn({
|
|
116
|
-
capName: 'cap-x',
|
|
117
|
-
nodeId: 'dev-agent-0',
|
|
118
|
-
udsChildId: null, // not hub-local
|
|
119
|
-
getLocalChildRegistry: () => null,
|
|
120
|
-
getResolver: () => resolver.fake,
|
|
121
|
-
legacyBrokerCall: legacy,
|
|
122
|
-
})
|
|
123
|
-
|
|
124
|
-
const result = await fn('listPages', { deviceId: 3 })
|
|
125
|
-
|
|
126
|
-
expect(result).toEqual({ from: 'resolver' })
|
|
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
|
-
])
|
|
133
|
-
expect(legacy).not.toHaveBeenCalled()
|
|
134
|
-
})
|
|
135
|
-
|
|
136
|
-
it('explicit targetNodeId overrides the registered nodeId in resolution', async () => {
|
|
137
|
-
const resolver = recordingResolver()
|
|
138
|
-
const legacy = vi.fn<(m: string, p: unknown, n: string) => Promise<unknown>>()
|
|
139
|
-
const fn = buildCapCallFn({
|
|
140
|
-
capName: 'cap-x',
|
|
141
|
-
nodeId: 'dev-agent-0',
|
|
142
|
-
udsChildId: null,
|
|
143
|
-
getLocalChildRegistry: () => null,
|
|
144
|
-
getResolver: () => resolver.fake,
|
|
145
|
-
legacyBrokerCall: legacy,
|
|
146
|
-
})
|
|
147
|
-
|
|
148
|
-
await fn('listPages', undefined, 'dev-agent-1')
|
|
149
|
-
|
|
150
|
-
expect(resolver.resolveCalls).toEqual([
|
|
151
|
-
{ capName: 'cap-x', nodeId: 'dev-agent-1', deviceId: undefined },
|
|
152
|
-
])
|
|
153
|
-
})
|
|
154
|
-
|
|
155
|
-
it('resolver not yet built → falls back to the legacy broker call', async () => {
|
|
156
|
-
const legacy = vi.fn<(m: string, p: unknown, n: string) => Promise<unknown>>(async () => ({
|
|
157
|
-
from: 'legacy',
|
|
158
|
-
}))
|
|
159
|
-
const fn = buildCapCallFn({
|
|
160
|
-
capName: 'cap-x',
|
|
161
|
-
nodeId: 'dev-agent-0',
|
|
162
|
-
udsChildId: null,
|
|
163
|
-
getLocalChildRegistry: () => null,
|
|
164
|
-
getResolver: () => null, // pre-init window
|
|
165
|
-
legacyBrokerCall: legacy,
|
|
166
|
-
})
|
|
167
|
-
|
|
168
|
-
const result = await fn('listPages', { deviceId: 1 })
|
|
169
|
-
|
|
170
|
-
expect(result).toEqual({ from: 'legacy' })
|
|
171
|
-
expect(legacy).toHaveBeenCalledWith('listPages', { deviceId: 1 }, 'dev-agent-0')
|
|
172
|
-
})
|
|
173
|
-
})
|
|
@@ -1,107 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* cap-call-fn — the per-(cap, node) dispatcher behind every CapabilityRegistry
|
|
3
|
-
* provider proxy the hub builds in `applyNodeManifest`.
|
|
4
|
-
*
|
|
5
|
-
* Extracted from `MoleculerService` so its routing branches are unit-testable
|
|
6
|
-
* in isolation (the closure that lived inline could only be exercised by
|
|
7
|
-
* standing up a full broker). It closes a UDS-migration gap: the inline
|
|
8
|
-
* version hand-rolled a `broker.call` for any non-hub-local provider, so an
|
|
9
|
-
* AGENT-hosted addon cap (a UDS child of the agent, NOT a Moleculer service)
|
|
10
|
-
* resolved to a Moleculer node that does not exist → `waitForServices` waited
|
|
11
|
-
* its full 30s discovery timeout → "Services waiting is timed out". The fix
|
|
12
|
-
* routes everything that isn't a hub-local child through the unified
|
|
13
|
-
* `CapRouteResolver`, which classifies an agent node as `agent-child-forward`
|
|
14
|
-
* (hub → agent over Moleculer → agent's UDS child) and a direct remote as
|
|
15
|
-
* `remote-moleculer`.
|
|
16
|
-
*/
|
|
17
|
-
import type { CallFn, CapRoute, CapRouteOpts, CapCallInput } from '@camstack/kernel'
|
|
18
|
-
|
|
19
|
-
/** Minimal LocalChildRegistry surface this dispatcher needs (per-child UDS). */
|
|
20
|
-
export interface CapCallFnLocalChild {
|
|
21
|
-
childProvides(childId: string, capName: string, deviceId?: number): boolean
|
|
22
|
-
callCapOnChild(childId: string, input: CapCallInput): Promise<unknown>
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/** Minimal CapRouteResolver surface this dispatcher needs. */
|
|
26
|
-
export interface CapCallFnResolver {
|
|
27
|
-
resolveCapRoute(capName: string, opts: CapRouteOpts): CapRoute
|
|
28
|
-
dispatch(route: CapRoute, method: string, args: unknown): Promise<unknown>
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export interface CapCallFnDeps {
|
|
32
|
-
/** The capability this dispatcher routes. */
|
|
33
|
-
readonly capName: string
|
|
34
|
-
/** The registering node's id (the agent node for agent-hosted providers). */
|
|
35
|
-
readonly nodeId: string
|
|
36
|
-
/**
|
|
37
|
-
* Runner id of the hub-local UDS child that owns this provider, or `null`
|
|
38
|
-
* for agent-hosted / remote providers. Only hub-local children are reachable
|
|
39
|
-
* over UDS.
|
|
40
|
-
*/
|
|
41
|
-
readonly udsChildId: string | null
|
|
42
|
-
/** Live getter for the UDS child registry (`null` if the UDS server is down). */
|
|
43
|
-
readonly getLocalChildRegistry: () => CapCallFnLocalChild | null
|
|
44
|
-
/** Live getter for the resolver (`null` before `onModuleInit` builds it). */
|
|
45
|
-
readonly getResolver: () => CapCallFnResolver | null
|
|
46
|
-
/** Legacy Moleculer call — used ONLY in the pre-init window (no resolver yet). */
|
|
47
|
-
readonly legacyBrokerCall: (
|
|
48
|
-
method: string,
|
|
49
|
-
params: unknown,
|
|
50
|
-
targetNodeId: string,
|
|
51
|
-
) => Promise<unknown>
|
|
52
|
-
/** Optional diagnostic hook fired the first time this cap routes over UDS. */
|
|
53
|
-
readonly onUdsRoute?: (capName: string) => void
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/** Extract a numeric `deviceId` routing hint out of arbitrary method args. */
|
|
57
|
-
function extractDeviceId(params: unknown): number | undefined {
|
|
58
|
-
if (params === null || typeof params !== 'object') return undefined
|
|
59
|
-
const raw: unknown = Reflect.get(params, 'deviceId')
|
|
60
|
-
return typeof raw === 'number' ? raw : undefined
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* Build the `CallFn` (`(method, params, targetNodeId?) => Promise`) for one
|
|
65
|
-
* registered provider. See module docs for the routing rationale.
|
|
66
|
-
*/
|
|
67
|
-
export function buildCapCallFn(deps: CapCallFnDeps): CallFn {
|
|
68
|
-
return async (method: string, params: unknown, targetNodeId?: string): Promise<unknown> => {
|
|
69
|
-
const deviceId = extractDeviceId(params)
|
|
70
|
-
|
|
71
|
-
// ── Hub-local child: route per-child over UDS (collection-safe — keyed by
|
|
72
|
-
// the specific child, not by capName which would collapse a collection
|
|
73
|
-
// cap onto the first child). NEVER fall back to Moleculer: a hub-local
|
|
74
|
-
// child is not a Moleculer service, so a broker call would wait the full
|
|
75
|
-
// discovery timeout for a node that never appears. If the child isn't
|
|
76
|
-
// currently providing the cap, fail fast.
|
|
77
|
-
if (deps.udsChildId !== null && targetNodeId === undefined) {
|
|
78
|
-
const registry = deps.getLocalChildRegistry()
|
|
79
|
-
if (registry !== null && registry.childProvides(deps.udsChildId, deps.capName, deviceId)) {
|
|
80
|
-
deps.onUdsRoute?.(deps.capName)
|
|
81
|
-
const input: CapCallInput = {
|
|
82
|
-
capName: deps.capName,
|
|
83
|
-
method,
|
|
84
|
-
args: params,
|
|
85
|
-
...(deviceId !== undefined ? { deviceId } : {}),
|
|
86
|
-
}
|
|
87
|
-
return registry.callCapOnChild(deps.udsChildId, input)
|
|
88
|
-
}
|
|
89
|
-
throw new Error(
|
|
90
|
-
`hub-local child "${deps.udsChildId}" does not currently provide cap "${deps.capName}"`,
|
|
91
|
-
)
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
// ── Agent-hosted or explicit remote: delegate to the unified resolver.
|
|
95
|
-
const resolver = deps.getResolver()
|
|
96
|
-
if (resolver !== null) {
|
|
97
|
-
const route = resolver.resolveCapRoute(deps.capName, {
|
|
98
|
-
nodeId: targetNodeId ?? deps.nodeId,
|
|
99
|
-
...(deviceId !== undefined ? { deviceId } : {}),
|
|
100
|
-
})
|
|
101
|
-
return resolver.dispatch(route, method, params)
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
// ── Pre-init window (resolver not yet constructed): legacy Moleculer call.
|
|
105
|
-
return deps.legacyBrokerCall(method, params, targetNodeId ?? deps.nodeId)
|
|
106
|
-
}
|
|
107
|
-
}
|
|
@@ -1,194 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Adapter factories that bridge the server-side registry/service state into the
|
|
3
|
-
* narrow interfaces that CapRouteResolver (in @camstack/kernel) requires.
|
|
4
|
-
*
|
|
5
|
-
* Layer note: this file is in server/backend and may import server-side types.
|
|
6
|
-
* The resolver itself (in @camstack/kernel) must NOT import from here —
|
|
7
|
-
* it depends only on the narrow interfaces (NodeCapAuthority, InProcessProviderLookup)
|
|
8
|
-
* defined in the kernel. These factories are the wiring adapters.
|
|
9
|
-
*
|
|
10
|
-
* nodeIsAgent format rule (from agent-registry.service.ts:74-77):
|
|
11
|
-
* hub → 'hub' → not an agent
|
|
12
|
-
* hub child → 'hub/<runnerId>' → not an agent
|
|
13
|
-
* agent → bare id with no '/' → agent
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
import type { NodeCapAuthority, InProcessProviderLookup } from '@camstack/kernel'
|
|
17
|
-
import type { InProcessProviderRef } from '@camstack/kernel'
|
|
18
|
-
|
|
19
|
-
// ---------------------------------------------------------------------------
|
|
20
|
-
// Minimal interface for the HubNodeRegistry dependency
|
|
21
|
-
// ---------------------------------------------------------------------------
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Minimal view of HubNodeRegistry that this adapter needs.
|
|
25
|
-
* Keeps the adapter decoupled from the concrete class — only structural match
|
|
26
|
-
* is required. HubNodeRegistry satisfies this interface structurally.
|
|
27
|
-
*/
|
|
28
|
-
export interface NodeRegistryLike {
|
|
29
|
-
getNodeManifest(
|
|
30
|
-
nodeId: string,
|
|
31
|
-
): readonly { readonly addonId: string; readonly capabilities: readonly string[] }[] | undefined
|
|
32
|
-
listNodeIds(): readonly string[]
|
|
33
|
-
/**
|
|
34
|
-
* Optional: returns flat (nodeId, addonId, capName, deviceId) native-cap tuples.
|
|
35
|
-
* When provided, `nodeKnowsCap` and `isNativeCap` also consult native caps so
|
|
36
|
-
* device-scoped native caps (ptz, motion-zones, …) are visible to the resolver.
|
|
37
|
-
*/
|
|
38
|
-
listNativeCapEntries?(): readonly {
|
|
39
|
-
readonly nodeId: string
|
|
40
|
-
readonly addonId: string
|
|
41
|
-
readonly capName: string
|
|
42
|
-
readonly deviceId: number
|
|
43
|
-
}[]
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
// ---------------------------------------------------------------------------
|
|
47
|
-
// Minimal interface for the CapabilityService dependency
|
|
48
|
-
// ---------------------------------------------------------------------------
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Minimal view of CapabilityService that the InProcessProviderLookup adapter needs.
|
|
52
|
-
* The real CapabilityService satisfies this interface structurally.
|
|
53
|
-
*/
|
|
54
|
-
export interface CapabilityServiceLike {
|
|
55
|
-
getSingleton(capability: string): Record<string, unknown> | null
|
|
56
|
-
/** Resolve the hub-local provider honoring the 'hub' per-node override. */
|
|
57
|
-
getSingletonForNode?(capability: string, nodeId: string): Record<string, unknown> | null
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// ---------------------------------------------------------------------------
|
|
61
|
-
// createNodeCapAuthority
|
|
62
|
-
// ---------------------------------------------------------------------------
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* Optional resolver injected into createNodeCapAuthority so that getAddonId
|
|
66
|
-
* can honor per-node singleton overrides and the cluster-global default,
|
|
67
|
-
* constrained to addons the node actually hosts in its manifest.
|
|
68
|
-
*/
|
|
69
|
-
export interface SingletonNodeResolver {
|
|
70
|
-
/** Bare addonId a node should use for a singleton cap (override→default→first), or null. */
|
|
71
|
-
resolveSingleton(capName: string, nodeId: string): string | null
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* Build a NodeCapAuthority backed by a HubNodeRegistry.
|
|
76
|
-
*
|
|
77
|
-
* nodeIsAgent rule: a node is an agent when its id is not 'hub' and does not
|
|
78
|
-
* contain '/' (hub children are `hub/<runnerId>`; agents are bare ids).
|
|
79
|
-
*
|
|
80
|
-
* nodeOnline: uses registry membership — per CLAUDE.md "the registry is the
|
|
81
|
-
* union of registerNode manifests minus disconnected nodes" because
|
|
82
|
-
* `removeNode` is called on `$node.disconnected`. Registry membership is the
|
|
83
|
-
* cleanest, cast-free liveness check.
|
|
84
|
-
*
|
|
85
|
-
* getAgentChildId: always returns null — the hub cannot resolve which forked
|
|
86
|
-
* child under an agent provides a cap (the agent flattens its subtree into
|
|
87
|
-
* one merged manifest). The agent resolves its own child locally (Task 6).
|
|
88
|
-
*/
|
|
89
|
-
export function createNodeCapAuthority(
|
|
90
|
-
nodeRegistry: NodeRegistryLike,
|
|
91
|
-
resolver?: SingletonNodeResolver,
|
|
92
|
-
): NodeCapAuthority {
|
|
93
|
-
return {
|
|
94
|
-
nodeKnowsCap(nodeId: string, capName: string): boolean {
|
|
95
|
-
// Check system (manifest) caps first
|
|
96
|
-
const manifest = nodeRegistry.getNodeManifest(nodeId)
|
|
97
|
-
if (
|
|
98
|
-
manifest !== undefined &&
|
|
99
|
-
manifest.some((addon) => addon.capabilities.includes(capName))
|
|
100
|
-
) {
|
|
101
|
-
return true
|
|
102
|
-
}
|
|
103
|
-
// Also check device-scoped native caps — these are NOT in the addon manifest
|
|
104
|
-
const nativeEntries = nodeRegistry.listNativeCapEntries?.() ?? []
|
|
105
|
-
return nativeEntries.some((n) => n.nodeId === nodeId && n.capName === capName)
|
|
106
|
-
},
|
|
107
|
-
|
|
108
|
-
getAddonId(nodeId: string, capName: string): string | null {
|
|
109
|
-
// Check system (manifest) caps first
|
|
110
|
-
const manifest = nodeRegistry.getNodeManifest(nodeId)
|
|
111
|
-
if (manifest !== undefined) {
|
|
112
|
-
const manifestAddons = manifest
|
|
113
|
-
.filter((addon) => addon.capabilities.includes(capName))
|
|
114
|
-
.map((addon) => addon.addonId)
|
|
115
|
-
if (manifestAddons.length > 0) {
|
|
116
|
-
// Per-node override / global-default resolution, constrained to what
|
|
117
|
-
// the node actually hosts. Falls back to the first manifest match.
|
|
118
|
-
const resolved = resolver?.resolveSingleton(capName, nodeId) ?? null
|
|
119
|
-
if (resolved !== null && manifestAddons.includes(resolved)) return resolved
|
|
120
|
-
return manifestAddons[0] ?? null
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
// Check device-scoped native caps (unchanged path)
|
|
124
|
-
const nativeEntries = nodeRegistry.listNativeCapEntries?.() ?? []
|
|
125
|
-
const nat = nativeEntries.find((n) => n.nodeId === nodeId && n.capName === capName)
|
|
126
|
-
return nat?.addonId ?? null
|
|
127
|
-
},
|
|
128
|
-
|
|
129
|
-
nodeIsAgent(nodeId: string): boolean {
|
|
130
|
-
return nodeId !== 'hub' && !nodeId.includes('/')
|
|
131
|
-
},
|
|
132
|
-
|
|
133
|
-
nodeOnline(nodeId: string): boolean {
|
|
134
|
-
// O(1) Map lookup — registry membership is the authoritative liveness
|
|
135
|
-
// check: HubNodeRegistry.removeNode is called on $node.disconnected,
|
|
136
|
-
// so a defined manifest means the node is connected.
|
|
137
|
-
return nodeRegistry.getNodeManifest(nodeId) !== undefined
|
|
138
|
-
},
|
|
139
|
-
|
|
140
|
-
listNodeIds(): readonly string[] {
|
|
141
|
-
return nodeRegistry.listNodeIds()
|
|
142
|
-
},
|
|
143
|
-
|
|
144
|
-
getAgentChildId(_agentNodeId: string, _capName: string): string | null {
|
|
145
|
-
// The hub cannot resolve the agent's child — the agent resolves locally (Task 6).
|
|
146
|
-
return null
|
|
147
|
-
},
|
|
148
|
-
|
|
149
|
-
isNativeCap(nodeId: string, capName: string, deviceId?: number): boolean {
|
|
150
|
-
const nativeEntries = nodeRegistry.listNativeCapEntries?.() ?? []
|
|
151
|
-
if (deviceId !== undefined) {
|
|
152
|
-
return nativeEntries.some(
|
|
153
|
-
(n) => n.nodeId === nodeId && n.capName === capName && n.deviceId === deviceId,
|
|
154
|
-
)
|
|
155
|
-
}
|
|
156
|
-
return nativeEntries.some((n) => n.nodeId === nodeId && n.capName === capName)
|
|
157
|
-
},
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
// ---------------------------------------------------------------------------
|
|
162
|
-
// createInProcessProviderLookup
|
|
163
|
-
// ---------------------------------------------------------------------------
|
|
164
|
-
|
|
165
|
-
/**
|
|
166
|
-
* Build an InProcessProviderLookup backed by a CapabilityService.
|
|
167
|
-
*
|
|
168
|
-
* Cast-free design: `getSingleton<Record<string, unknown>>` makes the provider
|
|
169
|
-
* indexable without any `as` casts. `typeof fn === 'function'` narrows to
|
|
170
|
-
* Function (implicit `any` return); calling via `fn.call(provider, args)` is
|
|
171
|
-
* then safe and the result is captured as `unknown`. No `as` casts anywhere.
|
|
172
|
-
*/
|
|
173
|
-
export function createInProcessProviderLookup(
|
|
174
|
-
capabilityService: CapabilityServiceLike,
|
|
175
|
-
): InProcessProviderLookup {
|
|
176
|
-
return (capName: string): InProcessProviderRef | null => {
|
|
177
|
-
const provider =
|
|
178
|
-
capabilityService.getSingletonForNode?.(capName, 'hub') ??
|
|
179
|
-
capabilityService.getSingleton(capName)
|
|
180
|
-
if (provider === null || provider === undefined) return null
|
|
181
|
-
|
|
182
|
-
const ref: InProcessProviderRef = {
|
|
183
|
-
invoke: (method: string, args: unknown): Promise<unknown> => {
|
|
184
|
-
const fn = provider[method]
|
|
185
|
-
if (typeof fn !== 'function') {
|
|
186
|
-
return Promise.reject(new Error(`method "${method}" not found on cap "${capName}"`))
|
|
187
|
-
}
|
|
188
|
-
const result: unknown = fn.call(provider, args)
|
|
189
|
-
return Promise.resolve(result)
|
|
190
|
-
},
|
|
191
|
-
}
|
|
192
|
-
return ref
|
|
193
|
-
}
|
|
194
|
-
}
|