matrix_sdk/sliding_sync/list/
mod.rs

1mod builder;
2mod frozen;
3mod request_generator;
4
5use std::{
6    fmt,
7    ops::RangeInclusive,
8    sync::{Arc, RwLock as StdRwLock},
9};
10
11use eyeball::{SharedObservable, Subscriber};
12use futures_core::Stream;
13use ruma::{api::client::sync::sync_events::v5 as http, assign, events::StateEventType};
14use serde::{Deserialize, Serialize};
15use tokio::sync::broadcast::Sender;
16use tracing::{instrument, warn};
17
18pub use self::builder::*;
19pub(super) use self::{frozen::FrozenSlidingSyncList, request_generator::*};
20use super::{Error, PollTimeout, SlidingSyncInternalMessage};
21use crate::Result;
22
23/// Should this [`SlidingSyncList`] be stored in the cache, and automatically
24/// reloaded from the cache upon creation?
25#[derive(Clone, Copy, Debug)]
26pub(crate) enum SlidingSyncListCachePolicy {
27    /// Store and load this list from the cache.
28    Enabled,
29    /// Don't store and load this list from the cache.
30    Disabled,
31}
32
33/// The type used to express natural bounds (including but not limited to:
34/// ranges, timeline limit) in the Sliding Sync.
35pub type Bound = u32;
36
37/// One range of rooms in a response from Sliding Sync.
38pub type Range = RangeInclusive<Bound>;
39
40/// Many ranges of rooms.
41pub type Ranges = Vec<Range>;
42
43/// Holding a specific filtered list within the concept of sliding sync.
44///
45/// It is OK to clone this type as much as you need: cloning it is cheap.
46#[derive(Clone, Debug)]
47pub struct SlidingSyncList {
48    inner: Arc<SlidingSyncListInner>,
49}
50
51impl SlidingSyncList {
52    /// Create a new [`SlidingSyncListBuilder`] with the given name.
53    pub fn builder(name: impl Into<String>) -> SlidingSyncListBuilder {
54        SlidingSyncListBuilder::new(name)
55    }
56
57    /// Get the name of the list.
58    pub fn name(&self) -> &str {
59        self.inner.name.as_str()
60    }
61
62    /// Change the sync-mode.
63    ///
64    /// It is sometimes necessary to change the sync-mode of a list on-the-fly.
65    ///
66    /// This will change the sync-mode but also the request generator. A new
67    /// request generator is generated. Since requests are calculated based on
68    /// the request generator, changing the sync-mode is equivalent to
69    /// “resetting” the list. The ranges and the state will be updated when the
70    /// next request will be sent and a response will be received. The
71    /// maximum number of rooms won't change.
72    pub fn set_sync_mode<M>(&self, sync_mode: M)
73    where
74        M: Into<SlidingSyncMode>,
75    {
76        self.inner.set_sync_mode(sync_mode.into());
77
78        // When the sync mode is changed, the sync loop must skip over any work in its
79        // iteration and jump to the next iteration.
80        self.inner.internal_channel_send_if_possible(
81            SlidingSyncInternalMessage::SyncLoopSkipOverCurrentIteration,
82        );
83    }
84
85    /// Get the current state.
86    pub fn state(&self) -> SlidingSyncListLoadingState {
87        self.inner.state.get()
88    }
89
90    /// Check whether this list requires a [`http::Request::timeout`] value.
91    ///
92    /// A list requires a `timeout` query if and only if we want the server to
93    /// wait on new updates, i.e. to do a long-polling.
94    pub(super) fn requires_timeout(&self) -> PollTimeout {
95        let request_generator = &*self.inner.request_generator.read().unwrap();
96
97        (self.inner.requires_timeout)(request_generator)
98    }
99
100    /// Get a stream of state updates.
101    ///
102    /// If this list has been reloaded from a cache, the initial value read from
103    /// the cache will be published.
104    ///
105    /// There's no guarantee of ordering between items emitted by this stream
106    /// and those emitted by other streams exposed on this structure.
107    ///
108    /// The first part of the returned tuple is the actual loading state, and
109    /// the second part is the `Stream` to receive updates.
110    pub fn state_stream(
111        &self,
112    ) -> (SlidingSyncListLoadingState, impl Stream<Item = SlidingSyncListLoadingState>) {
113        (self.inner.state.get(), self.inner.state.subscribe())
114    }
115
116    /// Get the timeline limit.
117    pub fn timeline_limit(&self) -> Bound {
118        *self.inner.timeline_limit.read().unwrap()
119    }
120
121    /// Set timeline limit.
122    pub fn set_timeline_limit(&self, timeline: Bound) {
123        *self.inner.timeline_limit.write().unwrap() = timeline;
124    }
125
126    /// Get the maximum number of rooms. See [`Self::maximum_number_of_rooms`]
127    /// to learn more.
128    pub fn maximum_number_of_rooms(&self) -> Option<u32> {
129        self.inner.maximum_number_of_rooms.get()
130    }
131
132    /// Get a stream of rooms count.
133    ///
134    /// If this list has been reloaded from a cache, the initial value is
135    /// published too.
136    ///
137    /// There's no guarantee of ordering between items emitted by this stream
138    /// and those emitted by other streams exposed on this structure.
139    pub fn maximum_number_of_rooms_stream(&self) -> Subscriber<Option<u32>> {
140        self.inner.maximum_number_of_rooms.subscribe()
141    }
142
143    /// Calculate the next request and return it.
144    ///
145    /// The next request is entirely calculated based on the request generator
146    /// ([`SlidingSyncListRequestGenerator`]).
147    pub(super) fn next_request(&self) -> Result<http::request::List, Error> {
148        self.inner.next_request()
149    }
150
151    /// Returns the current cache policy for this list.
152    pub(super) fn cache_policy(&self) -> SlidingSyncListCachePolicy {
153        self.inner.cache_policy
154    }
155
156    /// Update the list based on the server's response.
157    ///
158    /// # Parameters
159    ///
160    /// - `maximum_number_of_rooms`: the `lists.$this_list.count` value, i.e.
161    ///   maximum number of available rooms in this list, as defined by the
162    ///   server.
163    #[instrument(skip(self), fields(name = self.name()))]
164    pub(super) fn update(&mut self, maximum_number_of_rooms: Option<u32>) -> Result<bool, Error> {
165        // Make sure to update the generator state first; ordering matters because
166        // `update_room_list` observes the latest ranges in the response.
167        if let Some(maximum_number_of_rooms) = maximum_number_of_rooms {
168            self.inner.update_request_generator_state(maximum_number_of_rooms)?;
169        }
170
171        let new_changes = self.inner.update_room_list(maximum_number_of_rooms)?;
172
173        Ok(new_changes)
174    }
175
176    /// Get the sync-mode.
177    #[cfg(feature = "testing")]
178    pub fn sync_mode(&self) -> SlidingSyncMode {
179        self.inner.sync_mode.read().unwrap().clone()
180    }
181
182    /// Set the maximum number of rooms.
183    pub(super) fn set_maximum_number_of_rooms(&self, maximum_number_of_rooms: Option<u32>) {
184        self.inner.maximum_number_of_rooms.set(maximum_number_of_rooms);
185    }
186}
187
188pub(super) struct SlidingSyncListInner {
189    /// Name of this list to easily recognize them.
190    name: String,
191
192    /// The state this list is in.
193    state: SharedObservable<SlidingSyncListLoadingState>,
194
195    /// Does this list require a timeout?
196    #[cfg(not(target_family = "wasm"))]
197    requires_timeout: Arc<dyn Fn(&SlidingSyncListRequestGenerator) -> PollTimeout + Send + Sync>,
198    #[cfg(target_family = "wasm")]
199    requires_timeout: Arc<dyn Fn(&SlidingSyncListRequestGenerator) -> PollTimeout>,
200
201    /// Any filters to apply to the query.
202    filters: Option<http::request::ListFilters>,
203
204    /// Required states to return per room.
205    required_state: Vec<(StateEventType, String)>,
206
207    /// The maximum number of timeline events to query for.
208    timeline_limit: StdRwLock<Bound>,
209
210    /// The total number of rooms that is possible to interact with for the
211    /// given list.
212    ///
213    /// It's not the total rooms that have been fetched. The server tells the
214    /// client that it's possible to fetch this amount of rooms maximum.
215    /// Since this number can change according to the list filters, it's
216    /// observable.
217    maximum_number_of_rooms: SharedObservable<Option<u32>>,
218
219    /// The request generator, i.e. a type that yields the appropriate list
220    /// request. See [`SlidingSyncListRequestGenerator`] to learn more.
221    request_generator: StdRwLock<SlidingSyncListRequestGenerator>,
222
223    /// Cache policy for this list.
224    cache_policy: SlidingSyncListCachePolicy,
225
226    /// The Sliding Sync internal channel sender. See
227    /// [`SlidingSyncInner::internal_channel`] to learn more.
228    sliding_sync_internal_channel_sender: Sender<SlidingSyncInternalMessage>,
229
230    #[cfg(any(test, feature = "testing"))]
231    sync_mode: StdRwLock<SlidingSyncMode>,
232}
233
234impl fmt::Debug for SlidingSyncListInner {
235    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
236        f.debug_struct("SlidingSyncListInner")
237            .field("name", &self.name)
238            .field("state", &self.state)
239            .finish()
240    }
241}
242
243impl SlidingSyncListInner {
244    /// Change the sync-mode.
245    ///
246    /// This will change the sync-mode but also the request generator.
247    ///
248    /// The [`Self::state`] is immediately updated to reflect the new state. The
249    /// [`Self::maximum_number_of_rooms`] won't change.
250    pub fn set_sync_mode(&self, sync_mode: SlidingSyncMode) {
251        #[cfg(any(test, feature = "testing"))]
252        {
253            *self.sync_mode.write().unwrap() = sync_mode.clone();
254        }
255
256        {
257            let mut request_generator = self.request_generator.write().unwrap();
258            *request_generator = SlidingSyncListRequestGenerator::new(sync_mode);
259        }
260
261        self.state.update(|state| {
262            *state = match state {
263                SlidingSyncListLoadingState::NotLoaded => SlidingSyncListLoadingState::NotLoaded,
264                SlidingSyncListLoadingState::Preloaded => SlidingSyncListLoadingState::Preloaded,
265                SlidingSyncListLoadingState::PartiallyLoaded
266                | SlidingSyncListLoadingState::FullyLoaded => {
267                    SlidingSyncListLoadingState::PartiallyLoaded
268                }
269            }
270        });
271    }
272
273    /// Update the state to the next request, and return it.
274    fn next_request(&self) -> Result<http::request::List, Error> {
275        let ranges = {
276            // Use a dedicated scope to ensure the lock is released before continuing.
277            let mut request_generator = self.request_generator.write().unwrap();
278            request_generator.generate_next_ranges(self.maximum_number_of_rooms.get())?
279        };
280
281        // Here we go.
282        Ok(self.request(ranges))
283    }
284
285    /// Build a [`http::request::List`] based on the current state of the
286    /// request generator.
287    #[instrument(skip(self), fields(name = self.name))]
288    fn request(&self, ranges: Ranges) -> http::request::List {
289        let ranges = ranges.into_iter().map(|r| ((*r.start()).into(), (*r.end()).into())).collect();
290
291        let mut request = assign!(http::request::List::default(), { ranges });
292        request.room_details.timeline_limit = (*self.timeline_limit.read().unwrap()).into();
293        request.filters = self.filters.clone();
294        request.room_details.required_state = self.required_state.clone();
295
296        request
297    }
298
299    /// Update `[Self::maximum_number_of_rooms]`.
300    ///
301    /// The `maximum_number_of_rooms` is the `lists.$this_list.count` value,
302    /// i.e. maximum number of available rooms as defined by the server.
303    fn update_room_list(&self, maximum_number_of_rooms: Option<u32>) -> Result<bool, Error> {
304        let mut new_changes = false;
305
306        if maximum_number_of_rooms.is_some() {
307            // Update the `maximum_number_of_rooms` if it has changed.
308            if self.maximum_number_of_rooms.set_if_not_eq(maximum_number_of_rooms).is_some() {
309                new_changes = true;
310            }
311        }
312
313        Ok(new_changes)
314    }
315
316    /// Update the state of the [`SlidingSyncListRequestGenerator`] after
317    /// receiving a response.
318    fn update_request_generator_state(&self, maximum_number_of_rooms: u32) -> Result<(), Error> {
319        let mut request_generator = self.request_generator.write().unwrap();
320
321        let new_state = request_generator.handle_response(&self.name, maximum_number_of_rooms)?;
322        self.state.set_if_not_eq(new_state);
323
324        Ok(())
325    }
326
327    /// Send a message over the internal channel if there is a receiver, i.e. if
328    /// the sync loop is running.
329    #[instrument]
330    fn internal_channel_send_if_possible(&self, message: SlidingSyncInternalMessage) {
331        // If there is no receiver, the send will fail, but that's OK here.
332        let _ = self.sliding_sync_internal_channel_sender.send(message);
333    }
334}
335
336/// The state the [`SlidingSyncList`] is in.
337///
338/// The lifetime of a `SlidingSyncList` usually starts at `NotLoaded` or
339/// `Preloaded` (if it is restored from a cache). When loading rooms in a list,
340/// depending of the [`SlidingSyncMode`], it moves to `PartiallyLoaded` or
341/// `FullyLoaded`.
342///
343/// If the client has been offline for a while, though, the `SlidingSyncList`
344/// might return back to `PartiallyLoaded` at any point.
345#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
346pub enum SlidingSyncListLoadingState {
347    /// Sliding Sync has not started to load anything yet.
348    #[default]
349    NotLoaded,
350
351    /// Sliding Sync has been preloaded, i.e. restored from a cache for example.
352    Preloaded,
353
354    /// Updates are received from the loaded rooms, and new rooms are being
355    /// fetched in the background.
356    PartiallyLoaded,
357
358    /// Updates are received for all the loaded rooms, and all rooms have been
359    /// loaded!
360    FullyLoaded,
361}
362
363#[cfg(test)]
364impl SlidingSyncListLoadingState {
365    /// Check whether the state is [`Self::FullyLoaded`].
366    fn is_fully_loaded(&self) -> bool {
367        matches!(self, Self::FullyLoaded)
368    }
369}
370
371/// Builder for a new sliding sync list in selective mode.
372///
373/// Conveniently allows to add ranges.
374#[derive(Clone, Debug, Default)]
375pub struct SlidingSyncSelectiveModeBuilder {
376    ranges: Ranges,
377}
378
379impl SlidingSyncSelectiveModeBuilder {
380    /// Create a new `SlidingSyncSelectiveModeBuilder`.
381    fn new() -> Self {
382        Self::default()
383    }
384
385    /// Select a range to fetch.
386    pub fn add_range(mut self, range: Range) -> Self {
387        self.ranges.push(range);
388        self
389    }
390
391    /// Select many ranges to fetch.
392    pub fn add_ranges(mut self, ranges: Ranges) -> Self {
393        self.ranges.extend(ranges);
394        self
395    }
396}
397
398impl From<SlidingSyncSelectiveModeBuilder> for SlidingSyncMode {
399    fn from(builder: SlidingSyncSelectiveModeBuilder) -> Self {
400        Self::Selective { ranges: builder.ranges }
401    }
402}
403
404#[derive(Clone, Debug)]
405enum WindowedModeBuilderKind {
406    Paging,
407    Growing,
408}
409
410/// Builder for a new sliding sync list in growing/paging mode.
411#[derive(Clone, Debug)]
412pub struct SlidingSyncWindowedModeBuilder {
413    mode: WindowedModeBuilderKind,
414    batch_size: u32,
415    maximum_number_of_rooms_to_fetch: Option<u32>,
416}
417
418impl SlidingSyncWindowedModeBuilder {
419    fn new(mode: WindowedModeBuilderKind, batch_size: u32) -> Self {
420        Self { mode, batch_size, maximum_number_of_rooms_to_fetch: None }
421    }
422
423    /// The maximum number of rooms to fetch.
424    pub fn maximum_number_of_rooms_to_fetch(mut self, num: u32) -> Self {
425        self.maximum_number_of_rooms_to_fetch = Some(num);
426        self
427    }
428}
429
430impl From<SlidingSyncWindowedModeBuilder> for SlidingSyncMode {
431    fn from(builder: SlidingSyncWindowedModeBuilder) -> Self {
432        match builder.mode {
433            WindowedModeBuilderKind::Paging => Self::Paging {
434                batch_size: builder.batch_size,
435                maximum_number_of_rooms_to_fetch: builder.maximum_number_of_rooms_to_fetch,
436            },
437            WindowedModeBuilderKind::Growing => Self::Growing {
438                batch_size: builder.batch_size,
439                maximum_number_of_rooms_to_fetch: builder.maximum_number_of_rooms_to_fetch,
440            },
441        }
442    }
443}
444
445/// How a [`SlidingSyncList`] fetches the data.
446#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
447pub enum SlidingSyncMode {
448    /// Only sync the specific defined windows/ranges.
449    Selective {
450        /// The specific defined ranges.
451        ranges: Ranges,
452    },
453
454    /// Fully sync all rooms in the background, page by page of `batch_size`,
455    /// like `0..=19`, `20..=39`, `40..=59` etc. assuming the `batch_size` is
456    /// 20.
457    Paging {
458        /// The batch size.
459        batch_size: u32,
460
461        /// The maximum number of rooms to fetch. `None` to fetch everything
462        /// possible.
463        maximum_number_of_rooms_to_fetch: Option<u32>,
464    },
465
466    /// Fully sync all rooms in the background, with a growing window of
467    /// `batch_size`, like `0..=19`, `0..=39`, `0..=59` etc. assuming the
468    /// `batch_size` is 20.
469    Growing {
470        /// The batch size.
471        batch_size: u32,
472
473        /// The maximum number of rooms to fetch. `None` to fetch everything
474        /// possible.
475        maximum_number_of_rooms_to_fetch: Option<u32>,
476    },
477}
478
479impl Default for SlidingSyncMode {
480    fn default() -> Self {
481        Self::Selective { ranges: Vec::new() }
482    }
483}
484
485impl SlidingSyncMode {
486    /// Create a `SlidingSyncMode::Selective`.
487    pub fn new_selective() -> SlidingSyncSelectiveModeBuilder {
488        SlidingSyncSelectiveModeBuilder::new()
489    }
490
491    /// Create a `SlidingSyncMode::Paging`.
492    pub fn new_paging(batch_size: u32) -> SlidingSyncWindowedModeBuilder {
493        SlidingSyncWindowedModeBuilder::new(WindowedModeBuilderKind::Paging, batch_size)
494    }
495
496    /// Create a `SlidingSyncMode::Growing`.
497    pub fn new_growing(batch_size: u32) -> SlidingSyncWindowedModeBuilder {
498        SlidingSyncWindowedModeBuilder::new(WindowedModeBuilderKind::Growing, batch_size)
499    }
500}
501
502#[cfg(test)]
503mod tests {
504    use std::{
505        cell::Cell,
506        ops::Not,
507        sync::{Arc, Mutex},
508    };
509
510    use assert_matches::assert_matches;
511    use matrix_sdk_test::async_test;
512    use ruma::uint;
513    use serde_json::json;
514    use tokio::sync::broadcast::{channel, error::TryRecvError};
515
516    use super::{PollTimeout, SlidingSyncList, SlidingSyncListLoadingState, SlidingSyncMode};
517    use crate::sliding_sync::SlidingSyncInternalMessage;
518
519    macro_rules! assert_json_roundtrip {
520        (from $type:ty: $rust_value:expr => $json_value:expr) => {
521            let json = serde_json::to_value(&$rust_value).unwrap();
522            assert_eq!(json, $json_value);
523
524            let rust: $type = serde_json::from_value(json).unwrap();
525            assert_eq!(rust, $rust_value);
526        };
527    }
528
529    #[async_test]
530    async fn test_sliding_sync_list_selective_mode() {
531        let (sender, mut receiver) = channel(1);
532
533        // Set range on `Selective`.
534        let list = SlidingSyncList::builder("foo")
535            .sync_mode(SlidingSyncMode::new_selective().add_range(0..=1).add_range(2..=3))
536            .build(sender);
537
538        {
539            let mut generator = list.inner.request_generator.write().unwrap();
540            assert_eq!(generator.requested_ranges(), &[0..=1, 2..=3]);
541
542            let ranges = generator.generate_next_ranges(None).unwrap();
543            assert_eq!(ranges, &[0..=1, 2..=3]);
544        }
545
546        // There shouldn't be any internal request to restart the sync loop yet.
547        assert!(matches!(receiver.try_recv(), Err(TryRecvError::Empty)));
548
549        list.set_sync_mode(SlidingSyncMode::new_selective().add_range(4..=5));
550
551        {
552            let mut generator = list.inner.request_generator.write().unwrap();
553            assert_eq!(generator.requested_ranges(), &[4..=5]);
554
555            let ranges = generator.generate_next_ranges(None).unwrap();
556            assert_eq!(ranges, &[4..=5]);
557        }
558
559        // Setting the sync mode requests exactly one restart of the sync loop.
560        assert!(matches!(
561            receiver.try_recv(),
562            Ok(SlidingSyncInternalMessage::SyncLoopSkipOverCurrentIteration)
563        ));
564        assert!(matches!(receiver.try_recv(), Err(TryRecvError::Empty)));
565    }
566
567    #[test]
568    fn test_sliding_sync_list_timeline_limit() {
569        let (sender, _receiver) = channel(1);
570
571        let list = SlidingSyncList::builder("foo")
572            .sync_mode(SlidingSyncMode::new_selective().add_range(0..=1))
573            .timeline_limit(7)
574            .build(sender);
575
576        assert_eq!(list.timeline_limit(), 7);
577
578        list.set_timeline_limit(42);
579        assert_eq!(list.timeline_limit(), 42);
580    }
581
582    macro_rules! assert_ranges {
583        (
584            list = $list:ident,
585            list_state = $first_list_state:ident,
586            maximum_number_of_rooms = $maximum_number_of_rooms:expr,
587            timeout = $initial_timeout:ident,
588            $(
589                next => {
590                    ranges = $( $range_start:literal ..= $range_end:literal ),* ,
591                    is_fully_loaded = $is_fully_loaded:expr,
592                    list_state = $list_state:ident,
593                    timeout = $timeout:ident,
594                }
595            ),*
596            $(,)*
597        ) => {
598            assert_eq!($list.state(), SlidingSyncListLoadingState::$first_list_state, "first state");
599            assert_matches!(
600                $list.requires_timeout(),
601                PollTimeout:: $initial_timeout,
602                "initial timeout",
603            );
604
605            $(
606                {
607                    // Generate a new request.
608                    let request = $list.next_request().unwrap();
609
610                    assert_eq!(
611                        request.ranges,
612                        [
613                            $( (uint!( $range_start ), uint!( $range_end )) ),*
614                        ],
615                        "ranges",
616                    );
617
618                    // Fake a response.
619                    let _ = $list.update(Some($maximum_number_of_rooms));
620
621                    assert_eq!(
622                        $list.inner.request_generator.read().unwrap().is_fully_loaded(),
623                        $is_fully_loaded,
624                        "is fully loaded",
625                    );
626                    assert_eq!(
627                        $list.state(),
628                        SlidingSyncListLoadingState::$list_state,
629                        "state",
630                    );
631                    assert_matches!(
632                        $list.requires_timeout(),
633                        PollTimeout:: $timeout,
634                        "timeout",
635                    );
636                }
637            )*
638        };
639    }
640
641    #[test]
642    fn test_generator_paging_full_sync() {
643        let (sender, _receiver) = channel(1);
644
645        let mut list = SlidingSyncList::builder("testing")
646            .sync_mode(SlidingSyncMode::new_paging(10))
647            .build(sender);
648
649        assert_ranges! {
650            list = list,
651            list_state = NotLoaded,
652            maximum_number_of_rooms = 25,
653            timeout = None,
654            next => {
655                ranges = 0..=9,
656                is_fully_loaded = false,
657                list_state = PartiallyLoaded,
658                timeout = None,
659            },
660            next => {
661                ranges = 10..=19,
662                is_fully_loaded = false,
663                list_state = PartiallyLoaded,
664                timeout = None,
665            },
666            // The maximum number of rooms is reached!
667            next => {
668                ranges = 20..=24,
669                is_fully_loaded = true,
670                list_state = FullyLoaded,
671                timeout = Default,
672            },
673            // Now it's fully loaded, so the same request must be produced every time.
674            next => {
675                ranges = 0..=24, // the range starts at 0 now!
676                is_fully_loaded = true,
677                list_state = FullyLoaded,
678                timeout = Default,
679            },
680            next => {
681                ranges = 0..=24,
682                is_fully_loaded = true,
683                list_state = FullyLoaded,
684                timeout = Default,
685            },
686        };
687    }
688
689    #[test]
690    fn test_generator_paging_full_sync_with_a_maximum_number_of_rooms_to_fetch() {
691        let (sender, _receiver) = channel(1);
692
693        let mut list = SlidingSyncList::builder("testing")
694            .sync_mode(SlidingSyncMode::new_paging(10).maximum_number_of_rooms_to_fetch(22))
695            .build(sender);
696
697        assert_ranges! {
698            list = list,
699            list_state = NotLoaded,
700            maximum_number_of_rooms = 25,
701            timeout = None,
702            next => {
703                ranges = 0..=9,
704                is_fully_loaded = false,
705                list_state = PartiallyLoaded,
706                timeout = None,
707            },
708            next => {
709                ranges = 10..=19,
710                is_fully_loaded = false,
711                list_state = PartiallyLoaded,
712                timeout = None,
713            },
714            // The maximum number of rooms to fetch is reached!
715            next => {
716                ranges = 20..=21,
717                is_fully_loaded = true,
718                list_state = FullyLoaded,
719                timeout = Default,
720            },
721            // Now it's fully loaded, so the same request must be produced every time.
722            next => {
723                ranges = 0..=21, // the range starts at 0 now!
724                is_fully_loaded = true,
725                list_state = FullyLoaded,
726                timeout = Default,
727            },
728            next => {
729                ranges = 0..=21,
730                is_fully_loaded = true,
731                list_state = FullyLoaded,
732                timeout = Default,
733            },
734        };
735    }
736
737    #[test]
738    fn test_generator_growing_full_sync() {
739        let (sender, _receiver) = channel(1);
740
741        let mut list = SlidingSyncList::builder("testing")
742            .sync_mode(SlidingSyncMode::new_growing(10))
743            .build(sender);
744
745        assert_ranges! {
746            list = list,
747            list_state = NotLoaded,
748            maximum_number_of_rooms = 25,
749            timeout = None,
750            next => {
751                ranges = 0..=9,
752                is_fully_loaded = false,
753                list_state = PartiallyLoaded,
754                timeout = None,
755            },
756            next => {
757                ranges = 0..=19,
758                is_fully_loaded = false,
759                list_state = PartiallyLoaded,
760                timeout = None,
761            },
762            // The maximum number of rooms is reached!
763            next => {
764                ranges = 0..=24,
765                is_fully_loaded = true,
766                list_state = FullyLoaded,
767                timeout = Default,
768            },
769            // Now it's fully loaded, so the same request must be produced every time.
770            next => {
771                ranges = 0..=24,
772                is_fully_loaded = true,
773                list_state = FullyLoaded,
774                timeout = Default,
775            },
776            next => {
777                ranges = 0..=24,
778                is_fully_loaded = true,
779                list_state = FullyLoaded,
780                timeout = Default,
781            },
782        };
783    }
784
785    #[test]
786    fn test_generator_growing_full_sync_with_a_maximum_number_of_rooms_to_fetch() {
787        let (sender, _receiver) = channel(1);
788
789        let mut list = SlidingSyncList::builder("testing")
790            .sync_mode(SlidingSyncMode::new_growing(10).maximum_number_of_rooms_to_fetch(22))
791            .build(sender);
792
793        assert_ranges! {
794            list = list,
795            list_state = NotLoaded,
796            maximum_number_of_rooms = 25,
797            timeout = None,
798            next => {
799                ranges = 0..=9,
800                is_fully_loaded = false,
801                list_state = PartiallyLoaded,
802                timeout = None,
803            },
804            next => {
805                ranges = 0..=19,
806                is_fully_loaded = false,
807                list_state = PartiallyLoaded,
808                timeout = None,
809            },
810            // The maximum number of rooms is reached!
811            next => {
812                ranges = 0..=21,
813                is_fully_loaded = true,
814                list_state = FullyLoaded,
815                timeout = Default,
816            },
817            // Now it's fully loaded, so the same request must be produced every time.
818            next => {
819                ranges = 0..=21,
820                is_fully_loaded = true,
821                list_state = FullyLoaded,
822                timeout = Default,
823            },
824            next => {
825                ranges = 0..=21,
826                is_fully_loaded = true,
827                list_state = FullyLoaded,
828                timeout = Default,
829            },
830        };
831    }
832
833    #[test]
834    fn test_generator_selective() {
835        let (sender, _receiver) = channel(1);
836
837        let mut list = SlidingSyncList::builder("testing")
838            .sync_mode(SlidingSyncMode::new_selective().add_range(0..=10).add_range(42..=153))
839            .build(sender);
840
841        assert_ranges! {
842            list = list,
843            list_state = NotLoaded,
844            maximum_number_of_rooms = 25,
845            timeout = Default,
846            // The maximum number of rooms is reached directly!
847            next => {
848                ranges = 0..=10, 42..=153,
849                is_fully_loaded = true,
850                list_state = FullyLoaded,
851                timeout = Default,
852            },
853            // Now it's fully loaded, so the same request must be produced every time.
854            next => {
855                ranges = 0..=10, 42..=153,
856                is_fully_loaded = true,
857                list_state = FullyLoaded,
858                timeout = Default,
859            },
860            next => {
861                ranges = 0..=10, 42..=153,
862                is_fully_loaded = true,
863                list_state = FullyLoaded,
864                timeout = Default,
865            }
866        };
867    }
868
869    #[async_test]
870    async fn test_generator_selective_with_modifying_ranges_on_the_fly() {
871        let (sender, _receiver) = channel(4);
872
873        let mut list = SlidingSyncList::builder("testing")
874            .sync_mode(SlidingSyncMode::new_selective().add_range(0..=10).add_range(42..=153))
875            .build(sender);
876
877        assert_ranges! {
878            list = list,
879            list_state = NotLoaded,
880            maximum_number_of_rooms = 25,
881            timeout = Default,
882            // The maximum number of rooms is reached directly!
883            next => {
884                ranges = 0..=10, 42..=153,
885                is_fully_loaded = true,
886                list_state = FullyLoaded,
887                timeout = Default,
888            },
889            // Now it's fully loaded, so the same request must be produced every time.
890            next => {
891                ranges = 0..=10, 42..=153,
892                is_fully_loaded = true,
893                list_state = FullyLoaded,
894                timeout = Default,
895            },
896            next => {
897                ranges = 0..=10, 42..=153,
898                is_fully_loaded = true,
899                list_state = FullyLoaded,
900                timeout = Default,
901            }
902        };
903
904        list.set_sync_mode(SlidingSyncMode::new_selective().add_range(3..=7));
905
906        assert_ranges! {
907            list = list,
908            list_state = PartiallyLoaded,
909            maximum_number_of_rooms = 25,
910            timeout = Default,
911            next => {
912                ranges = 3..=7,
913                is_fully_loaded = true,
914                list_state = FullyLoaded,
915                timeout = Default,
916            },
917        };
918
919        list.set_sync_mode(SlidingSyncMode::new_selective().add_range(42..=77));
920
921        assert_ranges! {
922            list = list,
923            list_state = PartiallyLoaded,
924            maximum_number_of_rooms = 25,
925            timeout = Default,
926            next => {
927                ranges = 42..=77,
928                is_fully_loaded = true,
929                list_state = FullyLoaded,
930                timeout = Default,
931            },
932        };
933
934        list.set_sync_mode(SlidingSyncMode::new_selective());
935
936        assert_ranges! {
937            list = list,
938            list_state = PartiallyLoaded,
939            maximum_number_of_rooms = 25,
940            timeout = Default,
941            next => {
942                ranges = ,
943                is_fully_loaded = true,
944                list_state = FullyLoaded,
945                timeout = Default,
946            },
947        };
948    }
949
950    #[async_test]
951    async fn test_generator_changing_sync_mode_to_various_modes() {
952        let (sender, _receiver) = channel(4);
953
954        let mut list = SlidingSyncList::builder("testing")
955            .sync_mode(SlidingSyncMode::new_selective().add_range(0..=10).add_range(42..=153))
956            .build(sender);
957
958        assert_ranges! {
959            list = list,
960            list_state = NotLoaded,
961            maximum_number_of_rooms = 25,
962            timeout = Default,
963            // The maximum number of rooms is reached directly!
964            next => {
965                ranges = 0..=10, 42..=153,
966                is_fully_loaded = true,
967                list_state = FullyLoaded,
968                timeout = Default,
969            },
970            // Now it's fully loaded, so the same request must be produced every time.
971            next => {
972                ranges = 0..=10, 42..=153,
973                is_fully_loaded = true,
974                list_state = FullyLoaded,
975                timeout = Default,
976            },
977            next => {
978                ranges = 0..=10, 42..=153,
979                is_fully_loaded = true,
980                list_state = FullyLoaded,
981                timeout = Default,
982            }
983        };
984
985        // Changing from `Selective` to `Growing`.
986        list.set_sync_mode(SlidingSyncMode::new_growing(10));
987
988        assert_ranges! {
989            list = list,
990            list_state = PartiallyLoaded, // we had some partial state, but we can't be sure it's fully loaded until the next request
991            maximum_number_of_rooms = 25,
992            timeout = None,
993            next => {
994                ranges = 0..=9,
995                is_fully_loaded = false,
996                list_state = PartiallyLoaded,
997                timeout = None,
998            },
999            next => {
1000                ranges = 0..=19,
1001                is_fully_loaded = false,
1002                list_state = PartiallyLoaded,
1003                timeout = None,
1004            },
1005            // The maximum number of rooms is reached!
1006            next => {
1007                ranges = 0..=24,
1008                is_fully_loaded = true,
1009                list_state = FullyLoaded,
1010                timeout = Default,
1011            },
1012            // Now it's fully loaded, so the same request must be produced every time.
1013            next => {
1014                ranges = 0..=24,
1015                is_fully_loaded = true,
1016                list_state = FullyLoaded,
1017                timeout = Default,
1018            },
1019            next => {
1020                ranges = 0..=24,
1021                is_fully_loaded = true,
1022                list_state = FullyLoaded,
1023                timeout = Default,
1024            },
1025        };
1026
1027        // Changing from `Growing` to `Paging`.
1028        list.set_sync_mode(SlidingSyncMode::new_paging(10));
1029
1030        assert_ranges! {
1031            list = list,
1032            list_state = PartiallyLoaded, // we had some partial state, but we can't be sure it's fully loaded until the next request
1033            maximum_number_of_rooms = 25,
1034            timeout = None,
1035            next => {
1036                ranges = 0..=9,
1037                is_fully_loaded = false,
1038                list_state = PartiallyLoaded,
1039                timeout = None,
1040            },
1041            next => {
1042                ranges = 10..=19,
1043                is_fully_loaded = false,
1044                list_state = PartiallyLoaded,
1045                timeout = None,
1046            },
1047            // The maximum number of rooms is reached!
1048            next => {
1049                ranges = 20..=24,
1050                is_fully_loaded = true,
1051                list_state = FullyLoaded,
1052                timeout = Default,
1053            },
1054            // Now it's fully loaded, so the same request must be produced every time.
1055            next => {
1056                ranges = 0..=24, // the range starts at 0 now!
1057                is_fully_loaded = true,
1058                list_state = FullyLoaded,
1059                timeout = Default,
1060            },
1061            next => {
1062                ranges = 0..=24,
1063                is_fully_loaded = true,
1064                list_state = FullyLoaded,
1065                timeout = Default,
1066            },
1067        };
1068
1069        // Changing from `Paging` to `Selective`.
1070        list.set_sync_mode(SlidingSyncMode::new_selective());
1071
1072        assert_eq!(list.state(), SlidingSyncListLoadingState::PartiallyLoaded); // we had some partial state, but we can't be sure it's fully loaded until the
1073        // next request
1074
1075        // We need to update the ranges, of course, as they are not managed
1076        // automatically anymore.
1077        list.set_sync_mode(SlidingSyncMode::new_selective().add_range(0..=100));
1078
1079        assert_ranges! {
1080            list = list,
1081            list_state = PartiallyLoaded, // we had some partial state, but we can't be sure it's fully loaded until the next request
1082            maximum_number_of_rooms = 25,
1083            timeout = Default,
1084            // The maximum number of rooms is reached directly!
1085            next => {
1086                ranges = 0..=100,
1087                is_fully_loaded = true,
1088                list_state = FullyLoaded,
1089                timeout = Default,
1090            },
1091            // Now it's fully loaded, so the same request must be produced every time.
1092            next => {
1093                ranges = 0..=100,
1094                is_fully_loaded = true,
1095                list_state = FullyLoaded,
1096                timeout = Default,
1097            },
1098            next => {
1099                ranges = 0..=100,
1100                is_fully_loaded = true,
1101                list_state = FullyLoaded,
1102                timeout = Default,
1103            }
1104        };
1105    }
1106
1107    #[async_test]
1108    #[allow(clippy::await_holding_lock)]
1109    async fn test_inner_update_maximum_number_of_rooms() {
1110        let (sender, _receiver) = channel(1);
1111
1112        let mut list = SlidingSyncList::builder("foo")
1113            .sync_mode(SlidingSyncMode::new_selective().add_range(0..=3))
1114            .build(sender);
1115
1116        assert!(list.maximum_number_of_rooms().is_none());
1117
1118        // Simulate a request.
1119        let _ = list.next_request();
1120        let new_changes = list.update(Some(5)).unwrap();
1121        assert!(new_changes);
1122
1123        // The `maximum_number_of_rooms` has been updated as expected.
1124        assert_eq!(list.maximum_number_of_rooms(), Some(5));
1125
1126        // Simulate another request.
1127        let _ = list.next_request();
1128        let new_changes = list.update(Some(5)).unwrap();
1129        assert!(!new_changes);
1130
1131        // The `maximum_number_of_rooms` has not changed.
1132        assert_eq!(list.maximum_number_of_rooms(), Some(5));
1133    }
1134
1135    #[test]
1136    fn test_sliding_sync_mode_serialization() {
1137        assert_json_roundtrip!(
1138            from SlidingSyncMode: SlidingSyncMode::from(SlidingSyncMode::new_paging(1).maximum_number_of_rooms_to_fetch(2)) => json!({
1139                "Paging": {
1140                    "batch_size": 1,
1141                    "maximum_number_of_rooms_to_fetch": 2
1142                }
1143            })
1144        );
1145        assert_json_roundtrip!(
1146            from SlidingSyncMode: SlidingSyncMode::from(SlidingSyncMode::new_growing(1).maximum_number_of_rooms_to_fetch(2)) => json!({
1147                "Growing": {
1148                    "batch_size": 1,
1149                    "maximum_number_of_rooms_to_fetch": 2
1150                }
1151            })
1152        );
1153        assert_json_roundtrip!(from SlidingSyncMode: SlidingSyncMode::from(SlidingSyncMode::new_selective()) => json!({
1154                "Selective": {
1155                    "ranges": []
1156                }
1157            })
1158        );
1159    }
1160
1161    #[test]
1162    fn test_sliding_sync_list_loading_state_serialization() {
1163        assert_json_roundtrip!(from SlidingSyncListLoadingState: SlidingSyncListLoadingState::NotLoaded => json!("NotLoaded"));
1164        assert_json_roundtrip!(from SlidingSyncListLoadingState: SlidingSyncListLoadingState::Preloaded => json!("Preloaded"));
1165        assert_json_roundtrip!(from SlidingSyncListLoadingState: SlidingSyncListLoadingState::PartiallyLoaded => json!("PartiallyLoaded"));
1166        assert_json_roundtrip!(from SlidingSyncListLoadingState: SlidingSyncListLoadingState::FullyLoaded => json!("FullyLoaded"));
1167    }
1168
1169    #[test]
1170    fn test_sliding_sync_list_loading_state_is_fully_loaded() {
1171        assert!(SlidingSyncListLoadingState::NotLoaded.is_fully_loaded().not());
1172        assert!(SlidingSyncListLoadingState::Preloaded.is_fully_loaded().not());
1173        assert!(SlidingSyncListLoadingState::PartiallyLoaded.is_fully_loaded().not());
1174        assert!(SlidingSyncListLoadingState::FullyLoaded.is_fully_loaded());
1175    }
1176
1177    #[test]
1178    fn test_once_built() {
1179        let (sender, _receiver) = channel(1);
1180
1181        let probe = Arc::new(Mutex::new(Cell::new(false)));
1182        let probe_clone = probe.clone();
1183
1184        let _list = SlidingSyncList::builder("testing")
1185            .once_built(move |list| {
1186                let mut probe_lock = probe.lock().unwrap();
1187                *probe_lock.get_mut() = true;
1188
1189                list
1190            })
1191            .build(sender);
1192
1193        let probe_lock = probe_clone.lock().unwrap();
1194        assert!(probe_lock.get());
1195    }
1196}