Skip to main content

matrix_sdk/event_cache/caches/thread/
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::{AsyncLock, ObservableWriteGuard, SharedObservable};
18use eyeball_im::VectorDiff;
19use matrix_sdk_base::{
20    apply_redaction, check_validity_of_replacement_events,
21    deserialized_responses::ThreadSummary,
22    event_cache::{Event, Gap, store::EventCacheStoreLockGuard, thread::ThreadInfo},
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, Result,
47            deduplicator::{DeduplicationOutcome, filter_duplicate_events},
48            persistence::{
49                find_event, find_event_with_relations, load_linked_chunk_metadata,
50                send_updates_to_store,
51            },
52            states::{ReloadPreprocessing, StateLockReadGuard, StateLockWriteGuard},
53        },
54        EventLocation,
55        event_linked_chunk::{EventLinkedChunk, sort_positions_descending},
56        read_receipts::{
57            MaybeReceiptEventContent, ThreadReadReceiptEventFilter, compute_unread_counts,
58        },
59        room::RoomEventCacheLinkedChunkUpdate,
60        subscriber::SubscribersHandle,
61    },
62    ThreadEventCacheUpdateSender,
63};
64use crate::room::WeakRoom;
65
66pub struct ThreadEventCacheState {
67    /// The room owning this thread.
68    pub room_id: OwnedRoomId,
69
70    /// The ID of the thread root event, which is the first event in the thread
71    /// (and eventually the first in the linked chunk).
72    pub thread_id: OwnedEventId,
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 rules for the version of this room.
81    room_version_rules: RoomVersionRules,
82
83    /// The linked chunk for this thread.
84    thread_linked_chunk: EventLinkedChunk,
85
86    /// The information related to this thread, [`ThreadInfo`].
87    pub thread_info: SharedObservable<ThreadInfo, AsyncLock>,
88
89    /// A clone of [`super::ThreadEventCacheInner::update_sender`].
90    ///
91    /// This is used only by the [`LockedThreadEventCacheState::read`] and
92    /// [`LockedThreadEventCacheState::write`] when the state must be reset.
93    pub update_sender: ThreadEventCacheUpdateSender,
94
95    /// A sender for the globally observable linked chunk updates that happened
96    /// during a sync or a back-pagination.
97    ///
98    /// See also [`super::super::EventCacheInner::linked_chunk_update_sender`].
99    linked_chunk_update_sender: Sender<RoomEventCacheLinkedChunkUpdate>,
100
101    /// Have we ever waited for a previous-batch-token to come from sync, in
102    /// the context of pagination? We do this at most once per room/thread (?),
103    /// the first time we try to run backward pagination. We reset
104    /// that upon clearing the timeline events.
105    waited_for_initial_prev_token: bool,
106
107    /// A handle for subscribers.
108    subscribers_handle: SubscribersHandle,
109}
110
111impl ThreadEventCacheState {
112    /// Create a new state, or reload it from storage if it's been enabled.
113    ///
114    /// Not all events are going to be loaded. Only a portion of them. The
115    /// [`EventLinkedChunk`] relies on a [`LinkedChunk`] to store all
116    /// events. Only the last chunk will be loaded. It means the
117    /// events are loaded from the most recent to the oldest. To
118    /// load more events, see [`ThreadPagination`].
119    ///
120    /// [`LinkedChunk`]: matrix_sdk_common::linked_chunk::LinkedChunk
121    /// [`ThreadPagination`]: super::pagination::ThreadPagination
122    #[allow(clippy::too_many_arguments)]
123    pub async fn new(
124        room_id: OwnedRoomId,
125        thread_id: OwnedEventId,
126        weak_room: WeakRoom,
127        own_user_id: OwnedUserId,
128        room_version_rules: RoomVersionRules,
129        store_guard: EventCacheStoreLockGuard,
130        update_sender: ThreadEventCacheUpdateSender,
131        linked_chunk_update_sender: Sender<RoomEventCacheLinkedChunkUpdate>,
132    ) -> Result<Self> {
133        let linked_chunk_id = LinkedChunkId::Thread(&room_id, &thread_id);
134
135        // Load the thread info.
136        //
137        // It will register the thread in the list of threads. It does nothing regarding
138        // events or linked chunks.
139        let thread_info = store_guard.load_thread_info(&room_id, &thread_id).await?;
140
141        // Load the full linked chunk's metadata, so as to feed the order tracker.
142        //
143        // If loading the full linked chunk failed, we'll clear the event cache, as it
144        // indicates that at some point, there's some malformed data.
145        let full_linked_chunk_metadata =
146            match load_linked_chunk_metadata(&store_guard, linked_chunk_id).await {
147                Ok(metas) => metas,
148                Err(err) => {
149                    error!("error when loading a linked chunk's metadata from the store: {err}");
150
151                    // Try to clear storage for this thread.
152                    store_guard
153                        .handle_linked_chunk_updates(linked_chunk_id, vec![Update::Clear])
154                        .await?;
155
156                    // Restart with an empty linked chunk.
157                    None
158                }
159            };
160
161        let linked_chunk = match store_guard
162            .load_last_chunk(linked_chunk_id)
163            .await
164            .map_err(EventCacheError::from)
165            .and_then(|(last_chunk, chunk_identifier_generator)| {
166                lazy_loader::from_last_chunk(last_chunk, chunk_identifier_generator)
167                    .map_err(EventCacheError::from)
168            }) {
169            Ok(linked_chunk) => linked_chunk,
170            Err(err) => {
171                error!("error when loading a linked chunk's latest chunk from the store: {err}");
172
173                // Try to clear storage for this thread.
174                store_guard
175                    .handle_linked_chunk_updates(linked_chunk_id, vec![Update::Clear])
176                    .await?;
177
178                None
179            }
180        };
181
182        Ok(ThreadEventCacheState {
183            room_id,
184            thread_id,
185            weak_room,
186            own_user_id,
187            room_version_rules,
188            thread_linked_chunk: EventLinkedChunk::with_initial_linked_chunk(
189                linked_chunk,
190                full_linked_chunk_metadata,
191            ),
192            thread_info: SharedObservable::new_async(thread_info),
193            update_sender,
194            linked_chunk_update_sender,
195            waited_for_initial_prev_token: false,
196            subscribers_handle: SubscribersHandle::default(),
197        })
198    }
199
200    /// If storage is enabled, unload all the chunks, then reloads only the
201    /// last one.
202    ///
203    /// If storage's enabled, return a diff update that starts with a clear
204    /// of all events; as a result, the caller may override any
205    /// pending diff updates with the result of this function.
206    ///
207    /// Otherwise, returns `None`.
208    #[instrument(skip(self, store))]
209    async fn shrink_to_last_reloaded_chunk(
210        &mut self,
211        store: &EventCacheStoreLockGuard,
212    ) -> Result<()> {
213        // Attempt to load the last chunk.
214        let linked_chunk_id = LinkedChunkId::Thread(&self.room_id, &self.thread_id);
215
216        let full_linked_chunk_metadata =
217            match load_linked_chunk_metadata(store, linked_chunk_id).await {
218                Ok(metas) => metas,
219                Err(err) => {
220                    error!("error when reloading a linked chunk's metadata from the store: {err}");
221
222                    // Try to clear storage for this thread.
223                    store.handle_linked_chunk_updates(linked_chunk_id, vec![Update::Clear]).await?;
224
225                    // Restart with an empty linked chunk.
226                    None
227                }
228            };
229
230        let (last_chunk, chunk_identifier_generator) =
231            match store.load_last_chunk(linked_chunk_id).await {
232                Ok(pair) => pair,
233
234                Err(err) => {
235                    // If loading the last chunk failed, clear the entire linked chunk.
236                    error!("error when reloading a linked chunk from memory: {err}");
237
238                    // Clear storage for this thread.
239                    store.handle_linked_chunk_updates(linked_chunk_id, vec![Update::Clear]).await?;
240
241                    // Restart with an empty linked chunk.
242                    (None, ChunkIdentifierGenerator::new_from_scratch())
243                }
244            };
245
246        debug!("unloading the linked chunk, and resetting it to its last chunk");
247
248        // Remove all the chunks from the linked chunks, except for the last one, and
249        // updates the chunk identifier generator.
250        if let Err(err) = self.thread_linked_chunk.shrink_to_last_reloaded_chunk(
251            last_chunk,
252            chunk_identifier_generator,
253            full_linked_chunk_metadata,
254        ) {
255            error!("error when replacing the linked chunk: {err}");
256
257            self.thread_linked_chunk.reset();
258            self.propagate_changes(store).await?;
259
260            // Reset the pagination state too: pretend we never waited for the initial
261            // prev-batch token, and indicate that we're not at the start of the
262            // timeline, since we don't know about that anymore.
263            self.waited_for_initial_prev_token = false;
264
265            return Ok(());
266        }
267
268        Ok(())
269    }
270
271    pub async fn propagate_changes(&mut self, store: &EventCacheStoreLockGuard) -> Result<()> {
272        let updates = self.thread_linked_chunk.store_updates().take();
273
274        self.send_updates_to_store(updates, store).await
275    }
276
277    async fn send_updates_to_store(
278        &mut self,
279        updates: Vec<Update<Event, Gap>>,
280        store: &EventCacheStoreLockGuard,
281    ) -> Result<()> {
282        let linked_chunk_id =
283            OwnedLinkedChunkId::Thread(self.room_id.clone(), self.thread_id.clone());
284
285        send_updates_to_store(store, linked_chunk_id, &self.linked_chunk_update_sender, updates)
286            .await
287    }
288}
289
290impl<'a> StateLockReadGuard<'a, ThreadEventCacheState> {
291    /// Return a read-only reference to the underlying thread linked chunk.
292    pub fn thread_linked_chunk(&self) -> &EventLinkedChunk {
293        &self.state.thread_linked_chunk
294    }
295
296    /// Return a reference to subscribers handle.
297    pub fn subscribers_handle(&self) -> &SubscribersHandle {
298        &self.state.subscribers_handle
299    }
300
301    /// Compute and return the [`ThreadSummary`] for this thread.
302    pub async fn compute_thread_summary(&self) -> Result<Option<ThreadSummary>> {
303        // Find the latest event ID.
304        let latest_event_id = {
305            // Find the last non-edit, non-redaction, non-redacted event.
306            //
307            // TODO(@hywan): This is inefficient. We are bending the `LatestEvent` API here.
308            // Ultimately, we want to delegate the computation of `ThreadSummary` to
309            // `LatestEvent` instead of committing crimes like these ones.
310            let mut latest_event_id = self
311                .thread_linked_chunk()
312                .revents()
313                .find(|(_position, event)| {
314                    crate::latest_events::filter_timeline_event(
315                        event,
316                        None,
317                        &self.state.own_user_id,
318                        None,
319                    )
320                    .is_break()
321                })
322                .and_then(|(_position, event)| event.event_id().map(ToOwned::to_owned));
323
324            // If there's an edit to the latest event in the thread, use the latest edit
325            // event ID as the latest event ID for the thread summary.
326            //
327            // TODO(@hywan): This is one of the inefficiency I am talking about above.
328            if let Some(event_id) = &latest_event_id
329                && let Some((original_event, edits)) = self
330                    .find_event_with_relations(event_id, Some(vec![RelationType::Replacement]))
331                    .await?
332            {
333                let latest_valid_edit = edits.into_iter().rfind(|edit| {
334                    let original_json = original_event.raw();
335                    let original_encryption_info = original_event.encryption_info();
336                    let replacement_json = edit.raw();
337                    let replacement_encryption_info = edit.encryption_info();
338
339                    check_validity_of_replacement_events(
340                        original_json,
341                        original_encryption_info.map(|v| &**v),
342                        replacement_json,
343                        replacement_encryption_info.map(|v| &**v),
344                    )
345                    .is_ok()
346                });
347
348                if let Some(latest_valid_edit) = latest_valid_edit {
349                    latest_event_id = latest_valid_edit.event_id().map(ToOwned::to_owned);
350                }
351            }
352
353            latest_event_id
354        };
355
356        // Compute the thread summary.
357
358        // Read the latest number of thread replies from the store.
359        //
360        // Implementation note: since this is based on the `m.relates_to` field, and
361        // that field can only be present on room messages, we don't have to
362        // worry about filtering out aggregation events (like reactions/edits/etc.).
363        // Pretty neat, huh?
364        let num_replies = {
365            let thread_replies = self
366                .store
367                .find_event_relations(&self.room_id, &self.thread_id, Some(&[RelationType::Thread]))
368                .await?;
369            thread_replies.len().try_into().unwrap_or(u32::MAX)
370        };
371
372        let summary = if num_replies > 0 {
373            Some(ThreadSummary { num_replies, latest_reply: latest_event_id })
374        } else {
375            None
376        };
377
378        Ok(summary)
379    }
380
381    /// See documentation of [`find_event`].
382    pub(in super::super) async fn find_event(
383        &self,
384        event_id: &EventId,
385    ) -> Result<Option<(EventLocation, Event)>> {
386        find_event(event_id, &self.room_id, &self.thread_linked_chunk, &self.store).await
387    }
388
389    /// See documentation of [`find_event_with_relations`].
390    pub async fn find_event_with_relations(
391        &self,
392        event_id: &EventId,
393        filters: Option<Vec<RelationType>>,
394    ) -> Result<Option<(Event, Vec<Event>)>> {
395        find_event_with_relations(
396            event_id,
397            &self.room_id,
398            filters,
399            &self.thread_linked_chunk,
400            &self.store,
401        )
402        .await
403    }
404}
405
406impl<'a> StateLockWriteGuard<'a, ThreadEventCacheState> {
407    /// Return a read-only reference to the underlying thread linked chunk.
408    pub fn thread_linked_chunk(&self) -> &EventLinkedChunk {
409        &self.state.thread_linked_chunk
410    }
411
412    /// Return a mutable reference to the underlying thread linked chunk.
413    pub fn thread_linked_chunk_mut(&mut self) -> &mut EventLinkedChunk {
414        &mut self.state.thread_linked_chunk
415    }
416
417    /// Get the `waited_for_initial_prev_token` value.
418    pub fn waited_for_initial_prev_token(&self) -> bool {
419        self.state.waited_for_initial_prev_token
420    }
421
422    /// Get the `waited_for_initial_prev_token` value.
423    pub fn waited_for_initial_prev_token_mut(&mut self) -> &mut bool {
424        &mut self.state.waited_for_initial_prev_token
425    }
426
427    /// Reload the thread: only the last events will be reloaded, shrinking the
428    /// in-memory size of the cache.
429    ///
430    /// If `preprocessing` is set to [`ReloadPreprocessing::ForgetAll`], all
431    /// events will be erased before reloaded.
432    #[must_use = "Propagate `VectorDiff` updates via `TimelineVectorDiffs`"]
433    pub async fn reload(
434        &mut self,
435        preprocessing: ReloadPreprocessing,
436    ) -> Result<Vec<VectorDiff<Event>>> {
437        match preprocessing {
438            ReloadPreprocessing::ForgetAll => {
439                // Clear the `LinkedChunk` and broadcast the updates to the store.
440
441                self.thread_linked_chunk_mut().reset();
442                self.state.propagate_changes(&self.store).await?;
443
444                // Reset the pagination state too: pretend we never waited for the initial
445                // prev-batch token, and indicate that we're not at the start of the timeline,
446                // since we don't know about that anymore.
447                *self.waited_for_initial_prev_token_mut() = false;
448            }
449
450            ReloadPreprocessing::None => {}
451        }
452
453        self.state.shrink_to_last_reloaded_chunk(&self.store).await?;
454
455        Ok(self.thread_linked_chunk_mut().updates_as_vector_diffs())
456    }
457
458    #[must_use = "Propagate `VectorDiff` updates via `TimelineVectorDiffs`"]
459    pub async fn handle_sync(
460        &mut self,
461        timeline: Timeline,
462        read_receipts: &MaybeReceiptEventContent,
463    ) -> Result<(bool, Vec<VectorDiff<Event>>)> {
464        let prev_batch_token = &timeline.prev_batch;
465
466        let DeduplicationOutcome {
467            all_events: events,
468            in_memory_duplicated_event_ids,
469            in_store_duplicated_event_ids,
470            non_empty_all_duplicates: all_duplicates,
471        } = filter_duplicate_events(
472            &self.state.own_user_id,
473            &self.store,
474            LinkedChunkId::Thread(&self.state.room_id, &self.state.thread_id),
475            &self.state.thread_linked_chunk,
476            timeline.events,
477        )
478        .await?;
479
480        if all_duplicates {
481            // If all events are duplicates, we don't need to do anything; ignore
482            // the new events.
483            //
484            // We might have a new read receipt, though! If that's the case, handle it for
485            // unread counts tracking.
486            //
487            // Post-process the ephemeral events.
488            self.post_process_upserted_events(empty(), read_receipts.as_ref()).await?;
489
490            return Ok((false, Vec::new()));
491        }
492
493        let has_new_gap = prev_batch_token.is_some();
494
495        // If we've never waited for an initial previous-batch token, and we've now
496        // inserted a gap, no need to wait for a previous-batch token later.
497        if !self.state.waited_for_initial_prev_token && has_new_gap {
498            self.state.waited_for_initial_prev_token = true;
499        }
500
501        // Remove the old duplicated events.
502        //
503        // We don't have to worry about the removals can change the position of the
504        // existing events, because we are pushing all _new_ `events` at the back.
505        self.remove_events(in_memory_duplicated_event_ids, in_store_duplicated_event_ids).await?;
506
507        self.state.thread_linked_chunk.push_live_events(
508            prev_batch_token.as_ref().map(|prev_token| Gap { token: prev_token.clone() }),
509            &events,
510        );
511
512        // Update the store.
513        self.state.propagate_changes(&self.store).await?;
514
515        // Post-process newly inserted events.
516        self.post_process_upserted_events(events.iter(), read_receipts.as_ref()).await?;
517
518        if timeline.limited && has_new_gap {
519            // If there was a previous batch token for a limited timeline, unload the chunks
520            // so it only contains the last one; otherwise, there might be a
521            // valid gap in between, and observers may not render it (yet).
522            //
523            // We must do this *after* persisting these events to storage.
524            self.state.shrink_to_last_reloaded_chunk(&self.store).await?;
525        }
526
527        let timeline_event_diffs = self.state.thread_linked_chunk.updates_as_vector_diffs();
528
529        Ok((has_new_gap, timeline_event_diffs))
530    }
531
532    /// Post-process newly inserted or updated events.
533    pub(super) async fn post_process_upserted_events<'i, I>(
534        &mut self,
535        events: I,
536        receipt_event: Option<&ReceiptEventContent>,
537    ) -> Result<()>
538    where
539        I: Iterator<Item = &'i Event>,
540    {
541        for event in events {
542            // Handle redaction.
543            self.maybe_apply_new_redaction(event).await?;
544
545            // Save a bundled thread event, if there was one.
546            if let Some(bundled_thread) = event.bundled_latest_thread_event() {
547                self.save_events([bundled_thread]).await?;
548            }
549        }
550
551        self.update_read_receipts(receipt_event).await?;
552
553        Ok(())
554    }
555
556    /// Update read receipts for all events in the thread, based on the current
557    /// state of the in-memory linked chunk.
558    pub async fn update_read_receipts(
559        &mut self,
560        receipt_event: Option<&ReceiptEventContent>,
561    ) -> Result<()> {
562        let Some(room) = self.state.weak_room.get() else {
563            debug!("can't update read receipts: client's closing");
564            return Ok(());
565        };
566
567        let prev_read_receipts = self.state.thread_info.read().await.read_receipts.clone();
568        let mut read_receipts = prev_read_receipts.clone();
569
570        let client = room.client();
571        let event_filter = ThreadReadReceiptEventFilter::new(&self.state, client.state_store());
572
573        compute_unread_counts(
574            &self.state.own_user_id,
575            receipt_event,
576            &self.state.thread_linked_chunk,
577            &event_filter,
578            &mut read_receipts,
579            None,
580        )
581        .await;
582
583        if prev_read_receipts != read_receipts {
584            // The read receipt has changed! Do a little dance to update the
585            // `ThreadInfo` in the store.
586            let mut thread_info = self.state.thread_info.write().await;
587
588            ObservableWriteGuard::update(&mut thread_info, |thread_info| {
589                thread_info.read_receipts = read_receipts;
590            });
591
592            let room_id = &self.state.room_id;
593            let thread_id = &self.state.thread_id;
594
595            if let Err(error) =
596                self.store.update_thread_info(room_id, thread_id, &thread_info).await
597            {
598                error!(?room_id, ?thread_id, ?error, "Failed to update the `ThreadInfo`");
599            }
600        }
601
602        Ok(())
603    }
604
605    /// If the given event is a redaction, try to retrieve the
606    /// to-be-redacted event in the chunk, and replace it by the
607    /// redacted form.
608    #[instrument(skip_all)]
609    async fn maybe_apply_new_redaction(&mut self, event: &Event) -> Result<()> {
610        let Some(event_id) =
611            extract_redaction_target(event.raw(), &self.room_version_rules.redaction)
612        else {
613            return Ok(());
614        };
615
616        // Replace the redacted event by a redacted form, if we knew about it.
617        let Some((location, mut target_event)) = self.find_event(&event_id).await? else {
618            trace!("redacted event is missing from the linked chunk");
619            return Ok(());
620        };
621
622        let target_event_raw = target_event.raw();
623
624        // Don't redact already redacted events.
625        if let Ok(deserialized) = target_event_raw.deserialize()
626            && deserialized.is_redacted()
627        {
628            return Ok(());
629        }
630
631        if let Some(redacted_event) = apply_redaction(
632            target_event_raw,
633            event.raw().cast_ref_unchecked::<SyncRoomRedactionEvent>(),
634            &self.room_version_rules.redaction,
635        ) {
636            // It's safe to cast `redacted_event` here:
637            // - either the event was an `AnyTimelineEvent` cast to `AnySyncTimelineEvent`
638            //   when calling .raw(), so it's still one under the hood.
639            // - or it wasn't, and it's a plain `AnySyncTimelineEvent` in this case.
640            target_event.replace_raw(redacted_event.cast_unchecked());
641
642            self.replace_event_at(location, target_event.clone()).await?;
643        }
644
645        Ok(())
646    }
647
648    /// See documentation of [`find_event`].
649    pub(super) async fn find_event(
650        &self,
651        event_id: &EventId,
652    ) -> Result<Option<(EventLocation, Event)>> {
653        find_event(event_id, &self.room_id, &self.thread_linked_chunk, &self.store).await
654    }
655
656    /// Replaces a single event, be it saved in memory or in the store.
657    ///
658    /// If it was saved in memory, this will emit a notification to
659    /// observers that a single item has been replaced. Otherwise,
660    /// such a notification is not emitted, because observers are
661    /// unlikely to observe the store updates directly.
662    pub async fn replace_event_at(
663        &mut self,
664        location: EventLocation,
665        new_event: Event,
666    ) -> Result<()> {
667        match location {
668            EventLocation::Memory(position) => {
669                self.state
670                    .thread_linked_chunk
671                    .replace_event_at(position, new_event)
672                    .expect("should have been a valid position of an item");
673                // We just changed the in-memory representation; synchronize this with
674                // the store.
675                self.state.propagate_changes(&self.store).await?;
676            }
677            EventLocation::Store => {
678                self.save_events([new_event]).await?;
679            }
680        }
681
682        Ok(())
683    }
684
685    /// Save events into the database, without notifying observers.
686    pub async fn save_events(&mut self, events: impl IntoIterator<Item = Event>) -> Result<()> {
687        let store = self.store.clone();
688        let room_id = self.state.room_id.clone();
689        let events = events.into_iter().collect::<Vec<_>>();
690
691        // Spawn a task so the save is uninterrupted by task cancellation.
692        spawn(async move {
693            for event in events {
694                store.save_event(&room_id, event).await?;
695            }
696
697            Result::Ok(())
698        })
699        .await
700        .expect("joining failed")?;
701
702        Ok(())
703    }
704
705    /// Remove events by their position, in `EventLinkedChunk`.
706    ///
707    /// This method is purposely isolated because it must ensure that
708    /// positions are sorted appropriately or it can be disastrous.
709    #[instrument(skip_all)]
710    pub async fn remove_events(
711        &mut self,
712        in_memory_events: Vec<(OwnedEventId, Position)>,
713        in_store_events: Vec<(OwnedEventId, Position)>,
714    ) -> Result<()> {
715        // In-store events.
716        if !in_store_events.is_empty() {
717            let mut positions = in_store_events
718                .into_iter()
719                .map(|(_event_id, position)| position)
720                .collect::<Vec<_>>();
721
722            sort_positions_descending(&mut positions);
723
724            let updates =
725                positions.into_iter().map(|pos| Update::RemoveItem { at: pos }).collect::<Vec<_>>();
726
727            self.apply_store_only_updates(updates).await?;
728        }
729
730        // In-memory events.
731        if in_memory_events.is_empty() {
732            // Nothing else to do, return early.
733            return Ok(());
734        }
735
736        // `remove_events_by_position` is responsible of sorting positions.
737        self.state
738            .thread_linked_chunk
739            .remove_events_by_position(
740                in_memory_events.into_iter().map(|(_event_id, position)| position).collect(),
741            )
742            .expect("failed to remove an event");
743
744        self.state.propagate_changes(&self.store).await
745    }
746
747    /// Automatically shrink the thread if there are no more subscribers, as
748    /// indicated by the atomic number of active subscribers.
749    #[must_use = "Propagate `VectorDiff` updates via `ThreadEventCacheUpdate`"]
750    pub async fn auto_shrink_if_no_subscribers(
751        &mut self,
752    ) -> Result<Option<Vec<VectorDiff<Event>>>> {
753        let number_of_subscribers = self.state.subscribers_handle.count();
754
755        trace!(number_of_subscribers, "received request to auto-shrink");
756
757        if number_of_subscribers == 0 {
758            // There is no more subscribers listening to this cache, we can shrink the state
759            // to its last chunk to save memory.
760            //
761            // In theory, between the condition (`… == 0`) and this instruction, a new
762            // subscriber could be created, creating a race, except that this method takes a
763            // `&mut`, ensuring an exclusive access to the state, ensuring no other
764            // subscribers can be created.
765            self.state.shrink_to_last_reloaded_chunk(&self.store).await?;
766
767            Ok(Some(self.state.thread_linked_chunk.updates_as_vector_diffs()))
768        } else {
769            Ok(None)
770        }
771    }
772
773    /// Apply some updates that are effective only on the store itself.
774    ///
775    /// This method should be used only for updates that happen *outside*
776    /// the in-memory linked chunk. Such updates must be applied
777    /// onto the ordering tracker as well as to the persistent
778    /// storage.
779    async fn apply_store_only_updates(&mut self, updates: Vec<Update<Event, Gap>>) -> Result<()> {
780        self.state.thread_linked_chunk.order_tracker.map_updates(&updates);
781        self.state.send_updates_to_store(updates, &self.store).await
782    }
783
784    /// Try to locate the events in the linked chunk corresponding to the given
785    /// list of resolved events, and replace them, while alerting observers
786    /// about the update.
787    #[cfg(feature = "e2e-encryption")]
788    #[must_use = "Propagate `VectorDiff` updates via `TimelineVectorDiffs`"]
789    pub(in super::super) fn replace_in_memory_utds(
790        &mut self,
791        resolved_events: &[MaybeResolvedEvent],
792    ) -> Result<Option<Vec<VectorDiff<Event>>>> {
793        Ok(if self.thread_linked_chunk_mut().replace_utds(resolved_events) {
794            Some(self.thread_linked_chunk_mut().updates_as_vector_diffs())
795        } else {
796            None
797        })
798    }
799}