atagen

matrix-rust-sdk

public
Star0
Watch0

Commit 0f2f98298976

tarball

atagen <boss@atagen.co>2026-10-26 05:28 UTC

2 files changed+48-6
event-cache: claim the start of a room only at its creation event

load_more_events_backwards reported StartOfTimeline whenever the store had nothing older and any event was retained. A sync that drops its prev_batch (not limited, or all duplicates) leaves retained events with no gap before them, so a quiet room showed Beginning of history with its history unloaded. It now paginates from the live end until /messages runs out or the creation event is retained, and a known-only page from the end keeps its token instead of ending pagination.
Modified file: crates/matrix-sdk/src/event_cache/caches/room/pagination.rs+15 additions−6 deletions
@@ -199,10 +199,15 @@ impl PaginatedCache for Arc<RoomEventCacheInner> {
199199 // sync for that room, because every room must have *at least* a room creation
200200 // event. Otherwise, we have reached the start of the timeline.
201201
202- if state.room_linked_chunk().events().next().is_some() {
203- // If there's at least one event, this means we've reached the start of the
204- // timeline, since the chunk is fully loaded.
205- trace!("chunk is fully loaded and non-empty: reached_start=true");
202+ // a sync that drops its prev_batch (not limited, or every event a duplicate)
203+ // leaves retained events with no gap before them, so only the creation event
204+ // proves the start. otherwise paginate from the end, which dedups what we hold
205+ let created = state.room_linked_chunk().events().next().is_some_and(|(_, event)| {
206+ event.raw().get_field::<String>("type").ok().flatten().as_deref()
207+ == Some("m.room.create")
208+ });
209+ if created {
210+ trace!("chunk is fully loaded and starts at creation: reached_start=true");
206211 return Ok(LoadMoreEventsBackwardsOutcome::StartOfTimeline);
207212 }
208213
@@ -349,6 +354,7 @@ impl PaginatedCache for Arc<RoomEventCacheInner> {
349354 return Ok(None);
350355 }
351356
357+ let from_end = prev_token.is_none();
352358 // Check that the previous token still exists; otherwise it's a sign that the
353359 // room's timeline has been cleared.
354360 let prev_gap_id = if let Some(token) = prev_token {
@@ -410,8 +416,11 @@ impl PaginatedCache for Arc<RoomEventCacheInner> {
410416 // All new events are duplicated, they can all be ignored.
411417 events.clear();
412418 // The gap can be ditched too, as it won't be useful to backpaginate any
413- // further.
414- new_token = None;
419+ // further. Not from the end though, where a known page only means we hold the
420+ // newest events, so the token still leads somewhere new.
421+ if !from_end {
422+ new_token = None;
423+ }
415424 }
416425
417426 // `/messages` has been called with `dir=b` (backwards), so the events are in
Modified file: crates/matrix-sdk/tests/recent_history.rs+33 additions
@@ -610,3 +610,36 @@ async fn test_recent_history_joins_on_any_retained_event() {
610610 assert_eq!(outcome.continuity, RecentHistoryContinuity::Joined);
611611 assert!(cache.history_gaps().await.unwrap().is_empty());
612612 }
613+
614+#[async_test]
615+async fn test_retained_events_without_a_gap_paginate_from_the_end_until_creation() {
616+ let server = MatrixMockServer::new().await;
617+ let client = server.client_builder().build().await;
618+ client.event_cache().subscribe().unwrap();
619+ let room_id = room_id!("!quiet:example.org");
620+ let room = server.sync_joined_room(&client, room_id).await;
621+ let (cache, _handles) = room.event_cache().await.unwrap();
622+ let f = event_factory(room_id);
623+ server
624+ .sync_room(
625+ &client,
626+ JoinedRoomBuilder::new(room_id)
627+ .add_timeline_event(f.text_msg("quiet").event_id(event_id!("$quiet"))),
628+ )
629+ .await;
630+ server
631+ .mock_room_messages()
632+ .ok(RoomMessagesResponseTemplate::default().end_token("older").events(vec![
633+ f.text_msg("quiet").event_id(event_id!("$quiet")),
634+ f.text_msg("days ago").event_id(event_id!("$days-ago")),
635+ ]))
636+ .mock_once()
637+ .mount()
638+ .await;
639+
640+ let outcome = cache.pagination().run_backwards_once(20).await.unwrap();
641+ assert!(!outcome.reached_start);
642+ let ids: Vec<_> =
643+ cache.events().await.unwrap().iter().filter_map(|event| event.event_id()).collect();
644+ assert_eq!(ids, [event_id!("$days-ago"), event_id!("$quiet")]);
645+}