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