atagen

matrix-rust-sdk

public
Star0
Watch0

Commit 3c85630430e1

tarball

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

6 files changed+141-52
event-cache: keep pinned and event-focused views alive across a clear

clear_loaded_views dropped the PinnedEventCache and drained the EventFocusedCache map, so an open pinned timeline got a Clear and then a closed stream and an event-focused one got MissingCache on its next paginate. Both now empty in place like threads and refill themselves, the pinned cache by restarting its listener and the focused cache by running its /context start again behind the generation fence. A pagination fenced by a clear no longer claims hit_end_of_timeline, and the timeline Lagged paths swap in the receiver they resubscribed with.
Modified file: crates/matrix-sdk-ui/src/timeline/tasks.rs+14 additions−12 deletions
@@ -56,18 +56,19 @@ pub(in crate::timeline) async fn pinned_events_task(
5656 // The updates might have lagged, but the room event cache might have
5757 // events, so retrieve them and add them back again to the timeline,
5858 // after clearing it.
59- let (initial_events, _) = match room_event_cache.subscribe_to_pinned_events().await
60- {
61- Ok(initial_events) => initial_events,
62- Err(err) => {
63- error!(
64- ?err,
65- "Failed to replace the initial remote events in the event cache"
66- );
67- break;
68- }
69- };
59+ let (initial_events, recv) =
60+ match room_event_cache.subscribe_to_pinned_events().await {
61+ Ok(initial_events) => initial_events,
62+ Err(err) => {
63+ error!(
64+ ?err,
65+ "Failed to replace the initial remote events in the event cache"
66+ );
67+ break;
68+ }
69+ };
7070
71+ pinned_events_recv = recv;
7172 timeline_controller
7273 .replace_with_initial_remote_events(initial_events, RemoteEventOrigin::Cache)
7374 .await;
@@ -130,7 +131,8 @@ pub(in crate::timeline) async fn event_focused_task(
130131 }
131132 };
132133
133- let (initial_events, _) = cache.subscribe().await;
134+ let (initial_events, recv) = cache.subscribe().await;
135+ event_focused_events_recv = recv;
134136
135137 timeline_controller
136138 .replace_with_initial_remote_events(initial_events, RemoteEventOrigin::Cache)
Modified file: crates/matrix-sdk/src/event_cache/caches/event_focused/mod.rs+46 additions−14 deletions
@@ -46,7 +46,7 @@ use tokio::sync::{
4646 RwLock,
4747 broadcast::{Receiver, Sender},
4848 };
49-use tracing::{instrument, trace};
49+use tracing::{instrument, trace, warn};
5050
5151 #[cfg(feature = "e2e-encryption")]
5252 use crate::event_cache::redecryptor::ResolvedUtd;
@@ -108,6 +108,9 @@ struct EventFocusedCacheInner {
108108 /// The pagination mode (room or thread).
109109 pagination_mode: EventFocusedPaginationMode,
110110
111+ /// How the cache was started, so a clear can start it again.
112+ context: Option<(u16, EventFocusThreadMode)>,
113+
111114 /// The linked chunk for this event-focused cache.
112115 chunk: EventLinkedChunk,
113116
@@ -145,14 +148,28 @@ impl EventFocusedCacheInner {
145148 room: Room,
146149 num_context_events: u16,
147150 thread_mode: EventFocusThreadMode,
148- ) -> Result<StartFromResult> {
151+ ) -> Result<Option<StartFromResult>> {
149152 trace!(num_context_events, "fetching event with context via /context");
150153
154+ let invalidation_lock = self.invalidation_lock.clone();
155+ let generation = {
156+ let _guard = invalidation_lock.lock().unwrap();
157+ self.synchronize_generation();
158+ self.loaded_generation
159+ };
160+
151161 let paginator = Paginator::new(room);
152162
153163 let result =
154164 paginator.start_from(&self.focused_event_id, UInt::from(num_context_events)).await?;
155165
166+ let _invalidation_guard = invalidation_lock.lock().unwrap();
167+ if generation != self.generation.load(Ordering::SeqCst) {
168+ self.synchronize_generation();
169+ return Ok(None);
170+ }
171+ self.context = Some((num_context_events, thread_mode));
172+
156173 // Detect if the focused event is part of a thread.
157174 let thread_root = match thread_mode {
158175 EventFocusThreadMode::ForceThread => {
@@ -253,16 +270,11 @@ impl EventFocusedCacheInner {
253270
254271 self.propagate_changes();
255272
256- // Empty the updates_as_vector_diffs(), since it's impossible for an observer to
257- // have subscribed to this cache yet, since this code is part of the constructor
258- // flow.
259- //
260- // If we didn't empty those, such initial updates would be duplicated, since the
261- // subscriber would get the full initial list of events as diffs and as a set of
262- // initial events.
263- let _ = self.chunk.updates_as_vector_diffs();
273+ // Draining the diffs keeps a later subscriber from seeing the initial events
274+ // twice, and refills subscribers that saw a clear.
275+ self.notify_subscribers(EventsOrigin::Cache);
264276
265- Ok(result)
277+ Ok(Some(result))
266278 }
267279
268280 /// Add initial events to the chunk, with gaps for pagination tokens.
@@ -357,7 +369,7 @@ impl EventFocusedCacheInner {
357369 let _invalidation_guard = invalidation_lock.lock().unwrap();
358370 if generation != self.generation.load(Ordering::SeqCst) {
359371 self.synchronize_generation();
360- return Ok(PaginationResult { events: Vec::new(), hit_end_of_timeline: true });
372+ return Ok(PaginationResult { events: Vec::new(), hit_end_of_timeline: false });
361373 }
362374
363375 let (mut events, new_token) = pagination?;
@@ -484,7 +496,7 @@ impl EventFocusedCacheInner {
484496 let _invalidation_guard = invalidation_lock.lock().unwrap();
485497 if generation != self.generation.load(Ordering::SeqCst) {
486498 self.synchronize_generation();
487- return Ok(PaginationResult { events: Vec::new(), hit_end_of_timeline: true });
499+ return Ok(PaginationResult { events: Vec::new(), hit_end_of_timeline: false });
488500 }
489501
490502 let (events, new_token) = pagination?;
@@ -599,6 +611,7 @@ impl EventFocusedCache {
599611 room,
600612 focused_event_id,
601613 pagination_mode: EventFocusedPaginationMode::Room { hide_thread_events: false },
614+ context: None,
602615 chunk: EventLinkedChunk::new(),
603616 sender: sender.clone(),
604617 linked_chunk_update_sender: linked_chunk_update_sender.clone(),
@@ -611,7 +624,26 @@ impl EventFocusedCache {
611624 }
612625 }
613626
627+ /// Empty this cache, keeping its subscribers, and start it again from its
628+ /// focused event in the background.
614629 pub(super) fn clear(&self) {
630+ self.invalidate();
631+ let cache = self.clone();
632+ matrix_sdk_common::executor::spawn(async move {
633+ let mut inner = cache.inner.write().await;
634+ let Some((num_context_events, thread_mode)) = inner.context else {
635+ return;
636+ };
637+ let Some(room) = inner.room.get() else {
638+ return;
639+ };
640+ if let Err(error) = inner.start_from(room, num_context_events, thread_mode).await {
641+ warn!(%error, "failed to restart the event-focused cache after a clear");
642+ }
643+ });
644+ }
645+
646+ pub(super) fn invalidate(&self) {
615647 let _guard = self.invalidation_lock.lock().unwrap();
616648 self.generation.fetch_add(1, Ordering::SeqCst);
617649 let _ = self.sender.send(TimelineVectorDiffs {
@@ -660,7 +692,7 @@ impl EventFocusedCache {
660692 room: Room,
661693 num_context_events: u16,
662694 thread_mode: EventFocusThreadMode,
663- ) -> Result<StartFromResult> {
695+ ) -> Result<Option<StartFromResult>> {
664696 self.inner.write().await.start_from(room, num_context_events, thread_mode).await
665697 }
666698
Modified file: crates/matrix-sdk/src/event_cache/caches/pinned_events/mod.rs+30 additions−14 deletions
@@ -12,7 +12,11 @@
1212 // See the License for the specific language governing permissions and
1313 // limitations under the License.
1414
15-use std::{cmp::Ordering, collections::BTreeSet, sync::Arc};
15+use std::{
16+ cmp::Ordering,
17+ collections::BTreeSet,
18+ sync::{Arc, Mutex},
19+};
1620
1721 use futures_util::{StreamExt as _, stream};
1822 use matrix_sdk_base::{
@@ -209,9 +213,11 @@ impl PinnedEventCacheState {
209213 pub struct PinnedEventCache {
210214 state: Arc<PinnedEventCacheStateLock>,
211215
216+ room: WeakRoom,
217+
212218 /// The task handling the refreshing of pinned events for this specific
213219 /// room.
214- _task: Arc<BackgroundTaskHandle>,
220+ task: Arc<Mutex<BackgroundTaskHandle>>,
215221 }
216222
217223 impl PinnedEventCache {
@@ -236,18 +242,21 @@ impl PinnedEventCache {
236242 store,
237243 };
238244 let state = Arc::new(PinnedEventCacheStateLock::new_inner(state));
245+ let weak_room =
246+ WeakRoom::new(WeakClient::from_client(&room.client()), room.room_id().to_owned());
247+ let task = Arc::new(Mutex::new(Self::spawn_listener(room, state.clone())));
239248
240- let task = Arc::new(
241- room.client()
242- .task_monitor()
243- .spawn_infinite_task(
244- "pinned_event_listener_task",
245- Self::pinned_event_listener_task(room, state.clone()),
246- )
247- .abort_on_drop(),
248- );
249-
250- Self { state, _task: task }
249+ Self { state, room: weak_room, task }
250+ }
251+
252+ fn spawn_listener(room: Room, state: Arc<PinnedEventCacheStateLock>) -> BackgroundTaskHandle {
253+ room.client()
254+ .task_monitor()
255+ .spawn_infinite_task(
256+ "pinned_event_listener_task",
257+ Self::pinned_event_listener_task(room, state),
258+ )
259+ .abort_on_drop()
251260 }
252261
253262 /// Subscribe to live events from this room's pinned events cache.
@@ -260,13 +269,20 @@ impl PinnedEventCache {
260269 Ok((events, recv))
261270 }
262271
272+ /// Empty this cache, keeping its subscribers, and reload the pinned events
273+ /// as a fresh listener would.
263274 pub(super) async fn clear(&self) -> Result<()> {
264- self._task.abort();
275+ self.task.lock().unwrap().abort();
265276 let mut guard = self.state.write().await?;
266277 guard.state.generation += 1;
267278 guard.state.chunk.reset();
268279 guard.propagate_changes().await?;
269280 guard.notify_subscribers(EventsOrigin::Cache);
281+ drop(guard);
282+
283+ if let Some(room) = self.room.get() {
284+ *self.task.lock().unwrap() = Self::spawn_listener(room, self.state.clone());
285+ }
270286
271287 Ok(())
272288 }
Modified file: crates/matrix-sdk/src/event_cache/caches/room/mod.rs+1 additions−1 deletions
@@ -231,7 +231,7 @@ impl RoomEventCache {
231231
232232 let mut guard = self.inner.state.write().await?;
233233 if self.inner.generation.load(Ordering::SeqCst) != generation {
234- cache.clear();
234+ cache.invalidate();
235235 continue;
236236 }
237237
Modified file: crates/matrix-sdk/src/event_cache/caches/room/state.rs+2 additions−2 deletions
@@ -862,10 +862,10 @@ impl<'a> RoomEventCacheStateLockWriteGuard<'a> {
862862 }
863863
864864 pub async fn clear_loaded_views(&mut self) -> Result<(), EventCacheError> {
865- for (_, focused) in self.state.event_focused_caches.drain() {
865+ for focused in self.state.event_focused_caches.values() {
866866 focused.clear();
867867 }
868- if let Some(pinned) = self.state.pinned_event_cache.take() {
868+ if let Some(pinned) = self.state.pinned_event_cache.get() {
869869 pinned.clear().await?;
870870 }
871871 for thread in self.state.threads.values_mut() {
Modified file: crates/matrix-sdk/tests/room_clear.rs+48 additions−9 deletions
@@ -12,7 +12,7 @@ use matrix_sdk::{
1212 use matrix_sdk_test::{ALICE, JoinedRoomBuilder, event_factory::EventFactory};
1313
1414 #[tokio::test]
15-async fn clear_invalidates_focused_cache_clones_and_inflight_pagination() {
15+async fn clear_restarts_focused_cache_and_fences_inflight_pagination() {
1616 let server = MatrixMockServer::new().await;
1717 let client = server.client_builder().build().await;
1818 let room_id = room_id!("!clear:localhost");
@@ -76,17 +76,45 @@ async fn clear_invalidates_focused_cache_clones_and_inflight_pagination() {
7676 .await
7777 .unwrap();
7878
79+ server
80+ .mock_room_event_context()
81+ .ok(RoomContextResponseTemplate::new(
82+ factory.text_msg("focused").event_id(focused_id).into_event(),
83+ )
84+ .start("fresh-focused-cursor"))
85+ .mock_once()
86+ .mount()
87+ .await;
7988 tokio::time::timeout(Duration::from_secs(2), cache.clear()).await.unwrap().unwrap();
8089 let clear =
8190 tokio::time::timeout(Duration::from_secs(2), updates.recv()).await.unwrap().unwrap();
8291 assert!(matches!(clear.diffs.as_slice(), [VectorDiff::Clear]));
83- tokio::time::timeout(Duration::from_secs(3), pagination).await.unwrap().unwrap().unwrap();
92+ let interrupted =
93+ tokio::time::timeout(Duration::from_secs(3), pagination).await.unwrap().unwrap().unwrap();
94+ assert!(interrupted.events.is_empty());
95+ assert!(!interrupted.hit_end_of_timeline);
96+
97+ let mut visible = imbl::Vector::new();
98+ tokio::time::timeout(Duration::from_secs(3), async {
99+ while visible.is_empty() {
100+ for diff in updates.recv().await.unwrap().diffs {
101+ diff.apply(&mut visible);
102+ }
103+ }
104+ })
105+ .await
106+ .unwrap();
107+ let ids: Vec<_> = visible.iter().filter_map(|event| event.event_id()).collect();
108+ assert_eq!(ids, vec![focused_id]);
84109 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());
110+ assert_eq!(cloned_events.len(), 1);
111+ assert!(
112+ cache
113+ .get_event_focused_cache(focused_id.to_owned(), EventFocusThreadMode::Automatic)
114+ .await
115+ .unwrap()
116+ .is_some()
117+ );
90118 }
91119
92120 #[tokio::test]
@@ -131,13 +159,13 @@ async fn clear_empties_subscribed_thread_cache() {
131159 }
132160
133161 #[tokio::test]
134-async fn clear_empties_loaded_pinned_cache() {
162+async fn clear_reloads_loaded_pinned_cache() {
135163 let server = MatrixMockServer::new().await;
136164 let room_id = room_id!("!clear:localhost");
137165 let pinned_event_id = event_id!("$pinned:localhost");
138166 let factory = EventFactory::new().room(room_id).sender(&ALICE);
139167 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;
168+ server.mock_room_event().match_event_id().ok(pinned_event).mount().await;
141169 let client = server.client_builder().build().await;
142170 client.event_cache().subscribe().unwrap();
143171 let room = server
@@ -171,4 +199,15 @@ async fn clear_empties_loaded_pinned_cache() {
171199 diff.apply(&mut visible);
172200 }
173201 assert!(visible.is_empty());
202+ tokio::time::timeout(Duration::from_secs(3), async {
203+ while visible.is_empty() {
204+ for diff in updates.recv().await.unwrap().diffs {
205+ diff.apply(&mut visible);
206+ }
207+ }
208+ })
209+ .await
210+ .unwrap();
211+ assert_eq!(visible.len(), 1);
212+ assert_eq!(visible[0].event_id().as_deref(), Some(pinned_event_id));
174213 }