Skip to main content

matrix_sdk/event_cache/caches/room/
state.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::iter::empty;
16
17use eyeball::SharedObservable;
18use eyeball_im::VectorDiff;
19use matrix_sdk_base::{
20    RoomInfoNotableUpdateReasons, apply_redaction,
21    deserialized_responses::{ThreadSummary, ThreadSummaryStatus},
22    event_cache::{Event, Gap, store::EventCacheStoreLockGuard},
23    linked_chunk::{
24        ChunkIdentifierGenerator, LinkedChunkId, OwnedLinkedChunkId, Position, Update, lazy_loader,
25    },
26    serde_helpers::extract_redaction_target,
27    sync::Timeline,
28};
29use matrix_sdk_common::executor::spawn;
30use ruma::{
31    EventId, OwnedEventId, OwnedRoomId, OwnedUserId,
32    events::{
33        receipt::ReceiptEventContent, relation::RelationType,
34        room::redaction::SyncRoomRedactionEvent,
35    },
36    room_version_rules::RoomVersionRules,
37};
38use tokio::sync::broadcast::Sender;
39use tracing::{debug, error, instrument, trace};
40
41#[cfg(feature = "e2e-encryption")]
42use super::super::super::redecryptor::MaybeResolvedEvent;
43use super::{
44    super::{
45        super::{
46            EventCacheError,
47            back_pagination_queue::BackPaginationQueue,
48            deduplicator::{DeduplicationOutcome, filter_duplicate_events},
49            persistence::{
50                find_event, find_event_relations, find_event_with_relations,
51                load_linked_chunk_metadata, send_updates_to_store,
52            },
53            states::{ReloadPreprocessing, StateLockReadGuard, StateLockWriteGuard},
54        },
55        EventLocation,
56        event_linked_chunk::EventLinkedChunk,
57        pagination::SharedPaginationStatus,
58        read_receipts::{
59            MaybeReceiptEventContent, RoomReadReceiptEventFilter, compute_unread_counts,
60        },
61        subscriber::SubscribersHandle,
62    },
63    RoomEventCacheLinkedChunkUpdate, RoomEventCacheUpdateSender, sort_positions_descending,
64};
65use crate::room::WeakRoom;
66
67pub struct RoomEventCacheState {
68    /// Whether thread support has been enabled for the event cache.
69    pub enabled_thread_support: bool,
70
71    /// The room this state relates to.
72    pub room_id: OwnedRoomId,
73
74    /// A weak reference to the actual room.
75    weak_room: WeakRoom,
76
77    /// The user's own user id.
78    pub own_user_id: OwnedUserId,
79
80    /// The loaded events for the current room, that is, the in-memory
81    /// linked chunk for this room.
82    room_linked_chunk: EventLinkedChunk,
83
84    pagination_status: SharedObservable<SharedPaginationStatus>,
85
86    /// A clone of [`super::RoomEventCacheInner::update_sender`].
87    ///
88    /// This is used only by the [`RoomEventCacheStateLock::read`] and
89    /// [`RoomEventCacheStateLock::write`] when the state must be reset.
90    pub update_sender: RoomEventCacheUpdateSender,
91
92    /// A clone of
93    /// [`super::super::EventCacheInner::linked_chunk_update_sender`].
94    pub(super) linked_chunk_update_sender: Sender<RoomEventCacheLinkedChunkUpdate>,
95
96    /// The rules for the version of this room.
97    room_version_rules: RoomVersionRules,
98
99    /// Have we ever waited for a previous-batch-token to come from sync, in
100    /// the context of pagination? We do this at most once per room,
101    /// the first time we try to run backward pagination. We reset
102    /// that upon clearing the timeline events.
103    waited_for_initial_prev_token: bool,
104
105    /// A handle for subscribers.
106    subscribers_handle: SubscribersHandle,
107
108    /// A handle to the shared back-pagination queue.
109    back_pagination_queue: Option<BackPaginationQueue>,
110}
111
112impl RoomEventCacheState {
113    /// Create a new state, or reload it from storage if it's been enabled.
114    ///
115    /// Not all events are going to be loaded. Only a portion of them. The
116    /// [`EventLinkedChunk`] relies on a [`LinkedChunk`] to store all
117    /// events. Only the last chunk will be loaded. It means the
118    /// events are loaded from the most recent to the oldest. To
119    /// load more events, see [`RoomPagination`].
120    ///
121    /// [`LinkedChunk`]: matrix_sdk_common::linked_chunk::LinkedChunk
122    /// [`RoomPagination`]: super::RoomPagination
123    #[allow(clippy::too_many_arguments)]
124    pub async fn new(
125        own_user_id: OwnedUserId,
126        room_id: OwnedRoomId,
127        weak_room: WeakRoom,
128        room_version_rules: RoomVersionRules,
129        enabled_thread_support: bool,
130        update_sender: RoomEventCacheUpdateSender,
131        linked_chunk_update_sender: Sender<RoomEventCacheLinkedChunkUpdate>,
132        store_guard: EventCacheStoreLockGuard,
133        pagination_status: SharedObservable<SharedPaginationStatus>,
134        back_pagination_queue: Option<BackPaginationQueue>,
135    ) -> Result<Self, EventCacheError> {
136        let linked_chunk_id = LinkedChunkId::Room(&room_id);
137
138        // Load the full linked chunk's metadata, so as to feed the order tracker.
139        //
140        // If loading the full linked chunk failed, we'll clear the event cache, as it
141        // indicates that at some point, there's some malformed data.
142        let full_linked_chunk_metadata =
143            match load_linked_chunk_metadata(&store_guard, linked_chunk_id).await {
144                Ok(metas) => metas,
145                Err(err) => {
146                    error!("error when loading a linked chunk's metadata from the store: {err}");
147
148                    // Try to clear storage for this room.
149                    store_guard
150                        .handle_linked_chunk_updates(linked_chunk_id, vec![Update::Clear])
151                        .await?;
152
153                    // Restart with an empty linked chunk.
154                    None
155                }
156            };
157
158        let linked_chunk = match store_guard
159            .load_last_chunk(linked_chunk_id)
160            .await
161            .map_err(EventCacheError::from)
162            .and_then(|(last_chunk, chunk_identifier_generator)| {
163                lazy_loader::from_last_chunk(last_chunk, chunk_identifier_generator)
164                    .map_err(EventCacheError::from)
165            }) {
166            Ok(linked_chunk) => linked_chunk,
167            Err(err) => {
168                error!("error when loading a linked chunk's latest chunk from the store: {err}");
169
170                // Try to clear storage for this room.
171                store_guard
172                    .handle_linked_chunk_updates(linked_chunk_id, vec![Update::Clear])
173                    .await?;
174
175                None
176            }
177        };
178
179        Ok(RoomEventCacheState {
180            own_user_id,
181            enabled_thread_support,
182            room_id,
183            weak_room,
184            room_linked_chunk: EventLinkedChunk::with_initial_linked_chunk(
185                linked_chunk,
186                full_linked_chunk_metadata,
187            ),
188            pagination_status,
189            update_sender,
190            linked_chunk_update_sender,
191            room_version_rules,
192            waited_for_initial_prev_token: false,
193            subscribers_handle: Default::default(),
194            back_pagination_queue,
195        })
196    }
197
198    /// Return a read-only reference to the underlying room linked chunk.
199    pub fn room_linked_chunk(&self) -> &EventLinkedChunk {
200        &self.room_linked_chunk
201    }
202}
203
204impl<'a> StateLockReadGuard<'a, RoomEventCacheState> {
205    /// Return a reference to subscribers handle.
206    pub fn subscribers_handle(&self) -> &SubscribersHandle {
207        &self.state.subscribers_handle
208    }
209
210    /// See documentation of [`find_event`].
211    pub async fn find_event(
212        &self,
213        event_id: &EventId,
214    ) -> Result<Option<(EventLocation, Event)>, EventCacheError> {
215        find_event(event_id, &self.room_id, &self.room_linked_chunk, &self.store).await
216    }
217
218    /// See documentation of [`find_event_with_relations`].
219    pub async fn find_event_with_relations(
220        &self,
221        event_id: &EventId,
222        filters: Option<Vec<RelationType>>,
223    ) -> Result<Option<(Event, Vec<Event>)>, EventCacheError> {
224        find_event_with_relations(
225            event_id,
226            &self.room_id,
227            filters,
228            &self.room_linked_chunk,
229            &self.store,
230        )
231        .await
232    }
233
234    /// See documentation of [`find_event_relations`].
235    pub async fn find_event_relations(
236        &self,
237        event_id: &EventId,
238        filters: Option<Vec<RelationType>>,
239    ) -> Result<Vec<Event>, EventCacheError> {
240        find_event_relations(event_id, &self.room_id, filters, &self.room_linked_chunk, &self.store)
241            .await
242    }
243
244    //// Find a single event in this room, starting from the most recent event.
245    ///
246    /// The `predicate` receives the current event as its single argument.
247    ///
248    /// **Warning**! It looks into the loaded events from the in-memory
249    /// linked chunk **only**. It doesn't look inside the storage,
250    /// contrary to [`Self::find_event`].
251    pub fn rfind_map_event_in_memory_by<O, P>(&self, mut predicate: P) -> Option<O>
252    where
253        P: FnMut(&Event) -> Option<O>,
254    {
255        self.state.room_linked_chunk.revents().find_map(|(_, event)| predicate(event))
256    }
257
258    #[cfg(test)]
259    pub fn is_dirty(&self) -> bool {
260        EventCacheStoreLockGuard::is_dirty(&self.store)
261    }
262}
263
264impl<'a> StateLockWriteGuard<'a, RoomEventCacheState> {
265    /// Return a mutable reference to the underlying room linked chunk.
266    pub fn room_linked_chunk_mut(&mut self) -> &mut EventLinkedChunk {
267        &mut self.state.room_linked_chunk
268    }
269
270    /// Get the `waited_for_initial_prev_token` value.
271    pub fn waited_for_initial_prev_token(&self) -> bool {
272        self.state.waited_for_initial_prev_token
273    }
274
275    /// Get a mutable reference to the `waited_for_initial_prev_token` value.
276    pub fn waited_for_initial_prev_token_mut(&mut self) -> &mut bool {
277        &mut self.state.waited_for_initial_prev_token
278    }
279
280    /// See documentation of [`find_event`].
281    pub async fn find_event(
282        &self,
283        event_id: &EventId,
284    ) -> Result<Option<(EventLocation, Event)>, EventCacheError> {
285        find_event(event_id, &self.room_id, &self.room_linked_chunk, &self.store).await
286    }
287
288    /// Reload the room: only the last events will be reloaded, shrinking the
289    /// in-memory size of the cache.
290    ///
291    /// If `preprocessing` is set to [`ReloadPreprocessing::ForgetAll`], all
292    /// events will be erased before reloaded.
293    #[must_use = "Propagate `VectorDiff` updates via `RoomEventCacheUpdate`"]
294    pub async fn reload(
295        &mut self,
296        preprocessing: ReloadPreprocessing,
297    ) -> Result<Vec<VectorDiff<Event>>, EventCacheError> {
298        match preprocessing {
299            ReloadPreprocessing::ForgetAll => {
300                // Clear the `LinkedChunk` and broadcast the updates to the store.
301                self.room_linked_chunk_mut().reset();
302                self.propagate_changes().await?;
303
304                // Reset the pagination state too: pretend we never waited for the initial
305                // prev-batch token, and indicate that we're not at the start of the timeline,
306                // since we don't know about that anymore.
307                *self.waited_for_initial_prev_token_mut() = false;
308
309                // Note: this may cancel an ongoing pagination.
310                self.state
311                    .pagination_status
312                    .set(SharedPaginationStatus::Idle { hit_timeline_start: false });
313            }
314
315            ReloadPreprocessing::None => {}
316        }
317
318        self.shrink_to_last_reloaded_chunk().await?;
319
320        Ok(self.room_linked_chunk_mut().updates_as_vector_diffs())
321    }
322
323    /// If storage is enabled, unload all the chunks, then reloads only the
324    /// last one.
325    ///
326    /// If storage's enabled, return a diff update that starts with a clear
327    /// of all events; as a result, the caller may override any
328    /// pending diff updates with the result of this function.
329    ///
330    /// Otherwise, returns `None`.
331    #[instrument(skip(self))]
332    async fn shrink_to_last_reloaded_chunk(&mut self) -> Result<(), EventCacheError> {
333        // Attempt to load the last chunk.
334        let linked_chunk_id = LinkedChunkId::Room(&self.state.room_id);
335
336        let full_linked_chunk_metadata =
337            match load_linked_chunk_metadata(&self.store, linked_chunk_id).await {
338                Ok(metas) => metas,
339                Err(err) => {
340                    error!("error when reloading a linked chunk's metadata from the store: {err}");
341
342                    // Try to clear storage for this room.
343                    self.store
344                        .handle_linked_chunk_updates(linked_chunk_id, vec![Update::Clear])
345                        .await?;
346
347                    // Restart with an empty linked chunk.
348                    None
349                }
350            };
351
352        let (last_chunk, chunk_identifier_generator) =
353            match self.store.load_last_chunk(linked_chunk_id).await {
354                Ok(pair) => pair,
355
356                Err(err) => {
357                    // If loading the last chunk failed, clear the entire linked chunk.
358                    error!("error when reloading a linked chunk from memory: {err}");
359
360                    // Clear storage for this room.
361                    self.store
362                        .handle_linked_chunk_updates(linked_chunk_id, vec![Update::Clear])
363                        .await?;
364
365                    // Restart with an empty linked chunk.
366                    (None, ChunkIdentifierGenerator::new_from_scratch())
367                }
368            };
369
370        debug!("unloading the linked chunk, and resetting it to its last chunk");
371
372        // Remove all the chunks from the linked chunks, except for the last one, and
373        // updates the chunk identifier generator.
374        if let Err(err) = self.state.room_linked_chunk.shrink_to_last_reloaded_chunk(
375            last_chunk,
376            chunk_identifier_generator,
377            full_linked_chunk_metadata,
378        ) {
379            error!("error when replacing the linked chunk: {err}");
380
381            self.state.room_linked_chunk.reset();
382            self.propagate_changes().await?;
383
384            // Reset the pagination state too: pretend we never waited for the initial
385            // prev-batch token, and indicate that we're not at the start of the
386            // timeline, since we don't know about that anymore.
387            self.state.waited_for_initial_prev_token = false;
388
389            // Note: this may cancel an ongoing pagination.
390            self.state
391                .pagination_status
392                .set(SharedPaginationStatus::Idle { hit_timeline_start: false });
393
394            return Ok(());
395        }
396
397        // Let pagination observers know that we may have not reached the start of the
398        // timeline. This may cancel an ongoing pagination.
399        self.state
400            .pagination_status
401            .set(SharedPaginationStatus::Idle { hit_timeline_start: false });
402
403        Ok(())
404    }
405
406    /// Automatically shrink the room if there are no more subscribers, as
407    /// indicated by the atomic number of active subscribers.
408    #[must_use = "Propagate `VectorDiff` updates via `RoomEventCacheUpdate`"]
409    pub async fn auto_shrink_if_no_subscribers(
410        &mut self,
411    ) -> Result<Option<Vec<VectorDiff<Event>>>, EventCacheError> {
412        let number_of_subscribers = self.state.subscribers_handle.count();
413
414        trace!(number_of_subscribers, "received request to auto-shrink");
415
416        if number_of_subscribers == 0 {
417            // There is no more subscribers listening to this cache, we can shrink the state
418            // to its last chunk to save memory.
419            //
420            // In theory, between the condition (`… == 0`) and this instruction, a new
421            // subscriber could be created, creating a race, except that this method takes a
422            // `&mut`, ensuring an exclusive access to the state, ensuring no other
423            // subscribers can be created.
424            self.shrink_to_last_reloaded_chunk().await?;
425
426            Ok(Some(self.state.room_linked_chunk.updates_as_vector_diffs()))
427        } else {
428            Ok(None)
429        }
430    }
431
432    /// Remove events by their position, in `EventLinkedChunk` and in
433    /// `EventCacheStore`.
434    ///
435    /// This method is purposely isolated because it must ensure that
436    /// positions are sorted appropriately or it can be disastrous.
437    #[instrument(skip_all)]
438    pub async fn remove_events(
439        &mut self,
440        in_memory_events: Vec<(OwnedEventId, Position)>,
441        in_store_events: Vec<(OwnedEventId, Position)>,
442    ) -> Result<(), EventCacheError> {
443        // In-store events.
444        if !in_store_events.is_empty() {
445            let mut positions = in_store_events
446                .into_iter()
447                .map(|(_event_id, position)| position)
448                .collect::<Vec<_>>();
449
450            sort_positions_descending(&mut positions);
451
452            let updates =
453                positions.into_iter().map(|pos| Update::RemoveItem { at: pos }).collect::<Vec<_>>();
454
455            self.apply_store_only_updates(updates).await?;
456        }
457
458        // In-memory events.
459        if in_memory_events.is_empty() {
460            // Nothing else to do, return early.
461            return Ok(());
462        }
463
464        // `remove_events_by_position` is responsible of sorting positions.
465        self.state
466            .room_linked_chunk
467            .remove_events_by_position(
468                in_memory_events.into_iter().map(|(_event_id, position)| position).collect(),
469            )
470            .expect("failed to remove an event");
471
472        self.propagate_changes().await
473    }
474
475    pub(super) async fn propagate_changes(&mut self) -> Result<(), EventCacheError> {
476        let updates = self.state.room_linked_chunk.store_updates().take();
477
478        self.send_updates_to_store(updates).await
479    }
480
481    /// Apply some updates that are effective only on the store itself.
482    ///
483    /// This method should be used only for updates that happen *outside*
484    /// the in-memory linked chunk. Such updates must be applied
485    /// onto the ordering tracker as well as to the persistent
486    /// storage.
487    async fn apply_store_only_updates(
488        &mut self,
489        updates: Vec<Update<Event, Gap>>,
490    ) -> Result<(), EventCacheError> {
491        self.state.room_linked_chunk.order_tracker.map_updates(&updates);
492        self.send_updates_to_store(updates).await
493    }
494
495    async fn send_updates_to_store(
496        &mut self,
497        updates: Vec<Update<Event, Gap>>,
498    ) -> Result<(), EventCacheError> {
499        let linked_chunk_id = OwnedLinkedChunkId::Room(self.state.room_id.clone());
500
501        send_updates_to_store(
502            &self.store,
503            linked_chunk_id,
504            &self.state.linked_chunk_update_sender,
505            updates,
506        )
507        .await
508    }
509
510    /// Handle the result of a sync.
511    ///
512    /// It may send room event cache updates to the given sender, if it
513    /// generated any of those.
514    ///
515    /// Returns `true` for the first part of the tuple if a new gap
516    /// (previous-batch token) has been inserted, `false` otherwise.
517    #[must_use = "Propagate `VectorDiff` updates via `RoomEventCacheUpdate`"]
518    pub async fn handle_sync(
519        &mut self,
520        mut timeline: Timeline,
521        read_receipt_event: &MaybeReceiptEventContent,
522    ) -> Result<(bool, Vec<VectorDiff<Event>>), EventCacheError> {
523        let mut prev_batch_token = timeline.prev_batch.take();
524
525        let DeduplicationOutcome {
526            all_events: events,
527            in_memory_duplicated_event_ids,
528            in_store_duplicated_event_ids,
529            non_empty_all_duplicates: all_duplicates,
530        } = filter_duplicate_events(
531            &self.state.own_user_id,
532            &self.store,
533            LinkedChunkId::Room(&self.state.room_id),
534            &self.state.room_linked_chunk,
535            timeline.events,
536        )
537        .await?;
538
539        // If the timeline isn't limited, and we already knew about some past events,
540        // then this definitely knows what the timeline head is (either we know
541        // about all the events persisted in storage, or we have a gap
542        // somewhere). In this case, we can ditch the previous-batch
543        // token, which is an optimization to avoid unnecessary future back-pagination
544        // requests.
545        //
546        // We can also ditch it if we knew about all the events that came from sync,
547        // namely, they were all deduplicated. In this case, using the
548        // previous-batch token would only result in fetching other events we
549        // knew about. This is slightly incorrect in the presence of
550        // network splits, but this has shown to be Good Enough™.
551        if !timeline.limited && self.state.room_linked_chunk.events().next().is_some()
552            || all_duplicates
553        {
554            prev_batch_token = None;
555        }
556
557        if all_duplicates {
558            // No new events and no gap (per the previous check), thus no need to change the
559            // room state. We're done!
560            //
561            // We might have a new read receipt, though! If that's the case, handle it for
562            // unread counts tracking.
563            //
564            // Post-process the ephemeral events.
565            self.post_process_upserted_events(empty(), read_receipt_event.as_ref()).await?;
566
567            return Ok((false, Vec::new()));
568        }
569
570        let has_new_gap = prev_batch_token.is_some();
571
572        // If we've never waited for an initial previous-batch token, and we've now
573        // inserted a gap, no need to wait for a previous-batch token later.
574        if !self.state.waited_for_initial_prev_token && has_new_gap {
575            self.state.waited_for_initial_prev_token = true;
576        }
577
578        // Remove the old duplicated events.
579        //
580        // We don't have to worry the removals can change the position of the existing
581        // events, because we are pushing all _new_ `events` at the back.
582        self.remove_events(in_memory_duplicated_event_ids, in_store_duplicated_event_ids).await?;
583
584        self.state.room_linked_chunk.push_live_events(
585            prev_batch_token.map(|prev_token| Gap { token: prev_token }),
586            &events,
587        );
588
589        // Update the store.
590        self.propagate_changes().await?;
591
592        // Post-process newly inserted events.
593        self.post_process_upserted_events(events.iter(), read_receipt_event.as_ref()).await?;
594
595        if timeline.limited && has_new_gap {
596            // If there was a previous batch token for a limited timeline, unload the chunks
597            // so it only contains the last one; otherwise, there might be a
598            // valid gap in between, and observers may not render it (yet).
599            //
600            // We must do this *after* persisting these events to storage.
601            self.shrink_to_last_reloaded_chunk().await?;
602        }
603
604        let timeline_event_diffs = self.room_linked_chunk.updates_as_vector_diffs();
605
606        Ok((has_new_gap, timeline_event_diffs))
607    }
608
609    // --------------------------------------------
610    // utility methods
611    // --------------------------------------------
612
613    /// Post-process newly inserted or updated events.
614    pub(super) async fn post_process_upserted_events<'i, I>(
615        &mut self,
616        events: I,
617        receipt_event: Option<&ReceiptEventContent>,
618    ) -> Result<(), EventCacheError>
619    where
620        I: Iterator<Item = &'i Event>,
621    {
622        for event in events {
623            self.maybe_apply_new_redaction(event).await?;
624
625            // Save a bundled thread event, if there was one.
626            if let Some(bundled_thread) = event.bundled_latest_thread_event() {
627                self.save_events([bundled_thread]).await?;
628            }
629        }
630
631        self.update_read_receipts(receipt_event).await?;
632
633        Ok(())
634    }
635
636    /// Update read receipts for all events in the room, based on the current
637    /// state of the in-memory linked chunk.
638    pub async fn update_read_receipts(
639        &mut self,
640        receipt_event: Option<&ReceiptEventContent>,
641    ) -> Result<(), EventCacheError> {
642        let Some(room) = self.state.weak_room.get() else {
643            debug!("can't update read receipts: client's closing");
644            return Ok(());
645        };
646
647        let prev_read_receipts = room.read_receipts().clone();
648        let mut read_receipts = prev_read_receipts.clone();
649
650        let client = room.client();
651        let event_filter = RoomReadReceiptEventFilter::new(&self.state, client.state_store());
652
653        compute_unread_counts(
654            &self.state.own_user_id,
655            receipt_event,
656            &self.state.room_linked_chunk,
657            &event_filter,
658            &mut read_receipts,
659            self.state.back_pagination_queue.as_ref(),
660        )
661        .await;
662
663        if prev_read_receipts != read_receipts {
664            // The read receipt has changed! Do a little dance to update the `RoomInfo` in
665            // the state store, and then in the room itself, so that observers
666            // can be notified of the change.
667            let result = room
668                .update_and_save_room_info(|mut room_info| {
669                    room_info.set_read_receipts(read_receipts);
670                    (room_info, RoomInfoNotableUpdateReasons::READ_RECEIPT)
671                })
672                .await;
673
674            if let Err(error) = result {
675                error!(room_id = ?room.room_id(), ?error, "Failed to save the changes");
676            }
677        }
678
679        Ok(())
680    }
681
682    /// Update a thread summary on the given thread root, if needs be.
683    #[must_use = "Propagate `VectorDiff` updates via `RoomEventCacheUpdate`"]
684    pub async fn update_thread_summary(
685        &mut self,
686        thread_id: &EventId,
687        new_thread_summary: Option<ThreadSummary>,
688    ) -> Result<Vec<VectorDiff<Event>>, EventCacheError> {
689        let Some((location, mut thread_root_event)) = self.find_event(thread_id).await? else {
690            trace!(%thread_id, "thread root event is missing from the room linked chunk");
691            return Ok(Vec::new());
692        };
693
694        // Trigger an update to observers.
695        trace!(%thread_id, "updating thread summary: {new_thread_summary:?}");
696        thread_root_event.thread_summary = ThreadSummaryStatus::from_opt(new_thread_summary);
697        self.replace_event_at(location, thread_root_event).await?;
698
699        Ok(self.room_linked_chunk.updates_as_vector_diffs())
700    }
701
702    /// Replaces a single event, be it saved in memory or in the store.
703    ///
704    /// If it was saved in memory, this will emit a notification to
705    /// observers that a single item has been replaced. Otherwise,
706    /// such a notification is not emitted, because observers are
707    /// unlikely to observe the store updates directly.
708    pub async fn replace_event_at(
709        &mut self,
710        location: EventLocation,
711        event: Event,
712    ) -> Result<(), EventCacheError> {
713        match location {
714            EventLocation::Memory(position) => {
715                self.state
716                    .room_linked_chunk
717                    .replace_event_at(position, event)
718                    .expect("should have been a valid position of an item");
719                // We just changed the in-memory representation; synchronize this with
720                // the store.
721                self.propagate_changes().await?;
722            }
723            EventLocation::Store => {
724                self.save_events([event]).await?;
725            }
726        }
727
728        Ok(())
729    }
730
731    /// If the given event is a redaction, try to retrieve the
732    /// to-be-redacted event in the chunk, and replace it by the
733    /// redacted form.
734    #[instrument(skip_all)]
735    async fn maybe_apply_new_redaction(&mut self, event: &Event) -> Result<(), EventCacheError> {
736        let Some(target_event_id) =
737            extract_redaction_target(event.raw(), &self.room_version_rules.redaction)
738        else {
739            trace!("missing target event id from the redaction event");
740            return Ok(());
741        };
742
743        // Replace the redacted event by a redacted form, if we knew about it.
744        let Some((location, mut target_event)) = self.find_event(&target_event_id).await? else {
745            trace!("redacted event is missing from the linked chunk");
746            return Ok(());
747        };
748
749        let target_event_raw = target_event.raw();
750
751        // Don't redact already redacted events.
752        if let Ok(deserialized) = target_event_raw.deserialize()
753            && deserialized.is_redacted()
754        {
755            return Ok(());
756        }
757
758        if let Some(redacted_event) = apply_redaction(
759            target_event_raw,
760            event.raw().cast_ref_unchecked::<SyncRoomRedactionEvent>(),
761            &self.room_version_rules.redaction,
762        ) {
763            // It's safe to cast `redacted_event` here:
764            // - either the event was an `AnyTimelineEvent` cast to `AnySyncTimelineEvent`
765            //   when calling .raw(), so it's still one under the hood.
766            // - or it wasn't, and it's a plain `AnySyncTimelineEvent` in this case.
767            target_event.replace_raw(redacted_event.cast_unchecked());
768
769            self.replace_event_at(location, target_event.clone()).await?;
770        }
771
772        Ok(())
773    }
774
775    /// Try to locate the events in the linked chunk corresponding to the given
776    /// list of resolved events, and replace them, while alerting observers
777    /// about the update.
778    #[cfg(feature = "e2e-encryption")]
779    #[must_use = "Propagate `VectorDiff` updates via `TimelineVectorDiffs`"]
780    pub(in super::super::super) async fn replace_in_memory_utds(
781        &mut self,
782        resolved_events: &[MaybeResolvedEvent],
783    ) -> Result<Option<Vec<VectorDiff<Event>>>, EventCacheError> {
784        Ok(if self.room_linked_chunk_mut().replace_utds(resolved_events) {
785            // Drain the updates to the store, events have already been updated with
786            // `save_events`!
787            let _ = self.room_linked_chunk_mut().store_updates().take();
788
789            self.post_process_upserted_events(
790                resolved_events.iter().filter_map(|resolved_event| resolved_event.as_resolved()),
791                // Read receipt events aren't encrypted, so we can't have decrypted a new
792                // one here. As a result, we don't have any new receipt events to
793                // post-process, so we can just pass `None` here.
794                //
795                // Note: read receipts may be updated anyhow in the post-processing step,
796                // as the redecryption may have decrypted some events that don't count as
797                // unreads.
798                None,
799            )
800            .await?;
801
802            Some(self.room_linked_chunk_mut().updates_as_vector_diffs())
803        } else {
804            None
805        })
806    }
807
808    /// Save events into the database, without notifying observers.
809    pub async fn save_events(
810        &mut self,
811        events: impl IntoIterator<Item = Event>,
812    ) -> Result<(), EventCacheError> {
813        let store = self.store.clone();
814        let room_id = self.state.room_id.clone();
815        let events = events.into_iter().collect::<Vec<_>>();
816
817        // Spawn a task so the save is uninterrupted by task cancellation.
818        spawn(async move {
819            for event in events {
820                store.save_event(&room_id, event).await?;
821            }
822            super::Result::Ok(())
823        })
824        .await
825        .expect("joining failed")?;
826
827        Ok(())
828    }
829
830    #[cfg(test)]
831    pub fn is_dirty(&self) -> bool {
832        EventCacheStoreLockGuard::is_dirty(&self.store)
833    }
834}
835
836#[cfg(test)]
837mod tests {
838    use matrix_sdk_base::RoomState;
839    use matrix_sdk_test::{async_test, event_factory::EventFactory};
840    use ruma::{event_id, room_id, user_id};
841
842    use crate::test_utils::logged_in_client;
843
844    #[async_test]
845    async fn test_save_event() {
846        let client = logged_in_client(None).await;
847        let room_id = room_id!("!galette:saucisse.bzh");
848
849        let event_cache = client.event_cache();
850        event_cache.subscribe().unwrap();
851
852        let f = EventFactory::new().room(room_id).sender(user_id!("@ben:saucisse.bzh"));
853        let event_id = event_id!("$1");
854
855        client.base_client().get_or_create_room(room_id, RoomState::Joined);
856        let room = client.get_room(room_id).unwrap();
857
858        let (room_event_cache, _drop_handles) = room.event_cache().await.unwrap();
859        room_event_cache
860            .inner
861            .state
862            .write()
863            .await
864            .unwrap()
865            .save_events([f.text_msg("hey there").event_id(event_id).into()])
866            .await
867            .unwrap();
868
869        // Retrieving the event at the room-wide cache works.
870        assert!(room_event_cache.find_event(event_id).await.unwrap().is_some());
871    }
872}