Skip to main content

matrix_sdk/event_cache/caches/
pagination.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//! The logic to paginate a cache (room, thread…) over the disk or the network.
16
17use std::{pin::Pin, sync::Arc, time::Duration};
18
19use eyeball::{ObservableWriteGuard, SharedObservable};
20use eyeball_im::VectorDiff;
21use futures_util::{
22    FutureExt as _,
23    future::{Either, Shared, ready},
24};
25use matrix_sdk_base::{
26    SendOutsideWasm, SyncOutsideWasm, event_cache::Event, executor::AbortOnDrop, timeout::timeout,
27};
28use matrix_sdk_common::executor::spawn;
29use tracing::{debug, instrument, trace, warn};
30
31use super::super::Result;
32
33/// Type to run paginations.
34#[derive(Clone, Debug)]
35pub(in super::super) struct Pagination<C: SendOutsideWasm + 'static> {
36    pub cache: C,
37}
38
39impl<C: SendOutsideWasm + 'static> Pagination<C> {
40    /// Create a new [`Pagination`].
41    pub fn new(cache: C) -> Self {
42        Self { cache }
43    }
44}
45
46impl<C> Pagination<C>
47where
48    C: Clone + PaginatedCache + SendOutsideWasm + 'static + SyncOutsideWasm,
49{
50    /// Starts a back-pagination for the requested number of events.
51    ///
52    /// This automatically takes care of waiting for a pagination token from
53    /// sync, if we haven't done that before.
54    ///
55    /// It will run multiple back-paginations until one of these two conditions
56    /// is met:
57    ///
58    /// - either we've reached the start of the timeline,
59    /// - or we've obtained enough events to fulfill the requested number of
60    ///   events.
61    #[instrument(skip(self))]
62    pub async fn run_backwards_until(
63        &self,
64        num_requested_events: u16,
65    ) -> Result<BackPaginationOutcome> {
66        let mut events = Vec::new();
67
68        loop {
69            if let Some(outcome) = self.run_backwards_impl(num_requested_events).await? {
70                events.extend(outcome.events);
71
72                if outcome.reached_start || events.len() >= num_requested_events as usize {
73                    return Ok(BackPaginationOutcome {
74                        reached_start: outcome.reached_start,
75                        events,
76                    });
77                }
78
79                trace!(
80                    "restarting back-pagination, because we haven't reached \
81                     the start or obtained enough events yet"
82                );
83            }
84
85            debug!("restarting back-pagination because of a timeline reset.");
86        }
87    }
88
89    /// Run a single back-pagination for the requested number of events.
90    ///
91    /// This automatically takes care of waiting for a pagination token from
92    /// sync, if we haven't done that before.
93    #[instrument(skip(self))]
94    pub async fn run_backwards_once(&self, batch_size: u16) -> Result<BackPaginationOutcome> {
95        loop {
96            if let Some(outcome) = self.run_backwards_impl(batch_size).await? {
97                return Ok(outcome);
98            }
99
100            debug!("restarting back-pagination because of a timeline reset");
101        }
102    }
103
104    /// Paginate from either the storage or the network, and let pagination
105    /// status observers know about updates.
106    ///
107    /// Returns `Ok(None)` if the pagination token used during a network
108    /// pagination has disappeared from the in-memory linked chunk after
109    /// handling the response.
110    // Implementation note: return a future instead of making the function
111    // async, so as to not cause issues because the `cache` field is borrowed
112    // across await points.
113    fn run_backwards_impl(
114        &self,
115        batch_size: u16,
116    ) -> impl Future<Output = Result<Option<BackPaginationOutcome>>> {
117        // There is at least one gap that must be resolved; reach the network.
118        // First, ensure there's no other ongoing back-pagination.
119        let status_observable = self.cache.status();
120
121        let mut status_guard = status_observable.write();
122
123        match &*status_guard {
124            SharedPaginationStatus::Idle { hit_timeline_start } => {
125                if *hit_timeline_start {
126                    // Force an extra notification for observers.
127                    ObservableWriteGuard::set(
128                        &mut status_guard,
129                        SharedPaginationStatus::Idle { hit_timeline_start: true },
130                    );
131
132                    return Either::Left(ready(Ok(Some(BackPaginationOutcome {
133                        reached_start: true,
134                        events: Vec::new(),
135                    }))));
136                }
137            }
138
139            SharedPaginationStatus::Paginating { shared_task: shared } => {
140                // There was already a back-pagination request in progress; wait
141                // for it to finish and return its result.
142                let shared = shared.clone();
143                drop(status_guard);
144                return Either::Right(shared.fut.clone());
145            }
146        }
147
148        let reset_status_on_drop_guard = ResetStatusOnDrop {
149            prev_status: Some(status_guard.clone()),
150            pagination_status: status_observable.clone(),
151        };
152
153        let this = self.clone();
154
155        let fut: Pin<Box<dyn SharedPaginationFuture>> = Box::pin(async move {
156            match this.paginate_backwards_impl(batch_size).await? {
157                Some(outcome) => {
158                    // Back-pagination's over and successful, don't reset the
159                    // status to the previous value.
160                    reset_status_on_drop_guard.disarm();
161
162                    // Notify subscribers that pagination ended.
163                    this.cache.status().set(SharedPaginationStatus::Idle {
164                        hit_timeline_start: outcome.reached_start,
165                    });
166
167                    Ok(Some(outcome))
168                }
169
170                None => Ok(None),
171            }
172        });
173
174        let shared_task = fut.shared();
175
176        // Start polling in the background, in a spawned task.
177        let shared_task_clone = shared_task.clone();
178        let join_handle = spawn(async move {
179            if let Err(err) = shared_task_clone.await {
180                warn!("event cache back-pagination failed: {err}");
181            }
182        });
183
184        ObservableWriteGuard::set(
185            &mut status_guard,
186            SharedPaginationStatus::Paginating {
187                shared_task: SharedPaginationTask {
188                    fut: shared_task.clone(),
189                    _join_handle: Arc::new(AbortOnDrop::new(join_handle)),
190                },
191            },
192        );
193
194        // Release the shared lock before waiting for the task to complete.
195        drop(status_guard);
196
197        Either::Right(shared_task)
198    }
199
200    /// Paginate from either the storage or the network.
201    ///
202    /// This method isn't concerned with setting the pagination status; only the
203    /// caller is.
204    ///
205    /// Returns `Ok(None)` if the pagination token used during a network
206    /// pagination has disappeared from the in-memory linked chunk after
207    /// handling the response.
208    async fn paginate_backwards_impl(
209        &self,
210        batch_size: u16,
211    ) -> Result<Option<BackPaginationOutcome>> {
212        // A linked chunk might not be entirely loaded (if it's been
213        // lazy-loaded). Try to load from disk/storage first, then from network
214        // if disk/storage indicated there's no previous events chunk to load.
215
216        loop {
217            match self.cache.load_more_events_backwards().await? {
218                LoadMoreEventsBackwardsOutcome::Gap {
219                    prev_token,
220                    waited_for_initial_prev_token,
221                } => {
222                    if prev_token.is_none() && !waited_for_initial_prev_token {
223                        // We didn't reload a pagination token, and we haven't
224                        // waited for one; wait and start over.
225
226                        const DEFAULT_WAIT_FOR_TOKEN_DURATION: Duration = Duration::from_secs(3);
227
228                        // Otherwise, wait for a notification that we received a
229                        // previous-batch token.
230                        trace!("waiting for a pagination token…");
231
232                        let _ = timeout(
233                            self.cache.wait_for_prev_token(),
234                            DEFAULT_WAIT_FOR_TOKEN_DURATION,
235                        )
236                        .await;
237
238                        trace!("done waiting");
239
240                        self.cache.mark_has_waited_for_initial_prev_token().await?;
241
242                        // Retry!
243                        //
244                        // Note: the next call to `load_more_events_backwards`
245                        // should not return `WaitForInitialPrevToken` because
246                        // we've just marked we've waited for the initial
247                        // `prev_token`, so this is not an infinite loop.
248                        //
249                        // Note 2: not a recursive call, because recursive and
250                        // async have a bad time together.
251                        continue;
252                    }
253
254                    // We have a gap, so resolve it with a network
255                    // back-pagination.
256                    return self.paginate_backwards_with_network(batch_size, prev_token).await;
257                }
258
259                LoadMoreEventsBackwardsOutcome::StartOfTimeline => {
260                    return Ok(Some(BackPaginationOutcome { reached_start: true, events: vec![] }));
261                }
262
263                LoadMoreEventsBackwardsOutcome::Events {
264                    events,
265                    timeline_event_diffs,
266                    reached_start,
267                } => {
268                    return Ok(Some(
269                        self.cache
270                            .conclude_backwards_pagination_from_disk(
271                                events,
272                                timeline_event_diffs,
273                                reached_start,
274                            )
275                            .await,
276                    ));
277                }
278            }
279        }
280    }
281
282    /// Run a single pagination request to the server.
283    ///
284    /// Returns `Ok(None)` if the pagination token used during the request has
285    /// disappeared from the in-memory linked chunk after handling the response.
286    async fn paginate_backwards_with_network(
287        &self,
288        batch_size: u16,
289        prev_token: Option<String>,
290    ) -> Result<Option<BackPaginationOutcome>> {
291        let Some((events, new_token)) =
292            self.cache.paginate_backwards_with_network(batch_size, &prev_token).await?
293        else {
294            // Return an empty default response.
295            return Ok(Some(BackPaginationOutcome {
296                reached_start: false,
297                events: Default::default(),
298            }));
299        };
300
301        self.cache.conclude_backwards_pagination_from_network(events, prev_token, new_token).await
302    }
303}
304
305trait SharedPaginationFuture:
306    Future<Output = Result<Option<BackPaginationOutcome>>> + SendOutsideWasm
307{
308}
309
310impl<T: Future<Output = Result<Option<BackPaginationOutcome>>> + SendOutsideWasm>
311    SharedPaginationFuture for T
312{
313}
314
315/// State for having a pagination run in the background, and be awaited upon by
316/// several tasks.
317///
318/// Such a pagination may be started automatically or manually. It's possible
319/// for a manual caller to wait upon its completion, by awaiting the underlying
320/// shared future.
321#[derive(Clone)]
322pub(in super::super) struct SharedPaginationTask {
323    /// The shared future for a pagination request running in the background, so
324    /// that multiple callers can await it.
325    fut: Shared<Pin<Box<dyn SharedPaginationFuture>>>,
326
327    /// The owned task that started the above future.
328    _join_handle: Arc<AbortOnDrop<()>>,
329}
330
331#[derive(Clone)]
332pub(in super::super) enum SharedPaginationStatus {
333    /// No pagination is happening right now.
334    Idle {
335        /// Have we hit the start of the timeline, i.e. paginating wouldn't have
336        /// any effect?
337        hit_timeline_start: bool,
338    },
339
340    /// Pagination is already running in the background.
341    Paginating { shared_task: SharedPaginationTask },
342}
343
344pub(in super::super) trait PaginatedCache {
345    fn status(&self) -> &SharedObservable<SharedPaginationStatus>;
346
347    fn load_more_events_backwards(
348        &self,
349    ) -> impl Future<Output = Result<LoadMoreEventsBackwardsOutcome>> + SendOutsideWasm;
350
351    fn mark_has_waited_for_initial_prev_token(
352        &self,
353    ) -> impl Future<Output = Result<()>> + SendOutsideWasm;
354
355    fn wait_for_prev_token(&self) -> impl Future<Output = ()> + SendOutsideWasm;
356
357    fn paginate_backwards_with_network(
358        &self,
359        batch_size: u16,
360        prev_token: &Option<String>,
361    ) -> impl Future<Output = Result<Option<(Vec<Event>, Option<String>)>>> + SendOutsideWasm;
362
363    fn conclude_backwards_pagination_from_disk(
364        &self,
365        events: Vec<Event>,
366        timeline_event_diffs: Vec<VectorDiff<Event>>,
367        reached_start: bool,
368    ) -> impl Future<Output = BackPaginationOutcome> + SendOutsideWasm;
369
370    fn conclude_backwards_pagination_from_network(
371        &self,
372        events: Vec<Event>,
373        prev_token: Option<String>,
374        new_token: Option<String>,
375    ) -> impl Future<Output = Result<Option<BackPaginationOutcome>>> + SendOutsideWasm;
376}
377
378/// Status for the pagination on a cache.
379#[derive(Debug, PartialEq, Clone, Copy)]
380#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
381pub enum PaginationStatus {
382    /// No pagination is happening right now.
383    Idle {
384        /// Have we hit the start of the timeline, i.e. paginating wouldn't have
385        /// any effect?
386        hit_timeline_start: bool,
387    },
388
389    /// Pagination is already running in the background.
390    Paginating,
391}
392
393/// Small RAII guard to reset the pagination status on drop, if not disarmed in
394/// the meanwhile.
395struct ResetStatusOnDrop {
396    prev_status: Option<SharedPaginationStatus>,
397    pagination_status: SharedObservable<SharedPaginationStatus>,
398}
399
400impl ResetStatusOnDrop {
401    /// Make the RAII guard have no effect.
402    fn disarm(mut self) {
403        self.prev_status = None;
404    }
405}
406
407impl Drop for ResetStatusOnDrop {
408    fn drop(&mut self) {
409        if let Some(status) = self.prev_status.take() {
410            let _ = self.pagination_status.set(status);
411        }
412    }
413}
414
415/// The result of a single back-pagination request.
416#[derive(Clone, Debug)]
417pub struct BackPaginationOutcome {
418    /// Did the back-pagination reach the start of the timeline?
419    pub reached_start: bool,
420
421    /// All the events that have been returned in the back-pagination request.
422    ///
423    /// Events are presented in reverse order: the first element of the vec, if
424    /// present, is the most "recent" event from the chunk (or technically, the
425    /// last one in the topological ordering).
426    pub events: Vec<Event>,
427}
428
429/// Internal type to represent the output of
430/// [`PaginatedCache::load_more_events_backwards`].
431#[derive(Debug)]
432pub(in super::super) enum LoadMoreEventsBackwardsOutcome {
433    /// A gap has been inserted.
434    Gap {
435        /// The previous batch token to be used as the "end" parameter in the
436        /// back-pagination request.
437        prev_token: Option<String>,
438
439        waited_for_initial_prev_token: bool,
440    },
441
442    /// The start of the timeline has been reached.
443    StartOfTimeline,
444
445    /// Events have been inserted.
446    Events { events: Vec<Event>, timeline_event_diffs: Vec<VectorDiff<Event>>, reached_start: bool },
447}