diff --git a/crates/matrix-sdk-base/src/event_cache/store/memory_store.rs b/crates/matrix-sdk-base/src/event_cache/store/memory_store.rs index bd764cad0db..92109029d3f 100644 --- a/crates/matrix-sdk-base/src/event_cache/store/memory_store.rs +++ b/crates/matrix-sdk-base/src/event_cache/store/memory_store.rs @@ -16,10 +16,11 @@ use std::{collections::HashMap, num::NonZeroUsize, sync::RwLock as StdRwLock, ti use async_trait::async_trait; use matrix_sdk_common::{ - linked_chunk::Update, ring_buffer::RingBuffer, + linked_chunk::{relational::RelationalLinkedChunk, Update}, + ring_buffer::RingBuffer, store_locks::memory_store_helper::try_take_leased_lock, }; -use ruma::{MxcUri, OwnedMxcUri}; +use ruma::{MxcUri, OwnedMxcUri, RoomId}; use super::{EventCacheStore, EventCacheStoreError, Result}; use crate::{ @@ -33,8 +34,14 @@ use crate::{ #[allow(clippy::type_complexity)] #[derive(Debug)] pub struct MemoryStore { - media: StdRwLock)>>, - leases: StdRwLock>, + inner: StdRwLock, +} + +#[derive(Debug)] +struct MemoryStoreInner { + media: RingBuffer<(OwnedMxcUri, String /* unique key */, Vec)>, + leases: HashMap, + events: RelationalLinkedChunk, } // SAFETY: `new_unchecked` is safe because 20 is not zero. @@ -43,8 +50,11 @@ const NUMBER_OF_MEDIAS: NonZeroUsize = unsafe { NonZeroUsize::new_unchecked(20) impl Default for MemoryStore { fn default() -> Self { Self { - media: StdRwLock::new(RingBuffer::new(NUMBER_OF_MEDIAS)), - leases: Default::default(), + inner: StdRwLock::new(MemoryStoreInner { + media: RingBuffer::new(NUMBER_OF_MEDIAS), + leases: Default::default(), + events: RelationalLinkedChunk::new(), + }), } } } @@ -67,14 +77,20 @@ impl EventCacheStore for MemoryStore { key: &str, holder: &str, ) -> Result { - Ok(try_take_leased_lock(&self.leases, lease_duration_ms, key, holder)) + let mut inner = self.inner.write().unwrap(); + + Ok(try_take_leased_lock(&mut inner.leases, lease_duration_ms, key, holder)) } async fn handle_linked_chunk_updates( &self, - _updates: &[Update], + room_id: &RoomId, + updates: Vec>, ) -> Result<(), Self::Error> { - todo!() + let mut inner = self.inner.write().unwrap(); + inner.events.apply_updates(room_id, updates); + + Ok(()) } async fn add_media_content( @@ -84,8 +100,10 @@ impl EventCacheStore for MemoryStore { ) -> Result<()> { // Avoid duplication. Let's try to remove it first. self.remove_media_content(request).await?; + // Now, let's add it. - self.media.write().unwrap().push((request.uri().to_owned(), request.unique_key(), data)); + let mut inner = self.inner.write().unwrap(); + inner.media.push((request.uri().to_owned(), request.unique_key(), data)); Ok(()) } @@ -97,8 +115,10 @@ impl EventCacheStore for MemoryStore { ) -> Result<(), Self::Error> { let expected_key = from.unique_key(); - let mut medias = self.media.write().unwrap(); - if let Some((mxc, key, _)) = medias.iter_mut().find(|(_, key, _)| *key == expected_key) { + let mut inner = self.inner.write().unwrap(); + + if let Some((mxc, key, _)) = inner.media.iter_mut().find(|(_, key, _)| *key == expected_key) + { *mxc = to.uri().to_owned(); *key = to.unique_key(); } @@ -109,8 +129,9 @@ impl EventCacheStore for MemoryStore { async fn get_media_content(&self, request: &MediaRequestParameters) -> Result>> { let expected_key = request.unique_key(); - let media = self.media.read().unwrap(); - Ok(media.iter().find_map(|(_media_uri, media_key, media_content)| { + let inner = self.inner.read().unwrap(); + + Ok(inner.media.iter().find_map(|(_media_uri, media_key, media_content)| { (media_key == &expected_key).then(|| media_content.to_owned()) })) } @@ -118,23 +139,27 @@ impl EventCacheStore for MemoryStore { async fn remove_media_content(&self, request: &MediaRequestParameters) -> Result<()> { let expected_key = request.unique_key(); - let mut media = self.media.write().unwrap(); - let Some(index) = media + let mut inner = self.inner.write().unwrap(); + + let Some(index) = inner + .media .iter() .position(|(_media_uri, media_key, _media_content)| media_key == &expected_key) else { return Ok(()); }; - media.remove(index); + inner.media.remove(index); Ok(()) } async fn remove_media_content_for_uri(&self, uri: &MxcUri) -> Result<()> { - let mut media = self.media.write().unwrap(); + let mut inner = self.inner.write().unwrap(); + let expected_key = uri.to_owned(); - let positions = media + let positions = inner + .media .iter() .enumerate() .filter_map(|(position, (media_uri, _media_key, _media_content))| { @@ -144,7 +169,7 @@ impl EventCacheStore for MemoryStore { // Iterate in reverse-order so that positions stay valid after first removals. for position in positions.into_iter().rev() { - media.remove(position); + inner.media.remove(position); } Ok(()) diff --git a/crates/matrix-sdk-base/src/event_cache/store/traits.rs b/crates/matrix-sdk-base/src/event_cache/store/traits.rs index dde80830973..3c35862b3c5 100644 --- a/crates/matrix-sdk-base/src/event_cache/store/traits.rs +++ b/crates/matrix-sdk-base/src/event_cache/store/traits.rs @@ -16,7 +16,7 @@ use std::{fmt, sync::Arc}; use async_trait::async_trait; use matrix_sdk_common::{linked_chunk::Update, AsyncTraitDeps}; -use ruma::MxcUri; +use ruma::{MxcUri, RoomId}; use super::EventCacheStoreError; use crate::{ @@ -45,7 +45,8 @@ pub trait EventCacheStore: AsyncTraitDeps { /// in-memory. This method aims at forwarding this update inside this store. async fn handle_linked_chunk_updates( &self, - updates: &[Update], + room_id: &RoomId, + updates: Vec>, ) -> Result<(), Self::Error>; /// Add a media file's content in the media store. @@ -144,9 +145,10 @@ impl EventCacheStore for EraseEventCacheStoreError { async fn handle_linked_chunk_updates( &self, - updates: &[Update], + room_id: &RoomId, + updates: Vec>, ) -> Result<(), Self::Error> { - self.0.handle_linked_chunk_updates(updates).await.map_err(Into::into) + self.0.handle_linked_chunk_updates(room_id, updates).await.map_err(Into::into) } async fn add_media_content( diff --git a/crates/matrix-sdk-common/src/linked_chunk/mod.rs b/crates/matrix-sdk-common/src/linked_chunk/mod.rs index eeba29be302..0aae2ecc942 100644 --- a/crates/matrix-sdk-common/src/linked_chunk/mod.rs +++ b/crates/matrix-sdk-common/src/linked_chunk/mod.rs @@ -93,6 +93,7 @@ macro_rules! assert_items_eq { } mod as_vector; +pub mod relational; mod updates; use std::{ @@ -871,12 +872,12 @@ impl Drop for LinkedChunk { } /// A [`LinkedChunk`] can be safely sent over thread boundaries if `Item: Send` -/// and `Gap: Send`. The only unsafe part if around the `NonNull`, but the API +/// and `Gap: Send`. The only unsafe part is around the `NonNull`, but the API /// and the lifetimes to deref them are designed safely. unsafe impl Send for LinkedChunk {} /// A [`LinkedChunk`] can be safely share between threads if `Item: Sync` and -/// `Gap: Sync`. The only unsafe part if around the `NonNull`, but the API and +/// `Gap: Sync`. The only unsafe part is around the `NonNull`, but the API and /// the lifetimes to deref them are designed safely. unsafe impl Sync for LinkedChunk {} @@ -935,6 +936,18 @@ impl ChunkIdentifierGenerator { #[repr(transparent)] pub struct ChunkIdentifier(u64); +impl ChunkIdentifier { + /// Create a new [`ChunkIdentifier`]. + pub(super) fn new(identifier: u64) -> Self { + Self(identifier) + } + + /// Get the underlying identifier. + fn index(&self) -> u64 { + self.0 + } +} + impl PartialEq for ChunkIdentifier { fn eq(&self, other: &u64) -> bool { self.0 == *other @@ -948,6 +961,11 @@ impl PartialEq for ChunkIdentifier { pub struct Position(ChunkIdentifier, usize); impl Position { + /// Create a new [`Position`]. + pub(super) fn new(chunk_identifier: ChunkIdentifier, index: usize) -> Self { + Self(chunk_identifier, index) + } + /// Get the chunk identifier of the item. pub fn chunk_identifier(&self) -> ChunkIdentifier { self.0 @@ -966,6 +984,16 @@ impl Position { pub fn decrement_index(&mut self) { self.1 = self.1.checked_sub(1).expect("Cannot decrement the index because it's already 0"); } + + /// Increment the index part (see [`Self::index`]), i.e. add 1. + /// + /// # Panic + /// + /// This method will panic if it will overflow, i.e. if the index is larger + /// than `usize::MAX`. + pub fn increment_index(&mut self) { + self.1 = self.1.checked_add(1).expect("Cannot increment the index because it's too large"); + } } /// An iterator over a [`LinkedChunk`] that traverses the chunk in backward diff --git a/crates/matrix-sdk-common/src/linked_chunk/relational.rs b/crates/matrix-sdk-common/src/linked_chunk/relational.rs new file mode 100644 index 00000000000..7f4983ca270 --- /dev/null +++ b/crates/matrix-sdk-common/src/linked_chunk/relational.rs @@ -0,0 +1,673 @@ +// Copyright 2024 The Matrix.org Foundation C.I.C. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Implementation for a _relational linked chunk_, see +//! [`RelationalLinkedChunk`]. + +use ruma::{OwnedRoomId, RoomId}; + +use crate::linked_chunk::{ChunkIdentifier, Position, Update}; + +/// A row of the [`RelationalLinkedChunk::chunks`]. +#[derive(Debug, PartialEq)] +struct ChunkRow { + room_id: OwnedRoomId, + previous_chunk: Option, + chunk: ChunkIdentifier, + next_chunk: Option, +} + +/// A row of the [`RelationalLinkedChunk::items`]. +#[derive(Debug, PartialEq)] +struct ItemRow { + room_id: OwnedRoomId, + position: Position, + item: Either, +} + +/// Kind of item. +#[derive(Debug, PartialEq)] +enum Either { + /// The content is an item. + Item(Item), + + /// The content is a gap. + Gap(Gap), +} + +/// A [`LinkedChunk`] but with a relational layout, similar to what we +/// would have in a database. +/// +/// This is used by memory stores. The idea is to have a data layout that is +/// similar for memory stores and for relational database stores, to represent a +/// [`LinkedChunk`]. +/// +/// This type is also designed to receive [`Update`]. Applying `Update`s +/// directly on a [`LinkedChunk`] is not ideal and particularly not trivial as +/// the `Update`s do _not_ match the internal data layout of the `LinkedChunk`, +/// they have been designed for storages, like a relational database for +/// example. +/// +/// This type is not as performant as [`LinkedChunk`] (in terms of memory +/// layout, CPU caches etc.). It is only designed to be used in memory stores, +/// which are mostly used for test purposes or light usage of the SDK. +/// +/// [`LinkedChunk`]: super::LinkedChunk +#[derive(Debug)] +pub struct RelationalLinkedChunk { + /// Chunks. + chunks: Vec, + + /// Items. + items: Vec>, +} + +impl RelationalLinkedChunk { + /// Create a new relational linked chunk. + pub fn new() -> Self { + Self { chunks: Vec::new(), items: Vec::new() } + } + + /// Apply [`Update`]s. That's the only way to write data inside this + /// relational linked chunk. + pub fn apply_updates(&mut self, room_id: &RoomId, updates: Vec>) { + for update in updates { + match update { + Update::NewItemsChunk { previous, new, next } => { + insert_chunk(&mut self.chunks, room_id, previous, new, next); + } + + Update::NewGapChunk { previous, new, next, gap } => { + insert_chunk(&mut self.chunks, room_id, previous, new, next); + self.items.push(ItemRow { + room_id: room_id.to_owned(), + position: Position::new(new, 0), + item: Either::Gap(gap), + }); + } + + Update::RemoveChunk(chunk_identifier) => { + remove_chunk(&mut self.chunks, room_id, chunk_identifier); + + let indices_to_remove = self + .items + .iter() + .enumerate() + .filter_map( + |(nth, ItemRow { room_id: room_id_candidate, position, .. })| { + (room_id == room_id_candidate + && position.chunk_identifier() == chunk_identifier) + .then_some(nth) + }, + ) + .collect::>(); + + for index_to_remove in indices_to_remove.into_iter().rev() { + self.items.remove(index_to_remove); + } + } + + Update::PushItems { mut at, items } => { + for item in items { + self.items.push(ItemRow { + room_id: room_id.to_owned(), + position: at, + item: Either::Item(item), + }); + at.increment_index(); + } + } + + Update::RemoveItem { at } => { + let mut entry_to_remove = None; + + for (nth, ItemRow { room_id: room_id_candidate, position, .. }) in + self.items.iter_mut().enumerate() + { + // Filter by room ID. + if room_id != room_id_candidate { + continue; + } + + // Find the item to remove. + if *position == at { + debug_assert!(entry_to_remove.is_none(), "Found the same entry twice"); + + entry_to_remove = Some(nth); + } + + // Update all items that come _after_ `at` to shift their index. + if position.chunk_identifier() == at.chunk_identifier() + && position.index() > at.index() + { + position.decrement_index(); + } + } + + self.items.remove(entry_to_remove.expect("Remove an unknown item")); + } + + Update::DetachLastItems { at } => { + let indices_to_remove = self + .items + .iter() + .enumerate() + .filter_map( + |(nth, ItemRow { room_id: room_id_candidate, position, .. })| { + (room_id == room_id_candidate + && position.chunk_identifier() == at.chunk_identifier() + && position.index() >= at.index()) + .then_some(nth) + }, + ) + .collect::>(); + + for index_to_remove in indices_to_remove.into_iter().rev() { + self.items.remove(index_to_remove); + } + } + + Update::StartReattachItems | Update::EndReattachItems => { /* nothing */ } + } + } + + fn insert_chunk( + chunks: &mut Vec, + room_id: &RoomId, + previous: Option, + new: ChunkIdentifier, + next: Option, + ) { + // Find the previous chunk, and update its next chunk. + if let Some(previous) = previous { + let entry_for_previous_chunk = chunks + .iter_mut() + .find(|ChunkRow { room_id: room_id_candidate, chunk, .. }| { + room_id == room_id_candidate && *chunk == previous + }) + .expect("Previous chunk should be present"); + + // Link the chunk. + entry_for_previous_chunk.next_chunk = Some(new); + } + + // Find the next chunk, and update its previous chunk. + if let Some(next) = next { + let entry_for_next_chunk = chunks + .iter_mut() + .find(|ChunkRow { room_id: room_id_candidate, chunk, .. }| { + room_id == room_id_candidate && *chunk == next + }) + .expect("Next chunk should be present"); + + // Link the chunk. + entry_for_next_chunk.previous_chunk = Some(new); + } + + // Insert the chunk. + chunks.push(ChunkRow { + room_id: room_id.to_owned(), + previous_chunk: previous, + chunk: new, + next_chunk: next, + }); + } + + fn remove_chunk( + chunks: &mut Vec, + room_id: &RoomId, + chunk_to_remove: ChunkIdentifier, + ) { + let entry_nth_to_remove = chunks + .iter() + .enumerate() + .find_map(|(nth, ChunkRow { room_id: room_id_candidate, chunk, .. })| { + (room_id == room_id_candidate && *chunk == chunk_to_remove).then_some(nth) + }) + .expect("Remove an unknown chunk"); + + let ChunkRow { room_id, previous_chunk: previous, next_chunk: next, .. } = + chunks.remove(entry_nth_to_remove); + + // Find the previous chunk, and update its next chunk. + if let Some(previous) = previous { + let entry_for_previous_chunk = chunks + .iter_mut() + .find(|ChunkRow { room_id: room_id_candidate, chunk, .. }| { + &room_id == room_id_candidate && *chunk == previous + }) + .expect("Previous chunk should be present"); + + // Insert the chunk. + entry_for_previous_chunk.next_chunk = next; + } + + // Find the next chunk, and update its previous chunk. + if let Some(next) = next { + let entry_for_next_chunk = chunks + .iter_mut() + .find(|ChunkRow { room_id: room_id_candidate, chunk, .. }| { + &room_id == room_id_candidate && *chunk == next + }) + .expect("Next chunk should be present"); + + // Insert the chunk. + entry_for_next_chunk.previous_chunk = previous; + } + } + } +} + +impl Default for RelationalLinkedChunk { + fn default() -> Self { + Self::new() + } +} + +#[cfg(test)] +mod tests { + use ruma::room_id; + + use super::{ChunkIdentifier as CId, *}; + + #[test] + fn test_new_items_chunk() { + let room_id = room_id!("!r0:matrix.org"); + let mut relational_linked_chunk = RelationalLinkedChunk::::new(); + + relational_linked_chunk.apply_updates( + room_id, + vec![ + // 0 + Update::NewItemsChunk { previous: None, new: CId::new(0), next: None }, + // 1 after 0 + Update::NewItemsChunk { previous: Some(CId::new(0)), new: CId::new(1), next: None }, + // 2 before 0 + Update::NewItemsChunk { previous: None, new: CId::new(2), next: Some(CId::new(0)) }, + // 3 between 2 and 0 + Update::NewItemsChunk { + previous: Some(CId::new(2)), + new: CId::new(3), + next: Some(CId::new(0)), + }, + ], + ); + + // Chunks are correctly linked. + assert_eq!( + relational_linked_chunk.chunks, + &[ + ChunkRow { + room_id: room_id.to_owned(), + previous_chunk: Some(CId::new(3)), + chunk: CId::new(0), + next_chunk: Some(CId::new(1)) + }, + ChunkRow { + room_id: room_id.to_owned(), + previous_chunk: Some(CId::new(0)), + chunk: CId::new(1), + next_chunk: None + }, + ChunkRow { + room_id: room_id.to_owned(), + previous_chunk: None, + chunk: CId::new(2), + next_chunk: Some(CId::new(3)) + }, + ChunkRow { + room_id: room_id.to_owned(), + previous_chunk: Some(CId::new(2)), + chunk: CId::new(3), + next_chunk: Some(CId::new(0)) + }, + ], + ); + // Items have not been modified. + assert!(relational_linked_chunk.items.is_empty()); + } + + #[test] + fn test_new_gap_chunk() { + let room_id = room_id!("!r0:matrix.org"); + let mut relational_linked_chunk = RelationalLinkedChunk::::new(); + + relational_linked_chunk.apply_updates( + room_id, + vec![ + // 0 + Update::NewItemsChunk { previous: None, new: CId::new(0), next: None }, + // 1 after 0 + Update::NewGapChunk { + previous: Some(CId::new(0)), + new: CId::new(1), + next: None, + gap: (), + }, + // 2 after 1 + Update::NewItemsChunk { previous: Some(CId::new(1)), new: CId::new(2), next: None }, + ], + ); + + // Chunks are correctly linked. + assert_eq!( + relational_linked_chunk.chunks, + &[ + ChunkRow { + room_id: room_id.to_owned(), + previous_chunk: None, + chunk: CId::new(0), + next_chunk: Some(CId::new(1)) + }, + ChunkRow { + room_id: room_id.to_owned(), + previous_chunk: Some(CId::new(0)), + chunk: CId::new(1), + next_chunk: Some(CId::new(2)) + }, + ChunkRow { + room_id: room_id.to_owned(), + previous_chunk: Some(CId::new(1)), + chunk: CId::new(2), + next_chunk: None + }, + ], + ); + // Items contains the gap. + assert_eq!( + relational_linked_chunk.items, + &[ItemRow { + room_id: room_id.to_owned(), + position: Position::new(CId::new(1), 0), + item: Either::Gap(()) + }], + ); + } + + #[test] + fn test_remove_chunk() { + let room_id = room_id!("!r0:matrix.org"); + let mut relational_linked_chunk = RelationalLinkedChunk::::new(); + + relational_linked_chunk.apply_updates( + room_id, + vec![ + // 0 + Update::NewItemsChunk { previous: None, new: CId::new(0), next: None }, + // 1 after 0 + Update::NewGapChunk { + previous: Some(CId::new(0)), + new: CId::new(1), + next: None, + gap: (), + }, + // 2 after 1 + Update::NewItemsChunk { previous: Some(CId::new(1)), new: CId::new(2), next: None }, + // remove 1 + Update::RemoveChunk(CId::new(1)), + ], + ); + + // Chunks are correctly linked. + assert_eq!( + relational_linked_chunk.chunks, + &[ + ChunkRow { + room_id: room_id.to_owned(), + previous_chunk: None, + chunk: CId::new(0), + next_chunk: Some(CId::new(2)) + }, + ChunkRow { + room_id: room_id.to_owned(), + previous_chunk: Some(CId::new(0)), + chunk: CId::new(2), + next_chunk: None + }, + ], + ); + // Items no longer contains the gap. + assert!(relational_linked_chunk.items.is_empty()); + } + + #[test] + fn test_push_items() { + let room_id = room_id!("!r0:matrix.org"); + let mut relational_linked_chunk = RelationalLinkedChunk::::new(); + + relational_linked_chunk.apply_updates( + room_id, + vec![ + // new chunk (this is not mandatory for this test, but let's try to be realistic) + Update::NewItemsChunk { previous: None, new: CId::new(0), next: None }, + // new items on 0 + Update::PushItems { at: Position::new(CId::new(0), 0), items: vec!['a', 'b', 'c'] }, + // new chunk (to test new items are pushed in the correct chunk) + Update::NewItemsChunk { previous: Some(CId::new(0)), new: CId::new(1), next: None }, + // new items on 1 + Update::PushItems { at: Position::new(CId::new(1), 0), items: vec!['x', 'y', 'z'] }, + // new items on 0 again + Update::PushItems { at: Position::new(CId::new(0), 3), items: vec!['d', 'e'] }, + ], + ); + + // Chunks are correctly linked. + assert_eq!( + relational_linked_chunk.chunks, + &[ + ChunkRow { + room_id: room_id.to_owned(), + previous_chunk: None, + chunk: CId::new(0), + next_chunk: Some(CId::new(1)) + }, + ChunkRow { + room_id: room_id.to_owned(), + previous_chunk: Some(CId::new(0)), + chunk: CId::new(1), + next_chunk: None + }, + ], + ); + // Items contains the pushed items. + assert_eq!( + relational_linked_chunk.items, + &[ + ItemRow { + room_id: room_id.to_owned(), + position: Position::new(CId::new(0), 0), + item: Either::Item('a') + }, + ItemRow { + room_id: room_id.to_owned(), + position: Position::new(CId::new(0), 1), + item: Either::Item('b') + }, + ItemRow { + room_id: room_id.to_owned(), + position: Position::new(CId::new(0), 2), + item: Either::Item('c') + }, + ItemRow { + room_id: room_id.to_owned(), + position: Position::new(CId::new(1), 0), + item: Either::Item('x') + }, + ItemRow { + room_id: room_id.to_owned(), + position: Position::new(CId::new(1), 1), + item: Either::Item('y') + }, + ItemRow { + room_id: room_id.to_owned(), + position: Position::new(CId::new(1), 2), + item: Either::Item('z') + }, + ItemRow { + room_id: room_id.to_owned(), + position: Position::new(CId::new(0), 3), + item: Either::Item('d') + }, + ItemRow { + room_id: room_id.to_owned(), + 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 mut relational_linked_chunk = RelationalLinkedChunk::::new(); + + relational_linked_chunk.apply_updates( + room_id, + vec![ + // new chunk (this is not mandatory for this test, but let's try to be realistic) + Update::NewItemsChunk { previous: None, new: CId::new(0), next: None }, + // new items on 0 + Update::PushItems { + at: Position::new(CId::new(0), 0), + items: vec!['a', 'b', 'c', 'd', 'e'], + }, + // remove an item: 'a' + Update::RemoveItem { at: Position::new(CId::new(0), 0) }, + // remove an item: 'd' + Update::RemoveItem { at: Position::new(CId::new(0), 2) }, + ], + ); + + // Chunks are correctly linked. + assert_eq!( + relational_linked_chunk.chunks, + &[ChunkRow { + room_id: room_id.to_owned(), + previous_chunk: None, + chunk: CId::new(0), + next_chunk: None + }], + ); + // Items contains the pushed items. + assert_eq!( + relational_linked_chunk.items, + &[ + ItemRow { + room_id: room_id.to_owned(), + position: Position::new(CId::new(0), 0), + item: Either::Item('b') + }, + ItemRow { + room_id: room_id.to_owned(), + position: Position::new(CId::new(0), 1), + item: Either::Item('c') + }, + ItemRow { + room_id: room_id.to_owned(), + 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 mut relational_linked_chunk = RelationalLinkedChunk::::new(); + + relational_linked_chunk.apply_updates( + room_id, + vec![ + // new chunk + Update::NewItemsChunk { previous: None, new: CId::new(0), next: None }, + // new chunk + Update::NewItemsChunk { previous: Some(CId::new(0)), new: CId::new(1), next: None }, + // new items on 0 + Update::PushItems { + at: Position::new(CId::new(0), 0), + items: vec!['a', 'b', 'c', 'd', 'e'], + }, + // new items on 1 + Update::PushItems { at: Position::new(CId::new(1), 0), items: vec!['x', 'y', 'z'] }, + // detach last items on 0 + Update::DetachLastItems { at: Position::new(CId::new(0), 2) }, + ], + ); + + // Chunks are correctly linked. + assert_eq!( + relational_linked_chunk.chunks, + &[ + ChunkRow { + room_id: room_id.to_owned(), + previous_chunk: None, + chunk: CId::new(0), + next_chunk: Some(CId::new(1)) + }, + ChunkRow { + room_id: room_id.to_owned(), + previous_chunk: Some(CId::new(0)), + chunk: CId::new(1), + next_chunk: None + }, + ], + ); + // Items contains the pushed items. + assert_eq!( + relational_linked_chunk.items, + &[ + ItemRow { + room_id: room_id.to_owned(), + position: Position::new(CId::new(0), 0), + item: Either::Item('a') + }, + ItemRow { + room_id: room_id.to_owned(), + position: Position::new(CId::new(0), 1), + item: Either::Item('b') + }, + ItemRow { + room_id: room_id.to_owned(), + position: Position::new(CId::new(1), 0), + item: Either::Item('x') + }, + ItemRow { + room_id: room_id.to_owned(), + position: Position::new(CId::new(1), 1), + item: Either::Item('y') + }, + ItemRow { + room_id: room_id.to_owned(), + 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 mut relational_linked_chunk = RelationalLinkedChunk::::new(); + + relational_linked_chunk + .apply_updates(room_id, vec![Update::StartReattachItems, Update::EndReattachItems]); + + // Nothing happened. + assert!(relational_linked_chunk.chunks.is_empty()); + assert!(relational_linked_chunk.items.is_empty()); + } +} diff --git a/crates/matrix-sdk-common/src/store_locks.rs b/crates/matrix-sdk-common/src/store_locks.rs index e31acc5ca0b..08af735ed22 100644 --- a/crates/matrix-sdk-common/src/store_locks.rs +++ b/crates/matrix-sdk-common/src/store_locks.rs @@ -361,7 +361,7 @@ mod tests { impl TestStore { fn try_take_leased_lock(&self, lease_duration_ms: u32, key: &str, holder: &str) -> bool { - try_take_leased_lock(&self.leases, lease_duration_ms, key, holder) + try_take_leased_lock(&mut self.leases.write().unwrap(), lease_duration_ms, key, holder) } } @@ -502,12 +502,11 @@ mod tests { pub mod memory_store_helper { use std::{ collections::{hash_map::Entry, HashMap}, - sync::RwLock, time::{Duration, Instant}, }; pub fn try_take_leased_lock( - leases: &RwLock>, + leases: &mut HashMap, lease_duration_ms: u32, key: &str, holder: &str, @@ -515,7 +514,7 @@ pub mod memory_store_helper { let now = Instant::now(); let expiration = now + Duration::from_millis(lease_duration_ms.into()); - match leases.write().unwrap().entry(key.to_owned()) { + match leases.entry(key.to_owned()) { // There is an existing holder. Entry::Occupied(mut entry) => { let (current_holder, current_expiration) = entry.get_mut(); diff --git a/crates/matrix-sdk-crypto/src/store/memorystore.rs b/crates/matrix-sdk-crypto/src/store/memorystore.rs index 6b177ea2641..8d5da350300 100644 --- a/crates/matrix-sdk-crypto/src/store/memorystore.rs +++ b/crates/matrix-sdk-crypto/src/store/memorystore.rs @@ -632,7 +632,7 @@ impl CryptoStore for MemoryStore { key: &str, holder: &str, ) -> Result { - Ok(try_take_leased_lock(&self.leases, lease_duration_ms, key, holder)) + Ok(try_take_leased_lock(&mut self.leases.write().unwrap(), lease_duration_ms, key, holder)) } } diff --git a/crates/matrix-sdk-sqlite/src/event_cache_store.rs b/crates/matrix-sdk-sqlite/src/event_cache_store.rs index dc74999e227..5ee8d92324e 100644 --- a/crates/matrix-sdk-sqlite/src/event_cache_store.rs +++ b/crates/matrix-sdk-sqlite/src/event_cache_store.rs @@ -8,7 +8,7 @@ use matrix_sdk_base::{ media::{MediaRequestParameters, UniqueKey}, }; use matrix_sdk_store_encryption::StoreCipher; -use ruma::MilliSecondsSinceUnixEpoch; +use ruma::{MilliSecondsSinceUnixEpoch, RoomId}; use rusqlite::OptionalExtension; use tokio::fs; use tracing::debug; @@ -185,7 +185,8 @@ impl EventCacheStore for SqliteEventCacheStore { async fn handle_linked_chunk_updates( &self, - _updates: &[Update], + _room_id: &RoomId, + _updates: Vec>, ) -> Result<(), Self::Error> { todo!() }