matrix_sdk/event_cache/
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//! A sub-object for running pagination tasks on a given room.
16
17use std::{sync::Arc, time::Duration};
18
19use eyeball::Subscriber;
20use matrix_sdk_base::timeout::timeout;
21use matrix_sdk_common::linked_chunk::ChunkContent;
22use tracing::{debug, instrument, trace};
23
24use super::{
25    paginator::{PaginationResult, PaginatorState},
26    room::{
27        events::{Gap, RoomEvents},
28        LoadMoreEventsBackwardsOutcome, RoomEventCacheInner,
29    },
30    BackPaginationOutcome, EventsOrigin, Result, RoomEventCacheUpdate,
31};
32
33/// An API object to run pagination queries on a [`super::RoomEventCache`].
34///
35/// Can be created with [`super::RoomEventCache::pagination()`].
36#[allow(missing_debug_implementations)]
37#[derive(Clone)]
38pub struct RoomPagination {
39    pub(super) inner: Arc<RoomEventCacheInner>,
40}
41
42impl RoomPagination {
43    /// Starts a back-pagination for the requested number of events.
44    ///
45    /// This automatically takes care of waiting for a pagination token from
46    /// sync, if we haven't done that before.
47    ///
48    /// It will run multiple back-paginations until one of these two conditions
49    /// is met:
50    /// - either we've reached the start of the timeline,
51    /// - or we've obtained enough events to fulfill the requested number of
52    ///   events.
53    #[instrument(skip(self))]
54    pub async fn run_backwards_until(
55        &self,
56        num_requested_events: u16,
57    ) -> Result<BackPaginationOutcome> {
58        let mut events = Vec::new();
59
60        loop {
61            if let Some(outcome) = self.run_backwards_impl(num_requested_events).await? {
62                events.extend(outcome.events);
63                if outcome.reached_start || events.len() >= num_requested_events as usize {
64                    return Ok(BackPaginationOutcome {
65                        reached_start: outcome.reached_start,
66                        events,
67                    });
68                }
69                trace!("restarting back-pagination, because we haven't reached the start or obtained enough events yet");
70            }
71
72            debug!("restarting back-pagination because of a timeline reset.");
73        }
74    }
75
76    /// Run a single back-pagination for the requested number of events.
77    ///
78    /// This automatically takes care of waiting for a pagination token from
79    /// sync, if we haven't done that before.
80    #[instrument(skip(self))]
81    pub async fn run_backwards_once(&self, batch_size: u16) -> Result<BackPaginationOutcome> {
82        loop {
83            if let Some(outcome) = self.run_backwards_impl(batch_size).await? {
84                return Ok(outcome);
85            }
86            debug!("restarting back-pagination because of a timeline reset.");
87        }
88    }
89
90    async fn run_backwards_impl(&self, batch_size: u16) -> Result<Option<BackPaginationOutcome>> {
91        const DEFAULT_WAIT_FOR_TOKEN_DURATION: Duration = Duration::from_secs(3);
92
93        // First off, remember that's the `RoomEvents` might be partially loaded
94        // (because not all events are fully loaded).
95        //
96        // Knowing that, if all gaps have been resolved in the linked chunk, let's try
97        // to load more events from the storage!
98
99        // Try to load one chunk backwards. If it returns events, no need to reach the
100        // network!
101        match self.inner.state.write().await.load_more_events_backwards().await? {
102            LoadMoreEventsBackwardsOutcome::Gap => {
103                // continue, let's resolve this gap!
104            }
105
106            LoadMoreEventsBackwardsOutcome::StartOfTimeline => {
107                return Ok(Some(BackPaginationOutcome { reached_start: true, events: vec![] }))
108            }
109
110            LoadMoreEventsBackwardsOutcome::Events(events, sync_timeline_events_diffs) => {
111                if !sync_timeline_events_diffs.is_empty() {
112                    let _ = self.inner.sender.send(RoomEventCacheUpdate::UpdateTimelineEvents {
113                        diffs: sync_timeline_events_diffs,
114                        origin: EventsOrigin::Pagination,
115                    });
116                }
117
118                return Ok(Some(BackPaginationOutcome {
119                    reached_start: false,
120                    // This is a backwards pagination. `BackPaginationOutcome` expects events to
121                    // be in “reverse order”.
122                    events: events.into_iter().rev().collect(),
123                }));
124            }
125        }
126
127        // There is at least one gap that must be resolved. Let's reach the network!
128
129        let prev_token = self.get_or_wait_for_token(Some(DEFAULT_WAIT_FOR_TOKEN_DURATION)).await;
130
131        let prev_token = match prev_token {
132            PaginationToken::HasMore(token) => Some(token),
133            PaginationToken::None => None,
134            PaginationToken::HitEnd => {
135                debug!("Not back-paginating since we've reached the start of the timeline.");
136                return Ok(Some(BackPaginationOutcome { reached_start: true, events: Vec::new() }));
137            }
138        };
139
140        let paginator = &self.inner.paginator;
141
142        paginator.set_idle_state(PaginatorState::Idle, prev_token.clone(), None)?;
143
144        // Run the actual pagination.
145        let PaginationResult { events, hit_end_of_timeline: reached_start } =
146            paginator.paginate_backward(batch_size.into()).await?;
147
148        // Make sure the `RoomEvents` isn't updated while we are saving events from
149        // backpagination.
150        let mut state = self.inner.state.write().await;
151
152        // Check that the previous token still exists; otherwise it's a sign that the
153        // room's timeline has been cleared.
154        let prev_gap_id = if let Some(token) = prev_token {
155            let gap_id = state.events().chunk_identifier(|chunk| {
156                matches!(chunk.content(), ChunkContent::Gap(Gap { ref prev_token }) if *prev_token == token)
157            });
158
159            // We got a previous-batch token from the linked chunk *before* running the
160            // request, which is missing from the linked chunk *after*
161            // completing the request. It may be a sign the linked chunk has
162            // been reset, and it's an error in any case.
163            if gap_id.is_none() {
164                return Ok(None);
165            }
166
167            gap_id
168        } else {
169            None
170        };
171
172        // The new prev token from this pagination.
173        let new_gap = paginator.prev_batch_token().map(|prev_token| Gap { prev_token });
174
175        let (mut events, duplicated_event_ids, all_deduplicated) =
176            state.collect_valid_and_duplicated_events(events).await?;
177
178        // During a backwards pagination, when a duplicated event is found, the old
179        // event is kept and the new event is ignored. This is the opposite strategy
180        // than during a sync where the old event is removed and the new event is added.
181        if !all_deduplicated {
182            // Let's forget the new events that are duplicated.
183            events.retain(|new_event| {
184                new_event
185                    .event_id()
186                    .map(|event_id| !duplicated_event_ids.contains(&event_id))
187                    .unwrap_or(false)
188            });
189        } else {
190            // All new events are duplicated, they can all be ignored.
191            events.clear();
192        }
193
194        let ((), sync_timeline_events_diffs) = state
195            .with_events_mut(|room_events| {
196            // Reverse the order of the events as `/messages` has been called with `dir=b`
197            // (backwards). The `RoomEvents` API expects the first event to be the oldest.
198            // Let's re-order them for this block.
199            let reversed_events = events
200                .iter()
201                .rev()
202                .cloned()
203                .collect::<Vec<_>>();
204
205            let first_event_pos = room_events.events().next().map(|(item_pos, _)| item_pos);
206
207            // First, insert events.
208            let insert_new_gap_pos = if let Some(gap_id) = prev_gap_id {
209                // There is a prior gap, let's replace it by new events!
210                if all_deduplicated {
211                    // All the events were duplicated; don't act upon them, and only remove the
212                    // prior gap that we just filled.
213                    trace!("removing previous gap, as all events have been deduplicated");
214                    room_events.remove_gap_at(gap_id).expect("gap identifier is a valid gap chunk id we read previously")
215                } else {
216                    trace!("replacing previous gap with the back-paginated events");
217
218                    // Replace the gap with the events we just deduplicated.
219                    room_events.replace_gap_at(reversed_events.clone(), gap_id)
220                        .expect("gap_identifier is a valid chunk id we read previously")
221                }
222            } else if let Some(pos) = first_event_pos {
223                // No prior gap, but we had some events: assume we need to prepend events
224                // before those.
225                trace!("inserted events before the first known event");
226
227                room_events
228                    .insert_events_at(reversed_events.clone(), pos)
229                    .expect("pos is a valid position we just read above");
230
231                Some(pos)
232            } else {
233                // No prior gap, and no prior events: push the events.
234                trace!("pushing events received from back-pagination");
235
236                room_events.push_events(reversed_events.clone());
237
238                // A new gap may be inserted before the new events, if there are any.
239                room_events.events().next().map(|(item_pos, _)| item_pos)
240            };
241
242            // And insert the new gap if needs be.
243            //
244            // We only do this when at least one new, non-duplicated event, has been added to
245            // the chunk. Otherwise it means we've back-paginated all the known events.
246            if !all_deduplicated {
247                if let Some(new_gap) = new_gap {
248                    if let Some(new_pos) = insert_new_gap_pos {
249                        room_events
250                            .insert_gap_at(new_gap, new_pos)
251                            .expect("events_chunk_pos represents a valid chunk position");
252                    } else {
253                        room_events.push_gap(new_gap);
254                    }
255                }
256            } else {
257                debug!("not storing previous batch token, because we deduplicated all new back-paginated events");
258            }
259
260            room_events.on_new_events(&self.inner.room_version, reversed_events.iter());
261        })
262        .await?;
263
264        let backpagination_outcome = BackPaginationOutcome { events, reached_start };
265
266        if !sync_timeline_events_diffs.is_empty() {
267            let _ = self.inner.sender.send(RoomEventCacheUpdate::UpdateTimelineEvents {
268                diffs: sync_timeline_events_diffs,
269                origin: EventsOrigin::Pagination,
270            });
271        }
272
273        Ok(Some(backpagination_outcome))
274    }
275
276    /// Get the latest pagination token, as stored in the room events linked
277    /// list, or wait for it for the given amount of time.
278    ///
279    /// It will only wait if we *never* saw an initial previous-batch token.
280    /// Otherwise, it will immediately skip.
281    #[doc(hidden)]
282    pub async fn get_or_wait_for_token(&self, wait_time: Option<Duration>) -> PaginationToken {
283        fn get_latest(events: &RoomEvents) -> Option<String> {
284            events.rchunks().find_map(|chunk| match chunk.content() {
285                ChunkContent::Gap(gap) => Some(gap.prev_token.clone()),
286                ChunkContent::Items(..) => None,
287            })
288        }
289
290        {
291            // Scope for the lock guard.
292            let state = self.inner.state.read().await;
293
294            // Check if the linked chunk contains any events. If so, absence of a gap means
295            // we've hit the start of the timeline. If not, absence of a gap
296            // means we've never received a pagination token from sync, and we
297            // should wait for one.
298            let has_events = state.events().events().next().is_some();
299
300            // Fast-path: we do have a previous-batch token already.
301            if let Some(found) = get_latest(state.events()) {
302                return PaginationToken::HasMore(found);
303            }
304
305            // If we had events, and there was no gap, then we've hit the end of the
306            // timeline.
307            if has_events {
308                return PaginationToken::HitEnd;
309            }
310
311            // If we've already waited for an initial previous-batch token before,
312            // immediately abort.
313            if state.waited_for_initial_prev_token {
314                return PaginationToken::None;
315            }
316        }
317
318        // If the caller didn't set a wait time, return none early.
319        let Some(wait_time) = wait_time else {
320            return PaginationToken::None;
321        };
322
323        // Otherwise, wait for a notification that we received a previous-batch token.
324        // Note the state lock is released while doing so, allowing other tasks to write
325        // into the linked chunk.
326        let _ = timeout(self.inner.pagination_batch_token_notifier.notified(), wait_time).await;
327
328        let mut state = self.inner.state.write().await;
329
330        state.waited_for_initial_prev_token = true;
331
332        if let Some(token) = get_latest(state.events()) {
333            PaginationToken::HasMore(token)
334        } else if state.events().events().next().is_some() {
335            // See logic above, in the read lock guard scope.
336            PaginationToken::HitEnd
337        } else {
338            PaginationToken::None
339        }
340    }
341
342    /// Returns a subscriber to the pagination status used for the
343    /// back-pagination integrated to the event cache.
344    pub fn status(&self) -> Subscriber<PaginatorState> {
345        self.inner.paginator.state()
346    }
347
348    /// Returns whether we've hit the start of the timeline.
349    ///
350    /// This is true if, and only if, we didn't have a previous-batch token and
351    /// running backwards pagination would be useless.
352    pub fn hit_timeline_start(&self) -> bool {
353        self.inner.paginator.hit_timeline_start()
354    }
355
356    /// Returns whether we've hit the end of the timeline.
357    ///
358    /// This is true if, and only if, we didn't have a next-batch token and
359    /// running forwards pagination would be useless.
360    pub fn hit_timeline_end(&self) -> bool {
361        self.inner.paginator.hit_timeline_end()
362    }
363}
364
365/// Pagination token data, indicating in which state is the current pagination.
366#[derive(Clone, Debug, PartialEq)]
367pub enum PaginationToken {
368    /// We never had a pagination token, so we'll start back-paginating from the
369    /// end, or forward-paginating from the start.
370    None,
371    /// We paginated once before, and we received a prev/next batch token that
372    /// we may reuse for the next query.
373    HasMore(String),
374    /// We've hit one end of the timeline (either the start or the actual end),
375    /// so there's no need to continue paginating.
376    HitEnd,
377}
378
379impl From<Option<String>> for PaginationToken {
380    fn from(token: Option<String>) -> Self {
381        match token {
382            Some(val) => Self::HasMore(val),
383            None => Self::None,
384        }
385    }
386}
387
388#[cfg(test)]
389mod tests {
390    // Those tests require time to work, and it does not on wasm32.
391    #[cfg(not(target_arch = "wasm32"))]
392    mod time_tests {
393        use std::time::{Duration, Instant};
394
395        use assert_matches::assert_matches;
396        use matrix_sdk_base::RoomState;
397        use matrix_sdk_test::{async_test, event_factory::EventFactory, ALICE};
398        use ruma::{event_id, room_id, user_id};
399        use tokio::{spawn, time::sleep};
400
401        use crate::{
402            event_cache::{pagination::PaginationToken, room::events::Gap},
403            test_utils::logged_in_client,
404        };
405
406        #[async_test]
407        async fn test_wait_no_pagination_token() {
408            let client = logged_in_client(None).await;
409            let room_id = room_id!("!galette:saucisse.bzh");
410            client.base_client().get_or_create_room(room_id, RoomState::Joined);
411
412            let event_cache = client.event_cache();
413
414            event_cache.subscribe().unwrap();
415
416            let (room_event_cache, _drop_handlers) = event_cache.for_room(room_id).await.unwrap();
417
418            let pagination = room_event_cache.pagination();
419
420            // If I have a room with no events, and try to get a pagination token without
421            // waiting,
422            let found = pagination.get_or_wait_for_token(None).await;
423            // Then I don't get any pagination token.
424            assert_matches!(found, PaginationToken::None);
425
426            // Reset waited_for_initial_prev_token and event state.
427            let _ = pagination.inner.state.write().await.reset().await.unwrap();
428
429            // If I wait for a back-pagination token for 0 seconds,
430            let before = Instant::now();
431            let found = pagination.get_or_wait_for_token(Some(Duration::default())).await;
432            let waited = before.elapsed();
433            // then I don't get any,
434            assert_matches!(found, PaginationToken::None);
435            // and I haven't waited long.
436            assert!(waited.as_secs() < 1);
437
438            // Reset waited_for_initial_prev_token state.
439            let _ = pagination.inner.state.write().await.reset().await.unwrap();
440
441            // If I wait for a back-pagination token for 1 second,
442            let before = Instant::now();
443            let found = pagination.get_or_wait_for_token(Some(Duration::from_secs(1))).await;
444            let waited = before.elapsed();
445            // then I still don't get any.
446            assert_matches!(found, PaginationToken::None);
447            // and I've waited a bit.
448            assert!(waited.as_secs() < 2);
449            assert!(waited.as_secs() >= 1);
450        }
451
452        #[async_test]
453        async fn test_wait_hit_end_of_timeline() {
454            let client = logged_in_client(None).await;
455            let room_id = room_id!("!galette:saucisse.bzh");
456            client.base_client().get_or_create_room(room_id, RoomState::Joined);
457
458            let event_cache = client.event_cache();
459
460            event_cache.subscribe().unwrap();
461
462            let (room_event_cache, _drop_handlers) = event_cache.for_room(room_id).await.unwrap();
463
464            let f = EventFactory::new().room(room_id).sender(*ALICE);
465            let pagination = room_event_cache.pagination();
466
467            // Add a previous event.
468            room_event_cache
469                .inner
470                .state
471                .write()
472                .await
473                .with_events_mut(|events| {
474                    events.push_events([f
475                        .text_msg("this is the start of the timeline")
476                        .into_event()]);
477                })
478                .await
479                .unwrap();
480
481            // If I have a room with events, and try to get a pagination token without
482            // waiting,
483            let found = pagination.get_or_wait_for_token(None).await;
484            // I've reached the start of the timeline.
485            assert_matches!(found, PaginationToken::HitEnd);
486
487            // If I wait for a back-pagination token for 0 seconds,
488            let before = Instant::now();
489            let found = pagination.get_or_wait_for_token(Some(Duration::default())).await;
490            let waited = before.elapsed();
491            // Then I still have reached the start of the timeline.
492            assert_matches!(found, PaginationToken::HitEnd);
493            // and I've waited very little.
494            assert!(waited.as_secs() < 1);
495
496            // If I wait for a back-pagination token for 1 second,
497            let before = Instant::now();
498            let found = pagination.get_or_wait_for_token(Some(Duration::from_secs(1))).await;
499            let waited = before.elapsed();
500            // then I still don't get any.
501            assert_matches!(found, PaginationToken::HitEnd);
502            // and I've waited very little (there's no point in waiting in this case).
503            assert!(waited.as_secs() < 1);
504
505            // Now, reset state. We'll add an event *after* we've started waiting, this
506            // time.
507            room_event_cache.clear().await.unwrap();
508
509            spawn(async move {
510                sleep(Duration::from_secs(1)).await;
511
512                room_event_cache
513                    .inner
514                    .state
515                    .write()
516                    .await
517                    .with_events_mut(|events| {
518                        events.push_events([f
519                            .text_msg("this is the start of the timeline")
520                            .into_event()]);
521                    })
522                    .await
523                    .unwrap();
524            });
525
526            // If I wait for a pagination token,
527            let before = Instant::now();
528            let found = pagination.get_or_wait_for_token(Some(Duration::from_secs(2))).await;
529            let waited = before.elapsed();
530            // since sync has returned all events, and no prior gap, I've hit the end.
531            assert_matches!(found, PaginationToken::HitEnd);
532            // and I've waited for the whole duration.
533            assert!(waited.as_secs() >= 2);
534            assert!(waited.as_secs() < 3);
535        }
536
537        #[async_test]
538        async fn test_wait_for_pagination_token_already_present() {
539            let client = logged_in_client(None).await;
540            let room_id = room_id!("!galette:saucisse.bzh");
541            client.base_client().get_or_create_room(room_id, RoomState::Joined);
542
543            let event_cache = client.event_cache();
544
545            event_cache.subscribe().unwrap();
546
547            let (room_event_cache, _drop_handlers) = event_cache.for_room(room_id).await.unwrap();
548
549            let expected_token = "old".to_owned();
550
551            // When I have events and multiple gaps, in a room,
552            {
553                room_event_cache
554                    .inner
555                    .state
556                    .write()
557                    .await
558                    .with_events_mut(|room_events| {
559                        room_events.push_gap(Gap { prev_token: expected_token.clone() });
560                        room_events.push_events([EventFactory::new()
561                            .text_msg("yolo")
562                            .sender(user_id!("@b:z.h"))
563                            .event_id(event_id!("$ida"))
564                            .into_event()]);
565                    })
566                    .await
567                    .unwrap();
568            }
569
570            let pagination = room_event_cache.pagination();
571
572            // If I don't wait for a back-pagination token,
573            let found = pagination.get_or_wait_for_token(None).await;
574            // Then I get it.
575            assert_eq!(found, PaginationToken::HasMore(expected_token.clone()));
576
577            // If I wait for a back-pagination token for 0 seconds,
578            let before = Instant::now();
579            let found = pagination.get_or_wait_for_token(Some(Duration::default())).await;
580            let waited = before.elapsed();
581            // then I do get one.
582            assert_eq!(found, PaginationToken::HasMore(expected_token.clone()));
583            // and I haven't waited long.
584            assert!(waited.as_millis() < 100);
585
586            // If I wait for a back-pagination token for 1 second,
587            let before = Instant::now();
588            let found = pagination.get_or_wait_for_token(Some(Duration::from_secs(1))).await;
589            let waited = before.elapsed();
590            // then I do get one.
591            assert_eq!(found, PaginationToken::HasMore(expected_token));
592            // and I haven't waited long.
593            assert!(waited.as_millis() < 100);
594        }
595
596        #[async_test]
597        async fn test_wait_for_late_pagination_token() {
598            let client = logged_in_client(None).await;
599            let room_id = room_id!("!galette:saucisse.bzh");
600            client.base_client().get_or_create_room(room_id, RoomState::Joined);
601
602            let event_cache = client.event_cache();
603
604            event_cache.subscribe().unwrap();
605
606            let (room_event_cache, _drop_handles) = event_cache.for_room(room_id).await.unwrap();
607
608            let expected_token = "old".to_owned();
609
610            let before = Instant::now();
611            let cloned_expected_token = expected_token.clone();
612            let cloned_room_event_cache = room_event_cache.clone();
613            let insert_token_task = spawn(async move {
614                // If a backpagination token is inserted after 400 milliseconds,
615                sleep(Duration::from_millis(400)).await;
616
617                cloned_room_event_cache
618                    .inner
619                    .state
620                    .write()
621                    .await
622                    .with_events_mut(|events| {
623                        events.push_gap(Gap { prev_token: cloned_expected_token })
624                    })
625                    .await
626                    .unwrap();
627            });
628
629            let pagination = room_event_cache.pagination();
630
631            // Then first I don't get it (if I'm not waiting,)
632            let found = pagination.get_or_wait_for_token(None).await;
633            assert_matches!(found, PaginationToken::None);
634
635            // And if I wait for the back-pagination token for 600ms,
636            let found = pagination.get_or_wait_for_token(Some(Duration::from_millis(600))).await;
637            let waited = before.elapsed();
638
639            // then I do get one eventually.
640            assert_eq!(found, PaginationToken::HasMore(expected_token));
641            // and I have waited between ~400 and ~1000 milliseconds.
642            assert!(waited.as_secs() < 1);
643            assert!(waited.as_millis() >= 400);
644
645            // The task succeeded.
646            insert_token_task.await.unwrap();
647        }
648
649        #[async_test]
650        async fn test_get_latest_token() {
651            let client = logged_in_client(None).await;
652            let room_id = room_id!("!galette:saucisse.bzh");
653            client.base_client().get_or_create_room(room_id, RoomState::Joined);
654
655            let event_cache = client.event_cache();
656
657            event_cache.subscribe().unwrap();
658
659            let (room_event_cache, _drop_handles) = event_cache.for_room(room_id).await.unwrap();
660
661            let old_token = "old".to_owned();
662            let new_token = "new".to_owned();
663
664            // Assuming a room event cache that contains both an old and a new pagination
665            // token, and events in between,
666            room_event_cache
667                .inner
668                .state
669                .write()
670                .await
671                .with_events_mut(|events| {
672                    let f = EventFactory::new().room(room_id).sender(*ALICE);
673
674                    // This simulates a valid representation of a room: first group of gap+events
675                    // were e.g. restored from the cache; second group of gap+events was received
676                    // from a subsequent sync.
677                    events.push_gap(Gap { prev_token: old_token });
678                    events.push_events([f.text_msg("oldest from cache").into()]);
679
680                    events.push_gap(Gap { prev_token: new_token.clone() });
681                    events.push_events([f.text_msg("sync'd gappy timeline").into()]);
682                })
683                .await
684                .unwrap();
685
686            let pagination = room_event_cache.pagination();
687
688            // Retrieving the pagination token will return the most recent one, not the old
689            // one.
690            let found = pagination.get_or_wait_for_token(None).await;
691            assert_eq!(found, PaginationToken::HasMore(new_token));
692        }
693    }
694}