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