Skip to main content

matrix_sdk/latest_events/
room_latest_events.rs

1// Copyright 2025 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
15use std::{collections::HashMap, ops::ControlFlow, sync::Arc};
16
17use matrix_sdk_base::RoomInfoNotableUpdateReasons;
18use ruma::{EventId, OwnedEventId, UserId, events::room::power_levels::RoomPowerLevels};
19use tokio::sync::{OnceCell, OwnedRwLockReadGuard, OwnedRwLockWriteGuard, RwLock};
20use tracing::{debug, error, instrument, warn};
21
22use super::{
23    LatestEvent, filter_timeline_event,
24    latest_event::{IsLatestEventValueNone, NeedMoreEvents, With},
25};
26use crate::{
27    Room,
28    event_cache::{
29        BackPaginationOutcome, EventCache, EventCacheError, RoomEventCache,
30        back_pagination_queue::{self, BackPaginationRequest},
31    },
32    room::WeakRoom,
33    send_queue::RoomSendQueueUpdate,
34};
35
36/// Type holding the [`LatestEvent`] for a room and for all its threads.
37#[derive(Debug)]
38pub(super) struct RoomLatestEvents {
39    /// The state of this type.
40    state: Arc<RwLock<RoomLatestEventsState>>,
41}
42
43impl RoomLatestEvents {
44    /// Create a new [`RoomLatestEvents`].
45    pub fn new(
46        weak_room: WeakRoom,
47        event_cache: &EventCache,
48    ) -> With<Self, IsLatestEventValueNone> {
49        let latest_event_with = Self::create_latest_event(&weak_room, None);
50
51        With::map(latest_event_with, |for_the_room| Self {
52            state: Arc::new(RwLock::new(RoomLatestEventsState {
53                for_the_room,
54                per_thread: HashMap::new(),
55                weak_room,
56                event_cache: event_cache.clone(),
57                room_event_cache: OnceCell::new(),
58            })),
59        })
60    }
61
62    fn create_latest_event(
63        weak_room: &WeakRoom,
64        thread_id: Option<&EventId>,
65    ) -> With<LatestEvent, IsLatestEventValueNone> {
66        LatestEvent::new(weak_room, thread_id)
67    }
68
69    /// Lock this type with shared read access, and return an owned lock guard.
70    pub async fn read(&self) -> RoomLatestEventsReadGuard {
71        RoomLatestEventsReadGuard { inner: self.state.clone().read_owned().await }
72    }
73
74    /// Lock this type with exclusive write access, and return an owned lock
75    /// guard.
76    pub async fn write(&self) -> RoomLatestEventsWriteGuard {
77        RoomLatestEventsWriteGuard { inner: self.state.clone().write_owned().await }
78    }
79}
80
81/// The state of [`RoomLatestEvents`].
82#[derive(Debug)]
83struct RoomLatestEventsState {
84    /// The latest event of the room.
85    for_the_room: LatestEvent,
86
87    /// The latest events for each thread.
88    per_thread: HashMap<OwnedEventId, LatestEvent>,
89
90    /// The event cache.
91    event_cache: EventCache,
92
93    /// The room event cache (lazily-loaded).
94    room_event_cache: OnceCell<RoomEventCache>,
95
96    /// The (weak) room.
97    ///
98    /// It used to to get the power-levels of the user for this room when
99    /// computing the latest events.
100    weak_room: WeakRoom,
101}
102
103/// The owned lock guard returned by [`RoomLatestEvents::read`].
104pub(super) struct RoomLatestEventsReadGuard {
105    inner: OwnedRwLockReadGuard<RoomLatestEventsState>,
106}
107
108impl RoomLatestEventsReadGuard {
109    /// Get the [`LatestEvent`] for the room.
110    pub fn for_room(&self) -> &LatestEvent {
111        &self.inner.for_the_room
112    }
113
114    /// Get the [`LatestEvent`] for the thread if it exists.
115    pub fn for_thread(&self, thread_id: &EventId) -> Option<&LatestEvent> {
116        self.inner.per_thread.get(thread_id)
117    }
118
119    #[cfg(test)]
120    pub fn per_thread(&self) -> &HashMap<OwnedEventId, LatestEvent> {
121        &self.inner.per_thread
122    }
123}
124
125/// The owned lock guard returned by [`RoomLatestEvents::write`].
126pub(super) struct RoomLatestEventsWriteGuard {
127    inner: OwnedRwLockWriteGuard<RoomLatestEventsState>,
128}
129
130impl RoomLatestEventsWriteGuard {
131    /// Check whether this [`RoomLatestEvents`] has a latest event for a
132    /// particular thread.
133    pub fn has_thread(&self, thread_id: &EventId) -> bool {
134        self.inner.per_thread.contains_key(thread_id)
135    }
136
137    /// Create the [`LatestEvent`] for thread `thread_id` and insert it in this
138    /// [`RoomLatestEvents`].
139    pub fn create_and_insert_latest_event_for_thread(&mut self, thread_id: &EventId) {
140        let latest_event_with =
141            RoomLatestEvents::create_latest_event(&self.inner.weak_room, Some(thread_id));
142
143        self.inner.per_thread.insert(thread_id.to_owned(), With::inner(latest_event_with));
144    }
145
146    /// Forget the thread `thread_id`.
147    pub fn forget_thread(&mut self, thread_id: &EventId) {
148        self.inner.per_thread.remove(thread_id);
149    }
150
151    /// Update the latest events for the room and its threads, based on the
152    /// event cache data.
153    pub async fn update_with_event_cache(&mut self) {
154        // Get the power levels of the user for the current room if the `WeakRoom` is
155        // still valid.
156        //
157        // Get it once for all the updates of all the latest events for this room (be
158        // the room and its threads).
159        let Some(room) = self.inner.weak_room.get() else {
160            // No room? Let's stop the update.
161            error!(room = ?self.inner.weak_room, "Room is unknown");
162
163            return;
164        };
165        let own_user_id = room.own_user_id();
166        let power_levels = room.power_levels().await.ok();
167
168        let inner = &mut *self.inner;
169        let for_the_room = &mut inner.for_the_room;
170        let per_thread = &mut inner.per_thread;
171
172        // Lazy-load the `RoomEventCache`.
173        let room_event_cache = match inner
174            .room_event_cache
175            .get_or_try_init(|| async {
176                // It's fine to drop the `EventCacheDropHandles` here as the caller
177                // (`LatestEventState`) owns a clone of the `EventCache`.
178                let (room_event_cache, _drop_handles) =
179                    inner.event_cache.room(room.room_id()).await?;
180
181                Ok::<RoomEventCache, EventCacheError>(room_event_cache)
182            })
183            .await
184        {
185            Ok(room_event_cache) => room_event_cache,
186            Err(err) => {
187                error!(room_id = ?room.room_id(), ?err, "Failed to fetch the `RoomEventCache`");
188                return;
189            }
190        };
191
192        // The room is left without a latest event so back-paginate its history
193        // in the background until a suitable event surfaces.
194        if matches!(
195            for_the_room
196                .update_with_event_cache(room_event_cache, own_user_id, power_levels.as_ref())
197                .await,
198            NeedMoreEvents::Yes
199        ) {
200            Self::back_paginate_for_candidate(&room, own_user_id, power_levels.as_ref());
201        }
202
203        for latest_event in per_thread.values_mut() {
204            latest_event
205                .update_with_event_cache(room_event_cache, own_user_id, power_levels.as_ref())
206                .await;
207        }
208    }
209
210    /// Update the latest events for the room and its threads, based on the
211    /// send queue update.
212    pub async fn update_with_send_queue(&mut self, send_queue_update: &RoomSendQueueUpdate) {
213        // Get the power levels of the user for the current room if the `WeakRoom` is
214        // still valid.
215        //
216        // Get it once for all the updates of all the latest events for this room (be
217        // the room and its threads).
218        let Some(room) = self.inner.weak_room.get() else {
219            // No room? Let's stop the update.
220            return;
221        };
222        let own_user_id = room.own_user_id();
223        let power_levels = room.power_levels().await.ok();
224
225        let inner = &mut *self.inner;
226        let for_the_room = &mut inner.for_the_room;
227        let per_thread = &mut inner.per_thread;
228
229        // Lazy-load the `RoomEventCache`.
230        let room_event_cache = match inner
231            .room_event_cache
232            .get_or_try_init(|| async {
233                // It's fine to drop the `EventCacheDropHandles` here as the caller
234                // (`LatestEventState`) owns a clone of the `EventCache`.
235                let (room_event_cache, _drop_handles) =
236                    inner.event_cache.room(room.room_id()).await?;
237
238                Ok::<RoomEventCache, EventCacheError>(room_event_cache)
239            })
240            .await
241        {
242            Ok(room_event_cache) => room_event_cache,
243            Err(err) => {
244                error!(room_id = ?room.room_id(), ?err, "Failed to fetch the `RoomEventCache`");
245                return;
246            }
247        };
248
249        for_the_room
250            .update_with_send_queue(
251                send_queue_update,
252                room_event_cache,
253                own_user_id,
254                power_levels.as_ref(),
255            )
256            .await;
257
258        for latest_event in per_thread.values_mut() {
259            latest_event
260                .update_with_send_queue(
261                    send_queue_update,
262                    room_event_cache,
263                    own_user_id,
264                    power_levels.as_ref(),
265                )
266                .await;
267        }
268    }
269
270    /// Update the latest events for the room and its threads, based on the room
271    /// info.
272    pub async fn update_with_room_info(&mut self, reasons: RoomInfoNotableUpdateReasons) {
273        // Get the state of the current room if the `WeakRoom` is still valid.
274        let Some(room) = self.inner.weak_room.get() else {
275            // No room? Let's stop the update.
276            return;
277        };
278
279        self.inner.for_the_room.update_with_room_info(room, reasons).await;
280    }
281
282    /// Back-paginate the room until a suitable latest-event candidate is loaded
283    /// into memory or the start of the timeline is reached.
284    ///
285    /// Enqueues a high-priority request on the shared [`BackPaginationQueue`],
286    /// with a stop predicate that fires as soon as a freshly loaded batch
287    /// contains a suitable latest-event candidate. No-ops if automatic
288    /// backpagination is disabled.
289    ///
290    /// Fire-and-forget as the events it loads emit an event cache update which
291    /// will trigger a recomputation.
292    ///
293    /// [`BackPaginationQueue`]: crate::event_cache::BackPaginationQueue
294    #[instrument(skip_all, fields(room_id = %room.room_id()))]
295    fn back_paginate_for_candidate(
296        room: &Room,
297        own_user_id: &UserId,
298        power_levels: Option<&RoomPowerLevels>,
299    ) {
300        let Some(queue) = room.client().event_cache().back_pagination_queue() else {
301            return;
302        };
303
304        let own_user_id = own_user_id.to_owned();
305        let power_levels = power_levels.cloned();
306
307        // This filters each batch to spot a candidate and `Builder::new_remote`
308        // filters the same events again when it computes the value afterwards. That
309        // second pass can't be skipped though as an event's edits are newer than it
310        // and a stop condition only ever sees the batch it just loaded.
311        let stop = move |outcome: &BackPaginationOutcome| {
312            let found = outcome.events.iter().any(|event| {
313                filter_timeline_event(event, None, &own_user_id, power_levels.as_ref()).is_break()
314            });
315
316            if found { ControlFlow::Break(()) } else { ControlFlow::Continue(()) }
317        };
318
319        debug!("started backfill request for latest events");
320
321        match queue.enqueue(BackPaginationRequest {
322            room_id: room.room_id().to_owned(),
323            priority: back_pagination_queue::Priority::High,
324            stop: Box::new(stop),
325            batch_size: back_pagination_queue::BATCH_SIZE,
326            max_batches: None,
327        }) {
328            // Nobody awaits the result, so detach the handle to let the request run to
329            // completion instead of cancelling it on drop.
330            Ok(handle) => handle.detach(),
331            Err(err) => warn!("couldn't enqueue a latest-event backfill request: {err}"),
332        }
333    }
334}
335
336#[cfg(all(test, not(target_family = "wasm")))]
337mod tests {
338    use assert_matches::assert_matches;
339    use matrix_sdk_base::{
340        RoomState,
341        event_cache::Gap,
342        linked_chunk::{ChunkIdentifier, LinkedChunkId, Update},
343    };
344    use matrix_sdk_test::{async_test, event_factory::EventFactory};
345    use ruma::{event_id, room_id, user_id};
346
347    use super::RoomLatestEvents;
348    use crate::{
349        assert_let_timeout,
350        client::WeakClient,
351        latest_events::LatestEventValue,
352        room::WeakRoom,
353        test_utils::mocks::{MatrixMockServer, RoomMessagesResponseTemplate},
354    };
355
356    /// When no latest-event candidate is present in memory, but one exists
357    /// behind a gap, `update_with_event_cache` back-paginates to surface it.
358    #[async_test]
359    async fn test_update_with_event_cache_backfills_for_a_candidate() {
360        let room_id = room_id!("!r0");
361        let sender = user_id!("@bob:example.org");
362
363        let server = MatrixMockServer::new().await;
364        let client = server
365            .client_builder()
366            .on_builder(|builder| builder.with_enable_automatic_back_pagination(true))
367            .build()
368            .await;
369
370        client.base_client().get_or_create_room(room_id, RoomState::Joined);
371
372        // A linked chunk with a single gap: no events in memory (so no candidate),
373        // but a token to paginate from. Set up directly so no sync (and thus no
374        // competing read-receipt pagination) races the backfill.
375        client
376            .event_cache_store()
377            .lock()
378            .await
379            .unwrap()
380            .as_clean()
381            .unwrap()
382            .handle_linked_chunk_updates(
383                LinkedChunkId::Room(room_id),
384                vec![Update::NewGapChunk {
385                    previous: None,
386                    new: ChunkIdentifier::new(0),
387                    next: None,
388                    gap: Gap { token: "prev_batch".to_owned() },
389                }],
390            )
391            .await
392            .unwrap();
393
394        let event_cache = client.event_cache();
395        event_cache.subscribe().unwrap();
396
397        // A displayable message lives behind the gap.
398        let f = EventFactory::new().room(room_id).sender(sender);
399        server
400            .mock_room_messages()
401            .match_from("prev_batch")
402            .ok(RoomMessagesResponseTemplate::default()
403                .events(vec![f.text_msg("hello").event_id(event_id!("$1"))]))
404            .mock_once()
405            .mount()
406            .await;
407
408        let weak_room = WeakRoom::new(WeakClient::from_client(&client), room_id.to_owned());
409        let room_latest_events = RoomLatestEvents::new(weak_room, event_cache);
410
411        // No candidate in memory yet.
412        assert_matches!(
413            room_latest_events.read().await.for_room().get().await,
414            LatestEventValue::None
415        );
416
417        let mut updates = event_cache.subscribe_to_room_generic_updates();
418
419        room_latest_events.write().await.update_with_event_cache().await;
420
421        // The backfill runs in the background; wait for the events it loads, then
422        // recompute as the update it emits would.
423        assert_let_timeout!(Ok(_) = updates.recv());
424
425        room_latest_events.write().await.update_with_event_cache().await;
426
427        // The backfill surfaced the message; the latest event resolved to it.
428        assert_matches!(
429            room_latest_events.read().await.for_room().get().await,
430            LatestEventValue::Remote(_)
431        );
432    }
433}