Skip to main content

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