@camstack/core 0.1.15 → 0.1.16

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 (382) hide show
  1. package/dist/addon/addon-api-factory.d.ts +36 -0
  2. package/dist/addon/addon-api-factory.d.ts.map +1 -0
  3. package/dist/addon-routes/addon-route-registry.d.ts +38 -0
  4. package/dist/addon-routes/addon-route-registry.d.ts.map +1 -0
  5. package/dist/auth/api-key-manager.d.ts +27 -0
  6. package/dist/auth/api-key-manager.d.ts.map +1 -0
  7. package/dist/auth/auth-manager.d.ts +47 -0
  8. package/dist/auth/auth-manager.d.ts.map +1 -0
  9. package/dist/auth/parse-record.d.ts +19 -0
  10. package/dist/auth/parse-record.d.ts.map +1 -0
  11. package/dist/auth/scoped-token-manager.d.ts +18 -0
  12. package/dist/auth/scoped-token-manager.d.ts.map +1 -0
  13. package/dist/auth/user-manager.d.ts +34 -0
  14. package/dist/auth/user-manager.d.ts.map +1 -0
  15. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.d.ts +54 -0
  16. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.d.ts.map +1 -0
  17. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +223 -217
  18. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js.map +1 -1
  19. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +216 -7
  20. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs.map +1 -1
  21. package/dist/builtins/addon-pages-aggregator/index.d.ts +2 -0
  22. package/dist/builtins/addon-pages-aggregator/index.d.ts.map +1 -0
  23. package/dist/builtins/addon-pages-aggregator/index.js +6 -221
  24. package/dist/builtins/addon-pages-aggregator/index.mjs +2 -9
  25. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.d.ts +33 -0
  26. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.d.ts.map +1 -0
  27. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +199 -197
  28. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js.map +1 -1
  29. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +192 -7
  30. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs.map +1 -1
  31. package/dist/builtins/addon-widgets-aggregator/index.d.ts +2 -0
  32. package/dist/builtins/addon-widgets-aggregator/index.d.ts.map +1 -0
  33. package/dist/builtins/addon-widgets-aggregator/index.js +6 -201
  34. package/dist/builtins/addon-widgets-aggregator/index.mjs +2 -9
  35. package/dist/builtins/alerts/alerts.addon.d.ts +82 -0
  36. package/dist/builtins/alerts/alerts.addon.d.ts.map +1 -0
  37. package/dist/builtins/alerts/alerts.addon.js +590 -430
  38. package/dist/builtins/alerts/alerts.addon.js.map +1 -1
  39. package/dist/builtins/alerts/alerts.addon.mjs +595 -7
  40. package/dist/builtins/alerts/alerts.addon.mjs.map +1 -1
  41. package/dist/builtins/alerts/index.d.ts +2 -0
  42. package/dist/builtins/alerts/index.d.ts.map +1 -0
  43. package/dist/builtins/alerts/index.js +3 -443
  44. package/dist/builtins/alerts/index.mjs +2 -8
  45. package/dist/builtins/auth-orchestrator/auth-orchestrator.addon.d.ts +8 -0
  46. package/dist/builtins/auth-orchestrator/auth-orchestrator.addon.d.ts.map +1 -0
  47. package/dist/builtins/auth-orchestrator/auth-orchestrator.addon.js +56 -0
  48. package/dist/builtins/auth-orchestrator/auth-orchestrator.addon.js.map +1 -0
  49. package/dist/builtins/auth-orchestrator/auth-orchestrator.addon.mjs +50 -0
  50. package/dist/builtins/auth-orchestrator/auth-orchestrator.addon.mjs.map +1 -0
  51. package/dist/builtins/auth-orchestrator/index.d.ts +2 -0
  52. package/dist/builtins/auth-orchestrator/index.d.ts.map +1 -0
  53. package/dist/builtins/auth-orchestrator/index.js +7 -0
  54. package/dist/builtins/auth-orchestrator/index.mjs +2 -0
  55. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.d.ts +148 -0
  56. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.d.ts.map +1 -0
  57. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +7639 -0
  58. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js.map +1 -0
  59. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +7627 -0
  60. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs.map +1 -0
  61. package/dist/builtins/backup-orchestrator/cron-helpers.d.ts +24 -0
  62. package/dist/builtins/backup-orchestrator/cron-helpers.d.ts.map +1 -0
  63. package/dist/builtins/backup-orchestrator/destination-policy.d.ts +73 -0
  64. package/dist/builtins/backup-orchestrator/destination-policy.d.ts.map +1 -0
  65. package/dist/builtins/backup-orchestrator/download-helpers.d.ts +13 -0
  66. package/dist/builtins/backup-orchestrator/download-helpers.d.ts.map +1 -0
  67. package/dist/builtins/backup-orchestrator/index.d.ts +3 -0
  68. package/dist/builtins/backup-orchestrator/index.d.ts.map +1 -0
  69. package/dist/builtins/backup-orchestrator/index.js +7 -0
  70. package/dist/builtins/backup-orchestrator/index.mjs +2 -0
  71. package/dist/builtins/backup-orchestrator/manifest-store.d.ts +78 -0
  72. package/dist/builtins/backup-orchestrator/manifest-store.d.ts.map +1 -0
  73. package/dist/builtins/console-logging/console-destination.d.ts +14 -0
  74. package/dist/builtins/console-logging/console-destination.d.ts.map +1 -0
  75. package/dist/builtins/console-logging/console-logging.addon.d.ts +26 -0
  76. package/dist/builtins/console-logging/console-logging.addon.d.ts.map +1 -0
  77. package/dist/builtins/console-logging/index.d.ts +4 -0
  78. package/dist/builtins/console-logging/index.d.ts.map +1 -0
  79. package/dist/builtins/console-logging/index.js +99 -235
  80. package/dist/builtins/console-logging/index.js.map +1 -1
  81. package/dist/builtins/console-logging/index.mjs +95 -9
  82. package/dist/builtins/console-logging/index.mjs.map +1 -1
  83. package/dist/builtins/device-manager/device-event-propagator.d.ts +27 -0
  84. package/dist/builtins/device-manager/device-event-propagator.d.ts.map +1 -0
  85. package/dist/builtins/device-manager/device-manager.addon.d.ts +259 -0
  86. package/dist/builtins/device-manager/device-manager.addon.d.ts.map +1 -0
  87. package/dist/builtins/device-manager/device-manager.addon.js +2125 -2127
  88. package/dist/builtins/device-manager/device-manager.addon.js.map +1 -1
  89. package/dist/builtins/device-manager/device-manager.addon.mjs +2145 -7
  90. package/dist/builtins/device-manager/device-manager.addon.mjs.map +1 -1
  91. package/dist/builtins/device-manager/index.d.ts +3 -0
  92. package/dist/builtins/device-manager/index.d.ts.map +1 -0
  93. package/dist/builtins/device-manager/index.js +6 -2156
  94. package/dist/builtins/device-manager/index.mjs +2 -10
  95. package/dist/builtins/hub-forwarder/hub-forwarder-destination.d.ts +45 -0
  96. package/dist/builtins/hub-forwarder/hub-forwarder-destination.d.ts.map +1 -0
  97. package/dist/builtins/hub-forwarder/hub-forwarder.addon.d.ts +16 -0
  98. package/dist/builtins/hub-forwarder/hub-forwarder.addon.d.ts.map +1 -0
  99. package/dist/builtins/hub-forwarder/index.d.ts +4 -0
  100. package/dist/builtins/hub-forwarder/index.d.ts.map +1 -0
  101. package/dist/builtins/hub-forwarder/index.js +150 -291
  102. package/dist/builtins/hub-forwarder/index.js.map +1 -1
  103. package/dist/builtins/hub-forwarder/index.mjs +145 -9
  104. package/dist/builtins/hub-forwarder/index.mjs.map +1 -1
  105. package/dist/builtins/local-auth/auth-schema.d.ts +12 -0
  106. package/dist/builtins/local-auth/auth-schema.d.ts.map +1 -0
  107. package/dist/builtins/local-auth/index.d.ts +2 -0
  108. package/dist/builtins/local-auth/index.d.ts.map +1 -0
  109. package/dist/builtins/local-auth/index.js +3 -623
  110. package/dist/builtins/local-auth/index.mjs +2 -8
  111. package/dist/builtins/local-auth/local-auth.addon.d.ts +17 -0
  112. package/dist/builtins/local-auth/local-auth.addon.d.ts.map +1 -0
  113. package/dist/builtins/local-auth/local-auth.addon.js +6861 -589
  114. package/dist/builtins/local-auth/local-auth.addon.js.map +1 -1
  115. package/dist/builtins/local-auth/local-auth.addon.mjs +6883 -7
  116. package/dist/builtins/local-auth/local-auth.addon.mjs.map +1 -1
  117. package/dist/builtins/local-network/index.d.ts +3 -0
  118. package/dist/builtins/local-network/index.d.ts.map +1 -0
  119. package/dist/builtins/local-network/index.js +9 -0
  120. package/dist/builtins/local-network/index.mjs +2 -0
  121. package/dist/builtins/local-network/local-network.addon.d.ts +102 -0
  122. package/dist/builtins/local-network/local-network.addon.d.ts.map +1 -0
  123. package/dist/builtins/local-network/local-network.addon.js +404 -0
  124. package/dist/builtins/local-network/local-network.addon.js.map +1 -0
  125. package/dist/builtins/local-network/local-network.addon.mjs +392 -0
  126. package/dist/builtins/local-network/local-network.addon.mjs.map +1 -0
  127. package/dist/builtins/mesh-orchestrator/index.d.ts +2 -0
  128. package/dist/builtins/mesh-orchestrator/index.d.ts.map +1 -0
  129. package/dist/builtins/mesh-orchestrator/index.js +7 -0
  130. package/dist/builtins/mesh-orchestrator/index.mjs +2 -0
  131. package/dist/builtins/mesh-orchestrator/mesh-orchestrator.addon.d.ts +9 -0
  132. package/dist/builtins/mesh-orchestrator/mesh-orchestrator.addon.d.ts.map +1 -0
  133. package/dist/builtins/mesh-orchestrator/mesh-orchestrator.addon.js +83 -0
  134. package/dist/builtins/mesh-orchestrator/mesh-orchestrator.addon.js.map +1 -0
  135. package/dist/builtins/mesh-orchestrator/mesh-orchestrator.addon.mjs +77 -0
  136. package/dist/builtins/mesh-orchestrator/mesh-orchestrator.addon.mjs.map +1 -0
  137. package/dist/builtins/native-metrics/index.d.ts +3 -0
  138. package/dist/builtins/native-metrics/index.d.ts.map +1 -0
  139. package/dist/builtins/native-metrics/native-metrics-provider.d.ts +49 -0
  140. package/dist/builtins/native-metrics/native-metrics-provider.d.ts.map +1 -0
  141. package/dist/builtins/native-metrics/native-metrics.addon.d.ts +74 -0
  142. package/dist/builtins/native-metrics/native-metrics.addon.d.ts.map +1 -0
  143. package/dist/builtins/native-metrics/native-metrics.addon.js +887 -861
  144. package/dist/builtins/native-metrics/native-metrics.addon.js.map +1 -1
  145. package/dist/builtins/native-metrics/native-metrics.addon.mjs +914 -5
  146. package/dist/builtins/native-metrics/native-metrics.addon.mjs.map +1 -1
  147. package/dist/builtins/platform-probe/index.d.ts +12 -0
  148. package/dist/builtins/platform-probe/index.d.ts.map +1 -0
  149. package/dist/builtins/platform-probe/index.js +539 -0
  150. package/dist/builtins/platform-probe/index.js.map +1 -0
  151. package/dist/builtins/platform-probe/index.mjs +530 -0
  152. package/dist/builtins/platform-probe/index.mjs.map +1 -0
  153. package/dist/builtins/platform-probe/inference-config-resolver.d.ts +30 -0
  154. package/dist/builtins/platform-probe/inference-config-resolver.d.ts.map +1 -0
  155. package/dist/builtins/platform-probe/platform-scorer.d.ts +22 -0
  156. package/dist/builtins/platform-probe/platform-scorer.d.ts.map +1 -0
  157. package/dist/builtins/remote-access-orchestrator/index.d.ts +2 -0
  158. package/dist/builtins/remote-access-orchestrator/index.d.ts.map +1 -0
  159. package/dist/builtins/remote-access-orchestrator/index.js +7 -0
  160. package/dist/builtins/remote-access-orchestrator/index.mjs +2 -0
  161. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.d.ts +9 -0
  162. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.d.ts.map +1 -0
  163. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +72 -0
  164. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js.map +1 -0
  165. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +66 -0
  166. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs.map +1 -0
  167. package/dist/builtins/snapshot/index.d.ts +3 -0
  168. package/dist/builtins/snapshot/index.d.ts.map +1 -0
  169. package/dist/builtins/snapshot/index.js +481 -491
  170. package/dist/builtins/snapshot/index.js.map +1 -1
  171. package/dist/builtins/snapshot/index.mjs +475 -464
  172. package/dist/builtins/snapshot/index.mjs.map +1 -1
  173. package/dist/builtins/snapshot/snapshot.addon.d.ts +121 -0
  174. package/dist/builtins/snapshot/snapshot.addon.d.ts.map +1 -0
  175. package/dist/builtins/sqlite-storage/config-store.d.ts +9 -0
  176. package/dist/builtins/sqlite-storage/config-store.d.ts.map +1 -0
  177. package/dist/builtins/sqlite-storage/device-store.d.ts +24 -0
  178. package/dist/builtins/sqlite-storage/device-store.d.ts.map +1 -0
  179. package/dist/builtins/sqlite-storage/filesystem-storage-provider.d.ts +87 -0
  180. package/dist/builtins/sqlite-storage/filesystem-storage-provider.d.ts.map +1 -0
  181. package/dist/builtins/sqlite-storage/filesystem-storage.addon.d.ts +32 -0
  182. package/dist/builtins/sqlite-storage/filesystem-storage.addon.d.ts.map +1 -0
  183. package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +312 -56
  184. package/dist/builtins/sqlite-storage/filesystem-storage.addon.js.map +1 -1
  185. package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +305 -7
  186. package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs.map +1 -1
  187. package/dist/builtins/sqlite-storage/index.d.ts +12 -0
  188. package/dist/builtins/sqlite-storage/index.d.ts.map +1 -0
  189. package/dist/builtins/sqlite-storage/index.js +229 -1001
  190. package/dist/builtins/sqlite-storage/index.js.map +1 -1
  191. package/dist/builtins/sqlite-storage/index.mjs +268 -26
  192. package/dist/builtins/sqlite-storage/index.mjs.map +1 -1
  193. package/dist/builtins/sqlite-storage/integration-registry.d.ts +28 -0
  194. package/dist/builtins/sqlite-storage/integration-registry.d.ts.map +1 -0
  195. package/dist/builtins/sqlite-storage/settings-store.d.ts +40 -0
  196. package/dist/builtins/sqlite-storage/settings-store.d.ts.map +1 -0
  197. package/dist/builtins/sqlite-storage/sql-schema.d.ts +33 -0
  198. package/dist/builtins/sqlite-storage/sql-schema.d.ts.map +1 -0
  199. package/dist/builtins/sqlite-storage/sqlite-settings-backend.d.ts +94 -0
  200. package/dist/builtins/sqlite-storage/sqlite-settings-backend.d.ts.map +1 -0
  201. package/dist/builtins/sqlite-storage/sqlite-settings.addon.d.ts +15 -0
  202. package/dist/builtins/sqlite-storage/sqlite-settings.addon.d.ts.map +1 -0
  203. package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +586 -653
  204. package/dist/builtins/sqlite-storage/sqlite-settings.addon.js.map +1 -1
  205. package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +582 -7
  206. package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs.map +1 -1
  207. package/dist/builtins/storage-orchestrator/index.d.ts +7 -0
  208. package/dist/builtins/storage-orchestrator/index.d.ts.map +1 -0
  209. package/dist/builtins/storage-orchestrator/index.js +9 -0
  210. package/dist/builtins/storage-orchestrator/index.mjs +2 -0
  211. package/dist/builtins/storage-orchestrator/location-store.d.ts +50 -0
  212. package/dist/builtins/storage-orchestrator/location-store.d.ts.map +1 -0
  213. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.d.ts +60 -0
  214. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.d.ts.map +1 -0
  215. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +755 -0
  216. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js.map +1 -0
  217. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +746 -0
  218. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs.map +1 -0
  219. package/dist/builtins/storage-orchestrator/storage-orchestrator.service.d.ts +121 -0
  220. package/dist/builtins/storage-orchestrator/storage-orchestrator.service.d.ts.map +1 -0
  221. package/dist/builtins/system-backup/system-backup.service.d.ts +138 -0
  222. package/dist/builtins/system-backup/system-backup.service.d.ts.map +1 -0
  223. package/dist/builtins/system-config/index.d.ts +2 -0
  224. package/dist/builtins/system-config/index.d.ts.map +1 -0
  225. package/dist/builtins/system-config/index.js +6 -188
  226. package/dist/builtins/system-config/index.mjs +2 -10
  227. package/dist/builtins/system-config/system-config.addon.d.ts +11 -0
  228. package/dist/builtins/system-config/system-config.addon.d.ts.map +1 -0
  229. package/dist/builtins/system-config/system-config.addon.js +227 -180
  230. package/dist/builtins/system-config/system-config.addon.js.map +1 -1
  231. package/dist/builtins/system-config/system-config.addon.mjs +226 -7
  232. package/dist/builtins/system-config/system-config.addon.mjs.map +1 -1
  233. package/dist/builtins/turn-orchestrator/index.d.ts +2 -0
  234. package/dist/builtins/turn-orchestrator/index.d.ts.map +1 -0
  235. package/dist/builtins/turn-orchestrator/index.js +7 -0
  236. package/dist/builtins/turn-orchestrator/index.mjs +2 -0
  237. package/dist/builtins/turn-orchestrator/turn-orchestrator.addon.d.ts +10 -0
  238. package/dist/builtins/turn-orchestrator/turn-orchestrator.addon.d.ts.map +1 -0
  239. package/dist/builtins/turn-orchestrator/turn-orchestrator.addon.js +78 -0
  240. package/dist/builtins/turn-orchestrator/turn-orchestrator.addon.js.map +1 -0
  241. package/dist/builtins/turn-orchestrator/turn-orchestrator.addon.mjs +72 -0
  242. package/dist/builtins/turn-orchestrator/turn-orchestrator.addon.mjs.map +1 -0
  243. package/dist/builtins/winston-logging/index.d.ts +4 -0
  244. package/dist/builtins/winston-logging/index.d.ts.map +1 -0
  245. package/dist/builtins/winston-logging/index.js +153 -300
  246. package/dist/builtins/winston-logging/index.js.map +1 -1
  247. package/dist/builtins/winston-logging/index.mjs +144 -9
  248. package/dist/builtins/winston-logging/index.mjs.map +1 -1
  249. package/dist/builtins/winston-logging/winston-destination.d.ts +22 -0
  250. package/dist/builtins/winston-logging/winston-destination.d.ts.map +1 -0
  251. package/dist/builtins/winston-logging/winston-logging.addon.d.ts +20 -0
  252. package/dist/builtins/winston-logging/winston-logging.addon.d.ts.map +1 -0
  253. package/dist/chunk-C13QxCFV.js +50 -0
  254. package/dist/chunk-hT5z_Zn9.mjs +35 -0
  255. package/dist/download/model-download-service.d.ts +42 -0
  256. package/dist/download/model-download-service.d.ts.map +1 -0
  257. package/dist/download/model-downloader.d.ts +32 -0
  258. package/dist/download/model-downloader.d.ts.map +1 -0
  259. package/dist/events/event-bus.d.ts +11 -0
  260. package/dist/events/event-bus.d.ts.map +1 -0
  261. package/dist/events/system-event-bus.d.ts +15 -0
  262. package/dist/events/system-event-bus.d.ts.map +1 -0
  263. package/dist/feature/feature-manager.d.ts +12 -0
  264. package/dist/feature/feature-manager.d.ts.map +1 -0
  265. package/dist/formatter-C-5An4Bl.mjs +164 -0
  266. package/dist/formatter-C-5An4Bl.mjs.map +1 -0
  267. package/dist/formatter-Dr_6NNZc.js +169 -0
  268. package/dist/formatter-Dr_6NNZc.js.map +1 -0
  269. package/dist/index.d.ts +76 -1696
  270. package/dist/index.d.ts.map +1 -0
  271. package/dist/index.js +7780 -8035
  272. package/dist/index.js.map +1 -1
  273. package/dist/index.mjs +7707 -2148
  274. package/dist/index.mjs.map +1 -1
  275. package/dist/lifecycle/lifecycle-state-machine.d.ts +29 -0
  276. package/dist/lifecycle/lifecycle-state-machine.d.ts.map +1 -0
  277. package/dist/logging/formatter.d.ts +31 -0
  278. package/dist/logging/formatter.d.ts.map +1 -0
  279. package/dist/logging/log-manager.d.ts +52 -0
  280. package/dist/logging/log-manager.d.ts.map +1 -0
  281. package/dist/logging/log-ring-buffer.d.ts +48 -0
  282. package/dist/logging/log-ring-buffer.d.ts.map +1 -0
  283. package/dist/logging/scoped-logger.d.ts +18 -0
  284. package/dist/logging/scoped-logger.d.ts.map +1 -0
  285. package/dist/network/network-quality.d.ts +12 -0
  286. package/dist/network/network-quality.d.ts.map +1 -0
  287. package/dist/notification/notification-service.d.ts +38 -0
  288. package/dist/notification/notification-service.d.ts.map +1 -0
  289. package/dist/notification/toast-service.d.ts +23 -0
  290. package/dist/notification/toast-service.d.ts.map +1 -0
  291. package/dist/pipeline/engine-manager-resolver.d.ts +16 -0
  292. package/dist/pipeline/engine-manager-resolver.d.ts.map +1 -0
  293. package/dist/pipeline/pipeline-runner.d.ts +9 -0
  294. package/dist/pipeline/pipeline-runner.d.ts.map +1 -0
  295. package/dist/pipeline/pipeline-validator.d.ts +14 -0
  296. package/dist/pipeline/pipeline-validator.d.ts.map +1 -0
  297. package/dist/process/resource-monitor.d.ts +12 -0
  298. package/dist/process/resource-monitor.d.ts.map +1 -0
  299. package/dist/python/python-env-manager.d.ts +13 -0
  300. package/dist/python/python-env-manager.d.ts.map +1 -0
  301. package/dist/repl/interfaces.d.ts +32 -0
  302. package/dist/repl/interfaces.d.ts.map +1 -0
  303. package/dist/repl/repl-engine.d.ts +9 -0
  304. package/dist/repl/repl-engine.d.ts.map +1 -0
  305. package/dist/resource-monitor-CmuWlmap.js +76 -0
  306. package/dist/resource-monitor-CmuWlmap.js.map +1 -0
  307. package/dist/resource-monitor-DcQdKGYU.mjs +59 -0
  308. package/dist/resource-monitor-DcQdKGYU.mjs.map +1 -0
  309. package/dist/storage/fs-storage-backend.d.ts +41 -0
  310. package/dist/storage/fs-storage-backend.d.ts.map +1 -0
  311. package/dist/storage/storage-location-manager.d.ts +24 -0
  312. package/dist/storage/storage-location-manager.d.ts.map +1 -0
  313. package/dist/storage/storage-manager.d.ts +77 -0
  314. package/dist/storage/storage-manager.d.ts.map +1 -0
  315. package/dist/tls/cert-manager.d.ts +27 -0
  316. package/dist/tls/cert-manager.d.ts.map +1 -0
  317. package/dist/tls/index.d.ts +2 -0
  318. package/dist/tls/index.d.ts.map +1 -0
  319. package/package.json +119 -23
  320. package/dist/builtins/addon-pages-aggregator/index.js.map +0 -1
  321. package/dist/builtins/addon-pages-aggregator/index.mjs.map +0 -1
  322. package/dist/builtins/addon-widgets-aggregator/index.js.map +0 -1
  323. package/dist/builtins/addon-widgets-aggregator/index.mjs.map +0 -1
  324. package/dist/builtins/alerts/index.js.map +0 -1
  325. package/dist/builtins/alerts/index.mjs.map +0 -1
  326. package/dist/builtins/device-manager/index.js.map +0 -1
  327. package/dist/builtins/device-manager/index.mjs.map +0 -1
  328. package/dist/builtins/local-auth/index.js.map +0 -1
  329. package/dist/builtins/local-auth/index.mjs.map +0 -1
  330. package/dist/builtins/local-backup/index.js +0 -173
  331. package/dist/builtins/local-backup/index.js.map +0 -1
  332. package/dist/builtins/local-backup/index.mjs +0 -10
  333. package/dist/builtins/local-backup/index.mjs.map +0 -1
  334. package/dist/builtins/system-config/index.js.map +0 -1
  335. package/dist/builtins/system-config/index.mjs.map +0 -1
  336. package/dist/chunk-2CIYKDRN.mjs +0 -1
  337. package/dist/chunk-2CIYKDRN.mjs.map +0 -1
  338. package/dist/chunk-2F76X6NL.mjs +0 -136
  339. package/dist/chunk-2F76X6NL.mjs.map +0 -1
  340. package/dist/chunk-2QUFBZ7M.mjs +0 -1
  341. package/dist/chunk-2QUFBZ7M.mjs.map +0 -1
  342. package/dist/chunk-3BK2Y7GY.mjs +0 -593
  343. package/dist/chunk-3BK2Y7GY.mjs.map +0 -1
  344. package/dist/chunk-4OOHFJHT.mjs +0 -421
  345. package/dist/chunk-4OOHFJHT.mjs.map +0 -1
  346. package/dist/chunk-4XHB7IHT.mjs +0 -809
  347. package/dist/chunk-4XHB7IHT.mjs.map +0 -1
  348. package/dist/chunk-6M2HSSTQ.mjs +0 -98
  349. package/dist/chunk-6M2HSSTQ.mjs.map +0 -1
  350. package/dist/chunk-7FI7SQS7.mjs +0 -135
  351. package/dist/chunk-7FI7SQS7.mjs.map +0 -1
  352. package/dist/chunk-ED57RCQE.mjs +0 -171
  353. package/dist/chunk-ED57RCQE.mjs.map +0 -1
  354. package/dist/chunk-FZN56HGQ.mjs +0 -626
  355. package/dist/chunk-FZN56HGQ.mjs.map +0 -1
  356. package/dist/chunk-GL4OOB25.mjs +0 -51
  357. package/dist/chunk-GL4OOB25.mjs.map +0 -1
  358. package/dist/chunk-KDG2NTDB.mjs +0 -137
  359. package/dist/chunk-KDG2NTDB.mjs.map +0 -1
  360. package/dist/chunk-NRBQWBDM.mjs +0 -191
  361. package/dist/chunk-NRBQWBDM.mjs.map +0 -1
  362. package/dist/chunk-O4V246GG.mjs +0 -2137
  363. package/dist/chunk-O4V246GG.mjs.map +0 -1
  364. package/dist/chunk-QT57H266.mjs +0 -163
  365. package/dist/chunk-QT57H266.mjs.map +0 -1
  366. package/dist/chunk-QX4RH25I.mjs +0 -141
  367. package/dist/chunk-QX4RH25I.mjs.map +0 -1
  368. package/dist/chunk-TB562PZX.mjs +0 -86
  369. package/dist/chunk-TB562PZX.mjs.map +0 -1
  370. package/dist/chunk-TDYPZXK5.mjs +0 -1
  371. package/dist/chunk-TDYPZXK5.mjs.map +0 -1
  372. package/dist/chunk-UJI4LN5P.mjs +0 -36
  373. package/dist/chunk-UJI4LN5P.mjs.map +0 -1
  374. package/dist/chunk-W6RTHQGP.mjs +0 -1
  375. package/dist/chunk-W6RTHQGP.mjs.map +0 -1
  376. package/dist/chunk-ZELBCPDC.mjs +0 -369
  377. package/dist/chunk-ZELBCPDC.mjs.map +0 -1
  378. package/dist/index.d.mts +0 -1696
  379. package/dist/resource-monitor-UZUGPIAU.mjs +0 -9
  380. package/dist/resource-monitor-UZUGPIAU.mjs.map +0 -1
  381. package/dist/storage-location-manager-HFNB3PCS.mjs +0 -7
  382. package/dist/storage-location-manager-HFNB3PCS.mjs.map +0 -1
@@ -1,9 +1,2147 @@
1
- import {
2
- DeviceManagerAddon,
3
- device_manager_addon_default
4
- } from "../../chunk-O4V246GG.mjs";
5
- export {
6
- DeviceManagerAddon,
7
- device_manager_addon_default as default
1
+ import { randomUUID } from "node:crypto";
2
+ import { BaseAddon, CAP_NAMES_WITH_STATUS, DeviceFeature, DeviceType, EventCategory, WELL_KNOWN_TAB_MAP, deviceManagerCapability, deviceStateCapability, errMsg } from "@camstack/types";
3
+ //#region src/builtins/device-manager/device-event-propagator.ts
4
+ /**
5
+ * Walks the parent chain for every device-sourced event and re-emits a
6
+ * copy on each ancestor scope with `via[]` populated.
7
+ *
8
+ * Design goals:
9
+ * - Transparent: drivers emit once on their own device scope; the
10
+ * framework handles fan-out. Zero provider boilerplate.
11
+ * - Anti-loop: events that already carry `via[]` are skipped (we only
12
+ * propagate ORIGINAL emissions).
13
+ * - Anti-cycle: the parent chain is bounded — if the device registry
14
+ * is corrupt and has a cycle, the walker caps at `MAX_CHAIN_DEPTH`
15
+ * and logs a warning.
16
+ * - Lazy: parent chain is resolved on-demand per event (no cached
17
+ * topology). The lookup is O(depth) which is ≤2 in practice.
18
+ *
19
+ * `via` contract (from SystemEvent.via JSDoc):
20
+ * - `via[0]` is the originating source (the device that produced the
21
+ * event). Subsequent entries walk up the parent chain.
22
+ * - On the re-emission, `source` is the ancestor at that level and
23
+ * `via[0..i]` is the prefix of the chain up to and including the
24
+ * first N ancestors below the current one.
25
+ *
26
+ * Example (grandchild → parent → grandparent):
27
+ * Original: { source: {id: 7}, data: {...}, via: undefined }
28
+ * Re-emit 1: { source: {id: 4}, data: {...}, via: [{id: 7}] }
29
+ * Re-emit 2: { source: {id: 1}, data: {...}, via: [{id: 7}, {id: 4}] }
30
+ *
31
+ * A consumer listening at `source.id === 1` receives re-emit 2 (with
32
+ * `via` showing the chain). A consumer listening at `source.id === 7`
33
+ * with `via === undefined` receives the original only.
34
+ */
35
+ /** Bounded walk — paranoia against corrupt device registries with cycles. */
36
+ var MAX_CHAIN_DEPTH = 16;
37
+ var DeviceEventPropagator = class {
38
+ unsubscribe = null;
39
+ constructor(opts) {
40
+ this.opts = opts;
41
+ }
42
+ start() {
43
+ if (this.unsubscribe) return;
44
+ const unsub = this.opts.eventBus.subscribe({}, (ev) => this.handle(ev));
45
+ this.unsubscribe = unsub;
46
+ }
47
+ stop() {
48
+ if (!this.unsubscribe) return;
49
+ this.unsubscribe();
50
+ this.unsubscribe = null;
51
+ }
52
+ /** Exposed for tests — lets them inject events without the full bus. */
53
+ handle(ev) {
54
+ if (ev.via !== void 0) return;
55
+ if (ev.source.type !== "device") return;
56
+ const rawId = ev.source.id;
57
+ const deviceId = typeof rawId === "number" ? rawId : Number(rawId);
58
+ if (!Number.isFinite(deviceId)) return;
59
+ const chain = this.resolveParentChain(deviceId);
60
+ if (chain.length === 0) return;
61
+ const via = [ev.source];
62
+ for (const ancestorId of chain) {
63
+ const reEmission = {
64
+ ...ev,
65
+ source: {
66
+ type: "device",
67
+ id: ancestorId
68
+ },
69
+ via: [...via]
70
+ };
71
+ this.opts.eventBus.emit(reEmission);
72
+ via.push({
73
+ type: "device",
74
+ id: ancestorId
75
+ });
76
+ }
77
+ }
78
+ resolveParentChain(deviceId) {
79
+ const chain = [];
80
+ const seen = new Set([deviceId]);
81
+ let current = this.opts.getParentOf(deviceId);
82
+ while (current != null) {
83
+ if (seen.has(current)) {
84
+ this.opts.logger.warn("device-event-propagator: cycle detected in parent chain — aborting propagation", {
85
+ tags: { deviceId },
86
+ meta: {
87
+ cycleAt: current,
88
+ chainSoFar: [...chain]
89
+ }
90
+ });
91
+ return chain;
92
+ }
93
+ seen.add(current);
94
+ chain.push(current);
95
+ if (chain.length >= MAX_CHAIN_DEPTH) {
96
+ this.opts.logger.warn("device-event-propagator: chain depth limit hit — truncating", {
97
+ tags: { deviceId },
98
+ meta: {
99
+ depth: chain.length,
100
+ max: MAX_CHAIN_DEPTH
101
+ }
102
+ });
103
+ break;
104
+ }
105
+ current = this.opts.getParentOf(current);
106
+ }
107
+ return chain;
108
+ }
8
109
  };
110
+ //#endregion
111
+ //#region src/builtins/device-manager/device-manager.addon.ts
112
+ /**
113
+ * Device Manager addon — hub-side singleton that unifies device persistence,
114
+ * live registry queries, and all management operations into a single
115
+ * tRPC-routable capability.
116
+ *
117
+ * Persistence strategy: all device data is stored via `ctx.settings`, the same
118
+ * settings API every other addon uses. No raw SQLite access.
119
+ *
120
+ * Addon store layout:
121
+ * deviceIndex → Record<addonId, stableId[]> (which devices exist per addon)
122
+ * deviceMeta → Record<"addonId:stableId", DeviceMeta> (type, name, parentDeviceId, id)
123
+ *
124
+ * Device store (per-device config):
125
+ * readDeviceStore(numericDeviceId) → config blob
126
+ * writeDeviceStore(numericDeviceId, patch)
127
+ *
128
+ * Live registry: resolved from the kernel capability registry after Phase 2.
129
+ * This gives direct access to in-memory IDevice instances registered by provider addons.
130
+ * The DeviceManagerAddon is the single owner of the live device operations API.
131
+ *
132
+ * Replaces:
133
+ * - `device-persistence` capability (absorbed here)
134
+ * - live operations previously served by `device-management.router.ts`
135
+ */
136
+ function shallowEqual(a, b) {
137
+ const ak = Object.keys(a);
138
+ const bk = Object.keys(b);
139
+ if (ak.length !== bk.length) return false;
140
+ for (const k of ak) if (a[k] !== b[k]) return false;
141
+ return true;
142
+ }
143
+ function deviceKey(addonId, stableId) {
144
+ return `${addonId}:${stableId}`;
145
+ }
146
+ function isCameraDevice(device) {
147
+ return "getStreamSources" in device && typeof device.getStreamSources === "function";
148
+ }
149
+ var DEVICE_FEATURE_VALUES = new Set(Object.values(DeviceFeature));
150
+ /**
151
+ * Validate persisted feature strings against the `DeviceFeature` enum
152
+ * — workers serialise the live `device.features` array (so every entry
153
+ * is a valid enum value at write time) but the persisted blob is loose
154
+ * `string[]` on the wire. The narrow keeps unknown values out of the
155
+ * `getDevice` response without losing the enum-typed contract.
156
+ */
157
+ function persistedFeatures(features) {
158
+ if (!features) return [];
159
+ const out = [];
160
+ for (const f of features) if (DEVICE_FEATURE_VALUES.has(f)) out.push(f);
161
+ return out;
162
+ }
163
+ function toDeviceInfo(addonId, device, metadata = null, metaRow = null) {
164
+ const configValues = {};
165
+ for (const entry of device.config.entries()) configValues[entry.key] = entry.value;
166
+ const name = metaRow?.name ?? device.name;
167
+ const location = metaRow?.location !== void 0 ? metaRow.location : device.location;
168
+ const disabled = metaRow?.disabled ?? device.disabled;
169
+ return {
170
+ id: device.id,
171
+ stableId: device.stableId,
172
+ addonId,
173
+ type: device.type,
174
+ name,
175
+ location,
176
+ disabled,
177
+ parentDeviceId: device.parentDeviceId,
178
+ role: device.role ?? null,
179
+ online: device.online,
180
+ features: [...device.features],
181
+ isCamera: isCameraDevice(device),
182
+ config: configValues,
183
+ metadata
184
+ };
185
+ }
186
+ function resolveDeviceById(registry, deviceId) {
187
+ const device = registry.getById(deviceId);
188
+ if (!device) return null;
189
+ const addonId = registry.getAddonId(deviceId);
190
+ if (!addonId) return null;
191
+ return {
192
+ addonId,
193
+ device
194
+ };
195
+ }
196
+ /**
197
+ * Walk the sections/fields of a contribution and inject `writerCapName` +
198
+ * `writerAddonId` + `source` on each editable field. Readonly fields and
199
+ * structural fields (separator/info/button) pass through untouched. The
200
+ * aggregator is the single place that knows provenance — provider schemas
201
+ * stay clean, UI-bound metadata is attached once at the boundary.
202
+ */
203
+ function tagContribution(contribution, capName, addonId, kind) {
204
+ const source = kind === "settings" ? "settings" : "live";
205
+ return {
206
+ ...contribution.tabs ? { tabs: [...contribution.tabs] } : {},
207
+ sections: contribution.sections.map((section) => ({
208
+ ...section,
209
+ fields: section.fields.map((field) => tagField(field, capName, addonId, source, kind))
210
+ }))
211
+ };
212
+ }
213
+ function isFieldRecord(value) {
214
+ return value !== null && typeof value === "object" && !Array.isArray(value);
215
+ }
216
+ /**
217
+ * Convert a strict `ConfigUISchemaWithValues` (readonly arrays, typed
218
+ * field union) into the cap wire shape `ContributionShape` (mutable
219
+ * arrays, opaque field records). Required because the cap method z.infer
220
+ * uses mutable arrays — readonly arrays are not assignable to mutable
221
+ * even when structurally identical, so a structural copy bridges the gap
222
+ * without disabling the type checker.
223
+ */
224
+ function toWireShape(input) {
225
+ const out = { sections: input.sections.map((s) => ({
226
+ id: s.id,
227
+ title: s.title,
228
+ ...s.description !== void 0 ? { description: s.description } : {},
229
+ ...s.style !== void 0 ? { style: s.style } : {},
230
+ ...s.defaultCollapsed !== void 0 ? { defaultCollapsed: s.defaultCollapsed } : {},
231
+ ...s.columns !== void 0 ? { columns: s.columns } : {},
232
+ ...s.tab !== void 0 ? { tab: s.tab } : {},
233
+ ...s.location !== void 0 ? { location: s.location } : {},
234
+ ...s.order !== void 0 ? { order: s.order } : {},
235
+ fields: [...s.fields]
236
+ })) };
237
+ if (input.tabs) out.tabs = [...input.tabs];
238
+ return out;
239
+ }
240
+ function tagField(field, capName, addonId, source, kind) {
241
+ if (!isFieldRecord(field)) return field;
242
+ const f = field;
243
+ const structuralTypes = new Set([
244
+ "separator",
245
+ "info",
246
+ "button"
247
+ ]);
248
+ if (typeof f.type === "string" && structuralTypes.has(f.type)) return field;
249
+ const tagged = {
250
+ ...f,
251
+ source
252
+ };
253
+ if (kind === "live" || f.readonlyField === true) tagged.readonlyField = true;
254
+ else {
255
+ tagged.writerCapName = capName;
256
+ tagged.writerAddonId = addonId;
257
+ }
258
+ if (f.type === "group") {
259
+ const children = Array.isArray(f.fields) ? f.fields : [];
260
+ if (children.length > 0) tagged.fields = children.map((child) => tagField(child, capName, addonId, source, kind));
261
+ } else if (f.type === "sub-tabs") {
262
+ const rawTabs = Array.isArray(f.tabs) ? f.tabs : [];
263
+ if (rawTabs.length > 0) tagged.tabs = rawTabs.map((tab) => {
264
+ if (!isFieldRecord(tab)) return tab;
265
+ const tabChildren = Array.isArray(tab.fields) ? tab.fields : [];
266
+ return {
267
+ ...tab,
268
+ fields: tabChildren.map((child) => tagField(child, capName, addonId, source, kind))
269
+ };
270
+ });
271
+ }
272
+ return tagged;
273
+ }
274
+ function mergeAggregates(parts) {
275
+ const tabDecls = /* @__PURE__ */ new Map();
276
+ const sections = [];
277
+ for (const part of parts) {
278
+ if (part.tabs) {
279
+ for (const t of part.tabs) if (!tabDecls.has(t.id)) tabDecls.set(t.id, t);
280
+ }
281
+ for (const s of part.sections) sections.push(s);
282
+ }
283
+ for (const s of sections) {
284
+ const tabId = s.tab ?? "general";
285
+ if (tabDecls.has(tabId)) continue;
286
+ const known = WELL_KNOWN_TAB_MAP[tabId];
287
+ if (known) tabDecls.set(tabId, {
288
+ id: known.id,
289
+ label: known.label,
290
+ icon: known.icon,
291
+ order: known.order
292
+ });
293
+ else tabDecls.set(tabId, {
294
+ id: tabId,
295
+ label: tabId,
296
+ icon: "wrench",
297
+ order: 100
298
+ });
299
+ }
300
+ sections.sort((a, b) => {
301
+ const tabA = a.tab ?? "general";
302
+ const tabB = b.tab ?? "general";
303
+ if (tabA !== tabB) {
304
+ const orderA = tabDecls.get(tabA)?.order ?? 100;
305
+ const orderB = tabDecls.get(tabB)?.order ?? 100;
306
+ if (orderA !== orderB) return orderA - orderB;
307
+ return tabA.localeCompare(tabB);
308
+ }
309
+ return (a.order ?? 0) - (b.order ?? 0);
310
+ });
311
+ const sortedTabs = [...tabDecls.values()].sort((a, b) => (a.order ?? 100) - (b.order ?? 100));
312
+ const out = { sections };
313
+ if (sortedTabs.length > 0) out.tabs = sortedTabs;
314
+ return out;
315
+ }
316
+ var DeviceManagerAddon = class DeviceManagerAddon extends BaseAddon {
317
+ constructor() {
318
+ super({});
319
+ }
320
+ /** Shorthand for the kernel-injected capability registry. */
321
+ get capabilityRegistry() {
322
+ return this.ctx.kernel.capabilityRegistry;
323
+ }
324
+ /**
325
+ * Parent-chain event propagator. Started in `onInitialize` once the
326
+ * hub's `deviceRegistry` is available; listens to every device-sourced
327
+ * event and re-emits a copy on each ancestor scope with `via[]`
328
+ * populated. Stopped in `onShutdown`.
329
+ */
330
+ propagator = null;
331
+ /**
332
+ * Hub-side mirror of every device's cap-keyed runtime state.
333
+ * Populated whenever any caller writes via `deviceState.setCapSlice`
334
+ * (the canonical cross-layer write entrypoint) and on first load
335
+ * via `loadRuntimeState`. Cross-process consumers reach the mirror
336
+ * through the `deviceState` cap router; per-cap event subscribers
337
+ * (e.g. `battery.onStatusChanged`) get the same data via
338
+ * cap-specific events still emitted by the owning device.
339
+ *
340
+ * Key: deviceId. Value: per-cap slice map. Empty by default —
341
+ * slices show up as `setCapSlice` calls trickle in.
342
+ */
343
+ stateMirror = /* @__PURE__ */ new Map();
344
+ /**
345
+ * Per-device disk-write debouncer for runtime-state. `setCapSlice`
346
+ * updates the in-memory mirror synchronously and emits the change
347
+ * event immediately, but the disk write is coalesced — frequent
348
+ * back-to-back writes (motion phase transitions, battery pushes,
349
+ * etc.) collapse to one `writeDeviceRuntimeState` per
350
+ * `RUNTIME_STATE_DEBOUNCE_MS` window. `flushRuntimeStateWrites`
351
+ * awaits any in-flight write + scheduled flush so shutdown is
352
+ * lossless.
353
+ */
354
+ runtimeStateDebounce = /* @__PURE__ */ new Map();
355
+ static RUNTIME_STATE_DEBOUNCE_MS = 1e3;
356
+ /**
357
+ * Cross-process native-provider cache: deviceId (numeric) → capName → { addonId, nodeId }.
358
+ * Kept in sync with `device.bindings-changed` events emitted by forked
359
+ * workers. Union'd into `getBindings` so hub-side consumers see every
360
+ * native cap regardless of which process owns the IDevice. No persistence
361
+ * — entries re-register when the worker restarts. Purged on
362
+ * `$node.disconnected` to avoid stale routing after a worker crash.
363
+ */
364
+ remoteNativeCaps = /* @__PURE__ */ new Map();
365
+ /** Wait for a device-provider by addonId, returning null on timeout. */
366
+ async waitDeviceProvider(addonId, timeoutMs = 5e3) {
367
+ const provider = await this.capabilityRegistry?.waitForProvider("device-provider", addonId, timeoutMs);
368
+ return provider ? provider : null;
369
+ }
370
+ /** Require a device-provider by addonId — throws if not found. */
371
+ async requireDeviceProvider(addonId) {
372
+ const dp = await this.waitDeviceProvider(addonId);
373
+ if (!dp) throw new Error(`Device provider "${addonId}" not found or not registered`);
374
+ return dp;
375
+ }
376
+ async readBindingsStore() {
377
+ return { deviceBindings: (await this.ctx.settings.readAddonStore()).deviceBindings ?? {} };
378
+ }
379
+ async writeBindingsStore(next) {
380
+ await this.ctx.settings.writeAddonStore({ deviceBindings: next.deviceBindings });
381
+ }
382
+ resolveWrapperNodeId(_wrapperAddonId) {
383
+ return "hub";
384
+ }
385
+ /**
386
+ * Active discovery of native caps registered on a worker node.
387
+ * Called from the `$node.connected` handler to recover from lost
388
+ * `DeviceBindingsChanged` broadcasts after worker restart.
389
+ *
390
+ * Iterates the broker's service registry, picks
391
+ * `<addonId>.native-provider.<capName>` services owned by the
392
+ * connected node, calls each service's `$listDeviceIds` action to
393
+ * fetch the deviceIds the worker has registered that cap on, then
394
+ * writes entries into `remoteNativeCaps`.
395
+ *
396
+ * Best-effort: per-service failures are logged and swallowed so a
397
+ * single bad service doesn't abort the whole rebuild.
398
+ */
399
+ async discoverWorkerNativeCaps(connectedNodeId, _connectedAddonId) {
400
+ const cluster = this.ctx.kernel.cluster;
401
+ if (!cluster) return;
402
+ const broker = cluster.broker;
403
+ const services = broker.registry?.getServiceList?.({
404
+ onlyAvailable: true,
405
+ withActions: false
406
+ }) ?? [];
407
+ const NATIVE_INFIX = ".native-provider.";
408
+ const matched = services.filter((s) => s.nodeID === connectedNodeId && s.name.includes(NATIVE_INFIX));
409
+ if (matched.length === 0) return;
410
+ for (const svc of matched) {
411
+ const idx = svc.name.indexOf(NATIVE_INFIX);
412
+ if (idx <= 0) continue;
413
+ const addonId = svc.name.slice(0, idx);
414
+ const capName = svc.name.slice(idx + 17);
415
+ if (!addonId || !capName) continue;
416
+ try {
417
+ const action = `${svc.name}.$listDeviceIds`;
418
+ const deviceIds = await broker.call?.(action, {}, { nodeID: connectedNodeId });
419
+ if (!deviceIds || deviceIds.length === 0) continue;
420
+ for (const deviceId of deviceIds) {
421
+ if (this.capabilityRegistry?.getNativeAddonId(capName, deviceId)) continue;
422
+ let perDevice = this.remoteNativeCaps.get(deviceId);
423
+ if (!perDevice) {
424
+ perDevice = /* @__PURE__ */ new Map();
425
+ this.remoteNativeCaps.set(deviceId, perDevice);
426
+ }
427
+ perDevice.set(capName, {
428
+ addonId,
429
+ nodeId: connectedNodeId
430
+ });
431
+ }
432
+ this.ctx.logger.debug("worker native-cap discovered", { meta: {
433
+ nodeId: connectedNodeId,
434
+ addonId,
435
+ capName,
436
+ deviceIds
437
+ } });
438
+ } catch (err) {
439
+ this.ctx.logger.debug("worker native-cap $listDeviceIds failed", { meta: {
440
+ service: svc.name,
441
+ nodeId: connectedNodeId,
442
+ error: errMsg(err)
443
+ } });
444
+ }
445
+ }
446
+ this.ctx.logger.info("worker native-cap discovery completed", { meta: {
447
+ nodeId: connectedNodeId,
448
+ services: matched.length
449
+ } });
450
+ }
451
+ async getBindings(input) {
452
+ const storeKey = String(input.deviceId);
453
+ const perDevice = (await this.readBindingsStore()).deviceBindings[storeKey] ?? {};
454
+ const entries = [];
455
+ const seenCaps = /* @__PURE__ */ new Set();
456
+ for (const [capName, { wrapperAddonId }] of Object.entries(perDevice)) {
457
+ const hubLocalNative = this.capabilityRegistry?.getNativeAddonId(capName, input.deviceId) ?? null;
458
+ const remoteNative = this.remoteNativeCaps.get(input.deviceId)?.get(capName) ?? null;
459
+ const nativeAddonId = hubLocalNative ?? remoteNative?.addonId ?? "";
460
+ const nativeNodeId = hubLocalNative ? this.ctx.kernel.localNodeId ?? "hub" : remoteNative?.nodeId ?? this.ctx.kernel.localNodeId ?? "hub";
461
+ if (wrapperAddonId === null && !nativeAddonId) {
462
+ seenCaps.add(capName);
463
+ continue;
464
+ }
465
+ entries.push({
466
+ capName,
467
+ kind: wrapperAddonId ? "wrapped" : "native",
468
+ providerAddonId: wrapperAddonId ?? nativeAddonId,
469
+ providerNodeId: wrapperAddonId ? this.resolveWrapperNodeId(wrapperAddonId) : nativeNodeId,
470
+ nativeAddonId
471
+ });
472
+ seenCaps.add(capName);
473
+ }
474
+ const remote = this.remoteNativeCaps.get(input.deviceId);
475
+ if (this.capabilityRegistry) for (const capName of this.capabilityRegistry.getCapsWithDefaultWrapper()) {
476
+ if (seenCaps.has(capName)) continue;
477
+ const defaultWrapperAddonId = this.capabilityRegistry.getDefaultWrapperForCap(capName);
478
+ if (!defaultWrapperAddonId) continue;
479
+ const hubLocalNative = this.capabilityRegistry.getNativeAddonId(capName, input.deviceId) ?? null;
480
+ const remoteNative = remote?.get(capName) ?? null;
481
+ const nativeAddonId = hubLocalNative ?? remoteNative?.addonId ?? "";
482
+ entries.push({
483
+ capName,
484
+ kind: "wrapped",
485
+ providerAddonId: defaultWrapperAddonId,
486
+ providerNodeId: this.resolveWrapperNodeId(defaultWrapperAddonId),
487
+ nativeAddonId
488
+ });
489
+ seenCaps.add(capName);
490
+ }
491
+ if (this.capabilityRegistry) for (const capName of this.capabilityRegistry.getNativeCapsForDevice(input.deviceId)) {
492
+ if (seenCaps.has(capName)) continue;
493
+ const nativeAddonId = this.capabilityRegistry.getNativeAddonId(capName, input.deviceId) ?? "";
494
+ entries.push({
495
+ capName,
496
+ kind: "native",
497
+ providerAddonId: nativeAddonId,
498
+ providerNodeId: this.ctx.kernel.localNodeId ?? "hub",
499
+ nativeAddonId
500
+ });
501
+ seenCaps.add(capName);
502
+ }
503
+ if (remote) for (const [capName, info] of remote) {
504
+ if (seenCaps.has(capName)) continue;
505
+ entries.push({
506
+ capName,
507
+ kind: "native",
508
+ providerAddonId: info.addonId,
509
+ providerNodeId: info.nodeId,
510
+ nativeAddonId: info.addonId
511
+ });
512
+ seenCaps.add(capName);
513
+ }
514
+ return {
515
+ deviceId: input.deviceId,
516
+ entries
517
+ };
518
+ }
519
+ /**
520
+ * Whole-fleet binding dump. Iterates every device known to the
521
+ * deviceRegistry and reuses the per-device `getBindings` resolver
522
+ * for each — same routing rules, single round-trip. Used by
523
+ * `SystemManager.init()` for warm-boot.
524
+ *
525
+ * Bindings change rarely (wrapper toggle, device add/remove) so
526
+ * clients invalidate via the existing
527
+ * `capability.binding-changed` event rather than re-fetching this
528
+ * payload periodically.
529
+ */
530
+ async getAllBindings() {
531
+ const hubRegistry = this.ctx.kernel?.deviceRegistry;
532
+ if (!hubRegistry) return [];
533
+ const out = [];
534
+ for (const device of hubRegistry.getAll()) out.push(await this.getBindings({ deviceId: device.id }));
535
+ return out;
536
+ }
537
+ /**
538
+ * Resolve a numeric deviceId to a stableId via persisted meta.
539
+ * Used only by the device-identity section of the device-details
540
+ * aggregator (see `buildBaseDeviceSection`) to surface the stableId as
541
+ * a readonly display field. All runtime/registry lookups are keyed by
542
+ * numeric deviceId; this helper is display-only.
543
+ */
544
+ async lookupPersistedStableId(deviceId) {
545
+ const meta = (await this.ctx.settings.readAddonStore()).deviceMeta ?? {};
546
+ for (const [key, m] of Object.entries(meta)) if (m.id === deviceId) {
547
+ const sep = key.indexOf(":");
548
+ if (sep < 0) continue;
549
+ return key.slice(sep + 1);
550
+ }
551
+ }
552
+ async getDeviceAggregate(deviceId, kind) {
553
+ const registry = this.capabilityRegistry;
554
+ if (!registry) {
555
+ this.ctx.logger.debug("capability registry unavailable — aggregate empty", { meta: { kind } });
556
+ return null;
557
+ }
558
+ const method = kind === "settings" ? "getDeviceSettingsContribution" : "getDeviceLiveContribution";
559
+ const contributors = [];
560
+ for (const info of registry.listCapabilities()) {
561
+ if (!registry.getDefinition(info.name)?.exposesDeviceSettings) continue;
562
+ const seen = /* @__PURE__ */ new Set();
563
+ const sorted = [...info.providers].sort((a, b) => a.length - b.length);
564
+ for (const addonId of sorted) {
565
+ if (addonId.includes("::native-")) continue;
566
+ const baseId = addonId.includes("@") ? addonId.slice(0, addonId.indexOf("@")) : addonId;
567
+ if (seen.has(baseId)) continue;
568
+ seen.add(baseId);
569
+ contributors.push({
570
+ capName: info.name,
571
+ addonId
572
+ });
573
+ }
574
+ }
575
+ const results = await Promise.all(contributors.map(async ({ capName, addonId }) => {
576
+ const provider = registry.getProviderByAddon(capName, addonId);
577
+ if (!provider) throw new Error(`[device-manager] capability "${capName}" lists provider "${addonId}" but getProviderByAddon returned null — registry inconsistency`);
578
+ try {
579
+ const contribution = await provider[method]({ deviceId });
580
+ if (!contribution) return null;
581
+ return {
582
+ capName,
583
+ addonId,
584
+ contribution: tagContribution(toWireShape(contribution), capName, addonId, kind)
585
+ };
586
+ } catch (err) {
587
+ const msg = err instanceof Error ? err.message : String(err);
588
+ this.ctx.logger.warn("contribution method failed", {
589
+ tags: {
590
+ deviceId,
591
+ addonId
592
+ },
593
+ meta: {
594
+ capName,
595
+ method,
596
+ error: msg
597
+ }
598
+ });
599
+ return null;
600
+ }
601
+ }));
602
+ const base = kind === "settings" ? await this.buildBaseDeviceSection(deviceId) : null;
603
+ const parts = [...base ? [tagContribution(base, "device-manager", "device-manager", kind)] : [], ...results.filter((r) => r !== null).map((r) => r.contribution)];
604
+ if (parts.length === 0) return null;
605
+ return mergeAggregates(parts);
606
+ }
607
+ /**
608
+ * Build the device-manager's own contribution to the aggregator — the
609
+ * device identity (id, stableId, addonId, type, online) + the
610
+ * driver-specific config exposed by the device class via
611
+ * `zodEntriesToConfigUI`.
612
+ *
613
+ * Two paths, deliberately symmetric with `getSettingsSchema`:
614
+ *
615
+ * - Hub-local: device's IDevice instance lives in this process'
616
+ * DeviceRegistry, we read config + schema directly by reference.
617
+ * - Cross-process: device lives in a forked worker (RtspCamera on
618
+ * provider-rtsp, ONVIF on provider-onvif, …). We ask the worker's
619
+ * `device-ops.getSettingsSchema` native provider for a wire-
620
+ * serializable ConfigUISchema and merge it in under the same
621
+ * "Driver Config" section, so the UI sees the same shape regardless
622
+ * of where the IDevice physically runs.
623
+ *
624
+ * Returns `null` only when the device genuinely doesn't exist anywhere
625
+ * (no hub-local, no persisted ownership, no device-ops native). The
626
+ * aggregator falls back to contributor sections only in that case.
627
+ */
628
+ async buildBaseDeviceSection(deviceId) {
629
+ const hubRegistry = this.ctx.kernel?.deviceRegistry;
630
+ const hubLocal = hubRegistry ? resolveDeviceById(hubRegistry, deviceId) : null;
631
+ const stableId = hubLocal?.device.stableId ?? await this.lookupPersistedStableId(deviceId);
632
+ const nativeOwner = this.resolveNativeDeviceOwner(deviceId);
633
+ const addonId = hubLocal?.addonId ?? nativeOwner?.addonId ?? null;
634
+ if (!hubLocal && !nativeOwner) return null;
635
+ const sections = [{
636
+ id: "device-identity",
637
+ title: "Identity",
638
+ tab: "general",
639
+ order: 0,
640
+ fields: [
641
+ {
642
+ type: "text",
643
+ key: "_deviceId",
644
+ label: "Device ID",
645
+ readonlyField: true,
646
+ value: String(deviceId)
647
+ },
648
+ {
649
+ type: "text",
650
+ key: "_stableId",
651
+ label: "Stable ID",
652
+ readonlyField: true,
653
+ value: stableId ?? ""
654
+ },
655
+ {
656
+ type: "text",
657
+ key: "_addonId",
658
+ label: "Driver",
659
+ readonlyField: true,
660
+ value: addonId ?? "unknown"
661
+ },
662
+ ...hubLocal ? [{
663
+ type: "text",
664
+ key: "_type",
665
+ label: "Type",
666
+ readonlyField: true,
667
+ value: hubLocal.device.type
668
+ }, {
669
+ type: "text",
670
+ key: "_online",
671
+ label: "Online",
672
+ readonlyField: true,
673
+ value: hubLocal.device.online ? "yes" : "no"
674
+ }] : []
675
+ ]
676
+ }];
677
+ const driverSchema = await this.resolveDriverConfigSchema(deviceId, hubLocal);
678
+ if (driverSchema) for (const section of driverSchema.sections) sections.push({
679
+ id: section.id,
680
+ title: section.title,
681
+ tab: section.tab ?? "general",
682
+ order: section.order ?? 1,
683
+ fields: [...section.fields],
684
+ ...section.description !== void 0 ? { description: section.description } : {},
685
+ ...section.columns !== void 0 ? { columns: section.columns } : {}
686
+ });
687
+ return {
688
+ sections,
689
+ ...driverSchema?.tabs ? { tabs: [...driverSchema.tabs] } : {}
690
+ };
691
+ }
692
+ /**
693
+ * Lookup the native owner for `device-ops` on `deviceId` — the native-cap
694
+ * registry (hub-local and remote) is keyed by numeric id.
695
+ */
696
+ resolveNativeDeviceOwner(deviceId) {
697
+ const local = this.capabilityRegistry?.getNativeAddonId("device-ops", deviceId) ?? null;
698
+ if (local) return {
699
+ addonId: local,
700
+ nodeId: this.ctx.kernel.localNodeId ?? "hub"
701
+ };
702
+ const remote = this.remoteNativeCaps.get(deviceId)?.get("device-ops") ?? null;
703
+ return remote ? {
704
+ addonId: remote.addonId,
705
+ nodeId: remote.nodeId
706
+ } : null;
707
+ }
708
+ /**
709
+ * Aggregate `status` across every registered cap for a device.
710
+ *
711
+ * Walks the supplied cap list (or `CAP_NAMES_WITH_STATUS` when
712
+ * omitted), looks up a native provider per cap via the capability
713
+ * registry, calls `provider.getStatus({ deviceId })`, and validates
714
+ * the return against the cap's own `status.schema`. Validation
715
+ * failures log a warning and yield `null` for that cap so the
716
+ * overall aggregate stays usable — a single misbehaving provider
717
+ * must not blank out a device's entire status view.
718
+ *
719
+ * Returned shape is `Record<capName, unknown | null>`; the client-
720
+ * side hook tightens this to `CapStatusTypeMap` via the generated
721
+ * `cap-status-types.ts`.
722
+ */
723
+ async getDeviceStatusAggregate(input) {
724
+ const capNames = input.caps ?? CAP_NAMES_WITH_STATUS;
725
+ const registry = this.capabilityRegistry;
726
+ const out = {};
727
+ if (!registry) {
728
+ for (const name of capNames) out[name] = null;
729
+ return out;
730
+ }
731
+ await Promise.all(capNames.map(async (capName) => {
732
+ try {
733
+ const def = registry.getDefinition(capName);
734
+ if (!def?.status) {
735
+ out[capName] = null;
736
+ return;
737
+ }
738
+ const provider = registry.getNativeProvider(capName, input.deviceId);
739
+ if (!provider || typeof provider.getStatus !== "function") {
740
+ out[capName] = null;
741
+ return;
742
+ }
743
+ const raw = await provider.getStatus({ deviceId: input.deviceId });
744
+ if (raw == null) {
745
+ out[capName] = null;
746
+ return;
747
+ }
748
+ const parsed = def.status.schema.safeParse(raw);
749
+ if (!parsed.success) {
750
+ this.ctx.logger.warn("getDeviceStatusAggregate: provider returned invalid status, dropping", {
751
+ tags: { deviceId: input.deviceId },
752
+ meta: {
753
+ capName,
754
+ issues: parsed.error.issues.slice(0, 3)
755
+ }
756
+ });
757
+ out[capName] = null;
758
+ return;
759
+ }
760
+ out[capName] = parsed.data;
761
+ } catch (err) {
762
+ this.ctx.logger.warn("getDeviceStatusAggregate: provider threw, dropping", {
763
+ tags: { deviceId: input.deviceId },
764
+ meta: {
765
+ capName,
766
+ error: errMsg(err)
767
+ }
768
+ });
769
+ out[capName] = null;
770
+ }
771
+ }));
772
+ return out;
773
+ }
774
+ /**
775
+ * Return the driver-specific device-settings contribution. Hub-local
776
+ * devices call `getSettingsUISchema()` directly; forked-worker devices
777
+ * go through the `device-ops.getSettingsSchema` cap method on the
778
+ * numeric-id-keyed native registry.
779
+ */
780
+ async resolveDriverConfigSchema(deviceId, hubLocal) {
781
+ if (hubLocal) {
782
+ const schema = hubLocal.device.getSettingsUISchema();
783
+ return schema.sections.length === 0 ? null : toWireShape(schema);
784
+ }
785
+ const ops = this.capabilityRegistry?.getNativeProvider("device-ops", deviceId);
786
+ if (!ops) return null;
787
+ try {
788
+ const schema = await ops.getSettingsSchema({ deviceId });
789
+ if (!schema) return null;
790
+ const wire = schema;
791
+ return wire.sections.length === 0 ? null : toWireShape(wire);
792
+ } catch (err) {
793
+ const msg = err instanceof Error ? err.message : String(err);
794
+ this.ctx.logger.warn("cross-process getSettingsSchema failed", {
795
+ tags: { deviceId },
796
+ meta: { error: msg }
797
+ });
798
+ return null;
799
+ }
800
+ }
801
+ async updateDeviceField(input) {
802
+ if (input.writerCapName === "device-manager") {
803
+ const hubRegistry = this.ctx.kernel?.deviceRegistry;
804
+ const found = hubRegistry ? resolveDeviceById(hubRegistry, input.deviceId) : null;
805
+ if (found) {
806
+ await found.device.applySettingsPatch({ [input.key]: input.value });
807
+ return { success: true };
808
+ }
809
+ const ops = this.capabilityRegistry?.getNativeProvider("device-ops", input.deviceId);
810
+ if (!ops) throw new Error(`[device-manager] device "${input.deviceId}" not found (no hub-local entry, no device-ops native provider)`);
811
+ await ops.setConfig({
812
+ deviceId: input.deviceId,
813
+ values: { [input.key]: input.value }
814
+ });
815
+ return { success: true };
816
+ }
817
+ const registry = this.capabilityRegistry;
818
+ if (!registry) throw new Error("[device-manager] updateDeviceField requires capability registry — unavailable on this node");
819
+ if (!registry.getDefinition(input.writerCapName)?.exposesDeviceSettings) throw new Error(`[device-manager] cap "${input.writerCapName}" does not expose device settings`);
820
+ const provider = registry.getProviderByAddon(input.writerCapName, input.writerAddonId);
821
+ if (!provider) throw new Error(`[device-manager] provider "${input.writerAddonId}" not registered for cap "${input.writerCapName}"`);
822
+ await provider.applyDeviceSettingsPatch({
823
+ deviceId: input.deviceId,
824
+ patch: { [input.key]: input.value }
825
+ });
826
+ return { success: true };
827
+ }
828
+ /**
829
+ * Batched counterpart of `updateDeviceField`. Groups changes by
830
+ * `(writerCapName, writerAddonId)` so each contributor receives a
831
+ * single `applyDeviceSettingsPatch` with all of its updates merged —
832
+ * avoids N round-trips for simultaneous edits in the same save.
833
+ *
834
+ * Per-provider failures are captured in the `failures[]` output so the
835
+ * admin UI can highlight which sections didn't persist; a failure on
836
+ * one provider does NOT abort the others.
837
+ */
838
+ async updateDeviceFieldsBatch(input) {
839
+ const groups = /* @__PURE__ */ new Map();
840
+ for (const change of input.changes) {
841
+ const key = `${change.writerCapName}::${change.writerAddonId}`;
842
+ const existing = groups.get(key);
843
+ if (existing) existing.patch[change.key] = change.value;
844
+ else groups.set(key, {
845
+ writerCapName: change.writerCapName,
846
+ writerAddonId: change.writerAddonId,
847
+ patch: { [change.key]: change.value }
848
+ });
849
+ }
850
+ const failures = [];
851
+ for (const group of groups.values()) try {
852
+ await this.applyGroupPatch(input.deviceId, group);
853
+ } catch (err) {
854
+ failures.push({
855
+ writerCapName: group.writerCapName,
856
+ writerAddonId: group.writerAddonId,
857
+ error: err instanceof Error ? err.message : String(err)
858
+ });
859
+ }
860
+ return {
861
+ success: true,
862
+ failures
863
+ };
864
+ }
865
+ /** Apply a single grouped patch to the appropriate provider. Mirrors
866
+ * `updateDeviceField` routing (special-case device-manager, else
867
+ * registry lookup). Used by `updateDeviceFieldsBatch`. */
868
+ async applyGroupPatch(deviceId, group) {
869
+ if (group.writerCapName === "device-manager") {
870
+ const hubRegistry = this.ctx.kernel?.deviceRegistry;
871
+ const found = hubRegistry ? resolveDeviceById(hubRegistry, deviceId) : null;
872
+ if (found) {
873
+ await found.device.applySettingsPatch(group.patch);
874
+ return;
875
+ }
876
+ const ops = this.capabilityRegistry?.getNativeProvider("device-ops", deviceId);
877
+ if (!ops) throw new Error(`[device-manager] device "${deviceId}" not found (no hub-local entry, no device-ops native provider)`);
878
+ await ops.setConfig({
879
+ deviceId,
880
+ values: group.patch
881
+ });
882
+ return;
883
+ }
884
+ const registry = this.capabilityRegistry;
885
+ if (!registry) throw new Error("[device-manager] capability registry unavailable");
886
+ if (!registry.getDefinition(group.writerCapName)?.exposesDeviceSettings) throw new Error(`[device-manager] cap "${group.writerCapName}" does not expose device settings`);
887
+ const provider = registry.getProviderByAddon(group.writerCapName, group.writerAddonId);
888
+ if (!provider) throw new Error(`[device-manager] provider "${group.writerAddonId}" not registered for cap "${group.writerCapName}"`);
889
+ await provider.applyDeviceSettingsPatch({
890
+ deviceId,
891
+ patch: group.patch
892
+ });
893
+ }
894
+ async listWrappersForCap(input) {
895
+ return [...this.capabilityRegistry?.getWrappersForCap(input.capName) ?? []];
896
+ }
897
+ async listBindableCapsForDeviceType(input) {
898
+ const registry = this.capabilityRegistry;
899
+ if (!registry) return [];
900
+ return registry.listDeviceScopedCapsForType(input.deviceType).map((capName) => ({
901
+ capName,
902
+ wrappers: [...registry.getWrappersForCap(capName)]
903
+ }));
904
+ }
905
+ async setWrapperActive(input) {
906
+ const storeKey = String(input.deviceId);
907
+ const store = await this.readBindingsStore();
908
+ const perDevice = { ...store.deviceBindings[storeKey] ?? {} };
909
+ if (input.active) perDevice[input.capName] = { wrapperAddonId: input.wrapperAddonId };
910
+ else perDevice[input.capName] = { wrapperAddonId: null };
911
+ const nextDeviceBindings = Object.keys(perDevice).length > 0 ? {
912
+ ...store.deviceBindings,
913
+ [storeKey]: perDevice
914
+ } : (() => {
915
+ const { [storeKey]: _drop, ...rest } = store.deviceBindings;
916
+ return rest;
917
+ })();
918
+ await this.writeBindingsStore({ deviceBindings: nextDeviceBindings });
919
+ this.ctx.eventBus.emit({
920
+ id: randomUUID(),
921
+ timestamp: /* @__PURE__ */ new Date(),
922
+ source: {
923
+ type: "addon",
924
+ id: this.ctx.id
925
+ },
926
+ category: EventCategory.DeviceBindingsChanged,
927
+ data: {
928
+ deviceId: input.deviceId,
929
+ capName: input.capName,
930
+ reason: input.active ? "wrapper-activated" : "wrapper-deactivated",
931
+ addonId: input.wrapperAddonId,
932
+ nodeId: this.resolveWrapperNodeId(input.wrapperAddonId)
933
+ }
934
+ });
935
+ }
936
+ async onInitialize() {
937
+ const settings = this.ctx.settings;
938
+ if (!settings) {
939
+ this.ctx.logger.warn("ctx.settings not available — device persistence unavailable");
940
+ return;
941
+ }
942
+ const registry = this.ctx.kernel.deviceRegistry ?? null;
943
+ if (!registry) this.ctx.logger.warn("device-registry not available — live operations will use persisted data only");
944
+ const localNodeId = this.ctx.kernel.localNodeId ?? "hub";
945
+ this.ctx.eventBus.subscribe({ category: EventCategory.DeviceBindingsChanged }, (event) => {
946
+ const { deviceId, capName, reason, addonId, nodeId } = event.data;
947
+ if (nodeId === localNodeId) return;
948
+ if (reason === "native-registered") {
949
+ let perDevice = this.remoteNativeCaps.get(deviceId);
950
+ if (!perDevice) {
951
+ perDevice = /* @__PURE__ */ new Map();
952
+ this.remoteNativeCaps.set(deviceId, perDevice);
953
+ }
954
+ perDevice.set(capName, {
955
+ addonId,
956
+ nodeId
957
+ });
958
+ } else if (reason === "native-unregistered") {
959
+ const perDevice = this.remoteNativeCaps.get(deviceId);
960
+ if (!perDevice) return;
961
+ perDevice.delete(capName);
962
+ if (perDevice.size === 0) this.remoteNativeCaps.delete(deviceId);
963
+ }
964
+ });
965
+ const cluster = this.ctx.kernel.cluster;
966
+ if (cluster) cluster.broker.localBus.on("$node.disconnected", (payload) => {
967
+ const gone = payload.node.id;
968
+ const emptyDevices = [];
969
+ for (const [deviceId, perDevice] of this.remoteNativeCaps) {
970
+ const toDelete = [];
971
+ for (const [capName, entry] of perDevice) if (entry.nodeId === gone) toDelete.push(capName);
972
+ for (const capName of toDelete) perDevice.delete(capName);
973
+ if (perDevice.size === 0) emptyDevices.push(deviceId);
974
+ }
975
+ for (const deviceId of emptyDevices) this.remoteNativeCaps.delete(deviceId);
976
+ });
977
+ const requireDeviceOps = (deviceId) => {
978
+ const ops = this.capabilityRegistry?.getNativeProvider("device-ops", deviceId);
979
+ if (!ops) throw new Error(`[device-manager] device-ops native provider not found for '${deviceId}'`);
980
+ return ops;
981
+ };
982
+ const readStore = async () => {
983
+ return await settings.readAddonStore();
984
+ };
985
+ const readIndex = async () => {
986
+ return (await readStore()).deviceIndex ?? {};
987
+ };
988
+ const readMeta = async () => {
989
+ return (await readStore()).deviceMeta ?? {};
990
+ };
991
+ /** Hardware-identity metadata map. Lives in a sibling key on the
992
+ * device-manager addon store so its writers (`setMetadata`) never
993
+ * collide with the lifecycle writers on `deviceMeta`
994
+ * (`registerDevice` / `setName` / `setLocation` / `setDisabled`).
995
+ * Single-writer per row eliminates the "writer X clobbers writer
996
+ * Y's field" bug class — `setMetadata` is the only producer. */
997
+ const readMetadataMap = async () => {
998
+ return (await readStore()).deviceMetadata ?? {};
999
+ };
1000
+ let metaWriteChain = Promise.resolve();
1001
+ const withMetaWriteLock = async (fn) => {
1002
+ const previous = metaWriteChain;
1003
+ let release = () => {};
1004
+ metaWriteChain = new Promise((resolve) => {
1005
+ release = resolve;
1006
+ });
1007
+ try {
1008
+ await previous.catch(() => {});
1009
+ return await fn();
1010
+ } finally {
1011
+ release();
1012
+ }
1013
+ };
1014
+ /**
1015
+ * Resolve a numeric deviceId to the owning `(addonId, stableId)` pair.
1016
+ * Scans persisted meta — live IDevice lookup (hub registry) is handled
1017
+ * separately per call site so callers can decide whether to route to
1018
+ * an in-process driver or to the cross-process `device-ops` bridge.
1019
+ * Returns null when no device with that id is known to the hub.
1020
+ */
1021
+ const resolvePersistedById = async (deviceId) => {
1022
+ const meta = await readMeta();
1023
+ for (const [key, m] of Object.entries(meta)) if (m.id === deviceId) {
1024
+ const sep = key.indexOf(":");
1025
+ if (sep < 0) continue;
1026
+ return {
1027
+ addonId: key.slice(0, sep),
1028
+ stableId: key.slice(sep + 1),
1029
+ meta: m
1030
+ };
1031
+ }
1032
+ return null;
1033
+ };
1034
+ const idToAddonId = /* @__PURE__ */ new Map();
1035
+ {
1036
+ const meta = await readMeta();
1037
+ for (const [key, m] of Object.entries(meta)) {
1038
+ const sep = key.indexOf(":");
1039
+ if (sep < 0) continue;
1040
+ idToAddonId.set(m.id, key.slice(0, sep));
1041
+ }
1042
+ }
1043
+ if (cluster) cluster.broker.localBus.on("$node.connected", (payload) => {
1044
+ const connectedNodeId = payload.node.id;
1045
+ const lastSlash = connectedNodeId.lastIndexOf("/");
1046
+ if (lastSlash < 0) return;
1047
+ const connectedAddonId = connectedNodeId.slice(lastSlash + 1);
1048
+ if (connectedAddonId.length === 0) return;
1049
+ for (const [deviceId, ownerAddonId] of idToAddonId) {
1050
+ if (ownerAddonId !== connectedAddonId) continue;
1051
+ if (this.capabilityRegistry?.getNativeAddonId("device-ops", deviceId)) continue;
1052
+ let perDevice = this.remoteNativeCaps.get(deviceId);
1053
+ if (!perDevice) {
1054
+ perDevice = /* @__PURE__ */ new Map();
1055
+ this.remoteNativeCaps.set(deviceId, perDevice);
1056
+ }
1057
+ if (!perDevice.has("device-ops")) perDevice.set("device-ops", {
1058
+ addonId: connectedAddonId,
1059
+ nodeId: connectedNodeId
1060
+ });
1061
+ }
1062
+ setTimeout(() => {
1063
+ this.discoverWorkerNativeCaps(connectedNodeId, connectedAddonId).catch((err) => {
1064
+ this.ctx.logger.warn("worker native-cap discovery failed", { meta: {
1065
+ nodeId: connectedNodeId,
1066
+ addonId: connectedAddonId,
1067
+ error: errMsg(err)
1068
+ } });
1069
+ });
1070
+ }, 500);
1071
+ });
1072
+ const allocateNextDeviceId = async () => {
1073
+ const current = (await readStore()).nextDeviceId ?? 1;
1074
+ await settings.writeAddonStore({ nextDeviceId: current + 1 });
1075
+ return current;
1076
+ };
1077
+ const provider = {
1078
+ /** Sync ownership lookup backing persistence fallbacks (e.g. remove()
1079
+ * when the owning worker is offline). Ownership is keyed by numeric
1080
+ * deviceId → owning addonId as recorded in the persisted meta. NOT
1081
+ * a native-cap lookup: an addon can own a device without registering
1082
+ * every possible cap natively (e.g. RtspCamera without snapshotUrl
1083
+ * doesn't register the snapshot cap). Use
1084
+ * `resolveNativeCapOwnerSync` for cap-resolution paths.
1085
+ */
1086
+ resolveDeviceOwnerSync: (deviceId) => {
1087
+ return idToAddonId.get(deviceId) ?? null;
1088
+ },
1089
+ /** Sync lookup for the addon that registered a native provider for
1090
+ * `(capName, deviceId)`. Backs `CapabilityRegistry`'s native fallback
1091
+ * so the hub only synthesizes a cross-process proxy when the cap is
1092
+ * actually published — never on speculative device ownership.
1093
+ *
1094
+ * Consults hub-local registrations first (in-process natives),
1095
+ * then the `remoteNativeCaps` map populated from
1096
+ * `DeviceBindingsChanged` events emitted by forked-worker
1097
+ * `registerNativeCap` calls. Both are generic: any addon that hosts
1098
+ * devices and registers caps via the standard context API shows up
1099
+ * here without per-addon branching.
1100
+ */
1101
+ resolveNativeCapOwnerSync: (capName, deviceId) => {
1102
+ const localAddonId = this.capabilityRegistry?.getNativeAddonId(capName, deviceId) ?? null;
1103
+ if (localAddonId) return {
1104
+ addonId: localAddonId,
1105
+ nodeId: this.ctx.kernel.localNodeId ?? "hub"
1106
+ };
1107
+ const remote = this.remoteNativeCaps.get(deviceId)?.get(capName) ?? null;
1108
+ if (remote) return {
1109
+ addonId: remote.addonId,
1110
+ nodeId: remote.nodeId
1111
+ };
1112
+ return null;
1113
+ },
1114
+ /** Idempotent numeric-id reservation. Callers invoke this before
1115
+ * constructing the owning `IDevice` so `DeviceContext.id` is bound
1116
+ * at construction time. A repeat call for the same `(addonId,
1117
+ * stableId)` returns the already-persisted id — same physical
1118
+ * device reconnecting after a driver restart keeps its original
1119
+ * number. Fresh pairs burn one slot from the monotonic
1120
+ * `nextDeviceId` counter and seed a meta placeholder so the
1121
+ * `deviceMeta` → `id` invariant holds even before
1122
+ * `registerDevice` completes. */
1123
+ allocateDeviceId: async (input) => {
1124
+ const { addonId, stableId } = input;
1125
+ const key = deviceKey(addonId, stableId);
1126
+ return await withMetaWriteLock(async () => {
1127
+ const meta = await readMeta();
1128
+ const existing = meta[key];
1129
+ if (existing) return { id: existing.id };
1130
+ const id = await allocateNextDeviceId();
1131
+ await settings.writeAddonStore({ deviceMeta: {
1132
+ ...meta,
1133
+ [key]: {
1134
+ type: "generic",
1135
+ name: stableId,
1136
+ location: null,
1137
+ disabled: false,
1138
+ parentDeviceId: null,
1139
+ id
1140
+ }
1141
+ } });
1142
+ return { id };
1143
+ });
1144
+ },
1145
+ registerDevice: async (input) => {
1146
+ const { addonId, stableId, id, type, name, parentDeviceId, features, config } = input;
1147
+ const key = deviceKey(addonId, stableId);
1148
+ const featuresArr = Array.isArray(features) ? [...features] : [];
1149
+ const { isFirstRegistration } = await withMetaWriteLock(async () => {
1150
+ const index = await readIndex();
1151
+ const existing = index[addonId] ?? [];
1152
+ const wasInIndex = existing.includes(stableId);
1153
+ if (!wasInIndex) await settings.writeAddonStore({ deviceIndex: {
1154
+ ...index,
1155
+ [addonId]: [...existing, stableId]
1156
+ } });
1157
+ const meta = await readMeta();
1158
+ const existingMeta = meta[key];
1159
+ const isFirst = !existingMeta || !wasInIndex;
1160
+ await settings.writeAddonStore({ deviceMeta: {
1161
+ ...meta,
1162
+ [key]: {
1163
+ type,
1164
+ name: existingMeta && existingMeta.name !== stableId ? existingMeta.name : name,
1165
+ location: existingMeta?.location ?? null,
1166
+ disabled: existingMeta?.disabled ?? false,
1167
+ parentDeviceId,
1168
+ id,
1169
+ features: featuresArr
1170
+ }
1171
+ } });
1172
+ return { isFirstRegistration: isFirst };
1173
+ });
1174
+ if (Object.keys(config).length > 0) await settings.writeDeviceStore(id, config);
1175
+ idToAddonId.set(id, addonId);
1176
+ if (isFirstRegistration) this.ctx.eventBus.emit({
1177
+ id: randomUUID(),
1178
+ timestamp: /* @__PURE__ */ new Date(),
1179
+ source: {
1180
+ type: "device",
1181
+ id
1182
+ },
1183
+ category: EventCategory.DeviceRegistered,
1184
+ data: {
1185
+ deviceId: id,
1186
+ name: name.length > 0 ? name : stableId,
1187
+ providerId: addonId,
1188
+ parentDeviceId: parentDeviceId ?? null
1189
+ }
1190
+ });
1191
+ else this.ctx.eventBus.emit({
1192
+ id: randomUUID(),
1193
+ timestamp: /* @__PURE__ */ new Date(),
1194
+ source: {
1195
+ type: "device",
1196
+ id
1197
+ },
1198
+ category: EventCategory.DeviceMetaChanged,
1199
+ data: {
1200
+ deviceId: id,
1201
+ name: name.length > 0 ? name : stableId,
1202
+ providerId: addonId,
1203
+ parentDeviceId: parentDeviceId ?? null,
1204
+ features: featuresArr
1205
+ }
1206
+ });
1207
+ },
1208
+ removeDevice: async (input) => {
1209
+ const { deviceId } = input;
1210
+ const persisted = await resolvePersistedById(deviceId);
1211
+ if (!persisted) return;
1212
+ const { addonId, stableId, meta: persistedMeta } = persisted;
1213
+ const key = deviceKey(addonId, stableId);
1214
+ const deviceName = persistedMeta.name;
1215
+ await withMetaWriteLock(async () => {
1216
+ const index = await readIndex();
1217
+ const remaining = (index[addonId] ?? []).filter((sid) => sid !== stableId);
1218
+ const updatedIndex = remaining.length > 0 ? {
1219
+ ...index,
1220
+ [addonId]: remaining
1221
+ } : (() => {
1222
+ const { [addonId]: _removed, ...rest } = index;
1223
+ return rest;
1224
+ })();
1225
+ await settings.writeAddonStore({ deviceIndex: updatedIndex });
1226
+ const { [key]: _removedMeta, ...restMeta } = await readMeta();
1227
+ await settings.writeAddonStore({ deviceMeta: restMeta });
1228
+ const map = await readMetadataMap();
1229
+ if (key in map) {
1230
+ const { [key]: _removedMetadata, ...restMap } = map;
1231
+ await settings.writeAddonStore({ deviceMetadata: restMap });
1232
+ }
1233
+ });
1234
+ await settings.clearDeviceStore(deviceId);
1235
+ const bindingsStore = await this.readBindingsStore();
1236
+ const bindingKey = String(deviceId);
1237
+ if (bindingsStore.deviceBindings[bindingKey]) {
1238
+ const { [bindingKey]: _removedBindings, ...restBindings } = bindingsStore.deviceBindings;
1239
+ await this.writeBindingsStore({ deviceBindings: restBindings });
1240
+ }
1241
+ this.remoteNativeCaps.delete(deviceId);
1242
+ this.capabilityRegistry?.unregisterAllNativeForDevice(deviceId);
1243
+ idToAddonId.delete(deviceId);
1244
+ this.ctx.logger.info("removed device", { tags: {
1245
+ deviceId,
1246
+ deviceName: deviceName.length > 0 ? deviceName : stableId
1247
+ } });
1248
+ this.ctx.eventBus.emit({
1249
+ id: randomUUID(),
1250
+ timestamp: /* @__PURE__ */ new Date(),
1251
+ source: {
1252
+ type: "device",
1253
+ id: deviceId
1254
+ },
1255
+ category: EventCategory.DeviceUnregistered,
1256
+ data: {
1257
+ deviceId,
1258
+ providerId: addonId,
1259
+ parentDeviceId: persistedMeta.parentDeviceId ?? null
1260
+ }
1261
+ });
1262
+ },
1263
+ persistConfig: async (input) => {
1264
+ const { deviceId, data } = input;
1265
+ if (!await resolvePersistedById(deviceId)) throw new Error(`[device-manager] persistConfig: unknown device id=${deviceId}`);
1266
+ await settings.writeDeviceStore(deviceId, data);
1267
+ },
1268
+ loadConfig: async (input) => {
1269
+ const { deviceId } = input;
1270
+ if (!await resolvePersistedById(deviceId)) return {};
1271
+ return settings.readDeviceStore(deviceId);
1272
+ },
1273
+ /**
1274
+ * Load the operator-organisational meta surface for one device
1275
+ * (`name` / `location` / `disabled` / `type` / `parentDeviceId`
1276
+ * / `addonId` + `id` / `stableId`). Used by the kernel proxy's
1277
+ * device-context factory to populate `ctx.deviceMeta` before
1278
+ * the device class constructor runs. Returns `null` when no
1279
+ * persisted row exists for the id.
1280
+ *
1281
+ * Reads default `location` to `null` and `disabled` to `false`
1282
+ * for legacy rows that predate the field — production code
1283
+ * relies on the IDevice type contract that both are present.
1284
+ */
1285
+ loadMeta: async (input) => {
1286
+ const { deviceId } = input;
1287
+ const persisted = await resolvePersistedById(deviceId);
1288
+ if (!persisted) return null;
1289
+ const { addonId, stableId, meta: m } = persisted;
1290
+ const key = deviceKey(addonId, stableId);
1291
+ const metadata = (await readMetadataMap())[key] ?? null;
1292
+ return {
1293
+ id: m.id,
1294
+ stableId,
1295
+ addonId,
1296
+ type: m.type,
1297
+ name: m.name,
1298
+ location: m.location ?? null,
1299
+ disabled: m.disabled ?? false,
1300
+ parentDeviceId: m.parentDeviceId,
1301
+ metadata
1302
+ };
1303
+ },
1304
+ /**
1305
+ * Update the operator-edited display name. Writes the meta
1306
+ * row, emits a `DeviceMetaChanged` event so live consumers
1307
+ * (UI device list, alert center) see the rename without
1308
+ * polling. The live `IDevice.name` mirror is updated by the
1309
+ * kernel proxy on its side (`device-cap-proxy.ts`).
1310
+ */
1311
+ setName: async (input) => {
1312
+ const { deviceId, name } = input;
1313
+ await withMetaWriteLock(async () => {
1314
+ const persisted = await resolvePersistedById(deviceId);
1315
+ if (!persisted) throw new Error(`[device-manager] setName: unknown device id=${deviceId}`);
1316
+ const { addonId, stableId, meta: m } = persisted;
1317
+ const key = deviceKey(addonId, stableId);
1318
+ const allMeta = await readMeta();
1319
+ await settings.writeAddonStore({ deviceMeta: {
1320
+ ...allMeta,
1321
+ [key]: {
1322
+ ...m,
1323
+ name
1324
+ }
1325
+ } });
1326
+ });
1327
+ this.ctx.eventBus.emit({
1328
+ id: randomUUID(),
1329
+ timestamp: /* @__PURE__ */ new Date(),
1330
+ source: {
1331
+ type: "device",
1332
+ id: deviceId
1333
+ },
1334
+ category: EventCategory.DeviceMetaChanged,
1335
+ data: {
1336
+ deviceId,
1337
+ field: "name",
1338
+ value: name
1339
+ }
1340
+ });
1341
+ },
1342
+ /**
1343
+ * Update the operator-organisational location label. `null`
1344
+ * clears it. Mirrors the same persist-then-emit shape as
1345
+ * `setName`; consumers subscribe to `DeviceMetaChanged` and
1346
+ * filter on `field: 'location'`.
1347
+ */
1348
+ setLocation: async (input) => {
1349
+ const { deviceId, location } = input;
1350
+ await withMetaWriteLock(async () => {
1351
+ const persisted = await resolvePersistedById(deviceId);
1352
+ if (!persisted) throw new Error(`[device-manager] setLocation: unknown device id=${deviceId}`);
1353
+ const { addonId, stableId, meta: m } = persisted;
1354
+ const key = deviceKey(addonId, stableId);
1355
+ const allMeta = await readMeta();
1356
+ await settings.writeAddonStore({ deviceMeta: {
1357
+ ...allMeta,
1358
+ [key]: {
1359
+ ...m,
1360
+ location
1361
+ }
1362
+ } });
1363
+ });
1364
+ this.ctx.eventBus.emit({
1365
+ id: randomUUID(),
1366
+ timestamp: /* @__PURE__ */ new Date(),
1367
+ source: {
1368
+ type: "device",
1369
+ id: deviceId
1370
+ },
1371
+ category: EventCategory.DeviceMetaChanged,
1372
+ data: {
1373
+ deviceId,
1374
+ field: "location",
1375
+ value: location
1376
+ }
1377
+ });
1378
+ },
1379
+ /**
1380
+ * Patch the device's hardware-identity metadata blob. Shallow
1381
+ * merge — `null` removes a key, anything else overwrites.
1382
+ * Drivers populate factual fields on first probe; operators
1383
+ * augment via the Device Info tab. Idempotent: a no-op patch
1384
+ * (every key already present with the same value) doesn't emit
1385
+ * the meta-changed event.
1386
+ */
1387
+ setMetadata: async (input) => {
1388
+ const { deviceId, patch } = input;
1389
+ const result = await withMetaWriteLock(async () => {
1390
+ const persisted = await resolvePersistedById(deviceId);
1391
+ if (!persisted) throw new Error(`[device-manager] setMetadata: unknown device id=${deviceId}`);
1392
+ const { addonId, stableId } = persisted;
1393
+ const key = deviceKey(addonId, stableId);
1394
+ const map = await readMetadataMap();
1395
+ const next = { ...map[key] ?? {} };
1396
+ let changed = false;
1397
+ for (const [k, v] of Object.entries(patch)) if (v === null) {
1398
+ if (k in next) {
1399
+ delete next[k];
1400
+ changed = true;
1401
+ }
1402
+ } else if (next[k] !== v) {
1403
+ next[k] = v;
1404
+ changed = true;
1405
+ }
1406
+ if (!changed) return { changed: false };
1407
+ const hasFields = Object.keys(next).length > 0;
1408
+ const updatedMap = { ...map };
1409
+ if (hasFields) updatedMap[key] = next;
1410
+ else delete updatedMap[key];
1411
+ await settings.writeAddonStore({ deviceMetadata: updatedMap });
1412
+ return {
1413
+ changed: true,
1414
+ finalMeta: hasFields ? next : null
1415
+ };
1416
+ });
1417
+ if (!result.changed) return;
1418
+ this.ctx.eventBus.emit({
1419
+ id: randomUUID(),
1420
+ timestamp: /* @__PURE__ */ new Date(),
1421
+ source: {
1422
+ type: "device",
1423
+ id: deviceId
1424
+ },
1425
+ category: EventCategory.DeviceMetaChanged,
1426
+ data: {
1427
+ deviceId,
1428
+ field: "metadata",
1429
+ value: result.finalMeta
1430
+ }
1431
+ });
1432
+ },
1433
+ /**
1434
+ * Soft-disable the device. Persisted on the meta row;
1435
+ * lifecycle gating is the driver's responsibility (BaseDevice
1436
+ * exposes `this.disabled` for the driver to consult at the top
1437
+ * of its lifecycle methods).
1438
+ */
1439
+ setDisabled: async (input) => {
1440
+ const { deviceId, disabled } = input;
1441
+ await withMetaWriteLock(async () => {
1442
+ const persisted = await resolvePersistedById(deviceId);
1443
+ if (!persisted) throw new Error(`[device-manager] setDisabled: unknown device id=${deviceId}`);
1444
+ const { addonId, stableId, meta: m } = persisted;
1445
+ const key = deviceKey(addonId, stableId);
1446
+ const allMeta = await readMeta();
1447
+ await settings.writeAddonStore({ deviceMeta: {
1448
+ ...allMeta,
1449
+ [key]: {
1450
+ ...m,
1451
+ disabled
1452
+ }
1453
+ } });
1454
+ });
1455
+ this.ctx.eventBus.emit({
1456
+ id: randomUUID(),
1457
+ timestamp: /* @__PURE__ */ new Date(),
1458
+ source: {
1459
+ type: "device",
1460
+ id: deviceId
1461
+ },
1462
+ category: EventCategory.DeviceMetaChanged,
1463
+ data: {
1464
+ deviceId,
1465
+ field: "disabled",
1466
+ value: disabled
1467
+ }
1468
+ });
1469
+ },
1470
+ loadRuntimeState: async (input) => {
1471
+ const { deviceId } = input;
1472
+ if (!await resolvePersistedById(deviceId)) return {};
1473
+ const data = await settings.readDeviceRuntimeState(deviceId);
1474
+ this.seedMirror(deviceId, data);
1475
+ return data;
1476
+ },
1477
+ /**
1478
+ * Union of (1) operator-curated location registry and (2) labels
1479
+ * currently in use on persisted devices. Case-insensitive
1480
+ * dedupe (preserves the first-seen casing). Sorted
1481
+ * case-insensitively for stable UI. Drives the Device Info
1482
+ * location autocomplete.
1483
+ */
1484
+ listLocations: async () => {
1485
+ const store = await settings.readAddonStore();
1486
+ const meta = store.deviceMeta ?? {};
1487
+ const registry = store.locations ?? [];
1488
+ const seen = /* @__PURE__ */ new Map();
1489
+ const consider = (raw) => {
1490
+ if (typeof raw !== "string") return;
1491
+ const trimmed = raw.trim();
1492
+ if (trimmed.length === 0) return;
1493
+ const key = trimmed.toLowerCase();
1494
+ if (!seen.has(key)) seen.set(key, trimmed);
1495
+ };
1496
+ for (const label of registry) consider(label);
1497
+ for (const m of Object.values(meta)) consider(m.location);
1498
+ return [...seen.values()].sort((a, b) => a.localeCompare(b, void 0, { sensitivity: "base" }));
1499
+ },
1500
+ /**
1501
+ * Add a label to the curated location registry. Idempotent:
1502
+ * existing entries (case-insensitive match) are silently kept.
1503
+ * Empty / whitespace-only inputs throw — operators must supply a
1504
+ * meaningful label.
1505
+ */
1506
+ addLocation: async (input) => {
1507
+ const trimmed = input.name.trim();
1508
+ if (trimmed.length === 0) throw new Error("[device-manager] addLocation: name must be non-empty");
1509
+ const current = (await settings.readAddonStore()).locations ?? [];
1510
+ if (current.some((l) => l.toLowerCase() === trimmed.toLowerCase())) return;
1511
+ await settings.writeAddonStore({ locations: [...current, trimmed] });
1512
+ },
1513
+ /**
1514
+ * Remove a label from the curated registry. Match is
1515
+ * case-insensitive. Devices that still reference this label keep
1516
+ * their `meta.location` value (the registry is a suggestion
1517
+ * list, not a foreign key) — pass `cascade: true` to also clear
1518
+ * `setLocation` on every device that referenced this exact
1519
+ * label. Cascade only matches case-insensitively + trimmed, same
1520
+ * as the registry equality check.
1521
+ */
1522
+ removeLocation: async (input) => {
1523
+ const trimmed = input.name.trim();
1524
+ if (trimmed.length === 0) return;
1525
+ const store = await settings.readAddonStore();
1526
+ const current = store.locations ?? [];
1527
+ const remaining = current.filter((l) => l.toLowerCase() !== trimmed.toLowerCase());
1528
+ if (remaining.length !== current.length) await settings.writeAddonStore({ locations: remaining });
1529
+ if (input.cascade !== true) return;
1530
+ const meta = store.deviceMeta ?? {};
1531
+ const updates = { ...meta };
1532
+ const cleared = [];
1533
+ for (const [key, m] of Object.entries(meta)) {
1534
+ if (typeof m.location !== "string") continue;
1535
+ if (m.location.trim().toLowerCase() !== trimmed.toLowerCase()) continue;
1536
+ updates[key] = {
1537
+ ...m,
1538
+ location: null
1539
+ };
1540
+ cleared.push(m.id);
1541
+ }
1542
+ if (cleared.length === 0) return;
1543
+ await settings.writeAddonStore({ deviceMeta: updates });
1544
+ for (const deviceId of cleared) this.ctx.eventBus.emit({
1545
+ id: randomUUID(),
1546
+ timestamp: /* @__PURE__ */ new Date(),
1547
+ source: {
1548
+ type: "device",
1549
+ id: deviceId
1550
+ },
1551
+ category: EventCategory.DeviceMetaChanged,
1552
+ data: {
1553
+ deviceId,
1554
+ field: "location",
1555
+ value: null
1556
+ }
1557
+ });
1558
+ },
1559
+ listPersistedByAddon: async (input) => {
1560
+ const { addonId } = input;
1561
+ const [index, meta] = await Promise.all([readIndex(), readMeta()]);
1562
+ return (index[addonId] ?? []).map((stableId) => {
1563
+ const m = meta[deviceKey(addonId, stableId)];
1564
+ return {
1565
+ id: m.id,
1566
+ stableId,
1567
+ type: m.type,
1568
+ name: m.name,
1569
+ location: m.location ?? null,
1570
+ disabled: m.disabled ?? false,
1571
+ parentDeviceId: m.parentDeviceId
1572
+ };
1573
+ });
1574
+ },
1575
+ listAll: async (input) => {
1576
+ const { addonId } = input;
1577
+ const results = [];
1578
+ const seen = /* @__PURE__ */ new Set();
1579
+ const meta = await readMeta();
1580
+ const metadataMap = await readMetadataMap();
1581
+ if (registry) {
1582
+ const liveEntries = addonId ? registry.getAllForAddon(addonId).map((device) => ({
1583
+ addonId,
1584
+ device
1585
+ })) : registry.getAllWithAddonId();
1586
+ for (const { addonId: aid, device } of liveEntries) {
1587
+ const key = deviceKey(aid, device.stableId);
1588
+ const metadata = metadataMap[key] ?? null;
1589
+ const metaRow = meta[key] ?? null;
1590
+ results.push(toDeviceInfo(aid, device, metadata, metaRow));
1591
+ seen.add(key);
1592
+ }
1593
+ }
1594
+ const index = await readIndex();
1595
+ const targetAddons = addonId ? [addonId] : Object.keys(index);
1596
+ for (const aid of targetAddons) for (const stableId of index[aid] ?? []) {
1597
+ const key = deviceKey(aid, stableId);
1598
+ if (seen.has(key)) continue;
1599
+ const m = meta[key];
1600
+ const persistedType = m.type;
1601
+ const persistedConfig = await settings.readDeviceStore(m.id);
1602
+ const metadata = metadataMap[key] ?? null;
1603
+ results.push({
1604
+ id: m.id,
1605
+ stableId,
1606
+ addonId: aid,
1607
+ type: persistedType,
1608
+ name: m?.name ?? stableId,
1609
+ location: m?.location ?? null,
1610
+ disabled: m?.disabled ?? false,
1611
+ parentDeviceId: m?.parentDeviceId ?? null,
1612
+ role: null,
1613
+ online: registry !== null,
1614
+ features: persistedFeatures(m?.features),
1615
+ isCamera: persistedType === DeviceType.Camera,
1616
+ config: persistedConfig ?? {},
1617
+ metadata
1618
+ });
1619
+ }
1620
+ return results;
1621
+ },
1622
+ getDevice: async (input) => {
1623
+ const { deviceId } = input;
1624
+ if (registry) {
1625
+ const found = resolveDeviceById(registry, deviceId);
1626
+ if (found) {
1627
+ const key = deviceKey(found.addonId, found.device.stableId);
1628
+ const [map, metaMap] = await Promise.all([readMetadataMap(), readMeta()]);
1629
+ const metadata = map[key] ?? null;
1630
+ const metaRow = metaMap[key] ?? null;
1631
+ return toDeviceInfo(found.addonId, found.device, metadata, metaRow);
1632
+ }
1633
+ }
1634
+ const persisted = await resolvePersistedById(deviceId);
1635
+ if (!persisted) return null;
1636
+ const { addonId: aid, stableId, meta: m } = persisted;
1637
+ const persistedConfig = await settings.readDeviceStore(m.id);
1638
+ const key = deviceKey(aid, stableId);
1639
+ const metadata = (await readMetadataMap())[key] ?? null;
1640
+ return {
1641
+ id: deviceId,
1642
+ stableId,
1643
+ addonId: aid,
1644
+ type: m.type,
1645
+ name: m.name,
1646
+ location: m.location ?? null,
1647
+ disabled: m.disabled ?? false,
1648
+ parentDeviceId: m.parentDeviceId,
1649
+ role: null,
1650
+ online: true,
1651
+ features: persistedFeatures(m.features),
1652
+ isCamera: false,
1653
+ config: persistedConfig ?? {},
1654
+ metadata
1655
+ };
1656
+ },
1657
+ getChildren: async (input) => {
1658
+ const { parentDeviceId } = input;
1659
+ let ownerAddonId = null;
1660
+ if (registry) {
1661
+ if (registry.getById(parentDeviceId)) ownerAddonId = registry.getAddonId(parentDeviceId);
1662
+ }
1663
+ if (!ownerAddonId) {
1664
+ const persisted = await resolvePersistedById(parentDeviceId);
1665
+ if (!persisted) return [];
1666
+ ownerAddonId = persisted.addonId;
1667
+ }
1668
+ const results = [];
1669
+ const seen = /* @__PURE__ */ new Set();
1670
+ const [index, meta, metadataMap] = await Promise.all([
1671
+ readIndex(),
1672
+ readMeta(),
1673
+ readMetadataMap()
1674
+ ]);
1675
+ if (registry) {
1676
+ const liveChildren = registry.getChildren(parentDeviceId);
1677
+ for (const device of liveChildren) {
1678
+ const key = deviceKey(ownerAddonId, device.stableId);
1679
+ const metadata = metadataMap[key] ?? null;
1680
+ const metaRow = meta[key] ?? null;
1681
+ results.push(toDeviceInfo(ownerAddonId, device, metadata, metaRow));
1682
+ seen.add(key);
1683
+ }
1684
+ }
1685
+ const persistedChildren = (index[ownerAddonId] ?? []).filter((sid) => meta[deviceKey(ownerAddonId, sid)]?.parentDeviceId === parentDeviceId);
1686
+ for (const childStableId of persistedChildren) {
1687
+ const key = deviceKey(ownerAddonId, childStableId);
1688
+ if (seen.has(key)) continue;
1689
+ const m = meta[key];
1690
+ const persistedConfig = await settings.readDeviceStore(m.id);
1691
+ const metadata = metadataMap[key] ?? null;
1692
+ results.push({
1693
+ id: m.id,
1694
+ stableId: childStableId,
1695
+ addonId: ownerAddonId,
1696
+ type: m.type,
1697
+ name: m.name,
1698
+ location: m.location ?? null,
1699
+ disabled: m.disabled ?? false,
1700
+ parentDeviceId,
1701
+ role: null,
1702
+ online: registry !== null,
1703
+ features: persistedFeatures(m.features),
1704
+ isCamera: false,
1705
+ config: persistedConfig ?? {},
1706
+ metadata
1707
+ });
1708
+ }
1709
+ return results;
1710
+ },
1711
+ getStreamSources: async (input) => {
1712
+ const { deviceId } = input;
1713
+ if (registry) {
1714
+ const found = resolveDeviceById(registry, deviceId);
1715
+ if (found) {
1716
+ if (!isCameraDevice(found.device)) return [];
1717
+ return (await found.device.getStreamSources()).map((s) => ({
1718
+ id: s.id,
1719
+ label: s.label,
1720
+ protocol: s.protocol,
1721
+ url: s.url,
1722
+ resolution: s.resolution,
1723
+ fps: s.fps,
1724
+ bitrate: s.bitrate,
1725
+ codec: s.codec,
1726
+ profileHint: s.profileHint
1727
+ }));
1728
+ }
1729
+ }
1730
+ if (!await resolvePersistedById(deviceId)) throw new Error(`[device-manager] Device with id ${deviceId} not found`);
1731
+ return (await requireDeviceOps(deviceId).getStreamSources({ deviceId })).map((s) => ({ ...s }));
1732
+ },
1733
+ getConfigSchema: async (input) => {
1734
+ const { deviceId } = input;
1735
+ if (registry) {
1736
+ const found = resolveDeviceById(registry, deviceId);
1737
+ if (found) return found.device.config.entries().map((entry) => ({
1738
+ key: entry.key,
1739
+ value: entry.value,
1740
+ ...entry.description !== void 0 ? { description: entry.description } : {}
1741
+ }));
1742
+ }
1743
+ if (!await resolvePersistedById(deviceId)) throw new Error(`[device-manager] Device with id ${deviceId} not found`);
1744
+ return (await requireDeviceOps(deviceId).getConfigEntries({ deviceId })).map((e) => ({ ...e }));
1745
+ },
1746
+ getSettingsSchema: async (input) => {
1747
+ const { deviceId } = input;
1748
+ if (registry) {
1749
+ const found = resolveDeviceById(registry, deviceId);
1750
+ if (found) return found.device.getSettingsUISchema();
1751
+ }
1752
+ if (!await resolvePersistedById(deviceId)) return null;
1753
+ return await requireDeviceOps(deviceId).getSettingsSchema({ deviceId }) ?? null;
1754
+ },
1755
+ updateConfig: async (input) => {
1756
+ const { deviceId } = input;
1757
+ if (registry) {
1758
+ const found = resolveDeviceById(registry, deviceId);
1759
+ if (found) {
1760
+ await found.device.config.setAll(input.values);
1761
+ return { success: true };
1762
+ }
1763
+ }
1764
+ if (!await resolvePersistedById(deviceId)) throw new Error(`[device-manager] Device with id ${deviceId} not found`);
1765
+ await requireDeviceOps(deviceId).setConfig({
1766
+ deviceId,
1767
+ values: input.values
1768
+ });
1769
+ return { success: true };
1770
+ },
1771
+ enable: async (input) => {
1772
+ await provider.setDisabled({
1773
+ deviceId: input.deviceId,
1774
+ disabled: false
1775
+ });
1776
+ return { success: true };
1777
+ },
1778
+ disable: async (input) => {
1779
+ await provider.setDisabled({
1780
+ deviceId: input.deviceId,
1781
+ disabled: true
1782
+ });
1783
+ return { success: true };
1784
+ },
1785
+ remove: async (input) => {
1786
+ const { deviceId } = input;
1787
+ if (registry) {
1788
+ const live = resolveDeviceById(registry, deviceId);
1789
+ if (live) {
1790
+ const deviceName = live.device.name;
1791
+ await live.device.removeDevice();
1792
+ registry.remove(deviceId);
1793
+ await provider.removeDevice({ deviceId });
1794
+ this.ctx.logger.info("removed hub-local device", { tags: {
1795
+ deviceId,
1796
+ deviceName
1797
+ } });
1798
+ return { success: true };
1799
+ }
1800
+ }
1801
+ const persisted = await resolvePersistedById(deviceId);
1802
+ if (!persisted) throw new Error(`[device-manager] Device with id ${deviceId} not found`);
1803
+ const { meta: persistedMeta } = persisted;
1804
+ try {
1805
+ await requireDeviceOps(deviceId).removeDevice({ deviceId });
1806
+ } catch (err) {
1807
+ this.ctx.logger.warn("remove via device-ops failed — clearing persistence anyway", {
1808
+ tags: {
1809
+ deviceId,
1810
+ deviceName: persistedMeta.name
1811
+ },
1812
+ meta: { error: errMsg(err) }
1813
+ });
1814
+ }
1815
+ await provider.removeDevice({ deviceId });
1816
+ return { success: true };
1817
+ },
1818
+ getStreamProfileMap: async (input) => {
1819
+ if (!registry) return {};
1820
+ const found = resolveDeviceById(registry, input.deviceId);
1821
+ if (!found) return {};
1822
+ const storedMap = found.device.config.entries().find((e) => e.key === "_profileMap")?.value;
1823
+ if (storedMap !== void 0 && typeof storedMap === "object" && storedMap !== null) return storedMap;
1824
+ if (!isCameraDevice(found.device)) return {};
1825
+ const sources = await found.device.getStreamSources();
1826
+ const profileMap = {};
1827
+ for (const s of sources) if (s.profileHint && s.id) profileMap[s.profileHint] = s.id;
1828
+ return profileMap;
1829
+ },
1830
+ setStreamProfileMap: async (input) => {
1831
+ const { deviceId } = input;
1832
+ if (registry) {
1833
+ const found = resolveDeviceById(registry, deviceId);
1834
+ if (found) {
1835
+ await found.device.config.setAll({ _profileMap: input.profileMap });
1836
+ return { success: true };
1837
+ }
1838
+ }
1839
+ if (!await resolvePersistedById(deviceId)) throw new Error(`[device-manager] Device with id ${deviceId} not found`);
1840
+ await requireDeviceOps(deviceId).setConfig({
1841
+ deviceId,
1842
+ values: { _profileMap: input.profileMap }
1843
+ });
1844
+ return { success: true };
1845
+ },
1846
+ probeStreams: async (input) => {
1847
+ const streamProbe = this.ctx.kernel.streamProbe;
1848
+ if (!streamProbe) return [];
1849
+ const sources = await provider.getStreamSources({ deviceId: input.deviceId });
1850
+ const results = [];
1851
+ for (const s of sources) {
1852
+ if (!s.url) continue;
1853
+ try {
1854
+ const metadata = await streamProbe.probe(s.url, { force: true });
1855
+ results.push({
1856
+ streamId: s.id,
1857
+ width: metadata.width,
1858
+ height: metadata.height,
1859
+ codec: metadata.codec,
1860
+ fps: metadata.fps,
1861
+ bitrateKbps: metadata.bitrateKbps
1862
+ });
1863
+ } catch (err) {
1864
+ this.ctx.logger.debug("streamProbe.probe failed — returning placeholder", { meta: {
1865
+ deviceId: input.deviceId,
1866
+ streamId: s.id,
1867
+ error: err instanceof Error ? err.message : String(err)
1868
+ } });
1869
+ results.push({ streamId: s.id });
1870
+ }
1871
+ }
1872
+ return results;
1873
+ },
1874
+ discoverDevices: async (input) => {
1875
+ const dp = await this.requireDeviceProvider(input.addonId);
1876
+ if (!await dp.supportsDiscovery({})) throw new Error(`Addon "${input.addonId}" does not support device discovery`);
1877
+ return (await dp.discoverDevices({})).map((d) => ({
1878
+ stableId: d.stableId,
1879
+ type: d.type,
1880
+ suggestedName: d.suggestedName,
1881
+ prefilledConfig: d.prefilledConfig
1882
+ }));
1883
+ },
1884
+ adoptDevice: async (input) => {
1885
+ const dp = await this.requireDeviceProvider(input.addonId);
1886
+ if (!await dp.supportsDiscovery({})) throw new Error(`Addon "${input.addonId}" does not support device adoption`);
1887
+ return dp.adoptDiscoveredDevice({ candidate: input.candidate });
1888
+ },
1889
+ getCreationSchema: async (input) => {
1890
+ const dp = await this.requireDeviceProvider(input.addonId);
1891
+ if (!await dp.supportsManualCreation({})) return null;
1892
+ return await dp.getChildCreationSchema({ type: input.type }) ?? null;
1893
+ },
1894
+ createDevice: async (input) => {
1895
+ const dp = await this.requireDeviceProvider(input.addonId);
1896
+ if (!await dp.supportsManualCreation({})) throw new Error(`Addon "${input.addonId}" does not support manual device creation`);
1897
+ return dp.createDevice({
1898
+ type: input.type,
1899
+ config: input.config
1900
+ });
1901
+ },
1902
+ testCreationField: async (input) => {
1903
+ return (await this.requireDeviceProvider(input.addonId)).testCreationField({
1904
+ type: input.type,
1905
+ key: input.key,
1906
+ value: input.value,
1907
+ ...input.formValues !== void 0 ? { formValues: input.formValues } : {}
1908
+ });
1909
+ },
1910
+ testField: async (input) => {
1911
+ const { deviceId } = input;
1912
+ let owningAddonId = null;
1913
+ if (registry) owningAddonId = registry.getAddonId(deviceId);
1914
+ if (!owningAddonId) owningAddonId = (await resolvePersistedById(deviceId))?.addonId ?? null;
1915
+ if (!owningAddonId) throw new Error(`Device with id ${deviceId} not found`);
1916
+ const dp = await this.waitDeviceProvider(owningAddonId);
1917
+ if (!dp) return {
1918
+ status: "ok",
1919
+ labels: [],
1920
+ error: void 0
1921
+ };
1922
+ if (typeof dp.testCreationField !== "function") return {
1923
+ status: "ok",
1924
+ labels: [],
1925
+ error: void 0
1926
+ };
1927
+ return dp.testCreationField({
1928
+ type: DeviceType.Camera,
1929
+ key: input.key,
1930
+ value: input.value
1931
+ });
1932
+ },
1933
+ getBindings: async (input) => {
1934
+ const result = await this.getBindings({ deviceId: input.deviceId });
1935
+ return {
1936
+ deviceId: input.deviceId,
1937
+ entries: result.entries
1938
+ };
1939
+ },
1940
+ getAllBindings: async () => {
1941
+ return this.getAllBindings();
1942
+ },
1943
+ setWrapperActive: async (input) => {
1944
+ return this.setWrapperActive({
1945
+ deviceId: input.deviceId,
1946
+ capName: input.capName,
1947
+ wrapperAddonId: input.wrapperAddonId,
1948
+ active: input.active
1949
+ });
1950
+ },
1951
+ listWrappersForCap: async (input) => this.listWrappersForCap(input),
1952
+ listBindableCapsForDeviceType: async (input) => this.listBindableCapsForDeviceType(input),
1953
+ getDeviceSettingsAggregate: async (input) => {
1954
+ return this.getDeviceAggregate(input.deviceId, "settings");
1955
+ },
1956
+ getDeviceLiveInfoAggregate: async (input) => {
1957
+ return this.getDeviceAggregate(input.deviceId, "live");
1958
+ },
1959
+ getDeviceAggregate: async (input) => {
1960
+ const [settings, live] = await Promise.all([this.getDeviceAggregate(input.deviceId, "settings"), this.getDeviceAggregate(input.deviceId, "live")]);
1961
+ return {
1962
+ settings,
1963
+ live
1964
+ };
1965
+ },
1966
+ updateDeviceField: async (input) => {
1967
+ return this.updateDeviceField({
1968
+ deviceId: input.deviceId,
1969
+ writerCapName: input.writerCapName,
1970
+ writerAddonId: input.writerAddonId,
1971
+ key: input.key,
1972
+ value: input.value
1973
+ });
1974
+ },
1975
+ updateDeviceFieldsBatch: async (input) => {
1976
+ return this.updateDeviceFieldsBatch({
1977
+ deviceId: input.deviceId,
1978
+ changes: input.changes
1979
+ });
1980
+ },
1981
+ getDeviceStatusAggregate: async (input) => this.getDeviceStatusAggregate(input)
1982
+ };
1983
+ this.ctx.logger.info("registered device-manager capability", { meta: { liveRegistry: registry !== null } });
1984
+ if (registry) {
1985
+ this.propagator = new DeviceEventPropagator({
1986
+ eventBus: this.ctx.eventBus,
1987
+ getParentOf: (id) => registry.getById(id)?.parentDeviceId ?? null,
1988
+ logger: {
1989
+ warn: (msg, meta) => this.ctx.logger.warn(msg, meta ?? {}),
1990
+ debug: (msg, meta) => this.ctx.logger.debug(msg, meta ?? {})
1991
+ }
1992
+ });
1993
+ this.propagator.start();
1994
+ this.ctx.logger.info("device-event-propagator started");
1995
+ }
1996
+ return [{
1997
+ capability: deviceManagerCapability,
1998
+ provider
1999
+ }, {
2000
+ capability: deviceStateCapability,
2001
+ provider: {
2002
+ getSnapshot: async (input) => {
2003
+ return this.snapshotForDevice(input.deviceId);
2004
+ },
2005
+ getCapSlice: async (input) => {
2006
+ const slice = this.stateMirror.get(input.deviceId)?.get(input.capName);
2007
+ return slice ? { ...slice } : null;
2008
+ },
2009
+ getAllSnapshots: async () => {
2010
+ const out = {};
2011
+ for (const [deviceId, perCap] of this.stateMirror) {
2012
+ const dev = {};
2013
+ for (const [capName, slice] of perCap) dev[capName] = { ...slice };
2014
+ out[String(deviceId)] = dev;
2015
+ }
2016
+ return out;
2017
+ },
2018
+ setCapSlice: async (input) => {
2019
+ const { deviceId, capName, slice } = input;
2020
+ if (!await resolvePersistedById(deviceId)) throw new Error(`[device-manager] setCapSlice: unknown device id=${deviceId}`);
2021
+ this.applySingleCapUpdate(deviceId, capName, slice);
2022
+ this.scheduleRuntimeStateDiskWrite(deviceId, settings);
2023
+ }
2024
+ }
2025
+ }];
2026
+ }
2027
+ /**
2028
+ * Single-cap mirror update — diff against the current mirror,
2029
+ * persist the new slice in-memory, emit `DeviceStateChanged` for
2030
+ * this cap. No-op on identical writes (both same shape and same
2031
+ * values). Called by `setCapSlice` provider.
2032
+ */
2033
+ applySingleCapUpdate(deviceId, capName, slice) {
2034
+ let perCap = this.stateMirror.get(deviceId);
2035
+ if (!perCap) {
2036
+ perCap = /* @__PURE__ */ new Map();
2037
+ this.stateMirror.set(deviceId, perCap);
2038
+ }
2039
+ const prior = perCap.get(capName);
2040
+ if (prior && shallowEqual(prior, slice)) return;
2041
+ perCap.set(capName, { ...slice });
2042
+ this.emitStateChanged(deviceId, capName, slice);
2043
+ }
2044
+ /**
2045
+ * Debounced disk writer. Coalesces frequent writes (motion phase
2046
+ * transitions, battery pushes) into one `writeDeviceRuntimeState`
2047
+ * per `RUNTIME_STATE_DEBOUNCE_MS` window. Reads the per-device
2048
+ * blob from the live mirror at flush time so the disk picture is
2049
+ * always the latest state — no risk of writing a stale snapshot.
2050
+ */
2051
+ scheduleRuntimeStateDiskWrite(deviceId, settings) {
2052
+ let slot = this.runtimeStateDebounce.get(deviceId);
2053
+ if (!slot) {
2054
+ slot = {
2055
+ timer: null,
2056
+ inFlight: null
2057
+ };
2058
+ this.runtimeStateDebounce.set(deviceId, slot);
2059
+ }
2060
+ if (slot.timer) return;
2061
+ slot.timer = setTimeout(() => {
2062
+ slot.timer = null;
2063
+ const blob = this.snapshotForDevice(deviceId);
2064
+ const write = (async () => {
2065
+ try {
2066
+ await settings.writeDeviceRuntimeState(deviceId, blob);
2067
+ } catch (err) {
2068
+ this.ctx.logger.warn("writeDeviceRuntimeState failed", {
2069
+ tags: { deviceId },
2070
+ meta: { error: err instanceof Error ? err.message : String(err) }
2071
+ });
2072
+ } finally {
2073
+ slot.inFlight = null;
2074
+ }
2075
+ })();
2076
+ slot.inFlight = write;
2077
+ }, DeviceManagerAddon.RUNTIME_STATE_DEBOUNCE_MS);
2078
+ }
2079
+ /**
2080
+ * One-shot mirror seed used by `loadRuntimeState` at boot so the
2081
+ * hub knows about every persisted slice without waiting for the
2082
+ * first `setCapSlice` call. No events emitted — this is
2083
+ * initial-state population, not a transition.
2084
+ */
2085
+ seedMirror(deviceId, blob) {
2086
+ let perCap = this.stateMirror.get(deviceId);
2087
+ if (!perCap) {
2088
+ perCap = /* @__PURE__ */ new Map();
2089
+ this.stateMirror.set(deviceId, perCap);
2090
+ }
2091
+ for (const [capName, raw] of Object.entries(blob)) {
2092
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) continue;
2093
+ perCap.set(capName, { ...raw });
2094
+ }
2095
+ }
2096
+ snapshotForDevice(deviceId) {
2097
+ const perCap = this.stateMirror.get(deviceId);
2098
+ if (!perCap) return {};
2099
+ const out = {};
2100
+ for (const [k, v] of perCap) out[k] = { ...v };
2101
+ return out;
2102
+ }
2103
+ emitStateChanged(deviceId, capName, slice) {
2104
+ this.ctx.eventBus.emit({
2105
+ id: randomUUID(),
2106
+ timestamp: /* @__PURE__ */ new Date(),
2107
+ source: {
2108
+ type: "device",
2109
+ id: deviceId
2110
+ },
2111
+ category: EventCategory.DeviceStateChanged,
2112
+ data: {
2113
+ deviceId,
2114
+ capName,
2115
+ slice
2116
+ }
2117
+ });
2118
+ }
2119
+ async onShutdown() {
2120
+ this.propagator?.stop();
2121
+ this.propagator = null;
2122
+ const settings = this.ctx.settings;
2123
+ const pending = [];
2124
+ for (const [deviceId, slot] of this.runtimeStateDebounce) {
2125
+ if (slot.timer) {
2126
+ clearTimeout(slot.timer);
2127
+ slot.timer = null;
2128
+ if (settings) {
2129
+ const blob = this.snapshotForDevice(deviceId);
2130
+ pending.push(settings.writeDeviceRuntimeState(deviceId, blob).catch((err) => {
2131
+ this.ctx.logger.warn("shutdown writeDeviceRuntimeState failed", {
2132
+ tags: { deviceId },
2133
+ meta: { error: err instanceof Error ? err.message : String(err) }
2134
+ });
2135
+ }));
2136
+ }
2137
+ }
2138
+ if (slot.inFlight) pending.push(slot.inFlight);
2139
+ }
2140
+ await Promise.all(pending);
2141
+ this.runtimeStateDebounce.clear();
2142
+ }
2143
+ };
2144
+ //#endregion
2145
+ export { DeviceManagerAddon, DeviceManagerAddon as default };
2146
+
9
2147
  //# sourceMappingURL=device-manager.addon.mjs.map