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}