atagen

matrix-rust-sdk

public
Star0
Watch0

Commit ef3c294b31cc

tarball

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

9 files changed+578-29
event_cache: add durable room history reseeding
Modified file: crates/matrix-sdk/Cargo.toml+4 additions
@@ -209,5 +209,9 @@ required-features = ["testing"]
209209 name = "response_commit"
210210 required-features = ["testing", "e2e-encryption"]
211211
212+[[test]]
213+name = "history_reseed"
214+required-features = ["testing"]
215+
212216 [lints]
213217 workspace = true
Modified file: crates/matrix-sdk/src/event_cache/caches/pagination.rs+45 additions−22 deletions
@@ -144,24 +144,31 @@ where
144144 }
145145 }
146146
147+ let generation = self.cache.generation();
147148 let reset_status_on_drop_guard = ResetStatusOnDrop {
148149 prev_status: Some(status_guard.clone()),
149- pagination_status: status_observable.clone(),
150+ cache: self.cache.clone(),
151+ generation,
150152 };
151153
152154 let this = self.clone();
153155
154156 let fut: Pin<Box<dyn SharedPaginationFuture>> = Box::pin(async move {
155- match this.paginate_backwards_impl(batch_size).await? {
157+ match this.paginate_backwards_impl(batch_size, generation).await? {
156158 Some(outcome) => {
159+ let mut status = this.cache.status().write();
160+ if this.cache.generation() != generation {
161+ return Ok(None);
162+ }
157163 // Back-pagination's over and successful, don't reset the status to the previous
158164 // value.
159165 reset_status_on_drop_guard.disarm();
160166
161167 // Notify subscribers that pagination ended.
162- this.cache.status().set(SharedPaginationStatus::Idle {
163- hit_timeline_start: outcome.reached_start,
164- });
168+ ObservableWriteGuard::set(
169+ &mut status,
170+ SharedPaginationStatus::Idle { hit_timeline_start: outcome.reached_start },
171+ );
165172
166173 Ok(Some(outcome))
167174 }
@@ -207,6 +214,7 @@ where
207214 async fn paginate_backwards_impl(
208215 &self,
209216 batch_size: u16,
217+ generation: u64,
210218 ) -> Result<Option<BackPaginationOutcome>> {
211219 // A linked chunk might not be entirely loaded (if it's been lazy-loaded). Try
212220 // to load from disk/storage first, then from network if disk/storage indicated
@@ -250,7 +258,9 @@ where
250258 }
251259
252260 // We have a gap, so resolve it with a network back-pagination.
253- return self.paginate_backwards_with_network(batch_size, prev_token).await;
261+ return self
262+ .paginate_backwards_with_network(batch_size, prev_token, generation)
263+ .await;
254264 }
255265
256266 LoadMoreEventsBackwardsOutcome::StartOfTimeline => {
@@ -262,15 +272,15 @@ where
262272 timeline_event_diffs,
263273 reached_start,
264274 } => {
265- return Ok(Some(
266- self.cache
267- .conclude_backwards_pagination_from_disk(
268- events,
269- timeline_event_diffs,
270- reached_start,
271- )
272- .await,
273- ));
275+ return self
276+ .cache
277+ .conclude_backwards_pagination_from_disk(
278+ events,
279+ timeline_event_diffs,
280+ reached_start,
281+ generation,
282+ )
283+ .await;
274284 }
275285 }
276286 }
@@ -285,6 +295,7 @@ where
285295 &self,
286296 batch_size: u16,
287297 prev_token: Option<String>,
298+ generation: u64,
288299 ) -> Result<Option<BackPaginationOutcome>> {
289300 let Some((events, new_token)) =
290301 self.cache.paginate_backwards_with_network(batch_size, &prev_token).await?
@@ -296,7 +307,9 @@ where
296307 }));
297308 };
298309
299- self.cache.conclude_backwards_pagination_from_network(events, prev_token, new_token).await
310+ self.cache
311+ .conclude_backwards_pagination_from_network(events, prev_token, new_token, generation)
312+ .await
300313 }
301314 }
302315
@@ -340,6 +353,10 @@ pub(in super::super) enum SharedPaginationStatus {
340353 }
341354
342355 pub(in super::super) trait PaginatedCache {
356+ fn generation(&self) -> u64 {
357+ 0
358+ }
359+
343360 fn status(&self) -> &SharedObservable<SharedPaginationStatus>;
344361
345362 fn load_more_events_backwards(
@@ -363,13 +380,15 @@ pub(in super::super) trait PaginatedCache {
363380 events: Vec<Event>,
364381 timeline_event_diffs: Vec<VectorDiff<Event>>,
365382 reached_start: bool,
366- ) -> impl Future<Output = BackPaginationOutcome> + SendOutsideWasm;
383+ generation: u64,
384+ ) -> impl Future<Output = Result<Option<BackPaginationOutcome>>> + SendOutsideWasm;
367385
368386 fn conclude_backwards_pagination_from_network(
369387 &self,
370388 events: Vec<Event>,
371389 prev_token: Option<String>,
372390 new_token: Option<String>,
391+ generation: u64,
373392 ) -> impl Future<Output = Result<Option<BackPaginationOutcome>>> + SendOutsideWasm;
374393 }
375394
@@ -390,22 +409,26 @@ pub enum PaginationStatus {
390409
391410 /// Small RAII guard to reset the pagination status on drop, if not disarmed in
392411 /// the meanwhile.
393-struct ResetStatusOnDrop {
412+struct ResetStatusOnDrop<C: PaginatedCache> {
394413 prev_status: Option<SharedPaginationStatus>,
395- pagination_status: SharedObservable<SharedPaginationStatus>,
414+ cache: C,
415+ generation: u64,
396416 }
397417
398-impl ResetStatusOnDrop {
418+impl<C: PaginatedCache> ResetStatusOnDrop<C> {
399419 /// Make the RAII guard have no effect.
400420 fn disarm(mut self) {
401421 self.prev_status = None;
402422 }
403423 }
404424
405-impl Drop for ResetStatusOnDrop {
425+impl<C: PaginatedCache> Drop for ResetStatusOnDrop<C> {
406426 fn drop(&mut self) {
407427 if let Some(status) = self.prev_status.take() {
408- let _ = self.pagination_status.set(status);
428+ let mut guard = self.cache.status().write();
429+ if self.cache.generation() == self.generation {
430+ ObservableWriteGuard::set(&mut guard, status);
431+ }
409432 }
410433 }
411434 }
Modified file: crates/matrix-sdk/src/event_cache/caches/room/mod.rs+58 additions−3 deletions
@@ -20,10 +20,13 @@ mod updates;
2020 use std::{
2121 collections::BTreeMap,
2222 fmt,
23- sync::{Arc, atomic::Ordering},
23+ sync::{
24+ Arc,
25+ atomic::{AtomicU64, Ordering},
26+ },
2427 };
2528
26-use eyeball::SharedObservable;
29+use eyeball::{ObservableWriteGuard, SharedObservable};
2730 use matrix_sdk_base::{
2831 deserialized_responses::AmbiguityChange,
2932 event_cache::Event,
@@ -336,7 +339,16 @@ impl RoomEventCache {
336339 /// storage.
337340 pub async fn clear(&self) -> Result<()> {
338341 // Clear the linked chunk and persisted storage.
339- let updates_as_vector_diffs = self.inner.state.write().await?.reset().await?;
342+ let mut state = self.inner.state.write().await?;
343+ let updates_as_vector_diffs = state.reset().await?;
344+ {
345+ let mut status = self.inner.shared_pagination_status.write();
346+ self.inner.generation.fetch_add(1, Ordering::SeqCst);
347+ ObservableWriteGuard::set(
348+ &mut status,
349+ SharedPaginationStatus::Idle { hit_timeline_start: false },
350+ );
351+ }
340352
341353 // Notify observers about the update.
342354 self.inner.update_sender.send(
@@ -350,6 +362,47 @@ impl RoomEventCache {
350362 Ok(())
351363 }
352364
365+ #[doc = include_str!("reseed.md")]
366+ pub async fn reseed(&self, events: Vec<Event>, prev_batch: Option<String>) -> Result<()> {
367+ let cache = self.clone();
368+ matrix_sdk_common::executor::spawn(async move {
369+ let mut state = cache.inner.state.write().await?;
370+ state.reseed(events, prev_batch.clone()).await?;
371+ {
372+ let mut status = cache.inner.shared_pagination_status.write();
373+ cache.inner.generation.fetch_add(1, Ordering::SeqCst);
374+ ObservableWriteGuard::set(
375+ &mut status,
376+ SharedPaginationStatus::Idle { hit_timeline_start: prev_batch.is_none() },
377+ );
378+ }
379+ let events =
380+ state.room_linked_chunk().events().map(|(_, event)| event.clone()).collect();
381+ let result = async {
382+ state.clear_loaded_threads().await?;
383+ state
384+ .post_process_new_events(events, PostProcessingOrigin::Backpagination, None)
385+ .await
386+ }
387+ .await;
388+ let items =
389+ state.room_linked_chunk().events().map(|(_, event)| event.clone()).collect();
390+ cache.inner.update_sender.send(
391+ RoomEventCacheUpdate::UpdateTimelineEvents(TimelineVectorDiffs {
392+ diffs: vec![eyeball_im::VectorDiff::Reset { values: items }],
393+ origin: EventsOrigin::Cache,
394+ }),
395+ Some(RoomEventCacheGenericUpdate { room_id: cache.inner.room_id.clone() }),
396+ );
397+ if prev_batch.is_some() {
398+ cache.inner.pagination_batch_token_notifier.notify_waiters();
399+ }
400+ result
401+ })
402+ .await
403+ .expect("joining history reseed failed")
404+ }
405+
353406 /// Return a reference to the state.
354407 pub(in super::super) fn state(&self) -> &LockedRoomEventCacheState {
355408 &self.inner.state
@@ -423,6 +476,7 @@ impl RoomEventCache {
423476
424477 /// The (non-cloneable) details of the `RoomEventCache`.
425478 pub(super) struct RoomEventCacheInner {
479+ pub generation: AtomicU64,
426480 /// The room id for this room.
427481 room_id: OwnedRoomId,
428482
@@ -458,6 +512,7 @@ impl RoomEventCacheInner {
458512 update_sender: RoomEventCacheUpdateSender,
459513 ) -> Self {
460514 Self {
515+ generation: AtomicU64::new(0),
461516 room_id,
462517 weak_room,
463518 state,
Modified file: crates/matrix-sdk/src/event_cache/caches/room/pagination.rs+16 additions−3 deletions
@@ -154,6 +154,10 @@ impl RoomPagination {
154154 }
155155
156156 impl PaginatedCache for Arc<RoomEventCacheInner> {
157+ fn generation(&self) -> u64 {
158+ self.generation.load(std::sync::atomic::Ordering::SeqCst)
159+ }
160+
157161 fn status(&self) -> &SharedObservable<SharedPaginationStatus> {
158162 &self.shared_pagination_status
159163 }
@@ -308,7 +312,12 @@ impl PaginatedCache for Arc<RoomEventCacheInner> {
308312 events: Vec<Event>,
309313 timeline_event_diffs: Vec<VectorDiff<Event>>,
310314 reached_start: bool,
311- ) -> BackPaginationOutcome {
315+ generation: u64,
316+ ) -> Result<Option<BackPaginationOutcome>> {
317+ let _state = self.state.read().await?;
318+ if self.generation.load(std::sync::atomic::Ordering::SeqCst) != generation {
319+ return Ok(None);
320+ }
312321 if !timeline_event_diffs.is_empty() {
313322 self.update_sender.send(
314323 RoomEventCacheUpdate::UpdateTimelineEvents(TimelineVectorDiffs {
@@ -319,12 +328,12 @@ impl PaginatedCache for Arc<RoomEventCacheInner> {
319328 );
320329 }
321330
322- BackPaginationOutcome {
331+ Ok(Some(BackPaginationOutcome {
323332 reached_start,
324333 // This is a backwards pagination. `BackPaginationOutcome` expects events to
325334 // be in “reverse order”.
326335 events: events.into_iter().rev().collect(),
327- }
336+ }))
328337 }
329338
330339 async fn conclude_backwards_pagination_from_network(
@@ -332,8 +341,12 @@ impl PaginatedCache for Arc<RoomEventCacheInner> {
332341 events: Vec<Event>,
333342 prev_token: Option<String>,
334343 mut new_token: Option<String>,
344+ generation: u64,
335345 ) -> Result<Option<BackPaginationOutcome>> {
336346 let mut state = self.state.write().await?;
347+ if self.generation.load(std::sync::atomic::Ordering::SeqCst) != generation {
348+ return Ok(None);
349+ }
337350
338351 // Check that the previous token still exists; otherwise it's a sign that the
339352 // room's timeline has been cleared.
Added file: crates/matrix-sdk/src/event_cache/caches/room/reseed.md+24 additions
@@ -0,0 +1,24 @@
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 loaded thread timelines. It does
13+not establish continuity with discarded history. Account credentials, encryption
14+state, and pending outgoing messages are not cleared.
15+
16+Once started, the replacement finishes even if the caller drops its future.
17+Failure to persist the replacement leaves the prior room timeline intact.
18+Post-processing can fail after the replacement is installed, in which case the
19+new timeline is still published and the error is returned. Completion means the
20+cache update has been emitted, not that an application mirror has consumed it.
21+
22+Callers must coordinate live sync and requests that fetched this window. Sync
23+updates are serialized against replacement but may append normally afterward.
24+In-flight older pagination is fenced against timeline replacement.
Modified file: crates/matrix-sdk/src/event_cache/caches/room/state.rs+25 additions
@@ -772,6 +772,31 @@ impl<'a> RoomEventCacheStateLockWriteGuard<'a> {
772772 Ok(diff_updates)
773773 }
774774
775+ pub async fn reseed(
776+ &mut self,
777+ mut events: Vec<Event>,
778+ prev_batch: Option<String>,
779+ ) -> Result<(), EventCacheError> {
780+ let mut event_ids = HashSet::new();
781+ events.retain(|event| event.event_id().is_none_or(|id| event_ids.insert(id)));
782+ let mut replacement = EventLinkedChunk::new();
783+ replacement.reset();
784+ replacement.push_live_events(prev_batch.clone().map(|token| Gap { token }), &events);
785+ let updates = replacement.store_updates().take();
786+ self.send_updates_to_store(updates).await?;
787+ let _ = replacement.updates_as_vector_diffs();
788+ self.state.room_linked_chunk = replacement;
789+ self.state.waited_for_initial_prev_token = true;
790+ Ok(())
791+ }
792+
793+ pub async fn clear_loaded_threads(&mut self) -> Result<(), EventCacheError> {
794+ for thread in self.state.threads.values_mut() {
795+ thread.clear().await?;
796+ }
797+ Ok(())
798+ }
799+
775800 async fn reset_internal(&mut self) -> Result<(), EventCacheError> {
776801 self.state.room_linked_chunk.reset();
777802
Modified file: crates/matrix-sdk/src/event_cache/caches/thread/pagination.rs+3 additions−1 deletions
@@ -170,7 +170,8 @@ impl PaginatedCache for ThreadEventCacheWrapper {
170170 _events: Vec<Event>,
171171 _timeline_event_diffs: Vec<VectorDiff<Event>>,
172172 _reached_start: bool,
173- ) -> BackPaginationOutcome {
173+ _generation: u64,
174+ ) -> Result<Option<BackPaginationOutcome>> {
174175 unimplemented!("loading from disk for threads is not implemented yet");
175176 }
176177
@@ -179,6 +180,7 @@ impl PaginatedCache for ThreadEventCacheWrapper {
179180 mut events: Vec<Event>,
180181 prev_token: Option<String>,
181182 new_token: Option<String>,
183+ _generation: u64,
182184 ) -> Result<Option<BackPaginationOutcome>> {
183185 let Some(room) = self.cache.weak_room.get() else {
184186 // The client is shutting down.
Added file: crates/matrix-sdk/tests/history_reseed.rs+262 additions
@@ -0,0 +1,262 @@
1+use std::{
2+ sync::{Arc, atomic::Ordering},
3+ time::Duration,
4+};
5+
6+use eyeball_im::VectorDiff;
7+use matrix_sdk::{
8+ config::StoreConfig,
9+ event_cache::{PaginationStatus, RoomEventCacheUpdate},
10+ room::MessagesOptions,
11+ ruma::{event_id, room_id},
12+ test_utils::mocks::{MatrixMockServer, RoomMessagesResponseTemplate},
13+};
14+use matrix_sdk_base::{event_cache::store::EventCacheStore, linked_chunk::LinkedChunkId};
15+use matrix_sdk_common::cross_process_lock::CrossProcessLockConfig;
16+use matrix_sdk_test::{ALICE, event_factory::EventFactory};
17+
18+#[path = "history_reseed/failing_store.rs"]
19+mod failing_store;
20+
21+#[tokio::test]
22+async fn reseed_publishes_one_reset_and_preserves_the_fetched_cursor() {
23+ let server = MatrixMockServer::new().await;
24+ let client = server.client_builder().build().await;
25+ let room_id = room_id!("!reseed:localhost");
26+ client.event_cache().subscribe().unwrap();
27+ server.sync_joined_room(&client, room_id).await;
28+ let room = client.get_room(room_id).unwrap();
29+ let (cache, _handles) = room.event_cache().await.unwrap();
30+ let (_, mut updates) = cache.subscribe().await.unwrap();
31+ let factory = EventFactory::new().room(room_id).sender(&ALICE);
32+ cache
33+ .reseed(vec![factory.text_msg("old").into_event()], Some("old-cursor".into()))
34+ .await
35+ .unwrap();
36+ updates.recv().await.unwrap();
37+ let recent_id = event_id!("$recent:localhost");
38+ let older_id = event_id!("$older:localhost");
39+ server
40+ .mock_room_messages()
41+ .ok(RoomMessagesResponseTemplate::default()
42+ .events(vec![
43+ factory.text_msg("recent").event_id(recent_id),
44+ factory.text_msg("older").event_id(older_id),
45+ factory.text_msg("older").event_id(older_id),
46+ ])
47+ .end_token("fetched-older-cursor"))
48+ .mock_once()
49+ .mount()
50+ .await;
51+ let mut history = room.messages(MessagesOptions::backward()).await.unwrap();
52+ history.chunk.reverse();
53+ cache.reseed(history.chunk, history.end).await.unwrap();
54+ let update =
55+ tokio::time::timeout(Duration::from_secs(2), updates.recv()).await.unwrap().unwrap();
56+ let RoomEventCacheUpdate::UpdateTimelineEvents(update) = update else {
57+ panic!("expected reset")
58+ };
59+ assert_eq!(update.diffs.len(), 1);
60+ let VectorDiff::Reset { values } = &update.diffs[0] else { panic!("expected reset") };
61+ assert_eq!(values.len(), 2);
62+ assert_eq!(values[0].event_id().as_deref(), Some(older_id));
63+ assert_eq!(values[1].event_id().as_deref(), Some(recent_id));
64+ assert!(updates.is_empty());
65+ assert_eq!(cache.events().await.unwrap().len(), 2);
66+ server
67+ .mock_room_messages()
68+ .match_from("fetched-older-cursor")
69+ .ok(RoomMessagesResponseTemplate::default())
70+ .mock_once()
71+ .mount()
72+ .await;
73+ assert!(cache.pagination().run_backwards_once(10).await.unwrap().reached_start);
74+}
75+
76+#[tokio::test]
77+async fn reseed_empty_history_distinguishes_a_cursor_from_the_start_of_history() {
78+ let server = MatrixMockServer::new().await;
79+ let client = server.client_builder().build().await;
80+ let room_id = room_id!("!reseed:localhost");
81+ client.event_cache().subscribe().unwrap();
82+ server.sync_joined_room(&client, room_id).await;
83+ let (cache, _handles) = client.get_room(room_id).unwrap().event_cache().await.unwrap();
84+ cache.reseed(Vec::new(), None).await.unwrap();
85+ assert_eq!(
86+ cache.pagination().status().get(),
87+ PaginationStatus::Idle { hit_timeline_start: true }
88+ );
89+ assert!(cache.pagination().run_backwards_once(10).await.unwrap().reached_start);
90+ cache.reseed(Vec::new(), Some("empty-page-cursor".into())).await.unwrap();
91+ assert_eq!(
92+ cache.pagination().status().get(),
93+ PaginationStatus::Idle { hit_timeline_start: false }
94+ );
95+ server
96+ .mock_room_messages()
97+ .match_from("empty-page-cursor")
98+ .ok(RoomMessagesResponseTemplate::default())
99+ .mock_once()
100+ .mount()
101+ .await;
102+ assert!(cache.pagination().run_backwards_once(10).await.unwrap().reached_start);
103+}
104+
105+#[tokio::test]
106+async fn reseed_fences_inflight_tokenless_pagination() {
107+ let server = MatrixMockServer::new().await;
108+ let client = server.client_builder().build().await;
109+ let room_id = room_id!("!reseed:localhost");
110+ client.event_cache().subscribe().unwrap();
111+ server.sync_joined_room(&client, room_id).await;
112+ let (cache, _handles) = client.get_room(room_id).unwrap().event_cache().await.unwrap();
113+ let factory = EventFactory::new().room(room_id).sender(&ALICE);
114+ let stale = factory.text_msg("stale request");
115+ server
116+ .mock_room_messages()
117+ .ok(RoomMessagesResponseTemplate::default()
118+ .events(vec![stale])
119+ .with_delay(Duration::from_millis(300)))
120+ .mock_once()
121+ .mount()
122+ .await;
123+ let pagination = tokio::spawn({
124+ let cache = cache.clone();
125+ async move { cache.pagination().run_backwards_once(10).await }
126+ });
127+ tokio::time::timeout(Duration::from_secs(8), async {
128+ loop {
129+ if server
130+ .received_requests()
131+ .await
132+ .unwrap()
133+ .iter()
134+ .any(|r| r.url.path().ends_with("/messages"))
135+ {
136+ break;
137+ }
138+ tokio::task::yield_now().await;
139+ }
140+ })
141+ .await
142+ .unwrap();
143+ let recent_id = event_id!("$fresh:localhost");
144+ cache
145+ .reseed(vec![factory.text_msg("fresh").event_id(recent_id).into_event()], None)
146+ .await
147+ .unwrap();
148+ tokio::time::timeout(Duration::from_secs(3), pagination).await.unwrap().unwrap().unwrap();
149+ let events = cache.events().await.unwrap();
150+ assert_eq!(events.len(), 1);
151+ assert_eq!(events[0].event_id().as_deref(), Some(recent_id));
152+ assert_eq!(
153+ cache.pagination().status().get(),
154+ PaginationStatus::Idle { hit_timeline_start: true }
155+ );
156+}
157+
158+#[tokio::test]
159+async fn reseed_preserves_timeline_when_store_write_fails() {
160+ let server = MatrixMockServer::new().await;
161+ let store = Arc::new(failing_store::FailingStore::default());
162+ let client = server
163+ .client_builder()
164+ .on_builder(|builder| {
165+ builder.store_config(
166+ StoreConfig::new(CrossProcessLockConfig::SingleProcess)
167+ .event_cache_store(store.clone()),
168+ )
169+ })
170+ .build()
171+ .await;
172+ let room_id = room_id!("!reseed:localhost");
173+ client.event_cache().subscribe().unwrap();
174+ server.sync_joined_room(&client, room_id).await;
175+ let (cache, _handles) = client.get_room(room_id).unwrap().event_cache().await.unwrap();
176+ let factory = EventFactory::new().room(room_id).sender(&ALICE);
177+ let original_id = event_id!("$original:localhost");
178+ cache
179+ .reseed(
180+ vec![factory.text_msg("original").event_id(original_id).into_event()],
181+ Some("original-cursor".into()),
182+ )
183+ .await
184+ .unwrap();
185+ let (original_events, updates) = cache.subscribe().await.unwrap();
186+ assert_eq!(original_events.len(), 1);
187+ assert_eq!(original_events[0].event_id().as_deref(), Some(original_id));
188+ let original_chunks = store.inner.load_all_chunks(LinkedChunkId::Room(room_id)).await.unwrap();
189+
190+ store.fail_writes.store(true, Ordering::SeqCst);
191+ let result = cache
192+ .reseed(
193+ vec![
194+ factory
195+ .text_msg("replacement")
196+ .event_id(event_id!("$replacement:localhost"))
197+ .into_event(),
198+ ],
199+ Some("replacement-cursor".into()),
200+ )
201+ .await;
202+ store.fail_writes.store(false, Ordering::SeqCst);
203+
204+ assert!(result.is_err());
205+ assert!(updates.is_empty());
206+ let current_events = cache.events().await.unwrap();
207+ assert_eq!(current_events.len(), 1);
208+ assert_eq!(current_events[0].event_id().as_deref(), Some(original_id));
209+ let current_chunks = store.inner.load_all_chunks(LinkedChunkId::Room(room_id)).await.unwrap();
210+ assert_eq!(format!("{current_chunks:?}"), format!("{original_chunks:?}"));
211+}
212+
213+#[cfg(feature = "sqlite")]
214+#[tokio::test]
215+async fn reseed_persists_the_window_and_cursor_across_client_recreation() {
216+ let server = MatrixMockServer::new().await;
217+ let directory = tempfile::tempdir().unwrap();
218+ let room_id = room_id!("!reseed:localhost");
219+ let recent_id = event_id!("$recent:localhost");
220+ {
221+ let client = server
222+ .client_builder()
223+ .on_builder(|b| b.sqlite_store(directory.path(), None))
224+ .build()
225+ .await;
226+ client.event_cache().subscribe().unwrap();
227+ server.sync_joined_room(&client, room_id).await;
228+ let (cache, _handles) = client.get_room(room_id).unwrap().event_cache().await.unwrap();
229+ cache
230+ .reseed(
231+ vec![
232+ EventFactory::new()
233+ .room(room_id)
234+ .sender(&ALICE)
235+ .text_msg("recent")
236+ .event_id(recent_id)
237+ .into_event(),
238+ ],
239+ Some("persisted-cursor".into()),
240+ )
241+ .await
242+ .unwrap();
243+ }
244+ let client = server
245+ .client_builder()
246+ .on_builder(|b| b.sqlite_store(directory.path(), None))
247+ .build()
248+ .await;
249+ client.event_cache().subscribe().unwrap();
250+ let (cache, _handles) = client.get_room(room_id).unwrap().event_cache().await.unwrap();
251+ let (events, _subscriber) = cache.subscribe().await.unwrap();
252+ assert_eq!(events.len(), 1);
253+ assert_eq!(events[0].event_id().as_deref(), Some(recent_id));
254+ server
255+ .mock_room_messages()
256+ .match_from("persisted-cursor")
257+ .ok(RoomMessagesResponseTemplate::default())
258+ .mock_once()
259+ .mount()
260+ .await;
261+ assert!(cache.pagination().run_backwards_once(10).await.unwrap().reached_start);
262+}
Added file: crates/matrix-sdk/tests/history_reseed/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+}