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    #[cfg(test)]
215    pub(super) fn set_maximum_number_of_rooms(&self, maximum_number_of_rooms: Option<u32>) {
216        self.inner.maximum_number_of_rooms.set(maximum_number_of_rooms);
217    }
218}
219
220#[derive(Debug)]
221pub(super) struct SlidingSyncListInner {
222    /// Name of this list to easily recognize them.
223    name: String,
224
225    /// The state this list is in.
226    state: StdRwLock<Observable<SlidingSyncListLoadingState>>,
227
228    /// Parameters that are sticky, and can be sent only once per session (until
229    /// the connection is dropped or the server invalidates what the client
230    /// knows).
231    sticky: StdRwLock<SlidingSyncStickyManager<SlidingSyncListStickyParameters>>,
232
233    /// The maximum number of timeline events to query for.
234    timeline_limit: StdRwLock<Bound>,
235
236    /// The total number of rooms that is possible to interact with for the
237    /// given list.
238    ///
239    /// It's not the total rooms that have been fetched. The server tells the
240    /// client that it's possible to fetch this amount of rooms maximum.
241    /// Since this number can change according to the list filters, it's
242    /// observable.
243    maximum_number_of_rooms: SharedObservable<Option<u32>>,
244
245    /// The request generator, i.e. a type that yields the appropriate list
246    /// request. See [`SlidingSyncListRequestGenerator`] to learn more.
247    request_generator: StdRwLock<SlidingSyncListRequestGenerator>,
248
249    /// Cache policy for this list.
250    cache_policy: SlidingSyncListCachePolicy,
251
252    /// The Sliding Sync internal channel sender. See
253    /// [`SlidingSyncInner::internal_channel`] to learn more.
254    sliding_sync_internal_channel_sender: Sender<SlidingSyncInternalMessage>,
255
256    #[cfg(any(test, feature = "testing"))]
257    sync_mode: StdRwLock<SlidingSyncMode>,
258}
259
260impl SlidingSyncListInner {
261    /// Change the sync-mode.
262    ///
263    /// This will change the sync-mode but also the request generator.
264    ///
265    /// The [`Self::state`] is immediately updated to reflect the new state. The
266    /// [`Self::maximum_number_of_rooms`] won't change.
267    pub fn set_sync_mode(&self, sync_mode: SlidingSyncMode) {
268        #[cfg(any(test, feature = "testing"))]
269        {
270            *self.sync_mode.write().unwrap() = sync_mode.clone();
271        }
272
273        {
274            let mut request_generator = self.request_generator.write().unwrap();
275            *request_generator = SlidingSyncListRequestGenerator::new(sync_mode);
276        }
277
278        {
279            let mut state = self.state.write().unwrap();
280
281            let next_state = match **state {
282                SlidingSyncListLoadingState::NotLoaded => SlidingSyncListLoadingState::NotLoaded,
283                SlidingSyncListLoadingState::Preloaded => SlidingSyncListLoadingState::Preloaded,
284                SlidingSyncListLoadingState::PartiallyLoaded
285                | SlidingSyncListLoadingState::FullyLoaded => {
286                    SlidingSyncListLoadingState::PartiallyLoaded
287                }
288            };
289
290            Observable::set(&mut state, next_state);
291        }
292    }
293
294    /// Update the state to the next request, and return it.
295    fn next_request(&self, txn_id: &mut LazyTransactionId) -> Result<http::request::List, Error> {
296        let ranges = {
297            // Use a dedicated scope to ensure the lock is released before continuing.
298            let mut request_generator = self.request_generator.write().unwrap();
299            request_generator.generate_next_ranges(self.maximum_number_of_rooms.get())?
300        };
301
302        // Here we go.
303        Ok(self.request(ranges, txn_id))
304    }
305
306    /// Build a [`http::request::List`] based on the current state of the
307    /// request generator.
308    #[instrument(skip(self), fields(name = self.name))]
309    fn request(&self, ranges: Ranges, txn_id: &mut LazyTransactionId) -> http::request::List {
310        let ranges = ranges.into_iter().map(|r| ((*r.start()).into(), (*r.end()).into())).collect();
311
312        let mut request = assign!(http::request::List::default(), { ranges });
313        request.room_details.timeline_limit = (*self.timeline_limit.read().unwrap()).into();
314
315        {
316            let mut sticky = self.sticky.write().unwrap();
317            sticky.maybe_apply(&mut request, txn_id);
318        }
319
320        request
321    }
322
323    /// Update `[Self::maximum_number_of_rooms]`.
324    ///
325    /// The `maximum_number_of_rooms` is the `lists.$this_list.count` value,
326    /// i.e. maximum number of available rooms as defined by the server.
327    fn update_room_list(&self, maximum_number_of_rooms: Option<u32>) -> Result<bool, Error> {
328        let mut new_changes = false;
329
330        if maximum_number_of_rooms.is_some() {
331            // Update the `maximum_number_of_rooms` if it has changed.
332            if self.maximum_number_of_rooms.set_if_not_eq(maximum_number_of_rooms).is_some() {
333                new_changes = true;
334            }
335        }
336
337        Ok(new_changes)
338    }
339
340    /// Update the state of the [`SlidingSyncListRequestGenerator`] after
341    /// receiving a response.
342    fn update_request_generator_state(&self, maximum_number_of_rooms: u32) -> Result<(), Error> {
343        let mut request_generator = self.request_generator.write().unwrap();
344        let new_state = request_generator.handle_response(&self.name, maximum_number_of_rooms)?;
345        Observable::set_if_not_eq(&mut self.state.write().unwrap(), new_state);
346        Ok(())
347    }
348
349    /// Send a message over the internal channel if there is a receiver, i.e. if
350    /// the sync loop is running.
351    #[instrument]
352    fn internal_channel_send_if_possible(&self, message: SlidingSyncInternalMessage) {
353        // If there is no receiver, the send will fail, but that's OK here.
354        let _ = self.sliding_sync_internal_channel_sender.send(message);
355    }
356}
357
358/// The state the [`SlidingSyncList`] is in.
359///
360/// The lifetime of a `SlidingSyncList` usually starts at `NotLoaded` or
361/// `Preloaded` (if it is restored from a cache). When loading rooms in a list,
362/// depending of the [`SlidingSyncMode`], it moves to `PartiallyLoaded` or
363/// `FullyLoaded`.
364///
365/// If the client has been offline for a while, though, the `SlidingSyncList`
366/// might return back to `PartiallyLoaded` at any point.
367#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
368pub enum SlidingSyncListLoadingState {
369    /// Sliding Sync has not started to load anything yet.
370    #[default]
371    NotLoaded,
372
373    /// Sliding Sync has been preloaded, i.e. restored from a cache for example.
374    Preloaded,
375
376    /// Updates are received from the loaded rooms, and new rooms are being
377    /// fetched in the background.
378    PartiallyLoaded,
379
380    /// Updates are received for all the loaded rooms, and all rooms have been
381    /// loaded!
382    FullyLoaded,
383}
384
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::{sticky_parameters::LazyTransactionId, SlidingSyncInternalMessage};
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}