refactor: optimize WebSocket handler lifecycle and fix extension asset path handling
- Change `_active_handlers` from list of tuples to dict mapping paths to handler instances - Cache handler instances on connect instead of recreating them for each event - Reuse cached instances in `_dispatch` instead of instantiating new ones - Remove redundant handler instantiation in `_on_disconnect` - Add type ignore comments for socketio event decorators - Fix extension asset path construction to use proper directory variable
frdel committed
Mar 17, 2026 at 17:00 UTC
7eb6d5b19af503048b5905ceb71b1d59fd635359
3 files changed
+25
-19
helpers/ws.py
+14
-15
@@ -32,7 +32,7 @@ class _SecurityContext:
32
33
34
_ws_contexts: dict[str, _SecurityContext] = {}
35
-_active_handlers: dict[str, list[tuple[str, type["WsHandler"]]]] = {}
35
+_active_handlers: dict[str, dict[str, "WsHandler"]] = {}
36
_contexts_lock = threading.Lock()
37
38
@@ -156,7 +156,7 @@ def register_ws_namespace(
156
cache.add(CACHE_AREA, path, handler_cls)
157
return handler_cls
158
159
- @socketio_server.on("connect", namespace="/ws")
159
+ @socketio_server.on("connect", namespace="/ws") # type: ignore
160
async def _on_connect(sid, environ, auth):
161
with webapp.request_context(environ):
162
origin_ok, origin_reason = validate_ws_origin(environ)
@@ -192,7 +192,7 @@ def register_ws_namespace(
192
if isinstance(raw, list):
193
handler_paths = [p for p in raw if isinstance(p, str)]
194
195
- activated: list[tuple[str, type[WsHandler]]] = []
195
+ activated: dict[str, WsHandler] = {}
196
for path in handler_paths:
197
try:
198
handler_cls = _resolve_cached(path)
@@ -203,7 +203,7 @@ def register_ws_namespace(
203
continue
204
instance = handler_cls(socketio_server, lock)
205
await instance.on_connect(sid)
206
- activated.append((path, handler_cls))
206
+ activated[path] = instance
207
except Exception as e:
208
PrintStyle.error(f"WS on_connect error ({path}): {format_error(e)}")
209
@@ -212,20 +212,19 @@ def register_ws_namespace(
212
213
return True
214
215
- @socketio_server.on("disconnect", namespace="/ws")
215
+ @socketio_server.on("disconnect", namespace="/ws") # type: ignore
216
async def _on_disconnect(sid):
217
with _contexts_lock:
218
- activated = _active_handlers.pop(sid, [])
218
+ activated = _active_handlers.pop(sid, {})
219
_ws_contexts.pop(sid, None)
220
221
- for path, handler_cls in activated:
221
+ for path, instance in activated.items():
222
try:
223
- instance = handler_cls(socketio_server, lock)
223
await instance.on_disconnect(sid)
224
except Exception as e:
225
PrintStyle.error(f"WS on_disconnect error ({path}): {format_error(e)}")
226
228
- @socketio_server.on("*", namespace="/ws")
227
+ @socketio_server.on("*", namespace="/ws") # type: ignore
228
async def _dispatch(event, sid, data):
229
path = event
230
payload = data if isinstance(data, dict) else {}
@@ -233,20 +232,20 @@ def register_ws_namespace(
232
try:
233
with _contexts_lock:
234
ctx = _ws_contexts.get(sid)
235
+ activated = _active_handlers.get(sid, {})
236
if ctx is None:
237
return {"ok": False, "error": "No security context", "code": 401}
238
239
- handler_cls = _resolve_cached(path)
240
- if handler_cls is None:
241
- return {"ok": False, "error": f"WS endpoint not found: {path}", "code": 404}
239
+ instance = activated.get(path)
240
+ if instance is None:
241
+ return {"ok": False, "error": f"WS endpoint not activated: {path}", "code": 404}
242
243
# Security check
244
- error = _check_security(handler_cls, ctx)
244
+ error = _check_security(type(instance), ctx)
245
if error is not None:
246
return error
247
248
- # Instantiate and process
249
- instance = handler_cls(socketio_server, lock)
248
+ # Use cached instance and process
249
return await instance.process(payload, sid)
250
251
except Exception as e:
run_ui.py
+4
-3
@@ -149,9 +149,10 @@ async def serve_plugin_asset(plugin_name, asset_path):
149
@webapp.route("/extensions/webui/<path:asset_path>", methods=["GET"])
150
@requires_auth
151
async def serve_extension_asset(asset_path):
152
- path = files.get_abs_path("extensions/webui", asset_path)
153
- if not files.is_in_dir(path, "extensions/webui"):
154
- return Response("Access denied", 403)
152
+ exts = files.get_abs_path("extensions/webui")
153
+ path = files.get_abs_path(exts, asset_path)
154
+ if not files.is_in_dir(path, exts):
155
+ return Response(f"Access denied", 403)
156
return send_file(path)
157
158
webui/components/sidebar/top-section/quick-actions.html
+7
-1
@@ -184,6 +184,7 @@
184
.quick-actions-dropdown {
185
position: fixed;
186
z-index: 9999;
187
+ margin-top: 0;
188
}
189
190
/* Unified scrollbar styling for dropdown */
@@ -209,7 +210,12 @@
210
}
211
212
/* Dropdown item icon sizing */
212
- .dropdown-item .material-symbols-outlined {
213
+ .quick-actions-dropdown .dropdown-item,
214
+ .quick-actions-dropdown .dropdown-item .material-symbols-outlined {
215
+ opacity: 1;
216
+ }
217
+
218
+ .quick-actions-dropdown .dropdown-item .material-symbols-outlined {
219
font-size: 18px;
220
}
221