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, 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, automatic_pagination::AutomaticPagination, 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        automatic_pagination: Option<AutomaticPagination>,
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                        automatic_pagination,
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        // Room.
295        {
296            let mut updates = updates.clone();
297            updates.timeline = aggregator::aggregate_timeline_for_room(updates.timeline);
298
299            room.handle_joined_room_update(updates).await?;
300        }
301
302        // Threads.
303        {
304            let mut updates = updates.clone();
305            updates.account_data.clear();
306            updates.ambiguity_changes.clear();
307
308            let timeline_for_threads = {
309                // To aggregate the timelines for threads, we need to lookup in the room cache
310                // and the thread caches. We acquire a read lock over all the caches, and select
311                // the room cache and thread cache' states.
312                let all_states_lock = states::CacheStateLock::new(
313                    states::selectors::AllStatesSelector::new(room.room_id().to_owned()),
314                    self.internals.state.clone(),
315                );
316                let all_states = all_states_lock.read().await?;
317
318                aggregator::aggregate_timeline_for_threads(
319                    &updates.timeline,
320                    all_states.threads(),
321                    all_states.room(),
322                    &internals.room_version_rules.redaction,
323                )
324                .await?
325            };
326
327            for (thread_id, timeline) in timeline_for_threads {
328                let mut updates = updates.clone();
329                updates.timeline = timeline;
330
331                let thread = self.thread(thread_id).await?;
332                thread.handle_joined_room_update(updates).await?;
333
334                let new_thread_summary =
335                    thread.state().read().await?.compute_thread_summary().await?;
336
337                room.update_thread_summary(thread.thread_id(), new_thread_summary).await?;
338            }
339        }
340
341        // Pinned-events.
342        if let Some(pinned_events) = pinned_events.get() {
343            let mut updates = updates.clone();
344            updates.timeline = aggregator::aggregate_timeline_for_pinned_events(
345                &updates.timeline,
346                &pinned_events.state().read().await?.current_event_ids(),
347                &internals.room_version_rules.redaction,
348            );
349
350            pinned_events.handle_joined_room_update(updates).await?;
351        }
352
353        // Event-focused.
354        {
355            // An event-focused cache isn't listening to live update. Consequently, it is
356            // not interested by this kind of update.
357            let _ = event_focused;
358        }
359
360        Ok(())
361    }
362
363    /// Update all the event caches with a [`LeftRoomUpdate`].
364    pub(super) async fn handle_left_room_update(&self, updates: LeftRoomUpdate) -> Result<()> {
365        let Self { room, threads: _, pinned_events, event_focused, internals } = &self;
366
367        // Room.
368        {
369            let mut updates = updates.clone();
370            updates.timeline = aggregator::aggregate_timeline_for_room(updates.timeline);
371
372            room.handle_left_room_update(updates).await?;
373        }
374
375        // Threads.
376        {
377            let mut updates = updates.clone();
378            updates.account_data.clear();
379            updates.ambiguity_changes.clear();
380
381            let timeline_for_threads = {
382                // To aggregate the timelines for threads, we need to lookup in the room cache
383                // and the thread caches. We acquire a read lock over all the caches, and select
384                // the room cache and thread cache' states.
385                let all_caches_states_lock = states::CacheStateLock::new(
386                    states::selectors::AllStatesSelector::new(room.room_id().to_owned()),
387                    self.internals.state.clone(),
388                );
389                let all_caches_states = all_caches_states_lock.read().await?;
390
391                aggregator::aggregate_timeline_for_threads(
392                    &updates.timeline,
393                    all_caches_states.threads(),
394                    all_caches_states.room(),
395                    &internals.room_version_rules.redaction,
396                )
397                .await?
398            };
399
400            for (thread_id, timeline) in timeline_for_threads {
401                let mut updates = updates.clone();
402                updates.timeline = timeline;
403
404                let thread = self.thread(thread_id).await?;
405                thread.handle_left_room_update(updates).await?;
406
407                let new_thread_summary =
408                    thread.state().read().await?.compute_thread_summary().await?;
409
410                room.update_thread_summary(thread.thread_id(), new_thread_summary).await?;
411            }
412        }
413
414        // Pinned-events.
415        if let Some(pinned_events) = pinned_events.get() {
416            let mut updates = updates.clone();
417            updates.timeline = aggregator::aggregate_timeline_for_pinned_events(
418                &updates.timeline,
419                &pinned_events.state().read().await?.current_event_ids(),
420                &internals.room_version_rules.redaction,
421            );
422
423            pinned_events.handle_left_room_update(updates).await?;
424        }
425
426        // Event-focused.
427        {
428            // An event-focused cache isn't listening to live update. Consequently, it is
429            // not interested by this kind of update.
430            let _ = event_focused;
431        }
432
433        Ok(())
434    }
435
436    /// Get all in-memory events from all the event caches managed by this
437    /// [`Caches`].
438    ///
439    /// Events can be duplicated if present in different event caches.
440    #[cfg(feature = "e2e-encryption")]
441    pub async fn all_in_memory_events(&self) -> Result<impl Iterator<Item = Event>> {
442        // We have to fetch events from all the caches.
443        //
444        // The room cache contains all the room events + the thread events + the
445        // pinned-events.
446        let mut events = self.room.events().await?;
447
448        // The last cache is the events from the event-focused cache.
449        {
450            let event_focused = self.event_focused.read().await;
451
452            for event_focused in event_focused.values() {
453                events.extend(event_focused.events().await?);
454            }
455        }
456
457        Ok(events.into_iter())
458    }
459
460    /// Get all encrypted events from all the event caches managed by this
461    /// [`Caches`].
462    ///
463    /// The `event_type` represents the type of the event to filter by.
464    /// The `session_id` represents the unique ID of the room key that was used
465    /// to encrypt the event
466    ///
467    /// Events can be duplicated if present in different event caches.
468    #[cfg(feature = "e2e-encryption")]
469    pub async fn all_events_of_type(
470        &self,
471        event_type: Option<&str>,
472        session_id: Option<&str>,
473    ) -> Result<impl Iterator<Item = Event>> {
474        // All caches store their events in the store except one. Let's start by looking
475        // inside the store.
476        let mut events = {
477            let state = self.internals.state.read().await?;
478
479            state.store.get_room_events(self.room.room_id(), event_type, session_id).await?
480        };
481
482        // The only cache to not store its events is the event-focused cache. Its events
483        // only live in memory.
484        {
485            let event_focused = self.event_focused.read().await;
486
487            for event_focused in event_focused.values() {
488                events.extend(
489                    event_focused
490                        .events()
491                        .await?
492                        .into_iter()
493                        .filter(|event| event_type == event.kind.event_type().as_deref())
494                        .filter(|event| session_id == event.kind.session_id()),
495                );
496            }
497        }
498
499        Ok(events.into_iter())
500    }
501}
502
503/// A diff update for an event cache timeline represented as a vector.
504#[derive(Clone, Debug)]
505pub struct TimelineVectorDiffs {
506    /// New vector diff for the thread timeline.
507    pub diffs: Vec<VectorDiff<Event>>,
508    /// The origin that triggered this update.
509    pub origin: EventsOrigin,
510}
511
512/// An enum representing where an event has been found.
513#[derive(Debug)]
514pub(super) enum EventLocation {
515    /// Event lives in memory (and likely in the store!).
516    Memory(Position),
517
518    /// Event lives in the store only, it has not been loaded in memory yet.
519    Store,
520}