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}