From 0247a03f3f26eb69a5715a8ba8e02fd6f2c26ee6 Mon Sep 17 00:00:00 2001 From: Henry Su Date: Thu, 10 Sep 2026 11:12:59 -0500 Subject: [PATCH] fix(realtime): process every key in a presence diff's leaves _sync_diff broke out of the leaves loop when a key was not present in the local state, so every remaining key in that diff was skipped: those presences stayed in presence.state forever and on_presence_leave never fired for them. Since leaves comes straight off the wire, a server diff listing an untracked key before a tracked one is enough to trigger it. Skip only the untracked key, matching Phoenix's presence.js. Fixes #1630 --- src/realtime/src/realtime/_async/presence.py | 2 +- src/realtime/tests/test_presence.py | 29 ++++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/src/realtime/src/realtime/_async/presence.py b/src/realtime/src/realtime/_async/presence.py index ad3767029..fc36550b2 100644 --- a/src/realtime/src/realtime/_async/presence.py +++ b/src/realtime/src/realtime/_async/presence.py @@ -117,7 +117,7 @@ def _sync_diff( current_presences = self.state.get(key, []) if len(current_presences) == 0: - break + continue presence_refs_to_remove = { presence.get("presence_ref") for presence in left_presences diff --git a/src/realtime/tests/test_presence.py b/src/realtime/tests/test_presence.py index 0d4c587da..c95a4c25b 100644 --- a/src/realtime/tests/test_presence.py +++ b/src/realtime/tests/test_presence.py @@ -273,3 +273,32 @@ async def test_resubscribe_on_presence_callback_addition() -> None: # Verify resubscribe was called assert channel._resubscribe.call_count == 1 + + +def test_sync_diff_processes_every_leave_key() -> None: + """Leaves for a key the client never tracked must not skip the remaining keys.""" + presence = AsyncRealtimePresence() + leave_events: List[Tuple[str, List[Dict], List[Presence]]] = [] + presence.on_leave( + lambda key, current, left: leave_events.append((key, current, left)) + ) + + state: RawPresenceState = { + "bob": {"metas": [{"phx_ref": "ref-bob"}]}, + } + presence._on_state_event(state) + assert list(presence.state) == ["bob"] + + # The server reports both an untracked key and a tracked one leaving. + presence._on_diff_event( + { + "joins": {}, + "leaves": { + "alice": {"metas": [{"phx_ref": "ref-alice"}]}, + "bob": {"metas": [{"phx_ref": "ref-bob"}]}, + }, + } + ) + + assert presence.state == {} + assert [key for key, _, _ in leave_events] == ["bob"]