matrix_sdk/sliding_sync/list/
mod.rs

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