matrix_sdk/sliding_sync/list/
request_generator.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
//! The logic to generate Sliding Sync list requests.
//!
//! Depending on the [`SlidingSyncMode`], the generated requests aren't the
//! same.
//!
//! In [`SlidingSyncMode::Selective`], it's pretty straightforward:
//!
//! * There is a set of ranges,
//! * Each request asks to load the particular ranges.
//!
//! In [`SlidingSyncMode::Paging`]:
//!
//! * There is a `batch_size`,
//! * Each request asks to load a new successive range containing exactly
//!   `batch_size` rooms.
//!
//! In [`SlidingSyncMode::Growing]:
//!
//! * There is a `batch_size`,
//! * Each request asks to load a new range, always starting from 0, but where
//!   the end is incremented by `batch_size` every time.
//!
//! The number of rooms to load is capped by a `maximum_number_of_rooms`, i.e.
//! the real number of rooms it is possible to load. This value comes from the
//! server.
//!
//! The number of rooms to load can _also_ be capped by the
//! `maximum_number_of_rooms_to_fetch`, i.e. a user-specified limit representing
//! the maximum number of rooms the user actually wants to load.

use std::cmp::min;

use super::{Range, Ranges, SlidingSyncMode};
use crate::{sliding_sync::Error, SlidingSyncListLoadingState};

/// The kind of request generator.
#[derive(Debug, PartialEq)]
pub(super) enum SlidingSyncListRequestGeneratorKind {
    /// Growing-mode (see [`SlidingSyncMode`]).
    Growing {
        /// Size of the batch, used to grow the range to fetch more rooms.
        batch_size: u32,
        /// Maximum number of rooms to fetch (see
        /// [`SlidingSyncList::full_sync_maximum_number_of_rooms_to_fetch`]).
        maximum_number_of_rooms_to_fetch: Option<u32>,
        /// Number of rooms that have been already fetched.
        number_of_fetched_rooms: u32,
        /// Whether all rooms have been loaded.
        fully_loaded: bool,
        /// End range requested in the previous request.
        requested_end: Option<u32>,
    },

    /// Paging-mode (see [`SlidingSyncMode`]).
    Paging {
        /// Size of the batch, used to grow the range to fetch more rooms.
        batch_size: u32,
        /// Maximum number of rooms to fetch (see
        /// [`SlidingSyncList::full_sync_maximum_number_of_rooms_to_fetch`]).
        maximum_number_of_rooms_to_fetch: Option<u32>,
        /// Number of rooms that have been already fetched.
        number_of_fetched_rooms: u32,
        /// Whether all romms have been loaded.
        fully_loaded: bool,
        /// End range requested in the previous request.
        requested_end: Option<u32>,
    },

    /// Selective-mode (see [`SlidingSyncMode`]).
    Selective,
}

/// A request generator for [`SlidingSyncList`].
#[derive(Debug)]
pub(in super::super) struct SlidingSyncListRequestGenerator {
    /// The current ranges used by this request generator.
    ///
    /// Note there's only one range in the `Growing` and `Paging` mode.
    ranges: Ranges,

    /// The kind of request generator.
    kind: SlidingSyncListRequestGeneratorKind,
}

impl SlidingSyncListRequestGenerator {
    /// Create a new request generator from scratch, given a sync mode.
    pub(super) fn new(sync_mode: SlidingSyncMode) -> Self {
        match sync_mode {
            SlidingSyncMode::Paging { batch_size, maximum_number_of_rooms_to_fetch } => Self {
                ranges: Vec::new(),
                kind: SlidingSyncListRequestGeneratorKind::Paging {
                    batch_size,
                    maximum_number_of_rooms_to_fetch,
                    number_of_fetched_rooms: 0,
                    fully_loaded: false,
                    requested_end: None,
                },
            },

            SlidingSyncMode::Growing { batch_size, maximum_number_of_rooms_to_fetch } => Self {
                ranges: Vec::new(),
                kind: SlidingSyncListRequestGeneratorKind::Growing {
                    batch_size,
                    maximum_number_of_rooms_to_fetch,
                    number_of_fetched_rooms: 0,
                    fully_loaded: false,
                    requested_end: None,
                },
            },

            SlidingSyncMode::Selective { ranges } => {
                Self { ranges, kind: SlidingSyncListRequestGeneratorKind::Selective }
            }
        }
    }

    /// Check whether this request generator is of kind
    /// [`SlidingSyncListRequestGeneratorKind::Selective`].
    pub(super) fn is_selective(&self) -> bool {
        matches!(self.kind, SlidingSyncListRequestGeneratorKind::Selective)
    }

    /// Return a view on the ranges requested by this generator.
    ///
    /// For generators in the selective mode, this is the initial set of ranges.
    /// For growing and paginated generators, this is the range committed in the
    /// latest response received from the server.
    #[cfg(test)]
    pub(super) fn requested_ranges(&self) -> &[Range] {
        &self.ranges
    }

    /// Update internal state of the generator (namely, ranges) before the next
    /// sliding sync request.
    pub(super) fn generate_next_ranges(
        &mut self,
        maximum_number_of_rooms: Option<u32>,
    ) -> Result<Ranges, Error> {
        match &mut self.kind {
            // Cases where all rooms have been fully loaded.
            SlidingSyncListRequestGeneratorKind::Paging { fully_loaded: true, .. }
            | SlidingSyncListRequestGeneratorKind::Growing { fully_loaded: true, .. }
            | SlidingSyncListRequestGeneratorKind::Selective => {
                // Nothing to do: we already have the full ranges, return the existing ranges.
                // For the growing and paging modes, keep the current value of `requested_end`,
                // which is still valid.
                Ok(self.ranges.clone())
            }

            SlidingSyncListRequestGeneratorKind::Paging {
                number_of_fetched_rooms,
                batch_size,
                maximum_number_of_rooms_to_fetch,
                requested_end,
                ..
            } => {
                // In paging-mode, range starts at the number of fetched rooms. Since ranges are
                // inclusive, and since the number of fetched rooms starts at 1,
                // not at 0, there is no need to add 1 here.
                let range_start = number_of_fetched_rooms;
                let range_desired_size = batch_size;

                // Create a new range, and use it as the current set of ranges.
                let next_range = create_range(
                    *range_start,
                    *range_desired_size,
                    *maximum_number_of_rooms_to_fetch,
                    maximum_number_of_rooms,
                )?;

                *requested_end = Some(*next_range.end());

                Ok(vec![next_range])
            }

            SlidingSyncListRequestGeneratorKind::Growing {
                number_of_fetched_rooms,
                batch_size,
                maximum_number_of_rooms_to_fetch,
                requested_end,
                ..
            } => {
                // In growing-mode, range always starts from 0. However, the end is growing by
                // adding `batch_size` to the previous number of fetched rooms.
                let range_start = 0;
                let range_desired_size = number_of_fetched_rooms.saturating_add(*batch_size);

                // Create a new range, and use it as the current set of ranges.
                let next_range = create_range(
                    range_start,
                    range_desired_size,
                    *maximum_number_of_rooms_to_fetch,
                    maximum_number_of_rooms,
                )?;

                *requested_end = Some(*next_range.end());

                Ok(vec![next_range])
            }
        }
    }

    /// Handle a sliding sync response, given a new maximum number of rooms.
    pub(super) fn handle_response(
        &mut self,
        list_name: &str,
        maximum_number_of_rooms: u32,
    ) -> Result<SlidingSyncListLoadingState, Error> {
        match &mut self.kind {
            SlidingSyncListRequestGeneratorKind::Paging {
                requested_end,
                number_of_fetched_rooms,
                fully_loaded,
                maximum_number_of_rooms_to_fetch,
                ..
            }
            | SlidingSyncListRequestGeneratorKind::Growing {
                requested_end,
                number_of_fetched_rooms,
                fully_loaded,
                maximum_number_of_rooms_to_fetch,
                ..
            } => {
                let range_end = requested_end.ok_or_else(|| {
                    Error::RequestGeneratorHasNotBeenInitialized(list_name.to_owned())
                })?;

                // Calculate the maximum bound for the range.
                // At this step, the server has given us a maximum number of rooms for this
                // list. That's our `range_maximum`.
                let mut range_maximum = maximum_number_of_rooms;

                // But maybe the user has defined a maximum number of rooms to fetch? In this
                // case, let's take the minimum of the two.
                if let Some(maximum_number_of_rooms_to_fetch) = maximum_number_of_rooms_to_fetch {
                    range_maximum = min(range_maximum, *maximum_number_of_rooms_to_fetch);
                }

                // Finally, ranges are inclusive!
                range_maximum = range_maximum.saturating_sub(1);

                // Now, we know what the maximum bound for the range is.

                // The current range hasn't reached its maximum, let's continue.
                if range_end < range_maximum {
                    // Update the number of fetched rooms forward. Do not forget that ranges are
                    // inclusive, so let's add 1.
                    *number_of_fetched_rooms = range_end.saturating_add(1);

                    // The list is still not fully loaded.
                    *fully_loaded = false;

                    // Update the range to cover from 0 to `range_end`.
                    self.ranges = vec![0..=range_end];

                    // Finally, return the new state.
                    Ok(SlidingSyncListLoadingState::PartiallyLoaded)
                }
                // Otherwise the current range has reached its maximum, we switched to `FullyLoaded`
                // mode.
                else {
                    // The number of fetched rooms is set to the maximum too.
                    *number_of_fetched_rooms = range_maximum;

                    // We update the `fully_loaded` marker.
                    *fully_loaded = true;

                    // The range is covering the entire list, from 0 to its maximum.
                    self.ranges = vec![0..=range_maximum];

                    // Finally, let's update the list' state.
                    Ok(SlidingSyncListLoadingState::FullyLoaded)
                }
            }

            SlidingSyncListRequestGeneratorKind::Selective => {
                // Selective mode always loads everything.
                Ok(SlidingSyncListLoadingState::FullyLoaded)
            }
        }
    }

    #[cfg(test)]
    pub(super) fn is_fully_loaded(&self) -> bool {
        match self.kind {
            SlidingSyncListRequestGeneratorKind::Paging { fully_loaded, .. }
            | SlidingSyncListRequestGeneratorKind::Growing { fully_loaded, .. } => fully_loaded,
            SlidingSyncListRequestGeneratorKind::Selective => true,
        }
    }
}

fn create_range(
    start: u32,
    desired_size: u32,
    maximum_number_of_rooms_to_fetch: Option<u32>,
    maximum_number_of_rooms: Option<u32>,
) -> Result<Range, Error> {
    // Calculate the range.
    // The `start` bound is given. Let's calculate the `end` bound.

    // The `end`, by default, is `start` + `desired_size`.
    let mut end = start + desired_size;

    // But maybe the user has defined a maximum number of rooms to fetch? In this
    // case, take the minimum of the two.
    if let Some(maximum_number_of_rooms_to_fetch) = maximum_number_of_rooms_to_fetch {
        end = min(end, maximum_number_of_rooms_to_fetch);
    }

    // But there is more! The server can tell us what is the maximum number of rooms
    // fulfilling a particular list. For example, if the server says there is 42
    // rooms for a particular list, with a `start` of 40 and a `batch_size` of 20,
    // the range must be capped to `[40; 42]`; the range `[40; 60]` would be invalid
    // and could be rejected by the server.
    if let Some(maximum_number_of_rooms) = maximum_number_of_rooms {
        end = min(end, maximum_number_of_rooms);
    }

    // Finally, because the bounds of the range are inclusive, 1 is subtracted.
    end = end.saturating_sub(1);

    // Make sure `start` is smaller than `end`. It can happen if `start` is greater
    // than `maximum_number_of_rooms_to_fetch` or `maximum_number_of_rooms`.
    if start > end {
        return Err(Error::InvalidRange { start, end });
    }

    Ok(Range::new(start, end))
}

#[cfg(test)]
mod tests {
    use std::ops::{Not, RangeInclusive};

    use assert_matches::assert_matches;

    use super::{
        create_range, SlidingSyncListRequestGenerator, SlidingSyncListRequestGeneratorKind,
    };
    use crate::{sliding_sync::Error, SlidingSyncMode};

    #[test]
    fn test_create_range_from() {
        // From 0, we want 100 items.
        assert_matches!(create_range(0, 100, None, None), Ok(range) if range == RangeInclusive::new(0, 99));

        // From 100, we want 100 items.
        assert_matches!(create_range(100, 100, None, None), Ok(range) if range == RangeInclusive::new(100, 199));

        // From 0, we want 100 items, but there is a maximum number of rooms to fetch
        // defined at 50.
        assert_matches!(create_range(0, 100, Some(50), None), Ok(range) if range == RangeInclusive::new(0, 49));

        // From 49, we want 100 items, but there is a maximum number of rooms to fetch
        // defined at 50. There is 1 item to load.
        assert_matches!(create_range(49, 100, Some(50), None), Ok(range) if range == RangeInclusive::new(49, 49));

        // From 50, we want 100 items, but there is a maximum number of rooms to fetch
        // defined at 50.
        assert_matches!(
            create_range(50, 100, Some(50), None),
            Err(Error::InvalidRange { start: 50, end: 49 })
        );

        // From 0, we want 100 items, but there is a maximum number of rooms defined at
        // 50.
        assert_matches!(create_range(0, 100, None, Some(50)), Ok(range) if range == RangeInclusive::new(0, 49));

        // From 49, we want 100 items, but there is a maximum number of rooms defined at
        // 50. There is 1 item to load.
        assert_matches!(create_range(49, 100, None, Some(50)), Ok(range) if range == RangeInclusive::new(49, 49));

        // From 50, we want 100 items, but there is a maximum number of rooms defined at
        // 50.
        assert_matches!(
            create_range(50, 100, None, Some(50)),
            Err(Error::InvalidRange { start: 50, end: 49 })
        );

        // From 0, we want 100 items, but there is a maximum number of rooms to fetch
        // defined at 75, and a maximum number of rooms defined at 50.
        assert_matches!(create_range(0, 100, Some(75), Some(50)), Ok(range) if range == RangeInclusive::new(0, 49));

        // From 0, we want 100 items, but there is a maximum number of rooms to fetch
        // defined at 50, and a maximum number of rooms defined at 75.
        assert_matches!(create_range(0, 100, Some(50), Some(75)), Ok(range) if range == RangeInclusive::new(0, 49));
    }

    #[test]
    fn test_request_generator_selective_from_sync_mode() {
        let sync_mode = SlidingSyncMode::new_selective();
        let request_generator = SlidingSyncListRequestGenerator::new(sync_mode.into());

        assert!(request_generator.ranges.is_empty());
        assert_eq!(request_generator.kind, SlidingSyncListRequestGeneratorKind::Selective);
        assert!(request_generator.is_selective());
    }

    #[test]
    fn test_request_generator_paging_from_sync_mode() {
        let sync_mode = SlidingSyncMode::new_paging(1).maximum_number_of_rooms_to_fetch(2);
        let request_generator = SlidingSyncListRequestGenerator::new(sync_mode.into());

        assert!(request_generator.ranges.is_empty());
        assert_eq!(
            request_generator.kind,
            SlidingSyncListRequestGeneratorKind::Paging {
                batch_size: 1,
                maximum_number_of_rooms_to_fetch: Some(2),
                number_of_fetched_rooms: 0,
                fully_loaded: false,
                requested_end: None,
            }
        );
        assert!(request_generator.is_selective().not());
    }

    #[test]
    fn test_request_generator_growing_from_sync_mode() {
        let sync_mode = SlidingSyncMode::new_growing(1).maximum_number_of_rooms_to_fetch(2);
        let request_generator = SlidingSyncListRequestGenerator::new(sync_mode.into());

        assert!(request_generator.ranges.is_empty());
        assert_eq!(
            request_generator.kind,
            SlidingSyncListRequestGeneratorKind::Growing {
                batch_size: 1,
                maximum_number_of_rooms_to_fetch: Some(2),
                number_of_fetched_rooms: 0,
                fully_loaded: false,
                requested_end: None,
            }
        );
        assert!(request_generator.is_selective().not());
    }
}