From 883cd86a08e44bb68d8390b0d377c03cec2effbe Mon Sep 17 00:00:00 2001 From: Andrew Morgan Date: Thu, 3 Jul 2025 12:15:18 +0100 Subject: [PATCH 1/2] Send only latest membership events down (legacy) /sync This commit aims to fix the edge case where a user has multiple membership events for a single room in a single sync period, and ends up receiving both down /sync. This confuses the client, as they don't know which membership event is the latest - only the server does. Thus we exclude "older" membership events from /sync, where ordering is defined by events' "stream_ordering". Now clients will only get one membership event per room per sync - the latest. --- synapse/handlers/sync.py | 31 +++++++++++++++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/synapse/handlers/sync.py b/synapse/handlers/sync.py index a400e63fd56..d6e61ee3af1 100644 --- a/synapse/handlers/sync.py +++ b/synapse/handlers/sync.py @@ -2539,9 +2539,36 @@ async def _get_room_changes_for_incremental_sync( assert since_token - mem_change_events_by_room_id: Dict[str, List[EventBase]] = {} + # Filter out older membership events for the same user in the same room, keeping only + # the most recent one based on stream_ordering. + # + # This prevents situations where a user leaves a room and is re-invited within + # the same incremental sync period, which would otherwise result in both the leave + # and invite appearing in the sync result and confusing clients about the user's + # actual state in the room. + most_recent_events_by_room_and_user: Dict[Tuple[str, str], EventBase] = {} for event in membership_change_events: - mem_change_events_by_room_id.setdefault(event.room_id, []).append(event) + room_and_user_key = (event.room_id, event.state_key) + if room_and_user_key in most_recent_events_by_room_and_user: + existing_event = most_recent_events_by_room_and_user[room_and_user_key] + + assert event.internal_metadata.stream_ordering + assert existing_event.internal_metadata.stream_ordering + + # Replace it if the new event has a higher stream ordering + if ( + event.internal_metadata.stream_ordering + > existing_event.internal_metadata.stream_ordering + ): + most_recent_events_by_room_and_user[room_and_user_key] = event + else: + most_recent_events_by_room_and_user[room_and_user_key] = event + + # Now rebuild mem_change_events_by_room_id using only the most recent membership event + # for each user in each room, ensuring the sync result reflects the latest state + mem_change_events_by_room_id: Dict[str, List[EventBase]] = {} + for (room_id, _), event in most_recent_events_by_room_and_user.items(): + mem_change_events_by_room_id.setdefault(room_id, []).append(event) newly_joined_rooms: List[str] = list( sync_result_builder.forced_newly_joined_room_ids From e5541ab1e8e648bdc0abe2b484e66ac4e133cc54 Mon Sep 17 00:00:00 2001 From: Andrew Morgan Date: Thu, 3 Jul 2025 13:02:10 +0100 Subject: [PATCH 2/2] newsfile --- changelog.d/18648.bugfix | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/18648.bugfix diff --git a/changelog.d/18648.bugfix b/changelog.d/18648.bugfix new file mode 100644 index 00000000000..2522e1357e1 --- /dev/null +++ b/changelog.d/18648.bugfix @@ -0,0 +1 @@ +Only include latest membership events for each user/room combination in a single incremental legacy sync response. \ No newline at end of file