Skip to main content

matrix_sdk/event_cache/caches/room/
pagination.rs

1// Copyright 2024 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//! The [`RoomPagination`] type makes it possible to paginate a
16//! [`RoomEventCache`].
17//!
18//! [`RoomEventCache`]: super::super::super::RoomEventCache
19
20use std::{
21    fmt,
22    pin::Pin,
23    sync::Arc,
24    task::{Context, Poll},
25};
26
27use eyeball::{SharedObservable, Subscriber};
28use futures_core::{Stream, ready};
29use matrix_sdk_base::{
30    event_cache::{Event, Gap},
31    linked_chunk::{ChunkContent, LinkedChunkId, Update},
32};
33use pin_project_lite::pin_project;
34use ruma::api::Direction;
35use tracing::{error, trace};
36
37pub use super::super::pagination::PaginationStatus;
38use super::{
39    super::{
40        super::{
41            EventCacheError, EventsOrigin, Result, RoomEventCacheGenericUpdate,
42            deduplicator::{DeduplicationOutcome, filter_duplicate_events},
43        },
44        TimelineVectorDiffs,
45        pagination::{
46            BackPaginationOutcome, LoadMoreEventsBackwardsOutcome, PaginatedCache, Pagination,
47        },
48        read_receipts::{contains_a_receipt_target, unresolved_receipt_targets},
49    },
50    RoomEventCacheInner, RoomEventCacheUpdate,
51};
52use crate::{event_cache::caches::pagination::SharedPaginationStatus, room::MessagesOptions};
53
54pin_project! {
55    /// A subscriber to a [`PaginationStatus`].
56    ///
57    /// This is a manual implementation of a map function on top of an internal
58    /// type representing a [`PaginationStatus`].
59    pub struct PaginationStatusSubscriber {
60        #[pin]
61        subscriber: Subscriber<SharedPaginationStatus>,
62    }
63}
64
65#[cfg(not(tarpaulin_include))]
66impl fmt::Debug for PaginationStatusSubscriber {
67    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
68        f.debug_struct("PaginationStatusSubscriber").finish_non_exhaustive()
69    }
70}
71
72impl PaginationStatusSubscriber {
73    fn map(from: SharedPaginationStatus) -> PaginationStatus {
74        match from {
75            SharedPaginationStatus::Idle { hit_timeline_start } => {
76                PaginationStatus::Idle { hit_timeline_start }
77            }
78            SharedPaginationStatus::Paginating { .. } => PaginationStatus::Paginating,
79        }
80    }
81
82    pub fn get(&self) -> PaginationStatus {
83        Self::map(self.subscriber.get())
84    }
85
86    pub async fn next(&mut self) -> Option<PaginationStatus> {
87        self.subscriber.next().await.map(Self::map)
88    }
89
90    pub fn next_now(&mut self) -> PaginationStatus {
91        Self::map(self.subscriber.next_now())
92    }
93}
94
95impl Stream for PaginationStatusSubscriber {
96    type Item = PaginationStatus;
97
98    fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
99        Poll::Ready(ready!(self.project().subscriber.as_mut().poll_next(cx)).map(Self::map))
100    }
101}
102
103/// An API object to run pagination queries on a [`RoomEventCache`].
104///
105/// Can be created with [`RoomEventCache::pagination()`].
106///
107/// [`RoomEventCache`]: super::super::super::RoomEventCache
108/// [`RoomEventCache::pagination()`]: super::super::super::RoomEventCache::pagination
109#[allow(missing_debug_implementations)]
110#[derive(Clone)]
111pub struct RoomPagination(Pagination<Arc<RoomEventCacheInner>>);
112
113impl RoomPagination {
114    /// Construct a new [`RoomPagination`].
115    pub(super) fn new(cache: Arc<RoomEventCacheInner>) -> Self {
116        Self(Pagination::new(cache))
117    }
118
119    /// Starts a back-pagination for the requested number of events.
120    ///
121    /// This automatically takes care of waiting for a pagination token from
122    /// sync, if we haven't done that before.
123    ///
124    /// It will run multiple back-paginations until one of these two conditions
125    /// is met:
126    ///
127    /// - either we've reached the start of the timeline,
128    /// - or we've obtained enough events to fulfill the requested number of
129    ///   events.
130    pub async fn run_backwards_until(
131        &self,
132        num_requested_events: u16,
133    ) -> Result<BackPaginationOutcome> {
134        self.0.run_backwards_until(num_requested_events).await
135    }
136
137    /// Run a single back-pagination for the requested number of events.
138    ///
139    /// This automatically takes care of waiting for a pagination token from
140    /// sync, if we haven't done that before.
141    pub async fn run_backwards_once(&self, batch_size: u16) -> Result<BackPaginationOutcome> {
142        self.0.run_backwards_once(batch_size).await
143    }
144
145    /// Returns a subscriber to the pagination status.
146    pub fn status(&self) -> PaginationStatusSubscriber {
147        PaginationStatusSubscriber { subscriber: self.0.cache.status().subscribe() }
148    }
149
150    #[cfg(test)]
151    pub(super) async fn load_more_events_backwards(
152        &self,
153    ) -> Result<LoadMoreEventsBackwardsOutcome> {
154        self.0.cache.load_more_events_backwards().await
155    }
156}
157
158impl PaginatedCache for Arc<RoomEventCacheInner> {
159    fn status(&self) -> &SharedObservable<SharedPaginationStatus> {
160        &self.shared_pagination_status
161    }
162
163    async fn load_more_events_backwards(&self) -> Result<LoadMoreEventsBackwardsOutcome> {
164        let mut state = self.state.write().await?;
165
166        // If any in-memory chunk is a gap, don't load more events, and let the
167        // caller resolve the gap.
168        if let Some(prev_token) = state.room_linked_chunk().rgap().map(|gap| gap.token) {
169            return Ok(LoadMoreEventsBackwardsOutcome::Gap {
170                prev_token: Some(prev_token),
171                waited_for_initial_prev_token: state.waited_for_initial_prev_token(),
172            });
173        }
174
175        let prev_first_chunk = state.room_linked_chunk().first_chunk();
176
177        // The first chunk is not a gap, we can load its previous chunk.
178        let linked_chunk_id = LinkedChunkId::Room(&state.state.room_id);
179        let new_first_chunk = match state
180            .store
181            .load_previous_chunk(linked_chunk_id, prev_first_chunk.identifier())
182            .await
183        {
184            Ok(Some(new_first_chunk)) => {
185                // All good, let's continue with this chunk.
186                new_first_chunk
187            }
188
189            Ok(None) => {
190                // If we never received events for this room, this means we've
191                // never received a sync for that room, because every room must
192                // have _at least_ a room creation event. Otherwise, we have
193                // reached the start of the timeline.
194
195                if state.room_linked_chunk().events().next().is_some() {
196                    // If there's at least one event, this means we've reached
197                    // the start of the timeline, since the chunk is fully
198                    // loaded.
199                    trace!("chunk is fully loaded and non-empty: reached_start=true");
200                    return Ok(LoadMoreEventsBackwardsOutcome::StartOfTimeline);
201                }
202
203                // Otherwise, start back-pagination from the end of the room.
204                return Ok(LoadMoreEventsBackwardsOutcome::Gap {
205                    prev_token: None,
206                    waited_for_initial_prev_token: state.waited_for_initial_prev_token(),
207                });
208            }
209
210            Err(err) => {
211                error!("error when loading the previous chunk of a linked chunk: {err}");
212
213                // Clear storage for this room.
214                state
215                    .store
216                    .handle_linked_chunk_updates(linked_chunk_id, vec![Update::Clear])
217                    .await?;
218
219                // Return the error.
220                return Err(err.into());
221            }
222        };
223
224        let chunk_content = new_first_chunk.content.clone();
225
226        // We've reached the start on disk, if and only if, there was no chunk
227        // prior to the one we just loaded.
228        //
229        // This value is correct, if and only if, it is used for a chunk content
230        // of kind `Items`.
231        let reached_start = new_first_chunk.previous.is_none();
232
233        if let Err(err) = state.room_linked_chunk_mut().insert_new_chunk_as_first(new_first_chunk) {
234            error!("error when inserting the previous chunk into its linked chunk: {err}");
235
236            // Clear storage for this room.
237            state
238                .store
239                .handle_linked_chunk_updates(
240                    LinkedChunkId::Room(&state.state.room_id),
241                    vec![Update::Clear],
242                )
243                .await?;
244
245            // Return the error.
246            return Err(err.into());
247        }
248
249        // ⚠️ Let's not propagate the updates to the store! We already have
250        // these data in the store! Let's drain them.
251        let _ = state.room_linked_chunk_mut().store_updates().take();
252
253        // However, we want to get updates as `VectorDiff`s.
254        let timeline_event_diffs = state.room_linked_chunk_mut().updates_as_vector_diffs();
255
256        Ok(match chunk_content {
257            ChunkContent::Gap(gap) => {
258                trace!("reloaded chunk from disk (gap)");
259
260                LoadMoreEventsBackwardsOutcome::Gap {
261                    prev_token: Some(gap.token),
262                    waited_for_initial_prev_token: state.waited_for_initial_prev_token(),
263                }
264            }
265
266            ChunkContent::Items(events) => {
267                trace!(?reached_start, "reloaded chunk from disk ({} items)", events.len());
268
269                // This chunk may reveal the event a read receipt points to,
270                // which the unread counts couldn't find last time. Only the
271                // counts need recomputing, and it must happen before the
272                // timeline update is sent, or observers see the diff with a
273                // stale count.
274                let reveals_a_receipt_target = self.weak_room.get().is_some_and(|room| {
275                    let read_receipts = room.read_receipts();
276
277                    contains_a_receipt_target(&events, &unresolved_receipt_targets(&read_receipts))
278                });
279
280                if reveals_a_receipt_target {
281                    let result = state.update_read_receipts(None).await;
282
283                    if let Err(err) = result {
284                        error!(
285                            ?err,
286                            "error when recomputing the read receipts after loading a chunk from disk"
287                        );
288                    }
289                }
290
291                // Send the diffs while the state lock is still held: the
292                // indices they carry are only valid if no other change to the
293                // linked chunk is sent before them.
294                if !timeline_event_diffs.is_empty() {
295                    state.update_sender.send(
296                        RoomEventCacheUpdate::UpdateTimelineEvents(TimelineVectorDiffs {
297                            diffs: timeline_event_diffs,
298                            origin: EventsOrigin::Cache,
299                        }),
300                        Some(RoomEventCacheGenericUpdate { room_id: self.room_id.clone() }),
301                    );
302                }
303
304                LoadMoreEventsBackwardsOutcome::Events { events, reached_start }
305            }
306        })
307    }
308
309    async fn mark_has_waited_for_initial_prev_token(&self) -> Result<()> {
310        *self.state.write().await?.waited_for_initial_prev_token_mut() = true;
311
312        Ok(())
313    }
314
315    async fn wait_for_prev_token(&self) {
316        self.pagination_batch_token_notifier.notified().await
317    }
318
319    async fn paginate_backwards_with_network(
320        &self,
321        batch_size: u16,
322        prev_token: &Option<String>,
323    ) -> Result<Option<(Vec<Event>, Option<String>)>> {
324        let Some(room) = self.weak_room.get() else {
325            // The client is shutting down.
326            return Ok(None);
327        };
328
329        let mut options = MessagesOptions::new(Direction::Backward).from(prev_token.as_deref());
330        options.limit = batch_size.into();
331
332        let response = room
333            .messages(options)
334            .await
335            .map_err(|err| EventCacheError::PaginationError(Arc::new(err)))?;
336
337        Ok(Some((response.chunk, response.end)))
338    }
339
340    async fn conclude_backwards_pagination_from_network(
341        &self,
342        events: Vec<Event>,
343        prev_token: Option<String>,
344        mut new_token: Option<String>,
345    ) -> Result<Option<BackPaginationOutcome>> {
346        let mut state = self.state.write().await?;
347
348        // Check that the previous token still exists; otherwise it's a sign
349        // that the room's timeline has been cleared.
350        let prev_gap_id = if let Some(token) = prev_token {
351            // Find the corresponding gap in the in-memory linked chunk.
352            let gap_chunk_id = state.room_linked_chunk().chunk_identifier(|chunk| {
353                    matches!(chunk.content(), ChunkContent::Gap(Gap { token: prev_token }) if *prev_token == token)
354                });
355
356            if gap_chunk_id.is_none() {
357                // We got a previous-batch token from the linked chunk _before_
358                // running the request, but it is missing _after_ completing the
359                // request.
360                //
361                // It may be a sign the linked chunk has been reset, but it's
362                // fine!
363                return Ok(None);
364            }
365
366            gap_chunk_id
367        } else {
368            None
369        };
370
371        let DeduplicationOutcome {
372            all_events: mut events,
373            in_memory_duplicated_event_ids,
374            in_store_duplicated_event_ids,
375            non_empty_all_duplicates: all_duplicates,
376        } = filter_duplicate_events(
377            &state.state.own_user_id,
378            &state.store,
379            LinkedChunkId::Room(&state.state.room_id),
380            state.room_linked_chunk(),
381            events,
382        )
383        .await?;
384
385        // If not all the events have been back-paginated, we need to remove the
386        // previous ones, otherwise we can end up with misordered events.
387        //
388        // Consider the following scenario:
389        //
390        // - sync returns [D, E, F]
391        // - then sync returns [] with a previous batch token PB1, so the
392        //   internal linked chunk state is [D, E, F, PB1].
393        // - back-paginating with PB1 may return [A, B, C, D, E, F].
394        //
395        // Only inserting the new events when replacing PB1 would result in a
396        // timeline ordering of [D, E, F, A, B, C], which is incorrect. So we do
397        // have to remove all the events, in case this happens (see also #4746).
398
399        if !all_duplicates {
400            // Let's forget all the previous events.
401            state
402                .remove_events(in_memory_duplicated_event_ids, in_store_duplicated_event_ids)
403                .await?;
404        } else {
405            // All new events are duplicated, they can all be ignored.
406            events.clear();
407            // The gap can be ditched too, as it won't be useful to backpaginate
408            // any further.
409            new_token = None;
410        }
411
412        // `/messages` has been called with `dir=b` (backwards), so the events
413        // are in the inverted order; reorder them.
414        let topo_ordered_events = events.iter().rev().cloned().collect::<Vec<_>>();
415
416        let new_gap = new_token.as_ref().map(|prev_token| Gap { token: prev_token.clone() });
417        let reached_start = state.room_linked_chunk_mut().push_backwards_pagination_events(
418            prev_gap_id,
419            new_gap,
420            &topo_ordered_events,
421        );
422
423        // Update the store.
424        state.propagate_changes().await?;
425
426        // A back-pagination can't include new read receipt events, as those are
427        // ephemeral events not included in /messages responses, so we can
428        // safely set the receipt event to None here.
429        //
430        // Note: read receipts may be updated anyhow in the post-processing
431        // step, as the back-pagination may have revealed the event pointed to
432        // by the latest read receipt.
433        let receipt_event = None;
434
435        // Post-process newly inserted events.
436        state.post_process_upserted_events(topo_ordered_events.iter(), receipt_event).await?;
437
438        let timeline_event_diffs = state.room_linked_chunk_mut().updates_as_vector_diffs();
439
440        if !timeline_event_diffs.is_empty() {
441            self.update_sender.send(
442                RoomEventCacheUpdate::UpdateTimelineEvents(TimelineVectorDiffs {
443                    diffs: timeline_event_diffs,
444                    origin: EventsOrigin::Pagination,
445                }),
446                Some(RoomEventCacheGenericUpdate { room_id: self.room_id.clone() }),
447            );
448        }
449
450        Ok(Some(BackPaginationOutcome { events, reached_start }))
451    }
452}
453
454impl fmt::Debug for RoomPagination {
455    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
456        formatter.debug_tuple("RoomPagination").finish_non_exhaustive()
457    }
458}