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