refactor: extract constants, deduplicate ack pattern

keyboardstaff committed Mar 28, 2026 at 02:51 UTC 4e222e2cf75d8dc7d427f0c7731be0f13d63e677
2 files changed +69 -57
helpers/state_monitor.py
+2 -1
@@ -15,6 +15,7 @@ from helpers.state_snapshot import (
15 build_snapshot_from_request,
16 )
17 from helpers.ws import ConnectionIdentity, ConnectionNotFoundError, _ws_debug_enabled, ws_debug
18 +from helpers.ws_manager import STATE_PUSH_EVENT
19
20 if TYPE_CHECKING: # pragma: no cover - hints only
21 from helpers.ws_manager import WsManager
@@ -292,7 +293,7 @@ class StateMonitor:
293 await manager.emit_to(
294 namespace,
295 sid,
295 - "state_push",
296 + STATE_PUSH_EVENT,
297 payload,
298 handler_id=handler_id,
299 )
helpers/ws_manager.py
+67 -56
@@ -233,6 +233,16 @@ class _HandlerExecution:
233 DIAGNOSTIC_EVENT = "ws_dev_console_event"
234 LIFECYCLE_CONNECT_EVENT = "ws_lifecycle_connect"
235 LIFECYCLE_DISCONNECT_EVENT = "ws_lifecycle_disconnect"
236 +STATE_PUSH_EVENT = "state_push"
237 +SERVER_RESTART_EVENT = "server_restart"
238 +
239 +# Error codes returned by _build_error_result
240 +ERR_NO_HANDLERS = "NO_HANDLERS"
241 +ERR_HANDLER_ERROR = "HANDLER_ERROR"
242 +ERR_INVALID_FILTER = "INVALID_FILTER"
243 +ERR_INVALID_EVENT = "INVALID_EVENT"
244 +ERR_CONNECTION_NOT_FOUND = "CONNECTION_NOT_FOUND"
245 +ERR_TIMEOUT = "TIMEOUT"
246
247
248 class WsManager:
@@ -559,13 +569,12 @@ class WsManager:
569 handler_payload["correlationId"] = correlation_id
570
571 if not handlers:
562 - error = self._build_error_result(
572 + return self._ack_error(
573 handler_id=self._identifier,
564 - code="NO_HANDLERS",
574 + code=ERR_NO_HANDLERS,
575 message="No handlers available after security filtering",
576 correlation_id=correlation_id,
577 )
568 - return {"correlationId": correlation_id, "results": [error]}
578
579 with self.lock:
580 info = self.connections.get((namespace, sid))
@@ -638,7 +647,7 @@ class WsManager:
647 results.append(
648 self._build_error_result(
649 handler_id=handler.identifier,
641 - code="HANDLER_ERROR",
650 + code=ERR_HANDLER_ERROR,
651 message="Internal server error",
652 details=str(value),
653 correlation_id=correlation_id,
@@ -722,7 +731,7 @@ class WsManager:
731 await self.emit_to(
732 namespace,
733 sid,
725 - "server_restart",
734 + SERVER_RESTART_EVENT,
735 {
736 "emittedAt": self._timestamp(),
737 "runtimeId": runtime.get_runtime_id(),
@@ -829,66 +838,55 @@ class WsManager:
838 include_meta_raw, "includeHandlers"
839 )
840 except ValueError as exc:
832 - error = self._build_error_result(
841 + return self._ack_error(
842 handler_id=handler_id or self._identifier,
834 - code="INVALID_FILTER",
843 + code=ERR_INVALID_FILTER,
844 message=str(exc),
845 correlation_id=correlation_id,
846 + ack=ack,
847 )
838 - if ack:
839 - ack({"correlationId": correlation_id, "results": [error]})
840 - return {"correlationId": correlation_id, "results": [error]}
848
849 try:
850 exclude_meta = self._normalize_handler_filter(
851 exclude_meta_raw, "excludeHandlers"
852 )
853 except ValueError as exc:
847 - error = self._build_error_result(
854 + return self._ack_error(
855 handler_id=handler_id or self._identifier,
849 - code="INVALID_FILTER",
856 + code=ERR_INVALID_FILTER,
857 message=str(exc),
858 correlation_id=correlation_id,
859 + ack=ack,
860 )
853 - payload_error = {"correlationId": correlation_id, "results": [error]}
854 - if ack:
855 - ack(payload_error)
856 - return payload_error
861
862 if exclude_meta_raw is not None and not allow_exclude:
859 - error = self._build_error_result(
863 + return self._ack_error(
864 handler_id=handler_id or self._identifier,
861 - code="INVALID_FILTER",
865 + code=ERR_INVALID_FILTER,
866 message="excludeHandlers is not supported for this operation",
867 correlation_id=correlation_id,
868 + ack=ack,
869 )
865 - if ack:
866 - ack({"correlationId": correlation_id, "results": [error]})
867 - return {"correlationId": correlation_id, "results": [error]}
870
871 if include_handlers is not None and include_meta is not None:
872 if include_handlers != include_meta:
871 - error = self._build_error_result(
873 + return self._ack_error(
874 handler_id=handler_id or self._identifier,
873 - code="INVALID_FILTER",
875 + code=ERR_INVALID_FILTER,
876 message="Conflicting includeHandlers filters supplied",
877 correlation_id=correlation_id,
878 + ack=ack,
879 )
877 - if ack:
878 - ack({"correlationId": correlation_id, "results": [error]})
879 - return {"correlationId": correlation_id, "results": [error]}
880
881 if allow_exclude and exclude_handlers is not None and exclude_meta is not None:
882 if exclude_handlers != exclude_meta:
883 - error = self._build_error_result(
883 + return self._ack_error(
884 handler_id=handler_id or self._identifier,
885 - code="INVALID_FILTER",
885 + code=ERR_INVALID_FILTER,
886 message="Conflicting excludeHandlers filters supplied",
887 correlation_id=correlation_id,
888 + ack=ack,
889 )
889 - if ack:
890 - ack({"correlationId": correlation_id, "results": [error]})
891 - return {"correlationId": correlation_id, "results": [error]}
890
891 include = include_handlers or include_meta
892 exclude = exclude_handlers or (exclude_meta if allow_exclude else None)
@@ -896,54 +894,46 @@ class WsManager:
894 try:
895 validate_event_type(event_type)
896 except (TypeError, ValueError) as exc:
899 - error = self._build_error_result(
897 + return self._ack_error(
898 handler_id=handler_id or self._identifier,
901 - code="INVALID_EVENT",
899 + code=ERR_INVALID_EVENT,
900 message=str(exc),
901 correlation_id=correlation_id,
902 + ack=ack,
903 )
905 - if ack:
906 - ack({"correlationId": correlation_id, "results": [error]})
907 - return {"correlationId": correlation_id, "results": [error]}
904
905 registered = self.handlers.get(namespace, [])
906 if not registered:
907 PrintStyle.warning(f"No handlers registered for namespace '{namespace}'")
912 - error = self._build_error_result(
908 + return self._ack_error(
909 handler_id=handler_id or self._identifier,
914 - code="NO_HANDLERS",
910 + code=ERR_NO_HANDLERS,
911 message=f"No handler for namespace '{namespace}'",
912 correlation_id=correlation_id,
913 + ack=ack,
914 )
918 - if ack:
919 - ack({"correlationId": correlation_id, "results": [error]})
920 - return {"correlationId": correlation_id, "results": [error]}
915
916 try:
917 selected_handlers, _ = self._select_handlers(
918 namespace, include=include, exclude=exclude
919 )
920 except ValueError as exc:
927 - error = self._build_error_result(
921 + return self._ack_error(
922 handler_id=handler_id or self._identifier,
929 - code="INVALID_FILTER",
923 + code=ERR_INVALID_FILTER,
924 message=str(exc),
925 correlation_id=correlation_id,
926 + ack=ack,
927 )
933 - if ack:
934 - ack({"correlationId": correlation_id, "results": [error]})
935 - return {"correlationId": correlation_id, "results": [error]}
928
929 if not selected_handlers:
938 - error = self._build_error_result(
930 + return self._ack_error(
931 handler_id=handler_id or self._identifier,
940 - code="NO_HANDLERS",
932 + code=ERR_NO_HANDLERS,
933 message=f"No handler for '{event_type}' after applying filters",
934 correlation_id=correlation_id,
935 + ack=ack,
936 )
944 - if ack:
945 - ack({"correlationId": correlation_id, "results": [error]})
946 - return {"correlationId": correlation_id, "results": [error]}
937
938 with self.lock:
939 info = self.connections.get((namespace, sid))
@@ -1011,7 +1001,7 @@ class WsManager:
1001 "results": [
1002 self._build_error_result(
1003 handler_id=handler_id or self._identifier,
1014 - code="CONNECTION_NOT_FOUND",
1004 + code=ERR_CONNECTION_NOT_FOUND,
1005 message=f"Connection '{sid}' not found in namespace '{namespace}'",
1006 correlation_id=correlation_id,
1007 )
@@ -1040,7 +1030,7 @@ class WsManager:
1030 "results": [
1031 self._build_error_result(
1032 handler_id=handler_id or self._identifier,
1043 - code="TIMEOUT",
1033 + code=ERR_TIMEOUT,
1034 message="Request timeout",
1035 correlation_id=correlation_id,
1036 )
@@ -1073,7 +1063,7 @@ class WsManager:
1063 except ValueError as exc:
1064 error = self._build_error_result(
1065 handler_id=handler_id or self._identifier,
1076 - code="INVALID_FILTER",
1066 + code=ERR_INVALID_FILTER,
1067 message=str(exc),
1068 correlation_id=correlation_id,
1069 )
@@ -1090,7 +1080,7 @@ class WsManager:
1080 elif exclude_meta is not None and exclude_combined != exclude_meta:
1081 error = self._build_error_result(
1082 handler_id=handler_id or self._identifier,
1093 - code="INVALID_FILTER",
1083 + code=ERR_INVALID_FILTER,
1084 message="Conflicting excludeHandlers filters supplied",
1085 correlation_id=correlation_id,
1086 )
@@ -1155,7 +1145,7 @@ class WsManager:
1145 "results": [
1146 self._build_error_result(
1147 handler_id=handler_id or self._identifier,
1158 - code="TIMEOUT",
1148 + code=ERR_TIMEOUT,
1149 message="Request timeout",
1150 correlation_id=correlation_id,
1151 )
@@ -1459,6 +1449,27 @@ class WsManager:
1449 result["durationMs"] = round(duration_ms, 4)
1450 return result
1451
1452 + def _ack_error(
1453 + self,
1454 + *,
1455 + handler_id: str | None = None,
1456 + code: str,
1457 + message: str,
1458 + correlation_id: str | None = None,
1459 + ack: Callable | None = None,
1460 + ) -> dict[str, Any]:
1461 + """Build an error response, optionally invoke the ack callback, and return."""
1462 + error = self._build_error_result(
1463 + handler_id=handler_id,
1464 + code=code,
1465 + message=message,
1466 + correlation_id=correlation_id,
1467 + )
1468 + response = {"correlationId": correlation_id, "results": [error]}
1469 + if ack:
1470 + ack(response)
1471 + return response
1472 +
1473 # Session tracking helpers (single-user defaults)
1474 def get_sids_for_user(self, user: str | None = None) -> list[ConnectionIdentity]:
1475 """Return connection identities for a user; single-user default returns all."""