Skip to main content

matrix_sdk/event_cache/caches/event_focused/
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//! Event-focused timeline cache.
16//!
17//! This module provides [`EventFocusedCache`], a cache for an event-focused
18//! timeline (e.g., for permalinks). It handles:
19//! - Initialization from a focused event via `/context`.
20//! - Thread detection (if the focused event is in a thread).
21//! - Forward and backward pagination.
22//! - In-memory storage, as these linked chunks are meant to be short-lived.
23//!
24//! Pagination tokens are stored as Gap items in the linked chunk:
25//! - Backward token: Gap at the front of the linked chunk.
26//! - Forward token: Gap at the back of the linked chunk.
27//!
28//! This allows pagination to resume at any point, and supports a future use
29//! case where we'd want to persist these caches on disk (e.g., for permalinks
30//! to work across sessions).
31
32use std::sync::Arc;
33
34use eyeball_im::VectorDiff;
35use matrix_sdk_base::{
36    deserialized_responses::TimelineEvent,
37    event_cache::{Event, Gap},
38    linked_chunk::OwnedLinkedChunkId,
39};
40use matrix_sdk_common::{linked_chunk::ChunkIdentifier, serde_helpers::extract_thread_root};
41use ruma::{OwnedEventId, OwnedRoomId, RoomId, UInt, api::Direction};
42use tokio::sync::broadcast::{Receiver, Sender};
43use tracing::{instrument, trace};
44
45#[cfg(feature = "e2e-encryption")]
46use super::super::redecryptor::{MaybeResolvedEvent, TryResolveEvents};
47use super::{
48    super::{
49        EventCacheError, EventsOrigin, Result, RoomEventCacheLinkedChunkUpdate,
50        states::{
51            CacheStateLock, ReloadPreprocessing, StateLock, selectors::EventFocusedStateSelector,
52        },
53    },
54    TimelineVectorDiffs,
55    event_linked_chunk::EventLinkedChunk,
56};
57use crate::{
58    Room,
59    event_cache::caches::room::LinkedChunkUpdateSender,
60    paginators::{PaginationResult, Paginator, StartFromResult, thread::PaginableThread},
61    room::{IncludeRelations, MessagesOptions, RelationsOptions, WeakRoom},
62};
63
64/// Options for controlling the behaviour of an `EventFocusedCache` when the
65/// focused event may be part of a thread, or a thread's root.
66#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
67pub enum EventFocusThreadMode {
68    /// Force the timeline represented by the linked chunk to only include
69    /// threaded events.
70    ///
71    /// When the focused event is part of a thread, the linked chunk will be
72    /// focused on that thread's root. Otherwise, the linked chunk will treat
73    /// the target event itself as the thread root. Threaded events will never
74    /// be hidden.
75    ForceThread,
76
77    /// Automatically determine if the target event is part of a thread or not.
78    ///
79    /// If the event is part of a thread, the linked chunk will be filtered to
80    /// on-thread events.
81    Automatic,
82}
83
84/// The mode of pagination for an event-focused linked chunk.
85#[derive(Debug, Clone)]
86pub(crate) enum EventFocusedPaginationMode {
87    /// Standard room pagination (for all events as for an unthreaded/main room
88    /// linked chunk).
89    Room { hide_thread_events: bool },
90
91    /// Threaded pagination (the focused event is part of a thread).
92    Thread {
93        /// The root event ID of the thread.
94        thread_root: OwnedEventId,
95    },
96}
97
98pub struct EventFocusedCacheState {
99    /// The room owning this event-focused cache.
100    room: WeakRoom,
101
102    /// The focused event ID.
103    focused_event_id: OwnedEventId,
104
105    /// The pagination mode (room or thread).
106    pagination_mode: EventFocusedPaginationMode,
107
108    /// The linked chunk for this event-focused cache.
109    chunk: EventLinkedChunk,
110
111    /// The `num_context_events` given to [`Self::start_from`].
112    ///
113    /// This is useful for [`Self::reload`] to load the same amount of events.
114    initial_num_context_events: u16,
115
116    /// The thread mode.
117    thread_mode: EventFocusThreadMode,
118
119    /// A sender of timeline updates.
120    pub update_sender: EventFocusedCacheUpdateSender,
121
122    /// A sender for globally observable linked chunk updates.
123    linked_chunk_update_sender: LinkedChunkUpdateSender,
124}
125
126impl EventFocusedCacheState {
127    /// Initialize the cache from a focused event.
128    ///
129    /// This uses `/context` to fetch the event with surrounding context.
130    ///
131    /// This detects if the event is part of a thread and sets up the
132    /// appropriate pagination mode.
133    ///
134    /// Pagination tokens are stored as gaps in the linked chunk:
135    ///
136    /// - Backward token (start): Gap at the front of the linked chunk.
137    /// - Forward token (end): Gap at the back of the linked chunk.
138    #[instrument(skip(self), fields(room_id = %self.room.room_id(), event_id = %self.focused_event_id))]
139    async fn start_from(
140        &mut self,
141        num_context_events: u16,
142        thread_mode: EventFocusThreadMode,
143    ) -> Result<StartFromResult> {
144        self.initial_num_context_events = num_context_events;
145        self.thread_mode = thread_mode;
146
147        let result = self.reload_impl().await?;
148
149        // Empty the updates_as_vector_diffs(), since it's impossible for an
150        // observer to have subscribed to this cache yet, since this code is
151        // part of the constructor flow.
152        //
153        // If we didn't empty those, such initial updates would be duplicated,
154        // since the subscriber would get the full initial list of events as
155        // diffs and as a set of initial events.
156        let _ = self.chunk.updates_as_vector_diffs();
157
158        Ok(result)
159    }
160
161    /// Reload the event-focused cache: only the last events will be reloaded,
162    /// shrinking the in-memory size of the cache.
163    ///
164    /// Since there is no persistent storage for this cache, `preprocessing` is
165    /// ignored.
166    #[must_use = "Propagate `VectorDiff` updates via `TimelineVectorDiffs`"]
167    pub async fn reload(
168        &mut self,
169        _preprocessing: ReloadPreprocessing,
170    ) -> Result<Vec<VectorDiff<Event>>> {
171        let _ = self.reload_impl().await?;
172
173        Ok(self.chunk.updates_as_vector_diffs())
174    }
175
176    /// Replace existing events, then load and store fresh events from
177    /// `/context`.
178    async fn reload_impl(&mut self) -> Result<StartFromResult> {
179        let room = self.room.get().ok_or(EventCacheError::ClientDropped)?;
180        let num_context_events = self.initial_num_context_events;
181        let thread_mode = self.thread_mode;
182
183        trace!(num_context_events, "fetching event with context via /context");
184
185        let paginator = Paginator::new(room);
186
187        let result =
188            paginator.start_from(&self.focused_event_id, UInt::from(num_context_events)).await?;
189
190        // Detect if the focused event is part of a thread.
191        let thread_root = match thread_mode {
192            EventFocusThreadMode::ForceThread => {
193                // Try to extract a thread root from the focused event, first.
194                let focused_event = result
195                    .events
196                    .iter()
197                    .find(|event| event.event_id() == Some(&self.focused_event_id));
198
199                // If the focused event has a thread root, use it.
200                let mut thread_root =
201                    focused_event.and_then(|event| extract_thread_root(event.raw()));
202
203                // If there's no thread root, consider that the focused event
204                // itself is the thread root.
205                if thread_root.is_none() {
206                    thread_root = Some(self.focused_event_id.clone());
207                }
208
209                trace!("force thread mode enabled, treating focused event as thread root");
210                thread_root
211            }
212
213            EventFocusThreadMode::Automatic => {
214                trace!(
215                    "automatic thread mode enabled, checking if focused event is part of a thread"
216                );
217                result
218                    .events
219                    .iter()
220                    .find(|event| event.event_id() == Some(&self.focused_event_id))
221                    .and_then(|event| extract_thread_root(event.raw()))
222            }
223        };
224
225        // Get pagination tokens from the paginator.
226        let tokens = paginator.tokens();
227
228        if let Some(root_id) = thread_root {
229            trace!(thread_root = %root_id, "focused event is part of a thread, setting up thread pagination");
230
231            // Check if the thread root is included in the response. Start from
232            // the beginning, since it's more likely to be around there, in that
233            // case.
234            let includes_root =
235                result.events.iter().any(|event| event.event_id() == Some(&root_id));
236
237            self.pagination_mode =
238                EventFocusedPaginationMode::Thread { thread_root: root_id.clone() };
239
240            // Filter events to only include those in the thread.
241            let thread_events = result
242                .events
243                .iter()
244                .filter(|event| {
245                    extract_thread_root(event.raw()).as_ref() == Some(&root_id)
246                        || event.event_id() == Some(&root_id)
247                })
248                .cloned()
249                .collect();
250
251            // Determine backward token (only if we don't have the thread root).
252            let backward_token = if includes_root {
253                // We have the root, no need for backward pagination.
254                None
255            } else {
256                tokens.previous.into_token()
257            };
258
259            // Forward token.
260            let forward_token = tokens.next.into_token();
261
262            self.add_initial_events_with_gaps(thread_events, backward_token, forward_token);
263        } else {
264            trace!("focused event is not part of a thread, setting up room pagination");
265
266            let backward_token = tokens.previous.into_token();
267            let forward_token = tokens.next.into_token();
268
269            let hide_thread_events =
270                matches!(thread_mode, EventFocusThreadMode::Automatic) && thread_root.is_none();
271
272            self.pagination_mode = EventFocusedPaginationMode::Room { hide_thread_events };
273
274            let events = if hide_thread_events {
275                result
276                    .events
277                    .iter()
278                    .filter(|event| extract_thread_root(event.raw()).is_none())
279                    .cloned()
280                    .collect()
281            } else {
282                result.events.clone()
283            };
284
285            self.add_initial_events_with_gaps(events, backward_token, forward_token);
286        }
287
288        self.propagate_changes();
289
290        Ok(result)
291    }
292
293    /// Add initial events to the chunk, with gaps for pagination tokens.
294    fn add_initial_events_with_gaps(
295        &mut self,
296        events: Vec<TimelineEvent>,
297        prev_gap_token: Option<String>,
298        next_gap_token: Option<String>,
299    ) {
300        // Clear all existing events as we are about to insert initial events.
301        self.chunk.reset();
302
303        // Insert backward gap at the back if we have a token, and the events
304        // themselves.
305        self.chunk
306            .push_live_events(prev_gap_token.map(|prev_token| Gap { token: prev_token }), &events);
307
308        // Insert forward gap at back if we have a token.
309        if let Some(next_token) = next_gap_token {
310            trace!("inserting forward pagination gap at back");
311            self.chunk.push_gap(Gap { token: next_token });
312        }
313    }
314
315    /// Propagate changes to the linked chunk update sender.
316    fn propagate_changes(&mut self) {
317        let updates = self.chunk.store_updates().take();
318        if !updates.is_empty() {
319            self.linked_chunk_update_sender.send(RoomEventCacheLinkedChunkUpdate {
320                updates,
321                linked_chunk_id: OwnedLinkedChunkId::EventFocused(
322                    self.room.room_id().to_owned(),
323                    self.focused_event_id.clone(),
324                ),
325            });
326        }
327    }
328
329    /// Notify subscribers of timeline updates.
330    fn notify_subscribers(&mut self, origin: EventsOrigin) {
331        let diffs = self.chunk.updates_as_vector_diffs();
332        if !diffs.is_empty() {
333            let _ = self.update_sender.send(TimelineVectorDiffs { diffs, origin });
334        }
335    }
336
337    /// Return the first chunk as a gap, if it's one.
338    fn first_chunk_as_gap(&self) -> Option<(ChunkIdentifier, Gap)> {
339        self.chunk.first_chunk_as_gap()
340    }
341
342    /// Return the last chunk as a gap, if it's one.
343    fn last_chunk_as_gap(&self) -> Option<(ChunkIdentifier, Gap)> {
344        self.chunk.last_chunk_as_gap()
345    }
346
347    /// Paginate backwards in this event-focused linked chunk.
348    ///
349    /// This finds the gap at the front of the linked chunk, fetches older
350    /// events, replaces the gap with the events, and inserts a new gap if there
351    /// are more events to fetch.
352    #[instrument(skip(self), fields(room_id = %self.room.room_id()))]
353    async fn paginate_backwards(&mut self, num_events: u16) -> Result<PaginationResult> {
354        let room = self.room.get().ok_or(EventCacheError::ClientDropped)?;
355
356        // Find the gap at the front (backward pagination token).
357        let Some((gap_id, gap)) = self.first_chunk_as_gap() else {
358            // No gap at front means we've already hit the start of the
359            // timeline.
360            trace!("no front gap found, already at timeline start");
361            return Ok(PaginationResult { events: Vec::new(), hit_end_of_timeline: true });
362        };
363
364        let token = gap.token;
365        trace!(?token, "paginating backwards with token from front gap");
366
367        // Fetch events based on pagination mode.
368        let (mut events, new_token) = match &self.pagination_mode {
369            EventFocusedPaginationMode::Room { .. } => {
370                Self::fetch_room_backwards(&room, num_events, &token).await?
371            }
372            EventFocusedPaginationMode::Thread { thread_root } => {
373                Self::fetch_thread_backwards(&room, num_events, &token, thread_root.clone()).await?
374            }
375        };
376
377        // Events are in the reverse order, per the API contracts defined in the
378        // two fetch methods.
379        events.reverse();
380
381        let hit_end = new_token.is_none();
382        let new_gap = new_token.map(|t| Gap { token: t });
383
384        let hide_thread_events = match &self.pagination_mode {
385            EventFocusedPaginationMode::Room { hide_thread_events } => *hide_thread_events,
386            EventFocusedPaginationMode::Thread { .. } => false,
387        };
388
389        let events = if hide_thread_events {
390            events.into_iter().filter(|event| extract_thread_root(event.raw()).is_none()).collect()
391        } else {
392            events
393        };
394
395        // Replace the gap and insert the new events.
396        self.chunk.push_backwards_pagination_events(Some(gap_id), new_gap, &events);
397
398        self.propagate_changes();
399        self.notify_subscribers(EventsOrigin::Pagination);
400
401        Ok(PaginationResult { events, hit_end_of_timeline: hit_end })
402    }
403
404    /// Fetch events for backward room pagination (returns events and optional
405    /// next token).
406    ///
407    /// Returns the events in the same ordering as the one received by the
408    /// server, i.e., newest to oldest.
409    async fn fetch_room_backwards(
410        room: &Room,
411        num_events: u16,
412        token: &str,
413    ) -> Result<(Vec<Event>, Option<String>)> {
414        let mut options = MessagesOptions::backward().from(token);
415        options.limit = UInt::from(num_events);
416
417        let messages = room
418            .messages(options)
419            .await
420            .map_err(|err| EventCacheError::PaginationError(Arc::new(err)))?;
421
422        Ok((messages.chunk, messages.end))
423    }
424
425    /// Fetch events for backward thread pagination.
426    ///
427    /// Returns the events in the same ordering as the one received by the
428    /// server, i.e., newest to oldest.
429    async fn fetch_thread_backwards(
430        room: &Room,
431        num_events: u16,
432        token: &str,
433        thread_root: OwnedEventId,
434    ) -> Result<(Vec<Event>, Option<String>)> {
435        let options = RelationsOptions {
436            from: Some(token.to_owned()),
437            dir: Direction::Backward,
438            limit: Some(UInt::from(num_events)),
439            include_relations: IncludeRelations::AllRelations,
440            recurse: true,
441        };
442
443        let mut result = room
444            .relations(thread_root.clone(), options)
445            .await
446            .map_err(|err| EventCacheError::PaginationError(Arc::new(err)))?;
447
448        // If we hit the end (no more token), load the thread root event.
449        if result.next_batch_token.is_none() {
450            let root_event = room
451                .load_event(&thread_root)
452                .await
453                .map_err(|err| EventCacheError::PaginationError(Arc::new(err)))?;
454            result.chunk.push(root_event);
455        }
456
457        Ok((result.chunk, result.next_batch_token))
458    }
459
460    /// Paginate forwards in this event-focused timeline.
461    ///
462    /// This finds the gap at the back of the linked chunk, fetches newer
463    /// events, replaces the gap with the events, and inserts a new gap if there
464    /// are more events to fetch.
465    #[instrument(skip(self), fields(room_id = %self.room.room_id()))]
466    async fn paginate_forwards(&mut self, num_events: u16) -> Result<PaginationResult> {
467        let room = self.room.get().ok_or(EventCacheError::ClientDropped)?;
468
469        // Find the gap at the back (forward pagination token).
470        let Some((gap_id, gap)) = self.last_chunk_as_gap() else {
471            // No gap at back means we've already hit the end of the timeline.
472            trace!("no back gap found, already at timeline end");
473            return Ok(PaginationResult { events: Vec::new(), hit_end_of_timeline: true });
474        };
475
476        let token = gap.token;
477        trace!(?token, "paginating forwards with token from back gap");
478
479        // Fetch events based on pagination mode.
480        let (events, new_token) = match &self.pagination_mode {
481            EventFocusedPaginationMode::Room { .. } => {
482                Self::fetch_room_forwards(&room, num_events, &token).await?
483            }
484            EventFocusedPaginationMode::Thread { thread_root } => {
485                Self::fetch_thread_forwards(&room, num_events, &token, thread_root.clone()).await?
486            }
487        };
488
489        let hit_end = new_token.is_none();
490        let new_gap = new_token.map(|t| Gap { token: t });
491
492        let hide_thread_events = match &self.pagination_mode {
493            EventFocusedPaginationMode::Room { hide_thread_events } => *hide_thread_events,
494            EventFocusedPaginationMode::Thread { .. } => false,
495        };
496
497        let events = if hide_thread_events {
498            events.into_iter().filter(|event| extract_thread_root(event.raw()).is_none()).collect()
499        } else {
500            events
501        };
502
503        // Replace the gap and insert new events.
504        self.chunk.push_forwards_pagination_events(Some(gap_id), new_gap, &events);
505
506        self.propagate_changes();
507        self.notify_subscribers(EventsOrigin::Pagination);
508
509        Ok(PaginationResult { events, hit_end_of_timeline: hit_end })
510    }
511
512    /// Fetch events for forward room pagination.
513    async fn fetch_room_forwards(
514        room: &Room,
515        num_events: u16,
516        token: &str,
517    ) -> Result<(Vec<Event>, Option<String>)> {
518        let mut options = MessagesOptions::new(Direction::Forward);
519        options = options.from(Some(token));
520        options.limit = UInt::from(num_events);
521
522        let messages = room
523            .messages(options)
524            .await
525            .map_err(|err| EventCacheError::PaginationError(Arc::new(err)))?;
526
527        Ok((messages.chunk, messages.end))
528    }
529
530    /// Fetch events for forward thread pagination.
531    async fn fetch_thread_forwards(
532        room: &Room,
533        num_events: u16,
534        token: &str,
535        thread_root: OwnedEventId,
536    ) -> Result<(Vec<Event>, Option<String>)> {
537        let options = RelationsOptions {
538            from: Some(token.to_owned()),
539            dir: Direction::Forward,
540            limit: Some(UInt::from(num_events)),
541            include_relations: IncludeRelations::AllRelations,
542            recurse: true,
543        };
544
545        let result = room
546            .relations(thread_root, options)
547            .await
548            .map_err(|err| EventCacheError::PaginationError(Arc::new(err)))?;
549
550        Ok((result.chunk, result.next_batch_token))
551    }
552}
553
554/// A cache for an event-focused timeline.
555///
556/// This represents a timeline centred around a specific event (e.g., from a
557/// permalink), supporting both forward and backward pagination. The focused
558/// event may be part of a thread, in which case pagination will use the
559/// `/relations` API instead of `/messages`.
560///
561/// Pagination tokens are stored as Gap items in the linked chunk itself:
562///
563/// - A gap at the **front** (first position) contains the backward pagination
564///   token.
565/// - A gap at the **back** (last position) contains the forward pagination
566///   token.
567///
568/// This is a shallow data structure, and can be cloned cheaply.
569#[derive(Clone)]
570pub struct EventFocusedCache {
571    room_id: OwnedRoomId,
572    inner: Arc<CacheStateLock<EventFocusedStateSelector>>,
573}
574
575impl EventFocusedCache {
576    /// Create a new empty event-focused cache.
577    pub(super) async fn new(
578        room: WeakRoom,
579        key: EventFocusedCacheKey,
580        state: &StateLock,
581        linked_chunk_update_sender: LinkedChunkUpdateSender,
582    ) -> Result<Self> {
583        let room_id = room.room_id().to_owned();
584
585        let cache_state = state
586            .try_insert_once_with(
587                EventFocusedStateSelector::new(room_id.clone(), key.clone()),
588                |_store_guard| async {
589                    Ok(EventFocusedCacheState {
590                        room,
591                        focused_event_id: key.focused_event_id,
592                        pagination_mode: EventFocusedPaginationMode::Room {
593                            hide_thread_events: false,
594                        },
595                        chunk: EventLinkedChunk::new(),
596                        initial_num_context_events: 0, // dummy value
597                        thread_mode: EventFocusThreadMode::Automatic, // dummy value
598                        update_sender: Sender::new(32),
599                        linked_chunk_update_sender,
600                    })
601                },
602            )
603            .await?;
604
605        Ok(Self { room_id, inner: Arc::new(cache_state) })
606    }
607
608    /// Get the room ID of this cache.
609    pub fn room_id(&self) -> &RoomId {
610        &self.room_id
611    }
612
613    /// Read all current events.
614    ///
615    /// Use [`EventFocusedCache::subscribe`] to get all current events, plus a
616    /// subscriber.
617    pub async fn events(&self) -> Result<Vec<Event>> {
618        let state = self.inner.read().await?;
619
620        Ok(state.chunk.events().map(|(_position, item)| item.clone()).collect())
621    }
622
623    /// Subscribe to updates from this event-focused timeline.
624    pub async fn subscribe(&self) -> Result<(Vec<Event>, Receiver<TimelineVectorDiffs>)> {
625        let state = self.inner.read().await?;
626        let events = state.chunk.events().map(|(_position, item)| item.clone()).collect();
627        let recv = state.update_sender.subscribe();
628        Ok((events, recv))
629    }
630
631    /// Check if we've hit the start of the timeline (no more backward
632    /// pagination possible).
633    pub async fn hit_timeline_start(&self) -> Result<bool> {
634        Ok(self.inner.read().await?.first_chunk_as_gap().is_none())
635    }
636
637    /// Check if we've hit the end of the timeline (no more forward pagination
638    /// possible).
639    pub async fn hit_timeline_end(&self) -> Result<bool> {
640        Ok(self.inner.read().await?.last_chunk_as_gap().is_none())
641    }
642
643    /// Start the event-focused timeline from the focused event, fetching
644    /// context events and detecting thread membership.
645    pub(super) async fn start_from(
646        &self,
647        num_context_events: u16,
648        thread_mode: EventFocusThreadMode,
649    ) -> Result<StartFromResult> {
650        self.inner.write().await?.start_from(num_context_events, thread_mode).await
651    }
652
653    /// Paginate backwards in this event-focused timeline, be it room or thread
654    /// pagination depending on the mode.
655    pub async fn paginate_backwards(&self, num_events: u16) -> Result<PaginationResult> {
656        self.inner.write().await?.paginate_backwards(num_events).await
657    }
658
659    /// Paginate forwards in this event-focused timeline, be it room or thread
660    /// pagination depending on the mode.
661    pub async fn paginate_forwards(&self, num_events: u16) -> Result<PaginationResult> {
662        self.inner.write().await?.paginate_forwards(num_events).await
663    }
664
665    /// Get the thread root event ID if this linked chunk is in thread mode.
666    pub async fn thread_root(&self) -> Result<Option<OwnedEventId>> {
667        Ok(match &self.inner.read().await?.pagination_mode {
668            EventFocusedPaginationMode::Thread { thread_root } => Some(thread_root.clone()),
669            _ => None,
670        })
671    }
672
673    /// Try to locate the events in the linked chunk corresponding to the given
674    /// list of resolved events, and replace them, while alerting observers
675    /// about the update.
676    #[cfg(feature = "e2e-encryption")]
677    pub(in super::super) async fn replace_in_memory_utds(
678        &self,
679        resolved_events: &[MaybeResolvedEvent],
680    ) -> Result<()> {
681        let mut state = self.inner.write().await?;
682
683        // `Redecryptor` tried to resolve the events partly based on in-store
684        // events. Because this cache doesn't persist anything in the store,
685        // `Redecryptor` is unable to resolve some events present here. To
686        // address that, let's try to resolve events here with the current
687        // `EventLinkedChunk`.
688        let new_resolved_events = resolved_events.try_resolve_events(&state.chunk);
689
690        if state.chunk.replace_utds(&new_resolved_events) {
691            state.propagate_changes();
692            state.notify_subscribers(EventsOrigin::Cache);
693        }
694
695        Ok(())
696    }
697}
698
699#[cfg(not(tarpaulin_include))]
700impl std::fmt::Debug for EventFocusedCache {
701    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
702        f.debug_struct("EventFocusedCache").finish_non_exhaustive()
703    }
704}
705
706/// Key for the event-focused caches.
707#[derive(Clone, Debug, Hash, PartialEq, Eq)]
708pub struct EventFocusedCacheKey {
709    /// The event ID that the cache is focused on.
710    pub focused_event_id: OwnedEventId,
711    /// The thread mode for this cache.
712    pub thread_mode: EventFocusThreadMode,
713}
714
715/// A small type to send updates in all channels.
716pub type EventFocusedCacheUpdateSender = Sender<TimelineVectorDiffs>;