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        let room_state = state
119            .try_insert_once_with(
120                states::selectors::RoomStateSelector::new(room_id.to_owned()),
121                |store_guard| {
122                    room::RoomEventCacheState::new(
123                        own_user_id.clone(),
124                        room_id.to_owned(),
125                        weak_room.clone(),
126                        room_version_rules.clone(),
127                        enabled_thread_support,
128                        update_sender.clone(),
129                        linked_chunk_update_sender.clone(),
130                        store_guard,
131                        pagination_status.clone(),
132                        back_pagination_queue,
133                    )
134                },
135            )
136            .await?;
137
138        let timeline_is_not_empty =
139            room_state.read().await?.room_linked_chunk().revents().next().is_some();
140
141        let room_event_cache = room::RoomEventCache::new(
142            room_id.to_owned(),
143            weak_room,
144            own_user_id,
145            room_state,
146            pagination_status,
147            auto_shrink_sender.clone(),
148            update_sender,
149        );
150
151        // If at least one event has been loaded, it means there is a timeline. Let's
152        // emit a generic update.
153        if timeline_is_not_empty {
154            let _ = generic_update_sender
155                .send(room::RoomEventCacheGenericUpdate { room_id: room_id.to_owned() });
156        }
157
158        Ok(Self {
159            room: room_event_cache,
160            threads: Arc::new(RwLock::new(HashMap::new())),
161            pinned_events: OnceCell::new(),
162            event_focused: Arc::new(RwLock::new(HashMap::new())),
163            internals: CachesInternals {
164                state: state.clone(),
165                auto_shrink_sender,
166                linked_chunk_update_sender,
167                room_version_rules,
168            },
169        })
170    }
171
172    /// Get the [`RoomEventCache`].
173    ///
174    /// [`RoomEventCache`]: room::RoomEventCache
175    pub fn room(&self) -> &room::RoomEventCache {
176        &self.room
177    }
178
179    /// Get or create a [`ThreadEventCache`].
180    ///
181    /// Note: it is impossible to know if `thread_id` represents a valid thread
182    /// identifier. It means it's possible to create a [`ThreadEventCache`] for
183    /// an event that is not a thread root.
184    ///
185    /// [`ThreadEventCache`]: thread::ThreadEventCache
186    pub async fn thread(
187        &self,
188        thread_id: OwnedEventId,
189    ) -> Result<
190        OwnedRwLockReadGuard<
191            HashMap<OwnedEventId, thread::ThreadEventCache>,
192            thread::ThreadEventCache,
193        >,
194    > {
195        Ok(
196            match OwnedRwLockWriteGuard::try_downgrade_map(
197                self.threads.clone().write_owned().await,
198                |threads| threads.get(&thread_id),
199            ) {
200                // Thread exists.
201                Ok(locked_cache) => locked_cache,
202                // Thread does not exist, let's create it.
203                Err(mut threads) => {
204                    let room = &self.room;
205                    let cache = thread::ThreadEventCache::new(
206                        room.room_id().to_owned(),
207                        thread_id.clone(),
208                        room.own_user_id().to_owned(),
209                        self.internals.room_version_rules.clone(),
210                        room.weak_room().to_owned(),
211                        &self.internals.state,
212                        self.internals.auto_shrink_sender.clone(),
213                        room.update_sender().generic_update_sender().clone(),
214                        self.internals.linked_chunk_update_sender.clone(),
215                    )
216                    .await?;
217
218                    threads.insert(thread_id.clone(), cache);
219
220                    OwnedRwLockWriteGuard::downgrade_map(threads, |threads| {
221                        threads.get(&thread_id).unwrap()
222                    })
223                }
224            },
225        )
226    }
227
228    /// Get or create a [`PinnedEventsCache`].
229    ///
230    /// [`PinnedEventsCache`]: pinned_events::PinnedEventsCache
231    pub async fn pinned_events(&self) -> Result<&pinned_events::PinnedEventsCache> {
232        self.pinned_events
233            .get_or_try_init(|| {
234                pinned_events::PinnedEventsCache::new(
235                    self.room.weak_room(),
236                    self.room.own_user_id().clone(),
237                    self.internals.room_version_rules.clone(),
238                    self.internals.linked_chunk_update_sender.clone(),
239                    &self.internals.state,
240                )
241            })
242            .await
243    }
244
245    /// Get or create a [`EventFocusedCache`].
246    ///
247    /// [`EventFocusedCache`]: event_focused::EventFocusedCache
248    pub async fn event_focused(
249        &self,
250        event_id: OwnedEventId,
251        thread_mode: event_focused::EventFocusThreadMode,
252        number_of_initial_events: u16,
253    ) -> Result<
254        OwnedRwLockReadGuard<
255            HashMap<event_focused::EventFocusedCacheKey, event_focused::EventFocusedCache>,
256            event_focused::EventFocusedCache,
257        >,
258    > {
259        let key = event_focused::EventFocusedCacheKey { focused_event_id: event_id, thread_mode };
260
261        Ok(
262            match OwnedRwLockWriteGuard::try_downgrade_map(
263                self.event_focused.clone().write_owned().await,
264                |event_focused_caches| event_focused_caches.get(&key),
265            ) {
266                // Event-focused cache exists.
267                Ok(locked_cache) => locked_cache,
268                // Event-focused cache does not exist, let's create it.
269                Err(mut event_focused_caches) => {
270                    let cache = event_focused::EventFocusedCache::new(
271                        self.room.weak_room().clone(),
272                        key.clone(),
273                        &self.internals.state,
274                        self.internals.linked_chunk_update_sender.clone(),
275                    )
276                    .await?;
277                    cache.start_from(number_of_initial_events, thread_mode).await?;
278
279                    event_focused_caches.insert(key.clone(), cache);
280
281                    OwnedRwLockWriteGuard::downgrade_map(
282                        event_focused_caches,
283                        |event_focused_caches| event_focused_caches.get(&key).unwrap(),
284                    )
285                }
286            },
287        )
288    }
289
290    /// Update all the event caches with a [`JoinedRoomUpdate`].
291    pub(super) async fn handle_joined_room_update(&self, updates: JoinedRoomUpdate) -> Result<()> {
292        let Self { room, threads: _, pinned_events, event_focused, internals } = &self;
293
294        // This method will compute a `JoinedRoomUpdate` for each cache. The game is to
295        // avoid cloning useless data or to clone as few data as possible. That's a fun
296        // game.
297        let JoinedRoomUpdate {
298            // Read receipts are computed by the Event Cache, see [`read_receipts`], we
299            // don't need the server value.
300            unread_notifications: _,
301            // State-events are not stored in the Event Cache.
302            state: _,
303
304            // Extract the original timeline and ephemeral events as timeline will be used by all
305            // caches, and ephemeral events by the room and thread caches.
306            timeline: original_timeline,
307            ephemeral: original_ephemeral,
308
309            // Extract other data, only useful for the room cache.
310            account_data,
311            ambiguity_changes,
312            avatar_changes,
313        } = updates;
314
315        // Filter ephemeral events.
316        let original_ephemeral = original_ephemeral
317            .into_iter()
318            .filter_map(|ephemeral_event| ephemeral_event.deserialize().ok())
319            .collect::<Vec<_>>();
320
321        // Room.
322        {
323            let (timeline, read_receipts) =
324                aggregator::aggregate_timeline_and_read_receipts_for_room(
325                    &original_timeline,
326                    &original_ephemeral,
327                );
328
329            room.handle_joined_room_update(
330                timeline,
331                read_receipts,
332                account_data,
333                ambiguity_changes,
334                avatar_changes,
335            )
336            .await?;
337        }
338
339        // Threads.
340        {
341            let timeline_and_read_receipts_for_threads = {
342                // To aggregate the timelines for threads, we need to lookup in the room cache
343                // and the thread caches. We acquire a read lock over all the caches, and select
344                // the room cache and thread cache' states.
345                let all_states_lock = states::CacheStateLock::new(
346                    states::selectors::AllStatesSelector::new(room.room_id().to_owned()),
347                    self.internals.state.clone(),
348                );
349                let all_states = all_states_lock.read().await?;
350
351                aggregator::aggregate_timeline_and_read_receipts_for_threads(
352                    &original_timeline,
353                    &original_ephemeral,
354                    all_states.threads(),
355                    all_states.room(),
356                    &internals.room_version_rules.redaction,
357                )
358                .await?
359            };
360
361            for (thread_id, (timeline, read_receipts)) in timeline_and_read_receipts_for_threads {
362                // Update the thread summary if and only if there are new events.
363                let update_thread_summary = timeline.events.is_empty().not();
364
365                let thread = self.thread(thread_id).await?;
366                thread.handle_joined_room_update(timeline, read_receipts).await?;
367
368                if update_thread_summary {
369                    let new_thread_summary =
370                        thread.state().read().await?.compute_thread_summary().await?;
371
372                    room.update_thread_summary(thread.thread_id(), new_thread_summary).await?;
373                }
374            }
375        }
376
377        // Pinned-events.
378        if let Some(pinned_events) = pinned_events.get() {
379            let timeline = aggregator::aggregate_timeline_for_pinned_events(
380                &original_timeline,
381                &pinned_events.state().read().await?.current_event_ids(),
382                &internals.room_version_rules.redaction,
383            );
384
385            pinned_events.handle_joined_room_update(timeline).await?;
386        }
387
388        // Event-focused.
389        {
390            // An event-focused cache isn't listening to live update. Consequently, it is
391            // not interested by this kind of update.
392            let _ = event_focused;
393        }
394
395        Ok(())
396    }
397
398    /// Update all the event caches with a [`LeftRoomUpdate`].
399    pub(super) async fn handle_left_room_update(&self, updates: LeftRoomUpdate) -> Result<()> {
400        let Self { room, threads: _, pinned_events, event_focused, internals } = &self;
401
402        // This method will compute a `JoinedRoomUpdate` for each cache. The game is to
403        // avoid cloning useless data or to clone as few data as possible. That's a fun
404        // game.
405        let LeftRoomUpdate {
406            // State-events are not stored in the Event Cache.
407            state: _,
408            // Account data are not used by any cache.
409            account_data: _,
410
411            // Extract the original timeline as it's going to be used by all caches.
412            timeline: original_timeline,
413
414            // Extract other data, only useful for the room cache.
415            ambiguity_changes,
416        } = updates;
417
418        // Room.
419        {
420            let (timeline, _read_receipts) =
421                aggregator::aggregate_timeline_and_read_receipts_for_room(&original_timeline, &[]);
422
423            room.handle_left_room_update(timeline, ambiguity_changes).await?;
424        }
425
426        // Threads.
427        {
428            let timeline_and_read_receipts_for_threads = {
429                // To aggregate the timelines for threads, we need to lookup in the room cache
430                // and the thread caches. We acquire a read lock over all the caches, and select
431                // the room cache and thread cache' states.
432                let all_caches_states_lock = states::CacheStateLock::new(
433                    states::selectors::AllStatesSelector::new(room.room_id().to_owned()),
434                    self.internals.state.clone(),
435                );
436                let all_caches_states = all_caches_states_lock.read().await?;
437
438                aggregator::aggregate_timeline_and_read_receipts_for_threads(
439                    &original_timeline,
440                    &[],
441                    all_caches_states.threads(),
442                    all_caches_states.room(),
443                    &internals.room_version_rules.redaction,
444                )
445                .await?
446            };
447
448            for (thread_id, (timeline, _read_receipts)) in timeline_and_read_receipts_for_threads {
449                let thread = self.thread(thread_id).await?;
450                thread.handle_left_room_update(timeline).await?;
451            }
452        }
453
454        // Pinned-events.
455        if let Some(pinned_events) = pinned_events.get() {
456            let timeline = aggregator::aggregate_timeline_for_pinned_events(
457                &original_timeline,
458                &pinned_events.state().read().await?.current_event_ids(),
459                &internals.room_version_rules.redaction,
460            );
461
462            pinned_events.handle_left_room_update(timeline).await?;
463        }
464
465        // Event-focused.
466        {
467            // An event-focused cache isn't listening to live update. Consequently, it is
468            // not interested by this kind of update.
469            let _ = event_focused;
470        }
471
472        Ok(())
473    }
474
475    /// Get all in-memory events from all the event caches managed by this
476    /// [`Caches`].
477    ///
478    /// Events can be duplicated if present in different event caches.
479    #[cfg(feature = "e2e-encryption")]
480    pub async fn all_in_memory_events(&self) -> Result<impl Iterator<Item = Event>> {
481        // We have to fetch events from all the caches.
482        //
483        // The room cache contains all the room events + the thread events + the
484        // pinned-events.
485        let mut events = self.room.events().await?;
486
487        // The last cache is the events from the event-focused cache.
488        {
489            let event_focused = self.event_focused.read().await;
490
491            for event_focused in event_focused.values() {
492                events.extend(event_focused.events().await?);
493            }
494        }
495
496        Ok(events.into_iter())
497    }
498
499    /// Get all encrypted events from all the event caches managed by this
500    /// [`Caches`].
501    ///
502    /// The `event_type` represents the type of the event to filter by.
503    /// The `session_id` represents the unique ID of the room key that was used
504    /// to encrypt the event
505    ///
506    /// Events can be duplicated if present in different event caches.
507    #[cfg(feature = "e2e-encryption")]
508    pub async fn all_events_of_type(
509        &self,
510        event_type: Option<&str>,
511        session_id: Option<&str>,
512    ) -> Result<impl Iterator<Item = Event>> {
513        // All caches store their events in the store except one. Let's start by looking
514        // inside the store.
515        let mut events = {
516            let state = self.internals.state.read().await?;
517
518            state.store.get_room_events(self.room.room_id(), event_type, session_id).await?
519        };
520
521        // The only cache to not store its events is the event-focused cache. Its events
522        // only live in memory.
523        {
524            let event_focused = self.event_focused.read().await;
525
526            for event_focused in event_focused.values() {
527                events.extend(
528                    event_focused
529                        .events()
530                        .await?
531                        .into_iter()
532                        .filter(|event| event_type == event.kind.event_type().as_deref())
533                        .filter(|event| session_id == event.kind.session_id()),
534                );
535            }
536        }
537
538        Ok(events.into_iter())
539    }
540}
541
542/// A diff update for an event cache timeline represented as a vector.
543#[derive(Clone, Debug)]
544pub struct TimelineVectorDiffs {
545    /// New vector diff for the thread timeline.
546    pub diffs: Vec<VectorDiff<Event>>,
547    /// The origin that triggered this update.
548    pub origin: EventsOrigin,
549}
550
551/// An enum representing where an event has been found.
552#[derive(Debug)]
553pub(super) enum EventLocation {
554    /// Event lives in memory (and likely in the store!).
555    Memory(Position),
556
557    /// Event lives in the store only, it has not been loaded in memory yet.
558    Store,
559}