@camera.ui/server 2.0.3 → 2.0.5

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 (285) hide show
  1. package/dist/interface/assets/{404-4kT4tezJ.js → 404-DRRPa9Hr.js} +1 -1
  2. package/dist/interface/assets/About-Db5MifHo.js +1 -0
  3. package/dist/interface/assets/{Adminpanel-B38Z5X54.js → Adminpanel-ByDZ8Xpn.js} +1 -1
  4. package/dist/interface/assets/{Automation-BbX3-xf_.js → Automation-BqJ8PyPa.js} +2 -2
  5. package/dist/interface/assets/{AutomationImport-D5Ov1XoG.js → AutomationImport-BvuLSZcZ.js} +2 -2
  6. package/dist/interface/assets/{AutomationStore-CVQpLFae.js → AutomationStore-Ca07W8uH.js} +2 -2
  7. package/dist/interface/assets/{AutomationStoreDetail-NNXJ16cM.js → AutomationStoreDetail-DmFvT2az.js} +2 -2
  8. package/dist/interface/assets/{Automations-w8GvcQRP.js → Automations-Dr8K-cDz.js} +2 -2
  9. package/dist/interface/assets/{Camera-Bx-Fc8Rk.js → Camera-Bu1U8Oal.js} +2 -2
  10. package/dist/interface/assets/{CameraConsole-Br1Ot4HR.js → CameraConsole-BcvP76iJ.js} +1 -1
  11. package/dist/interface/assets/{Cameras-C-NQYdbQ.js → Cameras-Dwr5fbzg.js} +2 -2
  12. package/dist/interface/assets/{Camview-DWgaBueW.js → Camview-F0vVBWeZ.js} +1 -1
  13. package/dist/interface/assets/Config-CIAVylkc.js +2 -0
  14. package/dist/interface/assets/{ConfigActionCameraControl-Cvjyd2Rq.js → ConfigActionCameraControl-C1dsQnYF.js} +1 -1
  15. package/dist/interface/assets/{ConfigActionDelay-Rnxk--Qt.js → ConfigActionDelay-DTjjadCG.js} +1 -1
  16. package/dist/interface/assets/{ConfigActionHttp-BF8vrrHR.js → ConfigActionHttp-IFqZaHDZ.js} +1 -1
  17. package/dist/interface/assets/{ConfigActionImageInput-C8_H3ubM.js → ConfigActionImageInput-DY8RQJ2h.js} +1 -1
  18. package/dist/interface/assets/{ConfigActionNotification-T35A9o6f.js → ConfigActionNotification-CJL8uNR-.js} +1 -1
  19. package/dist/interface/assets/{ConfigActionOutput-CxB3eLLl.js → ConfigActionOutput-Butmz0jI.js} +1 -1
  20. package/dist/interface/assets/{ConfigActionPlugin-BHdyn9Te.js → ConfigActionPlugin-D5oE8oXm.js} +1 -1
  21. package/dist/interface/assets/{ConfigActionSensor-CMrg6h2b.js → ConfigActionSensor-CJ-mk6eS.js} +1 -1
  22. package/dist/interface/assets/{ConfigActionSnapshot-Dz6LdZsA.js → ConfigActionSnapshot-DCCiYu-s.js} +1 -1
  23. package/dist/interface/assets/{ConfigActionVariable-DIlUMHDy.js → ConfigActionVariable-F28sicqK.js} +1 -1
  24. package/dist/interface/assets/{ConfigConditionIfElse-0XRDQcTw.js → ConfigConditionIfElse-BJhRgP7t.js} +1 -1
  25. package/dist/interface/assets/{ConfigConditionSensorState-BzFQHkML.js → ConfigConditionSensorState-B6MdgPfo.js} +1 -1
  26. package/dist/interface/assets/{ConfigConditionSwitch-C7pp1EZI.js → ConfigConditionSwitch-BKToCaFK.js} +1 -1
  27. package/dist/interface/assets/{ConfigConditionTime-B0zctDc4.js → ConfigConditionTime-ChXeDbZB.js} +1 -1
  28. package/dist/interface/assets/{ConfigTriggerDetection-DOS-gNhG.js → ConfigTriggerDetection-BWd3QqYf.js} +1 -1
  29. package/dist/interface/assets/{ConfigTriggerGeofence-YggfZUFH.js → ConfigTriggerGeofence-BoT9t-8V.js} +1 -1
  30. package/dist/interface/assets/{ConfigTriggerManual-BpBTh3CS.js → ConfigTriggerManual-Bt62nmi8.js} +1 -1
  31. package/dist/interface/assets/{ConfigTriggerSchedule-DZG10NcA.js → ConfigTriggerSchedule-oBDIqAMo.js} +1 -1
  32. package/dist/interface/assets/{ConfigTriggerSensor-Cu657NGX.js → ConfigTriggerSensor-BlTHlstZ.js} +1 -1
  33. package/dist/interface/assets/{ConfigTriggerSystem-BfIHn9AZ.js → ConfigTriggerSystem--nEg_iQ0.js} +1 -1
  34. package/dist/interface/assets/{ConfigTriggerWebhook-BKPOGAs3.js → ConfigTriggerWebhook-83rLUOjQ.js} +1 -1
  35. package/dist/interface/assets/{Console-C6A7MqYh.js → Console-2sHljg8h.js} +1 -1
  36. package/dist/interface/assets/{CuiActionButton-BSy6VaJ6.js → CuiActionButton-CNjL01gn.js} +1 -1
  37. package/dist/interface/assets/{CuiAvatar-BwAVgBUB.js → CuiAvatar-BDT9c-UI.js} +1 -1
  38. package/dist/interface/assets/{CuiBBoxPlayground-BUPzZ0sI.js → CuiBBoxPlayground-CMnBcPwI.js} +1 -1
  39. package/dist/interface/assets/CuiBottomSheet-WuxFsOrw.js +1 -0
  40. package/dist/interface/assets/{CuiBottomSheet-CxYeW_sq.js → CuiBottomSheet-h5sjwTf4.js} +1 -1
  41. package/dist/interface/assets/{CuiCameraCard-Ci09TTwg.js → CuiCameraCard-Bip9OIZO.js} +1 -1
  42. package/dist/interface/assets/{CuiCameraPipCard-fYyJQ9DL.js → CuiCameraPipCard-DElQvAB0.js} +1 -1
  43. package/dist/interface/assets/{CuiCameraRecordings-75jdIwEG.js → CuiCameraRecordings-Bf5IG7OM.js} +1 -1
  44. package/dist/interface/assets/{CuiCameraShares-C2HDYSGy.js → CuiCameraShares-Cw-Mffg8.js} +1 -1
  45. package/dist/interface/assets/{CuiCameraSnapshot-Cz8aYjoo.js → CuiCameraSnapshot-BS-_U34M.js} +1 -1
  46. package/dist/interface/assets/{CuiCameraTable-DrIhSgmU.js → CuiCameraTable-DY_unr4m.js} +1 -1
  47. package/dist/interface/assets/{CuiDataTable-DhnQRMXG.js → CuiDataTable-C5qi7zpz.js} +1 -1
  48. package/dist/interface/assets/{CuiFloatingButton-DRKyDsPh.js → CuiFloatingButton-Pmur2pAJ.js} +1 -1
  49. package/dist/interface/assets/{CuiImage-i_fTDsHd.js → CuiImage-DVoYtgtl.js} +1 -1
  50. package/dist/interface/assets/{CuiMenu-DSTlEsg9.js → CuiMenu-B7AhQbgB.js} +1 -1
  51. package/dist/interface/assets/{CuiPluginCard-XmOOko_4.js → CuiPluginCard-B5PhaGoF.js} +2 -2
  52. package/dist/interface/assets/{CuiPluginCategoryChip-BwL2qCTM.js → CuiPluginCategoryChip-nEwIiKiK.js} +1 -1
  53. package/dist/interface/assets/{CuiPluginOAuthButton-CmW7OY0L.js → CuiPluginOAuthButton-CJGsl-89.js} +2 -2
  54. package/dist/interface/assets/{CuiPluginTrustBadge-KcOasEJH.js → CuiPluginTrustBadge-DExKttfC.js} +1 -1
  55. package/dist/interface/assets/{CuiQRCode-DgaxwrkH.js → CuiQRCode-DkRZ5hfS.js} +1 -1
  56. package/dist/interface/assets/{CuiRecordingsGrid-BxoJSn6Z.js → CuiRecordingsGrid-mFXSsuG8.js} +1 -1
  57. package/dist/interface/assets/{CuiSchema-BDrsTIqf.js → CuiSchema-BSv1nUun.js} +1 -1
  58. package/dist/interface/assets/{CuiTopNavbarItem-DVwItfbG.js → CuiTopNavbarItem-w21c3xtQ.js} +1 -1
  59. package/dist/interface/assets/{CuiTopbarSlot-BT6XOANB.js → CuiTopbarSlot-CYPUqVPx.js} +1 -1
  60. package/dist/interface/assets/{Faces-DvYCASPC.js → Faces-DLpVsIkY.js} +1 -1
  61. package/dist/interface/assets/{FirstSteps-DAsmDJJE.js → FirstSteps-BDDy9x_6.js} +1 -1
  62. package/dist/interface/assets/{Instances-D4fzQrI3.js → Instances-FeIAnJix.js} +1 -1
  63. package/dist/interface/assets/{Logs-DYXtlueg.js → Logs-CIUyuGwj.js} +1 -1
  64. package/dist/interface/assets/{Menu-BcBthWK9.js → Menu-Gqs2UiSW.js} +1 -1
  65. package/dist/interface/assets/{OAuthDeviceFlow-C70vC26b.js → OAuthDeviceFlow-hNJ_UrdG.js} +1 -1
  66. package/dist/interface/assets/{Plugin-Ek8CJHb3.js → Plugin-BKItJas0.js} +1 -1
  67. package/dist/interface/assets/{PluginConsole-n1wMznj9.js → PluginConsole-Bwd5Mktk.js} +1 -1
  68. package/dist/interface/assets/PluginDetail-BBh5y9qU.js +2 -0
  69. package/dist/interface/assets/{PluginSchema-DDpK-w_q.js → PluginSchema-D92qcqf9.js} +1 -1
  70. package/dist/interface/assets/PluginSearch-BN3U3NY6.js +2 -0
  71. package/dist/interface/assets/PluginSearch-nHAFKMX0.js +1 -0
  72. package/dist/interface/assets/Plugins-gxyxkQ5i.js +1 -0
  73. package/dist/interface/assets/Recordings-B3Xw23vh.js +1 -0
  74. package/dist/interface/assets/{Settings-DqA1TU3I.js → Settings-D2LHMohy2.js} +1 -1
  75. package/dist/interface/assets/SettingsAccount-DUN1oTTn.js +1 -0
  76. package/dist/interface/assets/{SettingsAccount-BEVxkPFS.js → SettingsAccount-b4eVxI3M.js} +1 -1
  77. package/dist/interface/assets/{SettingsAppearance-B9KzFCdx.js → SettingsAppearance--Eidr-fz.js} +1 -1
  78. package/dist/interface/assets/SettingsAppearance-CLVMmnR_.js +1 -0
  79. package/dist/interface/assets/{SettingsBackup-j_z4sOId.js → SettingsBackup-DUYVdKjD.js} +1 -1
  80. package/dist/interface/assets/{SettingsNotifications-D2WgP9Pd.js → SettingsNotifications-xYOuLZsr.js} +1 -1
  81. package/dist/interface/assets/SettingsRecordings-BQDf4Zeg.js +1 -0
  82. package/dist/interface/assets/{SettingsRemote-C7wTY9EH.js → SettingsRemote-BXr1HLHm.js} +1 -1
  83. package/dist/interface/assets/SettingsSystem-DAjNwp4I.js +2 -0
  84. package/dist/interface/assets/{SettingsUsers-CdVrMc0h.js → SettingsUsers-DJ-eMMWe.js} +1 -1
  85. package/dist/interface/assets/{Terminal-pyMSi5jh.js → Terminal-P3yVGG8s.js} +1 -1
  86. package/dist/interface/assets/{VariableInput-DmE1m9Il.js → VariableInput-DdVw4SIR.js} +1 -1
  87. package/dist/interface/assets/{VariableSuggestions-eII-1Bd_.js → VariableSuggestions-SJPd5c8d.js} +1 -1
  88. package/dist/interface/assets/VersionsHandler-DcZ3RtHV.js +7 -0
  89. package/dist/interface/assets/{Workers-KL_fb-Ki.js → Workers-DWSJsRko.js} +1 -1
  90. package/dist/interface/assets/api-CKqAhmSC.css +2 -0
  91. package/dist/interface/assets/{api-DVDZCfaM.js → api-Cv3vt1pa.js} +1 -1
  92. package/dist/interface/assets/api-Ex2VUVD1.js +2 -0
  93. package/dist/interface/assets/{auth-pmpjgGBQ.js → auth-W-JA6GOD.js} +1 -1
  94. package/dist/interface/assets/{automations-xpnVYpRH.js → automations-d_B5L3sO.js} +1 -1
  95. package/dist/interface/assets/{automations-73X55Wxy.js → automations-irBgx9Ew.js} +1 -1
  96. package/dist/interface/assets/{backup-G0wpvhtl.js → backup-BT-P0duG.js} +1 -1
  97. package/dist/interface/assets/{cameras-CURX5SWn.js → cameras-DxjpuzW1.js} +1 -1
  98. package/dist/interface/assets/{cameras.schema-UcmqaqCT.js → cameras.schema-7ex1wBck.js} +1 -1
  99. package/dist/interface/assets/cloudHandoff-Cu1sRatW.js +1 -0
  100. package/dist/interface/assets/config-DwlSsbcP.js +1 -0
  101. package/dist/interface/assets/{deviceId-DRtZsFhF.js → deviceId-I24oaPe6.js} +1 -1
  102. package/dist/interface/assets/{hasPermission-DKf8cjsF.js → hasPermission-Cz0dkfyp.js} +1 -1
  103. package/dist/interface/assets/{instances-DvkGEhQ9.js → instances-D6wHW9_0.js} +1 -1
  104. package/dist/interface/assets/{main-D8ZyVvgT.css → main-BjuXWRyJ.css} +1 -1
  105. package/dist/interface/assets/main-C-WCGnJh.js +3 -0
  106. package/dist/interface/assets/{notifications-CThoWQfW.js → notifications-DLNUbEKC.js} +1 -1
  107. package/dist/interface/assets/{plugins-DN9592Gw.js → plugins-D-tGC0mr.js} +1 -1
  108. package/dist/interface/assets/{round-restart-alt-C6cWOig8.js → round-restart-alt-CaOjUTav.js} +1 -1
  109. package/dist/interface/assets/router-Cbt2STjH.js +2 -0
  110. package/dist/interface/assets/{routerState-DxipV4mY.js → routerState-_BXrxePY.js} +1 -1
  111. package/dist/interface/assets/{server-BaJ0ampx.js → server-ACfR-bOw.js} +1 -1
  112. package/dist/interface/assets/{shares-B6eaflmY.js → shares-Ut3rTZUx.js} +1 -1
  113. package/dist/interface/assets/{theme-Dbkvi1e_.js → theme-CDU91XW5.js} +1 -1
  114. package/dist/interface/assets/{types-CAW47AJe.js → types-Blh_1COV.js} +1 -1
  115. package/dist/interface/assets/ui-SZnjYe2F.js +1 -0
  116. package/dist/interface/assets/useAppVersion-Bo_n0OVQ.js +1 -0
  117. package/dist/interface/assets/{useBiometricCredentials-BJheacnv.js → useBiometricCredentials-XyEQbPBo.js} +1 -1
  118. package/dist/interface/assets/{useCameraOptions-r_TT2WJk.js → useCameraOptions-C3-ZVV2A.js} +1 -1
  119. package/dist/interface/assets/{useCuiCameraDrawer-DwlhaZ2i.js → useCuiCameraDrawer-CebQFNWX.js} +1 -1
  120. package/dist/interface/assets/useCuiDialog-ClphnG79.js +2 -0
  121. package/dist/interface/assets/{useFileDownload-BE32i7R1.js → useFileDownload-CdDprX3v.js} +1 -1
  122. package/dist/interface/assets/{useLogsSocket-CLSJzN6z.js → useLogsSocket-DKU0kKe_.js} +1 -1
  123. package/dist/interface/assets/{useServerRestart-DCJcb-IB.js → useServerRestart-CgCmTap7.js} +1 -1
  124. package/dist/interface/assets/{useSharedComposables-CWxbvFAw.js → useSharedComposables-BcUR_c17.js} +1 -1
  125. package/dist/interface/assets/{useStreamStatus-DH9KQ7aE.js → useStreamStatus-BTa-XXZJ.js} +1 -1
  126. package/dist/interface/assets/useUpdateChannel-WznJBL9L.js +1 -0
  127. package/dist/interface/assets/{useUserOptions-GzbPgMnC.js → useUserOptions-DffxmIf7.js} +1 -1
  128. package/dist/interface/assets/{users.schema-DKf9LQAY.js → users.schema-BcwArBbH.js} +1 -1
  129. package/dist/interface/assets/{vendor-cameraui-DhctzD3V.js → vendor-cameraui-zHth8Pa5.js} +2 -2
  130. package/dist/interface/assets/{vendor-chart-BjXPY6za.js → vendor-chart-CTk1N5fJ.js} +1 -1
  131. package/dist/interface/assets/{vendor-core-DnC_SdtD.js → vendor-core-_UeC1YMQ.js} +1 -1
  132. package/dist/interface/assets/{vendor-primevue-OV61z06m.js → vendor-primevue-BXMgwPEu.js} +1 -1
  133. package/dist/interface/assets/{view-grid-Y-11W6nc.js → view-grid-CX0RwdlE.js} +1 -1
  134. package/dist/interface/index.html +45 -44
  135. package/dist/interface/sw.js +1 -1
  136. package/dist/server/package.json +26 -16
  137. package/dist/server/requirements.txt +3 -2
  138. package/dist/server/src/_camera_ui_tools/camera_ui_common/common_utils/signal_handler.py +17 -9
  139. package/dist/server/src/_camera_ui_tools/camera_ui_sdk/camera/detection.py +10 -0
  140. package/dist/server/src/_camera_ui_tools/camera_ui_sdk/internal/event_emitter.py +45 -0
  141. package/dist/server/src/_camera_ui_tools/camera_ui_sdk/storage/__init__.py +47 -12
  142. package/dist/server/src/api/controllers/plugins.controller.js +11 -3
  143. package/dist/server/src/api/controllers/plugins.controller.js.map +1 -1
  144. package/dist/server/src/api/schemas/cameras.schema.d.ts +27 -0
  145. package/dist/server/src/api/schemas/cameras.schema.js +6 -0
  146. package/dist/server/src/api/schemas/cameras.schema.js.map +1 -1
  147. package/dist/server/src/api/schemas/config.schema.d.ts +2 -0
  148. package/dist/server/src/api/schemas/config.schema.js +1 -0
  149. package/dist/server/src/api/schemas/config.schema.js.map +1 -1
  150. package/dist/server/src/api/services/plugins.service.js +1 -1
  151. package/dist/server/src/api/services/plugins.service.js.map +1 -1
  152. package/dist/server/src/api/types/index.d.ts +1 -1
  153. package/dist/server/src/api.d.ts +1 -0
  154. package/dist/server/src/api.js +11 -3
  155. package/dist/server/src/api.js.map +1 -1
  156. package/dist/server/src/camera/decoder/ptz-autotracker.js +51 -78
  157. package/dist/server/src/camera/decoder/ptz-autotracker.js.map +1 -1
  158. package/dist/server/src/go2rtc/api/routes/streams.js +5 -6
  159. package/dist/server/src/go2rtc/api/routes/streams.js.map +1 -1
  160. package/dist/server/src/go2rtc/index.js +1 -0
  161. package/dist/server/src/go2rtc/index.js.map +1 -1
  162. package/dist/server/src/go2rtc/types.d.ts +3 -0
  163. package/dist/server/src/go2rtc/types.js.map +1 -1
  164. package/dist/server/src/main.js +1 -1
  165. package/dist/server/src/main.js.map +1 -1
  166. package/dist/server/src/manager/deviceManager.js +4 -1
  167. package/dist/server/src/manager/deviceManager.js.map +1 -1
  168. package/dist/server/src/manager/discoveryManager.d.ts +3 -2
  169. package/dist/server/src/manager/discoveryManager.js +58 -29
  170. package/dist/server/src/manager/discoveryManager.js.map +1 -1
  171. package/dist/server/src/plugins/config-store.d.ts +1 -1
  172. package/dist/server/src/plugins/config-store.js +17 -10
  173. package/dist/server/src/plugins/config-store.js.map +1 -1
  174. package/dist/server/src/plugins/index.d.ts +1 -0
  175. package/dist/server/src/plugins/index.js +38 -2
  176. package/dist/server/src/plugins/index.js.map +1 -1
  177. package/dist/server/src/plugins/runtime/base.js +10 -5
  178. package/dist/server/src/plugins/runtime/base.js.map +1 -1
  179. package/dist/server/src/plugins/runtime/go/index.js +1 -0
  180. package/dist/server/src/plugins/runtime/go/index.js.map +1 -1
  181. package/dist/server/src/plugins/runtime/node/child.d.ts +1 -0
  182. package/dist/server/src/plugins/runtime/node/child.js +62 -19
  183. package/dist/server/src/plugins/runtime/node/child.js.map +1 -1
  184. package/dist/server/src/plugins/runtime/node/configDb.d.ts +8 -0
  185. package/dist/server/src/plugins/runtime/node/configDb.js +15 -0
  186. package/dist/server/src/plugins/runtime/node/configDb.js.map +1 -1
  187. package/dist/server/src/plugins/runtime/node/pluginApi.d.ts +4 -0
  188. package/dist/server/src/plugins/runtime/node/pluginApi.js +13 -5
  189. package/dist/server/src/plugins/runtime/node/pluginApi.js.map +1 -1
  190. package/dist/server/src/plugins/runtime/node/proxy/cameraDevice.d.ts +1 -2
  191. package/dist/server/src/plugins/runtime/node/proxy/cameraDevice.js +2 -8
  192. package/dist/server/src/plugins/runtime/node/proxy/cameraDevice.js.map +1 -1
  193. package/dist/server/src/plugins/runtime/node/proxy/coreManager.d.ts +3 -2
  194. package/dist/server/src/plugins/runtime/node/proxy/coreManager.js +9 -13
  195. package/dist/server/src/plugins/runtime/node/proxy/coreManager.js.map +1 -1
  196. package/dist/server/src/plugins/runtime/node/proxy/deviceManager.d.ts +3 -2
  197. package/dist/server/src/plugins/runtime/node/proxy/deviceManager.js +14 -12
  198. package/dist/server/src/plugins/runtime/node/proxy/deviceManager.js.map +1 -1
  199. package/dist/server/src/plugins/runtime/node/proxy/fileServe.js +4 -9
  200. package/dist/server/src/plugins/runtime/node/proxy/fileServe.js.map +1 -1
  201. package/dist/server/src/plugins/runtime/node/proxy/notificationManager.js +3 -0
  202. package/dist/server/src/plugins/runtime/node/proxy/notificationManager.js.map +1 -1
  203. package/dist/server/src/plugins/runtime/node/storage.d.ts +5 -2
  204. package/dist/server/src/plugins/runtime/node/storage.js +92 -67
  205. package/dist/server/src/plugins/runtime/node/storage.js.map +1 -1
  206. package/dist/server/src/plugins/runtime/node/storageController.d.ts +2 -0
  207. package/dist/server/src/plugins/runtime/node/storageController.js +12 -4
  208. package/dist/server/src/plugins/runtime/node/storageController.js.map +1 -1
  209. package/dist/server/src/plugins/runtime/python/child.py +47 -38
  210. package/dist/server/src/plugins/runtime/python/config_db.py +28 -17
  211. package/dist/server/src/plugins/runtime/python/index.js +1 -0
  212. package/dist/server/src/plugins/runtime/python/index.js.map +1 -1
  213. package/dist/server/src/plugins/runtime/python/plugin_api.py +5 -5
  214. package/dist/server/src/plugins/runtime/python/proxy/camera_device.py +14 -13
  215. package/dist/server/src/plugins/runtime/python/proxy/core_manager.py +11 -14
  216. package/dist/server/src/plugins/runtime/python/proxy/device_manager.py +23 -18
  217. package/dist/server/src/plugins/runtime/python/remote_urls.py +3 -7
  218. package/dist/server/src/plugins/runtime/python/storage.py +157 -126
  219. package/dist/server/src/plugins/runtime/python/storage_controller.py +28 -13
  220. package/dist/server/src/plugins/runtime/python/store.py +513 -0
  221. package/dist/server/src/plugins/runtime/python/store_migrate.py +59 -0
  222. package/dist/server/src/plugins/store/coalescingWriter.d.ts +7 -0
  223. package/dist/server/src/plugins/store/coalescingWriter.js +52 -0
  224. package/dist/server/src/plugins/store/coalescingWriter.js.map +1 -0
  225. package/dist/server/src/plugins/store/envelope.d.ts +5 -0
  226. package/dist/server/src/plugins/store/envelope.js +40 -0
  227. package/dist/server/src/plugins/store/envelope.js.map +1 -0
  228. package/dist/server/src/plugins/store/layout.d.ts +2 -0
  229. package/dist/server/src/plugins/store/layout.js +60 -0
  230. package/dist/server/src/plugins/store/layout.js.map +1 -0
  231. package/dist/server/src/plugins/store/location.d.ts +14 -0
  232. package/dist/server/src/plugins/store/location.js +55 -0
  233. package/dist/server/src/plugins/store/location.js.map +1 -0
  234. package/dist/server/src/plugins/store/migrate.d.ts +1 -0
  235. package/dist/server/src/plugins/store/migrate.js +22 -0
  236. package/dist/server/src/plugins/store/migrate.js.map +1 -0
  237. package/dist/server/src/plugins/store/pluginStoreFile.d.ts +14 -0
  238. package/dist/server/src/plugins/store/pluginStoreFile.js +58 -0
  239. package/dist/server/src/plugins/store/pluginStoreFile.js.map +1 -0
  240. package/dist/server/src/plugins/store/storeFile.d.ts +5 -0
  241. package/dist/server/src/plugins/store/storeFile.js +112 -0
  242. package/dist/server/src/plugins/store/storeFile.js.map +1 -0
  243. package/dist/server/src/plugins/store/validate.d.ts +1 -0
  244. package/dist/server/src/plugins/store/validate.js +49 -0
  245. package/dist/server/src/plugins/store/validate.js.map +1 -0
  246. package/dist/server/src/plugins/worker.js +9 -8
  247. package/dist/server/src/plugins/worker.js.map +1 -1
  248. package/dist/server/src/remote/services/cloudflare-managed.js +2 -2
  249. package/dist/server/src/remote/services/cloudflare-managed.js.map +1 -1
  250. package/dist/server/src/remote/services/cloudflare.js +1 -0
  251. package/dist/server/src/remote/services/cloudflare.js.map +1 -1
  252. package/dist/server/src/remote/tunnel/client.js +1 -0
  253. package/dist/server/src/remote/tunnel/client.js.map +1 -1
  254. package/dist/server/src/rpc/server.js +12 -5
  255. package/dist/server/src/rpc/server.js.map +1 -1
  256. package/dist/server/src/services/config/defaults.js +1 -0
  257. package/dist/server/src/services/config/defaults.js.map +1 -1
  258. package/dist/server/src/services/config/types.d.ts +1 -0
  259. package/dist/server/src/utils/npm/index.d.ts +1 -1
  260. package/dist/server/src/utils/npm/index.js +7 -4
  261. package/dist/server/src/utils/npm/index.js.map +1 -1
  262. package/dist/server/src/workers/capabilities/plugin-host.js +1 -1
  263. package/dist/server/src/workers/capabilities/plugin-host.js.map +1 -1
  264. package/package.json +26 -16
  265. package/dist/interface/assets/About-CMxNy7no.js +0 -1
  266. package/dist/interface/assets/Config-CcQUpeLe.js +0 -2
  267. package/dist/interface/assets/CuiBottomSheet-CFGurpP0.js +0 -1
  268. package/dist/interface/assets/PluginDetail-COURDgOk.js +0 -2
  269. package/dist/interface/assets/PluginSearch-Czyt4or9.js +0 -2
  270. package/dist/interface/assets/PluginSearch-SpzDNe08.js +0 -1
  271. package/dist/interface/assets/Plugins-C0YqC8-O.js +0 -1
  272. package/dist/interface/assets/Recordings-CdJm5exh.js +0 -1
  273. package/dist/interface/assets/SettingsAccount-BZaLHEXI.js +0 -1
  274. package/dist/interface/assets/SettingsAppearance-DX4C4Xxw.js +0 -1
  275. package/dist/interface/assets/SettingsRecordings-DyMo9O0s.js +0 -1
  276. package/dist/interface/assets/SettingsSystem-CrrqIGPM.js +0 -2
  277. package/dist/interface/assets/VersionsHandler-C6NlIR5o.js +0 -7
  278. package/dist/interface/assets/api-BOvysXhM.js +0 -2
  279. package/dist/interface/assets/api-D7tGlI-z.css +0 -2
  280. package/dist/interface/assets/cloudHandoff-BFhzjzv4.js +0 -1
  281. package/dist/interface/assets/main-DL335xeQ.js +0 -3
  282. package/dist/interface/assets/router-UepCX3sI.js +0 -2
  283. package/dist/interface/assets/ui-CH55JEcY.js +0 -1
  284. package/dist/interface/assets/useCuiDialog-DSyAPuxQ.js +0 -2
  285. /package/dist/interface/assets/{logo-512-DLKVOU2S.png → error-256-DLKVOU2S.png} +0 -0
@@ -0,0 +1,513 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import math
5
+ import os
6
+ import shutil
7
+ import sys
8
+ import time
9
+ import zlib
10
+ from contextlib import suppress
11
+ from dataclasses import dataclass
12
+ from pathlib import Path
13
+ from typing import TYPE_CHECKING, Any, Protocol
14
+
15
+ import msgpack
16
+
17
+ if TYPE_CHECKING:
18
+ from collections.abc import Awaitable, Callable
19
+
20
+ STORE_FILE_NAME = "store.cui"
21
+
22
+ _MAGIC = b"CUI1"
23
+ _MAX_SAFE_INTEGER = 2**53 - 1
24
+ _RENAME_RETRY_DELAYS_MS = (10, 25, 60, 150, 300)
25
+ _IS_WINDOWS = sys.platform == "win32"
26
+
27
+
28
+ class StoreLogger(Protocol):
29
+ def debug(self, message: str, /) -> None: ...
30
+
31
+ def warn(self, message: str, /) -> None: ...
32
+
33
+ def error(self, message: str, /) -> None: ...
34
+
35
+
36
+ class StoreCorruptError(Exception):
37
+ def __init__(self, message: str) -> None:
38
+ super().__init__(f"store envelope: {message}")
39
+
40
+
41
+ def encode_envelope(payload: dict[str, Any]) -> bytes:
42
+ body: bytes = msgpack.packb(payload, use_bin_type=True)
43
+ return _MAGIC + body + zlib.crc32(body).to_bytes(4, "little")
44
+
45
+
46
+ def decode_envelope(buf: bytes) -> dict[str, Any]:
47
+ if len(buf) < len(_MAGIC) + 4 or buf[: len(_MAGIC)] != _MAGIC:
48
+ raise StoreCorruptError("bad magic")
49
+
50
+ body = buf[len(_MAGIC) : -4]
51
+ if zlib.crc32(body) != int.from_bytes(buf[-4:], "little"):
52
+ raise StoreCorruptError("crc mismatch")
53
+
54
+ try:
55
+ payload = msgpack.unpackb(body, raw=False, strict_map_key=False)
56
+ except Exception as error:
57
+ raise StoreCorruptError(f"payload decode failed: {error}") from error
58
+
59
+ if not isinstance(payload, dict):
60
+ raise StoreCorruptError("payload is not a map")
61
+
62
+ return payload
63
+
64
+
65
+ def _replace_with_retry(tmp_path: str, path: str) -> None:
66
+ for attempt, delay_ms in enumerate(_RENAME_RETRY_DELAYS_MS):
67
+ try:
68
+ os.replace(tmp_path, path)
69
+ return
70
+ except PermissionError:
71
+ # Windows AV scanners / the search indexer transiently hold the
72
+ # rename target open; the error is retryable there within a small
73
+ # budget (~0.5s total), nowhere else.
74
+ if not _IS_WINDOWS or attempt >= len(_RENAME_RETRY_DELAYS_MS) - 1:
75
+ raise
76
+ time.sleep(delay_ms / 1000)
77
+
78
+
79
+ def _write_store_file_sync(path: str, buf: bytes) -> None:
80
+ tmp_path = f"{path}.tmp-{os.getpid()}"
81
+ try:
82
+ fd = os.open(tmp_path, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o644)
83
+ try:
84
+ os.write(fd, buf)
85
+ os.fsync(fd)
86
+ finally:
87
+ os.close(fd)
88
+ _replace_with_retry(tmp_path, path)
89
+ except Exception:
90
+ with suppress(OSError):
91
+ os.unlink(tmp_path)
92
+ raise
93
+
94
+
95
+ async def write_store_file(path: str, payload: dict[str, Any], log: StoreLogger | None = None) -> None:
96
+ buf = encode_envelope(payload)
97
+
98
+ try:
99
+ await asyncio.to_thread(_write_store_file_sync, path, buf)
100
+ except Exception as error:
101
+ if log:
102
+ log.error(f"store: write {path} failed: {error}")
103
+ raise
104
+
105
+
106
+ def _read_bytes(path: str) -> bytes | None:
107
+ try:
108
+ return Path(path).read_bytes()
109
+ except FileNotFoundError:
110
+ return None
111
+
112
+
113
+ async def read_store_file(path: str, log: StoreLogger | None = None) -> dict[str, Any] | None:
114
+ buf = await asyncio.to_thread(_read_bytes, path)
115
+ if buf is None:
116
+ return None
117
+
118
+ try:
119
+ payload = decode_envelope(buf)
120
+ return payload
121
+ except StoreCorruptError as error:
122
+ if log:
123
+ log.error(f"store: {path} is corrupt ({error}) — trying backup")
124
+
125
+ bak_path = f"{path}.bak"
126
+ try:
127
+ bak_buf = await asyncio.to_thread(Path(bak_path).read_bytes)
128
+ except OSError:
129
+ bak_buf = None
130
+
131
+ if bak_buf is not None:
132
+ try:
133
+ payload = decode_envelope(bak_buf)
134
+ except StoreCorruptError as error:
135
+ if log:
136
+ log.error(f"store: backup {bak_path} is corrupt too ({error})")
137
+ else:
138
+ # Self-heal: replace the corrupt file immediately, otherwise the
139
+ # next backup refresh would clobber the only good copy.
140
+ await asyncio.to_thread(_write_store_file_sync, path, bak_buf)
141
+ if log:
142
+ log.warn(f"store: recovered {path} from backup ({len(bak_buf)} bytes)")
143
+ return payload
144
+
145
+ # A corrupt store must never silently become an empty one - that would
146
+ # wipe the plugin's persisted state. Recover from the backup generation or
147
+ # fail the open loudly.
148
+ raise StoreCorruptError(f"{path} unreadable and no usable backup")
149
+
150
+
151
+ def _backup_store_file_sync(path: str) -> None:
152
+ # Copy to a temp sibling first - a crash mid-copy must never leave a
153
+ # truncated .bak, it may be the only recovery generation.
154
+ tmp_path = f"{path}.bak.tmp-{os.getpid()}"
155
+ try:
156
+ shutil.copyfile(path, tmp_path)
157
+ _replace_with_retry(tmp_path, f"{path}.bak")
158
+ except Exception:
159
+ with suppress(OSError):
160
+ os.unlink(tmp_path)
161
+ raise
162
+
163
+
164
+ async def backup_store_file(path: str, log: StoreLogger | None = None) -> None:
165
+ try:
166
+ await asyncio.to_thread(_backup_store_file_sync, path)
167
+ except FileNotFoundError:
168
+ return
169
+ except OSError as error:
170
+ if log:
171
+ log.warn(f"store: backup for {path} failed: {error}")
172
+ return
173
+
174
+
175
+ class _PendingFlush:
176
+ __slots__ = ("future", "snapshot")
177
+
178
+ def __init__(self, snapshot: dict[str, Any], future: asyncio.Future[None]) -> None:
179
+ self.snapshot = snapshot
180
+ self.future = future
181
+
182
+
183
+ # Serializes flushes and collapses saves arriving mid-flush into one trailing
184
+ # flush of the latest snapshot. Snapshots are whole-document states, so the
185
+ # newest one contains every earlier caller's write - a burst of N saves costs
186
+ # at most 2 flushes without weakening the durability guarantee.
187
+ class CoalescingWriter:
188
+ def __init__(
189
+ self,
190
+ flush: Callable[[dict[str, Any]], Awaitable[None]],
191
+ log: StoreLogger | None = None,
192
+ ) -> None:
193
+ self._flush = flush
194
+ self._log = log
195
+ self._in_flight: asyncio.Task[None] | None = None
196
+ self._pending: _PendingFlush | None = None
197
+
198
+ def write(self, snapshot: dict[str, Any]) -> Awaitable[None]:
199
+ if self._in_flight is None:
200
+ self._in_flight = asyncio.ensure_future(self._run(snapshot))
201
+ return self._in_flight
202
+
203
+ if self._pending is not None:
204
+ self._pending.snapshot = snapshot
205
+ return self._pending.future
206
+
207
+ self._pending = _PendingFlush(snapshot, asyncio.get_running_loop().create_future())
208
+ return self._pending.future
209
+
210
+ async def idle(self) -> None:
211
+ while (in_flight := self._in_flight) is not None:
212
+ try:
213
+ await in_flight
214
+ except asyncio.CancelledError:
215
+ # Re-raise only our own cancellation; a cancelled flush task
216
+ # must not abort the drain loop.
217
+ if not in_flight.cancelled():
218
+ raise
219
+ except Exception:
220
+ pass
221
+
222
+ async def _run(self, snapshot: dict[str, Any]) -> None:
223
+ try:
224
+ await self._flush(snapshot)
225
+ finally:
226
+ pending = self._pending
227
+ self._pending = None
228
+
229
+ if pending is not None:
230
+ task = asyncio.ensure_future(self._run(pending.snapshot))
231
+ task.add_done_callback(_settler(pending.future))
232
+ self._in_flight = task
233
+ else:
234
+ self._in_flight = None
235
+
236
+
237
+ def _settler(future: asyncio.Future[None]) -> Callable[[asyncio.Task[None]], None]:
238
+ # Every handed-out awaitable must settle no matter how the trailing flush
239
+ # ends (success, error, or cancellation) - a pending future here would
240
+ # hang its awaiting caller forever.
241
+ def settle(task: asyncio.Task[None]) -> None:
242
+ if future.done():
243
+ return
244
+ if task.cancelled():
245
+ future.cancel()
246
+ elif (error := task.exception()) is not None:
247
+ future.set_exception(error)
248
+ else:
249
+ future.set_result(None)
250
+
251
+ return settle
252
+
253
+
254
+ @dataclass(frozen=True)
255
+ class PluginLocation:
256
+ pass
257
+
258
+
259
+ @dataclass(frozen=True)
260
+ class CameraLocation:
261
+ camera_id: str
262
+
263
+
264
+ @dataclass(frozen=True)
265
+ class SensorLocation:
266
+ camera_id: str
267
+ sensor_type: str
268
+ sensor_name: str
269
+
270
+
271
+ StoreLocation = PluginLocation | CameraLocation | SensorLocation
272
+
273
+
274
+ def read_location(config: dict[str, Any], location: StoreLocation) -> dict[str, Any] | None:
275
+ if isinstance(location, PluginLocation):
276
+ return config.get("plugin")
277
+ if isinstance(location, CameraLocation):
278
+ return (config.get("cameras") or {}).get(location.camera_id)
279
+ by_camera = (config.get("sensors") or {}).get(location.camera_id) or {}
280
+ return (by_camera.get(location.sensor_type) or {}).get(location.sensor_name)
281
+
282
+
283
+ def write_location(config: dict[str, Any], location: StoreLocation, values: dict[str, Any]) -> None:
284
+ if isinstance(location, PluginLocation):
285
+ config["plugin"] = values
286
+ return
287
+ if isinstance(location, CameraLocation):
288
+ config.setdefault("cameras", {})[location.camera_id] = values
289
+ return
290
+ by_camera = config.setdefault("sensors", {}).setdefault(location.camera_id, {})
291
+ by_camera.setdefault(location.sensor_type, {})[location.sensor_name] = values
292
+
293
+
294
+ def delete_location(config: dict[str, Any], location: StoreLocation) -> None:
295
+ if isinstance(location, PluginLocation):
296
+ config.pop("plugin", None)
297
+ return
298
+
299
+ if isinstance(location, CameraLocation):
300
+ cameras = config.get("cameras")
301
+ if isinstance(cameras, dict):
302
+ cameras.pop(location.camera_id, None)
303
+ _prune_if_empty(config, "cameras")
304
+ return
305
+
306
+ sensors = config.get("sensors")
307
+ if not isinstance(sensors, dict):
308
+ return
309
+ by_camera = sensors.get(location.camera_id)
310
+ if not isinstance(by_camera, dict):
311
+ return
312
+ by_type = by_camera.get(location.sensor_type)
313
+ if not isinstance(by_type, dict):
314
+ return
315
+
316
+ by_type.pop(location.sensor_name, None)
317
+ _prune_if_empty(by_camera, location.sensor_type)
318
+ _prune_if_empty(sensors, location.camera_id)
319
+ _prune_if_empty(config, "sensors")
320
+
321
+
322
+ def _prune_if_empty(parent: dict[str, Any], key: str) -> None:
323
+ child = parent.get(key)
324
+ if isinstance(child, dict) and not child:
325
+ del parent[key]
326
+
327
+
328
+ _CANONICAL_SECTIONS = ("plugin", "cameras", "sensors")
329
+ _LEGACY_SENSOR_MARKER = ":sensor:"
330
+
331
+
332
+ def is_canonical_layout(payload: dict[str, Any]) -> bool:
333
+ return all(key in _CANONICAL_SECTIONS for key in payload)
334
+
335
+
336
+ # Go-SDK legacy blobs ('<pluginId>.plugin', ...) are remapped by the Go SDK
337
+ # itself, not here: server and plugin update independently, and a plugin on
338
+ # an old SDK must keep finding its keys untouched.
339
+ def _is_go_legacy_layout(payload: dict[str, Any], plugin_id: str) -> bool:
340
+ return len(payload) > 0 and all(key.startswith(f"{plugin_id}.") for key in payload)
341
+
342
+
343
+ def remap_legacy_layout(
344
+ payload: dict[str, Any], plugin_id: str, log: StoreLogger | None = None
345
+ ) -> dict[str, Any]:
346
+ if is_canonical_layout(payload) or _is_go_legacy_layout(payload, plugin_id):
347
+ return payload
348
+
349
+ canonical: dict[str, Any] = {
350
+ section: payload[section] for section in _CANONICAL_SECTIONS if section in payload
351
+ }
352
+
353
+ for key, values in payload.items():
354
+ if key in _CANONICAL_SECTIONS:
355
+ continue
356
+
357
+ if key == "storage":
358
+ # In a mixed legacy+canonical document the canonical section is the
359
+ # newer write - the legacy duplicate is stale and must never win.
360
+ if "plugin" in canonical:
361
+ if log:
362
+ log.warn(
363
+ f"store: legacy key 'storage' dropped — canonical 'plugin' already present ({plugin_id})"
364
+ )
365
+ continue
366
+ canonical["plugin"] = values
367
+ continue
368
+
369
+ sensor_idx = key.find(_LEGACY_SENSOR_MARKER)
370
+ if sensor_idx != -1:
371
+ # <camId>:sensor:<type>:<pluginId>:<name> - type and pluginId contain
372
+ # no colons; the name keeps everything after them verbatim.
373
+ camera_id = key[:sensor_idx]
374
+ rest = key[sensor_idx + len(_LEGACY_SENSOR_MARKER) :]
375
+ type_end = rest.find(":")
376
+ plugin_end = rest.find(":", type_end + 1)
377
+ if type_end != -1 and plugin_end != -1:
378
+ sensor_type = rest[:type_end]
379
+ sensor_name = rest[plugin_end + 1 :]
380
+ by_camera = canonical.setdefault("sensors", {}).setdefault(camera_id, {})
381
+ if sensor_name in by_camera.get(sensor_type, {}):
382
+ if log:
383
+ log.warn(
384
+ f"store: legacy sensor key '{key}' dropped — canonical target already present ({plugin_id})"
385
+ )
386
+ continue
387
+ by_camera.setdefault(sensor_type, {})[sensor_name] = values
388
+ continue
389
+ if log:
390
+ log.warn(f"store: unparsable legacy sensor key '{key}' kept under cameras ({plugin_id})")
391
+
392
+ if key in (canonical.get("cameras") or {}):
393
+ if log:
394
+ log.warn(
395
+ f"store: legacy key '{key}' dropped — canonical 'cameras.{key}' already present ({plugin_id})"
396
+ )
397
+ continue
398
+ canonical.setdefault("cameras", {})[key] = values
399
+
400
+ return canonical
401
+
402
+
403
+ # Values must round-trip identically through all three runtimes' msgpack
404
+ # codecs - anything outside the shared domain is refused up front.
405
+ def validate_store_value(key: str, value: Any) -> None:
406
+ _walk_value(value, key, 0)
407
+
408
+
409
+ _MAX_VALUE_DEPTH = 64
410
+
411
+
412
+ def _walk_value(value: Any, path: str, depth: int) -> None:
413
+ # Depth cap: rejects circular references (and absurd nesting) with a clear
414
+ # error instead of a RecursionError.
415
+ if depth > _MAX_VALUE_DEPTH:
416
+ raise ValueError(
417
+ f"store: value at '{path}' exceeds {_MAX_VALUE_DEPTH} nesting levels — circular reference?"
418
+ )
419
+ # bool is an int subclass, so it must be accepted before the int check.
420
+ if value is None or isinstance(value, bool | str):
421
+ return
422
+
423
+ if isinstance(value, int):
424
+ if abs(value) > _MAX_SAFE_INTEGER:
425
+ raise ValueError(f"store: value at '{path}' exceeds the float64-safe integer range (±2^53)")
426
+ return
427
+
428
+ if isinstance(value, float):
429
+ if math.isnan(value) or math.isinf(value):
430
+ raise ValueError(f"store: value at '{path}' is {value} — NaN/Infinity are not storable")
431
+ return
432
+
433
+ if isinstance(value, bytes | bytearray | memoryview):
434
+ raise ValueError(
435
+ f"store: value at '{path}' is binary — large artifacts belong in files under the plugin storage dir"
436
+ )
437
+
438
+ if isinstance(value, list | tuple):
439
+ for index, entry in enumerate(value):
440
+ _walk_value(entry, f"{path}[{index}]", depth + 1)
441
+ return
442
+
443
+ if isinstance(value, dict):
444
+ for key, entry in value.items():
445
+ if not isinstance(key, str):
446
+ raise ValueError(f"store: map key {key!r} at '{path}' is not a string")
447
+ _walk_value(entry, f"{path}.{key}", depth + 1)
448
+ return
449
+
450
+ raise ValueError(
451
+ f"store: value at '{path}' is a {type(value).__name__} instance — only plain objects are storable"
452
+ )
453
+
454
+
455
+ class PluginStoreFile:
456
+ def __init__(self, volume_dir: str, plugin_id: str, log: StoreLogger | None = None) -> None:
457
+ self._volume_dir = volume_dir
458
+ self._plugin_id = plugin_id
459
+ self._log = log
460
+ self._path = str(Path(volume_dir) / STORE_FILE_NAME)
461
+ self._writer = CoalescingWriter(self._write_snapshot, log)
462
+ self._payload: dict[str, Any] = {}
463
+ self._closed = True
464
+
465
+ @property
466
+ def path(self) -> str:
467
+ return self._path
468
+
469
+ async def open(self) -> None:
470
+ await asyncio.to_thread(os.makedirs, self._volume_dir, exist_ok=True)
471
+ await asyncio.to_thread(self._remove_orphaned_tmp_files)
472
+
473
+ payload = await read_store_file(self._path, self._log)
474
+ if payload is None:
475
+ # Persist the initial envelope so later opens skip the legacy probe.
476
+ payload = {}
477
+ await write_store_file(self._path, payload, self._log)
478
+
479
+ remapped = remap_legacy_layout(payload, self._plugin_id, self._log)
480
+ if remapped is not payload:
481
+ await write_store_file(self._path, remapped, self._log)
482
+
483
+ self._payload = remapped
484
+ await backup_store_file(self._path, self._log)
485
+ self._closed = False
486
+
487
+ def get(self) -> dict[str, Any]:
488
+ return self._payload
489
+
490
+ async def put(self, payload: dict[str, Any]) -> None:
491
+ if self._closed:
492
+ # A silently "successful" no-op here would lose the write during
493
+ # shutdown windows. Fail loudly instead.
494
+ raise RuntimeError(f"store: put on closed store {self._path}")
495
+ self._payload = payload
496
+ await self._writer.write(payload)
497
+
498
+ async def close(self) -> None:
499
+ self._closed = True
500
+ await self._writer.idle()
501
+
502
+ async def _write_snapshot(self, snapshot: dict[str, Any]) -> None:
503
+ await write_store_file(self._path, snapshot, self._log)
504
+
505
+ def _remove_orphaned_tmp_files(self) -> None:
506
+ try:
507
+ names = os.listdir(self._volume_dir)
508
+ except OSError:
509
+ return
510
+ for name in names:
511
+ if name.startswith(STORE_FILE_NAME) and ".tmp-" in name:
512
+ with suppress(OSError):
513
+ os.unlink(os.path.join(self._volume_dir, name))
@@ -0,0 +1,59 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import json
5
+ import time
6
+ from typing import TYPE_CHECKING, Any, Literal, cast
7
+
8
+ from lmdbm import Lmdb
9
+
10
+ from plugins.runtime.python.store import write_store_file
11
+
12
+ if TYPE_CHECKING:
13
+ from pathlib import Path
14
+
15
+ from plugins.runtime.python.store import StoreLogger
16
+
17
+
18
+ class JsonLmdb(Lmdb[Literal["config"], dict[str, Any]]):
19
+ def _pre_key(self, key: Literal["config"]) -> bytes:
20
+ return key.encode("utf-8")
21
+
22
+ def _post_key(self, key: bytes) -> Literal["config"]:
23
+ _key: Any = key.decode("utf-8")
24
+ return _key
25
+
26
+ def _pre_value(self, value: dict[str, Any]) -> bytes:
27
+ return json.dumps(value).encode("utf-8")
28
+
29
+ def _post_value(self, value: bytes) -> dict[str, Any]:
30
+ return json.loads(value.decode("utf-8"))
31
+
32
+
33
+ def needs_lmdbm_migration(volume_dir: Path, store_path: Path) -> bool:
34
+ return not store_path.exists() and (volume_dir / "data.mdb").exists()
35
+
36
+
37
+ async def migrate_legacy_lmdbm(volume_dir: Path, store_path: Path, log: StoreLogger) -> None:
38
+ start = time.perf_counter()
39
+
40
+ def read_legacy_config() -> dict[str, Any] | None:
41
+ legacy_db = cast(
42
+ "Lmdb[Literal['config'], dict[str, Any]]",
43
+ JsonLmdb.open(file=str(volume_dir), flag="c"),
44
+ )
45
+ try:
46
+ return cast("dict[str, Any] | None", legacy_db.get("config"))
47
+ finally:
48
+ legacy_db.close()
49
+
50
+ config = await asyncio.to_thread(read_legacy_config)
51
+
52
+ if not config:
53
+ return
54
+
55
+ await write_store_file(str(store_path), config, log)
56
+ log.warn(
57
+ f"store: migrated legacy lmdbm env {volume_dir} -> {store_path} "
58
+ f"({len(config)} top-level keys) in {(time.perf_counter() - start) * 1000:.1f}ms"
59
+ )
@@ -0,0 +1,7 @@
1
+ export declare class CoalescingWriter {
2
+ #private;
3
+ private readonly flush;
4
+ constructor(flush: (snapshot: Record<string, any>) => Promise<void>);
5
+ write(snapshot: Record<string, any>): Promise<void>;
6
+ idle(): Promise<void>;
7
+ }
@@ -0,0 +1,52 @@
1
+ // Serializes flushes and collapses saves arriving mid-flush into one trailing
2
+ // flush of the latest snapshot. Snapshots are whole-document states, so the
3
+ // newest one contains every earlier caller's write — a burst of N saves costs
4
+ // at most 2 flushes without weakening the durability guarantee.
5
+ export class CoalescingWriter {
6
+ flush;
7
+ #inFlight;
8
+ #pending;
9
+ constructor(flush) {
10
+ this.flush = flush;
11
+ }
12
+ write(snapshot) {
13
+ if (!this.#inFlight) {
14
+ this.#inFlight = this.#run(snapshot);
15
+ return this.#inFlight;
16
+ }
17
+ if (this.#pending) {
18
+ this.#pending.snapshot = snapshot;
19
+ return this.#pending.promise;
20
+ }
21
+ let resolve;
22
+ let reject;
23
+ const promise = new Promise((res, rej) => {
24
+ resolve = res;
25
+ reject = rej;
26
+ });
27
+ this.#pending = { snapshot, promise, resolve, reject };
28
+ return promise;
29
+ }
30
+ async idle() {
31
+ while (this.#inFlight) {
32
+ await this.#inFlight.catch(() => { });
33
+ }
34
+ }
35
+ async #run(snapshot) {
36
+ try {
37
+ await this.flush(snapshot);
38
+ }
39
+ finally {
40
+ const pending = this.#pending;
41
+ this.#pending = undefined;
42
+ if (pending) {
43
+ this.#inFlight = this.#run(pending.snapshot);
44
+ this.#inFlight.then(pending.resolve, pending.reject);
45
+ }
46
+ else {
47
+ this.#inFlight = undefined;
48
+ }
49
+ }
50
+ }
51
+ }
52
+ //# sourceMappingURL=coalescingWriter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"coalescingWriter.js","sourceRoot":"","sources":["../../../../../src/plugins/store/coalescingWriter.ts"],"names":[],"mappings":"AAOA,8EAA8E;AAC9E,4EAA4E;AAC5E,8EAA8E;AAC9E,gEAAgE;AAChE,MAAM,OAAO,gBAAgB;IAIE;IAH7B,SAAS,CAAiB;IAC1B,QAAQ,CAAgB;IAExB,YAA6B,KAAuD;QAAvD,UAAK,GAAL,KAAK,CAAkD;IAAG,CAAC;IAEjF,KAAK,CAAC,QAA6B;QACxC,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,CAAC;YACpB,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;YACrC,OAAO,IAAI,CAAC,SAAS,CAAC;QACxB,CAAC;QAED,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;YAClB,IAAI,CAAC,QAAQ,CAAC,QAAQ,GAAG,QAAQ,CAAC;YAClC,OAAO,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC;QAC/B,CAAC;QAED,IAAI,OAAoB,CAAC;QACzB,IAAI,MAAiC,CAAC;QACtC,MAAM,OAAO,GAAG,IAAI,OAAO,CAAO,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE;YAC7C,OAAO,GAAG,GAAG,CAAC;YACd,MAAM,GAAG,GAAG,CAAC;QACf,CAAC,CAAC,CAAC;QACH,IAAI,CAAC,QAAQ,GAAG,EAAE,QAAQ,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC;QACvD,OAAO,OAAO,CAAC;IACjB,CAAC;IAEM,KAAK,CAAC,IAAI;QACf,OAAO,IAAI,CAAC,SAAS,EAAE,CAAC;YACtB,MAAM,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;QACvC,CAAC;IACH,CAAC;IAED,KAAK,CAAC,IAAI,CAAC,QAA6B;QACtC,IAAI,CAAC;YACH,MAAM,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;QAC7B,CAAC;gBAAS,CAAC;YACT,MAAM,OAAO,GAAG,IAAI,CAAC,QAAQ,CAAC;YAC9B,IAAI,CAAC,QAAQ,GAAG,SAAS,CAAC;YAE1B,IAAI,OAAO,EAAE,CAAC;gBACZ,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;gBAC7C,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,OAAO,CAAC,OAAO,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC;YACvD,CAAC;iBAAM,CAAC;gBACN,IAAI,CAAC,SAAS,GAAG,SAAS,CAAC;YAC7B,CAAC;QACH,CAAC;IACH,CAAC;CACF"}
@@ -0,0 +1,5 @@
1
+ export declare class StoreCorruptError extends Error {
2
+ constructor(message: string);
3
+ }
4
+ export declare function encodeEnvelope(payload: Record<string, any>): Buffer;
5
+ export declare function decodeEnvelope(buf: Buffer): Record<string, any>;
@@ -0,0 +1,40 @@
1
+ import { Packr } from 'msgpackr';
2
+ import { crc32 } from 'node:zlib';
3
+ // Standard msgpack only — records/string bundles are msgpackr-proprietary and
4
+ // unreadable by the Go/Python sides; int64AsType keeps foreign integers in
5
+ // the float64 number domain instead of surfacing as BigInt.
6
+ const packr = new Packr({ useRecords: false, bundleStrings: false, int64AsType: 'number' });
7
+ const MAGIC = Buffer.from('CUI1');
8
+ export class StoreCorruptError extends Error {
9
+ constructor(message) {
10
+ super(`store envelope: ${message}`);
11
+ this.name = 'StoreCorruptError';
12
+ }
13
+ }
14
+ export function encodeEnvelope(payload) {
15
+ const body = packr.pack(payload);
16
+ const crc = Buffer.allocUnsafe(4);
17
+ crc.writeUInt32LE(crc32(body) >>> 0);
18
+ return Buffer.concat([MAGIC, body, crc]);
19
+ }
20
+ export function decodeEnvelope(buf) {
21
+ if (buf.length < MAGIC.length + 4 || !buf.subarray(0, MAGIC.length).equals(MAGIC)) {
22
+ throw new StoreCorruptError('bad magic');
23
+ }
24
+ const body = buf.subarray(MAGIC.length, buf.length - 4);
25
+ if (crc32(body) >>> 0 !== buf.readUInt32LE(buf.length - 4)) {
26
+ throw new StoreCorruptError('crc mismatch');
27
+ }
28
+ let payload;
29
+ try {
30
+ payload = packr.unpack(body);
31
+ }
32
+ catch (error) {
33
+ throw new StoreCorruptError(`payload decode failed: ${error instanceof Error ? error.message : error}`);
34
+ }
35
+ if (payload === null || typeof payload !== 'object' || Array.isArray(payload)) {
36
+ throw new StoreCorruptError('payload is not a map');
37
+ }
38
+ return payload;
39
+ }
40
+ //# sourceMappingURL=envelope.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"envelope.js","sourceRoot":"","sources":["../../../../../src/plugins/store/envelope.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,UAAU,CAAC;AACjC,OAAO,EAAE,KAAK,EAAE,MAAM,WAAW,CAAC;AAElC,8EAA8E;AAC9E,2EAA2E;AAC3E,4DAA4D;AAC5D,MAAM,KAAK,GAAG,IAAI,KAAK,CAAC,EAAE,UAAU,EAAE,KAAK,EAAE,aAAa,EAAE,KAAK,EAAE,WAAW,EAAE,QAAQ,EAAE,CAAC,CAAC;AAE5F,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;AAElC,MAAM,OAAO,iBAAkB,SAAQ,KAAK;IAC1C,YAAY,OAAe;QACzB,KAAK,CAAC,mBAAmB,OAAO,EAAE,CAAC,CAAC;QACpC,IAAI,CAAC,IAAI,GAAG,mBAAmB,CAAC;IAClC,CAAC;CACF;AAED,MAAM,UAAU,cAAc,CAAC,OAA4B;IACzD,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IACjC,MAAM,GAAG,GAAG,MAAM,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC;IAClC,GAAG,CAAC,aAAa,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC;IACrC,OAAO,MAAM,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,IAAI,EAAE,GAAG,CAAC,CAAC,CAAC;AAC3C,CAAC;AAED,MAAM,UAAU,cAAc,CAAC,GAAW;IACxC,IAAI,GAAG,CAAC,MAAM,GAAG,KAAK,CAAC,MAAM,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC;QAClF,MAAM,IAAI,iBAAiB,CAAC,WAAW,CAAC,CAAC;IAC3C,CAAC;IAED,MAAM,IAAI,GAAG,GAAG,CAAC,QAAQ,CAAC,KAAK,CAAC,MAAM,EAAE,GAAG,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;IACxD,IAAI,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,MAAM,GAAG,CAAC,CAAC,EAAE,CAAC;QAC3D,MAAM,IAAI,iBAAiB,CAAC,cAAc,CAAC,CAAC;IAC9C,CAAC;IAED,IAAI,OAAgB,CAAC;IACrB,IAAI,CAAC;QACH,OAAO,GAAG,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;IAC/B,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,IAAI,iBAAiB,CAAC,0BAA0B,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC;IAC1G,CAAC;IAED,IAAI,OAAO,KAAK,IAAI,IAAI,OAAO,OAAO,KAAK,QAAQ,IAAI,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC;QAC9E,MAAM,IAAI,iBAAiB,CAAC,sBAAsB,CAAC,CAAC;IACtD,CAAC;IAED,OAAO,OAA8B,CAAC;AACxC,CAAC"}
@@ -0,0 +1,2 @@
1
+ export declare function isCanonicalLayout(payload: Record<string, any>): boolean;
2
+ export declare function remapLegacyLayout(payload: Record<string, any>, pluginId: string): Record<string, any>;