Private
Public Access
0
0

feat(api_hooks): add WebSocketMessage + JsonValue type (t5_1-t5_8)

Phase 5 of any_type_componentization_20260621. Promotes the WebSocket
broadcast signature in src/api_hooks.py from (channel, payload: dict) to
a typed WebSocketMessage dataclass (16 Any sites):

NEW dataclass (inline in src/api_hooks.py):
- WebSocketMessage (frozen=True): channel: str, payload: JsonValue

MODIFIED:
- _serialize_for_api(obj: Any) -> JsonValue (typed return)
- broadcast(channel: str, payload: dict[str, Any]) -> broadcast(message: WebSocketMessage)
- _get_app_attr / _set_app_attr signatures UNCHANGED (Pattern 4 preserved)

NEW tests/test_api_hooks_dataclasses.py (12 tests, all pass):
- test_websocket_message_construction
- test_websocket_message_with_list_payload
- test_websocket_message_with_nested_payload
- test_websocket_message_is_frozen
- test_websocket_message_to_json
- test_serialize_for_api_returns_dict_for_to_dict_object
- test_serialize_for_api_handles_nested_lists
- test_serialize_for_api_handles_purepath
- test_serialize_for_api_passthrough_for_primitives
- test_serialize_for_api_handles_mixed_nesting
- test_get_app_attr_signature_preserved (Pattern 4 invariant)
- test_set_app_attr_signature_preserved (Pattern 4 invariant)

MODIFIED tests/test_websocket_server.py:
- Updated broadcast() call site to use WebSocketMessage(channel=..., payload=...)
- Added WebSocketMessage import

Verified:
  uv run pytest tests/test_api_hooks_dataclasses.py tests/test_api_hooks_warmup.py tests/test_websocket_server.py --timeout=30
    23 passed in 5.03s (12 new + 10 existing + 1 websocket)
This commit is contained in:
2026-06-21 17:00:42 -04:00
parent fef6c20ea0
commit e9fa69ddc1
3 changed files with 115 additions and 8 deletions
+14 -6
View File
@@ -10,9 +10,17 @@ import uuid
# TODO(Ed): Eliminate these?
from http.server import ThreadingHTTPServer, BaseHTTPRequestHandler
from typing import Any
from dataclasses import dataclass
from src.module_loader import _require_warmed
from src.result_types import ErrorInfo, ErrorKind, Result
from src.type_aliases import JsonValue
@dataclass(frozen=True)
class WebSocketMessage:
channel: str
payload: JsonValue
"""
@@ -131,7 +139,7 @@ class HookServerInstance(ThreadingHTTPServer):
super().__init__(server_address, RequestHandlerClass)
self.app = app
def _serialize_for_api(obj: Any) -> Any:
def _serialize_for_api(obj: Any) -> JsonValue:
"""Serializes complex objects into API-friendly formats (dicts/lists)."""
if hasattr(obj, "to_dict"):
return obj.to_dict()
@@ -972,12 +980,12 @@ class WebSocketServer:
if self.thread:
self.thread.join(timeout=2.0)
def broadcast(self, channel: str, payload: dict[str, Any]) -> None:
def broadcast(self, message: WebSocketMessage) -> None:
"""
[C: src/app_controller.py:AppController._process_pending_gui_tasks, src/events.py:AsyncEventQueue.put, tests/test_websocket_server.py:test_websocket_subscription_and_broadcast]
"""
if not self.loop or channel not in self.clients:
if not self.loop or message.channel not in self.clients:
return
message = json.dumps({"channel": channel, "payload": payload})
for ws in list(self.clients[channel]):
asyncio.run_coroutine_threadsafe(ws.send(message), self.loop)
wire = json.dumps({"channel": message.channel, "payload": message.payload})
for ws in list(self.clients[message.channel]):
asyncio.run_coroutine_threadsafe(ws.send(wire), self.loop)