Skip to main content

matrix_sdk/event_cache/caches/thread/
mod.rs

1// Copyright 2026 The Matrix.org Foundation C.I.C.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Threads-related data structures.
16
17pub mod pagination;
18mod state;
19mod updates;
20
21use std::{fmt, sync::Arc};
22
23use eyeball::AsyncLock;
24use matrix_sdk_base::{
25    deserialized_responses::ThreadSummary,
26    event_cache::{Event, thread::ThreadInfo},
27    read_receipts::ReadReceipts,
28    sync::Timeline,
29};
30use ruma::{
31    EventId, OwnedEventId, OwnedRoomId, OwnedUserId, RoomId, events::relation::RelationType,
32    room_version_rules::RoomVersionRules,
33};
34use tokio::sync::{Notify, broadcast::Sender, mpsc};
35use tracing::{instrument, trace};
36
37pub(in super::super) use self::state::ThreadEventCacheState;
38pub(super) use self::updates::ThreadEventCacheUpdateSender;
39pub use self::{pagination::ThreadPagination, updates::ThreadEventCacheUpdate};
40#[cfg(feature = "e2e-encryption")]
41use super::super::redecryptor::MaybeResolvedEvent;
42use super::{
43    super::{
44        Result,
45        states::{CacheStateLock, StateLock, selectors::ThreadStateSelector},
46    },
47    EventsOrigin, TimelineVectorDiffs,
48    read_receipts::MaybeReceiptEventContent,
49    room::{RoomEventCacheGenericUpdate, RoomEventCacheLinkedChunkUpdate},
50    subscriber::{AutoShrinkMessage, Subscriber},
51};
52use crate::room::WeakRoom;
53
54/// All the information related to a single thread.
55///
56/// Cloning is shallow, and thus is cheap to do.
57#[derive(Clone)]
58pub struct ThreadEventCache {
59    inner: Arc<ThreadEventCacheInner>,
60}
61
62/// The (non-cloneable) details of the `ThreadEventCache`.
63struct ThreadEventCacheInner {
64    /// The room ID.
65    room_id: OwnedRoomId,
66
67    /// The thread root ID.
68    thread_id: OwnedEventId,
69
70    /// The room where this thread belongs to.
71    weak_room: WeakRoom,
72
73    /// State for this thread's event cache.
74    state: CacheStateLock<ThreadStateSelector>,
75
76    /// A notifier that we received a new pagination token.
77    pagination_batch_token_notifier: Notify,
78
79    /// Sender to the auto-shrink channel.
80    ///
81    /// See doc comment around [`EventCache::auto_shrink_linked_chunk_task`] for
82    /// more details.
83    auto_shrink_sender: mpsc::Sender<AutoShrinkMessage>,
84
85    /// Update sender for this thread.
86    update_sender: ThreadEventCacheUpdateSender,
87}
88
89impl fmt::Debug for ThreadEventCache {
90    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
91        f.debug_struct("ThreadEventCache").finish_non_exhaustive()
92    }
93}
94
95impl ThreadEventCache {
96    /// Create a new empty thread event cache.
97    #[allow(clippy::too_many_arguments)]
98    pub(super) async fn new(
99        room_id: OwnedRoomId,
100        thread_id: OwnedEventId,
101        own_user_id: OwnedUserId,
102        room_version_rules: RoomVersionRules,
103        weak_room: WeakRoom,
104        state: &StateLock,
105        auto_shrink_sender: mpsc::Sender<AutoShrinkMessage>,
106        generic_update_sender: Sender<RoomEventCacheGenericUpdate>,
107        linked_chunk_update_sender: Sender<RoomEventCacheLinkedChunkUpdate>,
108    ) -> Result<Self> {
109        let update_sender = ThreadEventCacheUpdateSender::new(generic_update_sender.clone());
110
111        let cache_state = state
112            .try_insert_once_with(
113                ThreadStateSelector::new(room_id.clone(), thread_id.clone()),
114                |store_guard| {
115                    ThreadEventCacheState::new(
116                        room_id.clone(),
117                        thread_id.clone(),
118                        weak_room.clone(),
119                        own_user_id,
120                        room_version_rules,
121                        store_guard,
122                        update_sender.clone(),
123                        linked_chunk_update_sender,
124                    )
125                },
126            )
127            .await?;
128
129        let timeline_is_not_empty =
130            cache_state.read().await?.thread_linked_chunk().revents().next().is_some();
131
132        let cache = Self {
133            inner: Arc::new(ThreadEventCacheInner {
134                room_id: room_id.clone(),
135                thread_id,
136                weak_room,
137                state: cache_state,
138                pagination_batch_token_notifier: Notify::new(),
139                auto_shrink_sender,
140                update_sender,
141            }),
142        };
143
144        // If at least one event has been loaded, it means there is a timeline.
145        // Let's emit a generic update.
146        if timeline_is_not_empty {
147            let _ = generic_update_sender
148                .send(RoomEventCacheGenericUpdate { room_id: room_id.to_owned() });
149        }
150
151        Ok(cache)
152    }
153
154    /// Get the room ID for this room.
155    pub fn room_id(&self) -> &RoomId {
156        &self.inner.room_id
157    }
158
159    /// Get the thread ID for this thread.
160    pub fn thread_id(&self) -> &EventId {
161        &self.inner.thread_id
162    }
163
164    /// Get the number of unread messages.
165    ///
166    /// To get multiple information about the read receipts, use
167    /// [`Self::read_receipts`] as it involves a single lock.
168    pub async fn num_unread_messages(&self) -> Result<u64> {
169        Ok(self.inner.state.read().await?.thread_info.read().await.read_receipts.num_unread)
170    }
171
172    /// Get the number of unread notifications.
173    ///
174    /// To get multiple information about the read receipts, use
175    /// [`Self::read_receipts`] as it involves a single lock.
176    pub async fn num_unread_notifications(&self) -> Result<u64> {
177        Ok(self.inner.state.read().await?.thread_info.read().await.read_receipts.num_notifications)
178    }
179
180    /// Get the number of unread mentions, that is, messages causing a highlight
181    /// in a room.
182    ///
183    /// To get multiple information about the read receipts, use
184    /// [`Self::read_receipts`] as it involves a single lock.
185    pub async fn num_unread_mentions(&self) -> Result<u64> {
186        Ok(self.inner.state.read().await?.thread_info.read().await.read_receipts.num_mentions)
187    }
188
189    /// Get the detailed information about read receipts for this thread.
190    pub async fn read_receipts(&self) -> Result<ReadReceipts> {
191        Ok(self.inner.state.read().await?.thread_info.read().await.read_receipts.clone())
192    }
193
194    /// Subscribe to update of the [`ThreadInfo`].
195    pub async fn subscribe_to_thread_info(
196        &self,
197    ) -> Result<eyeball::Subscriber<ThreadInfo, AsyncLock>> {
198        Ok(self.inner.state.read().await?.thread_info.subscribe().await)
199    }
200
201    /// Subscribe to this thread updates, after getting the initial list of
202    /// events.
203    ///
204    /// Creating, and especially dropping, a [`Subscriber`] isn't free, as it
205    /// triggers side-effects.
206    pub async fn subscribe(&self) -> Result<(Vec<Event>, Subscriber<ThreadEventCacheUpdate>)> {
207        let state = self.inner.state.read().await?;
208        let events =
209            state.thread_linked_chunk().events().map(|(_position, item)| item.clone()).collect();
210
211        let subscribers_handle = state.subscribers_handle();
212
213        let subscriber = Subscriber::new(
214            self.inner.update_sender.new_thread_receiver(),
215            AutoShrinkMessage::Thread {
216                room_id: self.inner.room_id.clone(),
217                thread_id: self.inner.thread_id.clone(),
218            },
219            self.inner.auto_shrink_sender.clone(),
220            subscribers_handle,
221        );
222
223        trace!("added a thread event cache subscriber; new count: {}", subscribers_handle.count());
224
225        Ok((events, subscriber))
226    }
227
228    /// Return a [`ThreadPagination`] useful for running back-pagination queries
229    /// in this thread.
230    pub fn pagination(&self) -> ThreadPagination {
231        ThreadPagination::new(self.inner.clone())
232    }
233
234    /// Return a reference to the state.
235    pub(in super::super) fn state(&self) -> &CacheStateLock<ThreadStateSelector> {
236        &self.inner.state
237    }
238
239    /// Handle an update from a joined room.
240    #[instrument(skip_all, fields(room_id = %self.inner.room_id, thread_root = %self.inner.thread_id))]
241    pub(super) async fn handle_joined_room_update(
242        &self,
243        timeline: Timeline,
244        read_receipts: MaybeReceiptEventContent,
245    ) -> Result<()> {
246        self.handle_timeline(timeline, read_receipts).await
247    }
248
249    /// Handle an update from a left room.
250    #[instrument(skip_all, fields(room_id = %self.inner.room_id, thread_root = %self.inner.thread_id))]
251    pub(super) async fn handle_left_room_update(&self, timeline: Timeline) -> Result<()> {
252        self.handle_timeline(timeline, MaybeReceiptEventContent::none()).await
253    }
254
255    /// Handle a [`Timeline`], i.e. new events received by a sync for this
256    /// thread.
257    async fn handle_timeline(
258        &self,
259        timeline: Timeline,
260        read_receipts: MaybeReceiptEventContent,
261    ) -> Result<()> {
262        if timeline.events.is_empty() && timeline.prev_batch.is_none() && read_receipts.is_none() {
263            return Ok(());
264        }
265
266        trace!("adding new events");
267
268        let mut state = self.inner.state.write().await?;
269
270        let (stored_prev_batch_token, timeline_event_diffs) =
271            state.handle_sync(timeline, &read_receipts).await?;
272
273        // Now that all events have been added, we can trigger the
274        // `pagination_token_notifier`.
275        if stored_prev_batch_token {
276            self.inner.pagination_batch_token_notifier.notify_one();
277        }
278
279        if !timeline_event_diffs.is_empty() {
280            state.update_sender.send(
281                ThreadEventCacheUpdate::UpdateTimelineEvents(TimelineVectorDiffs {
282                    diffs: timeline_event_diffs,
283                    origin: EventsOrigin::Sync,
284                }),
285                // This function is part of the `RoomEventCache` flow. The
286                // generic update is handled by it.
287                None,
288            );
289        }
290
291        if let Some(read_receipts) = read_receipts.into_inner() {
292            state
293                .update_sender
294                .send(ThreadEventCacheUpdate::AddReadReceiptEvent { event: read_receipts }, None);
295        }
296
297        Ok(())
298    }
299
300    /// Update the [`ThreadSummary`] for this thread, and return a copy of it.
301    pub(in super::super) async fn update_thread_summary(&self) -> Result<ThreadSummary> {
302        let mut state = self.inner.state.write().await?;
303
304        let thread_summary = state.update_thread_summary().await?;
305
306        state
307            .update_sender
308            .send(ThreadEventCacheUpdate::UpdateSummary(thread_summary.clone()), None);
309
310        Ok(thread_summary)
311    }
312
313    /// Find a single event in this thread.
314    ///
315    /// It starts by looking into loaded events in `EventLinkedChunk` before
316    /// looking inside the storage.
317    #[cfg(test)]
318    async fn find_event(
319        &self,
320        event_id: &EventId,
321    ) -> Result<Option<(super::EventLocation, Event)>> {
322        self.inner.state.read().await?.find_event(event_id).await
323    }
324
325    /// Try to find an event by ID in this thread, along with its related
326    /// events.
327    ///
328    /// You can filter which types of related events to retrieve using `filter`.
329    /// `None` will retrieve related events of any type.
330    ///
331    /// The related events are sorted like this:
332    ///
333    /// - events saved out-of-band (with `RoomEventCache::save_events`) will be
334    ///   located at the beginning of the array.
335    /// - events present in the linked chunk (be it in memory or in the storage)
336    ///   will be sorted according to their ordering in the linked chunk.
337    pub async fn find_event_with_relations(
338        &self,
339        event_id: &EventId,
340        filter: Option<Vec<RelationType>>,
341    ) -> Result<Option<(Event, Vec<Event>)>> {
342        // Search in all loaded or stored events.
343        Ok(self
344            .inner
345            .state
346            .read()
347            .await?
348            .find_event_with_relations(event_id, filter)
349            .await
350            .ok()
351            .flatten())
352    }
353
354    /// Try to locate the events in the linked chunk corresponding to the given
355    /// list of resolved events, and replace them, while alerting observers
356    /// about the update.
357    ///
358    /// Return `true` if at least one event has been updated.
359    #[cfg(feature = "e2e-encryption")]
360    pub(in super::super) async fn replace_in_memory_utds(
361        &self,
362        resolved_events: &[MaybeResolvedEvent],
363    ) -> Result<bool> {
364        let mut state = self.inner.state.write().await?;
365        let timeline_event_diffs = state.replace_in_memory_utds(resolved_events)?;
366
367        // Drain the updates to the store, events have already been updated
368        // before calling this method.
369        let _ = state.thread_linked_chunk_mut().store_updates().take();
370
371        state
372            .post_process_upserted_events(
373                resolved_events.iter().filter_map(|resolved_event| resolved_event.as_resolved()),
374                // Read receipt events aren't encrypted, so we can't have
375                // decrypted a new one here. As a result, we don't have any new
376                // receipt events to post-process, so we can just pass `None`
377                // here.
378                //
379                // Note: read receipts may be updated anyhow in the
380                // post-processing step, as the redecryption may have decrypted
381                // some events that don't count as unreads.
382                None,
383            )
384            .await?;
385
386        let timeline_event_diffs = timeline_event_diffs
387            .into_iter()
388            .flatten()
389            .chain(state.thread_linked_chunk_mut().updates_as_vector_diffs())
390            .collect::<Vec<_>>();
391
392        Ok(if !timeline_event_diffs.is_empty() {
393            state.update_sender.send(
394                ThreadEventCacheUpdate::UpdateTimelineEvents(TimelineVectorDiffs {
395                    diffs: timeline_event_diffs,
396                    origin: EventsOrigin::Cache,
397                }),
398                Some(RoomEventCacheGenericUpdate { room_id: self.inner.room_id.clone() }),
399            );
400
401            true
402        } else {
403            false
404        })
405    }
406}
407
408#[cfg(all(test, not(target_family = "wasm")))] // This uses the cross-process lock, so needs time support.
409mod timed_tests {
410    use std::sync::Arc;
411
412    use assert_matches::assert_matches;
413    use eyeball_im::VectorDiff;
414    use futures_util::FutureExt as _;
415    use matrix_sdk_base::{
416        RoomState, ThreadingSupport,
417        cross_process_lock::CrossProcessLockConfig,
418        event_cache::{
419            Gap,
420            store::{EventCacheStore as _, MemoryStore},
421        },
422        linked_chunk::{
423            ChunkContent, ChunkIdentifier, LinkedChunkId, Position, Update,
424            lazy_loader::from_all_chunks,
425        },
426        store::StoreConfig,
427        sync::Timeline,
428    };
429    use matrix_sdk_test::{ALICE, async_test, event_factory::EventFactory};
430    use ruma::{
431        event_id,
432        events::{AnySyncMessageLikeEvent, AnySyncTimelineEvent},
433        room_id, user_id,
434    };
435    use strass::assert_let;
436    use tokio::task::yield_now;
437
438    use super::{
439        super::{super::RoomEventCacheGenericUpdate, TimelineVectorDiffs},
440        MaybeReceiptEventContent, ThreadEventCacheUpdate,
441    };
442    use crate::{assert_let_timeout, test_utils::client::MockClientBuilder};
443
444    #[async_test]
445    async fn test_write_to_storage() {
446        let room_id = room_id!("!r0");
447        let thread_root = event_id!("$t0_ev0");
448        let thread_event_id_0 = event_id!("$t0_ev1");
449
450        let f = EventFactory::new().room(room_id).sender(user_id!("@mnt_io:matrix.org"));
451
452        let event_cache_store = Arc::new(MemoryStore::new());
453
454        let client = MockClientBuilder::new(None)
455            .on_builder(|builder| {
456                builder
457                    .store_config(
458                        StoreConfig::new(CrossProcessLockConfig::multi_process("hodor"))
459                            .event_cache_store(event_cache_store.clone()),
460                    )
461                    .with_threading_support(ThreadingSupport::Enabled { with_subscriptions: true })
462            })
463            .build()
464            .await;
465
466        let event_cache = client.event_cache();
467        event_cache.subscribe().unwrap();
468
469        client.base_client().get_or_create_room(room_id, RoomState::Joined);
470
471        let (thread_event_cache, _drop_handles) =
472            event_cache.thread(room_id, thread_root).await.unwrap();
473        let (thread_events, mut thread_stream) = thread_event_cache.subscribe().await.unwrap();
474
475        assert!(thread_events.is_empty());
476
477        // Propagate an update for a message and a prev-batch token.
478        let timeline = Timeline {
479            limited: true,
480            prev_batch: Some("raclette".to_owned()),
481            events: vec![
482                f.text_msg("salut")
483                    .event_id(thread_event_id_0)
484                    .in_thread(thread_root, thread_root)
485                    .into_event(),
486            ],
487        };
488
489        thread_event_cache
490            .handle_joined_room_update(timeline, MaybeReceiptEventContent::none())
491            .await
492            .unwrap();
493
494        assert_matches!(
495            thread_stream.recv().await,
496            Ok(ThreadEventCacheUpdate::UpdateTimelineEvents(TimelineVectorDiffs { diffs, .. })) => {
497                assert_eq!(diffs.len(), 2);
498                assert_matches!(&diffs[0], VectorDiff::Clear);
499                assert_matches!(&diffs[1], VectorDiff::Append { values: events } => {
500                    assert_eq!(events.len(), 1);
501                    assert_eq!(events[0].event_id(), Some(thread_event_id_0));
502                });
503            }
504        );
505        assert!(thread_stream.is_empty());
506
507        // Check the storage.
508        let linked_chunk = from_all_chunks::<3, _, _>(
509            event_cache_store
510                .load_all_chunks(LinkedChunkId::Thread(room_id, thread_root))
511                .await
512                .unwrap(),
513        )
514        .unwrap()
515        .unwrap();
516
517        assert_eq!(linked_chunk.chunks().count(), 2);
518
519        let mut chunks = linked_chunk.chunks();
520
521        // We start with the gap.
522        assert_matches!(chunks.next().unwrap().content(), ChunkContent::Gap(gap) => {
523            assert_eq!(gap.token, "raclette");
524        });
525
526        // Then we have the stored event.
527        assert_matches!(chunks.next().unwrap().content(), ChunkContent::Items(events) => {
528            assert_eq!(events.len(), 1);
529            assert_eq!(events[0].event_id(), Some(thread_event_id_0));
530        });
531
532        // That's all, folks!
533        assert!(chunks.next().is_none());
534    }
535
536    #[async_test]
537    async fn test_write_to_storage_strips_bundled_relations() {
538        let sender = user_id!("@mnt_io:matrix.org");
539        let room_id = room_id!("!r0");
540        let thread_root = event_id!("$t0_ev0");
541        let thread_event_id_0 = event_id!("$t0_ev1");
542
543        let f = EventFactory::new().room(room_id).sender(sender);
544
545        let event_cache_store = Arc::new(MemoryStore::new());
546
547        let client = MockClientBuilder::new(None)
548            .on_builder(|builder| {
549                builder
550                    .store_config(
551                        StoreConfig::new(CrossProcessLockConfig::multi_process("hodor"))
552                            .event_cache_store(event_cache_store.clone()),
553                    )
554                    .with_threading_support(ThreadingSupport::Enabled { with_subscriptions: true })
555            })
556            .build()
557            .await;
558
559        let event_cache = client.event_cache();
560        event_cache.subscribe().unwrap();
561
562        client.base_client().get_or_create_room(room_id, RoomState::Joined);
563
564        let (thread_event_cache, _drop_handles) =
565            event_cache.thread(room_id, thread_root).await.unwrap();
566
567        // Propagate an update for a message with bundled relations.
568        let timeline = Timeline {
569            limited: false,
570            prev_batch: None,
571            events: vec![
572                f.text_msg("s 'up")
573                    .event_id(thread_event_id_0)
574                    .with_bundled_edit(f.text_msg("Hello, Kind Sir").sender(sender))
575                    .in_thread(thread_root, thread_root)
576                    .into_event(),
577            ],
578        };
579
580        thread_event_cache
581            .handle_joined_room_update(timeline, MaybeReceiptEventContent::none())
582            .await
583            .unwrap();
584
585        // The in-memory linked chunk keeps the bundled relation.
586        {
587            let (events, _) = thread_event_cache.subscribe().await.unwrap();
588
589            assert_eq!(events.len(), 1);
590
591            let event = events[0].raw().deserialize().unwrap();
592            assert_eq!(event.event_id(), thread_event_id_0);
593            assert_let!(
594                AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomMessage(msg)) =
595                    event
596            );
597            assert!(msg.as_original().unwrap().unsigned.relations.replace.is_some());
598        }
599
600        // The one in storage does not.
601        let linked_chunk = from_all_chunks::<3, _, _>(
602            event_cache_store
603                .load_all_chunks(LinkedChunkId::Thread(room_id, thread_root))
604                .await
605                .unwrap(),
606        )
607        .unwrap()
608        .unwrap();
609
610        assert_eq!(linked_chunk.chunks().count(), 1);
611
612        let mut chunks = linked_chunk.chunks();
613        assert_matches!(chunks.next().unwrap().content(), ChunkContent::Items(events) => {
614            assert_eq!(events.len(), 1);
615
616            let event = events[0].raw().deserialize().unwrap();
617            assert_eq!(event.event_id(), thread_event_id_0);
618
619            assert_let!(AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomMessage(msg)) = event);
620            assert!(msg.as_original().unwrap().unsigned.relations.replace.is_none());
621        });
622
623        // That's all, folks!
624        assert!(chunks.next().is_none());
625    }
626
627    #[async_test]
628    async fn test_clear() {
629        let room_id = room_id!("!r0");
630        let f = EventFactory::new().room(room_id).sender(user_id!("@mnt_io:matrix.org"));
631
632        let event_cache_store = Arc::new(MemoryStore::new());
633
634        let thread_root = event_id!("$t0_ev0");
635        let thread_event_id_0 = event_id!("$t0_ev1");
636        let thread_event_id_1 = event_id!("$t0_ev2");
637
638        let thread_event_0 = f
639            .text_msg("foo")
640            .event_id(thread_event_id_0)
641            .in_thread(thread_root, thread_root)
642            .into_event();
643        let thread_event_1 = f
644            .text_msg("bar")
645            .event_id(thread_event_id_1)
646            .in_thread(thread_root, thread_event_id_0)
647            .into_event();
648
649        // Prefill the store with some data.
650        event_cache_store
651            .handle_linked_chunk_updates(
652                LinkedChunkId::Thread(room_id, thread_root),
653                vec![
654                    // An empty items chunk.
655                    Update::NewItemsChunk {
656                        previous: None,
657                        new: ChunkIdentifier::new(0),
658                        next: None,
659                    },
660                    // A gap chunk.
661                    Update::NewGapChunk {
662                        previous: Some(ChunkIdentifier::new(0)),
663                        // Chunk IDs aren't supposed to be ordered, so use a random value here.
664                        new: ChunkIdentifier::new(42),
665                        next: None,
666                        gap: Gap { token: "comté".to_owned() },
667                    },
668                    // Another items chunk, non-empty this time.
669                    Update::NewItemsChunk {
670                        previous: Some(ChunkIdentifier::new(42)),
671                        new: ChunkIdentifier::new(1),
672                        next: None,
673                    },
674                    Update::PushItems {
675                        at: Position::new(ChunkIdentifier::new(1), 0),
676                        items: vec![thread_event_0.clone()],
677                    },
678                    // And another items chunk, non-empty again.
679                    Update::NewItemsChunk {
680                        previous: Some(ChunkIdentifier::new(1)),
681                        new: ChunkIdentifier::new(2),
682                        next: None,
683                    },
684                    Update::PushItems {
685                        at: Position::new(ChunkIdentifier::new(2), 0),
686                        items: vec![thread_event_1.clone()],
687                    },
688                ],
689            )
690            .await
691            .unwrap();
692
693        let client = MockClientBuilder::new(None)
694            .on_builder(|builder| {
695                builder
696                    .store_config(
697                        StoreConfig::new(CrossProcessLockConfig::multi_process("hodor"))
698                            .event_cache_store(event_cache_store.clone()),
699                    )
700                    .with_threading_support(ThreadingSupport::Enabled { with_subscriptions: true })
701            })
702            .build()
703            .await;
704
705        let event_cache = client.event_cache();
706        event_cache.subscribe().unwrap();
707
708        client.base_client().get_or_create_room(room_id, RoomState::Joined);
709
710        let (thread_event_cache, _drop_handles) =
711            event_cache.thread(room_id, thread_root).await.unwrap();
712        let (thread_events, mut thread_stream) = thread_event_cache.subscribe().await.unwrap();
713
714        let mut generic_stream = event_cache.subscribe_to_room_generic_updates();
715
716        // The thread knows about all cached events.
717        {
718            assert!(thread_event_cache.find_event(thread_event_id_0).await.unwrap().is_some());
719            assert!(thread_event_cache.find_event(thread_event_id_1).await.unwrap().is_some());
720        }
721
722        // But only part of events are loaded from the store.
723        {
724            // The thread must contain only one event because only one chunk has
725            // been loaded.
726            assert_eq!(thread_events.len(), 1);
727            assert_eq!(thread_events[0].event_id().unwrap(), thread_event_id_1);
728
729            assert!(thread_stream.is_empty());
730        }
731
732        // Let's load more chunks to load all events.
733        {
734            thread_event_cache.pagination().run_backwards_once(20).await.unwrap();
735
736            assert_matches!(
737                thread_stream.recv().await,
738                Ok(ThreadEventCacheUpdate::UpdateTimelineEvents(TimelineVectorDiffs { diffs, .. })) => {
739                    assert_eq!(diffs.len(), 1);
740                    assert_matches!(&diffs[0], VectorDiff::Insert { index: 0, value: event } => {
741                        // Here you are `thread_event_0`!
742                        assert_eq!(event.event_id(), Some(thread_event_id_0));
743                    });
744                }
745            );
746            assert!(thread_stream.is_empty());
747
748            assert_matches!(
749                generic_stream.recv().await,
750                Ok(RoomEventCacheGenericUpdate { room_id: expected_room_id }) => {
751                    assert_eq!(room_id, expected_room_id);
752                }
753            );
754            assert!(generic_stream.is_empty());
755        }
756
757        // After clearing,…
758        event_cache.clear_all_rooms().await.unwrap();
759
760        //… we get an update that the content has been cleared.
761        assert_matches!(
762            thread_stream.recv().await,
763            Ok(ThreadEventCacheUpdate::UpdateTimelineEvents(TimelineVectorDiffs { diffs, .. })) => {
764                assert_eq!(diffs.len(), 1);
765                assert_matches!(&diffs[0], VectorDiff::Clear);
766            }
767        );
768
769        // … same with a generic update. (update for the clearing of the room)
770        assert_matches!(
771            generic_stream.recv().await,
772            Ok(RoomEventCacheGenericUpdate { room_id: received_room_id }) => {
773                assert_eq!(received_room_id, room_id);
774            }
775        );
776        // (update for the clearing of the thread)
777        assert_matches!(
778            generic_stream.recv().await,
779            Ok(RoomEventCacheGenericUpdate { room_id: received_room_id }) => {
780                assert_eq!(received_room_id, room_id);
781            }
782        );
783        assert!(generic_stream.is_empty());
784
785        // Events individually are forgotten by the event cache, after clearing
786        // the threads.
787        assert!(thread_event_cache.find_event(thread_event_id_0).await.unwrap().is_none());
788        assert!(thread_event_cache.find_event(thread_event_id_1).await.unwrap().is_none());
789
790        // And their presence in a linked chunk is forgotten.
791        let (thread_events, _) = thread_event_cache.subscribe().await.unwrap();
792        assert!(thread_events.is_empty());
793
794        // The event cache store is totally empty.
795        assert!(
796            event_cache_store
797                .load_all_chunks(LinkedChunkId::Thread(room_id, thread_root))
798                .await
799                .unwrap()
800                .is_empty()
801        );
802    }
803
804    #[async_test]
805    async fn test_load_from_storage() {
806        let room_id = room_id!("!r0");
807        let f = EventFactory::new().room(room_id).sender(user_id!("@mnt_io:matrix.org"));
808
809        let event_cache_store = Arc::new(MemoryStore::new());
810
811        let thread_root = event_id!("$t0");
812        let thread_event_id_0 = event_id!("$t0_ev0");
813        let thread_event_id_1 = event_id!("$t0_ev1");
814
815        let thread_event_0 = f
816            .text_msg("hello world")
817            .event_id(thread_event_id_0)
818            .in_thread(thread_root, thread_root)
819            .into_event();
820        let thread_event_1 = f
821            .text_msg("how's it going")
822            .event_id(thread_event_id_1)
823            .in_thread(thread_root, thread_event_id_1)
824            .into_event();
825
826        // Prefill the store with some data. The room usually has all events
827        // duplicated from the threads. It's important to make the test pass
828        // when checking the generic update.
829        let updates = vec![
830            // An empty items chunk.
831            Update::NewItemsChunk { previous: None, new: ChunkIdentifier::new(0), next: None },
832            // A gap chunk.
833            Update::NewGapChunk {
834                previous: Some(ChunkIdentifier::new(0)),
835                // Chunk IDs aren't supposed to be ordered, so use a random value here.
836                new: ChunkIdentifier::new(42),
837                next: None,
838                gap: Gap { token: "gruyère".to_owned() },
839            },
840            // Another items chunk, non-empty this time.
841            Update::NewItemsChunk {
842                previous: Some(ChunkIdentifier::new(42)),
843                new: ChunkIdentifier::new(1),
844                next: None,
845            },
846            Update::PushItems {
847                at: Position::new(ChunkIdentifier::new(1), 0),
848                items: vec![thread_event_0.clone()],
849            },
850            // And another items chunk, non-empty again.
851            Update::NewItemsChunk {
852                previous: Some(ChunkIdentifier::new(1)),
853                new: ChunkIdentifier::new(2),
854                next: None,
855            },
856            Update::PushItems {
857                at: Position::new(ChunkIdentifier::new(2), 0),
858                items: vec![thread_event_1.clone()],
859            },
860        ];
861        event_cache_store
862            .handle_linked_chunk_updates(LinkedChunkId::Room(room_id), updates.clone())
863            .await
864            .unwrap();
865        event_cache_store
866            .handle_linked_chunk_updates(LinkedChunkId::Thread(room_id, thread_root), updates)
867            .await
868            .unwrap();
869
870        let client = MockClientBuilder::new(None)
871            .on_builder(|builder| {
872                builder
873                    .store_config(
874                        StoreConfig::new(CrossProcessLockConfig::multi_process("hodor"))
875                            .event_cache_store(event_cache_store.clone()),
876                    )
877                    .with_threading_support(ThreadingSupport::Enabled { with_subscriptions: true })
878            })
879            .build()
880            .await;
881
882        let event_cache = client.event_cache();
883        event_cache.subscribe().unwrap();
884
885        client.base_client().get_or_create_room(room_id, RoomState::Joined);
886
887        // Let's check whether the generic updates are received for the
888        // initialisation.
889        let mut generic_stream = event_cache.subscribe_to_room_generic_updates();
890        let (thread_event_cache, _drop_handles) =
891            event_cache.thread(room_id, thread_root).await.unwrap();
892        let (thread_events, mut thread_stream) = thread_event_cache.subscribe().await.unwrap();
893
894        // The room **and** the thread have been loaded. Two generic updates
895        // must have been triggered.
896        for _ in 0..2 {
897            assert_matches!(
898                generic_stream.recv().await,
899                Ok(RoomEventCacheGenericUpdate { room_id: expected_room_id }) => {
900                    assert_eq!(room_id, expected_room_id);
901                }
902            );
903        }
904        assert!(generic_stream.is_empty());
905
906        // The initial events contain one event because only the last chunk is
907        // loaded by default.
908        assert_eq!(thread_events.len(), 1);
909        assert_eq!(thread_events[0].event_id().unwrap(), thread_event_id_1);
910        assert!(thread_stream.is_empty());
911
912        // The thread knows all events in the storage though, even if they
913        // aren't loaded.
914        assert!(thread_event_cache.find_event(thread_event_id_0).await.unwrap().is_some());
915        assert!(thread_event_cache.find_event(thread_event_id_1).await.unwrap().is_some());
916
917        // Let's paginate to load more events.
918        thread_event_cache.pagination().run_backwards_once(20).await.unwrap();
919
920        assert_matches!(
921            thread_stream.recv().await,
922            Ok(ThreadEventCacheUpdate::UpdateTimelineEvents(TimelineVectorDiffs { diffs, .. })) => {
923                assert_eq!(diffs.len(), 1);
924                assert_matches!(&diffs[0], VectorDiff::Insert { index: 0, value: event } => {
925                    assert_eq!(event.event_id(), Some(thread_event_id_0));
926                });
927            }
928        );
929        assert!(thread_stream.is_empty());
930
931        // A generic update is triggered too.
932        assert_matches!(
933            generic_stream.recv().await,
934            Ok(RoomEventCacheGenericUpdate { room_id: expected_room_id }) => {
935                assert_eq!(expected_room_id, room_id);
936            }
937        );
938        assert!(generic_stream.is_empty());
939
940        // A new update with one of these events leads to deduplication.
941        let timeline = Timeline { limited: false, prev_batch: None, events: vec![thread_event_1] };
942
943        thread_event_cache
944            .handle_joined_room_update(timeline, MaybeReceiptEventContent::none())
945            .await
946            .unwrap();
947
948        // Just checking the generic update is correct. There is a duplicate
949        // event, so no generic changes whatsoever!
950        assert!(generic_stream.recv().now_or_never().is_none());
951
952        // The stream doesn't report these changes _yet_. Use the events vector
953        // given when subscribing, to check that the events correspond to their
954        // new positions. The duplicated item is removed (so it's not the first
955        // element anymore), and it's added to the back of the list.
956        let (thread_events, _) = thread_event_cache.subscribe().await.unwrap();
957        assert_eq!(thread_events.len(), 2);
958        assert_eq!(thread_events[0].event_id(), Some(thread_event_id_0));
959        assert_eq!(thread_events[1].event_id(), Some(thread_event_id_1));
960    }
961
962    #[async_test]
963    async fn test_load_from_storage_resilient_to_failure() {
964        let room_id = room_id!("!r0");
965        let f = EventFactory::new().room(room_id).sender(user_id!("@mnt_io:matrix.org"));
966
967        let event_cache_store = Arc::new(MemoryStore::new());
968
969        let thread_root = event_id!("$t0");
970        let thread_event_id_0 = event_id!("$t0_ev0");
971
972        let thread_event_0 = f
973            .text_msg("hello world")
974            .event_id(thread_event_id_0)
975            .in_thread(thread_root, thread_root)
976            .into_event();
977
978        // Prefill the store with invalid data: two chunks that form a cycle.
979        event_cache_store
980            .handle_linked_chunk_updates(
981                LinkedChunkId::Thread(room_id, thread_root),
982                vec![
983                    Update::NewItemsChunk {
984                        previous: None,
985                        new: ChunkIdentifier::new(0),
986                        next: None,
987                    },
988                    Update::PushItems {
989                        at: Position::new(ChunkIdentifier::new(0), 0),
990                        items: vec![thread_event_0],
991                    },
992                    Update::NewItemsChunk {
993                        previous: Some(ChunkIdentifier::new(0)),
994                        new: ChunkIdentifier::new(1),
995                        next: Some(ChunkIdentifier::new(0)),
996                    },
997                ],
998            )
999            .await
1000            .unwrap();
1001
1002        let client = MockClientBuilder::new(None)
1003            .on_builder(|builder| {
1004                builder
1005                    .store_config(
1006                        StoreConfig::new(CrossProcessLockConfig::multi_process("holder"))
1007                            .event_cache_store(event_cache_store.clone()),
1008                    )
1009                    .with_threading_support(ThreadingSupport::Enabled { with_subscriptions: true })
1010            })
1011            .build()
1012            .await;
1013
1014        let event_cache = client.event_cache();
1015        event_cache.subscribe().unwrap();
1016
1017        client.base_client().get_or_create_room(room_id, RoomState::Joined);
1018
1019        let (thread_event_cache, _drop_handles) =
1020            event_cache.thread(room_id, thread_root).await.unwrap();
1021        let (thread_events, _) = thread_event_cache.subscribe().await.unwrap();
1022
1023        // Because the persisted content was invalid, the thread store is reset:
1024        // there are no events in the cache.
1025        assert!(thread_events.is_empty());
1026
1027        // Storage doesn't contain anything. It would also be valid that it
1028        // contains a single initial empty items chunk.
1029        let raw_chunks = event_cache_store
1030            .load_all_chunks(LinkedChunkId::Thread(room_id, thread_root))
1031            .await
1032            .unwrap();
1033        assert!(raw_chunks.is_empty());
1034    }
1035
1036    #[async_test]
1037    async fn test_reload_when_dirty() {
1038        let user_id = user_id!("@mnt_io:matrix.org");
1039        let room_id = room_id!("!raclette:patate.ch");
1040
1041        // The storage shared by the two clients.
1042        let event_cache_store = MemoryStore::new();
1043
1044        // Client for the process 0.
1045        let client_p0 = MockClientBuilder::new(None)
1046            .on_builder(|builder| {
1047                builder
1048                    .store_config(
1049                        StoreConfig::new(CrossProcessLockConfig::multi_process("process #0"))
1050                            .event_cache_store(event_cache_store.clone()),
1051                    )
1052                    .with_threading_support(ThreadingSupport::Enabled { with_subscriptions: true })
1053            })
1054            .build()
1055            .await;
1056
1057        // Client for the process 1.
1058        let client_p1 = MockClientBuilder::new(None)
1059            .on_builder(|builder| {
1060                builder
1061                    .store_config(
1062                        StoreConfig::new(CrossProcessLockConfig::multi_process("process #1"))
1063                            .event_cache_store(event_cache_store),
1064                    )
1065                    .with_threading_support(ThreadingSupport::Enabled { with_subscriptions: true })
1066            })
1067            .build()
1068            .await;
1069
1070        let event_factory = EventFactory::new().room(room_id).sender(user_id);
1071
1072        let thread_root = event_id!("$t0");
1073        let thread_event_id_0 = event_id!("$t0_ev0");
1074        let thread_event_id_1 = event_id!("$t0_ev1");
1075
1076        let thread_event_0 = event_factory
1077            .text_msg("comté")
1078            .event_id(thread_event_id_0)
1079            .in_thread(thread_root, thread_root)
1080            .into_event();
1081        let thread_event_1 = event_factory
1082            .text_msg("morbier")
1083            .event_id(thread_event_id_1)
1084            .in_thread(thread_root, thread_event_id_0)
1085            .into_event();
1086
1087        // Add events to the storage (shared by the two clients!).
1088        client_p0
1089            .event_cache_store()
1090            .lock()
1091            .await
1092            .expect("[p0] Could not acquire the event cache lock")
1093            .as_clean()
1094            .expect("[p0] Could not acquire a clean event cache lock")
1095            .handle_linked_chunk_updates(
1096                LinkedChunkId::Thread(room_id, thread_root),
1097                vec![
1098                    Update::NewItemsChunk {
1099                        previous: None,
1100                        new: ChunkIdentifier::new(0),
1101                        next: None,
1102                    },
1103                    Update::PushItems {
1104                        at: Position::new(ChunkIdentifier::new(0), 0),
1105                        items: vec![thread_event_0],
1106                    },
1107                    Update::NewItemsChunk {
1108                        previous: Some(ChunkIdentifier::new(0)),
1109                        new: ChunkIdentifier::new(1),
1110                        next: None,
1111                    },
1112                    Update::PushItems {
1113                        at: Position::new(ChunkIdentifier::new(1), 0),
1114                        items: vec![thread_event_1],
1115                    },
1116                ],
1117            )
1118            .await
1119            .unwrap();
1120
1121        // Subscribe the event caches, and create the room.
1122        let (thread_event_cache_p0, thread_event_cache_p1) = {
1123            let event_cache_p0 = client_p0.event_cache();
1124            event_cache_p0.subscribe().unwrap();
1125
1126            let event_cache_p1 = client_p1.event_cache();
1127            event_cache_p1.subscribe().unwrap();
1128
1129            client_p0.base_client().get_or_create_room(room_id, RoomState::Joined);
1130            client_p1.base_client().get_or_create_room(room_id, RoomState::Joined);
1131
1132            let (thread_event_cache_p0, _drop_handles) =
1133                event_cache_p0.thread(room_id, thread_root).await.unwrap();
1134            let (thread_event_cache_p1, _drop_handles) =
1135                event_cache_p1.thread(room_id, thread_root).await.unwrap();
1136
1137            (thread_event_cache_p0, thread_event_cache_p1)
1138        };
1139
1140        // Okay. We are ready for the test!
1141        //
1142        // First off, let's check `thread_event_cache_p0` has access to the
1143        // first event loaded in-memory, then do a pagination, and see more
1144        // events.
1145        let mut updates_stream_p0 = {
1146            let thread_event_cache = &thread_event_cache_p0;
1147
1148            let (initial_updates, mut updates_stream) =
1149                thread_event_cache_p0.subscribe().await.unwrap();
1150
1151            // Initial updates contain `thread_event_id_1` only.
1152            assert_eq!(initial_updates.len(), 1);
1153            assert_eq!(initial_updates[0].event_id(), Some(thread_event_id_1));
1154            assert!(updates_stream.is_empty());
1155
1156            // Load one more event with a backpagination.
1157            thread_event_cache.pagination().run_backwards_once(1).await.unwrap();
1158
1159            // A new update for `ev_id_0` must be present.
1160            assert_matches!(
1161                updates_stream.recv().await.unwrap(),
1162                ThreadEventCacheUpdate::UpdateTimelineEvents(TimelineVectorDiffs { diffs, .. }) => {
1163                    assert_eq!(diffs.len(), 1, "{diffs:#?}");
1164                    assert_matches!(
1165                        &diffs[0],
1166                        VectorDiff::Insert { index: 0, value: event } => {
1167                            assert_eq!(event.event_id(), Some(thread_event_id_0));
1168                        }
1169                    );
1170                }
1171            );
1172
1173            updates_stream
1174        };
1175
1176        // Second, let's check `thread_event_cache_p1` has the same accesses.
1177        let mut updates_stream_p1 = {
1178            let thread_event_cache = &thread_event_cache_p1;
1179            let (initial_updates, mut updates_stream) =
1180                thread_event_cache_p1.subscribe().await.unwrap();
1181
1182            // Initial updates contain `thread_event_id_1` only.
1183            assert_eq!(initial_updates.len(), 1);
1184            assert_eq!(initial_updates[0].event_id(), Some(thread_event_id_1));
1185            assert!(updates_stream.is_empty());
1186
1187            // Load one more event with a backpagination.
1188            thread_event_cache.pagination().run_backwards_once(1).await.unwrap();
1189
1190            // A new update for `thread_event_id_0` must be present.
1191            assert_matches!(
1192                updates_stream.recv().await.unwrap(),
1193                ThreadEventCacheUpdate::UpdateTimelineEvents(TimelineVectorDiffs { diffs, .. }) => {
1194                    assert_eq!(diffs.len(), 1, "{diffs:#?}");
1195                    assert_matches!(
1196                        &diffs[0],
1197                        VectorDiff::Insert { index: 0, value: event } => {
1198                            assert_eq!(event.event_id(), Some(thread_event_id_0));
1199                        }
1200                    );
1201                }
1202            );
1203
1204            updates_stream
1205        };
1206
1207        // Do this a couple times, for the fun.
1208        for _ in 0..3 {
1209            // Third, because `thread_event_cache_p1` has locked the store, the
1210            // lock is dirty for `thread_event_cache_p0`, so it will shrink to
1211            // its last chunk for the thread!
1212            {
1213                let thread_event_cache = &thread_event_cache_p0;
1214                let updates_stream = &mut updates_stream_p0;
1215
1216                // `thread_event_id_1` must be loaded in memory, just like
1217                // before. However, `thread_event_id_0` must NOT be loaded in
1218                // memory. It WAS loaded, but the state has been reloaded to its
1219                // last chunk.
1220                let (initial_updates, _) = thread_event_cache.subscribe().await.unwrap();
1221
1222                assert_eq!(initial_updates.len(), 1);
1223                assert_eq!(initial_updates[0].event_id(), Some(thread_event_id_1));
1224
1225                // The reload can be observed via the updates too.
1226                assert_matches!(
1227                    updates_stream.recv().await.unwrap(),
1228                    ThreadEventCacheUpdate::UpdateTimelineEvents(TimelineVectorDiffs { diffs, .. }) => {
1229                        assert_eq!(diffs.len(), 2, "{diffs:#?}");
1230                        assert_matches!(&diffs[0], VectorDiff::Clear);
1231                        assert_matches!(
1232                            &diffs[1],
1233                            VectorDiff::Append { values: events } => {
1234                                assert_eq!(events.len(), 1);
1235                                assert_eq!(events[0].event_id(), Some(thread_event_id_1));
1236                            }
1237                        );
1238                    }
1239                );
1240                assert_matches!(
1241                    updates_stream.recv().await.unwrap(),
1242                    ThreadEventCacheUpdate::UpdateSummary(summary) => {
1243                        assert_eq!(summary.latest_reply.as_deref(), Some(thread_event_id_1));
1244                        assert_eq!(summary.num_replies, 2);
1245                    }
1246                );
1247
1248                // Load one more event with a backpagination.
1249                thread_event_cache.pagination().run_backwards_once(1).await.unwrap();
1250
1251                // `thread_event_id_0` must now be loaded in memory. The
1252                // pagination can be observed via the updates.
1253                assert_matches!(
1254                    updates_stream.recv().await.unwrap(),
1255                    ThreadEventCacheUpdate::UpdateTimelineEvents(TimelineVectorDiffs { diffs, .. }) => {
1256                        assert_eq!(diffs.len(), 1, "{diffs:#?}");
1257                        assert_matches!(
1258                            &diffs[0],
1259                            VectorDiff::Insert { index: 0, value: event } => {
1260                                assert_eq!(event.event_id(), Some(thread_event_id_0));
1261                            }
1262                        );
1263                    }
1264                );
1265
1266                assert!(updates_stream.is_empty());
1267            }
1268
1269            // Fourth, because `thread_event_cache_p0` has locked the store
1270            // again, the lock is dirty for `thread_event_cache_p1` too!, so it
1271            // will shrink to its last chunk for the thread!
1272            {
1273                let thread_event_cache = &thread_event_cache_p1;
1274                let updates_stream = &mut updates_stream_p1;
1275
1276                // `thread_event_id_1` must be loaded in memory, just like
1277                // before. However, `thread_event_id_0` must NOT be loaded in
1278                // memory. It WAS loaded, but the state has shrunk to its last
1279                // chunk.
1280                let (initial_updates, _) = thread_event_cache.subscribe().await.unwrap();
1281
1282                assert_eq!(initial_updates.len(), 1);
1283                assert_eq!(initial_updates[0].event_id(), Some(thread_event_id_1));
1284
1285                // The reload can be observed via the updates too.
1286                assert_matches!(
1287                    updates_stream.recv().await.unwrap(),
1288                    ThreadEventCacheUpdate::UpdateTimelineEvents(TimelineVectorDiffs { diffs, .. }) => {
1289                        assert_eq!(diffs.len(), 2, "{diffs:#?}");
1290                        assert_matches!(&diffs[0], VectorDiff::Clear);
1291                        assert_matches!(
1292                            &diffs[1],
1293                            VectorDiff::Append { values: events } => {
1294                                assert_eq!(events.len(), 1);
1295                                assert_eq!(events[0].event_id(), Some(thread_event_id_1));
1296                            }
1297                        );
1298                    }
1299                );
1300                assert_matches!(
1301                    updates_stream.recv().await.unwrap(),
1302                    ThreadEventCacheUpdate::UpdateSummary(summary) => {
1303                        assert_eq!(summary.latest_reply.as_deref(), Some(thread_event_id_1));
1304                        assert_eq!(summary.num_replies, 2);
1305                    }
1306                );
1307
1308                // Load one more event with a backpagination.
1309                thread_event_cache.pagination().run_backwards_once(1).await.unwrap();
1310
1311                // `thread_event_id_0` must now be loaded in memory. The
1312                // pagination can be observed via the updates.
1313                assert_matches!(
1314                    updates_stream.recv().await.unwrap(),
1315                    ThreadEventCacheUpdate::UpdateTimelineEvents(TimelineVectorDiffs { diffs, .. }) => {
1316                        assert_eq!(diffs.len(), 1, "{diffs:#?}");
1317                        assert_matches!(
1318                            &diffs[0],
1319                            VectorDiff::Insert { index: 0, value: event } => {
1320                                assert_eq!(event.event_id(), Some(thread_event_id_0));
1321                            }
1322                        );
1323                    }
1324                );
1325            }
1326        }
1327    }
1328
1329    #[async_test]
1330    async fn test_auto_shrink_after_all_subscribers_are_gone() {
1331        let room_id = room_id!("!r0");
1332        let thread_id = event_id!("$t0");
1333
1334        let client = MockClientBuilder::new(None).build().await;
1335
1336        let f = EventFactory::new().room(room_id).sender(*ALICE);
1337
1338        let event_id_0 = event_id!("$ev0");
1339        let event_id_1 = event_id!("$ev1");
1340
1341        let thread_root =
1342            f.text_msg("gr00t").event_id(thread_id).in_thread(thread_id, thread_id).into_event();
1343        let event_0 =
1344            f.text_msg("hello").event_id(event_id_0).in_thread(thread_id, event_id_0).into_event();
1345        let event_1 =
1346            f.text_msg("world").event_id(event_id_1).in_thread(thread_id, event_id_1).into_event();
1347
1348        // Fill the event cache store with an initial linked chunk with 2 events
1349        // chunks.
1350        {
1351            client
1352                .event_cache_store()
1353                .lock()
1354                .await
1355                .expect("Could not acquire the event cache lock")
1356                .as_clean()
1357                .expect("Could not acquire a clean event cache lock")
1358                .handle_linked_chunk_updates(
1359                    LinkedChunkId::Thread(room_id, thread_id),
1360                    vec![
1361                        Update::NewItemsChunk {
1362                            previous: None,
1363                            new: ChunkIdentifier::new(0),
1364                            next: None,
1365                        },
1366                        Update::PushItems {
1367                            at: Position::new(ChunkIdentifier::new(0), 0),
1368                            items: vec![thread_root, event_0],
1369                        },
1370                        Update::NewItemsChunk {
1371                            previous: Some(ChunkIdentifier::new(0)),
1372                            new: ChunkIdentifier::new(1),
1373                            next: None,
1374                        },
1375                        Update::PushItems {
1376                            at: Position::new(ChunkIdentifier::new(1), 0),
1377                            items: vec![event_1],
1378                        },
1379                    ],
1380                )
1381                .await
1382                .unwrap();
1383        }
1384
1385        let event_cache = client.event_cache();
1386        event_cache.subscribe().unwrap();
1387
1388        client.base_client().get_or_create_room(room_id, RoomState::Joined);
1389
1390        let (thread_event_cache, _drop_handles) =
1391            event_cache.thread(room_id, thread_id).await.unwrap();
1392
1393        // Sanity check: lazily loaded, so only includes one item at start.
1394        let (events1, mut stream1) = thread_event_cache.subscribe().await.unwrap();
1395        assert_eq!(events1.len(), 1);
1396        assert_eq!(events1[0].event_id(), Some(event_id_1));
1397        assert!(stream1.is_empty());
1398
1399        let mut generic_stream = event_cache.subscribe_to_room_generic_updates();
1400
1401        // Force loading the full linked chunk by back-paginating.
1402        let outcome = thread_event_cache.pagination().run_backwards_once(20).await.unwrap();
1403        assert_eq!(outcome.events.len(), 2);
1404        assert_eq!(outcome.events[0].event_id(), Some(event_id_0));
1405        assert_eq!(outcome.events[1].event_id(), Some(thread_id));
1406        assert!(outcome.reached_start);
1407
1408        // We also get an update about the loading from the store. Ignore it,
1409        // for this test's sake.
1410        assert_let_timeout!(
1411            Ok(ThreadEventCacheUpdate::UpdateTimelineEvents(TimelineVectorDiffs { diffs, .. })) =
1412                stream1.recv()
1413        );
1414        assert_eq!(diffs.len(), 2);
1415        assert_matches!(&diffs[0], VectorDiff::Insert { index: 0, value } => {
1416            assert_eq!(value.event_id(), Some(thread_id));
1417        });
1418        assert_matches!(&diffs[1], VectorDiff::Insert { index: 1, value } => {
1419            assert_eq!(value.event_id(), Some(event_id_0));
1420        });
1421
1422        assert!(stream1.is_empty());
1423
1424        assert_let_timeout!(
1425            Ok(RoomEventCacheGenericUpdate { room_id: expected_room_id }) = generic_stream.recv()
1426        );
1427        assert_eq!(expected_room_id, room_id);
1428        assert!(generic_stream.is_empty());
1429
1430        // Have another subscriber. Since it's not the first one, and the
1431        // previous one loaded some more events, the second subscribers sees
1432        // them all.
1433        let (events2, stream2) = thread_event_cache.subscribe().await.unwrap();
1434        assert_eq!(events2.len(), 3);
1435        assert_eq!(events2[0].event_id(), Some(thread_id));
1436        assert_eq!(events2[1].event_id(), Some(event_id_0));
1437        assert_eq!(events2[2].event_id(), Some(event_id_1));
1438        assert!(stream2.is_empty());
1439
1440        // Grab a receiver for testing no diffs is sent.
1441        let subscriber = {
1442            let state = thread_event_cache.inner.state.read().await.unwrap();
1443            state.update_sender.new_thread_receiver()
1444        };
1445
1446        // Drop the first stream, and wait a bit.
1447        drop(stream1);
1448        yield_now().await;
1449
1450        // The second stream remains undisturbed.
1451        assert!(stream2.is_empty());
1452
1453        // Now drop the second stream, and wait a bit.
1454        drop(stream2);
1455        yield_now().await;
1456
1457        // The linked chunk must have auto-shrunk by now.
1458
1459        {
1460            // Check the inner state: there's no more shared auto-shrinker.
1461            let state = thread_event_cache.inner.state.read().await.unwrap();
1462            assert_eq!(state.subscribers_handle().count(), 0);
1463
1464            // No diff is sent when the linked chunk has auto-shrunk.
1465            assert!(subscriber.is_empty());
1466            assert!(generic_stream.is_empty());
1467        }
1468
1469        // Getting the events will only give us the latest chunk.
1470        let events3 = thread_event_cache
1471            .inner
1472            .state
1473            .read()
1474            .await
1475            .unwrap()
1476            .thread_linked_chunk()
1477            .events()
1478            .map(|(_position, item)| item.clone())
1479            .collect::<Vec<_>>();
1480        assert_eq!(events3.len(), 1);
1481        assert_eq!(events3[0].event_id(), Some(event_id_1));
1482    }
1483}