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}