atagen

matrix-rust-sdk

public
Star0
Watch0

Commit 18c8e73713d4

tarball

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

2 files changed+69-1
event-cache: write no cursor gap for a joined recent load

A joined recent load still inserted a plain Gap carrying the joining page end token, and upstream back-pagination takes the newest plain gap first, so on a fresh login it paginated through that token and placed older events after the newest ones. The cursor gap now only stands for a Fresh window, so a join leaves no gap chunk behind.
Modified file: crates/matrix-sdk/src/event_cache/caches/room/history.rs+3 additions−1 deletions
@@ -353,7 +353,9 @@ impl RoomEventCache {
353353 visited_cursors: visited,
354354 });
355355 let history = history.filter(|_| fetched || first_arrival.is_none());
356- if history.is_some() || cursor.is_some() && (fetched || first_arrival.is_none()) {
356+ if history.is_some()
357+ || anchor.is_none() && cursor.is_some() && (fetched || first_arrival.is_none())
358+ {
357359 candidate.insert_history_before(
358360 before.as_deref(),
359361 Vec::new(),
Modified file: crates/matrix-sdk/tests/recent_history.rs+66 additions
@@ -6,6 +6,7 @@ use matrix_sdk::{
66 },
77 test_utils::mocks::{MatrixMockServer, RoomMessagesResponseTemplate},
88 };
9+use matrix_sdk_base::linked_chunk::{ChunkContent, LinkedChunkId};
910 use matrix_sdk_test::{JoinedRoomBuilder, async_test, event_factory::EventFactory};
1011 use ruma::{event_id, room_id, user_id};
1112
@@ -510,3 +511,68 @@ async fn test_cancelling_all_recent_history_waiters_allows_a_fresh_retry() {
510511 vec![event_id!("$old"), event_id!("$recent")]
511512 );
512513 }
514+
515+#[async_test]
516+async fn test_joined_recent_history_leaves_no_gap_before_live_pagination() {
517+ let server = MatrixMockServer::new().await;
518+ let client = server.client_builder().build().await;
519+ client.event_cache().subscribe().unwrap();
520+ let room_id = room_id!("!history:example.org");
521+ let f = event_factory(room_id);
522+ let room = server
523+ .sync_room(
524+ &client,
525+ JoinedRoomBuilder::new(room_id)
526+ .add_timeline_event(f.text_msg("one").event_id(event_id!("$e1")))
527+ .add_timeline_event(f.text_msg("two").event_id(event_id!("$e2")))
528+ .set_timeline_limited()
529+ .set_timeline_prev_batch("init"),
530+ )
531+ .await;
532+ let (cache, _handles) = room.event_cache().await.unwrap();
533+ server
534+ .mock_room_messages()
535+ .match_limit(50)
536+ .ok(RoomMessagesResponseTemplate::default().end_token("joined-page-end").events(vec![
537+ f.text_msg("two").event_id(event_id!("$e2")),
538+ f.text_msg("one").event_id(event_id!("$e1")),
539+ ]))
540+ .mock_once()
541+ .mount()
542+ .await;
543+
544+ let outcome = cache.load_recent_history().await.unwrap();
545+ assert_eq!(outcome.continuity, RecentHistoryContinuity::Joined);
546+
547+ let chunks = client
548+ .event_cache_store()
549+ .lock()
550+ .await
551+ .unwrap()
552+ .as_clean()
553+ .unwrap()
554+ .load_all_chunks(LinkedChunkId::Room(room_id))
555+ .await
556+ .unwrap();
557+ let gaps: Vec<_> = chunks
558+ .iter()
559+ .filter_map(|chunk| match &chunk.content {
560+ ChunkContent::Gap(gap) => Some(gap.token.as_str()),
561+ ChunkContent::Items(_) => None,
562+ })
563+ .collect();
564+ assert_eq!(gaps, ["init"]);
565+
566+ server
567+ .mock_room_messages()
568+ .match_from("init")
569+ .ok(RoomMessagesResponseTemplate::default()
570+ .events(vec![f.text_msg("zero").event_id(event_id!("$e0"))]))
571+ .mock_once()
572+ .mount()
573+ .await;
574+ cache.pagination().run_backwards_once(20).await.unwrap();
575+ let ids: Vec<_> =
576+ cache.events().await.unwrap().iter().filter_map(|event| event.event_id()).collect();
577+ assert_eq!(ids, [event_id!("$e0"), event_id!("$e1"), event_id!("$e2")]);
578+}