use std::sync::{Arc, RwLock};
use eyeball_im::VectorDiff;
use super::{
Position,
updates::{ReaderToken, Update, UpdatesInner},
};
use crate::linked_chunk::{ChunkMetadata, UpdateToVectorDiff};
#[derive(Debug)]
pub struct OrderTracker<Item, Gap> {
updates: Arc<RwLock<UpdatesInner<Item, Gap>>>,
token: ReaderToken,
mapper: UpdateToVectorDiff<Item, NullAccumulator<Item>>,
}
struct NullAccumulator<Item> {
_phantom: std::marker::PhantomData<Item>,
}
#[cfg(not(tarpaulin_include))]
impl<Item> std::fmt::Debug for NullAccumulator<Item> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("NullAccumulator")
}
}
impl<Item> super::UpdatesAccumulator<Item> for NullAccumulator<Item> {
fn new(_num_updates_hint: usize) -> Self {
Self { _phantom: std::marker::PhantomData }
}
}
impl<Item> Extend<VectorDiff<Item>> for NullAccumulator<Item> {
fn extend<T: IntoIterator<Item = VectorDiff<Item>>>(&mut self, _iter: T) {
}
}
impl<Item, Gap> OrderTracker<Item, Gap>
where
Item: Clone,
{
pub(super) fn new(
updates: Arc<RwLock<UpdatesInner<Item, Gap>>>,
token: ReaderToken,
all_chunks_metadata: Vec<ChunkMetadata>,
) -> Self {
{
let mut updates = updates.write().unwrap();
let _ = updates.take_with_token(token);
}
Self { updates, token, mapper: UpdateToVectorDiff::from_metadata(all_chunks_metadata) }
}
pub fn flush_updates(&mut self, inhibit: bool) {
if inhibit {
let _ = self.updates.write().unwrap().take_with_token(self.token);
} else {
let mut updater = self.updates.write().unwrap();
let updates = updater.take_with_token(self.token);
let _ = self.mapper.map(updates);
}
}
pub fn map_updates(&mut self, updates: &[Update<Item, Gap>]) {
let _ = self.mapper.map(updates);
}
pub fn ordering(&self, event_pos: Position) -> Option<usize> {
debug_assert!(self.updates.read().unwrap().is_reader_up_to_date(self.token));
let mut ordering = 0;
for (chunk_id, chunk_length) in &self.mapper.chunks {
if *chunk_id == event_pos.chunk_identifier() {
let offset_within_chunk = event_pos.index();
if offset_within_chunk >= *chunk_length {
return None;
}
return Some(ordering + offset_within_chunk);
}
ordering += *chunk_length;
}
None
}
}
#[cfg(test)]
mod tests {
use assert_matches::assert_matches;
use matrix_sdk_test_macros::async_test;
use crate::linked_chunk::{
ChunkContent, ChunkIdentifier, ChunkIdentifierGenerator, ChunkMetadata, LinkedChunk,
OrderTracker, Position, RawChunk, Update, lazy_loader::from_last_chunk,
};
#[async_test]
async fn test_linked_chunk_without_update_history_no_tracking() {
let mut linked_chunk = LinkedChunk::<10, char, ()>::new();
assert_matches!(linked_chunk.order_tracker(None), None);
}
fn assert_order_fully_loaded(
linked_chunk: &LinkedChunk<3, char, ()>,
tracker: &OrderTracker<char, ()>,
) {
assert_order(linked_chunk, tracker, 0);
}
fn assert_order(
linked_chunk: &LinkedChunk<3, char, ()>,
tracker: &OrderTracker<char, ()>,
offset: usize,
) {
for (i, (item_pos, _value)) in linked_chunk.items().enumerate() {
assert_eq!(tracker.ordering(item_pos), Some(i + offset));
}
}
#[async_test]
async fn test_non_lazy_updates() {
let mut linked_chunk = LinkedChunk::<3, _, _>::new_with_update_history();
let mut tracker = linked_chunk.order_tracker(None).unwrap();
{
linked_chunk.push_items_back(['a', 'b', 'c']);
tracker.flush_updates(false);
assert_order_fully_loaded(&linked_chunk, &tracker);
}
{
linked_chunk.push_gap_back(());
tracker.flush_updates(false);
assert_order_fully_loaded(&linked_chunk, &tracker);
}
{
let pos_b = linked_chunk.item_position(|c| *c == 'b').unwrap();
linked_chunk.insert_items_at(pos_b, ['d', 'e']).unwrap();
tracker.flush_updates(false);
assert_order_fully_loaded(&linked_chunk, &tracker);
}
{
let c_pos = linked_chunk.item_position(|c| *c == 'c').unwrap();
linked_chunk.insert_gap_at((), c_pos).unwrap();
tracker.flush_updates(false);
assert_order_fully_loaded(&linked_chunk, &tracker);
}
{
let last_gap =
linked_chunk.rchunks().filter(|c| c.is_gap()).last().unwrap().identifier();
linked_chunk.replace_gap_at(['f', 'g'], last_gap).unwrap();
tracker.flush_updates(false);
assert_order_fully_loaded(&linked_chunk, &tracker);
}
{
let a_pos = linked_chunk.item_position(|c| *c == 'd').unwrap();
linked_chunk.remove_item_at(a_pos).unwrap();
tracker.flush_updates(false);
assert_order_fully_loaded(&linked_chunk, &tracker);
}
{
let b_pos = linked_chunk.item_position(|c| *c == 'e').unwrap();
linked_chunk.replace_item_at(b_pos, 'E').unwrap();
tracker.flush_updates(false);
assert_order_fully_loaded(&linked_chunk, &tracker);
}
{
linked_chunk.clear();
tracker.flush_updates(false);
assert_eq!(tracker.ordering(Position::new(ChunkIdentifier::new(0), 0)), None);
assert_eq!(tracker.ordering(Position::new(ChunkIdentifier::new(3), 0)), None);
}
}
#[async_test]
async fn test_lazy_loading() {
let db_metadata = vec![
ChunkMetadata {
previous: None,
identifier: ChunkIdentifier(0),
next: Some(ChunkIdentifier(1)),
num_items: 3,
},
ChunkMetadata {
previous: Some(ChunkIdentifier(0)),
identifier: ChunkIdentifier(1),
next: Some(ChunkIdentifier(2)),
num_items: 0,
},
ChunkMetadata {
previous: Some(ChunkIdentifier(1)),
identifier: ChunkIdentifier(2),
next: Some(ChunkIdentifier(3)),
num_items: 3,
},
ChunkMetadata {
previous: Some(ChunkIdentifier(2)),
identifier: ChunkIdentifier(3),
next: None,
num_items: 1,
},
];
let mut linked_chunk = from_last_chunk::<3, _, ()>(
Some(RawChunk {
content: ChunkContent::Items(vec!['g']),
previous: Some(ChunkIdentifier(2)),
identifier: ChunkIdentifier(3),
next: None,
}),
ChunkIdentifierGenerator::new_from_previous_chunk_identifier(ChunkIdentifier(3)),
)
.expect("could recreate the linked chunk")
.expect("the linked chunk isn't empty");
let tracker = linked_chunk.order_tracker(Some(db_metadata)).unwrap();
assert_eq!(tracker.ordering(Position::new(ChunkIdentifier::new(0), 0)), Some(0));
assert_eq!(tracker.ordering(Position::new(ChunkIdentifier::new(0), 1)), Some(1));
assert_eq!(tracker.ordering(Position::new(ChunkIdentifier::new(0), 2)), Some(2));
assert_eq!(tracker.ordering(Position::new(ChunkIdentifier::new(0), 42)), None);
assert_eq!(tracker.ordering(Position::new(ChunkIdentifier::new(1), 0)), None);
assert_eq!(tracker.ordering(Position::new(ChunkIdentifier::new(1), 42)), None);
assert_eq!(tracker.ordering(Position::new(ChunkIdentifier::new(2), 0)), Some(3));
assert_eq!(tracker.ordering(Position::new(ChunkIdentifier::new(2), 1)), Some(4));
assert_eq!(tracker.ordering(Position::new(ChunkIdentifier::new(2), 2)), Some(5));
assert_eq!(tracker.ordering(Position::new(ChunkIdentifier::new(2), 3)), None);
assert_eq!(tracker.ordering(Position::new(ChunkIdentifier::new(3), 0)), Some(6));
assert_eq!(tracker.ordering(Position::new(ChunkIdentifier::new(3), 1)), None);
}
#[async_test]
async fn test_lazy_updates() {
let db_metadata = vec![
ChunkMetadata {
previous: None,
identifier: ChunkIdentifier(0),
next: Some(ChunkIdentifier(1)),
num_items: 2,
},
ChunkMetadata {
previous: Some(ChunkIdentifier(0)),
identifier: ChunkIdentifier(1),
next: Some(ChunkIdentifier(2)),
num_items: 0,
},
ChunkMetadata {
previous: Some(ChunkIdentifier(1)),
identifier: ChunkIdentifier(2),
next: Some(ChunkIdentifier(3)),
num_items: 3,
},
ChunkMetadata {
previous: Some(ChunkIdentifier(2)),
identifier: ChunkIdentifier(3),
next: None,
num_items: 1,
},
];
let mut linked_chunk = from_last_chunk(
Some(RawChunk {
content: ChunkContent::Items(vec!['g']),
previous: Some(ChunkIdentifier(2)),
identifier: ChunkIdentifier(3),
next: None,
}),
ChunkIdentifierGenerator::new_from_previous_chunk_identifier(ChunkIdentifier(3)),
)
.expect("could recreate the linked chunk")
.expect("the linked chunk isn't empty");
let mut tracker = linked_chunk.order_tracker(Some(db_metadata)).unwrap();
{
assert_eq!(tracker.ordering(Position::new(ChunkIdentifier::new(0), 1)), Some(1));
assert_eq!(tracker.ordering(Position::new(ChunkIdentifier::new(3), 0)), Some(5));
}
{
linked_chunk.push_items_back(['h', 'i']);
tracker.flush_updates(false);
assert_eq!(tracker.ordering(Position::new(ChunkIdentifier::new(0), 1)), Some(1));
assert_eq!(tracker.ordering(Position::new(ChunkIdentifier::new(3), 0)), Some(5));
assert_order(&linked_chunk, &tracker, 5);
}
let gap_id = {
linked_chunk.push_gap_back(());
tracker.flush_updates(false);
let last_chunk = linked_chunk.rchunks().next().unwrap();
assert!(last_chunk.is_gap());
assert_eq!(tracker.ordering(Position::new(last_chunk.identifier(), 0)), None);
assert_eq!(tracker.ordering(Position::new(last_chunk.identifier(), 42)), None);
assert_eq!(tracker.ordering(Position::new(ChunkIdentifier::new(0), 1)), Some(1));
assert_eq!(tracker.ordering(Position::new(ChunkIdentifier::new(3), 0)), Some(5));
assert_order(&linked_chunk, &tracker, 5);
last_chunk.identifier()
};
{
let pos_h = linked_chunk.item_position(|c| *c == 'h').unwrap();
linked_chunk.insert_items_at(pos_h, ['j', 'k']).unwrap();
tracker.flush_updates(false);
assert_eq!(tracker.ordering(Position::new(ChunkIdentifier::new(0), 1)), Some(1));
assert_eq!(tracker.ordering(Position::new(ChunkIdentifier::new(3), 0)), Some(5));
assert_order(&linked_chunk, &tracker, 5);
}
{
linked_chunk.replace_gap_at(['l', 'm'], gap_id).unwrap();
tracker.flush_updates(false);
assert_eq!(tracker.ordering(Position::new(ChunkIdentifier::new(0), 1)), Some(1));
assert_eq!(tracker.ordering(Position::new(ChunkIdentifier::new(3), 0)), Some(5));
assert_order(&linked_chunk, &tracker, 5);
}
{
let j_pos = linked_chunk.item_position(|c| *c == 'j').unwrap();
linked_chunk.remove_item_at(j_pos).unwrap();
tracker.flush_updates(false);
assert_eq!(tracker.ordering(Position::new(ChunkIdentifier::new(0), 1)), Some(1));
assert_eq!(tracker.ordering(Position::new(ChunkIdentifier::new(3), 0)), Some(5));
assert_order(&linked_chunk, &tracker, 5);
}
{
let k_pos = linked_chunk.item_position(|c| *c == 'k').unwrap();
linked_chunk.replace_item_at(k_pos, 'K').unwrap();
tracker.flush_updates(false);
assert_eq!(tracker.ordering(Position::new(ChunkIdentifier::new(0), 1)), Some(1));
assert_eq!(tracker.ordering(Position::new(ChunkIdentifier::new(3), 0)), Some(5));
assert_order(&linked_chunk, &tracker, 5);
}
{
linked_chunk.clear();
tracker.flush_updates(false);
assert_eq!(tracker.ordering(Position::new(ChunkIdentifier::new(0), 0)), None);
assert_eq!(tracker.ordering(Position::new(ChunkIdentifier::new(3), 0)), None);
}
}
#[async_test]
async fn test_out_of_band_updates() {
let db_metadata = vec![
ChunkMetadata {
previous: None,
identifier: ChunkIdentifier(0),
next: Some(ChunkIdentifier(1)),
num_items: 2,
},
ChunkMetadata {
previous: Some(ChunkIdentifier(0)),
identifier: ChunkIdentifier(1),
next: Some(ChunkIdentifier(2)),
num_items: 0,
},
ChunkMetadata {
previous: Some(ChunkIdentifier(1)),
identifier: ChunkIdentifier(2),
next: Some(ChunkIdentifier(3)),
num_items: 3,
},
ChunkMetadata {
previous: Some(ChunkIdentifier(2)),
identifier: ChunkIdentifier(3),
next: None,
num_items: 1,
},
];
let mut linked_chunk = LinkedChunk::<3, char, ()>::new_with_update_history();
let mut tracker = linked_chunk.order_tracker(Some(db_metadata)).unwrap();
assert_eq!(tracker.ordering(Position::new(ChunkIdentifier::new(0), 1)), Some(1));
assert_eq!(tracker.ordering(Position::new(ChunkIdentifier::new(2), 1)), Some(3));
tracker.map_updates(&[Update::RemoveChunk(ChunkIdentifier::new(0))]);
assert_eq!(tracker.ordering(Position::new(ChunkIdentifier::new(0), 1)), None);
assert_eq!(tracker.ordering(Position::new(ChunkIdentifier::new(2), 1)), Some(1));
}
}