Skip to main content

matrix_sdk/event_cache/caches/
mod.rs

1// Copyright 2026 The Matrix.org Foundation C.I.C.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use std::{collections::HashMap, ops::Not, sync::Arc};
16
17use eyeball::SharedObservable;
18use eyeball_im::VectorDiff;
19use matrix_sdk_base::{
20    ThreadingSupport,
21    event_cache::Event,
22    linked_chunk::Position,
23    sync::{JoinedRoomUpdate, LeftRoomUpdate},
24};
25use ruma::{OwnedEventId, RoomId, room_version_rules::RoomVersionRules};
26use tokio::sync::{
27    OnceCell, OwnedRwLockReadGuard, OwnedRwLockWriteGuard, RwLock, broadcast::Sender, mpsc,
28};
29
30use self::subscriber::AutoShrinkMessage;
31use super::{
32    EventCacheError, EventsOrigin, Result, back_pagination_queue::BackPaginationQueue, states,
33};
34use crate::{client::WeakClient, room::WeakRoom};
35
36mod aggregator;
37pub mod event_focused;
38pub mod event_linked_chunk;
39pub mod pagination;
40pub mod pinned_events;
41mod read_receipts;
42pub mod room;
43pub mod subscriber;
44pub mod thread;
45
46/// A type to hold all the caches for a given room.
47#[derive(Debug)]
48pub(super) struct Caches {
49    /// The one and only [`RoomEventCache`].
50    ///
51    /// [`RoomEventCache`]: room::RoomEventCache
52    pub room: room::RoomEventCache,
53
54    /// All the lazily-loaded [`ThreadEventCache`].
55    ///
56    /// [`ThreadEventCache`]: thread::ThreadEventCache
57    // An `Arc` is used to get an owned lock.
58    pub threads: Arc<RwLock<HashMap<OwnedEventId, thread::ThreadEventCache>>>,
59
60    /// The one and only [`PinnedEventsCache`].
61    ///
62    /// [`PinnedEventsCache`]: pinned_events::PinnedEventsCache
63    pub pinned_events: OnceCell<pinned_events::PinnedEventsCache>,
64
65    /// All the lazily-loaded [`EventFocusedCache`].
66    ///
67    /// [`EventFocusedCache`]: event_focused::EventFocusedCache
68    // An `Arc` is used to get an owned lock.
69    pub event_focused:
70        Arc<RwLock<HashMap<event_focused::EventFocusedCacheKey, event_focused::EventFocusedCache>>>,
71
72    /// Internals data, used to lazily create caches.
73    internals: CachesInternals,
74}
75
76#[derive(Debug)]
77struct CachesInternals {
78    state: states::StateLock,
79    auto_shrink_sender: mpsc::Sender<AutoShrinkMessage>,
80    linked_chunk_update_sender: Sender<room::RoomEventCacheLinkedChunkUpdate>,
81    room_version_rules: RoomVersionRules,
82}
83
84impl Caches {
85    /// Create a new [`Caches`].
86    pub async fn new(
87        weak_client: &WeakClient,
88        room_id: &RoomId,
89        generic_update_sender: Sender<room::RoomEventCacheGenericUpdate>,
90        linked_chunk_update_sender: Sender<room::RoomEventCacheLinkedChunkUpdate>,
91        auto_shrink_sender: mpsc::Sender<AutoShrinkMessage>,
92        state: &states::StateLock,
93        back_pagination_queue: Option<BackPaginationQueue>,
94    ) -> Result<Self> {
95        let Some(client) = weak_client.get() else {
96            return Err(EventCacheError::ClientDropped);
97        };
98
99        let weak_room = WeakRoom::new(weak_client.clone(), room_id.to_owned());
100
101        let room = client
102            .get_room(room_id)
103            .ok_or_else(|| EventCacheError::RoomNotFound { room_id: room_id.to_owned() })?;
104        let room_version_rules = room.clone_info().room_version_rules_or_default();
105
106        let pagination_status = SharedObservable::new(pagination::SharedPaginationStatus::Idle {
107            hit_timeline_start: false,
108        });
109
110        let enabled_thread_support =
111            matches!(client.base_client().threading_support, ThreadingSupport::Enabled { .. });
112
113        let update_sender = room::RoomEventCacheUpdateSender::new(generic_update_sender.clone());
114
115        let own_user_id =
116            client.user_id().expect("the user must be logged in, at this point").to_owned();
117
118        // Computed while building the state: nothing may await once the state
119        // is inserted, or a cancellation or error would leave it
120        // inserted without its `Caches`, making every later attempt
121        // fail.
122        let mut timeline_is_not_empty = false;
123
124        let room_state = state
125            .try_insert_once_with(
126                states::selectors::RoomStateSelector::new(room_id.to_owned()),
127                async |store_guard| {
128                    let room_state = room::RoomEventCacheState::new(
129                        own_user_id.clone(),
130                        room_id.to_owned(),
131                        weak_room.clone(),
132                        room_version_rules.clone(),
133                        enabled_thread_support,
134                        update_sender.clone(),
135                        linked_chunk_update_sender.clone(),
136                        store_guard,
137                        pagination_status.clone(),
138                        back_pagination_queue,
139                    )
140                    .await?;
141
142                    timeline_is_not_empty =
143                        room_state.room_linked_chunk().revents().next().is_some();
144
145                    Ok(room_state)
146                },
147            )
148            .await?;
149
150        let room_event_cache = room::RoomEventCache::new(
151            room_id.to_owned(),
152            weak_room,
153            own_user_id,
154            room_state,
155            pagination_status,
156            auto_shrink_sender.clone(),
157            update_sender,
158        );
159
160        // If at least one event has been loaded, it means there is a timeline.
161        // Let's emit a generic update.
162        if timeline_is_not_empty {
163            let _ = generic_update_sender
164                .send(room::RoomEventCacheGenericUpdate { room_id: room_id.to_owned() });
165        }
166
167        Ok(Self {
168            room: room_event_cache,
169            threads: Arc::new(RwLock::new(HashMap::new())),
170            pinned_events: OnceCell::new(),
171            event_focused: Arc::new(RwLock::new(HashMap::new())),
172            internals: CachesInternals {
173                state: state.clone(),
174                auto_shrink_sender,
175                linked_chunk_update_sender,
176                room_version_rules,
177            },
178        })
179    }
180
181    /// Get the [`RoomEventCache`].
182    ///
183    /// [`RoomEventCache`]: room::RoomEventCache
184    pub fn room(&self) -> &room::RoomEventCache {
185        &self.room
186    }
187
188    /// Get or create a [`ThreadEventCache`].
189    ///
190    /// Note: it is impossible to know if `thread_id` represents a valid thread
191    /// identifier. It means it's possible to create a [`ThreadEventCache`] for
192    /// an event that is not a thread root.
193    ///
194    /// [`ThreadEventCache`]: thread::ThreadEventCache
195    pub async fn thread(
196        &self,
197        thread_id: OwnedEventId,
198    ) -> Result<
199        OwnedRwLockReadGuard<
200            HashMap<OwnedEventId, thread::ThreadEventCache>,
201            thread::ThreadEventCache,
202        >,
203    > {
204        Ok(
205            match OwnedRwLockWriteGuard::try_downgrade_map(
206                self.threads.clone().write_owned().await,
207                |threads| threads.get(&thread_id),
208            ) {
209                // Thread exists.
210                Ok(locked_cache) => locked_cache,
211                // Thread does not exist, let's create it.
212                Err(mut threads) => {
213                    let room = &self.room;
214                    let cache = thread::ThreadEventCache::new(
215                        room.room_id().to_owned(),
216                        thread_id.clone(),
217                        room.own_user_id().to_owned(),
218                        self.internals.room_version_rules.clone(),
219                        room.weak_room().to_owned(),
220                        &self.internals.state,
221                        self.internals.auto_shrink_sender.clone(),
222                        room.update_sender().generic_update_sender().clone(),
223                        self.internals.linked_chunk_update_sender.clone(),
224                    )
225                    .await?;
226
227                    threads.insert(thread_id.clone(), cache);
228
229                    OwnedRwLockWriteGuard::downgrade_map(threads, |threads| {
230                        threads.get(&thread_id).unwrap()
231                    })
232                }
233            },
234        )
235    }
236
237    /// Get or create a [`PinnedEventsCache`].
238    ///
239    /// [`PinnedEventsCache`]: pinned_events::PinnedEventsCache
240    pub async fn pinned_events(&self) -> Result<&pinned_events::PinnedEventsCache> {
241        self.pinned_events
242            .get_or_try_init(|| {
243                pinned_events::PinnedEventsCache::new(
244                    self.room.weak_room(),
245                    self.room.own_user_id().clone(),
246                    self.internals.room_version_rules.clone(),
247                    self.internals.linked_chunk_update_sender.clone(),
248                    &self.internals.state,
249                )
250            })
251            .await
252    }
253
254    /// Get or create a [`EventFocusedCache`].
255    ///
256    /// [`EventFocusedCache`]: event_focused::EventFocusedCache
257    pub async fn event_focused(
258        &self,
259        event_id: OwnedEventId,
260        thread_mode: event_focused::EventFocusThreadMode,
261        number_of_initial_events: u16,
262    ) -> Result<
263        OwnedRwLockReadGuard<
264            HashMap<event_focused::EventFocusedCacheKey, event_focused::EventFocusedCache>,
265            event_focused::EventFocusedCache,
266        >,
267    > {
268        let key = event_focused::EventFocusedCacheKey { focused_event_id: event_id, thread_mode };
269
270        Ok(
271            match OwnedRwLockWriteGuard::try_downgrade_map(
272                self.event_focused.clone().write_owned().await,
273                |event_focused_caches| event_focused_caches.get(&key),
274            ) {
275                // Event-focused cache exists.
276                Ok(locked_cache) => locked_cache,
277                // Event-focused cache does not exist, let's create it.
278                Err(mut event_focused_caches) => {
279                    let cache = event_focused::EventFocusedCache::new(
280                        self.room.weak_room().clone(),
281                        key.clone(),
282                        &self.internals.state,
283                        self.internals.linked_chunk_update_sender.clone(),
284                    )
285                    .await?;
286                    cache.start_from(number_of_initial_events, thread_mode).await?;
287
288                    event_focused_caches.insert(key.clone(), cache);
289
290                    OwnedRwLockWriteGuard::downgrade_map(
291                        event_focused_caches,
292                        |event_focused_caches| event_focused_caches.get(&key).unwrap(),
293                    )
294                }
295            },
296        )
297    }
298
299    /// Update all the event caches with a [`JoinedRoomUpdate`].
300    pub(super) async fn handle_joined_room_update(&self, updates: JoinedRoomUpdate) -> Result<()> {
301        let Self { room, threads: _, pinned_events, event_focused, internals } = &self;
302
303        // This method will compute a `JoinedRoomUpdate` for each cache. The
304        // game is to avoid cloning useless data or to clone as few data as
305        // possible. That's a fun game.
306        let JoinedRoomUpdate {
307            // Read receipts are computed by the Event Cache, see
308            // [`read_receipts`], we don't need the server value.
309            unread_notifications: _,
310            // State-events are not stored in the Event Cache.
311            state: _,
312
313            // Extract the original timeline and ephemeral events as timeline
314            // will be used by all caches, and ephemeral events by the room and
315            // thread caches.
316            timeline: original_timeline,
317            ephemeral: original_ephemeral,
318
319            // Extract other data, only useful for the room cache.
320            account_data,
321            ambiguity_changes,
322            avatar_changes,
323        } = updates;
324
325        // Filter ephemeral events.
326        let original_ephemeral = original_ephemeral
327            .into_iter()
328            .filter_map(|ephemeral_event| ephemeral_event.deserialize().ok())
329            .collect::<Vec<_>>();
330
331        // Room.
332        {
333            let (timeline, read_receipts) =
334                aggregator::aggregate_timeline_and_read_receipts_for_room(
335                    &original_timeline,
336                    &original_ephemeral,
337                );
338
339            room.handle_joined_room_update(
340                timeline,
341                read_receipts,
342                account_data,
343                ambiguity_changes,
344                avatar_changes,
345            )
346            .await?;
347        }
348
349        // Threads.
350        {
351            let timeline_and_read_receipts_for_threads = {
352                // To aggregate the timelines for threads, we need to lookup in
353                // the room cache and the thread caches. We acquire a read lock
354                // over all the caches, and select the room cache and thread
355                // cache' states.
356                let all_states_lock = states::CacheStateLock::new(
357                    states::selectors::AllStatesSelector::new(room.room_id().to_owned()),
358                    self.internals.state.clone(),
359                );
360                let all_states = all_states_lock.read().await?;
361
362                aggregator::aggregate_timeline_and_read_receipts_for_threads(
363                    &original_timeline,
364                    &original_ephemeral,
365                    all_states.threads(),
366                    all_states.room(),
367                    &internals.room_version_rules.redaction,
368                )
369                .await?
370            };
371
372            for (thread_id, (timeline, read_receipts)) in timeline_and_read_receipts_for_threads {
373                // Update the thread summary if and only if there are new
374                // events.
375                let update_thread_summary = timeline.events.is_empty().not();
376
377                let thread = self.thread(thread_id).await?;
378                thread.handle_joined_room_update(timeline, read_receipts).await?;
379
380                if update_thread_summary {
381                    let new_thread_summary = thread.update_thread_summary().await?;
382
383                    room.update_thread_summary(thread.thread_id(), new_thread_summary)?;
384                }
385            }
386        }
387
388        // Pinned-events.
389        if let Some(pinned_events) = pinned_events.get() {
390            let timeline = aggregator::aggregate_timeline_for_pinned_events(
391                &original_timeline,
392                &pinned_events.state().read().await?.current_event_ids(),
393                &internals.room_version_rules.redaction,
394            );
395
396            pinned_events.handle_joined_room_update(timeline).await?;
397        }
398
399        // Event-focused.
400        {
401            // An event-focused cache isn't listening to live update.
402            // Consequently, it is not interested by this kind of update.
403            let _ = event_focused;
404        }
405
406        Ok(())
407    }
408
409    /// Update all the event caches with a [`LeftRoomUpdate`].
410    pub(super) async fn handle_left_room_update(&self, updates: LeftRoomUpdate) -> Result<()> {
411        let Self { room, threads: _, pinned_events, event_focused, internals } = &self;
412
413        // This method will compute a `JoinedRoomUpdate` for each cache. The
414        // game is to avoid cloning useless data or to clone as few data as
415        // possible. That's a fun game.
416        let LeftRoomUpdate {
417            // State-events are not stored in the Event Cache.
418            state: _,
419            // Account data are not used by any cache.
420            account_data: _,
421
422            // Extract the original timeline as it's going to be used by all caches.
423            timeline: original_timeline,
424
425            // Extract other data, only useful for the room cache.
426            ambiguity_changes,
427        } = updates;
428
429        // Room.
430        {
431            let (timeline, _read_receipts) =
432                aggregator::aggregate_timeline_and_read_receipts_for_room(&original_timeline, &[]);
433
434            room.handle_left_room_update(timeline, ambiguity_changes).await?;
435        }
436
437        // Threads.
438        {
439            let timeline_and_read_receipts_for_threads = {
440                // To aggregate the timelines for threads, we need to lookup in
441                // the room cache and the thread caches. We acquire a read lock
442                // over all the caches, and select the room cache and thread
443                // cache' states.
444                let all_caches_states_lock = states::CacheStateLock::new(
445                    states::selectors::AllStatesSelector::new(room.room_id().to_owned()),
446                    self.internals.state.clone(),
447                );
448                let all_caches_states = all_caches_states_lock.read().await?;
449
450                aggregator::aggregate_timeline_and_read_receipts_for_threads(
451                    &original_timeline,
452                    &[],
453                    all_caches_states.threads(),
454                    all_caches_states.room(),
455                    &internals.room_version_rules.redaction,
456                )
457                .await?
458            };
459
460            for (thread_id, (timeline, _read_receipts)) in timeline_and_read_receipts_for_threads {
461                let thread = self.thread(thread_id).await?;
462                thread.handle_left_room_update(timeline).await?;
463            }
464        }
465
466        // Pinned-events.
467        if let Some(pinned_events) = pinned_events.get() {
468            let timeline = aggregator::aggregate_timeline_for_pinned_events(
469                &original_timeline,
470                &pinned_events.state().read().await?.current_event_ids(),
471                &internals.room_version_rules.redaction,
472            );
473
474            pinned_events.handle_left_room_update(timeline).await?;
475        }
476
477        // Event-focused.
478        {
479            // An event-focused cache isn't listening to live update.
480            // Consequently, it is not interested by this kind of update.
481            let _ = event_focused;
482        }
483
484        Ok(())
485    }
486
487    /// Get all in-memory events from all the event caches managed by this
488    /// [`Caches`].
489    ///
490    /// Events can be duplicated if present in different event caches.
491    #[cfg(feature = "e2e-encryption")]
492    pub async fn all_in_memory_events(&self) -> Result<impl Iterator<Item = Event>> {
493        // We have to fetch events from all the caches.
494        //
495        // The room cache contains all the room events + the thread events + the
496        // pinned-events.
497        let mut events = self.room.events().await?;
498
499        // The last cache is the events from the event-focused cache.
500        {
501            let event_focused = self.event_focused.read().await;
502
503            for event_focused in event_focused.values() {
504                events.extend(event_focused.events().await?);
505            }
506        }
507
508        Ok(events.into_iter())
509    }
510
511    /// Get all the events of this room saved in the store.
512    #[cfg(feature = "experimental-search")]
513    pub async fn stored_events(&self) -> Result<Vec<Event>> {
514        let state = self.internals.state.read().await?;
515        Ok(state.store.get_room_events(self.room.room_id(), None, None).await?)
516    }
517
518    /// Get all encrypted events from all the event caches managed by this
519    /// [`Caches`].
520    ///
521    /// The `event_type` represents the type of the event to filter by. The
522    /// `session_id` represents the unique ID of the room key that was used to
523    /// encrypt the event
524    ///
525    /// Events can be duplicated if present in different event caches.
526    #[cfg(feature = "e2e-encryption")]
527    pub async fn all_events_of_type(
528        &self,
529        event_type: Option<&str>,
530        session_id: Option<&str>,
531    ) -> Result<impl Iterator<Item = Event>> {
532        // All caches store their events in the store except one. Let's start by
533        // looking inside the store.
534        let mut events = {
535            let state = self.internals.state.read().await?;
536
537            state.store.get_room_events(self.room.room_id(), event_type, session_id).await?
538        };
539
540        // The only cache to not store its events is the event-focused cache.
541        // Its events only live in memory.
542        {
543            let event_focused = self.event_focused.read().await;
544
545            for event_focused in event_focused.values() {
546                events.extend(
547                    event_focused
548                        .events()
549                        .await?
550                        .into_iter()
551                        .filter(|event| event_type == event.kind.event_type().as_deref())
552                        .filter(|event| session_id == event.kind.session_id()),
553                );
554            }
555        }
556
557        Ok(events.into_iter())
558    }
559}
560
561/// A diff update for an event cache timeline represented as a vector.
562#[derive(Clone, Debug)]
563pub struct TimelineVectorDiffs {
564    /// New vector diff for the thread timeline.
565    pub diffs: Vec<VectorDiff<Event>>,
566    /// The origin that triggered this update.
567    pub origin: EventsOrigin,
568}
569
570/// An enum representing where an event has been found.
571#[derive(Debug)]
572pub(super) enum EventLocation {
573    /// Event lives in memory (and likely in the store!).
574    Memory(Position),
575
576    /// Event lives in the store only, it has not been loaded in memory yet.
577    Store,
578}