Skip to main content

arminius_lib_sync/syncv4/plan/
apply_state.rs

1// SPDX-FileCopyrightText: 2026 BWI GmbH
2//
3// SPDX-License-Identifier: Apache-2.0
4
5//! Contains logic to apply the per-connection state to an already filtered list of rooms (see
6//! [MatchedRoom]).
7
8mod tests;
9
10use std::collections::HashMap;
11
12use arminius_lib_db_sync::syncv4::{RoomInfo, RoomState};
13use arminius_lib_model::{
14	syncv4::{RoomConfig, StreamUpperBound},
15	types::StreamId,
16};
17use ruma::OwnedRoomId;
18
19use crate::syncv4::plan::MatchedRoom;
20
21/// A room to sync, produced by [`apply_state`].
22///
23/// We differentiate between rooms the user is or has been a member of and the ones they never have.
24#[derive(Debug)]
25pub enum RoomToSync {
26	/// A room to sync that the user is or has been a member of.
27	Joined {
28		/// From which point this room should be synced.
29		from_stream_id: StreamId,
30		/// Configuration to apply when fetching this room.
31		room_config: RoomConfig,
32		/// Whether it is the first time this room is synced or not.
33		initial: bool,
34	},
35	/// Never joined rooms will only have their stripped state fetched.
36	NeverJoined {
37		/// Whether it is the first time this room is synced or not.
38		initial: bool,
39	},
40}
41
42impl RoomToSync {
43	/// Helper to construct a [RoomToSync].
44	///
45	/// If `was_ever_joined` is `true`, this will produce a [RoomToSync::Joined], otherwise a
46	/// [RoomToSync::NeverJoined].
47	#[must_use]
48	pub fn new_conditional(
49		was_ever_joined: bool,
50		room_config: RoomConfig,
51		sync_room_from: StreamId,
52		initial: bool,
53	) -> Self {
54		match was_ever_joined {
55			true => RoomToSync::Joined { from_stream_id: sync_room_from, room_config, initial },
56			false => RoomToSync::NeverJoined { initial },
57		}
58	}
59}
60
61/// Result produced by [`apply_state`]
62#[derive(Debug)]
63pub(super) struct AppliedState {
64	/// The rooms that will be part of the current sync.
65	pub(super) rooms_to_sync: HashMap<OwnedRoomId, RoomToSync>,
66	/// The rooms belonging to the previous position and that should be marked to PREVIOUSLY during
67	/// the current sync.
68	pub(super) stale_rooms: Vec<OwnedRoomId>,
69}
70
71/// Apply the current connection's state (i.e. rooms streams and previously used configs) to the
72/// rooms matched by the request.
73///
74/// This function will also update the state to reflect what is being synced.
75pub(super) fn apply_state(
76	prev_events_stream_id: StreamId,
77	matched_rooms: &HashMap<OwnedRoomId, MatchedRoom>,
78	user_rooms: &HashMap<OwnedRoomId, RoomInfo>,
79	rooms_states: &mut HashMap<OwnedRoomId, RoomState>,
80) -> AppliedState {
81	let rooms_to_sync = mark_rooms_to_sync(prev_events_stream_id, matched_rooms, rooms_states);
82	let stale_rooms =
83		mark_stale_rooms(rooms_states, user_rooms, &rooms_to_sync, prev_events_stream_id);
84
85	AppliedState { rooms_to_sync, stale_rooms }
86}
87
88/// Computes the list of rooms to sync by applying the rooms states to the rooms matched by the
89/// request.
90///
91/// This will also update the `rooms_states` by marking all the rooms to sync as LIVE.
92fn mark_rooms_to_sync(
93	prev_events_stream_id: StreamId,
94	matched_rooms: &HashMap<OwnedRoomId, MatchedRoom>,
95	rooms_states: &mut HashMap<OwnedRoomId, RoomState>,
96) -> HashMap<OwnedRoomId, RoomToSync> {
97	let mut rooms_to_sync = HashMap::new();
98
99	for (room_id, MatchedRoom { room_info, room_config }) in matched_rooms {
100		// Check if we have a state for that room (i.e. if it was synced before within the
101		// current connection)
102		match rooms_states.get(room_id) {
103			// The room has been synced previously
104			Some(prev_room_state) => {
105				if let Some(sync_from) =
106					// If the room was LIVE and there are new events in it, sync it from the
107					// last token's point
108					if prev_room_state.events_stream_upper_bound
109						== StreamUpperBound::Live
110						&& room_info.max_event_stream_id > prev_events_stream_id
111					{
112						Some(prev_events_stream_id)
113					}
114					// If the room was PREVIOUSLY(up_to) and there are new events past
115					// `up_to`, sync it from `up_to`
116					else if let StreamUpperBound::Previously(up_to) =
117						prev_room_state.events_stream_upper_bound
118						&& room_info.max_event_stream_id > up_to
119					{
120						Some(up_to)
121					}
122					// Otherwise, don't sync it
123					else {
124						None
125					} {
126					rooms_to_sync.insert(
127						room_id.clone(),
128						RoomToSync::new_conditional(
129							room_info.was_ever_joined,
130							room_config.clone(),
131							sync_from,
132							false,
133						),
134					);
135				} else {
136					continue;
137				}
138			}
139			// If the room was never synced, perform initial sync of it
140			None => {
141				rooms_to_sync.insert(
142					room_id.clone(),
143					RoomToSync::new_conditional(
144						room_info.was_ever_joined,
145						room_config.clone(),
146						0,
147						true,
148					),
149				);
150			}
151		}
152
153		// If we reach here, we're syncing the current room, so we need to mark its state as
154		// LIVE (overwrites any existing state for it if any).
155		// TODO: Also update the room config we store for it
156		rooms_states.insert(
157			room_id.clone(),
158			RoomState { events_stream_upper_bound: StreamUpperBound::Live },
159		);
160	}
161
162	rooms_to_sync
163}
164
165/// Update and get the list of stale rooms.
166///
167/// A stale room is a room that was previously marked as LIVE, and contains new events in it
168/// that will not be returned as part of the current request. These rooms therefore need to be
169/// marked as `PREVIOUSLY` with the `up_to` set to the last request's events stream ID (because
170/// that is the point up to which those rooms were LIVE).
171pub(super) fn mark_stale_rooms(
172	rooms_states: &mut HashMap<OwnedRoomId, RoomState>,
173	user_rooms: &HashMap<OwnedRoomId, RoomInfo>,
174	rooms_to_sync: &HashMap<OwnedRoomId, RoomToSync>,
175	prev_events_stream_id: StreamId,
176) -> Vec<OwnedRoomId> {
177	rooms_states
178		.iter_mut()
179		.filter(|(room_id, room_state)| {
180			// Only consider rooms that were marked as LIVE,
181			room_state.events_stream_upper_bound == StreamUpperBound::Live
182					// That were not part of the current sync
183					&& !rooms_to_sync.contains_key(*room_id)
184					// And contains new data
185					&& user_rooms.get(*room_id).is_some_and(|room_info| {
186						room_info.max_event_stream_id > prev_events_stream_id
187					})
188		})
189		.map(|(room_id, room_state)| {
190			// Mark the room as PREVIOUSLY(prev_max_event_stream_id)
191			room_state.events_stream_upper_bound =
192				StreamUpperBound::Previously(prev_events_stream_id);
193
194			room_id.clone()
195		})
196		.collect()
197}