matrix_sdk/sliding_sync/list/
request_generator.rs

1//! The logic to generate Sliding Sync list requests.
2//!
3//! Depending on the [`SlidingSyncMode`], the generated requests aren't the
4//! same.
5//!
6//! In [`SlidingSyncMode::Selective`], it's pretty straightforward:
7//!
8//! * There is a set of ranges,
9//! * Each request asks to load the particular ranges.
10//!
11//! In [`SlidingSyncMode::Paging`]:
12//!
13//! * There is a `batch_size`,
14//! * Each request asks to load a new successive range containing exactly
15//!   `batch_size` rooms.
16//!
17//! In [`SlidingSyncMode::Growing]:
18//!
19//! * There is a `batch_size`,
20//! * Each request asks to load a new range, always starting from 0, but where
21//!   the end is incremented by `batch_size` every time.
22//!
23//! The number of rooms to load is capped by a `maximum_number_of_rooms`, i.e.
24//! the real number of rooms it is possible to load. This value comes from the
25//! server.
26//!
27//! The number of rooms to load can _also_ be capped by the
28//! `maximum_number_of_rooms_to_fetch`, i.e. a user-specified limit representing
29//! the maximum number of rooms the user actually wants to load.
30
31use std::cmp::min;
32
33use super::{Range, Ranges, SlidingSyncMode};
34use crate::{SlidingSyncListLoadingState, sliding_sync::Error};
35
36/// The kind of request generator.
37#[derive(Debug, PartialEq)]
38pub enum SlidingSyncListRequestGeneratorKind {
39    /// Growing-mode (see [`SlidingSyncMode`]).
40    Growing {
41        /// Size of the batch, used to grow the range to fetch more rooms.
42        batch_size: u32,
43        /// Maximum number of rooms to fetch (see
44        /// [`SlidingSyncList::full_sync_maximum_number_of_rooms_to_fetch`]).
45        maximum_number_of_rooms_to_fetch: Option<u32>,
46        /// Number of rooms that have been already fetched.
47        number_of_fetched_rooms: u32,
48        /// Whether all rooms have been loaded.
49        fully_loaded: bool,
50        /// End range requested in the previous request.
51        requested_end: Option<u32>,
52    },
53
54    /// Paging-mode (see [`SlidingSyncMode`]).
55    Paging {
56        /// Size of the batch, used to grow the range to fetch more rooms.
57        batch_size: u32,
58        /// Maximum number of rooms to fetch (see
59        /// [`SlidingSyncList::full_sync_maximum_number_of_rooms_to_fetch`]).
60        maximum_number_of_rooms_to_fetch: Option<u32>,
61        /// Number of rooms that have been already fetched.
62        number_of_fetched_rooms: u32,
63        /// Whether all romms have been loaded.
64        fully_loaded: bool,
65        /// End range requested in the previous request.
66        requested_end: Option<u32>,
67    },
68
69    /// Selective-mode (see [`SlidingSyncMode`]).
70    Selective,
71}
72
73/// A request generator for [`SlidingSyncList`].
74#[derive(Debug)]
75pub struct SlidingSyncListRequestGenerator {
76    /// The current ranges used by this request generator.
77    ///
78    /// Note there's only one range in the `Growing` and `Paging` mode.
79    ranges: Ranges,
80
81    /// The kind of request generator.
82    kind: SlidingSyncListRequestGeneratorKind,
83}
84
85impl SlidingSyncListRequestGenerator {
86    /// Create a new request generator from scratch, given a sync mode.
87    pub(super) fn new(sync_mode: SlidingSyncMode) -> Self {
88        match sync_mode {
89            SlidingSyncMode::Paging { batch_size, maximum_number_of_rooms_to_fetch } => Self {
90                ranges: Vec::new(),
91                kind: SlidingSyncListRequestGeneratorKind::Paging {
92                    batch_size,
93                    maximum_number_of_rooms_to_fetch,
94                    number_of_fetched_rooms: 0,
95                    fully_loaded: false,
96                    requested_end: None,
97                },
98            },
99
100            SlidingSyncMode::Growing { batch_size, maximum_number_of_rooms_to_fetch } => Self {
101                ranges: Vec::new(),
102                kind: SlidingSyncListRequestGeneratorKind::Growing {
103                    batch_size,
104                    maximum_number_of_rooms_to_fetch,
105                    number_of_fetched_rooms: 0,
106                    fully_loaded: false,
107                    requested_end: None,
108                },
109            },
110
111            SlidingSyncMode::Selective { ranges } => {
112                Self { ranges, kind: SlidingSyncListRequestGeneratorKind::Selective }
113            }
114        }
115    }
116
117    /// Return a view on the ranges requested by this generator.
118    ///
119    /// For generators in the selective mode, this is the initial set of ranges.
120    /// For growing and paginated generators, this is the range committed in the
121    /// latest response received from the server.
122    pub fn requested_ranges(&self) -> &[Range] {
123        &self.ranges
124    }
125
126    /// Return the kind of request generator is used by this generator.
127    pub fn kind(&self) -> &SlidingSyncListRequestGeneratorKind {
128        &self.kind
129    }
130
131    /// Update internal state of the generator (namely, ranges) before the next
132    /// sliding sync request.
133    pub(super) fn generate_next_ranges(
134        &mut self,
135        maximum_number_of_rooms: Option<u32>,
136    ) -> Result<Ranges, Error> {
137        match &mut self.kind {
138            // Cases where all rooms have been fully loaded.
139            SlidingSyncListRequestGeneratorKind::Paging { fully_loaded: true, .. }
140            | SlidingSyncListRequestGeneratorKind::Growing { fully_loaded: true, .. }
141            | SlidingSyncListRequestGeneratorKind::Selective => {
142                // Nothing to do: we already have the full ranges, return the existing ranges.
143                // For the growing and paging modes, keep the current value of `requested_end`,
144                // which is still valid.
145                Ok(self.ranges.clone())
146            }
147
148            SlidingSyncListRequestGeneratorKind::Paging {
149                number_of_fetched_rooms,
150                batch_size,
151                maximum_number_of_rooms_to_fetch,
152                requested_end,
153                ..
154            } => {
155                // In paging-mode, range starts at the number of fetched rooms. Since ranges are
156                // inclusive, and since the number of fetched rooms starts at 1,
157                // not at 0, there is no need to add 1 here.
158                let range_start = number_of_fetched_rooms;
159                let range_desired_size = batch_size;
160
161                // Create a new range, and use it as the current set of ranges.
162                let next_range = create_range(
163                    *range_start,
164                    *range_desired_size,
165                    *maximum_number_of_rooms_to_fetch,
166                    maximum_number_of_rooms,
167                )?;
168
169                *requested_end = Some(*next_range.end());
170
171                Ok(vec![next_range])
172            }
173
174            SlidingSyncListRequestGeneratorKind::Growing {
175                number_of_fetched_rooms,
176                batch_size,
177                maximum_number_of_rooms_to_fetch,
178                requested_end,
179                ..
180            } => {
181                // In growing-mode, range always starts from 0. However, the end is growing by
182                // adding `batch_size` to the previous number of fetched rooms.
183                let range_start = 0;
184                let range_desired_size = number_of_fetched_rooms.saturating_add(*batch_size);
185
186                // Create a new range, and use it as the current set of ranges.
187                let next_range = create_range(
188                    range_start,
189                    range_desired_size,
190                    *maximum_number_of_rooms_to_fetch,
191                    maximum_number_of_rooms,
192                )?;
193
194                *requested_end = Some(*next_range.end());
195
196                Ok(vec![next_range])
197            }
198        }
199    }
200
201    /// Handle a sliding sync response, given a new maximum number of rooms.
202    pub(super) fn handle_response(
203        &mut self,
204        list_name: &str,
205        maximum_number_of_rooms: u32,
206    ) -> Result<SlidingSyncListLoadingState, Error> {
207        match &mut self.kind {
208            SlidingSyncListRequestGeneratorKind::Paging {
209                requested_end,
210                number_of_fetched_rooms,
211                fully_loaded,
212                maximum_number_of_rooms_to_fetch,
213                ..
214            }
215            | SlidingSyncListRequestGeneratorKind::Growing {
216                requested_end,
217                number_of_fetched_rooms,
218                fully_loaded,
219                maximum_number_of_rooms_to_fetch,
220                ..
221            } => {
222                let range_end = requested_end.ok_or_else(|| {
223                    Error::RequestGeneratorHasNotBeenInitialized(list_name.to_owned())
224                })?;
225
226                // Calculate the maximum bound for the range.
227                // At this step, the server has given us a maximum number of rooms for this
228                // list. That's our `range_maximum`.
229                let mut range_maximum = maximum_number_of_rooms;
230
231                // But maybe the user has defined a maximum number of rooms to fetch? In this
232                // case, let's take the minimum of the two.
233                if let Some(maximum_number_of_rooms_to_fetch) = maximum_number_of_rooms_to_fetch {
234                    range_maximum = min(range_maximum, *maximum_number_of_rooms_to_fetch);
235                }
236
237                // Finally, ranges are inclusive!
238                range_maximum = range_maximum.saturating_sub(1);
239
240                // Now, we know what the maximum bound for the range is.
241
242                // The current range hasn't reached its maximum, let's continue.
243                if range_end < range_maximum {
244                    // Update the number of fetched rooms forward. Do not forget that ranges are
245                    // inclusive, so let's add 1.
246                    *number_of_fetched_rooms = range_end.saturating_add(1);
247
248                    // The list is still not fully loaded.
249                    *fully_loaded = false;
250
251                    // Update the range to cover from 0 to `range_end`.
252                    self.ranges = vec![0..=range_end];
253
254                    // Finally, return the new state.
255                    Ok(SlidingSyncListLoadingState::PartiallyLoaded)
256                }
257                // Otherwise the current range has reached its maximum, we switched to `FullyLoaded`
258                // mode.
259                else {
260                    // The number of fetched rooms is set to the maximum too.
261                    *number_of_fetched_rooms = range_maximum;
262
263                    // We update the `fully_loaded` marker.
264                    *fully_loaded = true;
265
266                    // The range is covering the entire list, from 0 to its maximum.
267                    self.ranges = vec![0..=range_maximum];
268
269                    // Finally, let's update the list' state.
270                    Ok(SlidingSyncListLoadingState::FullyLoaded)
271                }
272            }
273
274            SlidingSyncListRequestGeneratorKind::Selective => {
275                // Selective mode always loads everything.
276                Ok(SlidingSyncListLoadingState::FullyLoaded)
277            }
278        }
279    }
280
281    /// Check whether the list is fully loaded.
282    pub fn is_fully_loaded(&self) -> bool {
283        match self.kind {
284            SlidingSyncListRequestGeneratorKind::Paging { fully_loaded, .. }
285            | SlidingSyncListRequestGeneratorKind::Growing { fully_loaded, .. } => fully_loaded,
286            SlidingSyncListRequestGeneratorKind::Selective => true,
287        }
288    }
289
290    /// Check whether this request generator is of kind
291    /// [`SlidingSyncListRequestGeneratorKind::Selective`].
292    #[cfg(test)]
293    pub fn is_selective(&self) -> bool {
294        matches!(self.kind, SlidingSyncListRequestGeneratorKind::Selective)
295    }
296}
297
298fn create_range(
299    start: u32,
300    desired_size: u32,
301    maximum_number_of_rooms_to_fetch: Option<u32>,
302    maximum_number_of_rooms: Option<u32>,
303) -> Result<Range, Error> {
304    // Calculate the range.
305    // The `start` bound is given. Let's calculate the `end` bound.
306
307    // The `end`, by default, is `start` + `desired_size`.
308    let mut end = start + desired_size;
309
310    // But maybe the user has defined a maximum number of rooms to fetch? In this
311    // case, take the minimum of the two.
312    if let Some(maximum_number_of_rooms_to_fetch) = maximum_number_of_rooms_to_fetch {
313        end = min(end, maximum_number_of_rooms_to_fetch);
314    }
315
316    // But there is more! The server can tell us what is the maximum number of rooms
317    // fulfilling a particular list. For example, if the server says there is 42
318    // rooms for a particular list, with a `start` of 40 and a `batch_size` of 20,
319    // the range must be capped to `[40; 42]`; the range `[40; 60]` would be invalid
320    // and could be rejected by the server.
321    if let Some(maximum_number_of_rooms) = maximum_number_of_rooms {
322        end = min(end, maximum_number_of_rooms);
323    }
324
325    // Finally, because the bounds of the range are inclusive, 1 is subtracted.
326    end = end.saturating_sub(1);
327
328    // Make sure `start` is smaller than `end`. It can happen if `start` is greater
329    // than `maximum_number_of_rooms_to_fetch` or `maximum_number_of_rooms`.
330    if start > end {
331        return Err(Error::InvalidRange { start, end });
332    }
333
334    Ok(Range::new(start, end))
335}
336
337#[cfg(test)]
338mod tests {
339    use std::ops::{Not, RangeInclusive};
340
341    use assert_matches::assert_matches;
342
343    use super::{
344        SlidingSyncListRequestGenerator, SlidingSyncListRequestGeneratorKind, create_range,
345    };
346    use crate::{SlidingSyncMode, sliding_sync::Error};
347
348    #[test]
349    fn test_create_range_from() {
350        // From 0, we want 100 items.
351        assert_matches!(create_range(0, 100, None, None), Ok(range) if range == RangeInclusive::new(0, 99));
352
353        // From 100, we want 100 items.
354        assert_matches!(create_range(100, 100, None, None), Ok(range) if range == RangeInclusive::new(100, 199));
355
356        // From 0, we want 100 items, but there is a maximum number of rooms to fetch
357        // defined at 50.
358        assert_matches!(create_range(0, 100, Some(50), None), Ok(range) if range == RangeInclusive::new(0, 49));
359
360        // From 49, we want 100 items, but there is a maximum number of rooms to fetch
361        // defined at 50. There is 1 item to load.
362        assert_matches!(create_range(49, 100, Some(50), None), Ok(range) if range == RangeInclusive::new(49, 49));
363
364        // From 50, we want 100 items, but there is a maximum number of rooms to fetch
365        // defined at 50.
366        assert_matches!(
367            create_range(50, 100, Some(50), None),
368            Err(Error::InvalidRange { start: 50, end: 49 })
369        );
370
371        // From 0, we want 100 items, but there is a maximum number of rooms defined at
372        // 50.
373        assert_matches!(create_range(0, 100, None, Some(50)), Ok(range) if range == RangeInclusive::new(0, 49));
374
375        // From 49, we want 100 items, but there is a maximum number of rooms defined at
376        // 50. There is 1 item to load.
377        assert_matches!(create_range(49, 100, None, Some(50)), Ok(range) if range == RangeInclusive::new(49, 49));
378
379        // From 50, we want 100 items, but there is a maximum number of rooms defined at
380        // 50.
381        assert_matches!(
382            create_range(50, 100, None, Some(50)),
383            Err(Error::InvalidRange { start: 50, end: 49 })
384        );
385
386        // From 0, we want 100 items, but there is a maximum number of rooms to fetch
387        // defined at 75, and a maximum number of rooms defined at 50.
388        assert_matches!(create_range(0, 100, Some(75), Some(50)), Ok(range) if range == RangeInclusive::new(0, 49));
389
390        // From 0, we want 100 items, but there is a maximum number of rooms to fetch
391        // defined at 50, and a maximum number of rooms defined at 75.
392        assert_matches!(create_range(0, 100, Some(50), Some(75)), Ok(range) if range == RangeInclusive::new(0, 49));
393    }
394
395    #[test]
396    fn test_request_generator_selective_from_sync_mode() {
397        let sync_mode = SlidingSyncMode::new_selective();
398        let request_generator = SlidingSyncListRequestGenerator::new(sync_mode.into());
399
400        assert!(request_generator.ranges.is_empty());
401        assert_eq!(request_generator.kind, SlidingSyncListRequestGeneratorKind::Selective);
402        assert!(request_generator.is_selective());
403    }
404
405    #[test]
406    fn test_request_generator_paging_from_sync_mode() {
407        let sync_mode = SlidingSyncMode::new_paging(1).maximum_number_of_rooms_to_fetch(2);
408        let request_generator = SlidingSyncListRequestGenerator::new(sync_mode.into());
409
410        assert!(request_generator.ranges.is_empty());
411        assert_eq!(
412            request_generator.kind,
413            SlidingSyncListRequestGeneratorKind::Paging {
414                batch_size: 1,
415                maximum_number_of_rooms_to_fetch: Some(2),
416                number_of_fetched_rooms: 0,
417                fully_loaded: false,
418                requested_end: None,
419            }
420        );
421        assert!(request_generator.is_selective().not());
422    }
423
424    #[test]
425    fn test_request_generator_growing_from_sync_mode() {
426        let sync_mode = SlidingSyncMode::new_growing(1).maximum_number_of_rooms_to_fetch(2);
427        let request_generator = SlidingSyncListRequestGenerator::new(sync_mode.into());
428
429        assert!(request_generator.ranges.is_empty());
430        assert_eq!(
431            request_generator.kind,
432            SlidingSyncListRequestGeneratorKind::Growing {
433                batch_size: 1,
434                maximum_number_of_rooms_to_fetch: Some(2),
435                number_of_fetched_rooms: 0,
436                fully_loaded: false,
437                requested_end: None,
438            }
439        );
440        assert!(request_generator.is_selective().not());
441    }
442}