use std::{
collections::{BTreeMap, HashMap},
hash::Hash,
};
use ruma::{OwnedEventId, OwnedRoomId, RoomId};
use super::{ChunkContent, ChunkIdentifierGenerator, RawChunk};
use crate::{
deserialized_responses::TimelineEvent,
linked_chunk::{
ChunkIdentifier, ChunkMetadata, LinkedChunkId, OwnedLinkedChunkId, Position, Update,
},
};
#[derive(Debug, PartialEq)]
struct ChunkRow {
linked_chunk_id: OwnedLinkedChunkId,
previous_chunk: Option<ChunkIdentifier>,
chunk: ChunkIdentifier,
next_chunk: Option<ChunkIdentifier>,
}
#[derive(Debug, PartialEq)]
struct ItemRow<ItemId, Gap> {
linked_chunk_id: OwnedLinkedChunkId,
position: Position,
item: Either<ItemId, Gap>,
}
#[derive(Debug, PartialEq)]
enum Either<Item, Gap> {
Item(Item),
Gap(Gap),
}
#[derive(Debug)]
pub struct RelationalLinkedChunk<ItemId, Item, Gap> {
chunks: Vec<ChunkRow>,
items_chunks: Vec<ItemRow<ItemId, Gap>>,
items: HashMap<OwnedLinkedChunkId, BTreeMap<ItemId, (Item, Option<Position>)>>,
}
pub trait IndexableItem {
type ItemId: Hash + PartialEq + Eq + Clone;
fn id(&self) -> Self::ItemId;
}
impl IndexableItem for TimelineEvent {
type ItemId = OwnedEventId;
fn id(&self) -> Self::ItemId {
self.event_id()
.expect("all events saved into a relational linked chunk must have a valid event id")
}
}
impl<ItemId, Item, Gap> RelationalLinkedChunk<ItemId, Item, Gap>
where
Item: IndexableItem<ItemId = ItemId>,
ItemId: Hash + PartialEq + Eq + Clone + Ord,
{
pub fn new() -> Self {
Self { chunks: Vec::new(), items_chunks: Vec::new(), items: HashMap::new() }
}
pub fn clear(&mut self) {
self.chunks.clear();
self.items_chunks.clear();
self.items.clear();
}
pub fn apply_updates(
&mut self,
linked_chunk_id: LinkedChunkId<'_>,
updates: Vec<Update<Item, Gap>>,
) {
for update in updates {
match update {
Update::NewItemsChunk { previous, new, next } => {
Self::insert_chunk(&mut self.chunks, linked_chunk_id, previous, new, next);
}
Update::NewGapChunk { previous, new, next, gap } => {
Self::insert_chunk(&mut self.chunks, linked_chunk_id, previous, new, next);
self.items_chunks.push(ItemRow {
linked_chunk_id: linked_chunk_id.to_owned(),
position: Position::new(new, 0),
item: Either::Gap(gap),
});
}
Update::RemoveChunk(chunk_identifier) => {
Self::remove_chunk(&mut self.chunks, linked_chunk_id, chunk_identifier);
let indices_to_remove = self
.items_chunks
.iter()
.enumerate()
.filter_map(
|(
nth,
ItemRow {
linked_chunk_id: linked_chunk_id_candidate,
position,
..
},
)| {
(linked_chunk_id == linked_chunk_id_candidate
&& position.chunk_identifier() == chunk_identifier)
.then_some(nth)
},
)
.collect::<Vec<_>>();
for index_to_remove in indices_to_remove.into_iter().rev() {
self.items_chunks.remove(index_to_remove);
}
}
Update::PushItems { mut at, items } => {
for item in items {
let item_id = item.id();
self.items
.entry(linked_chunk_id.to_owned())
.or_default()
.insert(item_id.clone(), (item, Some(at)));
self.items_chunks.push(ItemRow {
linked_chunk_id: linked_chunk_id.to_owned(),
position: at,
item: Either::Item(item_id),
});
at.increment_index();
}
}
Update::ReplaceItem { at, item } => {
let existing = self
.items_chunks
.iter_mut()
.find(|item| item.position == at)
.expect("trying to replace at an unknown position");
assert!(
matches!(existing.item, Either::Item(..)),
"trying to replace a gap with an item"
);
let item_id = item.id();
self.items
.entry(linked_chunk_id.to_owned())
.or_default()
.insert(item_id.clone(), (item, Some(at)));
existing.item = Either::Item(item_id);
}
Update::RemoveItem { at } => {
let mut entry_to_remove = None;
for (
nth,
ItemRow { linked_chunk_id: linked_chunk_id_candidate, position, .. },
) in self.items_chunks.iter_mut().enumerate()
{
if linked_chunk_id != &*linked_chunk_id_candidate {
continue;
}
if *position == at {
debug_assert!(entry_to_remove.is_none(), "Found the same entry twice");
entry_to_remove = Some(nth);
}
if position.chunk_identifier() == at.chunk_identifier()
&& position.index() > at.index()
{
position.decrement_index();
}
}
self.items_chunks.remove(entry_to_remove.expect("Remove an unknown item"));
}
Update::DetachLastItems { at } => {
let indices_to_remove = self
.items_chunks
.iter()
.enumerate()
.filter_map(
|(
nth,
ItemRow {
linked_chunk_id: linked_chunk_id_candidate,
position,
..
},
)| {
(linked_chunk_id == linked_chunk_id_candidate
&& position.chunk_identifier() == at.chunk_identifier()
&& position.index() >= at.index())
.then_some(nth)
},
)
.collect::<Vec<_>>();
for index_to_remove in indices_to_remove.into_iter().rev() {
self.items_chunks.remove(index_to_remove);
}
}
Update::StartReattachItems | Update::EndReattachItems => { }
Update::Clear => {
self.chunks.retain(|chunk| chunk.linked_chunk_id != linked_chunk_id);
self.items_chunks.retain(|chunk| chunk.linked_chunk_id != linked_chunk_id);
}
}
}
}
fn insert_chunk(
chunks: &mut Vec<ChunkRow>,
linked_chunk_id: LinkedChunkId<'_>,
previous: Option<ChunkIdentifier>,
new: ChunkIdentifier,
next: Option<ChunkIdentifier>,
) {
if let Some(previous) = previous {
let entry_for_previous_chunk = chunks
.iter_mut()
.find(|ChunkRow { linked_chunk_id: linked_chunk_id_candidate, chunk, .. }| {
linked_chunk_id == linked_chunk_id_candidate && *chunk == previous
})
.expect("Previous chunk should be present");
entry_for_previous_chunk.next_chunk = Some(new);
}
if let Some(next) = next {
let entry_for_next_chunk = chunks
.iter_mut()
.find(|ChunkRow { linked_chunk_id: linked_chunk_id_candidate, chunk, .. }| {
linked_chunk_id == linked_chunk_id_candidate && *chunk == next
})
.expect("Next chunk should be present");
entry_for_next_chunk.previous_chunk = Some(new);
}
chunks.push(ChunkRow {
linked_chunk_id: linked_chunk_id.to_owned(),
previous_chunk: previous,
chunk: new,
next_chunk: next,
});
}
fn remove_chunk(
chunks: &mut Vec<ChunkRow>,
linked_chunk_id: LinkedChunkId<'_>,
chunk_to_remove: ChunkIdentifier,
) {
let entry_nth_to_remove = chunks
.iter()
.enumerate()
.find_map(
|(nth, ChunkRow { linked_chunk_id: linked_chunk_id_candidate, chunk, .. })| {
(linked_chunk_id == linked_chunk_id_candidate && *chunk == chunk_to_remove)
.then_some(nth)
},
)
.expect("Remove an unknown chunk");
let ChunkRow { linked_chunk_id, previous_chunk: previous, next_chunk: next, .. } =
chunks.remove(entry_nth_to_remove);
if let Some(previous) = previous {
let entry_for_previous_chunk = chunks
.iter_mut()
.find(|ChunkRow { linked_chunk_id: linked_chunk_id_candidate, chunk, .. }| {
&linked_chunk_id == linked_chunk_id_candidate && *chunk == previous
})
.expect("Previous chunk should be present");
entry_for_previous_chunk.next_chunk = next;
}
if let Some(next) = next {
let entry_for_next_chunk = chunks
.iter_mut()
.find(|ChunkRow { linked_chunk_id: linked_chunk_id_candidate, chunk, .. }| {
&linked_chunk_id == linked_chunk_id_candidate && *chunk == next
})
.expect("Next chunk should be present");
entry_for_next_chunk.previous_chunk = previous;
}
}
pub fn unordered_linked_chunk_items<'a>(
&'a self,
target: &OwnedLinkedChunkId,
) -> impl Iterator<Item = (&'a Item, Position)> + use<'a, ItemId, Item, Gap> {
self.items.get(target).into_iter().flat_map(|items| {
items.values().filter_map(|(item, pos)| pos.map(|pos| (item, pos)))
})
}
pub fn items<'a>(
&'a self,
room_id: &'a RoomId,
) -> impl Iterator<Item = (&'a Item, Option<Position>)> {
self.items
.iter()
.filter_map(move |(linked_chunk_id, items)| {
(linked_chunk_id.room_id() == room_id).then_some(items)
})
.flat_map(|items| items.values().map(|(item, pos)| (item, *pos)))
}
pub fn save_item(&mut self, room_id: OwnedRoomId, item: Item) {
let id = item.id();
let linked_chunk_id = OwnedLinkedChunkId::Room(room_id);
let map = self.items.entry(linked_chunk_id).or_default();
if let Some(prev_value) = map.get_mut(&id) {
prev_value.0 = item;
} else {
map.insert(id, (item, None));
}
}
}
impl<ItemId, Item, Gap> RelationalLinkedChunk<ItemId, Item, Gap>
where
Gap: Clone,
Item: Clone,
ItemId: Hash + PartialEq + Eq + Ord,
{
#[doc(hidden)]
pub fn load_all_chunks(
&self,
linked_chunk_id: LinkedChunkId<'_>,
) -> Result<Vec<RawChunk<Item, Gap>>, String> {
self.chunks
.iter()
.filter(|chunk| chunk.linked_chunk_id == linked_chunk_id)
.map(|chunk_row| load_raw_chunk(self, chunk_row, linked_chunk_id))
.collect::<Result<Vec<_>, String>>()
}
#[doc(hidden)]
pub fn load_all_chunks_metadata(
&self,
linked_chunk_id: LinkedChunkId<'_>,
) -> Result<Vec<ChunkMetadata>, String> {
self.chunks
.iter()
.filter(|chunk| chunk.linked_chunk_id == linked_chunk_id)
.map(|chunk_row| load_raw_chunk_metadata(self, chunk_row, linked_chunk_id))
.collect::<Result<Vec<_>, String>>()
}
pub fn load_last_chunk(
&self,
linked_chunk_id: LinkedChunkId<'_>,
) -> Result<(Option<RawChunk<Item, Gap>>, ChunkIdentifierGenerator), String> {
let chunk_identifier_generator = match self
.chunks
.iter()
.filter_map(|chunk_row| {
(chunk_row.linked_chunk_id == linked_chunk_id).then_some(chunk_row.chunk)
})
.max()
{
Some(last_chunk_identifier) => {
ChunkIdentifierGenerator::new_from_previous_chunk_identifier(last_chunk_identifier)
}
None => ChunkIdentifierGenerator::new_from_scratch(),
};
let mut number_of_chunks = 0;
let mut chunk_row = None;
for chunk_row_candidate in &self.chunks {
if chunk_row_candidate.linked_chunk_id == linked_chunk_id {
number_of_chunks += 1;
if chunk_row_candidate.next_chunk.is_none() {
chunk_row = Some(chunk_row_candidate);
break;
}
}
}
let chunk_row = match chunk_row {
Some(chunk_row) => chunk_row,
None if number_of_chunks == 0 => {
return Ok((None, chunk_identifier_generator));
}
None => {
return Err(
"last chunk is not found but chunks exist: the linked chunk contains a cycle"
.to_owned(),
);
}
};
load_raw_chunk(self, chunk_row, linked_chunk_id)
.map(|raw_chunk| (Some(raw_chunk), chunk_identifier_generator))
}
pub fn load_previous_chunk(
&self,
linked_chunk_id: LinkedChunkId<'_>,
before_chunk_identifier: ChunkIdentifier,
) -> Result<Option<RawChunk<Item, Gap>>, String> {
let Some(chunk_row) = self.chunks.iter().find(|chunk_row| {
chunk_row.linked_chunk_id == linked_chunk_id
&& chunk_row.next_chunk == Some(before_chunk_identifier)
}) else {
return Ok(None);
};
load_raw_chunk(self, chunk_row, linked_chunk_id).map(Some)
}
}
impl<ItemId, Item, Gap> Default for RelationalLinkedChunk<ItemId, Item, Gap>
where
Item: IndexableItem<ItemId = ItemId>,
ItemId: Hash + PartialEq + Eq + Clone + Ord,
{
fn default() -> Self {
Self::new()
}
}
fn load_raw_chunk<ItemId, Item, Gap>(
relational_linked_chunk: &RelationalLinkedChunk<ItemId, Item, Gap>,
chunk_row: &ChunkRow,
linked_chunk_id: LinkedChunkId<'_>,
) -> Result<RawChunk<Item, Gap>, String>
where
Item: Clone,
Gap: Clone,
ItemId: Hash + PartialEq + Eq + Ord,
{
let mut items = relational_linked_chunk
.items_chunks
.iter()
.filter(|item_row| {
item_row.linked_chunk_id == linked_chunk_id
&& item_row.position.chunk_identifier() == chunk_row.chunk
})
.peekable();
let Some(first_item) = items.peek() else {
return Ok(RawChunk {
content: ChunkContent::Items(Vec::new()),
previous: chunk_row.previous_chunk,
identifier: chunk_row.chunk,
next: chunk_row.next_chunk,
});
};
Ok(match first_item.item {
Either::Item(_) => {
let mut collected_items = Vec::new();
for item_row in items {
match &item_row.item {
Either::Item(item_id) => {
collected_items.push((item_id, item_row.position.index()))
}
Either::Gap(_) => {
return Err(format!(
"unexpected gap in items chunk {}",
chunk_row.chunk.index()
));
}
}
}
collected_items.sort_unstable_by_key(|(_item, index)| *index);
RawChunk {
content: ChunkContent::Items(
collected_items
.into_iter()
.filter_map(|(item_id, _index)| {
Some(
relational_linked_chunk
.items
.get(&linked_chunk_id.to_owned())?
.get(item_id)?
.0
.clone(),
)
})
.collect(),
),
previous: chunk_row.previous_chunk,
identifier: chunk_row.chunk,
next: chunk_row.next_chunk,
}
}
Either::Gap(ref gap) => {
assert!(items.next().is_some(), "we just peeked the gap");
if items.next().is_some() {
return Err(format!(
"there shouldn't be more than one item row attached in gap chunk {}",
chunk_row.chunk.index()
));
}
RawChunk {
content: ChunkContent::Gap(gap.clone()),
previous: chunk_row.previous_chunk,
identifier: chunk_row.chunk,
next: chunk_row.next_chunk,
}
}
})
}
fn load_raw_chunk_metadata<ItemId, Item, Gap>(
relational_linked_chunk: &RelationalLinkedChunk<ItemId, Item, Gap>,
chunk_row: &ChunkRow,
linked_chunk_id: LinkedChunkId<'_>,
) -> Result<ChunkMetadata, String>
where
Item: Clone,
Gap: Clone,
ItemId: Hash + PartialEq + Eq,
{
let mut items = relational_linked_chunk
.items_chunks
.iter()
.filter(|item_row| {
item_row.linked_chunk_id == linked_chunk_id
&& item_row.position.chunk_identifier() == chunk_row.chunk
})
.peekable();
let Some(first_item) = items.peek() else {
return Ok(ChunkMetadata {
num_items: 0,
previous: chunk_row.previous_chunk,
identifier: chunk_row.chunk,
next: chunk_row.next_chunk,
});
};
Ok(match first_item.item {
Either::Item(_) => {
let mut num_items = 0;
for item in items {
match &item.item {
Either::Item(_) => num_items += 1,
Either::Gap(_) => {
return Err(format!(
"unexpected gap in items chunk {}",
chunk_row.chunk.index()
));
}
}
}
ChunkMetadata {
num_items,
previous: chunk_row.previous_chunk,
identifier: chunk_row.chunk,
next: chunk_row.next_chunk,
}
}
Either::Gap(..) => {
assert!(items.next().is_some(), "we just peeked the gap");
if items.next().is_some() {
return Err(format!(
"there shouldn't be more than one item row attached in gap chunk {}",
chunk_row.chunk.index()
));
}
ChunkMetadata {
num_items: 0,
previous: chunk_row.previous_chunk,
identifier: chunk_row.chunk,
next: chunk_row.next_chunk,
}
}
})
}
#[cfg(test)]
mod tests {
use std::collections::BTreeMap;
use assert_matches::assert_matches;
use ruma::room_id;
use super::{super::lazy_loader::from_all_chunks, ChunkIdentifier as CId, *};
impl IndexableItem for char {
type ItemId = char;
fn id(&self) -> Self::ItemId {
*self
}
}
#[test]
fn test_new_items_chunk() {
let room_id = room_id!("!r0:matrix.org");
let linked_chunk_id = OwnedLinkedChunkId::Room(room_id.to_owned());
let mut relational_linked_chunk = RelationalLinkedChunk::<_, char, ()>::new();
relational_linked_chunk.apply_updates(
linked_chunk_id.as_ref(),
vec![
Update::NewItemsChunk { previous: None, new: CId::new(0), next: None },
Update::NewItemsChunk { previous: Some(CId::new(0)), new: CId::new(1), next: None },
Update::NewItemsChunk { previous: None, new: CId::new(2), next: Some(CId::new(0)) },
Update::NewItemsChunk {
previous: Some(CId::new(2)),
new: CId::new(3),
next: Some(CId::new(0)),
},
],
);
assert_eq!(
relational_linked_chunk.chunks,
&[
ChunkRow {
linked_chunk_id: linked_chunk_id.clone(),
previous_chunk: Some(CId::new(3)),
chunk: CId::new(0),
next_chunk: Some(CId::new(1))
},
ChunkRow {
linked_chunk_id: linked_chunk_id.clone(),
previous_chunk: Some(CId::new(0)),
chunk: CId::new(1),
next_chunk: None
},
ChunkRow {
linked_chunk_id: linked_chunk_id.clone(),
previous_chunk: None,
chunk: CId::new(2),
next_chunk: Some(CId::new(3))
},
ChunkRow {
linked_chunk_id,
previous_chunk: Some(CId::new(2)),
chunk: CId::new(3),
next_chunk: Some(CId::new(0))
},
],
);
assert!(relational_linked_chunk.items_chunks.is_empty());
}
#[test]
fn test_new_gap_chunk() {
let room_id = room_id!("!r0:matrix.org");
let linked_chunk_id = OwnedLinkedChunkId::Room(room_id.to_owned());
let mut relational_linked_chunk = RelationalLinkedChunk::<_, char, ()>::new();
relational_linked_chunk.apply_updates(
linked_chunk_id.as_ref(),
vec![
Update::NewItemsChunk { previous: None, new: CId::new(0), next: None },
Update::NewGapChunk {
previous: Some(CId::new(0)),
new: CId::new(1),
next: None,
gap: (),
},
Update::NewItemsChunk { previous: Some(CId::new(1)), new: CId::new(2), next: None },
],
);
assert_eq!(
relational_linked_chunk.chunks,
&[
ChunkRow {
linked_chunk_id: linked_chunk_id.clone(),
previous_chunk: None,
chunk: CId::new(0),
next_chunk: Some(CId::new(1))
},
ChunkRow {
linked_chunk_id: linked_chunk_id.clone(),
previous_chunk: Some(CId::new(0)),
chunk: CId::new(1),
next_chunk: Some(CId::new(2))
},
ChunkRow {
linked_chunk_id: linked_chunk_id.clone(),
previous_chunk: Some(CId::new(1)),
chunk: CId::new(2),
next_chunk: None
},
],
);
assert_eq!(
relational_linked_chunk.items_chunks,
&[ItemRow {
linked_chunk_id,
position: Position::new(CId::new(1), 0),
item: Either::Gap(())
}],
);
}
#[test]
fn test_remove_chunk() {
let room_id = room_id!("!r0:matrix.org");
let linked_chunk_id = OwnedLinkedChunkId::Room(room_id.to_owned());
let mut relational_linked_chunk = RelationalLinkedChunk::<_, char, ()>::new();
relational_linked_chunk.apply_updates(
linked_chunk_id.as_ref(),
vec![
Update::NewItemsChunk { previous: None, new: CId::new(0), next: None },
Update::NewGapChunk {
previous: Some(CId::new(0)),
new: CId::new(1),
next: None,
gap: (),
},
Update::NewItemsChunk { previous: Some(CId::new(1)), new: CId::new(2), next: None },
Update::RemoveChunk(CId::new(1)),
],
);
assert_eq!(
relational_linked_chunk.chunks,
&[
ChunkRow {
linked_chunk_id: linked_chunk_id.clone(),
previous_chunk: None,
chunk: CId::new(0),
next_chunk: Some(CId::new(2))
},
ChunkRow {
linked_chunk_id,
previous_chunk: Some(CId::new(0)),
chunk: CId::new(2),
next_chunk: None
},
],
);
assert!(relational_linked_chunk.items_chunks.is_empty());
}
#[test]
fn test_push_items() {
let room_id = room_id!("!r0:matrix.org");
let linked_chunk_id = OwnedLinkedChunkId::Room(room_id.to_owned());
let mut relational_linked_chunk = RelationalLinkedChunk::<_, char, ()>::new();
relational_linked_chunk.apply_updates(
linked_chunk_id.as_ref(),
vec![
Update::NewItemsChunk { previous: None, new: CId::new(0), next: None },
Update::PushItems { at: Position::new(CId::new(0), 0), items: vec!['a', 'b', 'c'] },
Update::NewItemsChunk { previous: Some(CId::new(0)), new: CId::new(1), next: None },
Update::PushItems { at: Position::new(CId::new(1), 0), items: vec!['x', 'y', 'z'] },
Update::PushItems { at: Position::new(CId::new(0), 3), items: vec!['d', 'e'] },
],
);
assert_eq!(
relational_linked_chunk.chunks,
&[
ChunkRow {
linked_chunk_id: linked_chunk_id.clone(),
previous_chunk: None,
chunk: CId::new(0),
next_chunk: Some(CId::new(1))
},
ChunkRow {
linked_chunk_id: linked_chunk_id.clone(),
previous_chunk: Some(CId::new(0)),
chunk: CId::new(1),
next_chunk: None
},
],
);
assert_eq!(
relational_linked_chunk.items_chunks,
&[
ItemRow {
linked_chunk_id: linked_chunk_id.clone(),
position: Position::new(CId::new(0), 0),
item: Either::Item('a')
},
ItemRow {
linked_chunk_id: linked_chunk_id.clone(),
position: Position::new(CId::new(0), 1),
item: Either::Item('b')
},
ItemRow {
linked_chunk_id: linked_chunk_id.clone(),
position: Position::new(CId::new(0), 2),
item: Either::Item('c')
},
ItemRow {
linked_chunk_id: linked_chunk_id.clone(),
position: Position::new(CId::new(1), 0),
item: Either::Item('x')
},
ItemRow {
linked_chunk_id: linked_chunk_id.clone(),
position: Position::new(CId::new(1), 1),
item: Either::Item('y')
},
ItemRow {
linked_chunk_id: linked_chunk_id.clone(),
position: Position::new(CId::new(1), 2),
item: Either::Item('z')
},
ItemRow {
linked_chunk_id: linked_chunk_id.clone(),
position: Position::new(CId::new(0), 3),
item: Either::Item('d')
},
ItemRow {
linked_chunk_id: linked_chunk_id.clone(),
position: Position::new(CId::new(0), 4),
item: Either::Item('e')
},
],
);
}
#[test]
fn test_remove_item() {
let room_id = room_id!("!r0:matrix.org");
let linked_chunk_id = OwnedLinkedChunkId::Room(room_id.to_owned());
let mut relational_linked_chunk = RelationalLinkedChunk::<_, char, ()>::new();
relational_linked_chunk.apply_updates(
linked_chunk_id.as_ref(),
vec![
Update::NewItemsChunk { previous: None, new: CId::new(0), next: None },
Update::PushItems {
at: Position::new(CId::new(0), 0),
items: vec!['a', 'b', 'c', 'd', 'e'],
},
Update::RemoveItem { at: Position::new(CId::new(0), 0) },
Update::RemoveItem { at: Position::new(CId::new(0), 2) },
],
);
assert_eq!(
relational_linked_chunk.chunks,
&[ChunkRow {
linked_chunk_id: linked_chunk_id.clone(),
previous_chunk: None,
chunk: CId::new(0),
next_chunk: None
}],
);
assert_eq!(
relational_linked_chunk.items_chunks,
&[
ItemRow {
linked_chunk_id: linked_chunk_id.clone(),
position: Position::new(CId::new(0), 0),
item: Either::Item('b')
},
ItemRow {
linked_chunk_id: linked_chunk_id.clone(),
position: Position::new(CId::new(0), 1),
item: Either::Item('c')
},
ItemRow {
linked_chunk_id: linked_chunk_id.clone(),
position: Position::new(CId::new(0), 2),
item: Either::Item('e')
},
],
);
}
#[test]
fn test_detach_last_items() {
let room_id = room_id!("!r0:matrix.org");
let linked_chunk_id = OwnedLinkedChunkId::Room(room_id.to_owned());
let mut relational_linked_chunk = RelationalLinkedChunk::<_, char, ()>::new();
relational_linked_chunk.apply_updates(
linked_chunk_id.as_ref(),
vec![
Update::NewItemsChunk { previous: None, new: CId::new(0), next: None },
Update::NewItemsChunk { previous: Some(CId::new(0)), new: CId::new(1), next: None },
Update::PushItems {
at: Position::new(CId::new(0), 0),
items: vec!['a', 'b', 'c', 'd', 'e'],
},
Update::PushItems { at: Position::new(CId::new(1), 0), items: vec!['x', 'y', 'z'] },
Update::DetachLastItems { at: Position::new(CId::new(0), 2) },
],
);
assert_eq!(
relational_linked_chunk.chunks,
&[
ChunkRow {
linked_chunk_id: linked_chunk_id.clone(),
previous_chunk: None,
chunk: CId::new(0),
next_chunk: Some(CId::new(1))
},
ChunkRow {
linked_chunk_id: linked_chunk_id.clone(),
previous_chunk: Some(CId::new(0)),
chunk: CId::new(1),
next_chunk: None
},
],
);
assert_eq!(
relational_linked_chunk.items_chunks,
&[
ItemRow {
linked_chunk_id: linked_chunk_id.clone(),
position: Position::new(CId::new(0), 0),
item: Either::Item('a')
},
ItemRow {
linked_chunk_id: linked_chunk_id.clone(),
position: Position::new(CId::new(0), 1),
item: Either::Item('b')
},
ItemRow {
linked_chunk_id: linked_chunk_id.clone(),
position: Position::new(CId::new(1), 0),
item: Either::Item('x')
},
ItemRow {
linked_chunk_id: linked_chunk_id.clone(),
position: Position::new(CId::new(1), 1),
item: Either::Item('y')
},
ItemRow {
linked_chunk_id: linked_chunk_id.clone(),
position: Position::new(CId::new(1), 2),
item: Either::Item('z')
},
],
);
}
#[test]
fn test_start_and_end_reattach_items() {
let room_id = room_id!("!r0:matrix.org");
let linked_chunk_id = OwnedLinkedChunkId::Room(room_id.to_owned());
let mut relational_linked_chunk = RelationalLinkedChunk::<_, char, ()>::new();
relational_linked_chunk.apply_updates(
linked_chunk_id.as_ref(),
vec![Update::StartReattachItems, Update::EndReattachItems],
);
assert!(relational_linked_chunk.chunks.is_empty());
assert!(relational_linked_chunk.items_chunks.is_empty());
}
#[test]
fn test_clear() {
let r0 = room_id!("!r0:matrix.org");
let linked_chunk_id0 = OwnedLinkedChunkId::Room(r0.to_owned());
let r1 = room_id!("!r1:matrix.org");
let linked_chunk_id1 = OwnedLinkedChunkId::Room(r1.to_owned());
let mut relational_linked_chunk = RelationalLinkedChunk::<_, char, ()>::new();
relational_linked_chunk.apply_updates(
linked_chunk_id0.as_ref(),
vec![
Update::NewItemsChunk { previous: None, new: CId::new(0), next: None },
Update::PushItems { at: Position::new(CId::new(0), 0), items: vec!['a', 'b', 'c'] },
],
);
relational_linked_chunk.apply_updates(
linked_chunk_id1.as_ref(),
vec![
Update::NewItemsChunk { previous: None, new: CId::new(0), next: None },
Update::PushItems { at: Position::new(CId::new(0), 0), items: vec!['x'] },
],
);
assert_eq!(
relational_linked_chunk.chunks,
&[
ChunkRow {
linked_chunk_id: linked_chunk_id0.to_owned(),
previous_chunk: None,
chunk: CId::new(0),
next_chunk: None,
},
ChunkRow {
linked_chunk_id: linked_chunk_id1.to_owned(),
previous_chunk: None,
chunk: CId::new(0),
next_chunk: None,
}
],
);
assert_eq!(
relational_linked_chunk.items_chunks,
&[
ItemRow {
linked_chunk_id: linked_chunk_id0.to_owned(),
position: Position::new(CId::new(0), 0),
item: Either::Item('a')
},
ItemRow {
linked_chunk_id: linked_chunk_id0.to_owned(),
position: Position::new(CId::new(0), 1),
item: Either::Item('b')
},
ItemRow {
linked_chunk_id: linked_chunk_id0.to_owned(),
position: Position::new(CId::new(0), 2),
item: Either::Item('c')
},
ItemRow {
linked_chunk_id: linked_chunk_id1.to_owned(),
position: Position::new(CId::new(0), 0),
item: Either::Item('x')
},
],
);
relational_linked_chunk.apply_updates(linked_chunk_id0.as_ref(), vec![Update::Clear]);
assert_eq!(
relational_linked_chunk.chunks,
&[ChunkRow {
linked_chunk_id: linked_chunk_id1.to_owned(),
previous_chunk: None,
chunk: CId::new(0),
next_chunk: None,
}],
);
assert_eq!(
relational_linked_chunk.items_chunks,
&[ItemRow {
linked_chunk_id: linked_chunk_id1.to_owned(),
position: Position::new(CId::new(0), 0),
item: Either::Item('x')
},],
);
}
#[test]
fn test_load_empty_linked_chunk() {
let room_id = room_id!("!r0:matrix.org");
let linked_chunk_id = OwnedLinkedChunkId::Room(room_id.to_owned());
let relational_linked_chunk = RelationalLinkedChunk::<_, char, char>::new();
let result = relational_linked_chunk.load_all_chunks(linked_chunk_id.as_ref()).unwrap();
assert!(result.is_empty());
}
#[test]
fn test_load_all_chunks_with_empty_items() {
let room_id = room_id!("!r0:matrix.org");
let linked_chunk_id = OwnedLinkedChunkId::Room(room_id.to_owned());
let mut relational_linked_chunk = RelationalLinkedChunk::<_, char, char>::new();
relational_linked_chunk.apply_updates(
linked_chunk_id.as_ref(),
vec![Update::NewItemsChunk { previous: None, new: CId::new(0), next: None }],
);
let lc = from_all_chunks::<3, _, _>(
relational_linked_chunk.load_all_chunks(linked_chunk_id.as_ref()).unwrap(),
)
.expect("building succeeds")
.expect("this leads to a non-empty linked chunk");
assert_items_eq!(lc, []);
}
#[test]
fn test_rebuild_linked_chunk() {
let room_id = room_id!("!r0:matrix.org");
let linked_chunk_id = OwnedLinkedChunkId::Room(room_id.to_owned());
let mut relational_linked_chunk = RelationalLinkedChunk::<_, char, char>::new();
relational_linked_chunk.apply_updates(
linked_chunk_id.as_ref(),
vec![
Update::NewItemsChunk { previous: None, new: CId::new(0), next: None },
Update::PushItems { at: Position::new(CId::new(0), 0), items: vec!['a', 'b', 'c'] },
Update::NewGapChunk {
previous: Some(CId::new(0)),
new: CId::new(1),
next: None,
gap: 'g',
},
Update::NewItemsChunk { previous: Some(CId::new(1)), new: CId::new(2), next: None },
Update::PushItems { at: Position::new(CId::new(2), 0), items: vec!['d', 'e', 'f'] },
],
);
let lc = from_all_chunks::<3, _, _>(
relational_linked_chunk.load_all_chunks(linked_chunk_id.as_ref()).unwrap(),
)
.expect("building succeeds")
.expect("this leads to a non-empty linked chunk");
assert_items_eq!(lc, ['a', 'b', 'c'] [-] ['d', 'e', 'f']);
}
#[test]
fn test_replace_item() {
let room_id = room_id!("!r0:matrix.org");
let linked_chunk_id = OwnedLinkedChunkId::Room(room_id.to_owned());
let mut relational_linked_chunk = RelationalLinkedChunk::<_, char, ()>::new();
relational_linked_chunk.apply_updates(
linked_chunk_id.as_ref(),
vec![
Update::NewItemsChunk { previous: None, new: CId::new(0), next: None },
Update::PushItems { at: Position::new(CId::new(0), 0), items: vec!['a', 'b', 'c'] },
Update::ReplaceItem { at: Position::new(CId::new(0), 1), item: 'B' },
],
);
assert_eq!(
relational_linked_chunk.chunks,
&[ChunkRow {
linked_chunk_id: linked_chunk_id.clone(),
previous_chunk: None,
chunk: CId::new(0),
next_chunk: None,
},],
);
assert_eq!(
relational_linked_chunk.items_chunks,
&[
ItemRow {
linked_chunk_id: linked_chunk_id.clone(),
position: Position::new(CId::new(0), 0),
item: Either::Item('a')
},
ItemRow {
linked_chunk_id: linked_chunk_id.clone(),
position: Position::new(CId::new(0), 1),
item: Either::Item('B')
},
ItemRow {
linked_chunk_id,
position: Position::new(CId::new(0), 2),
item: Either::Item('c')
},
],
);
}
#[test]
fn test_unordered_events() {
let room_id = room_id!("!r0:matrix.org");
let linked_chunk_id = OwnedLinkedChunkId::Room(room_id.to_owned());
let other_room_id = room_id!("!r1:matrix.org");
let other_linked_chunk_id = OwnedLinkedChunkId::Room(other_room_id.to_owned());
let mut relational_linked_chunk = RelationalLinkedChunk::<_, char, ()>::new();
relational_linked_chunk.apply_updates(
linked_chunk_id.as_ref(),
vec![
Update::NewItemsChunk { previous: None, new: CId::new(0), next: None },
Update::PushItems { at: Position::new(CId::new(0), 0), items: vec!['a', 'b', 'c'] },
Update::NewItemsChunk { previous: Some(CId::new(0)), new: CId::new(1), next: None },
Update::PushItems { at: Position::new(CId::new(1), 0), items: vec!['d', 'e', 'f'] },
],
);
relational_linked_chunk.apply_updates(
other_linked_chunk_id.as_ref(),
vec![
Update::NewItemsChunk { previous: None, new: CId::new(0), next: None },
Update::PushItems { at: Position::new(CId::new(0), 0), items: vec!['x', 'y', 'z'] },
],
);
let events = BTreeMap::from_iter(
relational_linked_chunk.unordered_linked_chunk_items(&linked_chunk_id),
);
assert_eq!(events.len(), 6);
assert_eq!(*events.get(&'a').unwrap(), Position::new(CId::new(0), 0));
assert_eq!(*events.get(&'b').unwrap(), Position::new(CId::new(0), 1));
assert_eq!(*events.get(&'c').unwrap(), Position::new(CId::new(0), 2));
assert_eq!(*events.get(&'d').unwrap(), Position::new(CId::new(1), 0));
assert_eq!(*events.get(&'e').unwrap(), Position::new(CId::new(1), 1));
assert_eq!(*events.get(&'f').unwrap(), Position::new(CId::new(1), 2));
}
#[test]
fn test_load_last_chunk() {
let room_id = room_id!("!r0:matrix.org");
let linked_chunk_id = OwnedLinkedChunkId::Room(room_id.to_owned());
let mut relational_linked_chunk = RelationalLinkedChunk::<_, char, ()>::new();
{
let (last_chunk, chunk_identifier_generator) =
relational_linked_chunk.load_last_chunk(linked_chunk_id.as_ref()).unwrap();
assert!(last_chunk.is_none());
assert_eq!(chunk_identifier_generator.current(), 0);
}
{
relational_linked_chunk.apply_updates(
linked_chunk_id.as_ref(),
vec![
Update::NewItemsChunk { previous: None, new: CId::new(42), next: None },
Update::PushItems { at: Position::new(CId::new(42), 0), items: vec!['a', 'b'] },
],
);
let (last_chunk, chunk_identifier_generator) =
relational_linked_chunk.load_last_chunk(linked_chunk_id.as_ref()).unwrap();
assert_matches!(last_chunk, Some(last_chunk) => {
assert_eq!(last_chunk.identifier, 42);
assert!(last_chunk.previous.is_none());
assert!(last_chunk.next.is_none());
assert_matches!(last_chunk.content, ChunkContent::Items(items) => {
assert_eq!(items.len(), 2);
assert_eq!(items, &['a', 'b']);
});
});
assert_eq!(chunk_identifier_generator.current(), 42);
}
{
relational_linked_chunk.apply_updates(
linked_chunk_id.as_ref(),
vec![
Update::NewItemsChunk {
previous: Some(CId::new(42)),
new: CId::new(7),
next: None,
},
Update::PushItems {
at: Position::new(CId::new(7), 0),
items: vec!['c', 'd', 'e'],
},
],
);
let (last_chunk, chunk_identifier_generator) =
relational_linked_chunk.load_last_chunk(linked_chunk_id.as_ref()).unwrap();
assert_matches!(last_chunk, Some(last_chunk) => {
assert_eq!(last_chunk.identifier, 7);
assert_matches!(last_chunk.previous, Some(previous) => {
assert_eq!(previous, 42);
});
assert!(last_chunk.next.is_none());
assert_matches!(last_chunk.content, ChunkContent::Items(items) => {
assert_eq!(items.len(), 3);
assert_eq!(items, &['c', 'd', 'e']);
});
});
assert_eq!(chunk_identifier_generator.current(), 42);
}
}
#[test]
fn test_load_last_chunk_with_a_cycle() {
let room_id = room_id!("!r0:matrix.org");
let linked_chunk_id = OwnedLinkedChunkId::Room(room_id.to_owned());
let mut relational_linked_chunk = RelationalLinkedChunk::<_, char, ()>::new();
relational_linked_chunk.apply_updates(
linked_chunk_id.as_ref(),
vec![
Update::NewItemsChunk { previous: None, new: CId::new(0), next: None },
Update::NewItemsChunk {
previous: Some(CId::new(0)),
new: CId::new(1),
next: Some(CId::new(0)),
},
],
);
relational_linked_chunk.load_last_chunk(linked_chunk_id.as_ref()).unwrap_err();
}
#[test]
fn test_load_previous_chunk() {
let room_id = room_id!("!r0:matrix.org");
let linked_chunk_id = OwnedLinkedChunkId::Room(room_id.to_owned());
let mut relational_linked_chunk = RelationalLinkedChunk::<_, char, ()>::new();
{
let previous_chunk = relational_linked_chunk
.load_previous_chunk(linked_chunk_id.as_ref(), CId::new(153))
.unwrap();
assert!(previous_chunk.is_none());
}
{
relational_linked_chunk.apply_updates(
linked_chunk_id.as_ref(),
vec![Update::NewItemsChunk { previous: None, new: CId::new(42), next: None }],
);
let previous_chunk = relational_linked_chunk
.load_previous_chunk(linked_chunk_id.as_ref(), CId::new(42))
.unwrap();
assert!(previous_chunk.is_none());
}
{
relational_linked_chunk.apply_updates(
linked_chunk_id.as_ref(),
vec![
Update::NewItemsChunk {
previous: None,
new: CId::new(7),
next: Some(CId::new(42)),
},
Update::PushItems {
at: Position::new(CId::new(7), 0),
items: vec!['a', 'b', 'c'],
},
],
);
let previous_chunk = relational_linked_chunk
.load_previous_chunk(linked_chunk_id.as_ref(), CId::new(42))
.unwrap();
assert_matches!(previous_chunk, Some(previous_chunk) => {
assert_eq!(previous_chunk.identifier, 7);
assert!(previous_chunk.previous.is_none());
assert_matches!(previous_chunk.next, Some(next) => {
assert_eq!(next, 42);
});
assert_matches!(previous_chunk.content, ChunkContent::Items(items) => {
assert_eq!(items.len(), 3);
assert_eq!(items, &['a', 'b', 'c']);
});
});
}
}
}