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());
}
}