atagen

matrix-rust-sdk

public
Star0
Watch0

Commit 090dac278176

tarball

atagen <boss@atagen.co>2026-10-25 08:25 UTC

2 files changed+85-8
event-cache: take the limited sync boundary from the store

handle_sync read older_boundary from memory only, so a room shrunk to a trailing gap recorded a limited sync as an ordinary gap without a HistoryGap and the retained and new events rendered as continuous. The non-limited prev_batch drop now also counts stored events as known history.
Modified file: crates/matrix-sdk/src/event_cache/caches/room/mod.rs+61 additions
@@ -2003,6 +2003,67 @@ mod timed_tests {
20032003 }
20042004
20052005 #[async_test]
2006+ async fn test_limited_sync_after_shrink_to_a_gap_keeps_its_discontinuity() {
2007+ let room_id = room_id!("!shrunk-history:saucisse.bzh");
2008+ let client = MockClientBuilder::new(None).build().await;
2009+ let event_cache = client.event_cache();
2010+ event_cache.subscribe().unwrap();
2011+ client.base_client().get_or_create_room(room_id, RoomState::Joined);
2012+ let room = client.get_room(room_id).unwrap();
2013+ let (room_event_cache, _drop_handles) = room.event_cache().await.unwrap();
2014+ let f = EventFactory::new().room(room_id).sender(*ALICE);
2015+ let old_id = event_id!("$shrunk-old");
2016+ let new_id = event_id!("$shrunk-new");
2017+
2018+ room_event_cache
2019+ .handle_joined_room_update(JoinedRoomUpdate {
2020+ timeline: Timeline {
2021+ events: vec![f.text_msg("old").event_id(old_id).into_event()],
2022+ ..Default::default()
2023+ },
2024+ ..Default::default()
2025+ })
2026+ .await
2027+ .unwrap();
2028+ room_event_cache
2029+ .handle_joined_room_update(JoinedRoomUpdate {
2030+ timeline: Timeline {
2031+ limited: true,
2032+ prev_batch: Some("empty".to_owned()),
2033+ events: Vec::new(),
2034+ },
2035+ ..Default::default()
2036+ })
2037+ .await
2038+ .unwrap();
2039+ assert!(room_event_cache.events().await.unwrap().is_empty());
2040+ room_event_cache
2041+ .handle_joined_room_update(JoinedRoomUpdate {
2042+ timeline: Timeline {
2043+ limited: true,
2044+ prev_batch: Some("new".to_owned()),
2045+ events: vec![f.text_msg("new").event_id(new_id).into_event()],
2046+ },
2047+ ..Default::default()
2048+ })
2049+ .await
2050+ .unwrap();
2051+
2052+ let state = room_event_cache.inner.state.write().await.unwrap();
2053+ let candidate = state.history_candidate().await.unwrap();
2054+ let gaps: Vec<_> = candidate
2055+ .chunks()
2056+ .filter_map(|chunk| match chunk.content() {
2057+ ChunkContent::Gap(gap) => gap.history.clone(),
2058+ ChunkContent::Items(_) => None,
2059+ })
2060+ .collect();
2061+ assert_eq!(gaps.len(), 1);
2062+ assert_eq!(gaps[0].older_boundary.as_deref(), Some(old_id));
2063+ assert_eq!(gaps[0].newer_boundary.as_deref(), Some(new_id));
2064+ }
2065+
2066+ #[async_test]
20062067 async fn test_auto_shrink_after_all_subscribers_are_gone() {
20072068 let room_id = room_id!("!galette:saucisse.bzh");
20082069
Modified file: crates/matrix-sdk/src/event_cache/caches/room/state.rs+24 additions−8 deletions
@@ -30,7 +30,8 @@ use matrix_sdk_base::{
3030 store::{EventCacheStoreLock, EventCacheStoreLockGuard, EventCacheStoreLockState},
3131 },
3232 linked_chunk::{
33- ChunkIdentifierGenerator, ChunkMetadata, LinkedChunkId, OwnedLinkedChunkId, Position,
33+ ChunkContent, ChunkIdentifierGenerator, ChunkMetadata, LinkedChunkId, OwnedLinkedChunkId,
34+ Position,
3435 Update, lazy_loader,
3536 },
3637 serde_helpers::{extract_edit_target, extract_thread_root},
@@ -828,6 +829,25 @@ impl<'a> RoomEventCacheStateLockWriteGuard<'a> {
828829 super::history::gap_positions(&self.state.room_linked_chunk)
829830 }
830831
832+ /// Memory can hold no events at all, after a shrink to a trailing gap.
833+ async fn newest_retained_event_id(&self) -> Result<Option<OwnedEventId>, EventCacheError> {
834+ let chunk = &self.state.room_linked_chunk;
835+ if let Some(id) = chunk.revents().find_map(|(_, event)| event.event_id()) {
836+ return Ok(Some(id));
837+ }
838+ let linked_chunk_id = LinkedChunkId::Room(&self.state.room_id);
839+ let mut first = chunk.chunks().next().expect("a linked chunk is never empty").identifier();
840+ while let Some(previous) = self.store.load_previous_chunk(linked_chunk_id, first).await? {
841+ if let ChunkContent::Items(events) = &previous.content
842+ && let Some(id) = events.iter().rev().find_map(Event::event_id)
843+ {
844+ return Ok(Some(id));
845+ }
846+ first = previous.identifier;
847+ }
848+ Ok(None)
849+ }
850+
831851 pub(super) async fn commit_history_candidate(
832852 &mut self,
833853 mut candidate: EventLinkedChunk,
@@ -940,16 +960,12 @@ impl<'a> RoomEventCacheStateLockWriteGuard<'a> {
940960 // previous-batch token would only result in fetching other events we
941961 // knew about. This is slightly incorrect in the presence of
942962 // network splits, but this has shown to be Good Enough™.
943- if !timeline.limited && self.state.room_linked_chunk.events().next().is_some()
944- || all_duplicates
945- {
963+ let newest_retained = self.newest_retained_event_id().await?;
964+ if !timeline.limited && newest_retained.is_some() || all_duplicates {
946965 prev_batch = None;
947966 }
948967
949- let older_boundary = timeline
950- .limited
951- .then(|| self.state.room_linked_chunk.revents().find_map(|(_, event)| event.event_id()))
952- .flatten();
968+ let older_boundary = timeline.limited.then_some(newest_retained).flatten();
953969 let newer_boundary =
954970 timeline.limited.then(|| events.iter().find_map(Event::event_id)).flatten();
955971 if older_boundary.is_some() && events.iter().any(|event| event.event_id() == older_boundary)