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 eyeball_im::VectorDiff;
29use futures_core::{Stream, ready};
30use matrix_sdk_base::{
31    event_cache::{Event, Gap},
32    linked_chunk::{ChunkContent, LinkedChunkId, Update},
33};
34use pin_project_lite::pin_project;
35use ruma::api::Direction;
36use tracing::{error, trace};
37
38pub use super::super::pagination::PaginationStatus;
39use super::{
40    super::{
41        super::{
42            EventCacheError, EventsOrigin, Result, RoomEventCacheGenericUpdate,
43            deduplicator::{DeduplicationOutcome, filter_duplicate_events},
44        },
45        TimelineVectorDiffs,
46        pagination::{
47            BackPaginationOutcome, LoadMoreEventsBackwardsOutcome, PaginatedCache, Pagination,
48        },
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 type
58    /// 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    /// - either we've reached the start of the timeline,
127    /// - or we've obtained enough events to fulfill the requested number of
128    ///   events.
129    pub async fn run_backwards_until(
130        &self,
131        num_requested_events: u16,
132    ) -> Result<BackPaginationOutcome> {
133        self.0.run_backwards_until(num_requested_events).await
134    }
135
136    /// Run a single back-pagination for the requested number of events.
137    ///
138    /// This automatically takes care of waiting for a pagination token from
139    /// sync, if we haven't done that before.
140    pub async fn run_backwards_once(&self, batch_size: u16) -> Result<BackPaginationOutcome> {
141        self.0.run_backwards_once(batch_size).await
142    }
143
144    /// Returns a subscriber to the pagination status.
145    pub fn status(&self) -> PaginationStatusSubscriber {
146        PaginationStatusSubscriber { subscriber: self.0.cache.status().subscribe() }
147    }
148
149    #[cfg(test)]
150    pub(super) async fn load_more_events_backwards(
151        &self,
152    ) -> Result<LoadMoreEventsBackwardsOutcome> {
153        self.0.cache.load_more_events_backwards().await
154    }
155}
156
157impl PaginatedCache for Arc<RoomEventCacheInner> {
158    fn status(&self) -> &SharedObservable<SharedPaginationStatus> {
159        &self.shared_pagination_status
160    }
161
162    async fn load_more_events_backwards(&self) -> Result<LoadMoreEventsBackwardsOutcome> {
163        let mut state = self.state.write().await?;
164
165        // If any in-memory chunk is a gap, don't load more events, and let the caller
166        // resolve the gap.
167        if let Some(prev_token) = state.room_linked_chunk().rgap().map(|gap| gap.token) {
168            return Ok(LoadMoreEventsBackwardsOutcome::Gap {
169                prev_token: Some(prev_token),
170                waited_for_initial_prev_token: state.waited_for_initial_prev_token(),
171            });
172        }
173
174        let prev_first_chunk = state.room_linked_chunk().first_chunk();
175
176        // The first chunk is not a gap, we can load its previous chunk.
177        let linked_chunk_id = LinkedChunkId::Room(&state.state.room_id);
178        let new_first_chunk = match state
179            .store
180            .load_previous_chunk(linked_chunk_id, prev_first_chunk.identifier())
181            .await
182        {
183            Ok(Some(new_first_chunk)) => {
184                // All good, let's continue with this chunk.
185                new_first_chunk
186            }
187
188            Ok(None) => {
189                // If we never received events for this room, this means we've never received a
190                // sync for that room, because every room must have *at least* a room creation
191                // event. Otherwise, we have reached the start of the timeline.
192
193                if state.room_linked_chunk().events().next().is_some() {
194                    // If there's at least one event, this means we've reached the start of the
195                    // timeline, since the chunk is fully loaded.
196                    trace!("chunk is fully loaded and non-empty: reached_start=true");
197                    return Ok(LoadMoreEventsBackwardsOutcome::StartOfTimeline);
198                }
199
200                // Otherwise, start back-pagination from the end of the room.
201                return Ok(LoadMoreEventsBackwardsOutcome::Gap {
202                    prev_token: None,
203                    waited_for_initial_prev_token: state.waited_for_initial_prev_token(),
204                });
205            }
206
207            Err(err) => {
208                error!("error when loading the previous chunk of a linked chunk: {err}");
209
210                // Clear storage for this room.
211                state
212                    .store
213                    .handle_linked_chunk_updates(linked_chunk_id, vec![Update::Clear])
214                    .await?;
215
216                // Return the error.
217                return Err(err.into());
218            }
219        };
220
221        let chunk_content = new_first_chunk.content.clone();
222
223        // We've reached the start on disk, if and only if, there was no chunk prior to
224        // the one we just loaded.
225        //
226        // This value is correct, if and only if, it is used for a chunk content of kind
227        // `Items`.
228        let reached_start = new_first_chunk.previous.is_none();
229
230        if let Err(err) = state.room_linked_chunk_mut().insert_new_chunk_as_first(new_first_chunk) {
231            error!("error when inserting the previous chunk into its linked chunk: {err}");
232
233            // Clear storage for this room.
234            state
235                .store
236                .handle_linked_chunk_updates(
237                    LinkedChunkId::Room(&state.state.room_id),
238                    vec![Update::Clear],
239                )
240                .await?;
241
242            // Return the error.
243            return Err(err.into());
244        }
245
246        // ⚠️ Let's not propagate the updates to the store! We already have these data
247        // in the store! Let's drain them.
248        let _ = state.room_linked_chunk_mut().store_updates().take();
249
250        // However, we want to get updates as `VectorDiff`s.
251        let timeline_event_diffs = state.room_linked_chunk_mut().updates_as_vector_diffs();
252
253        Ok(match chunk_content {
254            ChunkContent::Gap(gap) => {
255                trace!("reloaded chunk from disk (gap)");
256
257                LoadMoreEventsBackwardsOutcome::Gap {
258                    prev_token: Some(gap.token),
259                    waited_for_initial_prev_token: state.waited_for_initial_prev_token(),
260                }
261            }
262
263            ChunkContent::Items(events) => {
264                trace!(?reached_start, "reloaded chunk from disk ({} items)", events.len());
265
266                LoadMoreEventsBackwardsOutcome::Events {
267                    events,
268                    timeline_event_diffs,
269                    reached_start,
270                }
271            }
272        })
273    }
274
275    async fn mark_has_waited_for_initial_prev_token(&self) -> Result<()> {
276        *self.state.write().await?.waited_for_initial_prev_token_mut() = true;
277
278        Ok(())
279    }
280
281    async fn wait_for_prev_token(&self) {
282        self.pagination_batch_token_notifier.notified().await
283    }
284
285    async fn paginate_backwards_with_network(
286        &self,
287        batch_size: u16,
288        prev_token: &Option<String>,
289    ) -> Result<Option<(Vec<Event>, Option<String>)>> {
290        let Some(room) = self.weak_room.get() else {
291            // The client is shutting down.
292            return Ok(None);
293        };
294
295        let mut options = MessagesOptions::new(Direction::Backward).from(prev_token.as_deref());
296        options.limit = batch_size.into();
297
298        let response = room
299            .messages(options)
300            .await
301            .map_err(|err| EventCacheError::PaginationError(Arc::new(err)))?;
302
303        Ok(Some((response.chunk, response.end)))
304    }
305
306    async fn conclude_backwards_pagination_from_disk(
307        &self,
308        events: Vec<Event>,
309        timeline_event_diffs: Vec<VectorDiff<Event>>,
310        reached_start: bool,
311    ) -> BackPaginationOutcome {
312        if !timeline_event_diffs.is_empty() {
313            self.update_sender.send(
314                RoomEventCacheUpdate::UpdateTimelineEvents(TimelineVectorDiffs {
315                    diffs: timeline_event_diffs,
316                    origin: EventsOrigin::Cache,
317                }),
318                Some(RoomEventCacheGenericUpdate { room_id: self.room_id.clone() }),
319            );
320        }
321
322        BackPaginationOutcome {
323            reached_start,
324            // This is a backwards pagination. `BackPaginationOutcome` expects events to
325            // be in “reverse order”.
326            events: events.into_iter().rev().collect(),
327        }
328    }
329
330    async fn conclude_backwards_pagination_from_network(
331        &self,
332        events: Vec<Event>,
333        prev_token: Option<String>,
334        mut new_token: Option<String>,
335    ) -> Result<Option<BackPaginationOutcome>> {
336        let mut state = self.state.write().await?;
337
338        // Check that the previous token still exists; otherwise it's a sign that the
339        // room's timeline has been cleared.
340        let prev_gap_id = if let Some(token) = prev_token {
341            // Find the corresponding gap in the in-memory linked chunk.
342            let gap_chunk_id = state.room_linked_chunk().chunk_identifier(|chunk| {
343                    matches!(chunk.content(), ChunkContent::Gap(Gap { token: prev_token }) if *prev_token == token)
344                });
345
346            if gap_chunk_id.is_none() {
347                // We got a previous-batch token from the linked chunk *before* running the
348                // request, but it is missing *after* completing the request.
349                //
350                // It may be a sign the linked chunk has been reset, but it's fine!
351                return Ok(None);
352            }
353
354            gap_chunk_id
355        } else {
356            None
357        };
358
359        let DeduplicationOutcome {
360            all_events: mut events,
361            in_memory_duplicated_event_ids,
362            in_store_duplicated_event_ids,
363            non_empty_all_duplicates: all_duplicates,
364        } = filter_duplicate_events(
365            &state.state.own_user_id,
366            &state.store,
367            LinkedChunkId::Room(&state.state.room_id),
368            state.room_linked_chunk(),
369            events,
370        )
371        .await?;
372
373        // If not all the events have been back-paginated, we need to remove the
374        // previous ones, otherwise we can end up with misordered events.
375        //
376        // Consider the following scenario:
377        // - sync returns [D, E, F]
378        // - then sync returns [] with a previous batch token PB1, so the internal
379        //   linked chunk state is [D, E, F, PB1].
380        // - back-paginating with PB1 may return [A, B, C, D, E, F].
381        //
382        // Only inserting the new events when replacing PB1 would result in a timeline
383        // ordering of [D, E, F, A, B, C], which is incorrect. So we do have to remove
384        // all the events, in case this happens (see also #4746).
385
386        if !all_duplicates {
387            // Let's forget all the previous events.
388            state
389                .remove_events(in_memory_duplicated_event_ids, in_store_duplicated_event_ids)
390                .await?;
391        } else {
392            // All new events are duplicated, they can all be ignored.
393            events.clear();
394            // The gap can be ditched too, as it won't be useful to backpaginate any
395            // further.
396            new_token = None;
397        }
398
399        // `/messages` has been called with `dir=b` (backwards), so the events are in
400        // the inverted order; reorder them.
401        let topo_ordered_events = events.iter().rev().cloned().collect::<Vec<_>>();
402
403        let new_gap = new_token.as_ref().map(|prev_token| Gap { token: prev_token.clone() });
404        let reached_start = state.room_linked_chunk_mut().push_backwards_pagination_events(
405            prev_gap_id,
406            new_gap,
407            &topo_ordered_events,
408        );
409
410        // A back-pagination can't include new read receipt events, as those are
411        // ephemeral events not included in /messages responses, so we can
412        // safely set the receipt event to None here.
413        //
414        // Note: read receipts may be updated anyhow in the post-processing step, as the
415        // back-pagination may have revealed the event pointed to by the latest read
416        // receipt.
417        let receipt_event = None;
418
419        // Note: this flushes updates to the store.
420        state.post_process_new_events(topo_ordered_events, receipt_event).await?;
421
422        let timeline_event_diffs = state.room_linked_chunk_mut().updates_as_vector_diffs();
423
424        if !timeline_event_diffs.is_empty() {
425            self.update_sender.send(
426                RoomEventCacheUpdate::UpdateTimelineEvents(TimelineVectorDiffs {
427                    diffs: timeline_event_diffs,
428                    origin: EventsOrigin::Pagination,
429                }),
430                Some(RoomEventCacheGenericUpdate { room_id: self.room_id.clone() }),
431            );
432        }
433
434        Ok(Some(BackPaginationOutcome { events, reached_start }))
435    }
436}
437
438impl fmt::Debug for RoomPagination {
439    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
440        formatter.debug_tuple("RoomPagination").finish_non_exhaustive()
441    }
442}