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