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