matrix_sdk/event_cache/
mod.rs

1// Copyright 2024 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
15//! The event cache is an abstraction layer, sitting between the Rust SDK and a
16//! final client, that acts as a global observer of all the rooms, gathering and
17//! inferring some extra useful information about each room. In particular, this
18//! doesn't require subscribing to a specific room to get access to this
19//! information.
20//!
21//! It's intended to be fast, robust and easy to maintain, having learned from
22//! previous endeavours at implementing middle to high level features elsewhere
23//! in the SDK, notably in the UI's Timeline object.
24//!
25//! See the [github issue](https://github.com/matrix-org/matrix-rust-sdk/issues/3058) for more
26//! details about the historical reasons that led us to start writing this.
27
28#![forbid(missing_docs)]
29
30use std::{
31    collections::BTreeMap,
32    fmt,
33    sync::{Arc, OnceLock},
34};
35
36use eyeball::{SharedObservable, Subscriber};
37use eyeball_im::VectorDiff;
38use futures_util::future::{join_all, try_join_all};
39use matrix_sdk_base::{
40    deserialized_responses::{AmbiguityChange, TimelineEvent},
41    event_cache::store::{EventCacheStoreError, EventCacheStoreLock},
42    linked_chunk::lazy_loader::LazyLoaderError,
43    store_locks::LockStoreError,
44    sync::RoomUpdates,
45    timer,
46};
47use matrix_sdk_common::executor::{spawn, JoinHandle};
48use room::RoomEventCacheState;
49use ruma::{events::AnySyncEphemeralRoomEvent, serde::Raw, OwnedEventId, OwnedRoomId, RoomId};
50use tokio::sync::{
51    broadcast::{channel, error::RecvError, Receiver, Sender},
52    mpsc, Mutex, RwLock,
53};
54use tracing::{debug, error, info, info_span, instrument, trace, warn, Instrument as _, Span};
55
56use crate::{client::WeakClient, Client};
57
58mod deduplicator;
59mod pagination;
60mod room;
61
62pub use pagination::{RoomPagination, RoomPaginationStatus};
63pub use room::{RoomEventCache, RoomEventCacheSubscriber, ThreadEventCacheUpdate};
64
65/// An error observed in the [`EventCache`].
66#[derive(thiserror::Error, Debug)]
67pub enum EventCacheError {
68    /// The [`EventCache`] instance hasn't been initialized with
69    /// [`EventCache::subscribe`]
70    #[error(
71        "The EventCache hasn't subscribed to sync responses yet, call `EventCache::subscribe()`"
72    )]
73    NotSubscribedYet,
74
75    /// Room is not found.
76    #[error("Room `{room_id}` is not found.")]
77    RoomNotFound {
78        /// The ID of the room not being found.
79        room_id: OwnedRoomId,
80    },
81
82    /// An error has been observed while back-paginating.
83    #[error(transparent)]
84    BackpaginationError(Box<crate::Error>),
85
86    /// Back-pagination was already happening in a given room, where we tried to
87    /// back-paginate again.
88    #[error("We were already back-paginating.")]
89    AlreadyBackpaginating,
90
91    /// An error happening when interacting with storage.
92    #[error(transparent)]
93    Storage(#[from] EventCacheStoreError),
94
95    /// An error happening when attempting to (cross-process) lock storage.
96    #[error(transparent)]
97    LockingStorage(#[from] LockStoreError),
98
99    /// The [`EventCache`] owns a weak reference to the [`Client`] it pertains
100    /// to. It's possible this weak reference points to nothing anymore, at
101    /// times where we try to use the client.
102    #[error("The owning client of the event cache has been dropped.")]
103    ClientDropped,
104
105    /// An error happening when interacting with the [`LinkedChunk`]'s lazy
106    /// loader.
107    ///
108    /// [`LinkedChunk`]: matrix_sdk_common::linked_chunk::LinkedChunk
109    #[error(transparent)]
110    LinkedChunkLoader(#[from] LazyLoaderError),
111
112    /// An error happened when reading the metadata of a linked chunk, upon
113    /// reload.
114    #[error("the linked chunk metadata is invalid: {details}")]
115    InvalidLinkedChunkMetadata {
116        /// A string containing details about the error.
117        details: String,
118    },
119}
120
121/// A result using the [`EventCacheError`].
122pub type Result<T> = std::result::Result<T, EventCacheError>;
123
124/// Hold handles to the tasks spawn by a [`EventCache`].
125pub struct EventCacheDropHandles {
126    /// Task that listens to room updates.
127    listen_updates_task: JoinHandle<()>,
128
129    /// Task that listens to updates to the user's ignored list.
130    ignore_user_list_update_task: JoinHandle<()>,
131
132    /// The task used to automatically shrink the linked chunks.
133    auto_shrink_linked_chunk_task: JoinHandle<()>,
134}
135
136impl fmt::Debug for EventCacheDropHandles {
137    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
138        f.debug_struct("EventCacheDropHandles").finish_non_exhaustive()
139    }
140}
141
142impl Drop for EventCacheDropHandles {
143    fn drop(&mut self) {
144        self.listen_updates_task.abort();
145        self.ignore_user_list_update_task.abort();
146        self.auto_shrink_linked_chunk_task.abort();
147    }
148}
149
150/// An event cache, providing lots of useful functionality for clients.
151///
152/// Cloning is shallow, and thus is cheap to do.
153///
154/// See also the module-level comment.
155#[derive(Clone)]
156pub struct EventCache {
157    /// Reference to the inner cache.
158    inner: Arc<EventCacheInner>,
159}
160
161impl fmt::Debug for EventCache {
162    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
163        f.debug_struct("EventCache").finish_non_exhaustive()
164    }
165}
166
167impl EventCache {
168    /// Create a new [`EventCache`] for the given client.
169    pub(crate) fn new(client: WeakClient, event_cache_store: EventCacheStoreLock) -> Self {
170        let (room_event_cache_generic_update_sender, _) = channel(32);
171
172        Self {
173            inner: Arc::new(EventCacheInner {
174                client,
175                store: event_cache_store,
176                multiple_room_updates_lock: Default::default(),
177                by_room: Default::default(),
178                drop_handles: Default::default(),
179                auto_shrink_sender: Default::default(),
180                room_event_cache_generic_update_sender,
181            }),
182        }
183    }
184
185    /// Starts subscribing the [`EventCache`] to sync responses, if not done
186    /// before.
187    ///
188    /// Re-running this has no effect if we already subscribed before, and is
189    /// cheap.
190    pub fn subscribe(&self) -> Result<()> {
191        let client = self.inner.client()?;
192
193        // Initialize the drop handles.
194        let _ = self.inner.drop_handles.get_or_init(|| {
195            // Spawn the task that will listen to all the room updates at once.
196            let listen_updates_task = spawn(Self::listen_task(
197                self.inner.clone(),
198                client.subscribe_to_all_room_updates(),
199            ));
200
201            let ignore_user_list_update_task = spawn(Self::ignore_user_list_update_task(
202                self.inner.clone(),
203                client.subscribe_to_ignore_user_list_changes(),
204            ));
205
206            let (auto_shrink_sender, auto_shrink_receiver) = mpsc::channel(32);
207
208            // Force-initialize the sender in the [`RoomEventCacheInner`].
209            self.inner.auto_shrink_sender.get_or_init(|| auto_shrink_sender);
210
211            let auto_shrink_linked_chunk_task = spawn(Self::auto_shrink_linked_chunk_task(
212                self.inner.clone(),
213                auto_shrink_receiver,
214            ));
215
216            Arc::new(EventCacheDropHandles {
217                listen_updates_task,
218                ignore_user_list_update_task,
219                auto_shrink_linked_chunk_task,
220            })
221        });
222
223        Ok(())
224    }
225
226    #[instrument(skip_all)]
227    async fn ignore_user_list_update_task(
228        inner: Arc<EventCacheInner>,
229        mut ignore_user_list_stream: Subscriber<Vec<String>>,
230    ) {
231        let span = info_span!(parent: Span::none(), "ignore_user_list_update_task");
232        span.follows_from(Span::current());
233
234        async move {
235            while ignore_user_list_stream.next().await.is_some() {
236                info!("Received an ignore user list change");
237                if let Err(err) = inner.clear_all_rooms().await {
238                    error!("when clearing room storage after ignore user list change: {err}");
239                }
240            }
241            info!("Ignore user list stream has closed");
242        }
243        .instrument(span)
244        .await;
245    }
246
247    /// For benchmarking purposes only.
248    #[doc(hidden)]
249    pub async fn handle_room_updates(&self, updates: RoomUpdates) -> Result<()> {
250        self.inner.handle_room_updates(updates).await
251    }
252
253    #[instrument(skip_all)]
254    async fn listen_task(
255        inner: Arc<EventCacheInner>,
256        mut room_updates_feed: Receiver<RoomUpdates>,
257    ) {
258        trace!("Spawning the listen task");
259        loop {
260            match room_updates_feed.recv().await {
261                Ok(updates) => {
262                    trace!("Receiving `RoomUpdates`");
263
264                    if let Err(err) = inner.handle_room_updates(updates).await {
265                        match err {
266                            EventCacheError::ClientDropped => {
267                                // The client has dropped, exit the listen task.
268                                info!(
269                                    "Closing the event cache global listen task because client dropped"
270                                );
271                                break;
272                            }
273                            err => {
274                                error!("Error when handling room updates: {err}");
275                            }
276                        }
277                    }
278                }
279
280                Err(RecvError::Lagged(num_skipped)) => {
281                    // Forget everything we know; we could have missed events, and we have
282                    // no way to reconcile at the moment!
283                    // TODO: implement Smart Matching™,
284                    warn!(num_skipped, "Lagged behind room updates, clearing all rooms");
285                    if let Err(err) = inner.clear_all_rooms().await {
286                        error!("when clearing storage after lag in listen_task: {err}");
287                    }
288                }
289
290                Err(RecvError::Closed) => {
291                    // The sender has shut down, exit.
292                    info!("Closing the event cache global listen task because receiver closed");
293                    break;
294                }
295            }
296        }
297    }
298
299    /// Spawns the task that will listen to auto-shrink notifications.
300    ///
301    /// The auto-shrink mechanism works this way:
302    ///
303    /// - Each time there's a new subscriber to a [`RoomEventCache`], it will
304    ///   increment the active number of subscribers to that room, aka
305    ///   [`RoomEventCacheState::subscriber_count`].
306    /// - When that subscriber is dropped, it will decrement that count; and
307    ///   notify the task below if it reached 0.
308    /// - The task spawned here, owned by the [`EventCacheInner`], will listen
309    ///   to such notifications that a room may be shrunk. It will attempt an
310    ///   auto-shrink, by letting the inner state decide whether this is a good
311    ///   time to do so (new subscribers might have spawned in the meanwhile).
312    #[instrument(skip_all)]
313    async fn auto_shrink_linked_chunk_task(
314        inner: Arc<EventCacheInner>,
315        mut rx: mpsc::Receiver<AutoShrinkChannelPayload>,
316    ) {
317        while let Some(room_id) = rx.recv().await {
318            trace!(for_room = %room_id, "received notification to shrink");
319
320            let room = match inner.for_room(&room_id).await {
321                Ok(room) => room,
322                Err(err) => {
323                    warn!(for_room = %room_id, "Failed to get the `RoomEventCache`: {err}");
324                    continue;
325                }
326            };
327
328            trace!("waiting for state lock…");
329            let mut state = room.inner.state.write().await;
330
331            match state.auto_shrink_if_no_subscribers().await {
332                Ok(diffs) => {
333                    if let Some(diffs) = diffs {
334                        // Hey, fun stuff: we shrunk the linked chunk, so there shouldn't be any
335                        // subscribers, right? RIGHT? Especially because the state is guarded behind
336                        // a lock.
337                        //
338                        // However, better safe than sorry, and it's cheap to send an update here,
339                        // so let's do it!
340                        if !diffs.is_empty() {
341                            let _ = room.inner.sender.send(
342                                RoomEventCacheUpdate::UpdateTimelineEvents {
343                                    diffs,
344                                    origin: EventsOrigin::Cache,
345                                },
346                            );
347                        }
348                    } else {
349                        debug!("auto-shrinking didn't happen");
350                    }
351                }
352
353                Err(err) => {
354                    // There's not much we can do here, unfortunately.
355                    warn!(for_room = %room_id, "error when attempting to shrink linked chunk: {err}");
356                }
357            }
358        }
359    }
360
361    /// Return a room-specific view over the [`EventCache`].
362    pub(crate) async fn for_room(
363        &self,
364        room_id: &RoomId,
365    ) -> Result<(RoomEventCache, Arc<EventCacheDropHandles>)> {
366        let Some(drop_handles) = self.inner.drop_handles.get().cloned() else {
367            return Err(EventCacheError::NotSubscribedYet);
368        };
369
370        let room = self.inner.for_room(room_id).await?;
371
372        Ok((room, drop_handles))
373    }
374
375    /// Cleanly clear all the rooms' event caches.
376    ///
377    /// This will notify any live observers that the room has been cleared.
378    pub async fn clear_all_rooms(&self) -> Result<()> {
379        self.inner.clear_all_rooms().await
380    }
381
382    /// Subscribe to room _generic_ updates.
383    ///
384    /// If one wants to listen what has changed in a specific room, the
385    /// [`RoomEventCache::subscribe`] is recommended. However, the
386    /// [`RoomEventCacheSubscriber`] type triggers side-effects.
387    ///
388    /// If one wants to get a high-overview, generic, updates for rooms, and
389    /// without side-effects, this method is recommended. For example, it
390    /// doesn't provide a list of new events, but rather a
391    /// [`RoomEventCacheGenericUpdate::UpdateTimeline`] update. Also, dropping
392    /// the receiver of this channel will not trigger any side-effect.
393    pub fn subscribe_to_room_generic_updates(&self) -> Receiver<RoomEventCacheGenericUpdate> {
394        self.inner.room_event_cache_generic_update_sender.subscribe()
395    }
396}
397
398struct EventCacheInner {
399    /// A weak reference to the inner client, useful when trying to get a handle
400    /// on the owning client.
401    client: WeakClient,
402
403    /// Reference to the underlying store.
404    store: EventCacheStoreLock,
405
406    /// A lock used when many rooms must be updated at once.
407    ///
408    /// [`Mutex`] is “fair”, as it is implemented as a FIFO. It is important to
409    /// ensure that multiple updates will be applied in the correct order, which
410    /// is enforced by taking this lock when handling an update.
411    // TODO: that's the place to add a cross-process lock!
412    multiple_room_updates_lock: Mutex<()>,
413
414    /// Lazily-filled cache of live [`RoomEventCache`], once per room.
415    by_room: RwLock<BTreeMap<OwnedRoomId, RoomEventCache>>,
416
417    /// Handles to keep alive the task listening to updates.
418    drop_handles: OnceLock<Arc<EventCacheDropHandles>>,
419
420    /// A sender for notifications that a room *may* need to be auto-shrunk.
421    ///
422    /// Needs to live here, so it may be passed to each [`RoomEventCache`]
423    /// instance.
424    ///
425    /// See doc comment of [`EventCache::auto_shrink_linked_chunk_task`].
426    auto_shrink_sender: OnceLock<mpsc::Sender<AutoShrinkChannelPayload>>,
427
428    /// A sender for room generic update.
429    ///
430    /// See doc comment of [`RoomEventCacheGenericUpdate`] and
431    /// [`EventCache::subscribe_to_room_generic_updates`].
432    room_event_cache_generic_update_sender: Sender<RoomEventCacheGenericUpdate>,
433}
434
435type AutoShrinkChannelPayload = OwnedRoomId;
436
437impl EventCacheInner {
438    fn client(&self) -> Result<Client> {
439        self.client.get().ok_or(EventCacheError::ClientDropped)
440    }
441
442    /// Clears all the room's data.
443    async fn clear_all_rooms(&self) -> Result<()> {
444        // Okay, here's where things get complicated.
445        //
446        // On the one hand, `by_room` may include storage for *some* rooms that we know
447        // about, but not *all* of them. Any room that hasn't been loaded in the
448        // client, or touched by a sync, will remain unloaded in memory, so it
449        // will be missing from `self.by_room`. As a result, we need to make
450        // sure that we're hitting the storage backend to *really* clear all the
451        // rooms, including those that haven't been loaded yet.
452        //
453        // On the other hand, one must NOT clear the `by_room` map, because if someone
454        // subscribed to a room update, they would never get any new update for
455        // that room, since re-creating the `RoomEventCache` would create a new,
456        // unrelated sender.
457        //
458        // So we need to *keep* the rooms in `by_room` alive, while clearing them in the
459        // store backend.
460        //
461        // As a result, for a short while, the in-memory linked chunks
462        // will be desynchronized from the storage. We need to be careful then. During
463        // that short while, we don't want *anyone* to touch the linked chunk
464        // (be it in memory or in the storage).
465        //
466        // And since that requirement applies to *any* room in `by_room` at the same
467        // time, we'll have to take the locks for *all* the live rooms, so as to
468        // properly clear the underlying storage.
469        //
470        // At this point, you might be scared about the potential for deadlocking. I am
471        // as well, but I'm convinced we're fine:
472        // 1. the lock for `by_room` is usually held only for a short while, and
473        //    independently of the other two kinds.
474        // 2. the state may acquire the store cross-process lock internally, but only
475        //    while the state's methods are called (so it's always transient). As a
476        //    result, as soon as we've acquired the state locks, the store lock ought to
477        //    be free.
478        // 3. The store lock is held explicitly only in a small scoped area below.
479        // 4. Then the store lock will be held internally when calling `reset()`, but at
480        //    this point it's only held for a short while each time, so rooms will take
481        //    turn to acquire it.
482
483        let rooms = self.by_room.write().await;
484
485        // Collect all the rooms' state locks, first: we can clear the storage only when
486        // nobody will touch it at the same time.
487        let room_locks = join_all(
488            rooms.values().map(|room| async move { (room, room.inner.state.write().await) }),
489        )
490        .await;
491
492        // Clear the storage for all the rooms, using the storage facility.
493        self.store.lock().await?.clear_all_linked_chunks().await?;
494
495        // At this point, all the in-memory linked chunks are desynchronized from the
496        // storage. Resynchronize them manually by calling reset(), and
497        // propagate updates to observers.
498        try_join_all(room_locks.into_iter().map(|(room, mut state_guard)| async move {
499            let updates_as_vector_diffs = state_guard.reset().await?;
500
501            let _ = room.inner.sender.send(RoomEventCacheUpdate::UpdateTimelineEvents {
502                diffs: updates_as_vector_diffs,
503                origin: EventsOrigin::Cache,
504            });
505
506            let _ = room
507                .inner
508                .generic_update_sender
509                .send(RoomEventCacheGenericUpdate::Clear { room_id: room.inner.room_id.clone() });
510
511            Ok::<_, EventCacheError>(())
512        }))
513        .await?;
514
515        Ok(())
516    }
517
518    /// Handles a single set of room updates at once.
519    #[instrument(skip(self, updates))]
520    async fn handle_room_updates(&self, updates: RoomUpdates) -> Result<()> {
521        // First, take the lock that indicates we're processing updates, to avoid
522        // handling multiple updates concurrently.
523        let _lock = {
524            let _timer = timer!("Taking the `multiple_room_updates_lock`");
525            self.multiple_room_updates_lock.lock().await
526        };
527
528        // Note: bnjbvr tried to make this concurrent at some point, but it turned out
529        // to be a performance regression, even for large sync updates. Lacking
530        // time to investigate, this code remains sequential for now. See also
531        // https://github.com/matrix-org/matrix-rust-sdk/pull/5426.
532
533        // Left rooms.
534        for (room_id, left_room_update) in updates.left {
535            let room = self.for_room(&room_id).await?;
536
537            if let Err(err) = room.inner.handle_left_room_update(left_room_update).await {
538                // Non-fatal error, try to continue to the next room.
539                error!("handling left room update: {err}");
540            }
541        }
542
543        // Joined rooms.
544        for (room_id, joined_room_update) in updates.joined {
545            trace!(?room_id, "Handling a `JoinedRoomUpdate`");
546
547            let room = self.for_room(&room_id).await?;
548
549            if let Err(err) = room.inner.handle_joined_room_update(joined_room_update).await {
550                // Non-fatal error, try to continue to the next room.
551                error!(%room_id, "handling joined room update: {err}");
552            }
553        }
554
555        // Invited rooms.
556        // TODO: we don't anything with `updates.invite` at this point.
557
558        Ok(())
559    }
560
561    /// Return a room-specific view over the [`EventCache`].
562    async fn for_room(&self, room_id: &RoomId) -> Result<RoomEventCache> {
563        // Fast path: the entry exists; let's acquire a read lock, it's cheaper than a
564        // write lock.
565        let by_room_guard = self.by_room.read().await;
566
567        match by_room_guard.get(room_id) {
568            Some(room) => Ok(room.clone()),
569
570            None => {
571                // Slow-path: the entry doesn't exist; let's acquire a write lock.
572                drop(by_room_guard);
573                let mut by_room_guard = self.by_room.write().await;
574
575                // In the meanwhile, some other caller might have obtained write access and done
576                // the same, so check for existence again.
577                if let Some(room) = by_room_guard.get(room_id) {
578                    return Ok(room.clone());
579                }
580
581                let pagination_status =
582                    SharedObservable::new(RoomPaginationStatus::Idle { hit_timeline_start: false });
583
584                let Some(client) = self.client.get() else {
585                    return Err(EventCacheError::ClientDropped);
586                };
587
588                let room = client
589                    .get_room(room_id)
590                    .ok_or_else(|| EventCacheError::RoomNotFound { room_id: room_id.to_owned() })?;
591                let room_version_rules = room.clone_info().room_version_rules_or_default();
592
593                let room_state = RoomEventCacheState::new(
594                    room_id.to_owned(),
595                    room_version_rules,
596                    self.store.clone(),
597                    pagination_status.clone(),
598                )
599                .await?;
600
601                let timeline_is_not_empty =
602                    room_state.room_linked_chunk().revents().next().is_some();
603
604                // SAFETY: we must have subscribed before reaching this code, otherwise
605                // something is very wrong.
606                let auto_shrink_sender =
607                    self.auto_shrink_sender.get().cloned().expect(
608                        "we must have called `EventCache::subscribe()` before calling here.",
609                    );
610
611                let room_event_cache = RoomEventCache::new(
612                    self.client.clone(),
613                    room_state,
614                    pagination_status,
615                    room_id.to_owned(),
616                    auto_shrink_sender,
617                    self.room_event_cache_generic_update_sender.clone(),
618                );
619
620                by_room_guard.insert(room_id.to_owned(), room_event_cache.clone());
621
622                // If at least one event has been loaded, it means there is a timeline. Let's
623                // emit a generic update.
624                if timeline_is_not_empty {
625                    let _ = self.room_event_cache_generic_update_sender.send(
626                        RoomEventCacheGenericUpdate::UpdateTimeline { room_id: room_id.to_owned() },
627                    );
628                }
629
630                Ok(room_event_cache)
631            }
632        }
633    }
634}
635
636/// The result of a single back-pagination request.
637#[derive(Debug)]
638pub struct BackPaginationOutcome {
639    /// Did the back-pagination reach the start of the timeline?
640    pub reached_start: bool,
641
642    /// All the events that have been returned in the back-pagination
643    /// request.
644    ///
645    /// Events are presented in reverse order: the first element of the vec,
646    /// if present, is the most "recent" event from the chunk (or
647    /// technically, the last one in the topological ordering).
648    pub events: Vec<TimelineEvent>,
649}
650
651/// Represents an update of a room. It hides the details of
652/// [`RoomEventCacheUpdate`] by being more generic.
653///
654/// This is used by [`EventCache::subscribe_to_room_generic_updates`]. Please
655/// read it to learn more about the motivation behind this type.
656#[derive(Clone, Debug)]
657pub enum RoomEventCacheGenericUpdate {
658    /// The timeline has been updated, i.e. an event has been added, redacted,
659    /// removed, or reloaded.
660    UpdateTimeline {
661        /// The room ID owning the timeline.
662        room_id: OwnedRoomId,
663    },
664
665    /// The room has been cleared, all events have been deleted.
666    Clear {
667        /// The ID of the room that has been cleared.
668        room_id: OwnedRoomId,
669    },
670}
671
672impl RoomEventCacheGenericUpdate {
673    /// Get the room ID that has triggered this generic update.
674    pub fn room_id(&self) -> &RoomId {
675        match self {
676            Self::UpdateTimeline { room_id } => room_id,
677            Self::Clear { room_id } => room_id,
678        }
679    }
680}
681
682/// An update related to events happened in a room.
683#[derive(Debug, Clone)]
684pub enum RoomEventCacheUpdate {
685    /// The fully read marker has moved to a different event.
686    MoveReadMarkerTo {
687        /// Event at which the read marker is now pointing.
688        event_id: OwnedEventId,
689    },
690
691    /// The members have changed.
692    UpdateMembers {
693        /// Collection of ambiguity changes that room member events trigger.
694        ///
695        /// This is a map of event ID of the `m.room.member` event to the
696        /// details of the ambiguity change.
697        ambiguity_changes: BTreeMap<OwnedEventId, AmbiguityChange>,
698    },
699
700    /// The room has received updates for the timeline as _diffs_.
701    UpdateTimelineEvents {
702        /// Diffs to apply to the timeline.
703        diffs: Vec<VectorDiff<TimelineEvent>>,
704
705        /// Where the diffs are coming from.
706        origin: EventsOrigin,
707    },
708
709    /// The room has received new ephemeral events.
710    AddEphemeralEvents {
711        /// XXX: this is temporary, until read receipts are handled in the event
712        /// cache
713        events: Vec<Raw<AnySyncEphemeralRoomEvent>>,
714    },
715}
716
717/// Indicate where events are coming from.
718#[derive(Debug, Clone)]
719pub enum EventsOrigin {
720    /// Events are coming from a sync.
721    Sync,
722
723    /// Events are coming from pagination.
724    Pagination,
725
726    /// The cause of the change is purely internal to the cache.
727    Cache,
728}
729
730#[cfg(test)]
731mod tests {
732    use std::ops::Not;
733
734    use assert_matches::assert_matches;
735    use futures_util::FutureExt as _;
736    use matrix_sdk_base::{
737        linked_chunk::{ChunkIdentifier, LinkedChunkId, Position, Update},
738        sync::{JoinedRoomUpdate, RoomUpdates, Timeline},
739        RoomState,
740    };
741    use matrix_sdk_test::{async_test, event_factory::EventFactory};
742    use ruma::{event_id, room_id, serde::Raw, user_id};
743    use serde_json::json;
744
745    use super::{EventCacheError, RoomEventCacheGenericUpdate, RoomEventCacheUpdate};
746    use crate::test_utils::{assert_event_matches_msg, logged_in_client};
747
748    #[async_test]
749    async fn test_must_explicitly_subscribe() {
750        let client = logged_in_client(None).await;
751
752        let event_cache = client.event_cache();
753
754        // If I create a room event subscriber for a room before subscribing the event
755        // cache,
756        let room_id = room_id!("!omelette:fromage.fr");
757        let result = event_cache.for_room(room_id).await;
758
759        // Then it fails, because one must explicitly call `.subscribe()` on the event
760        // cache.
761        assert_matches!(result, Err(EventCacheError::NotSubscribedYet));
762    }
763
764    #[async_test]
765    async fn test_uniq_read_marker() {
766        let client = logged_in_client(None).await;
767        let room_id = room_id!("!galette:saucisse.bzh");
768        client.base_client().get_or_create_room(room_id, RoomState::Joined);
769
770        let event_cache = client.event_cache();
771
772        event_cache.subscribe().unwrap();
773
774        let mut generic_stream = event_cache.subscribe_to_room_generic_updates();
775        let (room_event_cache, _drop_handles) = event_cache.for_room(room_id).await.unwrap();
776        let (events, mut stream) = room_event_cache.subscribe().await;
777
778        assert!(events.is_empty());
779
780        // When sending multiple times the same read marker event,…
781        let read_marker_event = Raw::from_json_string(
782            json!({
783                "content": {
784                    "event_id": "$crepe:saucisse.bzh"
785                },
786                "room_id": "!galette:saucisse.bzh",
787                "type": "m.fully_read"
788            })
789            .to_string(),
790        )
791        .unwrap();
792        let account_data = vec![read_marker_event; 100];
793
794        room_event_cache
795            .inner
796            .handle_joined_room_update(JoinedRoomUpdate { account_data, ..Default::default() })
797            .await
798            .unwrap();
799
800        // … there's only one read marker update.
801        assert_matches!(
802            stream.recv().await.unwrap(),
803            RoomEventCacheUpdate::MoveReadMarkerTo { .. }
804        );
805
806        assert!(stream.recv().now_or_never().is_none());
807
808        // None, because an account data doesn't trigger a generic update.
809        assert!(generic_stream.recv().now_or_never().is_none());
810    }
811
812    #[async_test]
813    async fn test_get_event_by_id() {
814        let client = logged_in_client(None).await;
815        let room_id1 = room_id!("!galette:saucisse.bzh");
816        let room_id2 = room_id!("!crepe:saucisse.bzh");
817
818        client.base_client().get_or_create_room(room_id1, RoomState::Joined);
819        client.base_client().get_or_create_room(room_id2, RoomState::Joined);
820
821        let event_cache = client.event_cache();
822        event_cache.subscribe().unwrap();
823
824        // Insert two rooms with a few events.
825        let f = EventFactory::new().room(room_id1).sender(user_id!("@ben:saucisse.bzh"));
826
827        let eid1 = event_id!("$1");
828        let eid2 = event_id!("$2");
829        let eid3 = event_id!("$3");
830
831        let joined_room_update1 = JoinedRoomUpdate {
832            timeline: Timeline {
833                events: vec![
834                    f.text_msg("hey").event_id(eid1).into(),
835                    f.text_msg("you").event_id(eid2).into(),
836                ],
837                ..Default::default()
838            },
839            ..Default::default()
840        };
841
842        let joined_room_update2 = JoinedRoomUpdate {
843            timeline: Timeline {
844                events: vec![f.text_msg("bjr").event_id(eid3).into()],
845                ..Default::default()
846            },
847            ..Default::default()
848        };
849
850        let mut updates = RoomUpdates::default();
851        updates.joined.insert(room_id1.to_owned(), joined_room_update1);
852        updates.joined.insert(room_id2.to_owned(), joined_room_update2);
853
854        // Have the event cache handle them.
855        event_cache.inner.handle_room_updates(updates).await.unwrap();
856
857        // We can find the events in a single room.
858        let room1 = client.get_room(room_id1).unwrap();
859
860        let (room_event_cache, _drop_handles) = room1.event_cache().await.unwrap();
861
862        let found1 = room_event_cache.find_event(eid1).await.unwrap();
863        assert_event_matches_msg(&found1, "hey");
864
865        let found2 = room_event_cache.find_event(eid2).await.unwrap();
866        assert_event_matches_msg(&found2, "you");
867
868        // Retrieving the event with id3 from the room which doesn't contain it will
869        // fail…
870        assert!(room_event_cache.find_event(eid3).await.is_none());
871    }
872
873    #[async_test]
874    async fn test_save_event() {
875        let client = logged_in_client(None).await;
876        let room_id = room_id!("!galette:saucisse.bzh");
877
878        let event_cache = client.event_cache();
879        event_cache.subscribe().unwrap();
880
881        let f = EventFactory::new().room(room_id).sender(user_id!("@ben:saucisse.bzh"));
882        let event_id = event_id!("$1");
883
884        client.base_client().get_or_create_room(room_id, RoomState::Joined);
885        let room = client.get_room(room_id).unwrap();
886
887        let (room_event_cache, _drop_handles) = room.event_cache().await.unwrap();
888        room_event_cache.save_events([f.text_msg("hey there").event_id(event_id).into()]).await;
889
890        // Retrieving the event at the room-wide cache works.
891        assert!(room_event_cache.find_event(event_id).await.is_some());
892    }
893
894    #[async_test]
895    async fn test_generic_update_when_loading_rooms() {
896        // Create 2 rooms. One of them has data in the event cache storage.
897        let user = user_id!("@mnt_io:matrix.org");
898        let client = logged_in_client(None).await;
899        let room_id_0 = room_id!("!raclette:patate.ch");
900        let room_id_1 = room_id!("!fondue:patate.ch");
901
902        let event_factory = EventFactory::new().room(room_id_0).sender(user);
903
904        let event_cache = client.event_cache();
905        event_cache.subscribe().unwrap();
906
907        client.base_client().get_or_create_room(room_id_0, RoomState::Joined);
908        client.base_client().get_or_create_room(room_id_1, RoomState::Joined);
909
910        client
911            .event_cache_store()
912            .lock()
913            .await
914            .unwrap()
915            .handle_linked_chunk_updates(
916                LinkedChunkId::Room(room_id_0),
917                vec![
918                    // Non-empty items chunk.
919                    Update::NewItemsChunk {
920                        previous: None,
921                        new: ChunkIdentifier::new(0),
922                        next: None,
923                    },
924                    Update::PushItems {
925                        at: Position::new(ChunkIdentifier::new(0), 0),
926                        items: vec![event_factory
927                            .text_msg("hello")
928                            .sender(user)
929                            .event_id(event_id!("$ev0"))
930                            .into_event()],
931                    },
932                ],
933            )
934            .await
935            .unwrap();
936
937        let mut generic_stream = event_cache.subscribe_to_room_generic_updates();
938
939        // Room 0 has initial data, so it must trigger a generic update.
940        {
941            let _room_event_cache = event_cache.for_room(room_id_0).await.unwrap();
942
943            assert_matches!(
944                generic_stream.recv().await,
945                Ok(RoomEventCacheGenericUpdate::UpdateTimeline { room_id }) => {
946                    assert_eq!(room_id, room_id_0);
947                }
948            );
949        }
950
951        // Room 1 has NO initial data, so nothing should happen.
952        {
953            let _room_event_cache = event_cache.for_room(room_id_1).await.unwrap();
954
955            assert!(generic_stream.recv().now_or_never().is_none());
956        }
957    }
958
959    #[async_test]
960    async fn test_generic_update_when_paginating_room() {
961        // Create 1 room, with 4 chunks in the event cache storage.
962        let user = user_id!("@mnt_io:matrix.org");
963        let client = logged_in_client(None).await;
964        let room_id = room_id!("!raclette:patate.ch");
965
966        let event_factory = EventFactory::new().room(room_id).sender(user);
967
968        let event_cache = client.event_cache();
969        event_cache.subscribe().unwrap();
970
971        client.base_client().get_or_create_room(room_id, RoomState::Joined);
972
973        client
974            .event_cache_store()
975            .lock()
976            .await
977            .unwrap()
978            .handle_linked_chunk_updates(
979                LinkedChunkId::Room(room_id),
980                vec![
981                    // Empty chunk.
982                    Update::NewItemsChunk {
983                        previous: None,
984                        new: ChunkIdentifier::new(0),
985                        next: None,
986                    },
987                    // Empty chunk.
988                    Update::NewItemsChunk {
989                        previous: Some(ChunkIdentifier::new(0)),
990                        new: ChunkIdentifier::new(1),
991                        next: None,
992                    },
993                    // Non-empty items chunk.
994                    Update::NewItemsChunk {
995                        previous: Some(ChunkIdentifier::new(1)),
996                        new: ChunkIdentifier::new(2),
997                        next: None,
998                    },
999                    Update::PushItems {
1000                        at: Position::new(ChunkIdentifier::new(2), 0),
1001                        items: vec![event_factory
1002                            .text_msg("hello")
1003                            .sender(user)
1004                            .event_id(event_id!("$ev0"))
1005                            .into_event()],
1006                    },
1007                    // Non-empty items chunk.
1008                    Update::NewItemsChunk {
1009                        previous: Some(ChunkIdentifier::new(2)),
1010                        new: ChunkIdentifier::new(3),
1011                        next: None,
1012                    },
1013                    Update::PushItems {
1014                        at: Position::new(ChunkIdentifier::new(3), 0),
1015                        items: vec![event_factory
1016                            .text_msg("world")
1017                            .sender(user)
1018                            .event_id(event_id!("$ev1"))
1019                            .into_event()],
1020                    },
1021                ],
1022            )
1023            .await
1024            .unwrap();
1025
1026        let mut generic_stream = event_cache.subscribe_to_room_generic_updates();
1027
1028        // Room is initialised, it gets one event in the timeline.
1029        let (room_event_cache, _) = event_cache.for_room(room_id).await.unwrap();
1030
1031        assert_matches!(
1032            generic_stream.recv().await,
1033            Ok(RoomEventCacheGenericUpdate::UpdateTimeline { room_id: expected_room_id }) => {
1034                assert_eq!(room_id, expected_room_id);
1035            }
1036        );
1037
1038        let pagination = room_event_cache.pagination();
1039
1040        // Paginate, it gets one new event in the timeline.
1041        let pagination_outcome = pagination.run_backwards_once(1).await.unwrap();
1042
1043        assert_eq!(pagination_outcome.events.len(), 1);
1044        assert!(pagination_outcome.reached_start.not());
1045        assert_matches!(
1046            generic_stream.recv().await,
1047            Ok(RoomEventCacheGenericUpdate::UpdateTimeline { room_id: expected_room_id }) => {
1048                assert_eq!(room_id, expected_room_id);
1049            }
1050        );
1051
1052        // Paginate, it gets zero new event in the timeline.
1053        let pagination_outcome = pagination.run_backwards_once(1).await.unwrap();
1054
1055        assert!(pagination_outcome.events.is_empty());
1056        assert!(pagination_outcome.reached_start.not());
1057        assert!(generic_stream.recv().now_or_never().is_none());
1058
1059        // Paginate once more. Just checking our scenario is correct.
1060        let pagination_outcome = pagination.run_backwards_once(1).await.unwrap();
1061
1062        assert!(pagination_outcome.reached_start);
1063        assert!(generic_stream.recv().now_or_never().is_none());
1064    }
1065
1066    #[async_test]
1067    async fn test_for_room_when_room_is_not_found() {
1068        let client = logged_in_client(None).await;
1069        let room_id = room_id!("!raclette:patate.ch");
1070
1071        let event_cache = client.event_cache();
1072        event_cache.subscribe().unwrap();
1073
1074        // Room doesn't exist. It returns an error.
1075        assert_matches!(
1076            event_cache.for_room(room_id).await,
1077            Err(EventCacheError::RoomNotFound { room_id: not_found_room_id }) => {
1078                assert_eq!(room_id, not_found_room_id);
1079            }
1080        );
1081
1082        // Now create the room.
1083        client.base_client().get_or_create_room(room_id, RoomState::Joined);
1084
1085        // Room exists. Everything fine.
1086        assert!(event_cache.for_room(room_id).await.is_ok());
1087    }
1088}