Skip to main content

matrix_sdk/paginators/
room.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 paginator is a stateful helper object that handles reaching an event,
16//! either from a cache or network, and surrounding events ("context"). Then, it
17//! makes it possible to paginate forward or backward, from that event, until
18//! one end of the timeline (front or back) is reached.
19
20use std::{
21    future::Future,
22    sync::{Arc, Mutex},
23};
24
25use eyeball::{SharedObservable, Subscriber};
26use matrix_sdk_base::{SendOutsideWasm, SyncOutsideWasm, deserialized_responses::TimelineEvent};
27use ruma::{EventId, UInt, api::Direction};
28
29use crate::{
30    Room,
31    paginators::{PaginationResult, PaginationToken, PaginatorError},
32    room::{EventWithContextResponse, Messages, MessagesOptions},
33};
34
35/// Current state of a [`Paginator`].
36#[derive(Debug, PartialEq, Copy, Clone)]
37#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
38pub enum PaginatorState {
39    /// The initial state of the paginator.
40    Initial,
41
42    /// The paginator is fetching the target initial event.
43    FetchingTargetEvent,
44
45    /// The target initial event could be found, zero or more paginations have
46    /// happened since then, and the paginator is at rest now.
47    Idle,
48
49    /// The paginator is… paginating one direction or another.
50    Paginating,
51}
52
53/// Paginations tokens used for backward and forward pagination.
54#[derive(Debug, Clone)]
55pub struct PaginationTokens {
56    /// Pagination token used for backward pagination.
57    pub previous: PaginationToken,
58    /// Pagination token used for forward pagination.
59    pub next: PaginationToken,
60}
61
62/// A stateful object to reach to an event, and then paginate backward and
63/// forward from it.
64///
65/// See also the module-level documentation.
66pub struct Paginator<PR: PaginableRoom> {
67    /// The room in which we're going to run the pagination.
68    room: PR,
69
70    /// Current state of the paginator.
71    state: SharedObservable<PaginatorState>,
72
73    /// Pagination tokens used for subsequent requests.
74    ///
75    /// This mutex is always short-lived, so it's sync.
76    tokens: Mutex<PaginationTokens>,
77}
78
79#[cfg(not(tarpaulin_include))]
80impl<PR: PaginableRoom> std::fmt::Debug for Paginator<PR> {
81    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
82        // Don't include the room in the debug output.
83        f.debug_struct("Paginator")
84            .field("state", &self.state.get())
85            .field("tokens", &self.tokens)
86            .finish_non_exhaustive()
87    }
88}
89
90/// The result of an initial [`Paginator::start_from`] query.
91#[derive(Debug)]
92pub struct StartFromResult {
93    /// All the events returned during this pagination, in topological ordering.
94    pub events: Vec<TimelineEvent>,
95
96    /// Whether the /context query returned a previous batch token.
97    pub has_prev: bool,
98
99    /// Whether the /context query returned a next batch token.
100    pub has_next: bool,
101}
102
103/// Reset the state to a given target on drop.
104struct ResetStateGuard {
105    target: Option<PaginatorState>,
106    state: SharedObservable<PaginatorState>,
107}
108
109impl ResetStateGuard {
110    /// Create a new reset state guard.
111    fn new(state: SharedObservable<PaginatorState>, target: PaginatorState) -> Self {
112        Self { target: Some(target), state }
113    }
114
115    /// Render the guard effectless, and consume it.
116    fn disarm(mut self) {
117        self.target = None;
118    }
119}
120
121impl Drop for ResetStateGuard {
122    fn drop(&mut self) {
123        if let Some(target) = self.target.take() {
124            self.state.set_if_not_eq(target);
125        }
126    }
127}
128
129impl<PR: PaginableRoom> Paginator<PR> {
130    /// Create a new [`Paginator`], given a room implementation.
131    pub fn new(room: PR) -> Self {
132        Self {
133            room,
134            state: SharedObservable::new(PaginatorState::Initial),
135            tokens: Mutex::new(PaginationTokens { previous: None.into(), next: None.into() }),
136        }
137    }
138
139    /// Check if the current state of the paginator matches the expected one.
140    fn check_state(&self, expected: PaginatorState) -> Result<(), PaginatorError> {
141        let actual = self.state.get();
142        if actual != expected {
143            Err(PaginatorError::InvalidPreviousState { expected, actual })
144        } else {
145            Ok(())
146        }
147    }
148
149    /// Returns a subscriber to the internal [`PaginatorState`] machine.
150    pub fn state(&self) -> Subscriber<PaginatorState> {
151        self.state.subscribe()
152    }
153
154    /// Starts the pagination from the initial event, requesting `num_events`
155    /// additional context events.
156    ///
157    /// Only works for fresh [`Paginator`] objects, which are in the
158    /// [`PaginatorState::Initial`] state.
159    pub async fn start_from(
160        &self,
161        event_id: &EventId,
162        num_events: UInt,
163    ) -> Result<StartFromResult, PaginatorError> {
164        self.check_state(PaginatorState::Initial)?;
165
166        // Note: it's possible two callers have checked the state and both
167        // figured it's initial. This check below makes sure there's at most one
168        // which can set the state to FetchingTargetEvent, preventing a race
169        // condition.
170        if self.state.set_if_not_eq(PaginatorState::FetchingTargetEvent).is_none() {
171            return Err(PaginatorError::InvalidPreviousState {
172                expected: PaginatorState::Initial,
173                actual: PaginatorState::FetchingTargetEvent,
174            });
175        }
176
177        let reset_state_guard = ResetStateGuard::new(self.state.clone(), PaginatorState::Initial);
178
179        // TODO: do we want to lazy load members?
180        let lazy_load_members = true;
181
182        let response =
183            self.room.event_with_context(event_id, lazy_load_members, num_events).await?;
184
185        // NOTE: it's super important to not have any `await` after this point,
186        // since we don't want the task to be interrupted anymore, or the
187        // internal state may become incorrect.
188
189        let has_prev = response.prev_batch_token.is_some();
190        let has_next = response.next_batch_token.is_some();
191
192        {
193            let mut tokens = self.tokens.lock().unwrap();
194            tokens.previous = match response.prev_batch_token {
195                Some(token) => PaginationToken::HasMore(token),
196                None => PaginationToken::HitEnd,
197            };
198            tokens.next = match response.next_batch_token {
199                Some(token) => PaginationToken::HasMore(token),
200                None => PaginationToken::HitEnd,
201            };
202        }
203
204        // Forget the reset state guard, so its Drop method is not called.
205        reset_state_guard.disarm();
206        // And set the final state.
207        self.state.set(PaginatorState::Idle);
208
209        // Consolidate the events into a linear timeline, topologically ordered.
210        //
211        // - the events before are returned in the reverse topological order: invert
212        //   them.
213        // - insert the target event, if set.
214        // - the events after are returned in the correct topological order.
215
216        let events = response
217            .events_before
218            .into_iter()
219            .rev()
220            .chain(response.event)
221            .chain(response.events_after)
222            .collect();
223
224        Ok(StartFromResult { events, has_prev, has_next })
225    }
226
227    /// Runs a backward pagination (requesting `num_events` to the server), from
228    /// the current state of the object.
229    ///
230    /// Will return immediately if we have already hit the start of the
231    /// timeline.
232    ///
233    /// May return an error if it's already paginating, or if the call to
234    /// /messages failed.
235    pub async fn paginate_backward(
236        &self,
237        num_events: UInt,
238    ) -> Result<PaginationResult, PaginatorError> {
239        self.paginate(Direction::Backward, num_events).await
240    }
241
242    /// Returns whether we've hit the start of the timeline.
243    ///
244    /// This is true if, and only if, we didn't have a previous-batch token and
245    /// running backwards pagination would be useless.
246    pub fn hit_timeline_start(&self) -> bool {
247        matches!(self.tokens.lock().unwrap().previous, PaginationToken::HitEnd)
248    }
249
250    /// Returns whether we've hit the end of the timeline.
251    ///
252    /// This is true if, and only if, we didn't have a next-batch token and
253    /// running forwards pagination would be useless.
254    pub fn hit_timeline_end(&self) -> bool {
255        matches!(self.tokens.lock().unwrap().next, PaginationToken::HitEnd)
256    }
257
258    /// Runs a forward pagination (requesting `num_events` to the server), from
259    /// the current state of the object.
260    ///
261    /// Will return immediately if we have already hit the end of the timeline.
262    ///
263    /// May return an error if it's already paginating, or if the call to
264    /// /messages failed.
265    pub async fn paginate_forward(
266        &self,
267        num_events: UInt,
268    ) -> Result<PaginationResult, PaginatorError> {
269        self.paginate(Direction::Forward, num_events).await
270    }
271
272    /// Paginate in the given direction, requesting `num_events` events to the
273    /// server, using the `token_lock` to read from and write the pagination
274    /// token.
275    async fn paginate(
276        &self,
277        dir: Direction,
278        num_events: UInt,
279    ) -> Result<PaginationResult, PaginatorError> {
280        self.check_state(PaginatorState::Idle)?;
281
282        let token = {
283            let tokens = self.tokens.lock().unwrap();
284
285            let token = match dir {
286                Direction::Backward => &tokens.previous,
287                Direction::Forward => &tokens.next,
288            };
289
290            match token {
291                PaginationToken::None => None,
292                PaginationToken::HasMore(val) => Some(val.clone()),
293                PaginationToken::HitEnd => {
294                    return Ok(PaginationResult { events: Vec::new(), hit_end_of_timeline: true });
295                }
296            }
297        };
298
299        // Note: it's possible two callers have checked the state and both
300        // figured it's idle. This check below makes sure there's at most one
301        // which can set the state to paginating, preventing a race condition.
302        if self.state.set_if_not_eq(PaginatorState::Paginating).is_none() {
303            return Err(PaginatorError::InvalidPreviousState {
304                expected: PaginatorState::Idle,
305                actual: PaginatorState::Paginating,
306            });
307        }
308
309        let reset_state_guard = ResetStateGuard::new(self.state.clone(), PaginatorState::Idle);
310
311        let mut options = MessagesOptions::new(dir).from(token.as_deref());
312        options.limit = num_events;
313
314        // In case of error, the state is reset to idle automatically thanks to
315        // reset_state_guard.
316        let response = self.room.messages(options).await?;
317
318        // NOTE: it's super important to not have any `await` after this point,
319        // since we don't want the task to be interrupted anymore, or the
320        // internal state may be incorrect.
321
322        let hit_end_of_timeline = response.end.is_none();
323
324        {
325            let mut tokens = self.tokens.lock().unwrap();
326
327            let token = match dir {
328                Direction::Backward => &mut tokens.previous,
329                Direction::Forward => &mut tokens.next,
330            };
331
332            *token = match response.end {
333                Some(val) => PaginationToken::HasMore(val),
334                None => PaginationToken::HitEnd,
335            };
336        }
337
338        // TODO: what to do with state events?
339
340        // Forget the reset state guard, so its Drop method is not called.
341        reset_state_guard.disarm();
342        // And set the final state.
343        self.state.set(PaginatorState::Idle);
344
345        Ok(PaginationResult { events: response.chunk, hit_end_of_timeline })
346    }
347
348    /// Returns the current pagination tokens.
349    pub fn tokens(&self) -> PaginationTokens {
350        self.tokens.lock().unwrap().clone()
351    }
352}
353
354/// A room that can be paginated.
355///
356/// Not [`crate::Room`] because we may want to paginate rooms we don't belong
357/// to.
358pub trait PaginableRoom: SendOutsideWasm + SyncOutsideWasm {
359    /// Runs a /context query for the given room.
360    ///
361    /// ## Parameters
362    ///
363    /// - `event_id` is the identifier of the target event.
364    /// - `lazy_load_members` controls whether room membership events are lazily
365    ///   loaded as context state events.
366    /// - `num_events` is the number of events (including the fetched event) to
367    ///   return as context.
368    ///
369    /// ## Returns
370    ///
371    /// Must return [`PaginatorError::EventNotFound`] whenever the target event
372    /// could not be found, instead of causing an http `Err` result.
373    fn event_with_context(
374        &self,
375        event_id: &EventId,
376        lazy_load_members: bool,
377        num_events: UInt,
378    ) -> impl Future<Output = Result<EventWithContextResponse, PaginatorError>> + SendOutsideWasm;
379
380    /// Runs a /messages query for the given room.
381    fn messages(
382        &self,
383        opts: MessagesOptions,
384    ) -> impl Future<Output = Result<Messages, PaginatorError>> + SendOutsideWasm;
385}
386
387impl PaginableRoom for Room {
388    async fn event_with_context(
389        &self,
390        event_id: &EventId,
391        lazy_load_members: bool,
392        num_events: UInt,
393    ) -> Result<EventWithContextResponse, PaginatorError> {
394        let response =
395            match self.event_with_context(event_id, lazy_load_members, num_events, None).await {
396                Ok(result) => result,
397
398                Err(err) => {
399                    // If the error was a 404, then the event wasn't found on
400                    // the server; special case this to make it easy to react to
401                    // such an error.
402                    if let Some(error) = err.as_client_api_error()
403                        && error.status_code == 404
404                    {
405                        // Event not found
406                        return Err(PaginatorError::EventNotFound(event_id.to_owned()));
407                    }
408
409                    // Otherwise, just return a wrapped error.
410                    return Err(PaginatorError::SdkError(Arc::new(err)));
411                }
412            };
413
414        Ok(response)
415    }
416
417    async fn messages(&self, opts: MessagesOptions) -> Result<Messages, PaginatorError> {
418        self.messages(opts).await.map_err(|err| PaginatorError::SdkError(Arc::new(err)))
419    }
420}
421
422#[cfg(all(not(target_family = "wasm"), test))]
423mod tests {
424    use std::sync::{Arc, LazyLock};
425
426    use futures_core::Future;
427    use futures_util::FutureExt as _;
428    use matrix_sdk_base::deserialized_responses::TimelineEvent;
429    use matrix_sdk_test::{async_test, event_factory::EventFactory};
430    use ruma::{EventId, RoomId, UInt, UserId, api::Direction, event_id, room_id, uint, user_id};
431    use strass::assert_let;
432    use tokio::{
433        spawn,
434        sync::{Mutex, Notify},
435        task::AbortHandle,
436    };
437
438    use super::{PaginableRoom, PaginatorError, PaginatorState};
439    use crate::{
440        paginators::Paginator,
441        room::{EventWithContextResponse, Messages, MessagesOptions},
442        test_utils::assert_event_matches_msg,
443    };
444
445    #[derive(Clone)]
446    struct TestRoom {
447        event_factory: Arc<EventFactory>,
448        wait_for_ready: bool,
449
450        target_event_text: Arc<Mutex<String>>,
451        next_events: Arc<Mutex<Vec<TimelineEvent>>>,
452        prev_events: Arc<Mutex<Vec<TimelineEvent>>>,
453        prev_batch_token: Arc<Mutex<Option<String>>>,
454        next_batch_token: Arc<Mutex<Option<String>>>,
455
456        room_ready: Arc<Notify>,
457    }
458
459    impl TestRoom {
460        fn new(wait_for_ready: bool, room_id: &RoomId, sender: &UserId) -> Self {
461            let event_factory = Arc::new(EventFactory::default().sender(sender).room(room_id));
462
463            Self {
464                event_factory,
465                wait_for_ready,
466
467                room_ready: Default::default(),
468                target_event_text: Default::default(),
469                next_events: Default::default(),
470                prev_events: Default::default(),
471                prev_batch_token: Default::default(),
472                next_batch_token: Default::default(),
473            }
474        }
475
476        /// Unblocks the next request.
477        fn mark_ready(&self) {
478            self.room_ready.notify_one();
479        }
480    }
481
482    static ROOM_ID: LazyLock<&RoomId> = LazyLock::new(|| room_id!("!dune:herbert.org"));
483    static USER_ID: LazyLock<&UserId> = LazyLock::new(|| user_id!("@paul:atreid.es"));
484
485    impl PaginableRoom for TestRoom {
486        async fn event_with_context(
487            &self,
488            event_id: &EventId,
489            _lazy_load_members: bool,
490            num_events: UInt,
491        ) -> Result<EventWithContextResponse, PaginatorError> {
492            // Wait for the room to be marked as ready first.
493            if self.wait_for_ready {
494                self.room_ready.notified().await;
495            }
496
497            let event = self
498                .event_factory
499                .text_msg(self.target_event_text.lock().await.clone())
500                .event_id(event_id)
501                .into_event();
502
503            // Properly simulate `num_events`: take either the closest
504            // num_events events before, or use all of the before events and
505            // then consume after events.
506            let mut num_events = u64::from(num_events) as usize;
507
508            let prev_events = self.prev_events.lock().await;
509
510            let events_before = if prev_events.is_empty() {
511                Vec::new()
512            } else {
513                let len = prev_events.len();
514                let take_before = num_events.min(len);
515                // Subtract is safe because take_before <= num_events.
516                num_events -= take_before;
517                // Subtract is safe because take_before <= len
518                prev_events[len - take_before..len].to_vec()
519            };
520
521            let events_after = self.next_events.lock().await;
522            let events_after = if events_after.is_empty() {
523                Vec::new()
524            } else {
525                events_after[0..num_events.min(events_after.len())].to_vec()
526            };
527
528            Ok(EventWithContextResponse {
529                event: Some(event),
530                events_before,
531                events_after,
532                prev_batch_token: self.prev_batch_token.lock().await.clone(),
533                next_batch_token: self.next_batch_token.lock().await.clone(),
534                state: Vec::new(),
535            })
536        }
537
538        async fn messages(&self, opts: MessagesOptions) -> Result<Messages, PaginatorError> {
539            if self.wait_for_ready {
540                self.room_ready.notified().await;
541            }
542
543            let limit = u64::from(opts.limit) as usize;
544
545            let (end, events) = match opts.dir {
546                Direction::Backward => {
547                    let events = self.prev_events.lock().await;
548                    let events = if events.is_empty() {
549                        Vec::new()
550                    } else {
551                        let len = events.len();
552                        let take_before = limit.min(len);
553                        // Subtract is safe because take_before <= len
554                        events[len - take_before..len].to_vec()
555                    };
556                    (self.prev_batch_token.lock().await.clone(), events)
557                }
558
559                Direction::Forward => {
560                    let events = self.next_events.lock().await;
561                    let events = if events.is_empty() {
562                        Vec::new()
563                    } else {
564                        events[0..limit.min(events.len())].to_vec()
565                    };
566                    (self.next_batch_token.lock().await.clone(), events)
567                }
568            };
569
570            Ok(Messages { start: opts.from.unwrap(), end, chunk: events, state: Vec::new() })
571        }
572    }
573
574    async fn assert_invalid_state<T: std::fmt::Debug>(
575        task: impl Future<Output = Result<T, PaginatorError>>,
576        expected: PaginatorState,
577        actual: PaginatorState,
578    ) {
579        assert_let!(
580            Err(PaginatorError::InvalidPreviousState {
581                expected: real_expected,
582                actual: real_actual
583            }) = task.await
584        );
585        assert_eq!(real_expected, expected);
586        assert_eq!(real_actual, actual);
587    }
588
589    #[async_test]
590    async fn test_start_from() {
591        // Prepare test data.
592        let room = TestRoom::new(false, *ROOM_ID, *USER_ID);
593
594        let event_id = event_id!("$yoyoyo");
595        let event_factory = &room.event_factory;
596
597        *room.target_event_text.lock().await = "fetch_from".to_owned();
598        *room.prev_events.lock().await = (0..10)
599            .rev()
600            .map(|i| event_factory.text_msg(format!("before-{i}")).into_event())
601            .collect();
602        *room.next_events.lock().await =
603            (0..10).map(|i| event_factory.text_msg(format!("after-{i}")).into_event()).collect();
604
605        // When I call `Paginator::start_from`, it works,
606        let paginator = Arc::new(Paginator::new(room.clone()));
607        let context =
608            paginator.start_from(event_id, uint!(100)).await.expect("start_from should work");
609
610        assert!(!context.has_prev);
611        assert!(!context.has_next);
612
613        // And I get the events I expected.
614
615        // 10 events before, the target event, 10 events after.
616        assert_eq!(context.events.len(), 21);
617
618        for (i, event) in context.events.iter().enumerate().take(10) {
619            assert_event_matches_msg(event, &format!("before-{i}"));
620        }
621
622        assert_event_matches_msg(&context.events[10], "fetch_from");
623        assert_eq!(context.events[10].raw().deserialize().unwrap().event_id(), event_id);
624
625        for i in 0..10 {
626            assert_event_matches_msg(&context.events[i + 11], &format!("after-{i}"));
627        }
628    }
629
630    #[async_test]
631    async fn test_start_from_with_num_events() {
632        // Prepare test data.
633        let room = TestRoom::new(false, *ROOM_ID, *USER_ID);
634
635        let event_id = event_id!("$yoyoyo");
636        let event_factory = &room.event_factory;
637
638        *room.target_event_text.lock().await = "fetch_from".to_owned();
639        *room.prev_events.lock().await =
640            (0..100).rev().map(|i| event_factory.text_msg(format!("ev{i}")).into_event()).collect();
641
642        // When I call `Paginator::start_from`, it works,
643        let paginator = Arc::new(Paginator::new(room.clone()));
644        let context =
645            paginator.start_from(event_id, uint!(10)).await.expect("start_from should work");
646
647        // Then I only get 10 events + the target event, even if there was more
648        // than 10 events in the room.
649        assert_eq!(context.events.len(), 11);
650
651        for (i, event) in context.events.iter().enumerate().take(10) {
652            assert_event_matches_msg(event, &format!("ev{i}"));
653        }
654
655        assert_event_matches_msg(&context.events[10], "fetch_from");
656    }
657
658    #[async_test]
659    async fn test_paginate_backward() {
660        // Prepare test data.
661        let room = TestRoom::new(false, *ROOM_ID, *USER_ID);
662
663        let event_id = event_id!("$yoyoyo");
664        let event_factory = &room.event_factory;
665
666        *room.target_event_text.lock().await = "initial".to_owned();
667        *room.prev_batch_token.lock().await = Some("prev".to_owned());
668
669        // When I call `Paginator::start_from`, it works,
670        let paginator = Arc::new(Paginator::new(room.clone()));
671
672        assert!(!paginator.hit_timeline_start(), "we must have a prev-batch token");
673        assert!(
674            !paginator.hit_timeline_end(),
675            "we don't know about the status of the next-batch token"
676        );
677
678        let context =
679            paginator.start_from(event_id, uint!(100)).await.expect("start_from should work");
680
681        // And I get the events I expected.
682        assert_eq!(context.events.len(), 1);
683        assert_event_matches_msg(&context.events[0], "initial");
684        assert_eq!(context.events[0].raw().deserialize().unwrap().event_id(), event_id);
685
686        // There's a previous batch, but no next batch.
687        assert!(context.has_prev);
688        assert!(!context.has_next);
689
690        assert!(!paginator.hit_timeline_start());
691        assert!(paginator.hit_timeline_end());
692
693        // Preparing data for the next back-pagination.
694        *room.prev_events.lock().await = vec![event_factory.text_msg("previous").into_event()];
695        *room.prev_batch_token.lock().await = Some("prev2".to_owned());
696
697        // When I backpaginate, I get the events I expect.
698        let prev =
699            paginator.paginate_backward(uint!(100)).await.expect("paginate backward should work");
700        assert!(!prev.hit_end_of_timeline);
701        assert!(!paginator.hit_timeline_start());
702        assert_eq!(prev.events.len(), 1);
703        assert_event_matches_msg(&prev.events[0], "previous");
704
705        // And I can backpaginate again, because there's a prev batch token
706        // still.
707        *room.prev_events.lock().await = vec![event_factory.text_msg("oldest").into_event()];
708        *room.prev_batch_token.lock().await = None;
709
710        let prev = paginator
711            .paginate_backward(uint!(100))
712            .await
713            .expect("paginate backward the second time should work");
714        assert!(prev.hit_end_of_timeline);
715        assert!(paginator.hit_timeline_start());
716        assert_eq!(prev.events.len(), 1);
717        assert_event_matches_msg(&prev.events[0], "oldest");
718
719        // I've hit the start of the timeline, but back-paginating again will
720        // return immediately.
721        let prev = paginator
722            .paginate_backward(uint!(100))
723            .await
724            .expect("paginate backward the third time should work");
725        assert!(prev.hit_end_of_timeline);
726        assert!(paginator.hit_timeline_start());
727        assert!(prev.events.is_empty());
728    }
729
730    #[async_test]
731    async fn test_paginate_backward_with_limit() {
732        // Prepare test data.
733        let room = TestRoom::new(false, *ROOM_ID, *USER_ID);
734
735        let event_id = event_id!("$yoyoyo");
736        let event_factory = &room.event_factory;
737
738        *room.target_event_text.lock().await = "initial".to_owned();
739        *room.prev_batch_token.lock().await = Some("prev".to_owned());
740
741        // When I call `Paginator::start_from`, it works,
742        let paginator = Arc::new(Paginator::new(room.clone()));
743        let context =
744            paginator.start_from(event_id, uint!(100)).await.expect("start_from should work");
745
746        // And I get the events I expected.
747        assert_eq!(context.events.len(), 1);
748        assert_event_matches_msg(&context.events[0], "initial");
749        assert_eq!(context.events[0].raw().deserialize().unwrap().event_id(), event_id);
750
751        // There's a previous batch.
752        assert!(context.has_prev);
753        assert!(!context.has_next);
754
755        // Preparing data for the next back-pagination.
756        *room.prev_events.lock().await = (0..100)
757            .rev()
758            .map(|i| event_factory.text_msg(format!("prev{i}")).into_event())
759            .collect();
760        *room.prev_batch_token.lock().await = None;
761
762        // When I backpaginate and request 100 events, I get only 10 events.
763        let prev =
764            paginator.paginate_backward(uint!(10)).await.expect("paginate backward should work");
765        assert!(prev.hit_end_of_timeline);
766        assert_eq!(prev.events.len(), 10);
767
768        for (i, event) in prev.events.iter().enumerate().take(10) {
769            assert_event_matches_msg(event, &format!("prev{}", 9 - i));
770        }
771    }
772
773    #[async_test]
774    async fn test_paginate_forward() {
775        // Prepare test data.
776        let room = TestRoom::new(false, *ROOM_ID, *USER_ID);
777
778        let event_id = event_id!("$yoyoyo");
779        let event_factory = &room.event_factory;
780
781        *room.target_event_text.lock().await = "initial".to_owned();
782        *room.next_batch_token.lock().await = Some("next".to_owned());
783
784        // When I call `Paginator::start_from`, it works,
785        let paginator = Arc::new(Paginator::new(room.clone()));
786        assert!(!paginator.hit_timeline_end(), "we must have a next-batch token");
787        assert!(
788            !paginator.hit_timeline_start(),
789            "we don't know about the status of the prev-batch token"
790        );
791
792        let context =
793            paginator.start_from(event_id, uint!(100)).await.expect("start_from should work");
794
795        // And I get the events I expected.
796        assert_eq!(context.events.len(), 1);
797        assert_event_matches_msg(&context.events[0], "initial");
798        assert_eq!(context.events[0].raw().deserialize().unwrap().event_id(), event_id);
799
800        // There's a next batch, but no previous batch (i.e. we've hit the start
801        // of the timeline).
802        assert!(!context.has_prev);
803        assert!(context.has_next);
804
805        assert!(paginator.hit_timeline_start());
806        assert!(!paginator.hit_timeline_end());
807
808        // Preparing data for the next forward-pagination.
809        *room.next_events.lock().await = vec![event_factory.text_msg("next").into_event()];
810        *room.next_batch_token.lock().await = Some("next2".to_owned());
811
812        // When I forward-paginate, I get the events I expect.
813        let next =
814            paginator.paginate_forward(uint!(100)).await.expect("paginate forward should work");
815        assert!(!next.hit_end_of_timeline);
816        assert_eq!(next.events.len(), 1);
817        assert_event_matches_msg(&next.events[0], "next");
818        assert!(!paginator.hit_timeline_end());
819
820        // And I can forward-paginate again, because there's a prev batch token
821        // still.
822        *room.next_events.lock().await = vec![event_factory.text_msg("latest").into_event()];
823        *room.next_batch_token.lock().await = None;
824
825        let next = paginator
826            .paginate_forward(uint!(100))
827            .await
828            .expect("paginate forward the second time should work");
829        assert!(next.hit_end_of_timeline);
830        assert_eq!(next.events.len(), 1);
831        assert_event_matches_msg(&next.events[0], "latest");
832        assert!(paginator.hit_timeline_end());
833
834        // I've hit the start of the timeline, but back-paginating again will
835        // return immediately.
836        let next = paginator
837            .paginate_forward(uint!(100))
838            .await
839            .expect("paginate forward the third time should work");
840        assert!(next.hit_end_of_timeline);
841        assert!(next.events.is_empty());
842        assert!(paginator.hit_timeline_end());
843    }
844
845    #[async_test]
846    async fn test_state() {
847        let room = TestRoom::new(true, *ROOM_ID, *USER_ID);
848
849        *room.prev_batch_token.lock().await = Some("prev".to_owned());
850        *room.next_batch_token.lock().await = Some("next".to_owned());
851
852        let paginator = Arc::new(Paginator::new(room.clone()));
853
854        let event_id = event_id!("$yoyoyo");
855
856        let mut state = paginator.state();
857
858        assert_eq!(state.get(), PaginatorState::Initial);
859        assert!(state.next().now_or_never().is_none());
860
861        // Attempting to run pagination must fail and not change the state.
862        assert_invalid_state(
863            paginator.paginate_backward(uint!(100)),
864            PaginatorState::Idle,
865            PaginatorState::Initial,
866        )
867        .await;
868
869        assert!(state.next().now_or_never().is_none());
870
871        // Running the initial query must work.
872        let p = paginator.clone();
873        let join_handle = spawn(async move { p.start_from(event_id, uint!(100)).await });
874
875        assert_eq!(state.next().await, Some(PaginatorState::FetchingTargetEvent));
876        assert!(state.next().now_or_never().is_none());
877
878        // The query is pending. Running other operations must fail.
879        assert_invalid_state(
880            paginator.start_from(event_id, uint!(100)),
881            PaginatorState::Initial,
882            PaginatorState::FetchingTargetEvent,
883        )
884        .await;
885
886        assert_invalid_state(
887            paginator.paginate_backward(uint!(100)),
888            PaginatorState::Idle,
889            PaginatorState::FetchingTargetEvent,
890        )
891        .await;
892
893        assert!(state.next().now_or_never().is_none());
894
895        // Mark the dummy room as ready. The query may now terminate.
896        room.mark_ready();
897
898        // After fetching the initial event data, the paginator switches to
899        // `Idle`.
900        assert_eq!(state.next().await, Some(PaginatorState::Idle));
901
902        join_handle.await.expect("joined failed").expect("/context failed");
903
904        assert!(state.next().now_or_never().is_none());
905
906        let p = paginator.clone();
907        let join_handle = spawn(async move { p.paginate_backward(uint!(100)).await });
908
909        assert_eq!(state.next().await, Some(PaginatorState::Paginating));
910
911        // The query is pending. Running other operations must fail.
912        assert_invalid_state(
913            paginator.start_from(event_id, uint!(100)),
914            PaginatorState::Initial,
915            PaginatorState::Paginating,
916        )
917        .await;
918
919        assert_invalid_state(
920            paginator.paginate_backward(uint!(100)),
921            PaginatorState::Idle,
922            PaginatorState::Paginating,
923        )
924        .await;
925
926        assert_invalid_state(
927            paginator.paginate_forward(uint!(100)),
928            PaginatorState::Idle,
929            PaginatorState::Paginating,
930        )
931        .await;
932
933        assert!(state.next().now_or_never().is_none());
934
935        room.mark_ready();
936
937        assert_eq!(state.next().await, Some(PaginatorState::Idle));
938
939        join_handle.await.expect("joined failed").expect("/messages failed");
940
941        assert!(state.next().now_or_never().is_none());
942    }
943
944    mod aborts {
945        use super::*;
946        use crate::paginators::room::{PaginationToken, PaginationTokens};
947
948        #[derive(Clone, Default)]
949        struct AbortingRoom {
950            abort_handle: Arc<Mutex<Option<AbortHandle>>>,
951            room_ready: Arc<Notify>,
952        }
953
954        impl AbortingRoom {
955            async fn wait_abort_and_yield(&self) -> ! {
956                // Wait for the controller to tell us we're ready.
957                self.room_ready.notified().await;
958
959                // Abort the given handle.
960                let mut guard = self.abort_handle.lock().await;
961                let handle = guard.take().expect("only call me when i'm initialized");
962                handle.abort();
963
964                // Enter an endless loop of yielding.
965                loop {
966                    tokio::task::yield_now().await;
967                }
968            }
969        }
970
971        impl PaginableRoom for AbortingRoom {
972            async fn event_with_context(
973                &self,
974                _event_id: &EventId,
975                _lazy_load_members: bool,
976                _num_events: UInt,
977            ) -> Result<EventWithContextResponse, PaginatorError> {
978                self.wait_abort_and_yield().await
979            }
980
981            async fn messages(&self, _opts: MessagesOptions) -> Result<Messages, PaginatorError> {
982                self.wait_abort_and_yield().await
983            }
984        }
985
986        #[async_test]
987        async fn test_abort_while_starting_from() {
988            let room = AbortingRoom::default();
989
990            let paginator = Arc::new(Paginator::new(room.clone()));
991
992            let mut state = paginator.state();
993
994            assert_eq!(state.get(), PaginatorState::Initial);
995            assert!(state.next().now_or_never().is_none());
996
997            // When I try to start the initial query…
998            let p = paginator.clone();
999            let join_handle = spawn(async move {
1000                let _ = p.start_from(event_id!("$yoyoyo"), uint!(100)).await;
1001            });
1002
1003            *room.abort_handle.lock().await = Some(join_handle.abort_handle());
1004
1005            assert_eq!(state.next().await, Some(PaginatorState::FetchingTargetEvent));
1006            assert!(state.next().now_or_never().is_none());
1007
1008            room.room_ready.notify_one();
1009
1010            // But it's aborted when awaiting the task.
1011            let join_result = join_handle.await;
1012            assert!(join_result.unwrap_err().is_cancelled());
1013
1014            // Then the state is reset to initial.
1015            assert_eq!(state.next().await, Some(PaginatorState::Initial));
1016            assert!(state.next().now_or_never().is_none());
1017        }
1018
1019        #[async_test]
1020        async fn test_abort_while_paginating() {
1021            let room = AbortingRoom::default();
1022
1023            // Assuming a paginator ready to back- or forward- paginate,
1024            let paginator = Paginator::new(room.clone());
1025            paginator.state.set(PaginatorState::Idle);
1026            *paginator.tokens.lock().unwrap() = PaginationTokens {
1027                previous: PaginationToken::HasMore("prev".to_owned()),
1028                next: PaginationToken::HasMore("next".to_owned()),
1029            };
1030
1031            let paginator = Arc::new(paginator);
1032
1033            let mut state = paginator.state();
1034
1035            assert_eq!(state.get(), PaginatorState::Idle);
1036            assert!(state.next().now_or_never().is_none());
1037
1038            // When I try to back-paginate…
1039            let p = paginator.clone();
1040            let join_handle = spawn(async move {
1041                let _ = p.paginate_backward(uint!(100)).await;
1042            });
1043
1044            *room.abort_handle.lock().await = Some(join_handle.abort_handle());
1045
1046            assert_eq!(state.next().await, Some(PaginatorState::Paginating));
1047            assert!(state.next().now_or_never().is_none());
1048
1049            room.room_ready.notify_one();
1050
1051            // But it's aborted when awaiting the task.
1052            let join_result = join_handle.await;
1053            assert!(join_result.unwrap_err().is_cancelled());
1054
1055            // Then the state is reset to idle.
1056            assert_eq!(state.next().await, Some(PaginatorState::Idle));
1057            assert!(state.next().now_or_never().is_none());
1058
1059            // And ditto for forward pagination.
1060            let p = paginator.clone();
1061            let join_handle = spawn(async move {
1062                let _ = p.paginate_forward(uint!(100)).await;
1063            });
1064
1065            *room.abort_handle.lock().await = Some(join_handle.abort_handle());
1066
1067            assert_eq!(state.next().await, Some(PaginatorState::Paginating));
1068            assert!(state.next().now_or_never().is_none());
1069
1070            room.room_ready.notify_one();
1071
1072            let join_result = join_handle.await;
1073            assert!(join_result.unwrap_err().is_cancelled());
1074
1075            assert_eq!(state.next().await, Some(PaginatorState::Idle));
1076            assert!(state.next().now_or_never().is_none());
1077        }
1078    }
1079}