@camera.ui/server 0.0.105 → 0.0.107
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/interface/assets/{404-dD7prz4d.js → 404-BAYYYl25.js} +1 -1
- package/dist/interface/assets/{About-4w_Ij32a.js → About-CF96M6HY.js} +1 -1
- package/dist/interface/assets/{Adminpanel-BgxU0Yh6.js → Adminpanel-DOaJyAyb.js} +1 -1
- package/dist/interface/assets/{Automation-CYsN1p2P.js → Automation-CKXlldCc.js} +2 -2
- package/dist/interface/assets/{Automations-CRd6MuM2.js → Automations-Bly4du8g.js} +1 -1
- package/dist/interface/assets/{Camera-DCqFhkAt.js → Camera-B_iqc3Zu.js} +2 -2
- package/dist/interface/assets/{CameraConsole-DfIBG8ZV.js → CameraConsole-DmkR-ATY.js} +1 -1
- package/dist/interface/assets/{Cameras-BK67w1OJ.js → Cameras-CBJqTSnN.js} +1 -1
- package/dist/interface/assets/{Camview-B0go1EMt.js → Camview-CF_Fa0E1.js} +1 -1
- package/dist/interface/assets/{Config-CeEi3tKd.js → Config-B10TESNa.js} +1 -1
- package/dist/interface/assets/{ConfigActionCameraControl-rP-oa_3f.js → ConfigActionCameraControl-BWpRxipc.js} +1 -1
- package/dist/interface/assets/{ConfigActionDelay-BJeC4VAJ.js → ConfigActionDelay-NBOa8ulm.js} +1 -1
- package/dist/interface/assets/{ConfigActionHttp-F8Mpo56C.js → ConfigActionHttp-CpeMles1.js} +1 -1
- package/dist/interface/assets/{ConfigActionImageInput-Drtv5ElC.js → ConfigActionImageInput-He5suR1p.js} +1 -1
- package/dist/interface/assets/{ConfigActionNotification-DR8c22Y1.js → ConfigActionNotification-BrYGUn_B.js} +1 -1
- package/dist/interface/assets/{ConfigActionOutput-CbT2-VnU.js → ConfigActionOutput-Dy32ypu0.js} +1 -1
- package/dist/interface/assets/{ConfigActionPlugin-DE-S6S0y.js → ConfigActionPlugin-DZgDVj2J.js} +1 -1
- package/dist/interface/assets/{ConfigActionSensor-Dw_V9cYY.js → ConfigActionSensor-B9aJwfb2.js} +1 -1
- package/dist/interface/assets/{ConfigActionSnapshot-5xUvGsae.js → ConfigActionSnapshot-Bisnu1Va.js} +1 -1
- package/dist/interface/assets/{ConfigActionVariable-Bub1V_a3.js → ConfigActionVariable-BcQ-pUs-.js} +1 -1
- package/dist/interface/assets/{ConfigConditionIfElse-CAt9ubD2.js → ConfigConditionIfElse-DKyGnLRO.js} +1 -1
- package/dist/interface/assets/{ConfigConditionSensorState-DhP32uo8.js → ConfigConditionSensorState-mKh6F8lJ.js} +1 -1
- package/dist/interface/assets/{ConfigConditionSwitch-DB2r6i-I.js → ConfigConditionSwitch-CSZTkyIE.js} +1 -1
- package/dist/interface/assets/{ConfigConditionTime-zqIQQ-tD.js → ConfigConditionTime-C8_R0ITE.js} +1 -1
- package/dist/interface/assets/{ConfigTriggerDetection-CVW5HXeG.js → ConfigTriggerDetection-a2NV6Ke1.js} +1 -1
- package/dist/interface/assets/{ConfigTriggerGeofence-BsPsPO4j.js → ConfigTriggerGeofence-DnS6ONJC.js} +1 -1
- package/dist/interface/assets/{ConfigTriggerManual-DhafkyQd.js → ConfigTriggerManual-CrKYDCVp.js} +1 -1
- package/dist/interface/assets/{ConfigTriggerSchedule-Bx3qlITv.js → ConfigTriggerSchedule-Dc6k-hJ2.js} +1 -1
- package/dist/interface/assets/{ConfigTriggerSensor-FVgMXGty.js → ConfigTriggerSensor-lmBAbpAF.js} +1 -1
- package/dist/interface/assets/{ConfigTriggerSystem-BJo-wIMS.js → ConfigTriggerSystem-BibBSkft.js} +1 -1
- package/dist/interface/assets/{ConfigTriggerWebhook-BaUrOAZB.js → ConfigTriggerWebhook-CcMI4u82.js} +1 -1
- package/dist/interface/assets/{Console-B23nM1WG.js → Console-CiqXek8C.js} +1 -1
- package/dist/interface/assets/{CuiActionButton-Dywht3_D.js → CuiActionButton-DmV0jhr5.js} +1 -1
- package/dist/interface/assets/{CuiAvatar-CdoyvPP1.js → CuiAvatar-DMZqcLoE.js} +1 -1
- package/dist/interface/assets/{CuiBBoxPlayground-DcAhwW8k.js → CuiBBoxPlayground-BakfNGCT.js} +1 -1
- package/dist/interface/assets/CuiBottomSheet-BJl4lLzC.js +1 -0
- package/dist/interface/assets/{CuiBottomSheet-CvkbklC_.js → CuiBottomSheet-CkJEMXQy.js} +1 -1
- package/dist/interface/assets/{CuiCameraCard-BxWf13Ct.js → CuiCameraCard-jWlEnJAQ.js} +1 -1
- package/dist/interface/assets/{CuiCameraPipCard-CR5a-MkM.js → CuiCameraPipCard-ClOJg9E-.js} +1 -1
- package/dist/interface/assets/{CuiCameraRecordings-BXfAlbHm.js → CuiCameraRecordings-BhM5UivW.js} +1 -1
- package/dist/interface/assets/{CuiCameraShares-CJpSVfdl.js → CuiCameraShares-CktUxrlR.js} +1 -1
- package/dist/interface/assets/{CuiCameraSnapshot-Cg8Vjve3.js → CuiCameraSnapshot-BE1xeM4q.js} +1 -1
- package/dist/interface/assets/{CuiCameraTable-CfJNlsEQ.js → CuiCameraTable-D87v9LhC.js} +1 -1
- package/dist/interface/assets/{CuiDataTable-C9mtC73b.js → CuiDataTable-C5toN4L-.js} +1 -1
- package/dist/interface/assets/{CuiFloatingButton-Bi1jS25U.js → CuiFloatingButton-DvdpuxUr.js} +1 -1
- package/dist/interface/assets/{CuiImage-DJcQfDpa.js → CuiImage-C-DSHyf2.js} +1 -1
- package/dist/interface/assets/{CuiMenu-150v0xB6.js → CuiMenu-CwumqO_L.js} +1 -1
- package/dist/interface/assets/{CuiPluginCard-CHLd5hzA.js → CuiPluginCard-DIqaLKcf.js} +2 -2
- package/dist/interface/assets/{CuiPluginOAuthButton-B7JHhElr.js → CuiPluginOAuthButton-CBTeQEb9.js} +2 -2
- package/dist/interface/assets/{CuiQRCode-ZbDxg42W.js → CuiQRCode-ChJuYXmm.js} +1 -1
- package/dist/interface/assets/{CuiRecordingsGrid-Bl0vRmPG.js → CuiRecordingsGrid-D1mYCCcE.js} +1 -1
- package/dist/interface/assets/{CuiSchema-DvWek91_.js → CuiSchema-BIGweVJy.js} +1 -1
- package/dist/interface/assets/{CuiTopNavbarItem-Dwm5c6so.js → CuiTopNavbarItem-FXY4aBoC.js} +1 -1
- package/dist/interface/assets/{CuiTopbarSlot-DE7fTsSm.js → CuiTopbarSlot-DCeAqmkQ.js} +1 -1
- package/dist/interface/assets/{Faces-6nDCr24_.js → Faces-B6JlzZ5i.js} +1 -1
- package/dist/interface/assets/{FirstSteps-DB7hnFYh.js → FirstSteps-CH1iro2p.js} +1 -1
- package/dist/interface/assets/{Instances-KpdnZAmV.js → Instances-BNNA4Z6H.js} +1 -1
- package/dist/interface/assets/{Logs-Cq2AF2x-.js → Logs-BkF4EFHV.js} +1 -1
- package/dist/interface/assets/{Menu-CeDR7bvT.js → Menu-DYejNhsi.js} +1 -1
- package/dist/interface/assets/{OAuthDeviceFlow-BVOHNDTs.js → OAuthDeviceFlow-BVeSCJiJ.js} +1 -1
- package/dist/interface/assets/{Plugin-BB6k9AtE.js → Plugin-zvdoGW2r.js} +1 -1
- package/dist/interface/assets/{PluginConsole-Cbo8LA6t.js → PluginConsole-CQA2A5Wc.js} +1 -1
- package/dist/interface/assets/{PluginSchema-CCfTSjTD.js → PluginSchema-xof7AYIK.js} +1 -1
- package/dist/interface/assets/{Plugins-DpTVWC5S.js → Plugins-DAwDjr_I.js} +2 -2
- package/dist/interface/assets/{Recordings-B0_4e1tu.js → Recordings-D7UhoAKi.js} +1 -1
- package/dist/interface/assets/{Settings-Wz10iv8b.js → Settings-CnU98cZv.js} +1 -1
- package/dist/interface/assets/{SettingsAccount-BNSg5Wx5.js → SettingsAccount-BVIztMFL.js} +1 -1
- package/dist/interface/assets/SettingsAccount-BmuJLRLH.js +1 -0
- package/dist/interface/assets/{SettingsAppearance-VHR3IVCx.js → SettingsAppearance-CW8mbN9Q.js} +1 -1
- package/dist/interface/assets/SettingsAppearance-CiVRn_RM.js +1 -0
- package/dist/interface/assets/{SettingsBackup-DGhqvHHm.js → SettingsBackup-DDNiOgvA.js} +1 -1
- package/dist/interface/assets/{SettingsNotifications-BHcRDJAp.js → SettingsNotifications-DLMT1o4I.js} +1 -1
- package/dist/interface/assets/{SettingsRecordings-CrMGLPfU.js → SettingsRecordings-DirvHeKB.js} +1 -1
- package/dist/interface/assets/{SettingsRemote-B1m3_4lr.js → SettingsRemote-CKWsHa5R.js} +1 -1
- package/dist/interface/assets/{SettingsSystem-z5r89MJJ.js → SettingsSystem-D9ZC11Mh.js} +2 -2
- package/dist/interface/assets/{SettingsUsers-yReLqEQ1.js → SettingsUsers-pcJeXZKp.js} +1 -1
- package/dist/interface/assets/{Terminal-BAiV8ZVj.js → Terminal-Cu3uC8EZ.js} +1 -1
- package/dist/interface/assets/{VariableInput-Dw0KbkD9.js → VariableInput-Bwu9vChK.js} +1 -1
- package/dist/interface/assets/{VariableSuggestions-D5_Zu_un.js → VariableSuggestions-l2MLjZre.js} +1 -1
- package/dist/interface/assets/{VersionsHandler-BDWjtbp0.js → VersionsHandler-BTxJHwdS.js} +1 -1
- package/dist/interface/assets/{Workers-DbnD_Bpy.js → Workers-BsEDKzmG.js} +1 -1
- package/dist/interface/assets/{api-D6SjwBWd.js → api-C3OjKngj.js} +1 -1
- package/dist/interface/assets/{api-C-NlYhuP.js → api-CFbWrm1k.js} +1 -1
- package/dist/interface/assets/{auth-dyUBxkxf.js → auth-C5IUhkWS.js} +1 -1
- package/dist/interface/assets/{automations-CrRSdJyJ.js → automations-DD8bCSnT.js} +1 -1
- package/dist/interface/assets/{automations-BH3C3DOU.js → automations-DusabINi.js} +1 -1
- package/dist/interface/assets/{backup-C2-2-LD2.js → backup-oG81xIPA.js} +1 -1
- package/dist/interface/assets/{cameras-Co3xGKAG.js → cameras-CmFEgULx.js} +1 -1
- package/dist/interface/assets/{cameras.schema-y2YP50-k.js → cameras.schema-iBaEWm0Y.js} +1 -1
- package/dist/interface/assets/cloudHandoff-BzwN18OZ.js +1 -0
- package/dist/interface/assets/{deviceId-CiXrj9Di.js → deviceId-opaLeE0R.js} +1 -1
- package/dist/interface/assets/{hasPermission-DKTmzAJw.js → hasPermission-ChIspc-L.js} +1 -1
- package/dist/interface/assets/{instances-BgFvZ06K.js → instances-ykXfoA2u.js} +1 -1
- package/dist/interface/assets/{locale-CH0o3x5d.js → locale-BliTvqss.js} +1 -1
- package/dist/interface/assets/{main-DZb_R3L7.js → main-DiUHKjqR.js} +2 -2
- package/dist/interface/assets/{notifications-DEU1TXne.js → notifications-BQyPgfnE.js} +1 -1
- package/dist/interface/assets/{plugins-Bj4_KCp7.js → plugins-BFzZB-8S.js} +1 -1
- package/dist/interface/assets/{round-restart-alt-BMdTt76H.js → round-restart-alt-C4TFl1Rk.js} +1 -1
- package/dist/interface/assets/{router-CIvI3oge.js → router-BzaHrqQ7.js} +2 -2
- package/dist/interface/assets/{routerState-cQknQxNw.js → routerState-UESa7YsN.js} +1 -1
- package/dist/interface/assets/{server-DeMUCrCz.js → server-7nJbrqvt.js} +1 -1
- package/dist/interface/assets/{shares-B5r7Nr5g.js → shares-BokQ1O4D.js} +1 -1
- package/dist/interface/assets/{theme-kssRfqyJ.js → theme-iSxYm9Ao.js} +1 -1
- package/dist/interface/assets/{types-BK05hngw.js → types-CVp3a4-E.js} +1 -1
- package/dist/interface/assets/{ui-B2i8B4yr.js → ui-CVUMjabl.js} +1 -1
- package/dist/interface/assets/{useBiometricCredentials-I52MbNzM.js → useBiometricCredentials-BTSuyr45.js} +1 -1
- package/dist/interface/assets/{useCameraOptions-uXN6hbSO.js → useCameraOptions-CuIFFMce.js} +1 -1
- package/dist/interface/assets/{useCuiCameraDrawer-BuIrXxOt.js → useCuiCameraDrawer-DHbL2uYT.js} +1 -1
- package/dist/interface/assets/{useCuiDialog-C4xS4vrs.js → useCuiDialog-DmnaFxId.js} +1 -1
- package/dist/interface/assets/{useFileDownload-C_Am2BRz.js → useFileDownload-D7uRiFqP.js} +1 -1
- package/dist/interface/assets/{useLogsSocket-PUFDb8MH.js → useLogsSocket-CA2uq1kZ.js} +1 -1
- package/dist/interface/assets/{useSharedComposables-Ca_A3w0h.js → useSharedComposables-Dsn2uiyW.js} +1 -1
- package/dist/interface/assets/{useStreamStatus-CIPBILoa.js → useStreamStatus-Dun63PvC.js} +1 -1
- package/dist/interface/assets/{users.schema-DGYmUjqk.js → users.schema-COvF8XP0.js} +1 -1
- package/dist/interface/assets/{vendor-cameraui-BABuGvhg.js → vendor-cameraui-B1JU4jwS.js} +38 -38
- package/dist/interface/assets/{vendor-core-C6lCucBy.js → vendor-core-BxjZmyZH.js} +1 -1
- package/dist/interface/assets/{vendor-primevue-CsC6U_Xu.js → vendor-primevue-gmNTz6qq.js} +1 -1
- package/dist/interface/assets/{view-grid-DM5fFhOq.js → view-grid-C3peaC6C.js} +1 -1
- package/dist/interface/index.html +40 -40
- package/dist/interface/sw.js +1 -1
- package/dist/server/package.json +2 -2
- package/dist/server/requirements.txt +1 -1
- package/dist/server/src/_camera_ui_tools/camera_ui_rpc/channel.py +29 -23
- package/dist/server/src/_camera_ui_tools/camera_ui_rpc/chunking.py +52 -5
- package/dist/server/src/_camera_ui_tools/camera_ui_rpc/client.py +682 -231
- package/dist/server/src/_camera_ui_tools/camera_ui_rpc/codec.py +277 -18
- package/dist/server/src/_camera_ui_tools/camera_ui_rpc/handler.py +105 -17
- package/dist/server/src/_camera_ui_tools/camera_ui_rpc/service.py +21 -7
- package/dist/server/src/_camera_ui_tools/camera_ui_rpc/types.py +13 -0
- package/dist/server/src/_camera_ui_tools/camera_ui_rpc/utils.py +50 -15
- package/dist/server/src/api/plugins/natsSubFirewall.js +0 -6
- package/dist/server/src/api/plugins/natsSubFirewall.js.map +1 -1
- package/dist/server/src/api/plugins/proxy.plugin.js +3 -0
- package/dist/server/src/api/plugins/proxy.plugin.js.map +1 -1
- package/dist/server/src/rpc/server.js +6 -3
- package/dist/server/src/rpc/server.js.map +1 -1
- package/package.json +2 -2
- package/dist/interface/assets/CuiBottomSheet-CU8irFoy.js +0 -1
- package/dist/interface/assets/SettingsAccount-DlGKfeGf.js +0 -1
- package/dist/interface/assets/SettingsAppearance-N4r7TCgr.js +0 -1
- package/dist/interface/assets/cloudHandoff-C9HXr7Oq.js +0 -1
|
@@ -30,9 +30,11 @@ class Channel:
|
|
|
30
30
|
self._closed: bool = False
|
|
31
31
|
self._initialized: bool = False
|
|
32
32
|
self._unsubscribe: Callable[[], Coroutine[Any, Any, None]] | None = None
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
self.
|
|
33
|
+
# Handler registries map handler -> is_async, so the async-ness is
|
|
34
|
+
# determined once at registration instead of per emitted message.
|
|
35
|
+
self.__handlers: dict[str, dict[Callable[[Any], None | Awaitable[None]], bool]] = {}
|
|
36
|
+
self.__close_handlers: dict[Callable[[], None | Awaitable[None]], bool] = {}
|
|
37
|
+
self.__error_handlers: dict[Callable[[Exception], None | Awaitable[None]], bool] = {}
|
|
36
38
|
self.__subscriptions: list[Callable[[], Coroutine[Any, Any, None]]] = []
|
|
37
39
|
self.__isolated_client: RPCClient | None = None
|
|
38
40
|
|
|
@@ -96,14 +98,15 @@ class Channel:
|
|
|
96
98
|
def on(self, event: Literal["error"], handler: Callable[[Exception], None | Awaitable[None]]) -> None: ...
|
|
97
99
|
def on(self, event: str, handler: Callable[..., None | Awaitable[None]]) -> None:
|
|
98
100
|
"""Listen for events."""
|
|
101
|
+
handler_is_async = is_async_function(handler)
|
|
99
102
|
if event == "close":
|
|
100
|
-
self.__close_handlers
|
|
103
|
+
self.__close_handlers[handler] = handler_is_async
|
|
101
104
|
elif event == "error":
|
|
102
|
-
self.__error_handlers
|
|
105
|
+
self.__error_handlers[handler] = handler_is_async
|
|
103
106
|
else:
|
|
104
107
|
if event not in self.__handlers:
|
|
105
|
-
self.__handlers[event] =
|
|
106
|
-
self.__handlers[event]
|
|
108
|
+
self.__handlers[event] = {}
|
|
109
|
+
self.__handlers[event][handler] = handler_is_async
|
|
107
110
|
|
|
108
111
|
@overload
|
|
109
112
|
def off(self, event: Literal["message"], handler: Callable[[Any], None | Awaitable[None]]) -> None: ...
|
|
@@ -116,12 +119,12 @@ class Channel:
|
|
|
116
119
|
def off(self, event: str, handler: Callable[..., None | Awaitable[None]]) -> None:
|
|
117
120
|
"""Remove event listener."""
|
|
118
121
|
if event == "close":
|
|
119
|
-
self.__close_handlers.
|
|
122
|
+
self.__close_handlers.pop(handler, None)
|
|
120
123
|
elif event == "error":
|
|
121
|
-
self.__error_handlers.
|
|
124
|
+
self.__error_handlers.pop(handler, None)
|
|
122
125
|
else:
|
|
123
126
|
if event in self.__handlers:
|
|
124
|
-
self.__handlers[event].
|
|
127
|
+
self.__handlers[event].pop(handler, None)
|
|
125
128
|
|
|
126
129
|
async def close(self) -> None:
|
|
127
130
|
"""Close the channel gracefully."""
|
|
@@ -142,15 +145,20 @@ class Channel:
|
|
|
142
145
|
await self._cleanup()
|
|
143
146
|
|
|
144
147
|
async def _emit(self, event: str, data: Any = None) -> None:
|
|
145
|
-
"""Emit an event to handlers.
|
|
148
|
+
"""Emit an event to handlers.
|
|
149
|
+
|
|
150
|
+
Sync handlers run inline on the event loop — the thread-pool hop
|
|
151
|
+
costs far more than the short callbacks used here and would break
|
|
152
|
+
per-channel message ordering.
|
|
153
|
+
"""
|
|
146
154
|
if event in self.__handlers:
|
|
147
|
-
|
|
155
|
+
# Snapshot: an inline handler may add/remove listeners re-entrantly.
|
|
156
|
+
for handler, handler_is_async in list(self.__handlers[event].items()):
|
|
148
157
|
try:
|
|
149
|
-
if
|
|
158
|
+
if handler_is_async:
|
|
150
159
|
await handler(data) # type: ignore[misc]
|
|
151
160
|
else:
|
|
152
|
-
|
|
153
|
-
await loop.run_in_executor(self._client.io_pool, handler, data)
|
|
161
|
+
handler(data)
|
|
154
162
|
except Exception as e:
|
|
155
163
|
print(f"Error in channel handler: {e}")
|
|
156
164
|
|
|
@@ -161,25 +169,23 @@ class Channel:
|
|
|
161
169
|
|
|
162
170
|
self._closed = True
|
|
163
171
|
|
|
164
|
-
for handler in self.__close_handlers:
|
|
172
|
+
for handler, handler_is_async in list(self.__close_handlers.items()):
|
|
165
173
|
with contextlib.suppress(Exception):
|
|
166
|
-
if
|
|
174
|
+
if handler_is_async:
|
|
167
175
|
await handler() # type: ignore[misc]
|
|
168
176
|
else:
|
|
169
|
-
|
|
170
|
-
await loop.run_in_executor(self._client.io_pool, handler)
|
|
177
|
+
handler()
|
|
171
178
|
|
|
172
179
|
asyncio.create_task(self._cleanup())
|
|
173
180
|
|
|
174
181
|
async def _handle_error(self, error: Exception) -> None:
|
|
175
182
|
"""Handle channel error."""
|
|
176
|
-
for handler in self.__error_handlers:
|
|
183
|
+
for handler, handler_is_async in list(self.__error_handlers.items()):
|
|
177
184
|
with contextlib.suppress(Exception):
|
|
178
|
-
if
|
|
185
|
+
if handler_is_async:
|
|
179
186
|
await handler(error) # type: ignore[misc]
|
|
180
187
|
else:
|
|
181
|
-
|
|
182
|
-
await loop.run_in_executor(self._client.io_pool, handler, error)
|
|
188
|
+
handler(error)
|
|
183
189
|
|
|
184
190
|
async def _cleanup(self) -> None:
|
|
185
191
|
"""Clean up resources."""
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
"""Optimized chunking system for large message handling."""
|
|
2
2
|
|
|
3
|
+
import time
|
|
3
4
|
from collections.abc import Callable, Generator
|
|
4
5
|
from typing import Any
|
|
5
6
|
|
|
6
|
-
from .codec import
|
|
7
|
+
from .codec import decode_message
|
|
7
8
|
from .types import ChunkData
|
|
8
9
|
|
|
9
10
|
|
|
@@ -152,10 +153,13 @@ class ChunkAssembler:
|
|
|
152
153
|
raise RuntimeError("Cannot get data: Not all chunks have been received.")
|
|
153
154
|
|
|
154
155
|
if self.buffer is not None:
|
|
155
|
-
# Optimal path: The buffer is already fully assembled.
|
|
156
|
-
#
|
|
156
|
+
# Optimal path: The buffer is already fully assembled. Chunked
|
|
157
|
+
# payloads are always full wire messages, so this goes through
|
|
158
|
+
# decode_message: a reassembled CUIB frame yields zero-copy
|
|
159
|
+
# memoryview slices into the assembly buffer (the views keep the
|
|
160
|
+
# bytearray alive after the assembler is pooled/reset).
|
|
157
161
|
data_view = memoryview(self.buffer)
|
|
158
|
-
return
|
|
162
|
+
return decode_message(data_view)
|
|
159
163
|
else:
|
|
160
164
|
# Fallback path: Manually combine chunks from temporary storage.
|
|
161
165
|
# This path involves a final copy operation.
|
|
@@ -167,7 +171,7 @@ class ChunkAssembler:
|
|
|
167
171
|
chunk_len = len(chunk)
|
|
168
172
|
combined[offset : offset + chunk_len] = chunk
|
|
169
173
|
offset += chunk_len
|
|
170
|
-
return
|
|
174
|
+
return decode_message(combined)
|
|
171
175
|
|
|
172
176
|
def get_progress(self) -> dict[str, int | float]:
|
|
173
177
|
"""Get the current progress of the chunk transfer."""
|
|
@@ -180,11 +184,22 @@ class ChunkAssembler:
|
|
|
180
184
|
class ChunkingManager:
|
|
181
185
|
"""Manages multiple concurrent chunk transfers."""
|
|
182
186
|
|
|
187
|
+
# Incomplete transfers hold their full pre-allocated buffer. If a single
|
|
188
|
+
# chunk is lost (sender reconnected mid-transfer), the assembler would
|
|
189
|
+
# otherwise sit in memory until the whole client disconnects — so stale
|
|
190
|
+
# transfers are swept lazily on every start_receiving/process_chunk call.
|
|
191
|
+
_STALE_TRANSFER_TTL: float = 30.0
|
|
192
|
+
# Sweeping on every chunk of a large transfer is pure overhead — one
|
|
193
|
+
# sweep per second is more than enough for a 30s TTL.
|
|
194
|
+
_SWEEP_INTERVAL: float = 1.0
|
|
195
|
+
|
|
183
196
|
def __init__(self) -> None:
|
|
184
197
|
self.assemblers: dict[str, ChunkAssembler] = {}
|
|
185
198
|
self.completed_callbacks: dict[str, Callable[[Any], None]] = {}
|
|
186
199
|
self.error_callbacks: dict[str, Callable[[Exception], None]] = {}
|
|
187
200
|
self._assembler_pool: list[ChunkAssembler] = []
|
|
201
|
+
self._last_activity: dict[str, float] = {}
|
|
202
|
+
self._last_sweep: float = 0.0
|
|
188
203
|
|
|
189
204
|
def _get_assembler(self, transfer_id: str) -> ChunkAssembler:
|
|
190
205
|
"""Get a reusable assembler from a pool or create a new one."""
|
|
@@ -228,14 +243,42 @@ class ChunkingManager:
|
|
|
228
243
|
on_error: Callback to execute if an error occurs.
|
|
229
244
|
total_size: The total size of the final data for pre-allocation.
|
|
230
245
|
"""
|
|
246
|
+
self._sweep_stale()
|
|
231
247
|
assembler = self._get_assembler(transfer_id)
|
|
232
248
|
assembler.set_expected_chunks(total_chunks, total_size, chunk_size)
|
|
233
249
|
self.assemblers[transfer_id] = assembler
|
|
234
250
|
self.completed_callbacks[transfer_id] = on_complete
|
|
235
251
|
self.error_callbacks[transfer_id] = on_error
|
|
252
|
+
self._last_activity[transfer_id] = time.monotonic()
|
|
253
|
+
|
|
254
|
+
def _sweep_stale(self) -> None:
|
|
255
|
+
"""Drop incomplete transfers with no activity for _STALE_TRANSFER_TTL.
|
|
256
|
+
|
|
257
|
+
Rate-limited to at most one sweep per _SWEEP_INTERVAL — this runs on
|
|
258
|
+
every start_receiving/process_chunk call.
|
|
259
|
+
"""
|
|
260
|
+
if not self._last_activity:
|
|
261
|
+
return
|
|
262
|
+
now = time.monotonic()
|
|
263
|
+
if now - self._last_sweep < self._SWEEP_INTERVAL:
|
|
264
|
+
return
|
|
265
|
+
self._last_sweep = now
|
|
266
|
+
cutoff = now - self._STALE_TRANSFER_TTL
|
|
267
|
+
stale = [tid for tid, ts in self._last_activity.items() if ts < cutoff]
|
|
268
|
+
for transfer_id in stale:
|
|
269
|
+
error_callback = self.error_callbacks.pop(transfer_id, None)
|
|
270
|
+
assembler = self.assemblers.pop(transfer_id, None)
|
|
271
|
+
if assembler:
|
|
272
|
+
self._return_assembler(assembler)
|
|
273
|
+
self.completed_callbacks.pop(transfer_id, None)
|
|
274
|
+
self._last_activity.pop(transfer_id, None)
|
|
275
|
+
if error_callback:
|
|
276
|
+
error_callback(RuntimeError("Transfer timed out"))
|
|
236
277
|
|
|
237
278
|
def process_chunk(self, chunk: dict[str, Any]) -> None:
|
|
238
279
|
"""Process an incoming chunk for a specific transfer."""
|
|
280
|
+
self._sweep_stale()
|
|
281
|
+
|
|
239
282
|
transfer_id = chunk.get("id") or chunk.get("transferId")
|
|
240
283
|
if not transfer_id:
|
|
241
284
|
return
|
|
@@ -245,6 +288,8 @@ class ChunkingManager:
|
|
|
245
288
|
# Received a chunk for an unknown or completed transfer.
|
|
246
289
|
return
|
|
247
290
|
|
|
291
|
+
self._last_activity[transfer_id] = time.monotonic()
|
|
292
|
+
|
|
248
293
|
try:
|
|
249
294
|
# Normalize keys in-place to avoid allocating a new dict.
|
|
250
295
|
# Handles minor key differences between JS/Go/Python senders.
|
|
@@ -261,6 +306,7 @@ class ChunkingManager:
|
|
|
261
306
|
# Cleanup and return assembler to the pool
|
|
262
307
|
del self.assemblers[transfer_id]
|
|
263
308
|
self.error_callbacks.pop(transfer_id, None)
|
|
309
|
+
self._last_activity.pop(transfer_id, None)
|
|
264
310
|
self._return_assembler(assembler)
|
|
265
311
|
|
|
266
312
|
if callback:
|
|
@@ -276,6 +322,7 @@ class ChunkingManager:
|
|
|
276
322
|
if assembler:
|
|
277
323
|
self._return_assembler(assembler)
|
|
278
324
|
self.completed_callbacks.pop(transfer_id, None)
|
|
325
|
+
self._last_activity.pop(transfer_id, None)
|
|
279
326
|
|
|
280
327
|
if error_callback:
|
|
281
328
|
error_callback(error)
|