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