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::{sliding_sync::Error, SlidingSyncListLoadingState};
35
36/// The kind of request generator.
37#[derive(Debug, PartialEq)]
38pub(super) 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(in super::super) 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    /// Check whether this request generator is of kind
118    /// [`SlidingSyncListRequestGeneratorKind::Selective`].
119    pub(super) fn is_selective(&self) -> bool {
120        matches!(self.kind, SlidingSyncListRequestGeneratorKind::Selective)
121    }
122
123    /// Return a view on the ranges requested by this generator.
124    ///
125    /// For generators in the selective mode, this is the initial set of ranges.
126    /// For growing and paginated generators, this is the range committed in the
127    /// latest response received from the server.
128    #[cfg(test)]
129    pub(super) fn requested_ranges(&self) -> &[Range] {
130        &self.ranges
131    }
132
133    /// Update internal state of the generator (namely, ranges) before the next
134    /// sliding sync request.
135    pub(super) fn generate_next_ranges(
136        &mut self,
137        maximum_number_of_rooms: Option<u32>,
138    ) -> Result<Ranges, Error> {
139        match &mut self.kind {
140            // Cases where all rooms have been fully loaded.
141            SlidingSyncListRequestGeneratorKind::Paging { fully_loaded: true, .. }
142            | SlidingSyncListRequestGeneratorKind::Growing { fully_loaded: true, .. }
143            | SlidingSyncListRequestGeneratorKind::Selective => {
144                // Nothing to do: we already have the full ranges, return the existing ranges.
145                // For the growing and paging modes, keep the current value of `requested_end`,
146                // which is still valid.
147                Ok(self.ranges.clone())
148            }
149
150            SlidingSyncListRequestGeneratorKind::Paging {
151                number_of_fetched_rooms,
152                batch_size,
153                maximum_number_of_rooms_to_fetch,
154                requested_end,
155                ..
156            } => {
157                // In paging-mode, range starts at the number of fetched rooms. Since ranges are
158                // inclusive, and since the number of fetched rooms starts at 1,
159                // not at 0, there is no need to add 1 here.
160                let range_start = number_of_fetched_rooms;
161                let range_desired_size = batch_size;
162
163                // Create a new range, and use it as the current set of ranges.
164                let next_range = create_range(
165                    *range_start,
166                    *range_desired_size,
167                    *maximum_number_of_rooms_to_fetch,
168                    maximum_number_of_rooms,
169                )?;
170
171                *requested_end = Some(*next_range.end());
172
173                Ok(vec![next_range])
174            }
175
176            SlidingSyncListRequestGeneratorKind::Growing {
177                number_of_fetched_rooms,
178                batch_size,
179                maximum_number_of_rooms_to_fetch,
180                requested_end,
181                ..
182            } => {
183                // In growing-mode, range always starts from 0. However, the end is growing by
184                // adding `batch_size` to the previous number of fetched rooms.
185                let range_start = 0;
186                let range_desired_size = number_of_fetched_rooms.saturating_add(*batch_size);
187
188                // Create a new range, and use it as the current set of ranges.
189                let next_range = create_range(
190                    range_start,
191                    range_desired_size,
192                    *maximum_number_of_rooms_to_fetch,
193                    maximum_number_of_rooms,
194                )?;
195
196                *requested_end = Some(*next_range.end());
197
198                Ok(vec![next_range])
199            }
200        }
201    }
202
203    /// Handle a sliding sync response, given a new maximum number of rooms.
204    pub(super) fn handle_response(
205        &mut self,
206        list_name: &str,
207        maximum_number_of_rooms: u32,
208    ) -> Result<SlidingSyncListLoadingState, Error> {
209        match &mut self.kind {
210            SlidingSyncListRequestGeneratorKind::Paging {
211                requested_end,
212                number_of_fetched_rooms,
213                fully_loaded,
214                maximum_number_of_rooms_to_fetch,
215                ..
216            }
217            | SlidingSyncListRequestGeneratorKind::Growing {
218                requested_end,
219                number_of_fetched_rooms,
220                fully_loaded,
221                maximum_number_of_rooms_to_fetch,
222                ..
223            } => {
224                let range_end = requested_end.ok_or_else(|| {
225                    Error::RequestGeneratorHasNotBeenInitialized(list_name.to_owned())
226                })?;
227
228                // Calculate the maximum bound for the range.
229                // At this step, the server has given us a maximum number of rooms for this
230                // list. That's our `range_maximum`.
231                let mut range_maximum = maximum_number_of_rooms;
232
233                // But maybe the user has defined a maximum number of rooms to fetch? In this
234                // case, let's take the minimum of the two.
235                if let Some(maximum_number_of_rooms_to_fetch) = maximum_number_of_rooms_to_fetch {
236                    range_maximum = min(range_maximum, *maximum_number_of_rooms_to_fetch);
237                }
238
239                // Finally, ranges are inclusive!
240                range_maximum = range_maximum.saturating_sub(1);
241
242                // Now, we know what the maximum bound for the range is.
243
244                // The current range hasn't reached its maximum, let's continue.
245                if range_end < range_maximum {
246                    // Update the number of fetched rooms forward. Do not forget that ranges are
247                    // inclusive, so let's add 1.
248                    *number_of_fetched_rooms = range_end.saturating_add(1);
249
250                    // The list is still not fully loaded.
251                    *fully_loaded = false;
252
253                    // Update the range to cover from 0 to `range_end`.
254                    self.ranges = vec![0..=range_end];
255
256                    // Finally, return the new state.
257                    Ok(SlidingSyncListLoadingState::PartiallyLoaded)
258                }
259                // Otherwise the current range has reached its maximum, we switched to `FullyLoaded`
260                // mode.
261                else {
262                    // The number of fetched rooms is set to the maximum too.
263                    *number_of_fetched_rooms = range_maximum;
264
265                    // We update the `fully_loaded` marker.
266                    *fully_loaded = true;
267
268                    // The range is covering the entire list, from 0 to its maximum.
269                    self.ranges = vec![0..=range_maximum];
270
271                    // Finally, let's update the list' state.
272                    Ok(SlidingSyncListLoadingState::FullyLoaded)
273                }
274            }
275
276            SlidingSyncListRequestGeneratorKind::Selective => {
277                // Selective mode always loads everything.
278                Ok(SlidingSyncListLoadingState::FullyLoaded)
279            }
280        }
281    }
282
283    #[cfg(test)]
284    pub(super) fn is_fully_loaded(&self) -> bool {
285        match self.kind {
286            SlidingSyncListRequestGeneratorKind::Paging { fully_loaded, .. }
287            | SlidingSyncListRequestGeneratorKind::Growing { fully_loaded, .. } => fully_loaded,
288            SlidingSyncListRequestGeneratorKind::Selective => true,
289        }
290    }
291}
292
293fn create_range(
294    start: u32,
295    desired_size: u32,
296    maximum_number_of_rooms_to_fetch: Option<u32>,
297    maximum_number_of_rooms: Option<u32>,
298) -> Result<Range, Error> {
299    // Calculate the range.
300    // The `start` bound is given. Let's calculate the `end` bound.
301
302    // The `end`, by default, is `start` + `desired_size`.
303    let mut end = start + desired_size;
304
305    // But maybe the user has defined a maximum number of rooms to fetch? In this
306    // case, take the minimum of the two.
307    if let Some(maximum_number_of_rooms_to_fetch) = maximum_number_of_rooms_to_fetch {
308        end = min(end, maximum_number_of_rooms_to_fetch);
309    }
310
311    // But there is more! The server can tell us what is the maximum number of rooms
312    // fulfilling a particular list. For example, if the server says there is 42
313    // rooms for a particular list, with a `start` of 40 and a `batch_size` of 20,
314    // the range must be capped to `[40; 42]`; the range `[40; 60]` would be invalid
315    // and could be rejected by the server.
316    if let Some(maximum_number_of_rooms) = maximum_number_of_rooms {
317        end = min(end, maximum_number_of_rooms);
318    }
319
320    // Finally, because the bounds of the range are inclusive, 1 is subtracted.
321    end = end.saturating_sub(1);
322
323    // Make sure `start` is smaller than `end`. It can happen if `start` is greater
324    // than `maximum_number_of_rooms_to_fetch` or `maximum_number_of_rooms`.
325    if start > end {
326        return Err(Error::InvalidRange { start, end });
327    }
328
329    Ok(Range::new(start, end))
330}
331
332#[cfg(test)]
333mod tests {
334    use std::ops::{Not, RangeInclusive};
335
336    use assert_matches::assert_matches;
337
338    use super::{
339        create_range, SlidingSyncListRequestGenerator, SlidingSyncListRequestGeneratorKind,
340    };
341    use crate::{sliding_sync::Error, SlidingSyncMode};
342
343    #[test]
344    fn test_create_range_from() {
345        // From 0, we want 100 items.
346        assert_matches!(create_range(0, 100, None, None), Ok(range) if range == RangeInclusive::new(0, 99));
347
348        // From 100, we want 100 items.
349        assert_matches!(create_range(100, 100, None, None), Ok(range) if range == RangeInclusive::new(100, 199));
350
351        // From 0, we want 100 items, but there is a maximum number of rooms to fetch
352        // defined at 50.
353        assert_matches!(create_range(0, 100, Some(50), None), Ok(range) if range == RangeInclusive::new(0, 49));
354
355        // From 49, we want 100 items, but there is a maximum number of rooms to fetch
356        // defined at 50. There is 1 item to load.
357        assert_matches!(create_range(49, 100, Some(50), None), Ok(range) if range == RangeInclusive::new(49, 49));
358
359        // From 50, we want 100 items, but there is a maximum number of rooms to fetch
360        // defined at 50.
361        assert_matches!(
362            create_range(50, 100, Some(50), None),
363            Err(Error::InvalidRange { start: 50, end: 49 })
364        );
365
366        // From 0, we want 100 items, but there is a maximum number of rooms defined at
367        // 50.
368        assert_matches!(create_range(0, 100, None, Some(50)), Ok(range) if range == RangeInclusive::new(0, 49));
369
370        // From 49, we want 100 items, but there is a maximum number of rooms defined at
371        // 50. There is 1 item to load.
372        assert_matches!(create_range(49, 100, None, Some(50)), Ok(range) if range == RangeInclusive::new(49, 49));
373
374        // From 50, we want 100 items, but there is a maximum number of rooms defined at
375        // 50.
376        assert_matches!(
377            create_range(50, 100, None, Some(50)),
378            Err(Error::InvalidRange { start: 50, end: 49 })
379        );
380
381        // From 0, we want 100 items, but there is a maximum number of rooms to fetch
382        // defined at 75, and a maximum number of rooms defined at 50.
383        assert_matches!(create_range(0, 100, Some(75), Some(50)), Ok(range) if range == RangeInclusive::new(0, 49));
384
385        // From 0, we want 100 items, but there is a maximum number of rooms to fetch
386        // defined at 50, and a maximum number of rooms defined at 75.
387        assert_matches!(create_range(0, 100, Some(50), Some(75)), Ok(range) if range == RangeInclusive::new(0, 49));
388    }
389
390    #[test]
391    fn test_request_generator_selective_from_sync_mode() {
392        let sync_mode = SlidingSyncMode::new_selective();
393        let request_generator = SlidingSyncListRequestGenerator::new(sync_mode.into());
394
395        assert!(request_generator.ranges.is_empty());
396        assert_eq!(request_generator.kind, SlidingSyncListRequestGeneratorKind::Selective);
397        assert!(request_generator.is_selective());
398    }
399
400    #[test]
401    fn test_request_generator_paging_from_sync_mode() {
402        let sync_mode = SlidingSyncMode::new_paging(1).maximum_number_of_rooms_to_fetch(2);
403        let request_generator = SlidingSyncListRequestGenerator::new(sync_mode.into());
404
405        assert!(request_generator.ranges.is_empty());
406        assert_eq!(
407            request_generator.kind,
408            SlidingSyncListRequestGeneratorKind::Paging {
409                batch_size: 1,
410                maximum_number_of_rooms_to_fetch: Some(2),
411                number_of_fetched_rooms: 0,
412                fully_loaded: false,
413                requested_end: None,
414            }
415        );
416        assert!(request_generator.is_selective().not());
417    }
418
419    #[test]
420    fn test_request_generator_growing_from_sync_mode() {
421        let sync_mode = SlidingSyncMode::new_growing(1).maximum_number_of_rooms_to_fetch(2);
422        let request_generator = SlidingSyncListRequestGenerator::new(sync_mode.into());
423
424        assert!(request_generator.ranges.is_empty());
425        assert_eq!(
426            request_generator.kind,
427            SlidingSyncListRequestGeneratorKind::Growing {
428                batch_size: 1,
429                maximum_number_of_rooms_to_fetch: Some(2),
430                number_of_fetched_rooms: 0,
431                fully_loaded: false,
432                requested_end: None,
433            }
434        );
435        assert!(request_generator.is_selective().not());
436    }
437}