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(), 2);
668                assert_matches!(&diffs[0], VectorDiff::Clear);
669                assert_matches!(&diffs[1], VectorDiff::Append { values } => {
670                    assert!(values.is_empty());
671                });
672            }
673        );
674
675        // … same with a generic update.
676        // (update for the clearing of the room)
677        assert_matches!(
678            generic_stream.recv().await,
679            Ok(RoomEventCacheGenericUpdate { room_id: received_room_id }) => {
680                assert_eq!(received_room_id, room_id);
681            }
682        );
683        // (update for the clearing of the thread)
684        assert_matches!(
685            generic_stream.recv().await,
686            Ok(RoomEventCacheGenericUpdate { room_id: received_room_id }) => {
687                assert_eq!(received_room_id, room_id);
688            }
689        );
690        assert!(generic_stream.is_empty());
691
692        // Events individually are forgotten by the event cache, after clearing the
693        // threads.
694        assert!(thread_event_cache.find_event(thread_event_id_0).await.unwrap().is_none());
695        assert!(thread_event_cache.find_event(thread_event_id_1).await.unwrap().is_none());
696
697        // And their presence in a linked chunk is forgotten.
698        let (thread_events, _) = thread_event_cache.subscribe().await.unwrap();
699        assert!(thread_events.is_empty());
700
701        // The event cache store is totally empty.
702        let linked_chunk = from_all_chunks::<3, _, _>(
703            event_cache_store
704                .load_all_chunks(LinkedChunkId::Thread(room_id, thread_root))
705                .await
706                .unwrap(),
707        )
708        .unwrap()
709        .unwrap();
710
711        // Note: while the event cache store could return `None` here, clearing it will
712        // reset it to its initial form, maintaining the invariant that it
713        // contains a single items chunk that's empty.
714        assert_eq!(linked_chunk.num_items(), 0);
715    }
716
717    #[async_test]
718    async fn test_load_from_storage() {
719        let room_id = room_id!("!r0");
720        let f = EventFactory::new().room(room_id).sender(user_id!("@mnt_io:matrix.org"));
721
722        let event_cache_store = Arc::new(MemoryStore::new());
723
724        let thread_root = event_id!("$t0");
725        let thread_event_id_0 = event_id!("$t0_ev0");
726        let thread_event_id_1 = event_id!("$t0_ev1");
727
728        let thread_event_0 = f
729            .text_msg("hello world")
730            .event_id(thread_event_id_0)
731            .in_thread(thread_root, thread_root)
732            .into_event();
733        let thread_event_1 = f
734            .text_msg("how's it going")
735            .event_id(thread_event_id_1)
736            .in_thread(thread_root, thread_event_id_1)
737            .into_event();
738
739        // Prefill the store with some data. The room usually has all events duplicated
740        // from the threads. It's important to make the test pass when checking the
741        // generic update.
742        let updates = vec![
743            // An empty items chunk.
744            Update::NewItemsChunk { previous: None, new: ChunkIdentifier::new(0), next: None },
745            // A gap chunk.
746            Update::NewGapChunk {
747                previous: Some(ChunkIdentifier::new(0)),
748                // Chunk IDs aren't supposed to be ordered, so use a random value here.
749                new: ChunkIdentifier::new(42),
750                next: None,
751                gap: Gap { token: "gruyère".to_owned() },
752            },
753            // Another items chunk, non-empty this time.
754            Update::NewItemsChunk {
755                previous: Some(ChunkIdentifier::new(42)),
756                new: ChunkIdentifier::new(1),
757                next: None,
758            },
759            Update::PushItems {
760                at: Position::new(ChunkIdentifier::new(1), 0),
761                items: vec![thread_event_0.clone()],
762            },
763            // And another items chunk, non-empty again.
764            Update::NewItemsChunk {
765                previous: Some(ChunkIdentifier::new(1)),
766                new: ChunkIdentifier::new(2),
767                next: None,
768            },
769            Update::PushItems {
770                at: Position::new(ChunkIdentifier::new(2), 0),
771                items: vec![thread_event_1.clone()],
772            },
773        ];
774        event_cache_store
775            .handle_linked_chunk_updates(LinkedChunkId::Room(room_id), updates.clone())
776            .await
777            .unwrap();
778        event_cache_store
779            .handle_linked_chunk_updates(LinkedChunkId::Thread(room_id, thread_root), updates)
780            .await
781            .unwrap();
782
783        let client = MockClientBuilder::new(None)
784            .on_builder(|builder| {
785                builder
786                    .store_config(
787                        StoreConfig::new(CrossProcessLockConfig::multi_process("hodor"))
788                            .event_cache_store(event_cache_store.clone()),
789                    )
790                    .with_threading_support(ThreadingSupport::Enabled { with_subscriptions: true })
791            })
792            .build()
793            .await;
794
795        let event_cache = client.event_cache();
796        event_cache.subscribe().unwrap();
797
798        client.base_client().get_or_create_room(room_id, RoomState::Joined);
799
800        // Let's check whether the generic updates are received for the initialisation.
801        let mut generic_stream = event_cache.subscribe_to_room_generic_updates();
802        let (thread_event_cache, _drop_handles) =
803            event_cache.thread(room_id, thread_root).await.unwrap();
804        let (thread_events, mut thread_stream) = thread_event_cache.subscribe().await.unwrap();
805
806        // The room **and** the thread have been loaded. Two generic updates must have
807        // been triggered.
808        for _ in 0..2 {
809            assert_matches!(
810                generic_stream.recv().await,
811                Ok(RoomEventCacheGenericUpdate { room_id: expected_room_id }) => {
812                    assert_eq!(room_id, expected_room_id);
813                }
814            );
815        }
816        assert!(generic_stream.is_empty());
817
818        // The initial events contain one event because only the last chunk is loaded by
819        // default.
820        assert_eq!(thread_events.len(), 1);
821        assert_eq!(thread_events[0].event_id().unwrap(), thread_event_id_1);
822        assert!(thread_stream.is_empty());
823
824        // The thread knows all events in the storage though, even if they aren't
825        // loaded.
826        assert!(thread_event_cache.find_event(thread_event_id_0).await.unwrap().is_some());
827        assert!(thread_event_cache.find_event(thread_event_id_1).await.unwrap().is_some());
828
829        // Let's paginate to load more events.
830        thread_event_cache.pagination().run_backwards_once(20).await.unwrap();
831
832        assert_matches!(
833            thread_stream.recv().await,
834            Ok(TimelineVectorDiffs { diffs, .. }) => {
835                assert_eq!(diffs.len(), 1);
836                assert_matches!(&diffs[0], VectorDiff::Insert { index: 0, value: event } => {
837                    assert_eq!(event.event_id(), Some(thread_event_id_0));
838                });
839            }
840        );
841        assert!(thread_stream.is_empty());
842
843        // A generic update is triggered too.
844        assert_matches!(
845            generic_stream.recv().await,
846            Ok(RoomEventCacheGenericUpdate { room_id: expected_room_id }) => {
847                assert_eq!(expected_room_id, room_id);
848            }
849        );
850        assert!(generic_stream.is_empty());
851
852        // A new update with one of these events leads to deduplication.
853        let timeline = Timeline { limited: false, prev_batch: None, events: vec![thread_event_1] };
854
855        thread_event_cache
856            .handle_joined_room_update(JoinedRoomUpdate { timeline, ..Default::default() })
857            .await
858            .unwrap();
859
860        // Just checking the generic update is correct. There is a duplicate event, so
861        // no generic changes whatsoever!
862        assert!(generic_stream.recv().now_or_never().is_none());
863
864        // The stream doesn't report these changes *yet*. Use the events vector given
865        // when subscribing, to check that the events correspond to their new
866        // positions. The duplicated item is removed (so it's not the first
867        // element anymore), and it's added to the back of the list.
868        let (thread_events, _) = thread_event_cache.subscribe().await.unwrap();
869        assert_eq!(thread_events.len(), 2);
870        assert_eq!(thread_events[0].event_id(), Some(thread_event_id_0));
871        assert_eq!(thread_events[1].event_id(), Some(thread_event_id_1));
872    }
873
874    #[async_test]
875    async fn test_load_from_storage_resilient_to_failure() {
876        let room_id = room_id!("!r0");
877        let f = EventFactory::new().room(room_id).sender(user_id!("@mnt_io:matrix.org"));
878
879        let event_cache_store = Arc::new(MemoryStore::new());
880
881        let thread_root = event_id!("$t0");
882        let thread_event_id_0 = event_id!("$t0_ev0");
883
884        let thread_event_0 = f
885            .text_msg("hello world")
886            .event_id(thread_event_id_0)
887            .in_thread(thread_root, thread_root)
888            .into_event();
889
890        // Prefill the store with invalid data: two chunks that form a cycle.
891        event_cache_store
892            .handle_linked_chunk_updates(
893                LinkedChunkId::Thread(room_id, thread_root),
894                vec![
895                    Update::NewItemsChunk {
896                        previous: None,
897                        new: ChunkIdentifier::new(0),
898                        next: None,
899                    },
900                    Update::PushItems {
901                        at: Position::new(ChunkIdentifier::new(0), 0),
902                        items: vec![thread_event_0],
903                    },
904                    Update::NewItemsChunk {
905                        previous: Some(ChunkIdentifier::new(0)),
906                        new: ChunkIdentifier::new(1),
907                        next: Some(ChunkIdentifier::new(0)),
908                    },
909                ],
910            )
911            .await
912            .unwrap();
913
914        let client = MockClientBuilder::new(None)
915            .on_builder(|builder| {
916                builder
917                    .store_config(
918                        StoreConfig::new(CrossProcessLockConfig::multi_process("holder"))
919                            .event_cache_store(event_cache_store.clone()),
920                    )
921                    .with_threading_support(ThreadingSupport::Enabled { with_subscriptions: true })
922            })
923            .build()
924            .await;
925
926        let event_cache = client.event_cache();
927        event_cache.subscribe().unwrap();
928
929        client.base_client().get_or_create_room(room_id, RoomState::Joined);
930
931        let (thread_event_cache, _drop_handles) =
932            event_cache.thread(room_id, thread_root).await.unwrap();
933        let (thread_events, _) = thread_event_cache.subscribe().await.unwrap();
934
935        // Because the persisted content was invalid, the thread store is reset:
936        // there are no events in the cache.
937        assert!(thread_events.is_empty());
938
939        // Storage doesn't contain anything. It would also be valid that it contains a
940        // single initial empty items chunk.
941        let raw_chunks = event_cache_store
942            .load_all_chunks(LinkedChunkId::Thread(room_id, thread_root))
943            .await
944            .unwrap();
945        assert!(raw_chunks.is_empty());
946    }
947
948    #[async_test]
949    async fn test_reload_when_dirty() {
950        let user_id = user_id!("@mnt_io:matrix.org");
951        let room_id = room_id!("!raclette:patate.ch");
952
953        // The storage shared by the two clients.
954        let event_cache_store = MemoryStore::new();
955
956        // Client for the process 0.
957        let client_p0 = MockClientBuilder::new(None)
958            .on_builder(|builder| {
959                builder
960                    .store_config(
961                        StoreConfig::new(CrossProcessLockConfig::multi_process("process #0"))
962                            .event_cache_store(event_cache_store.clone()),
963                    )
964                    .with_threading_support(ThreadingSupport::Enabled { with_subscriptions: true })
965            })
966            .build()
967            .await;
968
969        // Client for the process 1.
970        let client_p1 = MockClientBuilder::new(None)
971            .on_builder(|builder| {
972                builder
973                    .store_config(
974                        StoreConfig::new(CrossProcessLockConfig::multi_process("process #1"))
975                            .event_cache_store(event_cache_store),
976                    )
977                    .with_threading_support(ThreadingSupport::Enabled { with_subscriptions: true })
978            })
979            .build()
980            .await;
981
982        let event_factory = EventFactory::new().room(room_id).sender(user_id);
983
984        let thread_root = event_id!("$t0");
985        let thread_event_id_0 = event_id!("$t0_ev0");
986        let thread_event_id_1 = event_id!("$t0_ev1");
987
988        let thread_event_0 = event_factory
989            .text_msg("comté")
990            .event_id(thread_event_id_0)
991            .in_thread(thread_root, thread_root)
992            .into_event();
993        let thread_event_1 = event_factory
994            .text_msg("morbier")
995            .event_id(thread_event_id_1)
996            .in_thread(thread_root, thread_event_id_0)
997            .into_event();
998
999        // Add events to the storage (shared by the two clients!).
1000        client_p0
1001            .event_cache_store()
1002            .lock()
1003            .await
1004            .expect("[p0] Could not acquire the event cache lock")
1005            .as_clean()
1006            .expect("[p0] Could not acquire a clean event cache lock")
1007            .handle_linked_chunk_updates(
1008                LinkedChunkId::Thread(room_id, thread_root),
1009                vec![
1010                    Update::NewItemsChunk {
1011                        previous: None,
1012                        new: ChunkIdentifier::new(0),
1013                        next: None,
1014                    },
1015                    Update::PushItems {
1016                        at: Position::new(ChunkIdentifier::new(0), 0),
1017                        items: vec![thread_event_0],
1018                    },
1019                    Update::NewItemsChunk {
1020                        previous: Some(ChunkIdentifier::new(0)),
1021                        new: ChunkIdentifier::new(1),
1022                        next: None,
1023                    },
1024                    Update::PushItems {
1025                        at: Position::new(ChunkIdentifier::new(1), 0),
1026                        items: vec![thread_event_1],
1027                    },
1028                ],
1029            )
1030            .await
1031            .unwrap();
1032
1033        // Subscribe the event caches, and create the room.
1034        let (thread_event_cache_p0, thread_event_cache_p1) = {
1035            let event_cache_p0 = client_p0.event_cache();
1036            event_cache_p0.subscribe().unwrap();
1037
1038            let event_cache_p1 = client_p1.event_cache();
1039            event_cache_p1.subscribe().unwrap();
1040
1041            client_p0.base_client().get_or_create_room(room_id, RoomState::Joined);
1042            client_p1.base_client().get_or_create_room(room_id, RoomState::Joined);
1043
1044            let (thread_event_cache_p0, _drop_handles) =
1045                event_cache_p0.thread(room_id, thread_root).await.unwrap();
1046            let (thread_event_cache_p1, _drop_handles) =
1047                event_cache_p1.thread(room_id, thread_root).await.unwrap();
1048
1049            (thread_event_cache_p0, thread_event_cache_p1)
1050        };
1051
1052        // Okay. We are ready for the test!
1053        //
1054        // First off, let's check `thread_event_cache_p0` has access to the first event
1055        // loaded in-memory, then do a pagination, and see more events.
1056        let mut updates_stream_p0 = {
1057            let thread_event_cache = &thread_event_cache_p0;
1058
1059            let (initial_updates, mut updates_stream) =
1060                thread_event_cache_p0.subscribe().await.unwrap();
1061
1062            // Initial updates contain `thread_event_id_1` only.
1063            assert_eq!(initial_updates.len(), 1);
1064            assert_eq!(initial_updates[0].event_id(), Some(thread_event_id_1));
1065            assert!(updates_stream.is_empty());
1066
1067            // Load one more event with a backpagination.
1068            thread_event_cache.pagination().run_backwards_once(1).await.unwrap();
1069
1070            // A new update for `ev_id_0` must be present.
1071            assert_matches!(
1072                updates_stream.recv().await.unwrap(),
1073                TimelineVectorDiffs { diffs, .. } => {
1074                    assert_eq!(diffs.len(), 1, "{diffs:#?}");
1075                    assert_matches!(
1076                        &diffs[0],
1077                        VectorDiff::Insert { index: 0, value: event } => {
1078                            assert_eq!(event.event_id(), Some(thread_event_id_0));
1079                        }
1080                    );
1081                }
1082            );
1083
1084            updates_stream
1085        };
1086
1087        // Second, let's check `thread_event_cache_p1` has the same accesses.
1088        let mut updates_stream_p1 = {
1089            let thread_event_cache = &thread_event_cache_p1;
1090            let (initial_updates, mut updates_stream) =
1091                thread_event_cache_p1.subscribe().await.unwrap();
1092
1093            // Initial updates contain `thread_event_id_1` only.
1094            assert_eq!(initial_updates.len(), 1);
1095            assert_eq!(initial_updates[0].event_id(), Some(thread_event_id_1));
1096            assert!(updates_stream.is_empty());
1097
1098            // Load one more event with a backpagination.
1099            thread_event_cache.pagination().run_backwards_once(1).await.unwrap();
1100
1101            // A new update for `thread_event_id_0` must be present.
1102            assert_matches!(
1103                updates_stream.recv().await.unwrap(),
1104                TimelineVectorDiffs { diffs, .. } => {
1105                    assert_eq!(diffs.len(), 1, "{diffs:#?}");
1106                    assert_matches!(
1107                        &diffs[0],
1108                        VectorDiff::Insert { index: 0, value: event } => {
1109                            assert_eq!(event.event_id(), Some(thread_event_id_0));
1110                        }
1111                    );
1112                }
1113            );
1114
1115            updates_stream
1116        };
1117
1118        // Do this a couple times, for the fun.
1119        for _ in 0..3 {
1120            // Third, because `thread_event_cache_p1` has locked the store, the lock
1121            // is dirty for `thread_event_cache_p0`, so it will shrink to its last
1122            // chunk for the thread!
1123            {
1124                let thread_event_cache = &thread_event_cache_p0;
1125                let updates_stream = &mut updates_stream_p0;
1126
1127                // `thread_event_id_1` must be loaded in memory, just like before.
1128                // However, `thread_event_id_0` must NOT be loaded in memory. It WAS loaded, but
1129                // the state has been reloaded to its last chunk.
1130                let (initial_updates, _) = thread_event_cache.subscribe().await.unwrap();
1131
1132                assert_eq!(initial_updates.len(), 1);
1133                assert_eq!(initial_updates[0].event_id(), Some(thread_event_id_1));
1134
1135                // The reload can be observed via the updates too.
1136                assert_matches!(
1137                    updates_stream.recv().await.unwrap(),
1138                    TimelineVectorDiffs { diffs, .. } => {
1139                        assert_eq!(diffs.len(), 2, "{diffs:#?}");
1140                        assert_matches!(&diffs[0], VectorDiff::Clear);
1141                        assert_matches!(
1142                            &diffs[1],
1143                            VectorDiff::Append { values: events } => {
1144                                assert_eq!(events.len(), 1);
1145                                assert_eq!(events[0].event_id(), Some(thread_event_id_1));
1146                            }
1147                        );
1148                    }
1149                );
1150
1151                // Load one more event with a backpagination.
1152                thread_event_cache.pagination().run_backwards_once(1).await.unwrap();
1153
1154                // `thread_event_id_0` must now be loaded in memory.
1155                // The pagination can be observed via the updates.
1156                assert_matches!(
1157                    updates_stream.recv().await.unwrap(),
1158                    TimelineVectorDiffs { diffs, .. } => {
1159                        assert_eq!(diffs.len(), 1, "{diffs:#?}");
1160                        assert_matches!(
1161                            &diffs[0],
1162                            VectorDiff::Insert { index: 0, value: event } => {
1163                                assert_eq!(event.event_id(), Some(thread_event_id_0));
1164                            }
1165                        );
1166                    }
1167                );
1168            }
1169
1170            // Fourth, because `thread_event_cache_p0` has locked the store again, the lock
1171            // is dirty for `thread_event_cache_p1` too!, so it will shrink to its last
1172            // chunk for the thread!
1173            {
1174                let thread_event_cache = &thread_event_cache_p1;
1175                let updates_stream = &mut updates_stream_p1;
1176
1177                // `thread_event_id_1` must be loaded in memory, just like before.
1178                // However, `thread_event_id_0` must NOT be loaded in memory. It WAS loaded, but
1179                // the state has shrunk to its last chunk.
1180                let (initial_updates, _) = thread_event_cache.subscribe().await.unwrap();
1181
1182                assert_eq!(initial_updates.len(), 1);
1183                assert_eq!(initial_updates[0].event_id(), Some(thread_event_id_1));
1184
1185                // The reload can be observed via the updates too.
1186                assert_matches!(
1187                    updates_stream.recv().await.unwrap(),
1188                    TimelineVectorDiffs { diffs, .. } => {
1189                        assert_eq!(diffs.len(), 2, "{diffs:#?}");
1190                        assert_matches!(&diffs[0], VectorDiff::Clear);
1191                        assert_matches!(
1192                            &diffs[1],
1193                            VectorDiff::Append { values: events } => {
1194                                assert_eq!(events.len(), 1);
1195                                assert_eq!(events[0].event_id(), Some(thread_event_id_1));
1196                            }
1197                        );
1198                    }
1199                );
1200
1201                // Load one more event with a backpagination.
1202                thread_event_cache.pagination().run_backwards_once(1).await.unwrap();
1203
1204                // `thread_event_id_0` must now be loaded in memory.
1205                // The pagination can be observed via the updates.
1206                assert_matches!(
1207                    updates_stream.recv().await.unwrap(),
1208                    TimelineVectorDiffs { diffs, .. } => {
1209                        assert_eq!(diffs.len(), 1, "{diffs:#?}");
1210                        assert_matches!(
1211                            &diffs[0],
1212                            VectorDiff::Insert { index: 0, value: event } => {
1213                                assert_eq!(event.event_id(), Some(thread_event_id_0));
1214                            }
1215                        );
1216                    }
1217                );
1218            }
1219        }
1220    }
1221
1222    #[async_test]
1223    async fn test_auto_shrink_after_all_subscribers_are_gone() {
1224        let room_id = room_id!("!r0");
1225        let thread_id = event_id!("$t0");
1226
1227        let client = MockClientBuilder::new(None).build().await;
1228
1229        let f = EventFactory::new().room(room_id).sender(*ALICE);
1230
1231        let event_id_0 = event_id!("$ev0");
1232        let event_id_1 = event_id!("$ev1");
1233
1234        let thread_root =
1235            f.text_msg("gr00t").event_id(thread_id).in_thread(thread_id, thread_id).into_event();
1236        let event_0 =
1237            f.text_msg("hello").event_id(event_id_0).in_thread(thread_id, event_id_0).into_event();
1238        let event_1 =
1239            f.text_msg("world").event_id(event_id_1).in_thread(thread_id, event_id_1).into_event();
1240
1241        // Fill the event cache store with an initial linked chunk with 2 events chunks.
1242        {
1243            client
1244                .event_cache_store()
1245                .lock()
1246                .await
1247                .expect("Could not acquire the event cache lock")
1248                .as_clean()
1249                .expect("Could not acquire a clean event cache lock")
1250                .handle_linked_chunk_updates(
1251                    LinkedChunkId::Thread(room_id, thread_id),
1252                    vec![
1253                        Update::NewItemsChunk {
1254                            previous: None,
1255                            new: ChunkIdentifier::new(0),
1256                            next: None,
1257                        },
1258                        Update::PushItems {
1259                            at: Position::new(ChunkIdentifier::new(0), 0),
1260                            items: vec![thread_root, event_0],
1261                        },
1262                        Update::NewItemsChunk {
1263                            previous: Some(ChunkIdentifier::new(0)),
1264                            new: ChunkIdentifier::new(1),
1265                            next: None,
1266                        },
1267                        Update::PushItems {
1268                            at: Position::new(ChunkIdentifier::new(1), 0),
1269                            items: vec![event_1],
1270                        },
1271                    ],
1272                )
1273                .await
1274                .unwrap();
1275        }
1276
1277        let event_cache = client.event_cache();
1278        event_cache.subscribe().unwrap();
1279
1280        client.base_client().get_or_create_room(room_id, RoomState::Joined);
1281
1282        let (thread_event_cache, _drop_handles) =
1283            event_cache.thread(room_id, thread_id).await.unwrap();
1284
1285        // Sanity check: lazily loaded, so only includes one item at start.
1286        let (events1, mut stream1) = thread_event_cache.subscribe().await.unwrap();
1287        assert_eq!(events1.len(), 1);
1288        assert_eq!(events1[0].event_id(), Some(event_id_1));
1289        assert!(stream1.is_empty());
1290
1291        let mut generic_stream = event_cache.subscribe_to_room_generic_updates();
1292
1293        // Force loading the full linked chunk by back-paginating.
1294        let outcome = thread_event_cache.pagination().run_backwards_once(20).await.unwrap();
1295        assert_eq!(outcome.events.len(), 2);
1296        assert_eq!(outcome.events[0].event_id(), Some(event_id_0));
1297        assert_eq!(outcome.events[1].event_id(), Some(thread_id));
1298        assert!(outcome.reached_start);
1299
1300        // We also get an update about the loading from the store. Ignore it, for this
1301        // test's sake.
1302        assert_let_timeout!(Ok(TimelineVectorDiffs { diffs, .. }) = stream1.recv());
1303        assert_eq!(diffs.len(), 2);
1304        assert_matches!(&diffs[0], VectorDiff::Insert { index: 0, value } => {
1305            assert_eq!(value.event_id(), Some(thread_id));
1306        });
1307        assert_matches!(&diffs[1], VectorDiff::Insert { index: 1, value } => {
1308            assert_eq!(value.event_id(), Some(event_id_0));
1309        });
1310
1311        assert!(stream1.is_empty());
1312
1313        assert_let_timeout!(
1314            Ok(RoomEventCacheGenericUpdate { room_id: expected_room_id }) = generic_stream.recv()
1315        );
1316        assert_eq!(expected_room_id, room_id);
1317        assert!(generic_stream.is_empty());
1318
1319        // Have another subscriber.
1320        // Since it's not the first one, and the previous one loaded some more events,
1321        // the second subscribers sees them all.
1322        let (events2, stream2) = thread_event_cache.subscribe().await.unwrap();
1323        assert_eq!(events2.len(), 3);
1324        assert_eq!(events2[0].event_id(), Some(thread_id));
1325        assert_eq!(events2[1].event_id(), Some(event_id_0));
1326        assert_eq!(events2[2].event_id(), Some(event_id_1));
1327        assert!(stream2.is_empty());
1328
1329        // Grab a receiver for testing no diffs is sent.
1330        let subscriber = {
1331            let state = thread_event_cache.inner.state.read().await.unwrap();
1332            state.update_sender.new_thread_receiver()
1333        };
1334
1335        // Drop the first stream, and wait a bit.
1336        drop(stream1);
1337        yield_now().await;
1338
1339        // The second stream remains undisturbed.
1340        assert!(stream2.is_empty());
1341
1342        // Now drop the second stream, and wait a bit.
1343        drop(stream2);
1344        yield_now().await;
1345
1346        // The linked chunk must have auto-shrunk by now.
1347
1348        {
1349            // Check the inner state: there's no more shared auto-shrinker.
1350            let state = thread_event_cache.inner.state.read().await.unwrap();
1351            assert_eq!(state.subscribers_handle().count(), 0);
1352
1353            // No diff is sent when the linked chunk has auto-shrunk.
1354            assert!(subscriber.is_empty());
1355            assert!(generic_stream.is_empty());
1356        }
1357
1358        // Getting the events will only give us the latest chunk.
1359        let events3 = thread_event_cache
1360            .inner
1361            .state
1362            .read()
1363            .await
1364            .unwrap()
1365            .thread_linked_chunk()
1366            .events()
1367            .map(|(_position, item)| item.clone())
1368            .collect::<Vec<_>>();
1369        assert_eq!(events3.len(), 1);
1370        assert_eq!(events3[0].event_id(), Some(event_id_1));
1371    }
1372}