feat(websocket): add WS foundational module and message type catalog - #96
Merged
yyy-router merged 4 commits intoJul 29, 2026
Merged
Conversation
Add infrastructure/websocket/: connection lifecycle, session.hello handshake, type-based message routing with unified error mapping, and a device-keyed connection manager for server-initiated push (best-effort, no Outbox yet). No business message handlers are implemented; router.register() is left for each feature owner. Add infrastructure/websocket/messages/: Pydantic models for every message type in the architecture doc's WS protocol (session, voice streaming, schedule upsert/list, location report, reminder control, system refs check/delete, schedule confirmed), each validated against the doc's example JSON in tests. Wire /ws into main.py's composition root alongside the existing /api/v1/health route. Extend test_architecture.py to also forbid infrastructure/websocket from importing data/gateway/intelligence, closing the gap the boundary doc calls out in its own §9.3. Issue 1024XEngineer#95
There was a problem hiding this comment.
Found four correctness and resilience issues in the WebSocket foundation. Inline comments cover reconnect cleanup, handshake validation, malformed/non-object frames, and best-effort broadcast behavior.
Verification: reviewed the complete current diff and ran git diff --check successfully. The automated suite was not rerun because uv is unavailable in this review environment.
- Drop build_result_envelope and ConnectionManager.broadcast(): both had zero callers and no documented message in the architecture doc needs a broadcast-to-all-devices pattern. - Reuse ErrorDetail inside build_error_envelope instead of hand-rolling the same error shape twice. - Extract session.invalid_device_id_error() so endpoint.py and session.py stop constructing the identical INVALID_DEVICE_ID error independently. Issue 1024XEngineer#95
session.py had exactly one caller (endpoint.py) and no concrete future second consumer, unlike connection_manager.py and router.py which are each awaiting a near-term second caller (a future workers dispatcher, and main.py registering business handlers). Merge handle_session_hello()/invalid_device_id_error() directly into endpoint.py and fold the matching tests into test_ws_endpoint.py. Issue 1024XEngineer#95
- ConnectionManager.unregister() now requires the connection instance and only clears the entry if it still refers to that connection, so a late-exiting old session can't remove a newer reconnected one. - handle_session_hello() now validates against SessionHello via model_validate() instead of only conditionally checking device_id, so missing device_id/app_version is rejected as the type requires. - Add _receive_message() to catch JSON decode failures and non-object frames (list/null/etc.) and return a unified MALFORMED_MESSAGE error instead of letting the exception crash the session; malformed frames before the handshake still close the connection, malformed frames after it do not. Issue 1024XEngineer#95
yyy-router
approved these changes
Jul 29, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
infrastructure/websocket/: connection lifecycle (session.hello→session.ready/session.error), a type-basedMessageRouterwith unified*.errormapping (unknown type, invalid message, handler exceptions all caught), and a device-keyedConnectionManagerfor server-initiated push (send/broadcast, best-effort — no Outbox yet, see Out of Scope below).infrastructure/websocket/messages/: Pydantic models for every message type in the architecture doc's WS protocol —session,voice(stream start/end/error, parse result),schedule(upsert/list),location(report/ack),reminder(control/ack, system refs check, system schedule/alarm delete, schedule confirmed). Each type is validated in tests against the doc's example JSON, field-by-field./wsintomain.py's composition root, alongside the existing/api/v1/healthroute.tests/test_architecture.pyto also forbidinfrastructure/websocketfrom importingdata/gateway/intelligence— this closes a gap the boundary doc calls out in its own §9.3 ("尚未检查 infrastructure/websocket 对 data/gateway/intelligence 的禁止依赖").Out of scope (intentional, see issue)
schedule.upsert,voice.stream.*, etc.) —router.register()is left for each feature's owner to wire in.scheduleshas no Outbox table yet; push is best-effort for now.device_idis only checked for presence and consistency between the connection query param andsession.hello.Issue
Closes #95
Test plan
ruff check .— cleanmypy(strict) — clean, 24 source filespytest— 49 passed: unit tests forConnectionManager/MessageRouter/handle_session_hello, an end-to-end integration test over a real WebSocket viaTestClient(connect → hello → route → disconnect cleanup), one success+failure test per message type validated against the architecture doc's example JSON, and the new architecture boundary testuvicornserver (not justTestClient) using a real TCP WebSocket client: missingdevice_id→INVALID_DEVICE_ID; full handshake →session.ready→ unregistered type →UNKNOWN_MESSAGE_TYPE; message before hello →SESSION_HELLO_REQUIRED