@camstack/server 0.2.2 → 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.
Files changed (234) hide show
  1. package/{src/agent-status-page.ts → dist/agent-status-page.js} +30 -45
  2. package/dist/api/addon-upload.js +441 -0
  3. package/dist/api/addons-custom.router.js +91 -0
  4. package/dist/api/auth-whoami.js +55 -0
  5. package/dist/api/bridge-addons.router.js +109 -0
  6. package/dist/api/capabilities.router.js +229 -0
  7. package/dist/api/core/addon-settings.router.js +117 -0
  8. package/dist/api/core/agents.router.js +73 -0
  9. package/dist/api/core/auth.router.js +286 -0
  10. package/dist/api/core/bulk-update-coordinator.js +229 -0
  11. package/dist/api/core/cap-providers.js +1124 -0
  12. package/dist/api/core/capabilities.router.js +138 -0
  13. package/dist/api/core/collection-preference.js +17 -0
  14. package/dist/api/core/event-bus-proxy.router.js +45 -0
  15. package/dist/api/core/hwaccel.router.js +91 -0
  16. package/dist/api/core/live-events.router.js +61 -0
  17. package/dist/api/core/logs.router.js +172 -0
  18. package/dist/api/core/notifications.router.js +67 -0
  19. package/dist/api/core/repl.router.js +35 -0
  20. package/dist/api/core/settings-backend.router.js +121 -0
  21. package/dist/api/core/stream-probe.router.js +58 -0
  22. package/dist/api/core/system-events.router.js +100 -0
  23. package/dist/api/health/health.routes.js +68 -0
  24. package/{src/api/oauth2/consent-page.ts → dist/api/oauth2/consent-page.js} +11 -20
  25. package/dist/api/oauth2/oauth2-routes.js +219 -0
  26. package/dist/api/trpc/cap-mount-helpers.js +194 -0
  27. package/dist/api/trpc/cap-route-error-formatter.js +133 -0
  28. package/dist/api/trpc/client-ip.js +147 -0
  29. package/dist/api/trpc/core-cap-bridge.js +115 -0
  30. package/dist/api/trpc/generated-cap-mounts.js +388 -0
  31. package/dist/api/trpc/generated-cap-routers.js +7635 -0
  32. package/dist/api/trpc/scope-access.js +93 -0
  33. package/dist/api/trpc/trpc.context.js +184 -0
  34. package/dist/api/trpc/trpc.middleware.js +139 -0
  35. package/dist/api/trpc/trpc.router.js +188 -0
  36. package/dist/auth/session-cookie.js +47 -0
  37. package/dist/boot/boot-config.js +241 -0
  38. package/dist/boot/integration-id-backfill.js +76 -0
  39. package/dist/boot/post-boot.service.js +85 -0
  40. package/dist/core/addon/addon-call-gateway.js +99 -0
  41. package/dist/core/addon/addon-package.service.js +1560 -0
  42. package/dist/core/addon/addon-registry.service.js +2739 -0
  43. package/{src/core/addon/addon-row-manifest.ts → dist/core/addon/addon-row-manifest.js} +5 -5
  44. package/dist/core/addon/addon-search.service.js +62 -0
  45. package/dist/core/addon/addon-settings-provider.js +102 -0
  46. package/dist/core/addon/addon.tokens.js +5 -0
  47. package/dist/core/addon-bridge/addon-bridge.service.js +145 -0
  48. package/dist/core/addon-pages/addon-pages.service.js +107 -0
  49. package/dist/core/addon-widgets/addon-widgets.service.js +120 -0
  50. package/dist/core/agent/agent-registry.service.js +477 -0
  51. package/dist/core/auth/auth.service.js +10 -0
  52. package/dist/core/capability/capability.service.js +58 -0
  53. package/dist/core/config/config.schema.js +7 -0
  54. package/dist/core/config/config.service.js +10 -0
  55. package/dist/core/events/event-bus.service.js +83 -0
  56. package/dist/core/feature/feature.service.js +10 -0
  57. package/dist/core/lifecycle/lifecycle-state-machine.js +6 -0
  58. package/dist/core/logging/log-ring-buffer.js +6 -0
  59. package/dist/core/logging/logging.service.js +130 -0
  60. package/dist/core/logging/scoped-logger.js +6 -0
  61. package/dist/core/moleculer/cap-call-fn.js +50 -0
  62. package/dist/core/moleculer/cap-route-authority.js +122 -0
  63. package/dist/core/moleculer/moleculer.service.js +898 -0
  64. package/dist/core/network/network-quality.service.js +7 -0
  65. package/dist/core/notification/notification-wrapper.service.js +33 -0
  66. package/dist/core/notification/toast-wrapper.service.js +25 -0
  67. package/dist/core/provider/provider.tokens.js +4 -0
  68. package/dist/core/repl/repl-engine.service.js +140 -0
  69. package/dist/core/storage/fs-storage-backend.js +6 -0
  70. package/dist/core/storage/storage-location-manager.js +6 -0
  71. package/dist/core/storage/storage.service.js +7 -0
  72. package/dist/core/streaming/stream-probe.service.js +209 -0
  73. package/dist/core/topology/topology-emitter.service.js +106 -0
  74. package/dist/launcher.js +325 -0
  75. package/dist/main.js +1098 -0
  76. package/dist/manual-boot.js +227 -0
  77. package/package.json +5 -1
  78. package/src/__tests__/addon-install-e2e.test.ts +0 -74
  79. package/src/__tests__/addon-pages-e2e.test.ts +0 -200
  80. package/src/__tests__/addon-route-session.test.ts +0 -17
  81. package/src/__tests__/addon-settings-router.spec.ts +0 -67
  82. package/src/__tests__/addon-upload.spec.ts +0 -475
  83. package/src/__tests__/agent-registry.spec.ts +0 -179
  84. package/src/__tests__/agent-status-page.spec.ts +0 -82
  85. package/src/__tests__/auth-session-cookie.test.ts +0 -48
  86. package/src/__tests__/bulk-update-coordinator.spec.ts +0 -303
  87. package/src/__tests__/cap-ownership-authority.spec.ts +0 -431
  88. package/src/__tests__/cap-providers/cap-providers-location-import.spec.ts +0 -206
  89. package/src/__tests__/cap-providers/cap-usage-graph.spec.ts +0 -37
  90. package/src/__tests__/cap-providers/compute-topology-categories.spec.ts +0 -110
  91. package/src/__tests__/cap-providers/integrations-delete-cascade.spec.ts +0 -292
  92. package/src/__tests__/cap-providers-bulk-update.spec.ts +0 -408
  93. package/src/__tests__/cap-route-adapter.spec.ts +0 -302
  94. package/src/__tests__/cap-routers/_meta.spec.ts +0 -199
  95. package/src/__tests__/cap-routers/addon-settings.router.spec.ts +0 -115
  96. package/src/__tests__/cap-routers/broker-routing.router.spec.ts +0 -177
  97. package/src/__tests__/cap-routers/cap-route-error-formatter.spec.ts +0 -125
  98. package/src/__tests__/cap-routers/capabilities-node.spec.ts +0 -68
  99. package/src/__tests__/cap-routers/device-link-overlay.spec.ts +0 -137
  100. package/src/__tests__/cap-routers/device-manager-aggregate.router.spec.ts +0 -194
  101. package/src/__tests__/cap-routers/harness.ts +0 -163
  102. package/src/__tests__/cap-routers/metrics-provider.router.spec.ts +0 -133
  103. package/src/__tests__/cap-routers/null-provider-guard.spec.ts +0 -64
  104. package/src/__tests__/cap-routers/pipeline-executor.router.spec.ts +0 -159
  105. package/src/__tests__/cap-routers/settings-store.router.spec.ts +0 -291
  106. package/src/__tests__/capability-e2e.test.ts +0 -384
  107. package/src/__tests__/cli-e2e.test.ts +0 -150
  108. package/src/__tests__/core-cap-bridge.spec.ts +0 -91
  109. package/src/__tests__/dev-bootstrap-shm-ring.spec.ts +0 -40
  110. package/src/__tests__/device-settings-contribution-dispatch.spec.ts +0 -280
  111. package/src/__tests__/embedded-deps-e2e.test.ts +0 -125
  112. package/src/__tests__/event-bus-proxy-router.spec.ts +0 -75
  113. package/src/__tests__/fixtures/mock-analysis-addon-a.ts +0 -37
  114. package/src/__tests__/fixtures/mock-analysis-addon-b.ts +0 -37
  115. package/src/__tests__/fixtures/mock-log-addon.ts +0 -37
  116. package/src/__tests__/fixtures/mock-storage-addon.ts +0 -40
  117. package/src/__tests__/framework-allowlist.spec.ts +0 -96
  118. package/src/__tests__/framework-installer-defer-restart.spec.ts +0 -165
  119. package/src/__tests__/https-e2e.test.ts +0 -124
  120. package/src/__tests__/lifecycle-e2e.test.ts +0 -189
  121. package/src/__tests__/live-events-subscription.spec.ts +0 -149
  122. package/src/__tests__/moleculer/uds-readiness.spec.ts +0 -150
  123. package/src/__tests__/moleculer/uds-topology.spec.ts +0 -418
  124. package/src/__tests__/moleculer/uds-unowned-call.spec.ts +0 -383
  125. package/src/__tests__/moleculer-register-node-idempotency.spec.ts +0 -273
  126. package/src/__tests__/native-cap-route.spec.ts +0 -427
  127. package/src/__tests__/oauth2-account-linking.spec.ts +0 -867
  128. package/src/__tests__/post-boot-restart.spec.ts +0 -161
  129. package/src/__tests__/singleton-contention.test.ts +0 -499
  130. package/src/__tests__/streaming-diagnostic.test.ts +0 -615
  131. package/src/__tests__/streaming-scale.test.ts +0 -314
  132. package/src/__tests__/uds-addon-call-wiring.spec.ts +0 -242
  133. package/src/__tests__/uds-log-ingest.spec.ts +0 -183
  134. package/src/api/__tests__/addons-custom.spec.ts +0 -148
  135. package/src/api/__tests__/capabilities.router.test.ts +0 -56
  136. package/src/api/addon-upload.ts +0 -529
  137. package/src/api/addons-custom.router.ts +0 -101
  138. package/src/api/auth-whoami.ts +0 -101
  139. package/src/api/bridge-addons.router.ts +0 -122
  140. package/src/api/capabilities.router.ts +0 -265
  141. package/src/api/core/__tests__/auth-router-totp.spec.ts +0 -297
  142. package/src/api/core/__tests__/integration-markers.spec.ts +0 -10
  143. package/src/api/core/addon-settings.router.ts +0 -127
  144. package/src/api/core/agents.router.ts +0 -86
  145. package/src/api/core/auth.router.ts +0 -322
  146. package/src/api/core/bulk-update-coordinator.ts +0 -305
  147. package/src/api/core/cap-providers.ts +0 -1339
  148. package/src/api/core/capabilities.router.ts +0 -149
  149. package/src/api/core/collection-preference.ts +0 -40
  150. package/src/api/core/event-bus-proxy.router.ts +0 -45
  151. package/src/api/core/hwaccel.router.ts +0 -108
  152. package/src/api/core/live-events.router.ts +0 -67
  153. package/src/api/core/logs.router.ts +0 -195
  154. package/src/api/core/notifications.router.ts +0 -66
  155. package/src/api/core/repl.router.ts +0 -39
  156. package/src/api/core/settings-backend.router.ts +0 -140
  157. package/src/api/core/stream-probe.router.ts +0 -57
  158. package/src/api/core/system-events.router.ts +0 -125
  159. package/src/api/health/health.routes.ts +0 -117
  160. package/src/api/oauth2/__tests__/oauth2-routes.spec.ts +0 -62
  161. package/src/api/oauth2/oauth2-routes.ts +0 -281
  162. package/src/api/trpc/__tests__/client-ip.spec.ts +0 -146
  163. package/src/api/trpc/__tests__/scope-access-device.spec.ts +0 -268
  164. package/src/api/trpc/__tests__/scope-access.spec.ts +0 -102
  165. package/src/api/trpc/__tests__/webrtc-session-ua-enrich.spec.ts +0 -136
  166. package/src/api/trpc/cap-mount-helpers.ts +0 -245
  167. package/src/api/trpc/cap-route-error-formatter.ts +0 -171
  168. package/src/api/trpc/client-ip.ts +0 -147
  169. package/src/api/trpc/core-cap-bridge.ts +0 -154
  170. package/src/api/trpc/generated-cap-mounts.ts +0 -1240
  171. package/src/api/trpc/generated-cap-routers.ts +0 -11523
  172. package/src/api/trpc/scope-access.ts +0 -110
  173. package/src/api/trpc/trpc.context.ts +0 -258
  174. package/src/api/trpc/trpc.middleware.ts +0 -146
  175. package/src/api/trpc/trpc.router.ts +0 -389
  176. package/src/auth/session-cookie.ts +0 -54
  177. package/src/boot/__tests__/integration-id-backfill.spec.ts +0 -131
  178. package/src/boot/boot-config.ts +0 -259
  179. package/src/boot/integration-id-backfill.ts +0 -109
  180. package/src/boot/post-boot.service.ts +0 -105
  181. package/src/core/addon/__tests__/addon-registry-capability.test.ts +0 -62
  182. package/src/core/addon/__tests__/addon-row-manifest.spec.ts +0 -62
  183. package/src/core/addon/addon-call-gateway.ts +0 -171
  184. package/src/core/addon/addon-package.service.ts +0 -1787
  185. package/src/core/addon/addon-registry.service.ts +0 -3130
  186. package/src/core/addon/addon-search.service.ts +0 -91
  187. package/src/core/addon/addon-settings-provider.ts +0 -220
  188. package/src/core/addon/addon.tokens.ts +0 -2
  189. package/src/core/addon-bridge/addon-bridge.service.ts +0 -130
  190. package/src/core/addon-pages/addon-pages.service.spec.ts +0 -117
  191. package/src/core/addon-pages/addon-pages.service.ts +0 -82
  192. package/src/core/addon-widgets/addon-widgets.service.ts +0 -95
  193. package/src/core/agent/agent-registry.service.ts +0 -529
  194. package/src/core/auth/auth.service.spec.ts +0 -86
  195. package/src/core/auth/auth.service.ts +0 -8
  196. package/src/core/capability/capability.service.ts +0 -66
  197. package/src/core/config/config.schema.ts +0 -3
  198. package/src/core/config/config.service.spec.ts +0 -175
  199. package/src/core/config/config.service.ts +0 -7
  200. package/src/core/events/event-bus.service.spec.ts +0 -235
  201. package/src/core/events/event-bus.service.ts +0 -89
  202. package/src/core/feature/feature.service.spec.ts +0 -99
  203. package/src/core/feature/feature.service.ts +0 -8
  204. package/src/core/lifecycle/lifecycle-state-machine.spec.ts +0 -166
  205. package/src/core/lifecycle/lifecycle-state-machine.ts +0 -3
  206. package/src/core/logging/log-ring-buffer.ts +0 -3
  207. package/src/core/logging/logging.service.spec.ts +0 -287
  208. package/src/core/logging/logging.service.ts +0 -143
  209. package/src/core/logging/scoped-logger.ts +0 -3
  210. package/src/core/moleculer/cap-call-fn.spec.ts +0 -173
  211. package/src/core/moleculer/cap-call-fn.ts +0 -107
  212. package/src/core/moleculer/cap-route-authority.ts +0 -194
  213. package/src/core/moleculer/moleculer.service.ts +0 -1072
  214. package/src/core/network/network-quality.service.spec.ts +0 -53
  215. package/src/core/network/network-quality.service.ts +0 -5
  216. package/src/core/notification/notification-wrapper.service.ts +0 -34
  217. package/src/core/notification/toast-wrapper.service.ts +0 -27
  218. package/src/core/provider/provider.tokens.ts +0 -1
  219. package/src/core/repl/repl-engine.service.spec.ts +0 -444
  220. package/src/core/repl/repl-engine.service.ts +0 -155
  221. package/src/core/storage/fs-storage-backend.spec.ts +0 -70
  222. package/src/core/storage/fs-storage-backend.ts +0 -3
  223. package/src/core/storage/storage-location-manager.spec.ts +0 -130
  224. package/src/core/storage/storage-location-manager.ts +0 -3
  225. package/src/core/storage/storage.service.spec.ts +0 -73
  226. package/src/core/storage/storage.service.ts +0 -3
  227. package/src/core/streaming/stream-probe.service.ts +0 -221
  228. package/src/core/topology/topology-emitter.service.ts +0 -105
  229. package/src/launcher.ts +0 -314
  230. package/src/main.ts +0 -1245
  231. package/src/manual-boot.ts +0 -301
  232. package/tsconfig.build.json +0 -8
  233. package/tsconfig.json +0 -33
  234. package/vitest.config.ts +0 -26
@@ -1,314 +0,0 @@
1
- /* eslint-disable @typescript-eslint/no-unsafe-argument -- test file, mock typing */
2
- /**
3
- * Scale test — how many sub-streams can we decode for motion analysis?
4
- * Run: FRIGATE_HOST=192.168.1.128 npx vitest run server/backend/src/__tests__/streaming-scale.test.ts --reporter verbose
5
- */
6
- import { describe, it, expect, afterAll } from 'vitest'
7
- import http from 'node:http'
8
- import { StreamBrokerManager, FfmpegDecoderProvider } from '@camstack/addon-pipeline/stream-broker'
9
- import type { FrameHandle, IStreamBroker } from '@camstack/types'
10
- import type { IScopedLogger } from '@camstack/types'
11
-
12
- const FRIGATE = process.env.FRIGATE_HOST ?? ''
13
-
14
- const mockLogger: IScopedLogger = {
15
- debug: () => {},
16
- info: () => {},
17
- warn: console.warn,
18
- error: console.error,
19
- child: () => mockLogger,
20
- }
21
- const mockLoggingService = { createLogger: () => mockLogger }
22
-
23
- function wait(ms: number) {
24
- return new Promise((r) => setTimeout(r, ms))
25
- }
26
-
27
- /**
28
- * Poll-based replacement for the removed `IStreamBroker.onDecodedFrame`
29
- * callback (Phase 5 / D9 — decoded frames now travel as shm `FrameHandle`s).
30
- * Opens a frame-handle subscription, drains `pullFrameHandles` on a 50ms
31
- * timer, and invokes `onHandle` once per decoded frame. The returned
32
- * function tears the subscription down. `byteLength` stands in for the old
33
- * `DecodedFrame.data.length`.
34
- */
35
- async function subscribeDecodedFrames(
36
- broker: IStreamBroker,
37
- maxFps: number,
38
- onHandle: (handle: FrameHandle) => void,
39
- ): Promise<() => Promise<void>> {
40
- const { subscriptionId } = await broker.subscribeFrameHandles({ format: 'rgb', maxFps })
41
- const iv = setInterval(() => {
42
- for (const handle of broker.pullFrameHandles(subscriptionId, 8)) {
43
- onHandle(handle)
44
- }
45
- }, 50)
46
- return async () => {
47
- clearInterval(iv)
48
- await broker.unsubscribeFrameHandles(subscriptionId)
49
- }
50
- }
51
-
52
- /** Fetch detect sub-stream URLs from Frigate config */
53
- async function discoverSubStreams(): Promise<Array<{ name: string; url: string }>> {
54
- return new Promise((resolve, reject) => {
55
- const req = http.get(`http://${FRIGATE}:5000/api/config`, (res) => {
56
- let data = ''
57
- res.on('data', (chunk: Buffer) => {
58
- data += chunk
59
- })
60
- res.on('end', () => {
61
- try {
62
- // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment --
63
- const config = JSON.parse(data)
64
- // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access --
65
- const cameras = config.cameras || {}
66
- const streams: Array<{ name: string; url: string }> = []
67
- // eslint-disable-next-line @typescript-eslint/no-explicit-any --
68
- for (const [name, cam] of Object.entries(cameras) as [string, any][]) {
69
- // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access --
70
- if (!cam.enabled && cam.enabled !== undefined) continue
71
- // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access --
72
- const inputs = cam?.ffmpeg?.inputs || []
73
- for (const inp of inputs) {
74
- // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access --
75
- if (inp.roles?.includes('detect') && inp.path) {
76
- let streamName: string
77
- // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access --
78
- if (inp.path.includes('8554/')) {
79
- // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access --
80
- streamName = inp.path.split('8554/').pop()!
81
- } else {
82
- streamName = `${name}_sub`
83
- }
84
- streams.push({ name, url: `rtsp://${FRIGATE}:8554/${streamName}` })
85
- }
86
- }
87
- }
88
- resolve(streams)
89
- } catch (e) {
90
- reject(e)
91
- }
92
- })
93
- })
94
- req.on('error', reject)
95
- req.setTimeout(5000, () => {
96
- req.destroy()
97
- reject(new Error('Timeout'))
98
- })
99
- })
100
- }
101
-
102
- describe.skipIf(!FRIGATE)('Streaming Scale Test (requires Frigate)', () => {
103
- // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call -- @camstack/addon-stream-broker types resolve as any in this test context
104
- const manager = new StreamBrokerManager([new FfmpegDecoderProvider()], mockLogger)
105
-
106
- afterAll(async () => {
107
- // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access --
108
- await manager.destroyAll()
109
- // Wait for ffmpeg processes to exit
110
- await wait(2000)
111
- })
112
-
113
- it('progressive scale: 5 → 10 → 20 → 30 sub-streams @ 2fps for motion', async () => {
114
- // Discover real streams, then duplicate to simulate more cameras
115
- const realStreams = await discoverSubStreams()
116
-
117
- // Test which streams actually respond (filter out 404s)
118
- const workingStreams: typeof realStreams = []
119
- for (const s of realStreams.slice(0, 5)) {
120
- try {
121
- // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access --
122
- const broker = await manager.createBroker(`probe-${s.name}`, {
123
- type: 'rtsp',
124
- url: s.url,
125
- videoCodec: 'h264',
126
- })
127
- // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access --
128
- await (broker as any).start({ type: 'rtsp', url: s.url, videoCodec: 'h264' })
129
- const ok = await new Promise<boolean>((resolve) => {
130
- const timeout = setTimeout(() => resolve(false), 5000)
131
- let unsub: (() => Promise<void>) | undefined
132
- subscribeDecodedFrames(broker, 2, () => {
133
- clearTimeout(timeout)
134
- void unsub?.()
135
- resolve(true)
136
- })
137
- .then((fn) => {
138
- unsub = fn
139
- })
140
- .catch(() => resolve(false))
141
- })
142
- // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access --
143
- await broker.stop()
144
- // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access --
145
- await manager.destroyBroker(`probe-${s.name}`)
146
- if (ok) workingStreams.push(s)
147
- } catch {
148
- /* skip */
149
- }
150
- }
151
-
152
- console.log(
153
- `\nDiscovered ${realStreams.length} sub-streams, ${workingStreams.length} responding`,
154
- )
155
- if (workingStreams.length === 0) {
156
- console.log(' No working sub-streams, skipping')
157
- return
158
- }
159
-
160
- // Generate virtual streams by duplicating working ones (same RTSP URL, different broker ID)
161
- function generateStreams(count: number) {
162
- const streams: Array<{ name: string; url: string }> = []
163
- for (let i = 0; i < count; i++) {
164
- const real = workingStreams[i % workingStreams.length]!
165
- streams.push({ name: `cam-${String(i).padStart(2, '0')}-${real.name}`, url: real.url })
166
- }
167
- return streams
168
- }
169
-
170
- const scaleLevels = [5, 10, 20, 30]
171
-
172
- for (const targetCount of scaleLevels) {
173
- console.log(`\n=== Scale: ${targetCount} sub-streams @ 2fps ===`)
174
-
175
- const memBefore = process.memoryUsage()
176
- const cpuBefore = process.cpuUsage()
177
- const brokerIds: string[] = []
178
- const unsubs: Array<() => Promise<void>> = []
179
- const perCamera: Record<string, { frames: number; totalBytes: number }> = {}
180
- let connectFailures = 0
181
-
182
- // Start N brokers IN PARALLEL (using duplicated working streams)
183
- const streams = generateStreams(targetCount)
184
- const startTime = Date.now()
185
-
186
- // Launch all brokers concurrently — don't wait for keyframe sequentially
187
- const connectPromises = streams.map(async (s) => {
188
- const brokerId = `scale-${s.name}`
189
- brokerIds.push(brokerId)
190
- perCamera[s.name] = { frames: 0, totalBytes: 0 }
191
-
192
- try {
193
- // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access --
194
- const broker = await manager.createBroker(brokerId, {
195
- type: 'rtsp',
196
- url: s.url,
197
- videoCodec: 'h264',
198
- })
199
- // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access --
200
- await (broker as any).start({ type: 'rtsp', url: s.url, videoCodec: 'h264' })
201
-
202
- // Wait for first frame with tight timeout
203
- await new Promise<void>((resolve) => {
204
- const timeout = setTimeout(() => {
205
- connectFailures++
206
- resolve()
207
- }, 10000)
208
- let unsub: (() => Promise<void>) | undefined
209
- subscribeDecodedFrames(broker, 2, () => {
210
- clearTimeout(timeout)
211
- void unsub?.()
212
- resolve()
213
- })
214
- .then((fn) => {
215
- unsub = fn
216
- })
217
- .catch(() => resolve())
218
- })
219
-
220
- // Subscribe for motion analysis
221
- const unsub = await subscribeDecodedFrames(broker, 2, (handle: FrameHandle) => {
222
- perCamera[s.name]!.frames++
223
- perCamera[s.name]!.totalBytes += handle.byteLength
224
- })
225
- unsubs.push(unsub)
226
- } catch (err) {
227
- connectFailures++
228
- }
229
- })
230
-
231
- await Promise.all(connectPromises)
232
-
233
- const connectTime = Date.now() - startTime
234
- console.log(
235
- ` Connected ${targetCount - connectFailures}/${targetCount} in ${(connectTime / 1000).toFixed(1)}s`,
236
- )
237
-
238
- // Run for 5 seconds of actual streaming
239
- const measureStart = Date.now()
240
- const cpuMeasureStart = process.cpuUsage()
241
- await wait(5000)
242
- const cpuMeasure = process.cpuUsage(cpuMeasureStart)
243
- const measureDuration = (Date.now() - measureStart) / 1000
244
-
245
- // Unsubscribe
246
- for (const unsub of unsubs) await unsub()
247
-
248
- // Calculate metrics
249
- const cpuAfter = process.cpuUsage(cpuBefore)
250
- const memAfter = process.memoryUsage()
251
-
252
- let totalFrames = 0
253
- let totalBytes = 0
254
- let activeCameras = 0
255
- let starvingCameras = 0
256
-
257
- for (const [name, data] of Object.entries(perCamera)) {
258
- totalFrames += data.frames
259
- totalBytes += data.totalBytes
260
- if (data.frames > 0) activeCameras++
261
- if (data.frames < 3) starvingCameras++ // less than 3 frames in 5s = starving
262
- }
263
-
264
- const cpuPct = (
265
- ((cpuMeasure.user + cpuMeasure.system) / (measureDuration * 1_000_000)) *
266
- 100
267
- ).toFixed(1)
268
- const rssMB = (memAfter.rss / 1024 / 1024).toFixed(0)
269
- const heapMB = (memAfter.heapUsed / 1024 / 1024).toFixed(0)
270
- const combinedFps = (totalFrames / measureDuration).toFixed(1)
271
- const avgFrameKB = totalFrames > 0 ? (totalBytes / totalFrames / 1024).toFixed(0) : '0'
272
- const bwMBs = (totalBytes / 1024 / 1024 / measureDuration).toFixed(1)
273
-
274
- console.log(` Active: ${activeCameras}/${targetCount} cameras producing frames`)
275
- console.log(` Starving: ${starvingCameras} cameras (<3 frames in 5s)`)
276
- console.log(` Total frames: ${totalFrames} (${combinedFps} fps combined)`)
277
- console.log(` Avg frame: ${avgFrameKB} KB`)
278
- console.log(` Decode BW: ${bwMBs} MB/s`)
279
- console.log(` CPU: ${cpuPct}%`)
280
- console.log(` RSS: ${rssMB} MB | Heap: ${heapMB} MB`)
281
-
282
- // Per-camera breakdown
283
- console.log(` ---`)
284
- const sorted = Object.entries(perCamera).toSorted((a, b) => b[1].frames - a[1].frames)
285
- for (const [name, data] of sorted) {
286
- const fps = (data.frames / measureDuration).toFixed(1)
287
- const avgKB = data.frames > 0 ? (data.totalBytes / data.frames / 1024).toFixed(0) : '-'
288
- const status = data.frames >= 3 ? '✓' : data.frames > 0 ? '⚠' : '✗'
289
- console.log(
290
- ` ${status} ${name.padEnd(35)} ${fps.padStart(4)} fps ${avgKB.padStart(5)} KB/f (${data.frames} frames)`,
291
- )
292
- }
293
-
294
- // Cleanup for next scale level
295
- for (const id of brokerIds) {
296
- // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access --
297
- const b = manager.getBroker(id)
298
- // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access --
299
- if (b) await b.stop()
300
- // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access --
301
- await manager.destroyBroker(id)
302
- }
303
- brokerIds.length = 0
304
- unsubs.length = 0
305
- Object.keys(perCamera).forEach((k) => delete perCamera[k])
306
-
307
- // Expect at least 80% of cameras producing
308
- expect(activeCameras).toBeGreaterThanOrEqual(Math.floor(targetCount * 0.8))
309
-
310
- // Pause between levels for cleanup
311
- await wait(2000)
312
- }
313
- }, 300000) // 5 min timeout for the full progressive test
314
- })
@@ -1,242 +0,0 @@
1
- /**
2
- * F3 backend wiring — forked-addon routes + custom actions over UDS.
3
- *
4
- * Verifies the production wiring `AddonRegistryService` builds, end-to-end over
5
- * a REAL LocalChildRegistry + LocalChildClient pair, with the child running the
6
- * REAL addon-runner-side dispatcher (`createChildAddonCallDispatch`):
7
- *
8
- * - Custom actions: a `CustomActionRegistry` whose dispatcher is the F3
9
- * `callAddonOnChild({target:'custom', action, args})` reaches the child's
10
- * real custom-action handler and returns its result (the path
11
- * `registerForkedAddonCustomActions` builds via `dispatchForkedCustomAction`).
12
- * - Routes: `callAddonOnChild({target:'routes'})` returns handler-stripped
13
- * descriptors; the per-route bridge handler dispatches through the
14
- * `addon-routes` `invoke()` cap method (the path `mountForkedAddonRoutes`
15
- * builds), and the captured envelope round-trips.
16
- *
17
- * The child-side `onAddonCall` handler is a faithful inline of the addon-runner
18
- * dispatcher (resolve route provider / custom registry; strip route handlers).
19
- */
20
-
21
- import { describe, it, expect, afterEach } from 'vitest'
22
- import { z } from 'zod'
23
- import {
24
- LocalChildRegistry,
25
- LocalChildClient,
26
- CustomActionRegistry,
27
- createLocalTransport,
28
- } from '@camstack/kernel'
29
- import type { AddonCallInput } from '@camstack/kernel'
30
- import { buildAddonRouteProvider, customAction, defineCustomActions } from '@camstack/types'
31
- import type { IAddonHttpRoute, CustomActionsSpec } from '@camstack/types'
32
-
33
- const nid = (): string => `f3-wiring-${process.pid}-${Math.random().toString(36).slice(2)}`
34
-
35
- describe('F3 — forked-addon routes + custom actions over UDS (backend wiring)', () => {
36
- let registry: LocalChildRegistry | null = null
37
- const clients: LocalChildClient[] = []
38
-
39
- afterEach(async () => {
40
- for (const c of clients) await c.close().catch(() => {})
41
- clients.length = 0
42
- await registry?.close().catch(() => {})
43
- registry = null
44
- })
45
-
46
- /** Stand up a hub-side registry + a child running the real addon-call dispatcher. */
47
- async function standUp(opts: {
48
- addonId: string
49
- routes?: readonly IAddonHttpRoute[]
50
- catalog?: CustomActionsSpec
51
- handlers?: Record<string, (input: unknown) => Promise<unknown>>
52
- }): Promise<{ childCustomActions: CustomActionRegistry }> {
53
- const nodeId = nid()
54
- registry = new LocalChildRegistry(createLocalTransport().createServer(nodeId))
55
- await registry.start()
56
-
57
- // The runner-side custom-action registry — populated as the addon-runner does.
58
- const childCustomActions = new CustomActionRegistry()
59
- if (opts.catalog && opts.handlers) {
60
- const handlers = opts.handlers
61
- childCustomActions.registerAddon(opts.addonId, opts.catalog, (action, input) => {
62
- const fn = handlers[action]
63
- if (!fn) throw new Error(`no handler for ${action}`)
64
- return fn(input)
65
- })
66
- }
67
-
68
- const routeProvider = opts.routes ? buildAddonRouteProvider(opts.addonId, opts.routes) : null
69
-
70
- const child = new LocalChildClient({
71
- nodeId,
72
- childId: opts.addonId,
73
- caps: [{ capName: 'addon-routes', mode: 'collection' }],
74
- // Cap dispatch — resolve the `addon-routes` cap's `invoke`/`getRoutes`
75
- // method on the live route provider (mirrors the runner's
76
- // createChildCapDispatch resolving allSingletonProviders['addon-routes']).
77
- dispatch: async (call) => {
78
- if (call.capName === 'addon-routes' && routeProvider !== null) {
79
- const fn = Reflect.get(routeProvider, call.method)
80
- if (typeof fn === 'function') return fn.call(routeProvider, call.args)
81
- }
82
- return null
83
- },
84
- })
85
- // Inline addon-runner dispatcher: routes → handler-stripped descriptors,
86
- // custom → the child custom-action registry. Mirrors
87
- // `createChildAddonCallDispatch` in the kernel.
88
- child.onAddonCall(async (call: AddonCallInput) => {
89
- if (call.target === 'routes') {
90
- if (routeProvider === null) throw new Error(`addon "${call.addonId}" has no routes`)
91
- const live = routeProvider.getRoutes()
92
- return live.map((r) => ({
93
- method: r.method,
94
- path: r.path,
95
- ...(r.access !== undefined ? { access: r.access } : {}),
96
- ...(r.description !== undefined ? { description: r.description } : {}),
97
- }))
98
- }
99
- const action = call.action
100
- if (typeof action !== 'string') throw new Error('missing action')
101
- const entry = childCustomActions.resolve(call.addonId, action)
102
- if (entry === null) throw new Error(`no custom action "${action}"`)
103
- return entry.handler(call.args)
104
- })
105
- clients.push(child)
106
- await child.start()
107
-
108
- return { childCustomActions }
109
- }
110
-
111
- it('custom action: the registerForkedAddonCustomActions dispatch path reaches the child handler', async () => {
112
- const catalog = defineCustomActions({
113
- runBenchmark: customAction(
114
- z.object({ iterations: z.number() }),
115
- z.object({ ran: z.number() }),
116
- { kind: 'mutation' },
117
- ),
118
- })
119
- await standUp({
120
- addonId: 'benchmark',
121
- catalog,
122
- handlers: {
123
- runBenchmark: async (input) => {
124
- const args = input as { iterations: number }
125
- return { ran: args.iterations }
126
- },
127
- },
128
- })
129
-
130
- // Hub-side registry — the dispatcher mirrors `dispatchForkedCustomAction`.
131
- const hubRegistry = new CustomActionRegistry()
132
- hubRegistry.registerAddon('benchmark', catalog, (action, input) =>
133
- registry!.callAddonOnChild('benchmark', {
134
- addonId: 'benchmark',
135
- target: 'custom',
136
- action,
137
- args: input,
138
- }),
139
- )
140
-
141
- const entry = hubRegistry.resolve('benchmark', 'runBenchmark')
142
- expect(entry).not.toBeNull()
143
- const result = await entry!.handler({ iterations: 9 })
144
- expect(result).toEqual({ ran: 9 })
145
- })
146
-
147
- it('routes: callAddonOnChild(routes) returns stripped descriptors and invoke() round-trips', async () => {
148
- const routes: IAddonHttpRoute[] = [
149
- {
150
- method: 'GET',
151
- path: '/hello/:name',
152
- access: 'public',
153
- handler: async (req, reply) => {
154
- reply.code(200)
155
- reply.send({ greeting: `hi ${req.params.name}` })
156
- },
157
- },
158
- ]
159
- await standUp({ addonId: 'export-alexa', routes })
160
-
161
- // (a) routes come back handler-stripped (the `mountForkedAddonRoutes` fetch).
162
- const rawRoutes = await registry!.callAddonOnChild('export-alexa', {
163
- addonId: 'export-alexa',
164
- target: 'routes',
165
- })
166
- expect(rawRoutes).toEqual([{ method: 'GET', path: '/hello/:name', access: 'public' }])
167
-
168
- // (b) dispatch through the addon-routes cap proxy's `invoke` cap method —
169
- // the bridge handler `mountForkedAddonRoutes` synthesizes calls this.
170
- const routesCapProxy = {
171
- invoke: (input: unknown) =>
172
- registry!.callCapOnChild('export-alexa', {
173
- capName: 'addon-routes',
174
- method: 'invoke',
175
- args: input,
176
- }),
177
- }
178
- const envelope = await routesCapProxy.invoke({
179
- method: 'GET',
180
- path: '/hello/world',
181
- params: { name: 'world' },
182
- query: {},
183
- body: undefined,
184
- headers: {},
185
- })
186
- expect(envelope).toMatchObject({ status: 200, body: { greeting: 'hi world' } })
187
- })
188
-
189
- it('auth-oidc shape: a forked addon with a SYNC array getRoutes + async handlers mounts via the strip path without serializing a function', async () => {
190
- // Reproduces the regression: `auth-oidc` declares a manual route provider
191
- // `{ id, getRoutes: () => routes }` (NO `buildAddonRouteProvider`, NO
192
- // `invoke`). It forks like every non-core addon, so the hub resolves an
193
- // async UDS cap proxy whose `getRoutes()` returns a Promise. The mount path
194
- // MUST NOT register that proxy directly (→ `getRoutes(...).map is not a
195
- // function`) nor dispatch its `getRoutes` cap method over UDS (the child's
196
- // `getRoutes()` returns the live array WITH async `handler` functions →
197
- // MsgPack "Unrecognized object: [object AsyncFunction]"). It uses the
198
- // handler-stripped `callAddonOnChild(target:'routes')` bridge instead.
199
- const routes: IAddonHttpRoute[] = [
200
- {
201
- method: 'GET',
202
- path: '/:providerId/start',
203
- access: 'public',
204
- description: 'Begin OIDC redirect login flow',
205
- handler: async (req, reply) => {
206
- reply.code(302)
207
- reply.send('')
208
- },
209
- },
210
- {
211
- method: 'GET',
212
- path: '/:providerId/callback',
213
- access: 'public',
214
- handler: async (req, reply) => {
215
- reply.code(200)
216
- reply.send('ok')
217
- },
218
- },
219
- ]
220
- await standUp({ addonId: 'auth-oidc', routes })
221
-
222
- // The handler-stripped descriptors cross the real UDS pair. If the strip
223
- // path leaked a `handler` function, MsgPack `encodeFrame` would throw
224
- // "Unrecognized object: [object AsyncFunction]" before this resolves.
225
- const rawRoutes = await registry!.callAddonOnChild('auth-oidc', {
226
- addonId: 'auth-oidc',
227
- target: 'routes',
228
- })
229
- expect(rawRoutes).toEqual([
230
- {
231
- method: 'GET',
232
- path: '/:providerId/start',
233
- access: 'public',
234
- description: 'Begin OIDC redirect login flow',
235
- },
236
- { method: 'GET', path: '/:providerId/callback', access: 'public' },
237
- ])
238
- // Coarse reachability gate the route-mount fallback uses: the child is
239
- // connected even though we only checked the addon-call surface.
240
- expect(registry!.isChildKnown('auth-oidc')).toBe(true)
241
- })
242
- })