matrix_sdk/sliding_sync/list/
mod.rs

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