atagen

matrix-rust-sdk

public
Star0
Watch0

Commit 4fae3b516f05

tarball

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

13 files changed+437-1010
event-cache: drop RoomEventCache::reseed

macltok moved to fill_history_gap_once and recent loads, so reseed was about 500 lines of fork surface with no caller. The generation fence stays for clear and commit_history_candidate, and the clear tests move to their own target with the history tests seeding through sync.
Modified file: crates/matrix-sdk-ui/tests/integration/timeline/mod.rs−1 deletions
@@ -56,7 +56,6 @@ mod queue;
5656 mod reactions;
5757 mod read_receipts;
5858 mod redecryption;
59-mod reseed;
6059 mod segmented_history;
6160 mod replies;
6261 mod subscribe;
Deleted file: crates/matrix-sdk-ui/tests/integration/timeline/reseed.rs−109 deletions
@@ -1,109 +0,0 @@
1-use std::time::Duration;
2-
3-use assert_matches::assert_matches;
4-use futures_util::StreamExt;
5-use matrix_sdk::test_utils::mocks::MatrixMockServer;
6-use matrix_sdk_test::{ALICE, JoinedRoomBuilder, async_test, event_factory::EventFactory};
7-use matrix_sdk_ui::timeline::{EventSendState, RoomExt};
8-use ruma::{event_id, events::room::message::RoomMessageEventContent, room_id};
9-use tokio::time::timeout;
10-
11-#[async_test]
12-async fn test_timeline_survives_room_cache_reseed() {
13- let server = MatrixMockServer::new().await;
14- let client = server.client_builder().build().await;
15- client.send_queue().set_enabled(false).await;
16-
17- server.mock_room_state_encryption().plain().mount().await;
18-
19- let room_id = room_id!("!a98sd12bjh:example.org");
20- let room = server.sync_joined_room(&client, room_id).await;
21- let timeline = room.timeline().await.unwrap();
22- let (_, mut timeline_stream) = timeline.subscribe().await;
23- let event_factory = EventFactory::new().room(room_id);
24-
25- let old_event_id = event_id!("$before_reseed");
26- server
27- .sync_room(
28- &client,
29- JoinedRoomBuilder::new(room_id).add_timeline_event(
30- event_factory.text_msg("old history").sender(&ALICE).event_id(old_event_id),
31- ),
32- )
33- .await;
34-
35- timeout(Duration::from_secs(5), async {
36- loop {
37- let items = timeline.items().await;
38- if items.iter().any(|item| {
39- item.as_event().is_some_and(|event| event.event_id() == Some(old_event_id))
40- }) {
41- break;
42- }
43- assert!(timeline_stream.next().await.is_some());
44- }
45- })
46- .await
47- .unwrap();
48-
49- timeline.send(RoomMessageEventContent::text_plain("pending local echo").into()).await.unwrap();
50-
51- timeout(Duration::from_secs(5), async {
52- loop {
53- let items = timeline.items().await;
54- if items.iter().any(|item| {
55- item.as_event().is_some_and(|event| {
56- event
57- .content()
58- .as_message()
59- .is_some_and(|message| message.body() == "pending local echo")
60- })
61- }) {
62- break;
63- }
64- assert!(timeline_stream.next().await.is_some());
65- }
66- })
67- .await
68- .unwrap();
69-
70- let new_event_id = event_id!("$after_reseed");
71- let new_event =
72- event_factory.text_msg("recent history").sender(&ALICE).event_id(new_event_id).into_event();
73- let (room_event_cache, _drop_handles) = room.event_cache().await.unwrap();
74- room_event_cache
75- .reseed(vec![new_event], Some("older".into()))
76- .await
77- .unwrap();
78-
79- timeout(Duration::from_secs(5), async {
80- loop {
81- let items = timeline.items().await;
82- if items.iter().any(|item| {
83- item.as_event().is_some_and(|event| event.event_id() == Some(new_event_id))
84- }) {
85- break;
86- }
87- assert!(timeline_stream.next().await.is_some());
88- }
89- })
90- .await
91- .unwrap();
92-
93- let items = timeline.items().await;
94- assert!(!items.iter().any(|item| {
95- item.as_event().is_some_and(|event| event.event_id() == Some(old_event_id))
96- }));
97-
98- let local_echo = items
99- .iter()
100- .filter_map(|item| item.as_event())
101- .find(|event| {
102- event
103- .content()
104- .as_message()
105- .is_some_and(|message| message.body() == "pending local echo")
106- })
107- .unwrap();
108- assert_matches!(local_echo.send_state(), Some(EventSendState::NotSentYet { .. }));
109-}
Modified file: crates/matrix-sdk-ui/tests/integration/timeline/segmented_history.rs+8 additions−7 deletions
@@ -2,7 +2,7 @@ use std::time::Duration;
22
33 use futures_util::StreamExt;
44 use matrix_sdk::test_utils::mocks::{MatrixMockServer, RoomMessagesResponseTemplate};
5-use matrix_sdk_test::{ALICE, async_test, event_factory::EventFactory};
5+use matrix_sdk_test::{ALICE, JoinedRoomBuilder, async_test, event_factory::EventFactory};
66 use matrix_sdk_ui::timeline::{RoomExt, VirtualTimelineItem};
77 use ruma::{event_id, events::room::message::RoomMessageEventContent, room_id};
88 use tokio::time::timeout;
@@ -22,13 +22,14 @@ async fn test_timeline_fills_segmented_history_without_replacing_retained_items(
2222 let old_event_id = event_id!("$old");
2323 let hidden_event_id = event_id!("$hidden");
2424 let new_event_id = event_id!("$new");
25- cache
26- .reseed(
27- vec![event_factory.text_msg("old").event_id(old_event_id).into_event()],
28- Some("old-cursor".into()),
25+ server
26+ .sync_room(
27+ &client,
28+ JoinedRoomBuilder::new(room_id)
29+ .add_timeline_event(event_factory.text_msg("old").event_id(old_event_id))
30+ .set_timeline_prev_batch("old-cursor"),
2931 )
30- .await
31- .unwrap();
32+ .await;
3233 let gap = cache
3334 .append_history_section(
3435 vec![
Modified file: crates/matrix-sdk/Cargo.toml+1 additions−1 deletions
@@ -210,7 +210,7 @@ name = "response_commit"
210210 required-features = ["testing", "e2e-encryption"]
211211
212212 [[test]]
213-name = "history_reseed"
213+name = "room_clear"
214214 required-features = ["testing"]
215215
216216 [[test]]
Modified file: crates/matrix-sdk/src/event_cache/caches/room/mod.rs−35 deletions
@@ -384,41 +384,6 @@ impl RoomEventCache {
384384 Ok(())
385385 }
386386
387- #[doc = include_str!("reseed.md")]
388- pub async fn reseed(&self, events: Vec<Event>, prev_batch: Option<String>) -> Result<()> {
389- let cache = self.clone();
390- matrix_sdk_common::executor::spawn(async move {
391- let mut state = cache.inner.state.write().await?;
392- state.reseed(events, prev_batch.clone()).await?;
393- let events =
394- state.room_linked_chunk().events().map(|(_, event)| event.clone()).collect();
395- let result = async {
396- state.clear_loaded_views().await?;
397- state
398- .post_process_new_events(events, PostProcessingOrigin::Backpagination, None)
399- .await
400- }
401- .await;
402- let items =
403- state.room_linked_chunk().events().map(|(_, event)| event.clone()).collect();
404- let _ = state.room_linked_chunk_mut().updates_as_vector_diffs();
405- cache.inner.update_sender.send(
406- RoomEventCacheUpdate::UpdateTimelineEvents(TimelineVectorDiffs {
407- diffs: vec![eyeball_im::VectorDiff::Reset { values: items }],
408- history_gaps: Some(history::gap_positions(state.room_linked_chunk())),
409- origin: EventsOrigin::Cache,
410- }),
411- Some(RoomEventCacheGenericUpdate { room_id: cache.inner.room_id.clone() }),
412- );
413- if prev_batch.is_some() {
414- cache.inner.pagination_batch_token_notifier.notify_waiters();
415- }
416- result
417- })
418- .await
419- .map_err(|error| EventCacheError::TaskInterrupted(error.to_string()))?
420- }
421-
422387 /// Return a reference to the state.
423388 pub(in super::super) fn state(&self) -> &LockedRoomEventCacheState {
424389 &self.inner.state
Deleted file: crates/matrix-sdk/src/event_cache/caches/room/reseed.md−25 deletions
@@ -1,25 +0,0 @@
1-Replace this room's timeline with independently fetched history and its older
2-pagination cursor. The replacement is persisted in one linked chunk update
3-transaction before a timeline reset is published.
4-
5-Pass events in topological order, oldest first. For a backwards
6-[`Room::messages`](crate::Room::messages) response, reverse its `chunk` and pass
7-its `end` as `prev_batch`. Decryption should happen through the normal room
8-messages API before reseeding. Duplicate event IDs within the supplied window
9-are retained once. `None` means the start of accessible history has been reached,
10-including for an empty window. An empty window with a cursor remains pageable.
11-
12-This replaces cached history and invalidates thread, pinned, and event-focused
13-timelines, including persisted views that have not been loaded. It does
14-not establish continuity with discarded history. Account credentials, encryption
15-state, and pending outgoing messages are not cleared.
16-
17-Once started, the replacement finishes even if the caller drops its future.
18-Failure to persist the replacement leaves the prior room timeline intact.
19-Post-processing can fail after the replacement is installed, in which case the
20-new timeline is still published and the error is returned. Completion means the
21-cache update has been emitted, not that an application mirror has consumed it.
22-
23-Callers must coordinate live sync and requests that fetched this window. Sync
24-updates are serialized against replacement but may append normally afterward.
25-In-flight older pagination is fenced against timeline replacement.
Modified file: crates/matrix-sdk/src/event_cache/caches/room/state.rs−22 deletions
@@ -781,28 +781,6 @@ impl<'a> RoomEventCacheStateLockWriteGuard<'a> {
781781 Ok(diff_updates)
782782 }
783783
784- pub async fn reseed(
785- &mut self,
786- mut events: Vec<Event>,
787- prev_batch: Option<String>,
788- ) -> Result<(), EventCacheError> {
789- let mut event_ids = HashSet::new();
790- events.retain(|event| event.event_id().is_none_or(|id| event_ids.insert(id)));
791- let mut replacement = EventLinkedChunk::new();
792- replacement.reset();
793- replacement.push_live_events(
794- prev_batch.clone().map(|token| Gap { token, history: None }),
795- &events,
796- );
797- let updates = replacement.store_updates().take();
798- self.send_updates_to_store(updates).await?;
799- let _ = replacement.updates_as_vector_diffs();
800- self.state.room_linked_chunk = replacement;
801- self.state.waited_for_initial_prev_token = true;
802- self.reset_pagination_status(prev_batch.is_none());
803- Ok(())
804- }
805-
806784 pub(super) async fn history_candidate(&self) -> Result<EventLinkedChunk, EventCacheError> {
807785 let linked_chunk_id = LinkedChunkId::Room(&self.state.room_id);
808786 let (last, generator) = self.store.load_last_chunk(linked_chunk_id).await?;
Deleted file: crates/matrix-sdk/tests/history_reseed.rs−598 deletions
@@ -1,598 +0,0 @@
1-use std::{
2- sync::{Arc, atomic::Ordering},
3- time::Duration,
4-};
5-
6-use eyeball_im::VectorDiff;
7-use matrix_sdk::{
8- ThreadingSupport,
9- config::StoreConfig,
10- event_cache::{EventFocusThreadMode, PaginationStatus, RoomEventCacheUpdate},
11- room::MessagesOptions,
12- ruma::{event_id, room_id},
13- test_utils::mocks::{
14- MatrixMockServer, RoomContextResponseTemplate, RoomMessagesResponseTemplate,
15- RoomRelationsResponseTemplate,
16- },
17-};
18-use matrix_sdk_base::{event_cache::store::EventCacheStore, linked_chunk::LinkedChunkId};
19-use matrix_sdk_common::cross_process_lock::CrossProcessLockConfig;
20-use matrix_sdk_test::{ALICE, JoinedRoomBuilder, event_factory::EventFactory};
21-
22-#[path = "history_reseed/failing_store.rs"]
23-mod failing_store;
24-
25-#[tokio::test]
26-async fn reseed_publishes_one_reset_and_preserves_the_fetched_cursor() {
27- let server = MatrixMockServer::new().await;
28- let client = server.client_builder().build().await;
29- let room_id = room_id!("!reseed:localhost");
30- client.event_cache().subscribe().unwrap();
31- server.sync_joined_room(&client, room_id).await;
32- let room = client.get_room(room_id).unwrap();
33- let (cache, _handles) = room.event_cache().await.unwrap();
34- let (_, mut updates) = cache.subscribe().await.unwrap();
35- let factory = EventFactory::new().room(room_id).sender(&ALICE);
36- cache
37- .reseed(vec![factory.text_msg("old").into_event()], Some("old-cursor".into()))
38- .await
39- .unwrap();
40- updates.recv().await.unwrap();
41- let recent_id = event_id!("$recent:localhost");
42- let older_id = event_id!("$older:localhost");
43- server
44- .mock_room_messages()
45- .ok(RoomMessagesResponseTemplate::default()
46- .events(vec![
47- factory.text_msg("recent").event_id(recent_id),
48- factory.text_msg("older").event_id(older_id),
49- factory.text_msg("older").event_id(older_id),
50- ])
51- .end_token("fetched-older-cursor"))
52- .mock_once()
53- .mount()
54- .await;
55- let mut history = room.messages(MessagesOptions::backward()).await.unwrap();
56- history.chunk.reverse();
57- cache.reseed(history.chunk, history.end).await.unwrap();
58- let update =
59- tokio::time::timeout(Duration::from_secs(2), updates.recv()).await.unwrap().unwrap();
60- let RoomEventCacheUpdate::UpdateTimelineEvents(update) = update else {
61- panic!("expected reset")
62- };
63- assert_eq!(update.diffs.len(), 1);
64- let VectorDiff::Reset { values } = &update.diffs[0] else { panic!("expected reset") };
65- assert_eq!(values.len(), 2);
66- assert_eq!(values[0].event_id().as_deref(), Some(older_id));
67- assert_eq!(values[1].event_id().as_deref(), Some(recent_id));
68- assert!(updates.is_empty());
69- assert_eq!(cache.events().await.unwrap().len(), 2);
70- server
71- .mock_room_messages()
72- .match_from("fetched-older-cursor")
73- .ok(RoomMessagesResponseTemplate::default())
74- .mock_once()
75- .mount()
76- .await;
77- assert!(cache.pagination().run_backwards_once(10).await.unwrap().reached_start);
78-}
79-
80-#[tokio::test]
81-async fn reseed_empty_history_distinguishes_a_cursor_from_the_start_of_history() {
82- let server = MatrixMockServer::new().await;
83- let client = server.client_builder().build().await;
84- let room_id = room_id!("!reseed:localhost");
85- client.event_cache().subscribe().unwrap();
86- server.sync_joined_room(&client, room_id).await;
87- let (cache, _handles) = client.get_room(room_id).unwrap().event_cache().await.unwrap();
88- cache.reseed(Vec::new(), None).await.unwrap();
89- assert_eq!(
90- cache.pagination().status().get(),
91- PaginationStatus::Idle { hit_timeline_start: true }
92- );
93- assert!(cache.pagination().run_backwards_once(10).await.unwrap().reached_start);
94- cache.reseed(Vec::new(), Some("empty-page-cursor".into())).await.unwrap();
95- assert_eq!(
96- cache.pagination().status().get(),
97- PaginationStatus::Idle { hit_timeline_start: false }
98- );
99- server
100- .mock_room_messages()
101- .match_from("empty-page-cursor")
102- .ok(RoomMessagesResponseTemplate::default())
103- .mock_once()
104- .mount()
105- .await;
106- assert!(cache.pagination().run_backwards_once(10).await.unwrap().reached_start);
107-}
108-
109-#[tokio::test]
110-async fn reseed_fences_inflight_tokenless_pagination() {
111- let server = MatrixMockServer::new().await;
112- let client = server.client_builder().build().await;
113- let room_id = room_id!("!reseed:localhost");
114- client.event_cache().subscribe().unwrap();
115- server.sync_joined_room(&client, room_id).await;
116- let (cache, _handles) = client.get_room(room_id).unwrap().event_cache().await.unwrap();
117- let factory = EventFactory::new().room(room_id).sender(&ALICE);
118- let stale = factory.text_msg("stale request");
119- server
120- .mock_room_messages()
121- .ok(RoomMessagesResponseTemplate::default()
122- .events(vec![stale])
123- .with_delay(Duration::from_millis(300)))
124- .mock_once()
125- .mount()
126- .await;
127- let pagination = tokio::spawn({
128- let cache = cache.clone();
129- async move { cache.pagination().run_backwards_once(10).await }
130- });
131- tokio::time::timeout(Duration::from_secs(8), async {
132- loop {
133- if server
134- .received_requests()
135- .await
136- .unwrap()
137- .iter()
138- .any(|r| r.url.path().ends_with("/messages"))
139- {
140- break;
141- }
142- tokio::task::yield_now().await;
143- }
144- })
145- .await
146- .unwrap();
147- let recent_id = event_id!("$fresh:localhost");
148- cache
149- .reseed(vec![factory.text_msg("fresh").event_id(recent_id).into_event()], None)
150- .await
151- .unwrap();
152- tokio::time::timeout(Duration::from_secs(3), pagination).await.unwrap().unwrap().unwrap();
153- let events = cache.events().await.unwrap();
154- assert_eq!(events.len(), 1);
155- assert_eq!(events[0].event_id().as_deref(), Some(recent_id));
156- assert_eq!(
157- cache.pagination().status().get(),
158- PaginationStatus::Idle { hit_timeline_start: true }
159- );
160-}
161-
162-#[tokio::test]
163-async fn reseed_preserves_timeline_when_store_write_fails() {
164- let server = MatrixMockServer::new().await;
165- let store = Arc::new(failing_store::FailingStore::default());
166- let client = server
167- .client_builder()
168- .on_builder(|builder| {
169- builder.store_config(
170- StoreConfig::new(CrossProcessLockConfig::SingleProcess)
171- .event_cache_store(store.clone()),
172- )
173- })
174- .build()
175- .await;
176- let room_id = room_id!("!reseed:localhost");
177- client.event_cache().subscribe().unwrap();
178- server.sync_joined_room(&client, room_id).await;
179- let (cache, _handles) = client.get_room(room_id).unwrap().event_cache().await.unwrap();
180- let factory = EventFactory::new().room(room_id).sender(&ALICE);
181- let original_id = event_id!("$original:localhost");
182- cache
183- .reseed(
184- vec![factory.text_msg("original").event_id(original_id).into_event()],
185- Some("original-cursor".into()),
186- )
187- .await
188- .unwrap();
189- let (original_events, updates) = cache.subscribe().await.unwrap();
190- assert_eq!(original_events.len(), 1);
191- assert_eq!(original_events[0].event_id().as_deref(), Some(original_id));
192- let original_chunks = store.inner.load_all_chunks(LinkedChunkId::Room(room_id)).await.unwrap();
193-
194- store.fail_writes.store(true, Ordering::SeqCst);
195- let result = cache
196- .reseed(
197- vec![
198- factory
199- .text_msg("replacement")
200- .event_id(event_id!("$replacement:localhost"))
201- .into_event(),
202- ],
203- Some("replacement-cursor".into()),
204- )
205- .await;
206- store.fail_writes.store(false, Ordering::SeqCst);
207-
208- assert!(result.is_err());
209- assert!(updates.is_empty());
210- let current_events = cache.events().await.unwrap();
211- assert_eq!(current_events.len(), 1);
212- assert_eq!(current_events[0].event_id().as_deref(), Some(original_id));
213- let current_chunks = store.inner.load_all_chunks(LinkedChunkId::Room(room_id)).await.unwrap();
214- assert_eq!(format!("{current_chunks:?}"), format!("{original_chunks:?}"));
215-}
216-
217-#[tokio::test]
218-async fn reseed_redaction_does_not_leak_stale_diff_into_next_sync() {
219- let server = MatrixMockServer::new().await;
220- let client = server.client_builder().build().await;
221- let room_id = room_id!("!reseed:localhost");
222- client.event_cache().subscribe().unwrap();
223- server.sync_joined_room(&client, room_id).await;
224- let room = client.get_room(room_id).unwrap();
225- let (cache, _handles) = room.event_cache().await.unwrap();
226- let (_, mut updates) = cache.subscribe().await.unwrap();
227- let factory = EventFactory::new().room(room_id).sender(&ALICE);
228- let redacted_id = event_id!("$redacted:localhost");
229- let redaction_id = event_id!("$redaction:localhost");
230-
231- cache
232- .reseed(
233- vec![
234- factory.text_msg("secret").event_id(redacted_id).into_event(),
235- factory.redaction(&redacted_id).event_id(redaction_id).into_event(),
236- ],
237- None,
238- )
239- .await
240- .unwrap();
241- let reset =
242- tokio::time::timeout(Duration::from_secs(2), updates.recv()).await.unwrap().unwrap();
243- let RoomEventCacheUpdate::UpdateTimelineEvents(reset) = reset else {
244- panic!("expected timeline reset")
245- };
246- let VectorDiff::Reset { values } = &reset.diffs[0] else { panic!("expected reset diff") };
247- assert_eq!(values.len(), 2);
248- assert!(values[0].raw().deserialize().unwrap().is_redacted());
249-
250- let new_event_id = event_id!("$new-live:localhost");
251- server
252- .sync_room(
253- &client,
254- JoinedRoomBuilder::new(room_id).add_timeline_event(
255- factory.text_msg("live").event_id(new_event_id).into_raw_sync(),
256- ),
257- )
258- .await;
259- let live_update =
260- tokio::time::timeout(Duration::from_secs(2), updates.recv()).await.unwrap().unwrap();
261- let RoomEventCacheUpdate::UpdateTimelineEvents(live_update) = live_update else {
262- panic!("expected timeline update")
263- };
264- assert!(live_update.diffs.iter().all(|diff| !matches!(diff, VectorDiff::Set { .. })));
265- assert!(live_update.diffs.iter().all(|diff| match diff {
266- VectorDiff::Append { values } => {
267- values.iter().all(|event| event.event_id().as_deref() == Some(new_event_id))
268- }
269- VectorDiff::PushBack { value } => value.event_id().as_deref() == Some(new_event_id),
270- _ => true,
271- }));
272- assert_eq!(cache.events().await.unwrap().len(), 3);
273-}
274-
275-#[tokio::test]
276-async fn reseed_retries_thread_pagination_without_stale_response() {
277- use wiremock::ResponseTemplate;
278-
279- let server = MatrixMockServer::new().await;
280- let client = server
281- .client_builder()
282- .on_builder(|builder| {
283- builder.with_threading_support(ThreadingSupport::Enabled { with_subscriptions: false })
284- })
285- .build()
286- .await;
287- let room_id = room_id!("!reseed:localhost");
288- client.event_cache().subscribe().unwrap();
289- let thread_root_id = event_id!("$thread-root:localhost");
290- let initial_reply_id = event_id!("$initial-reply:localhost");
291- let factory = EventFactory::new().room(room_id).sender(&ALICE);
292- let room = server
293- .sync_room(
294- &client,
295- JoinedRoomBuilder::new(room_id)
296- .set_timeline_prev_batch("initial-thread-cursor")
297- .add_timeline_event(
298- factory
299- .text_msg("initial reply")
300- .in_thread(thread_root_id, thread_root_id)
301- .event_id(initial_reply_id),
302- ),
303- )
304- .await;
305- let (cache, _handles) = room.event_cache().await.unwrap();
306- let (initial_events, _thread_updates) =
307- cache.subscribe_to_thread(thread_root_id.to_owned()).await.unwrap();
308- assert_eq!(initial_events.len(), 1);
309- assert_eq!(initial_events[0].event_id().as_deref(), Some(initial_reply_id));
310-
311- let stale_reply_id = event_id!("$stale-reply:localhost");
312- let fresh_reply_id = event_id!("$fresh-reply:localhost");
313- server
314- .mock_room_relations()
315- .match_target_event(thread_root_id.to_owned())
316- .ok(RoomRelationsResponseTemplate::default().events(vec![
317- factory
318- .text_msg("stale reply")
319- .in_thread(thread_root_id, thread_root_id)
320- .event_id(stale_reply_id),
321- ]))
322- .mock_once()
323- .mount()
324- .await;
325- let root_event = factory.text_msg("thread root").event_id(thread_root_id).into_event();
326- server
327- .mock_room_event()
328- .room(room_id)
329- .ok_with_template(
330- ResponseTemplate::new(200)
331- .set_body_json(root_event.into_raw().json())
332- .set_delay(Duration::from_millis(300)),
333- )
334- .mock_once()
335- .mount()
336- .await;
337-
338- let pagination = tokio::spawn({
339- let cache = cache.clone();
340- let thread_root_id = thread_root_id.to_owned();
341- async move {
342- cache.thread_pagination(thread_root_id).await.unwrap().run_backwards_once(10).await
343- }
344- });
345- tokio::time::timeout(Duration::from_secs(8), async {
346- loop {
347- if server
348- .received_requests()
349- .await
350- .unwrap()
351- .iter()
352- .any(|request| request.url.path().contains("/event/"))
353- {
354- break;
355- }
356- tokio::task::yield_now().await;
357- }
358- })
359- .await
360- .unwrap();
361- server
362- .mock_room_relations()
363- .match_target_event(thread_root_id.to_owned())
364- .ok(RoomRelationsResponseTemplate::default()
365- .events(vec![
366- factory
367- .text_msg("fresh reply")
368- .in_thread(thread_root_id, thread_root_id)
369- .event_id(fresh_reply_id),
370- ])
371- .next_batch("fresh-cursor"))
372- .mock_once()
373- .mount()
374- .await;
375- cache.reseed(Vec::new(), None).await.unwrap();
376-
377- let outcome =
378- tokio::time::timeout(Duration::from_secs(8), pagination).await.unwrap().unwrap().unwrap();
379- assert!(outcome.events.iter().all(|event| event.event_id().as_deref() != Some(stale_reply_id)));
380- assert!(outcome.events.iter().any(|event| event.event_id().as_deref() == Some(fresh_reply_id)));
381- let (thread_events, _thread_updates) =
382- cache.subscribe_to_thread(thread_root_id.to_owned()).await.unwrap();
383- assert!(thread_events.iter().all(|event| event.event_id().as_deref() != Some(stale_reply_id)));
384- assert!(thread_events.iter().any(|event| event.event_id().as_deref() == Some(fresh_reply_id)));
385-}
386-
387-#[tokio::test]
388-async fn clear_invalidates_focused_cache_clones_and_inflight_pagination() {
389- let server = MatrixMockServer::new().await;
390- let client = server.client_builder().build().await;
391- let room_id = room_id!("!reseed:localhost");
392- client.event_cache().subscribe().unwrap();
393- server.sync_joined_room(&client, room_id).await;
394- let room = client.get_room(room_id).unwrap();
395- let (cache, _handles) = room.event_cache().await.unwrap();
396- let factory = EventFactory::new().room(room_id).sender(&ALICE);
397- let focused_id = event_id!("$focused:localhost");
398- server
399- .mock_room_event_context()
400- .ok(RoomContextResponseTemplate::new(
401- factory.text_msg("focused").event_id(focused_id).into_event(),
402- )
403- .start("old-focused-cursor"))
404- .mock_once()
405- .mount()
406- .await;
407- let focused = cache
408- .get_or_create_event_focused_cache(
409- focused_id.to_owned(),
410- 10,
411- EventFocusThreadMode::Automatic,
412- )
413- .await
414- .unwrap();
415- let focused_clone = focused.clone();
416- let (initial_events, mut updates) = focused.subscribe().await;
417- assert_eq!(initial_events.len(), 1);
418- assert_eq!(initial_events[0].event_id().as_deref(), Some(focused_id));
419-
420- let stale_event_id = event_id!("$stale-focused:localhost");
421- server
422- .mock_room_messages()
423- .match_from("old-focused-cursor")
424- .ok(RoomMessagesResponseTemplate::default()
425- .events(vec![factory.text_msg("stale").event_id(stale_event_id)])
426- .end_token("stale-next-cursor")
427- .with_delay(Duration::from_millis(300)))
428- .mock_once()
429- .mount()
430- .await;
431- let pagination = tokio::spawn({
432- let focused = focused.clone();
433- async move { focused.paginate_backwards(10).await }
434- });
435- tokio::time::timeout(Duration::from_secs(8), async {
436- loop {
437- if server
438- .received_requests()
439- .await
440- .unwrap()
441- .iter()
442- .any(|request| request.url.path().ends_with("/messages"))
443- {
444- break;
445- }
446- tokio::task::yield_now().await;
447- }
448- })
449- .await
450- .unwrap();
451-
452- tokio::time::timeout(Duration::from_secs(2), cache.clear()).await.unwrap().unwrap();
453- let clear =
454- tokio::time::timeout(Duration::from_secs(2), updates.recv()).await.unwrap().unwrap();
455- assert!(matches!(clear.diffs.as_slice(), [VectorDiff::Clear]));
456- tokio::time::timeout(Duration::from_secs(3), pagination).await.unwrap().unwrap().unwrap();
457- let (cloned_events, _cloned_updates) = focused_clone.subscribe().await;
458- assert!(cloned_events.is_empty());
459- let result = focused_clone.paginate_backwards(10).await.unwrap();
460- assert!(result.hit_end_of_timeline);
461- let (events_after_pagination, _updates_after_pagination) = focused_clone.subscribe().await;
462- assert!(events_after_pagination.is_empty());
463-}
464-
465-#[tokio::test]
466-async fn clear_empties_subscribed_thread_cache() {
467- let server = MatrixMockServer::new().await;
468- let client = server
469- .client_builder()
470- .on_builder(|builder| {
471- builder.with_threading_support(ThreadingSupport::Enabled { with_subscriptions: false })
472- })
473- .build()
474- .await;
475- let room_id = room_id!("!reseed:localhost");
476- client.event_cache().subscribe().unwrap();
477- let thread_root_id = event_id!("$thread-root:localhost");
478- let thread_reply_id = event_id!("$thread-reply:localhost");
479- let factory = EventFactory::new().room(room_id).sender(&ALICE);
480- let room = server
481- .sync_room(
482- &client,
483- JoinedRoomBuilder::new(room_id).add_timeline_event(
484- factory
485- .text_msg("thread reply")
486- .in_thread(thread_root_id, thread_root_id)
487- .event_id(thread_reply_id),
488- ),
489- )
490- .await;
491- let (cache, _handles) = room.event_cache().await.unwrap();
492- let (initial_events, mut updates) =
493- cache.subscribe_to_thread(thread_root_id.to_owned()).await.unwrap();
494- assert_eq!(initial_events.len(), 1);
495- assert_eq!(initial_events[0].event_id().as_deref(), Some(thread_reply_id));
496-
497- cache.clear().await.unwrap();
498- let clear =
499- tokio::time::timeout(Duration::from_secs(2), updates.recv()).await.unwrap().unwrap();
500- assert!(matches!(clear.diffs.as_slice(), [VectorDiff::Clear]));
501- let (events_after_clear, _updates_after_clear) =
502- cache.subscribe_to_thread(thread_root_id.to_owned()).await.unwrap();
503- assert!(events_after_clear.is_empty());
504-}
505-
506-#[tokio::test]
507-async fn clear_empties_loaded_pinned_cache() {
508- let server = MatrixMockServer::new().await;
509- let room_id = room_id!("!reseed:localhost");
510- let pinned_event_id = event_id!("$pinned:localhost");
511- let factory = EventFactory::new().room(room_id).sender(&ALICE);
512- let pinned_event = factory.text_msg("pinned").event_id(pinned_event_id).into_event();
513- server.mock_room_event().match_event_id().ok(pinned_event).mock_once().mount().await;
514- let client = server.client_builder().build().await;
515- client.event_cache().subscribe().unwrap();
516- let room = server
517- .sync_room(
518- &client,
519- JoinedRoomBuilder::new(room_id).add_state_bulk(vec![
520- factory.room_pinned_events(vec![pinned_event_id.to_owned()]).into(),
521- ]),
522- )
523- .await;
524- let (cache, _handles) = room.event_cache().await.unwrap();
525- let (initial_events, mut updates) = cache.subscribe_to_pinned_events().await.unwrap();
526- let mut visible: imbl::Vector<_> = initial_events.into();
527- tokio::time::timeout(Duration::from_secs(3), async {
528- while visible.is_empty() {
529- for diff in updates.recv().await.unwrap().diffs {
530- diff.apply(&mut visible);
531- }
532- }
533- })
534- .await
535- .unwrap();
536- assert_eq!(visible.len(), 1);
537- assert_eq!(visible[0].event_id().as_deref(), Some(pinned_event_id));
538-
539- cache.clear().await.unwrap();
540- let clear =
541- tokio::time::timeout(Duration::from_secs(2), updates.recv()).await.unwrap().unwrap();
542- assert!(matches!(clear.diffs.as_slice(), [VectorDiff::Clear]));
543- for diff in clear.diffs {
544- diff.apply(&mut visible);
545- }
546- assert!(visible.is_empty());
547-}
548-
549-#[cfg(feature = "sqlite")]
550-#[tokio::test]
551-async fn reseed_persists_the_window_and_cursor_across_client_recreation() {
552- let server = MatrixMockServer::new().await;
553- let directory = tempfile::tempdir().unwrap();
554- let room_id = room_id!("!reseed:localhost");
555- let recent_id = event_id!("$recent:localhost");
556- {
557- let client = server
558- .client_builder()
559- .on_builder(|b| b.sqlite_store(directory.path(), None))
560- .build()
561- .await;
562- client.event_cache().subscribe().unwrap();
563- server.sync_joined_room(&client, room_id).await;
564- let (cache, _handles) = client.get_room(room_id).unwrap().event_cache().await.unwrap();
565- cache
566- .reseed(
567- vec![
568- EventFactory::new()
569- .room(room_id)
570- .sender(&ALICE)
571- .text_msg("recent")
572- .event_id(recent_id)
573- .into_event(),
574- ],
575- Some("persisted-cursor".into()),
576- )
577- .await
578- .unwrap();
579- }
580- let client = server
581- .client_builder()
582- .on_builder(|b| b.sqlite_store(directory.path(), None))
583- .build()
584- .await;
585- client.event_cache().subscribe().unwrap();
586- let (cache, _handles) = client.get_room(room_id).unwrap().event_cache().await.unwrap();
587- let (events, _subscriber) = cache.subscribe().await.unwrap();
588- assert_eq!(events.len(), 1);
589- assert_eq!(events[0].event_id().as_deref(), Some(recent_id));
590- server
591- .mock_room_messages()
592- .match_from("persisted-cursor")
593- .ok(RoomMessagesResponseTemplate::default())
594- .mock_once()
595- .mount()
596- .await;
597- assert!(cache.pagination().run_backwards_once(10).await.unwrap().reached_start);
598-}
Deleted file: crates/matrix-sdk/tests/history_reseed/failing_store.rs−141 deletions
@@ -1,141 +0,0 @@
1-use std::sync::atomic::{AtomicBool, Ordering};
2-
3-use async_trait::async_trait;
4-use matrix_sdk_base::{
5- event_cache::{
6- Event, Gap,
7- store::{EventCacheStore, EventCacheStoreError, MemoryStore},
8- },
9- linked_chunk::{
10- ChunkIdentifier, ChunkIdentifierGenerator, ChunkMetadata, LinkedChunkId, Position,
11- RawChunk, Update,
12- },
13-};
14-use matrix_sdk_common::cross_process_lock::CrossProcessLockGeneration;
15-use ruma::{EventId, OwnedEventId, RoomId, events::relation::RelationType};
16-
17-#[derive(Debug, Default)]
18-pub struct FailingStore {
19- pub inner: MemoryStore,
20- pub fail_writes: AtomicBool,
21-}
22-
23-#[cfg_attr(target_family = "wasm", async_trait(?Send))]
24-#[cfg_attr(not(target_family = "wasm"), async_trait)]
25-impl EventCacheStore for FailingStore {
26- type Error = EventCacheStoreError;
27-
28- async fn try_take_leased_lock(
29- &self,
30- lease_duration_ms: u32,
31- key: &str,
32- holder: &str,
33- ) -> Result<Option<CrossProcessLockGeneration>, Self::Error> {
34- self.inner.try_take_leased_lock(lease_duration_ms, key, holder).await
35- }
36-
37- async fn handle_linked_chunk_updates(
38- &self,
39- linked_chunk_id: LinkedChunkId<'_>,
40- updates: Vec<Update<Event, Gap>>,
41- ) -> Result<(), Self::Error> {
42- if self.fail_writes.load(Ordering::SeqCst) {
43- return Err(EventCacheStoreError::InvalidData {
44- details: "injected write failure".into(),
45- });
46- }
47-
48- self.inner.handle_linked_chunk_updates(linked_chunk_id, updates).await
49- }
50-
51- async fn remove_room(&self, room_id: &RoomId) -> Result<(), Self::Error> {
52- self.inner.remove_room(room_id).await
53- }
54-
55- async fn load_all_chunks(
56- &self,
57- linked_chunk_id: LinkedChunkId<'_>,
58- ) -> Result<Vec<RawChunk<Event, Gap>>, Self::Error> {
59- self.inner.load_all_chunks(linked_chunk_id).await
60- }
61-
62- async fn load_all_chunks_metadata(
63- &self,
64- linked_chunk_id: LinkedChunkId<'_>,
65- ) -> Result<Vec<ChunkMetadata>, Self::Error> {
66- self.inner.load_all_chunks_metadata(linked_chunk_id).await
67- }
68-
69- async fn load_last_chunk(
70- &self,
71- linked_chunk_id: LinkedChunkId<'_>,
72- ) -> Result<(Option<RawChunk<Event, Gap>>, ChunkIdentifierGenerator), Self::Error> {
73- self.inner.load_last_chunk(linked_chunk_id).await
74- }
75-
76- async fn load_previous_chunk(
77- &self,
78- linked_chunk_id: LinkedChunkId<'_>,
79- before_chunk_identifier: ChunkIdentifier,
80- ) -> Result<Option<RawChunk<Event, Gap>>, Self::Error> {
81- self.inner.load_previous_chunk(linked_chunk_id, before_chunk_identifier).await
82- }
83-
84- async fn clear_all_linked_chunks(&self) -> Result<(), Self::Error> {
85- self.inner.clear_all_linked_chunks().await
86- }
87-
88- async fn filter_duplicated_events(
89- &self,
90- linked_chunk_id: LinkedChunkId<'_>,
91- events: Vec<OwnedEventId>,
92- ) -> Result<Vec<(OwnedEventId, Position)>, Self::Error> {
93- self.inner.filter_duplicated_events(linked_chunk_id, events).await
94- }
95-
96- async fn find_event(
97- &self,
98- room_id: &RoomId,
99- event_id: &EventId,
100- ) -> Result<Option<Event>, Self::Error> {
101- self.inner.find_event(room_id, event_id).await
102- }
103-
104- async fn find_event_relations(
105- &self,
106- room_id: &RoomId,
107- event_id: &EventId,
108- filter: Option<&[RelationType]>,
109- ) -> Result<Vec<(Event, Option<Position>)>, Self::Error> {
110- self.inner.find_event_relations(room_id, event_id, filter).await
111- }
112-
113- async fn get_room_events(
114- &self,
115- room_id: &RoomId,
116- event_type: Option<&str>,
117- session_id: Option<&str>,
118- ) -> Result<Vec<Event>, Self::Error> {
119- self.inner.get_room_events(room_id, event_type, session_id).await
120- }
121-
122- async fn save_event(&self, room_id: &RoomId, event: Event) -> Result<(), Self::Error> {
123- self.inner.save_event(room_id, event).await
124- }
125-
126- async fn close(&self) -> Result<(), Self::Error> {
127- self.inner.close().await
128- }
129-
130- async fn reopen(&self) -> Result<(), Self::Error> {
131- self.inner.reopen().await
132- }
133-
134- async fn optimize(&self) -> Result<(), Self::Error> {
135- self.inner.optimize().await
136- }
137-
138- async fn get_size(&self) -> Result<Option<usize>, Self::Error> {
139- self.inner.get_size().await
140- }
141-}
Modified file: crates/matrix-sdk/tests/recent_history.rs+63 additions−36 deletions
@@ -54,10 +54,13 @@ async fn test_recent_history_overlapping_a_limited_sync_drops_its_stale_gap() {
5454 let room = server.sync_joined_room(&client, room_id).await;
5555 let (cache, _handles) = room.event_cache().await.unwrap();
5656 let f = event_factory(room_id);
57- cache
58- .reseed(vec![f.text_msg("old").event_id(event_id!("$old")).into_event()], None)
59- .await
60- .unwrap();
57+ server
58+ .sync_room(
59+ &client,
60+ JoinedRoomBuilder::new(room_id)
61+ .add_timeline_event(f.text_msg("old").event_id(event_id!("$old"))),
62+ )
63+ .await;
6164 server
6265 .mock_room_messages()
6366 .match_limit(50)
@@ -91,10 +94,13 @@ async fn test_recent_history_ending_before_a_limited_sync_keeps_its_gap_after_th
9194 let room = server.sync_joined_room(&client, room_id).await;
9295 let (cache, _handles) = room.event_cache().await.unwrap();
9396 let f = event_factory(room_id);
94- cache
95- .reseed(vec![f.text_msg("old").event_id(event_id!("$old")).into_event()], None)
96- .await
97- .unwrap();
97+ server
98+ .sync_room(
99+ &client,
100+ JoinedRoomBuilder::new(room_id)
101+ .add_timeline_event(f.text_msg("old").event_id(event_id!("$old"))),
102+ )
103+ .await;
98104 server
99105 .mock_room_messages()
100106 .match_limit(50)
@@ -129,10 +135,13 @@ async fn test_recent_history_is_bounded_and_keeps_the_retained_window() {
129135 let room = server.sync_joined_room(&client, room_id).await;
130136 let (cache, _handles) = room.event_cache().await.unwrap();
131137 let f = event_factory(room_id);
132- cache
133- .reseed(vec![f.text_msg("old").event_id(event_id!("$old")).into_event()], None)
134- .await
135- .unwrap();
138+ server
139+ .sync_room(
140+ &client,
141+ JoinedRoomBuilder::new(room_id)
142+ .add_timeline_event(f.text_msg("old").event_id(event_id!("$old"))),
143+ )
144+ .await;
136145
137146 server
138147 .mock_room_messages()
@@ -202,10 +211,13 @@ async fn test_recent_history_joins_at_the_retained_boundary() {
202211 let room = server.sync_joined_room(&client, room_id).await;
203212 let (cache, _handles) = room.event_cache().await.unwrap();
204213 let f = event_factory(room_id);
205- cache
206- .reseed(vec![f.text_msg("old").event_id(event_id!("$old")).into_event()], None)
207- .await
208- .unwrap();
214+ server
215+ .sync_room(
216+ &client,
217+ JoinedRoomBuilder::new(room_id)
218+ .add_timeline_event(f.text_msg("old").event_id(event_id!("$old"))),
219+ )
220+ .await;
209221 server
210222 .mock_room_messages()
211223 .match_limit(50)
@@ -242,10 +254,13 @@ async fn test_recent_history_keeps_sync_arrivals_during_a_fetch() {
242254 let room = server.sync_joined_room(&client, room_id).await;
243255 let (cache, _handles) = room.event_cache().await.unwrap();
244256 let f = event_factory(room_id);
245- cache
246- .reseed(vec![f.text_msg("old").event_id(event_id!("$old")).into_event()], None)
247- .await
248- .unwrap();
257+ server
258+ .sync_room(
259+ &client,
260+ JoinedRoomBuilder::new(room_id)
261+ .add_timeline_event(f.text_msg("old").event_id(event_id!("$old"))),
262+ )
263+ .await;
249264 server
250265 .mock_room_messages()
251266 .match_limit(50)
@@ -288,10 +303,13 @@ async fn test_recent_history_continues_empty_pages_and_pauses_on_a_cursor_cycle(
288303 let room = server.sync_joined_room(&client, room_id).await;
289304 let (cache, _handles) = room.event_cache().await.unwrap();
290305 let f = event_factory(room_id);
291- cache
292- .reseed(vec![f.text_msg("old").event_id(event_id!("$old")).into_event()], None)
293- .await
294- .unwrap();
306+ server
307+ .sync_room(
308+ &client,
309+ JoinedRoomBuilder::new(room_id)
310+ .add_timeline_event(f.text_msg("old").event_id(event_id!("$old"))),
311+ )
312+ .await;
295313 server
296314 .mock_room_messages()
297315 .match_limit(50)
@@ -343,10 +361,13 @@ async fn test_concurrent_recent_history_calls_share_the_request_and_gap() {
343361 let room = server.sync_joined_room(&client, room_id).await;
344362 let (cache, _handles) = room.event_cache().await.unwrap();
345363 let f = event_factory(room_id);
346- cache
347- .reseed(vec![f.text_msg("old").event_id(event_id!("$old")).into_event()], None)
348- .await
349- .unwrap();
364+ server
365+ .sync_room(
366+ &client,
367+ JoinedRoomBuilder::new(room_id)
368+ .add_timeline_event(f.text_msg("old").event_id(event_id!("$old"))),
369+ )
370+ .await;
350371 server
351372 .mock_room_messages()
352373 .match_limit(50)
@@ -377,10 +398,13 @@ async fn test_cancelling_one_recent_history_waiter_keeps_the_shared_request_aliv
377398 let room = server.sync_joined_room(&client, room_id).await;
378399 let (cache, _handles) = room.event_cache().await.unwrap();
379400 let f = event_factory(room_id);
380- cache
381- .reseed(vec![f.text_msg("old").event_id(event_id!("$old")).into_event()], None)
382- .await
383- .unwrap();
401+ server
402+ .sync_room(
403+ &client,
404+ JoinedRoomBuilder::new(room_id)
405+ .add_timeline_event(f.text_msg("old").event_id(event_id!("$old"))),
406+ )
407+ .await;
384408 server
385409 .mock_room_messages()
386410 .match_limit(50)
@@ -426,10 +450,13 @@ async fn test_cancelling_all_recent_history_waiters_allows_a_fresh_retry() {
426450 let room = server.sync_joined_room(&client, room_id).await;
427451 let (cache, _handles) = room.event_cache().await.unwrap();
428452 let f = event_factory(room_id);
429- cache
430- .reseed(vec![f.text_msg("old").event_id(event_id!("$old")).into_event()], None)
431- .await
432- .unwrap();
453+ server
454+ .sync_room(
455+ &client,
456+ JoinedRoomBuilder::new(room_id)
457+ .add_timeline_event(f.text_msg("old").event_id(event_id!("$old"))),
458+ )
459+ .await;
433460 server
434461 .mock_room_messages()
435462 .match_limit(50)
Added file: crates/matrix-sdk/tests/room_clear.rs+174 additions
@@ -0,0 +1,174 @@
1+use std::time::Duration;
2+
3+use eyeball_im::VectorDiff;
4+use matrix_sdk::{
5+ ThreadingSupport,
6+ event_cache::EventFocusThreadMode,
7+ ruma::{event_id, room_id},
8+ test_utils::mocks::{
9+ MatrixMockServer, RoomContextResponseTemplate, RoomMessagesResponseTemplate,
10+ },
11+};
12+use matrix_sdk_test::{ALICE, JoinedRoomBuilder, event_factory::EventFactory};
13+
14+#[tokio::test]
15+async fn clear_invalidates_focused_cache_clones_and_inflight_pagination() {
16+ let server = MatrixMockServer::new().await;
17+ let client = server.client_builder().build().await;
18+ let room_id = room_id!("!clear:localhost");
19+ client.event_cache().subscribe().unwrap();
20+ server.sync_joined_room(&client, room_id).await;
21+ let room = client.get_room(room_id).unwrap();
22+ let (cache, _handles) = room.event_cache().await.unwrap();
23+ let factory = EventFactory::new().room(room_id).sender(&ALICE);
24+ let focused_id = event_id!("$focused:localhost");
25+ server
26+ .mock_room_event_context()
27+ .ok(RoomContextResponseTemplate::new(
28+ factory.text_msg("focused").event_id(focused_id).into_event(),
29+ )
30+ .start("old-focused-cursor"))
31+ .mock_once()
32+ .mount()
33+ .await;
34+ let focused = cache
35+ .get_or_create_event_focused_cache(
36+ focused_id.to_owned(),
37+ 10,
38+ EventFocusThreadMode::Automatic,
39+ )
40+ .await
41+ .unwrap();
42+ let focused_clone = focused.clone();
43+ let (initial_events, mut updates) = focused.subscribe().await;
44+ assert_eq!(initial_events.len(), 1);
45+ assert_eq!(initial_events[0].event_id().as_deref(), Some(focused_id));
46+
47+ let stale_event_id = event_id!("$stale-focused:localhost");
48+ server
49+ .mock_room_messages()
50+ .match_from("old-focused-cursor")
51+ .ok(RoomMessagesResponseTemplate::default()
52+ .events(vec![factory.text_msg("stale").event_id(stale_event_id)])
53+ .end_token("stale-next-cursor")
54+ .with_delay(Duration::from_millis(300)))
55+ .mock_once()
56+ .mount()
57+ .await;
58+ let pagination = tokio::spawn({
59+ let focused = focused.clone();
60+ async move { focused.paginate_backwards(10).await }
61+ });
62+ tokio::time::timeout(Duration::from_secs(8), async {
63+ loop {
64+ if server
65+ .received_requests()
66+ .await
67+ .unwrap()
68+ .iter()
69+ .any(|request| request.url.path().ends_with("/messages"))
70+ {
71+ break;
72+ }
73+ tokio::task::yield_now().await;
74+ }
75+ })
76+ .await
77+ .unwrap();
78+
79+ tokio::time::timeout(Duration::from_secs(2), cache.clear()).await.unwrap().unwrap();
80+ let clear =
81+ tokio::time::timeout(Duration::from_secs(2), updates.recv()).await.unwrap().unwrap();
82+ assert!(matches!(clear.diffs.as_slice(), [VectorDiff::Clear]));
83+ tokio::time::timeout(Duration::from_secs(3), pagination).await.unwrap().unwrap().unwrap();
84+ let (cloned_events, _cloned_updates) = focused_clone.subscribe().await;
85+ assert!(cloned_events.is_empty());
86+ let result = focused_clone.paginate_backwards(10).await.unwrap();
87+ assert!(result.hit_end_of_timeline);
88+ let (events_after_pagination, _updates_after_pagination) = focused_clone.subscribe().await;
89+ assert!(events_after_pagination.is_empty());
90+}
91+
92+#[tokio::test]
93+async fn clear_empties_subscribed_thread_cache() {
94+ let server = MatrixMockServer::new().await;
95+ let client = server
96+ .client_builder()
97+ .on_builder(|builder| {
98+ builder.with_threading_support(ThreadingSupport::Enabled { with_subscriptions: false })
99+ })
100+ .build()
101+ .await;
102+ let room_id = room_id!("!clear:localhost");
103+ client.event_cache().subscribe().unwrap();
104+ let thread_root_id = event_id!("$thread-root:localhost");
105+ let thread_reply_id = event_id!("$thread-reply:localhost");
106+ let factory = EventFactory::new().room(room_id).sender(&ALICE);
107+ let room = server
108+ .sync_room(
109+ &client,
110+ JoinedRoomBuilder::new(room_id).add_timeline_event(
111+ factory
112+ .text_msg("thread reply")
113+ .in_thread(thread_root_id, thread_root_id)
114+ .event_id(thread_reply_id),
115+ ),
116+ )
117+ .await;
118+ let (cache, _handles) = room.event_cache().await.unwrap();
119+ let (initial_events, mut updates) =
120+ cache.subscribe_to_thread(thread_root_id.to_owned()).await.unwrap();
121+ assert_eq!(initial_events.len(), 1);
122+ assert_eq!(initial_events[0].event_id().as_deref(), Some(thread_reply_id));
123+
124+ cache.clear().await.unwrap();
125+ let clear =
126+ tokio::time::timeout(Duration::from_secs(2), updates.recv()).await.unwrap().unwrap();
127+ assert!(matches!(clear.diffs.as_slice(), [VectorDiff::Clear]));
128+ let (events_after_clear, _updates_after_clear) =
129+ cache.subscribe_to_thread(thread_root_id.to_owned()).await.unwrap();
130+ assert!(events_after_clear.is_empty());
131+}
132+
133+#[tokio::test]
134+async fn clear_empties_loaded_pinned_cache() {
135+ let server = MatrixMockServer::new().await;
136+ let room_id = room_id!("!clear:localhost");
137+ let pinned_event_id = event_id!("$pinned:localhost");
138+ let factory = EventFactory::new().room(room_id).sender(&ALICE);
139+ let pinned_event = factory.text_msg("pinned").event_id(pinned_event_id).into_event();
140+ server.mock_room_event().match_event_id().ok(pinned_event).mock_once().mount().await;
141+ let client = server.client_builder().build().await;
142+ client.event_cache().subscribe().unwrap();
143+ let room = server
144+ .sync_room(
145+ &client,
146+ JoinedRoomBuilder::new(room_id).add_state_bulk(vec![
147+ factory.room_pinned_events(vec![pinned_event_id.to_owned()]).into(),
148+ ]),
149+ )
150+ .await;
151+ let (cache, _handles) = room.event_cache().await.unwrap();
152+ let (initial_events, mut updates) = cache.subscribe_to_pinned_events().await.unwrap();
153+ let mut visible: imbl::Vector<_> = initial_events.into();
154+ tokio::time::timeout(Duration::from_secs(3), async {
155+ while visible.is_empty() {
156+ for diff in updates.recv().await.unwrap().diffs {
157+ diff.apply(&mut visible);
158+ }
159+ }
160+ })
161+ .await
162+ .unwrap();
163+ assert_eq!(visible.len(), 1);
164+ assert_eq!(visible[0].event_id().as_deref(), Some(pinned_event_id));
165+
166+ cache.clear().await.unwrap();
167+ let clear =
168+ tokio::time::timeout(Duration::from_secs(2), updates.recv()).await.unwrap().unwrap();
169+ assert!(matches!(clear.diffs.as_slice(), [VectorDiff::Clear]));
170+ for diff in clear.diffs {
171+ diff.apply(&mut visible);
172+ }
173+ assert!(visible.is_empty());
174+}
Modified file: crates/matrix-sdk/tests/segmented_history.rs+50 additions−35 deletions
@@ -13,7 +13,7 @@ use matrix_sdk_common::cross_process_lock::CrossProcessLockConfig;
1313 use matrix_sdk_test::{JoinedRoomBuilder, async_test, event_factory::EventFactory};
1414 use ruma::{event_id, room_id, user_id};
1515
16-#[path = "history_reseed/failing_store.rs"]
16+#[path = "support/failing_store.rs"]
1717 mod failing_store;
1818
1919 #[async_test]
@@ -34,13 +34,14 @@ async fn test_segmented_history_resumes_after_restart_and_joins_only_at_boundary
3434 client.event_cache().subscribe().unwrap();
3535 let room = server.sync_joined_room(&client, room_id).await;
3636 let (cache, _handles) = room.event_cache().await.unwrap();
37- cache
38- .reseed(
39- vec![f.text_msg("retained").event_id(event_id!("$old")).into_event()],
40- Some("old-edge".into()),
37+ server
38+ .sync_room(
39+ &client,
40+ JoinedRoomBuilder::new(room_id)
41+ .add_timeline_event(f.text_msg("retained").event_id(event_id!("$old")))
42+ .set_timeline_prev_batch("old-edge"),
4143 )
42- .await
43- .unwrap();
44+ .await;
4445 let gap = cache
4546 .append_history_section(
4647 vec![f.text_msg("recent").event_id(event_id!("$new")).into_event()],
@@ -129,10 +130,13 @@ async fn test_subscribe_with_history_reports_atomic_gap_snapshot_and_updates() {
129130 let room = server.sync_joined_room(&client, room_id).await;
130131 let (cache, _handles) = room.event_cache().await.unwrap();
131132 let f = EventFactory::new().room(room_id).sender(user_id!("@alice:example.org"));
132- cache
133- .reseed(vec![f.text_msg("old").event_id(event_id!("$old")).into_event()], None)
134- .await
135- .unwrap();
133+ server
134+ .sync_room(
135+ &client,
136+ JoinedRoomBuilder::new(room_id)
137+ .add_timeline_event(f.text_msg("old").event_id(event_id!("$old"))),
138+ )
139+ .await;
136140 cache
137141 .append_history_section(
138142 vec![f.reaction(event_id!("$old"), "👍").event_id(event_id!("$hidden")).into_event()],
@@ -195,13 +199,14 @@ async fn test_segmented_history_keeps_unavailable_intervals_and_independent_curs
195199 let room = server.sync_joined_room(&client, room_id).await;
196200 let (cache, _handles) = room.event_cache().await.unwrap();
197201 let f = EventFactory::new().room(room_id).sender(user_id!("@alice:example.org"));
198- cache
199- .reseed(
200- vec![f.text_msg("old").event_id(event_id!("$old")).into_event()],
201- Some("old-edge".into()),
202+ server
203+ .sync_room(
204+ &client,
205+ JoinedRoomBuilder::new(room_id)
206+ .add_timeline_event(f.text_msg("old").event_id(event_id!("$old")))
207+ .set_timeline_prev_batch("old-edge"),
202208 )
203- .await
204- .unwrap();
209+ .await;
205210 let first = cache
206211 .append_history_section(
207212 vec![f.text_msg("middle").event_id(event_id!("$middle")).into_event()],
@@ -261,13 +266,14 @@ async fn test_segmented_history_failed_commit_keeps_events_and_cursor_retryable(
261266 let room = server.sync_joined_room(&client, room_id).await;
262267 let (cache, _handles) = room.event_cache().await.unwrap();
263268 let f = EventFactory::new().room(room_id).sender(user_id!("@alice:example.org"));
264- cache
265- .reseed(
266- vec![f.text_msg("old").event_id(event_id!("$old")).into_event()],
267- Some("old-edge".into()),
269+ server
270+ .sync_room(
271+ &client,
272+ JoinedRoomBuilder::new(room_id)
273+ .add_timeline_event(f.text_msg("old").event_id(event_id!("$old")))
274+ .set_timeline_prev_batch("old-edge"),
268275 )
269- .await
270- .unwrap();
276+ .await;
271277 store.fail_writes.store(true, Ordering::SeqCst);
272278 assert!(
273279 cache
@@ -321,10 +327,13 @@ async fn test_segmented_history_request_failure_preserves_existing_progress() {
321327 let room = server.sync_joined_room(&client, room_id).await;
322328 let (cache, _handles) = room.event_cache().await.unwrap();
323329 let f = EventFactory::new().room(room_id).sender(user_id!("@alice:example.org"));
324- cache
325- .reseed(vec![f.text_msg("old").event_id(event_id!("$old")).into_event()], None)
326- .await
327- .unwrap();
330+ server
331+ .sync_room(
332+ &client,
333+ JoinedRoomBuilder::new(room_id)
334+ .add_timeline_event(f.text_msg("old").event_id(event_id!("$old"))),
335+ )
336+ .await;
328337 let gap = cache
329338 .append_history_section(
330339 vec![f.text_msg("new").event_id(event_id!("$new")).into_event()],
@@ -365,10 +374,13 @@ async fn test_segmented_history_cancellation_keeps_the_saved_cursor() {
365374 let room = server.sync_joined_room(&client, room_id).await;
366375 let (cache, _handles) = room.event_cache().await.unwrap();
367376 let f = EventFactory::new().room(room_id).sender(user_id!("@alice:example.org"));
368- cache
369- .reseed(vec![f.text_msg("old").event_id(event_id!("$old")).into_event()], None)
370- .await
371- .unwrap();
377+ server
378+ .sync_room(
379+ &client,
380+ JoinedRoomBuilder::new(room_id)
381+ .add_timeline_event(f.text_msg("old").event_id(event_id!("$old"))),
382+ )
383+ .await;
372384 let gap = cache
373385 .append_history_section(
374386 vec![f.text_msg("new").event_id(event_id!("$new")).into_event()],
@@ -423,10 +435,13 @@ async fn test_segmented_history_preserves_sync_arrivals_during_a_fill() {
423435 let room = server.sync_joined_room(&client, room_id).await;
424436 let (cache, _handles) = room.event_cache().await.unwrap();
425437 let f = EventFactory::new().room(room_id).sender(user_id!("@alice:example.org"));
426- cache
427- .reseed(vec![f.text_msg("old").event_id(event_id!("$old")).into_event()], None)
428- .await
429- .unwrap();
438+ server
439+ .sync_room(
440+ &client,
441+ JoinedRoomBuilder::new(room_id)
442+ .add_timeline_event(f.text_msg("old").event_id(event_id!("$old"))),
443+ )
444+ .await;
430445 let gap = cache
431446 .append_history_section(
432447 vec![f.text_msg("new").event_id(event_id!("$new")).into_event()],
Added file: crates/matrix-sdk/tests/support/failing_store.rs+141 additions
@@ -0,0 +1,141 @@
1+use std::sync::atomic::{AtomicBool, Ordering};
2+
3+use async_trait::async_trait;
4+use matrix_sdk_base::{
5+ event_cache::{
6+ Event, Gap,
7+ store::{EventCacheStore, EventCacheStoreError, MemoryStore},
8+ },
9+ linked_chunk::{
10+ ChunkIdentifier, ChunkIdentifierGenerator, ChunkMetadata, LinkedChunkId, Position,
11+ RawChunk, Update,
12+ },
13+};
14+use matrix_sdk_common::cross_process_lock::CrossProcessLockGeneration;
15+use ruma::{EventId, OwnedEventId, RoomId, events::relation::RelationType};
16+
17+#[derive(Debug, Default)]
18+pub struct FailingStore {
19+ pub inner: MemoryStore,
20+ pub fail_writes: AtomicBool,
21+}
22+
23+#[cfg_attr(target_family = "wasm", async_trait(?Send))]
24+#[cfg_attr(not(target_family = "wasm"), async_trait)]
25+impl EventCacheStore for FailingStore {
26+ type Error = EventCacheStoreError;
27+
28+ async fn try_take_leased_lock(
29+ &self,
30+ lease_duration_ms: u32,
31+ key: &str,
32+ holder: &str,
33+ ) -> Result<Option<CrossProcessLockGeneration>, Self::Error> {
34+ self.inner.try_take_leased_lock(lease_duration_ms, key, holder).await
35+ }
36+
37+ async fn handle_linked_chunk_updates(
38+ &self,
39+ linked_chunk_id: LinkedChunkId<'_>,
40+ updates: Vec<Update<Event, Gap>>,
41+ ) -> Result<(), Self::Error> {
42+ if self.fail_writes.load(Ordering::SeqCst) {
43+ return Err(EventCacheStoreError::InvalidData {
44+ details: "injected write failure".into(),
45+ });
46+ }
47+
48+ self.inner.handle_linked_chunk_updates(linked_chunk_id, updates).await
49+ }
50+
51+ async fn remove_room(&self, room_id: &RoomId) -> Result<(), Self::Error> {
52+ self.inner.remove_room(room_id).await
53+ }
54+
55+ async fn load_all_chunks(
56+ &self,
57+ linked_chunk_id: LinkedChunkId<'_>,
58+ ) -> Result<Vec<RawChunk<Event, Gap>>, Self::Error> {
59+ self.inner.load_all_chunks(linked_chunk_id).await
60+ }
61+
62+ async fn load_all_chunks_metadata(
63+ &self,
64+ linked_chunk_id: LinkedChunkId<'_>,
65+ ) -> Result<Vec<ChunkMetadata>, Self::Error> {
66+ self.inner.load_all_chunks_metadata(linked_chunk_id).await
67+ }
68+
69+ async fn load_last_chunk(
70+ &self,
71+ linked_chunk_id: LinkedChunkId<'_>,
72+ ) -> Result<(Option<RawChunk<Event, Gap>>, ChunkIdentifierGenerator), Self::Error> {
73+ self.inner.load_last_chunk(linked_chunk_id).await
74+ }
75+
76+ async fn load_previous_chunk(
77+ &self,
78+ linked_chunk_id: LinkedChunkId<'_>,
79+ before_chunk_identifier: ChunkIdentifier,
80+ ) -> Result<Option<RawChunk<Event, Gap>>, Self::Error> {
81+ self.inner.load_previous_chunk(linked_chunk_id, before_chunk_identifier).await
82+ }
83+
84+ async fn clear_all_linked_chunks(&self) -> Result<(), Self::Error> {
85+ self.inner.clear_all_linked_chunks().await
86+ }
87+
88+ async fn filter_duplicated_events(
89+ &self,
90+ linked_chunk_id: LinkedChunkId<'_>,
91+ events: Vec<OwnedEventId>,
92+ ) -> Result<Vec<(OwnedEventId, Position)>, Self::Error> {
93+ self.inner.filter_duplicated_events(linked_chunk_id, events).await
94+ }
95+
96+ async fn find_event(
97+ &self,
98+ room_id: &RoomId,
99+ event_id: &EventId,
100+ ) -> Result<Option<Event>, Self::Error> {
101+ self.inner.find_event(room_id, event_id).await
102+ }
103+
104+ async fn find_event_relations(
105+ &self,
106+ room_id: &RoomId,
107+ event_id: &EventId,
108+ filter: Option<&[RelationType]>,
109+ ) -> Result<Vec<(Event, Option<Position>)>, Self::Error> {
110+ self.inner.find_event_relations(room_id, event_id, filter).await
111+ }
112+
113+ async fn get_room_events(
114+ &self,
115+ room_id: &RoomId,
116+ event_type: Option<&str>,
117+ session_id: Option<&str>,
118+ ) -> Result<Vec<Event>, Self::Error> {
119+ self.inner.get_room_events(room_id, event_type, session_id).await
120+ }
121+
122+ async fn save_event(&self, room_id: &RoomId, event: Event) -> Result<(), Self::Error> {
123+ self.inner.save_event(room_id, event).await
124+ }
125+
126+ async fn close(&self) -> Result<(), Self::Error> {
127+ self.inner.close().await
128+ }
129+
130+ async fn reopen(&self) -> Result<(), Self::Error> {
131+ self.inner.reopen().await
132+ }
133+
134+ async fn optimize(&self) -> Result<(), Self::Error> {
135+ self.inner.optimize().await
136+ }
137+
138+ async fn get_size(&self) -> Result<Option<usize>, Self::Error> {
139+ self.inner.get_size().await
140+ }
141+}