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