From fa3ededf3c10649d9be2bc2761d1f047644de5a9 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 5 Jul 2026 00:32:44 +0200 Subject: [PATCH] Trim cached websocket event payload once per event instead of per subscriber (#175569) Co-authored-by: Claude --- .../components/websocket_api/messages.py | 18 ++++++++++-------- .../components/websocket_api/test_messages.py | 14 ++++++++++++++ 2 files changed, 24 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/websocket_api/messages.py b/homeassistant/components/websocket_api/messages.py index 13f856e66674..a6d3b54f4271 100644 --- a/homeassistant/components/websocket_api/messages.py +++ b/homeassistant/components/websocket_api/messages.py @@ -131,7 +131,7 @@ def cached_event_message(message_id_as_bytes: bytes, event: Event) -> bytes: """ return b"".join( ( - _partial_cached_event_message(event)[:-1], + _partial_cached_event_message(event), b',"id":', message_id_as_bytes, b"}", @@ -143,13 +143,14 @@ def cached_event_message(message_id_as_bytes: bytes, event: Event) -> bytes: def _partial_cached_event_message(event: Event) -> bytes: """Cache and serialize the event to json. - The message is constructed without the id which appended - in cached_event_message. + The message is cached without the trailing "}" and without the id, both of + which are appended in cached_event_message. Trimming here means the slice + happens once per event instead of once per subscriber. """ return ( _message_to_json_bytes_or_none({"type": "event", "event": event.json_fragment}) or INVALID_JSON_PARTIAL_MESSAGE - ) + )[:-1] def cached_state_diff_message( @@ -165,7 +166,7 @@ def cached_state_diff_message( """ return b"".join( ( - _partial_cached_state_diff_message(event)[:-1], + _partial_cached_state_diff_message(event), b',"id":', message_id_as_bytes, b"}", @@ -177,15 +178,16 @@ def cached_state_diff_message( def _partial_cached_state_diff_message(event: Event[EventStateChangedData]) -> bytes: """Cache and serialize the event to json. - The message is constructed without the id which - will be appended in cached_state_diff_message + The message is cached without the trailing "}" and without the id, both of + which are appended in cached_state_diff_message. Trimming here means the + slice happens once per event instead of once per subscriber. """ return ( _message_to_json_bytes_or_none( {"type": "event", "event": _state_diff_event(event)} ) or INVALID_JSON_PARTIAL_MESSAGE - ) + )[:-1] def _state_diff_event( diff --git a/tests/components/websocket_api/test_messages.py b/tests/components/websocket_api/test_messages.py index 4632544e8d27..5615faf0fdf5 100644 --- a/tests/components/websocket_api/test_messages.py +++ b/tests/components/websocket_api/test_messages.py @@ -10,6 +10,7 @@ from homeassistant.components.websocket_api.messages import ( ) from homeassistant.const import EVENT_STATE_CHANGED from homeassistant.core import Context, Event, HomeAssistant, State, callback +from homeassistant.util.json import json_loads from tests.common import async_capture_events @@ -52,6 +53,19 @@ async def test_cached_event_message(hass: HomeAssistant) -> None: assert cache_info.currsize == 2 +async def test_cached_event_message_is_valid_json(hass: HomeAssistant) -> None: + """Test the cached event message is valid JSON with the id appended.""" + events = async_capture_events(hass, EVENT_STATE_CHANGED) + hass.states.async_set("light.window", "on") + await hass.async_block_till_done() + + parsed = json_loads(cached_event_message(b"2", events[0])) + + assert parsed["id"] == 2 + assert parsed["type"] == "event" + assert parsed["event"]["event_type"] == EVENT_STATE_CHANGED + + async def test_cached_event_message_with_different_idens(hass: HomeAssistant) -> None: """Test that we cache event messages when the subscrition idens differ."""