From fe79826c7a0329f0966aefbcc16e8b820ed83fbd Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Mon, 28 Oct 2024 12:08:43 +0100 Subject: [PATCH] feat(sdk): Find and remove duplicated events in `RoomEvents`. This patch uses the new `Deduplicator` type, along with `LinkeChunk::remove_item_at` to remove duplicated events. When a new event is received, the older one is removed. --- .../src/event_cache/deduplicator.rs | 10 +- .../src/event_cache/linked_chunk/mod.rs | 9 + .../matrix-sdk/src/event_cache/room/events.rs | 690 +++++++++++++----- 3 files changed, 544 insertions(+), 165 deletions(-) diff --git a/crates/matrix-sdk/src/event_cache/deduplicator.rs b/crates/matrix-sdk/src/event_cache/deduplicator.rs index dae5a274d74..1ac40894bac 100644 --- a/crates/matrix-sdk/src/event_cache/deduplicator.rs +++ b/crates/matrix-sdk/src/event_cache/deduplicator.rs @@ -15,11 +15,11 @@ //! Simple but efficient types to find duplicated events. See [`Deduplicator`] //! to learn more. -use std::{collections::BTreeSet, sync::Mutex}; +use std::{collections::BTreeSet, fmt, sync::Mutex}; use growable_bloom_filter::{GrowableBloom, GrowableBloomBuilder}; -use super::store::{Event, RoomEvents}; +use super::room::events::{Event, RoomEvents}; /// `Deduplicator` is an efficient type to find duplicated events. /// @@ -34,6 +34,12 @@ pub struct Deduplicator { bloom_filter: Mutex, } +impl fmt::Debug for Deduplicator { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.debug_struct("Deduplicator").finish_non_exhaustive() + } +} + impl Deduplicator { const APPROXIMATED_MAXIMUM_NUMBER_OF_EVENTS: usize = 800_000; const DESIRED_FALSE_POSITIVE_RATE: f64 = 0.001; diff --git a/crates/matrix-sdk/src/event_cache/linked_chunk/mod.rs b/crates/matrix-sdk/src/event_cache/linked_chunk/mod.rs index 1bfda5f9df7..99fd02fe5bc 100644 --- a/crates/matrix-sdk/src/event_cache/linked_chunk/mod.rs +++ b/crates/matrix-sdk/src/event_cache/linked_chunk/mod.rs @@ -948,6 +948,15 @@ impl Position { pub fn index(&self) -> usize { self.1 } + + /// Decrement the index part (see [`Self::index`]), i.e. subtract 1. + /// + /// # Panic + /// + /// This method will panic if it will underflow, i.e. if the index is 0. + pub(super) fn decrement_index(&mut self) { + self.1 = self.1.checked_sub(1).expect("Cannot decrement the index because it's already 0"); + } } /// An iterator over a [`LinkedChunk`] that traverses the chunk in backward diff --git a/crates/matrix-sdk/src/event_cache/room/events.rs b/crates/matrix-sdk/src/event_cache/room/events.rs index f1b4eba7a0a..322c5c193dd 100644 --- a/crates/matrix-sdk/src/event_cache/room/events.rs +++ b/crates/matrix-sdk/src/event_cache/room/events.rs @@ -12,9 +12,16 @@ // See the License for the specific language governing permissions and // limitations under the License. +use std::cmp::Ordering; + use matrix_sdk_common::deserialized_responses::SyncTimelineEvent; +use ruma::OwnedEventId; +use tracing::{debug, error, warn}; -use super::super::linked_chunk::{Chunk, ChunkIdentifier, Error, Iter, LinkedChunk, Position}; +use super::super::{ + deduplicator::{Decoration, Deduplicator}, + linked_chunk::{Chunk, ChunkIdentifier, Error, Iter, LinkedChunk, Position}, +}; /// An alias for the real event type. pub(crate) type Event = SyncTimelineEvent; @@ -33,6 +40,9 @@ const DEFAULT_CHUNK_CAPACITY: usize = 128; pub struct RoomEvents { /// The real in-memory storage for all the events. chunks: LinkedChunk, + + /// The events deduplicator instance to help finding duplicates. + deduplicator: Deduplicator, } impl Default for RoomEvents { @@ -44,7 +54,7 @@ impl Default for RoomEvents { impl RoomEvents { /// Build a new [`RoomEvents`] struct with zero events. pub fn new() -> Self { - Self { chunks: LinkedChunk::new() } + Self { chunks: LinkedChunk::new(), deduplicator: Deduplicator::new() } } /// Clear all events. @@ -58,9 +68,18 @@ impl RoomEvents { pub fn push_events(&mut self, events: I) where I: IntoIterator, - I::IntoIter: ExactSizeIterator, { - self.chunks.push_items_back(events) + let (unique_events, duplicated_event_ids) = + self.filter_duplicated_events(events.into_iter()); + + // Remove the _old_ duplicated events! + // + // We don't have to worry the removals can change the position of the existing + // events, because we are pushing all _new_ `events` at the back. + self.remove_events(duplicated_event_ids); + + // Push new `events`. + self.chunks.push_items_back(unique_events); } /// Push a gap after all events or gaps. @@ -69,12 +88,21 @@ impl RoomEvents { } /// Insert events at a specified position. - pub fn insert_events_at(&mut self, events: I, position: Position) -> Result<(), Error> + pub fn insert_events_at(&mut self, events: I, mut position: Position) -> Result<(), Error> where I: IntoIterator, - I::IntoIter: ExactSizeIterator, { - self.chunks.insert_items_at(events, position) + let (unique_events, duplicated_event_ids) = + self.filter_duplicated_events(events.into_iter()); + + // Remove the _old_ duplicated events! + // + // We **have to worry* the removals can change the position of the + // existing events. We **have** to update the `position` + // argument value for each removal. + self.remove_events_and_update_insert_position(duplicated_event_ids, &mut position); + + self.chunks.insert_items_at(unique_events, position) } /// Insert a gap at a specified position. @@ -96,9 +124,19 @@ impl RoomEvents { ) -> Result<&Chunk, Error> where I: IntoIterator, - I::IntoIter: ExactSizeIterator, { - self.chunks.replace_gap_at(events, gap_identifier) + let (unique_events, duplicated_event_ids) = + self.filter_duplicated_events(events.into_iter()); + + // Remove the _old_ duplicated events! + // + // We don't have to worry the removals can change the position of the existing + // events, because we are replacing a gap: its identifier will not change + // because of the removals. + self.remove_events(duplicated_event_ids); + + // Replace the gap by new events. + self.chunks.replace_gap_at(unique_events, gap_identifier) } /// Search for a chunk, and return its identifier. @@ -129,6 +167,148 @@ impl RoomEvents { pub fn events(&self) -> impl Iterator { self.chunks.items() } + + /// Deduplicate `events` considering all events in `Self::chunks`. + /// + /// The returned tuple contains (i) the unique events, and (ii) the + /// duplicated events (by ID). + fn filter_duplicated_events<'a, I>(&'a mut self, events: I) -> (Vec, Vec) + where + I: Iterator + 'a, + { + let mut duplicated_event_ids = Vec::new(); + + let deduplicated_events = self + .deduplicator + .scan_and_learn(events, self) + .filter_map(|decorated_event| match decorated_event { + Decoration::Unique(event) => Some(event), + Decoration::Duplicated(event) => { + debug!(event_id = ?event.event_id(), "Found a duplicated event"); + + duplicated_event_ids.push( + event + .event_id() + // SAFETY: An event with no ID is decorated as `Decoration::Invalid`. + // Thus, it's safe to unwrap the `Option` here. + .expect("The event has no ID"), + ); + + // Keep the new event! + Some(event) + } + Decoration::Invalid(event) => { + warn!(?event, "Found an event with no ID"); + + None + } + }) + .collect(); + + (deduplicated_events, duplicated_event_ids) + } +} + +// Private implementations, implementation specific. +impl RoomEvents { + /// Remove some events from `Self::chunks`. + /// + /// This method iterates over all event IDs in `event_ids` and removes the + /// associated event (if it exists) from `Self::chunks`. + /// + /// This is used to remove duplicated events, see + /// [`Self::filter_duplicated_events`]. + fn remove_events(&mut self, event_ids: Vec) { + for event_id in event_ids { + let Some(event_position) = self.revents().find_map(|(position, event)| { + (event.event_id().as_ref() == Some(&event_id)).then_some(position) + }) else { + error!(?event_id, "Cannot find the event to remove"); + + continue; + }; + + self.chunks + .remove_item_at(event_position) + .expect("Failed to remove an event we have just found"); + } + } + + /// Remove all events from `Self::chunks` and update a fix [`Position`]. + /// + /// This method iterates over all event IDs in `event_ids` and removes the + /// associated event (if it exists) from `Self::chunks`, exactly like + /// [`Self::remove_events`]. The difference is that it will maintain a + /// [`Position`] according to the removals. This is useful for example if + /// one needs to insert events at a particular position, but it first + /// collects events that must be removed before the insertions (e.g. + /// duplicated events). One has to remove events, but also to maintain the + /// `Position` to its correct initial _target_. Let's see a practical + /// example: + /// + /// ```text + /// // Pseudo-code. + /// + /// let room_events = room_events(['a', 'b', 'c']); + /// let position = position_of('b' in room_events); + /// room_events.remove_events(['a']) + /// + /// // `position` no longer targets 'b', it now targets 'c', because all + /// // items have shifted to the left once. Instead, let's do: + /// + /// let room_events = room_events(['a', 'b', 'c']); + /// let position = position_of('b' in room_events); + /// room_events.remove_events_and_update_insert_position(['a'], &mut position) + /// + /// // `position` has been updated to still target 'b'. + /// ``` + /// + /// This is used to remove duplicated events, see + /// [`Self::filter_duplicated_events`]. + fn remove_events_and_update_insert_position( + &mut self, + event_ids: Vec, + position: &mut Position, + ) { + for event_id in event_ids { + let Some(event_position) = self.revents().find_map(|(position, event)| { + (event.event_id().as_ref() == Some(&event_id)).then_some(position) + }) else { + error!(?event_id, "Cannot find the event to remove"); + + continue; + }; + + self.chunks + .remove_item_at(event_position) + .expect("Failed to remove an event we have just found"); + + // A `Position` is composed of a `ChunkIdentifier` and an index. + // The `ChunkIdentifier` is stable, i.e. it won't change if an + // event is removed in another chunk. It means we only need to + // update `position` if the removal happened in **the same + // chunk**. + if event_position.chunk_identifier() == position.chunk_identifier() { + // Now we can compare the position indices. + match event_position.index().cmp(&position.index()) { + // `event_position`'s index < `position`'s index + Ordering::Less => { + // An event has been removed _before_ the new + // events: `position` needs to be shifted to the + // left by 1. + position.decrement_index(); + } + + // `event_position`'s index >= `position`'s index + Ordering::Equal | Ordering::Greater => { + // An event has been removed at the _same_ position of + // or _after_ the new events: `position` does _NOT_ need + // to be modified. + } + } + } + } + } } #[cfg(test)] @@ -139,6 +319,23 @@ mod tests { use super::*; + macro_rules! assert_events_eq { + ( $events_iterator:expr, [ $( ( $event_id:ident at ( $chunk_identifier:literal, $index:literal ) ) ),* $(,)? ] ) => { + { + let mut events = $events_iterator; + + $( + assert_let!(Some((position, event)) = events.next()); + assert_eq!(position.chunk_identifier(), $chunk_identifier ); + assert_eq!(position.index(), $index ); + assert_eq!(event.event_id().unwrap(), $event_id ); + )* + + assert!(events.next().is_none(), "No more events are expected"); + } + }; + } + fn new_event(event_builder: &EventBuilder, event_id: &str) -> (OwnedEventId, Event) { let event_id = EventId::parse(event_id).unwrap(); @@ -171,26 +368,14 @@ mod tests { room_events.push_events([event_0, event_1]); room_events.push_events([event_2]); - { - let mut events = room_events.events(); - - assert_let!(Some((position, event)) = events.next()); - assert_eq!(position.chunk_identifier(), 0); - assert_eq!(position.index(), 0); - assert_eq!(event.event_id().unwrap(), event_id_0); - - assert_let!(Some((position, event)) = events.next()); - assert_eq!(position.chunk_identifier(), 0); - assert_eq!(position.index(), 1); - assert_eq!(event.event_id().unwrap(), event_id_1); - - assert_let!(Some((position, event)) = events.next()); - assert_eq!(position.chunk_identifier(), 0); - assert_eq!(position.index(), 2); - assert_eq!(event.event_id().unwrap(), event_id_2); - - assert!(events.next().is_none()); - } + assert_events_eq!( + room_events.events(), + [ + (event_id_0 at (0, 0)), + (event_id_1 at (0, 1)), + (event_id_2 at (0, 2)), + ] + ); } #[test] @@ -198,27 +383,31 @@ mod tests { let event_builder = EventBuilder::new(); let (event_id_0, event_0) = new_event(&event_builder, "$ev0"); + let (event_id_1, event_1) = new_event(&event_builder, "$ev1"); let mut room_events = RoomEvents::new(); - room_events.push_events([event_0.clone()]); - room_events.push_events([event_0]); - - { - let mut events = room_events.events(); + room_events.push_events([event_0.clone(), event_1]); - assert_let!(Some((position, event)) = events.next()); - assert_eq!(position.chunk_identifier(), 0); - assert_eq!(position.index(), 0); - assert_eq!(event.event_id().unwrap(), event_id_0); + assert_events_eq!( + room_events.events(), + [ + (event_id_0 at (0, 0)), + (event_id_1 at (0, 1)), + ] + ); - assert_let!(Some((position, event)) = events.next()); - assert_eq!(position.chunk_identifier(), 0); - assert_eq!(position.index(), 1); - assert_eq!(event.event_id().unwrap(), event_id_0); + // Everything is alright. Now let's push a duplicated event. + room_events.push_events([event_0]); - assert!(events.next().is_none()); - } + assert_events_eq!( + room_events.events(), + [ + // The first `event_id_0` has been removed. + (event_id_1 at (0, 0)), + (event_id_0 at (0, 1)), + ] + ); } #[test] @@ -234,21 +423,13 @@ mod tests { room_events.push_gap(Gap { prev_token: "hello".to_owned() }); room_events.push_events([event_1]); - { - let mut events = room_events.events(); - - assert_let!(Some((position, event)) = events.next()); - assert_eq!(position.chunk_identifier(), 0); - assert_eq!(position.index(), 0); - assert_eq!(event.event_id().unwrap(), event_id_0); - - assert_let!(Some((position, event)) = events.next()); - assert_eq!(position.chunk_identifier(), 2); - assert_eq!(position.index(), 0); - assert_eq!(event.event_id().unwrap(), event_id_1); - - assert!(events.next().is_none()); - } + assert_events_eq!( + room_events.events(), + [ + (event_id_0 at (0, 0)), + (event_id_1 at (2, 0)), + ] + ); { let mut chunks = room_events.chunks(); @@ -287,69 +468,60 @@ mod tests { room_events.insert_events_at([event_2], position_of_event_1).unwrap(); - { - let mut events = room_events.events(); - - assert_let!(Some((position, event)) = events.next()); - assert_eq!(position.chunk_identifier(), 0); - assert_eq!(position.index(), 0); - assert_eq!(event.event_id().unwrap(), event_id_0); - - assert_let!(Some((position, event)) = events.next()); - assert_eq!(position.chunk_identifier(), 0); - assert_eq!(position.index(), 1); - assert_eq!(event.event_id().unwrap(), event_id_2); - - assert_let!(Some((position, event)) = events.next()); - assert_eq!(position.chunk_identifier(), 0); - assert_eq!(position.index(), 2); - assert_eq!(event.event_id().unwrap(), event_id_1); - - assert!(events.next().is_none()); - } + assert_events_eq!( + room_events.events(), + [ + (event_id_0 at (0, 0)), + (event_id_2 at (0, 1)), + (event_id_1 at (0, 2)), + ] + ); } #[test] - fn test_insert_events_at_with_dupicates() { + fn test_insert_events_at_with_duplicates() { let event_builder = EventBuilder::new(); let (event_id_0, event_0) = new_event(&event_builder, "$ev0"); let (event_id_1, event_1) = new_event(&event_builder, "$ev1"); + let (event_id_2, event_2) = new_event(&event_builder, "$ev2"); + let (event_id_3, event_3) = new_event(&event_builder, "$ev3"); let mut room_events = RoomEvents::new(); - room_events.push_events([event_0, event_1.clone()]); + room_events.push_events([event_0.clone(), event_1, event_2]); - let position_of_event_1 = room_events + let position_of_event_2 = room_events .events() .find_map(|(position, event)| { - (event.event_id().unwrap() == event_id_1).then_some(position) + (event.event_id().unwrap() == event_id_2).then_some(position) }) .unwrap(); - room_events.insert_events_at([event_1], position_of_event_1).unwrap(); - - { - let mut events = room_events.events(); - - assert_let!(Some((position, event)) = events.next()); - assert_eq!(position.chunk_identifier(), 0); - assert_eq!(position.index(), 0); - assert_eq!(event.event_id().unwrap(), event_id_0); - - assert_let!(Some((position, event)) = events.next()); - assert_eq!(position.chunk_identifier(), 0); - assert_eq!(position.index(), 1); - assert_eq!(event.event_id().unwrap(), event_id_1); - - assert_let!(Some((position, event)) = events.next()); - assert_eq!(position.chunk_identifier(), 0); - assert_eq!(position.index(), 2); - assert_eq!(event.event_id().unwrap(), event_id_1); - - assert!(events.next().is_none()); - } + assert_events_eq!( + room_events.events(), + [ + (event_id_0 at (0, 0)), + (event_id_1 at (0, 1)), + (event_id_2 at (0, 2)), + ] + ); + + // Everything is alright. Now let's insert a duplicated events! + room_events.insert_events_at([event_0, event_3], position_of_event_2).unwrap(); + + assert_events_eq!( + room_events.events(), + [ + // The first `event_id_0` has been removed. + (event_id_1 at (0, 0)), + (event_id_0 at (0, 1)), + (event_id_3 at (0, 2)), + (event_id_2 at (0, 3)), + ] + ); } + #[test] fn test_insert_gap_at() { let event_builder = EventBuilder::new(); @@ -372,21 +544,13 @@ mod tests { .insert_gap_at(Gap { prev_token: "hello".to_owned() }, position_of_event_1) .unwrap(); - { - let mut events = room_events.events(); - - assert_let!(Some((position, event)) = events.next()); - assert_eq!(position.chunk_identifier(), 0); - assert_eq!(position.index(), 0); - assert_eq!(event.event_id().unwrap(), event_id_0); - - assert_let!(Some((position, event)) = events.next()); - assert_eq!(position.chunk_identifier(), 2); - assert_eq!(position.index(), 0); - assert_eq!(event.event_id().unwrap(), event_id_1); - - assert!(events.next().is_none()); - } + assert_events_eq!( + room_events.events(), + [ + (event_id_0 at (0, 0)), + (event_id_1 at (2, 0)), + ] + ); { let mut chunks = room_events.chunks(); @@ -425,26 +589,14 @@ mod tests { room_events.replace_gap_at([event_1, event_2], chunk_identifier_of_gap).unwrap(); - { - let mut events = room_events.events(); - - assert_let!(Some((position, event)) = events.next()); - assert_eq!(position.chunk_identifier(), 0); - assert_eq!(position.index(), 0); - assert_eq!(event.event_id().unwrap(), event_id_0); - - assert_let!(Some((position, event)) = events.next()); - assert_eq!(position.chunk_identifier(), 2); - assert_eq!(position.index(), 0); - assert_eq!(event.event_id().unwrap(), event_id_1); - - assert_let!(Some((position, event)) = events.next()); - assert_eq!(position.chunk_identifier(), 2); - assert_eq!(position.index(), 1); - assert_eq!(event.event_id().unwrap(), event_id_2); - - assert!(events.next().is_none()); - } + assert_events_eq!( + room_events.events(), + [ + (event_id_0 at (0, 0)), + (event_id_1 at (2, 0)), + (event_id_2 at (2, 1)), + ] + ); { let mut chunks = room_events.chunks(); @@ -465,10 +617,11 @@ mod tests { let (event_id_0, event_0) = new_event(&event_builder, "$ev0"); let (event_id_1, event_1) = new_event(&event_builder, "$ev1"); + let (event_id_2, event_2) = new_event(&event_builder, "$ev2"); let mut room_events = RoomEvents::new(); - room_events.push_events([event_0.clone()]); + room_events.push_events([event_0.clone(), event_1]); room_events.push_gap(Gap { prev_token: "hello".to_owned() }); let chunk_identifier_of_gap = room_events @@ -477,28 +630,26 @@ mod tests { .unwrap() .chunk_identifier(); - room_events.replace_gap_at([event_0, event_1], chunk_identifier_of_gap).unwrap(); - - { - let mut events = room_events.events(); - - assert_let!(Some((position, event)) = events.next()); - assert_eq!(position.chunk_identifier(), 0); - assert_eq!(position.index(), 0); - assert_eq!(event.event_id().unwrap(), event_id_0); - - assert_let!(Some((position, event)) = events.next()); - assert_eq!(position.chunk_identifier(), 2); - assert_eq!(position.index(), 0); - assert_eq!(event.event_id().unwrap(), event_id_0); - - assert_let!(Some((position, event)) = events.next()); - assert_eq!(position.chunk_identifier(), 2); - assert_eq!(position.index(), 1); - assert_eq!(event.event_id().unwrap(), event_id_1); - - assert!(events.next().is_none()); - } + assert_events_eq!( + room_events.events(), + [ + (event_id_0 at (0, 0)), + (event_id_1 at (0, 1)), + ] + ); + + // Everything is alright. Now let's replace a gap with a duplicated event. + room_events.replace_gap_at([event_0, event_2], chunk_identifier_of_gap).unwrap(); + + assert_events_eq!( + room_events.events(), + [ + // The first `event_id_0` has been removed. + (event_id_1 at (0, 0)), + (event_id_0 at (2, 0)), + (event_id_2 at (2, 1)), + ] + ); { let mut chunks = room_events.chunks(); @@ -512,4 +663,217 @@ mod tests { assert!(chunks.next().is_none()); } } + + #[test] + fn test_remove_events() { + let event_builder = EventBuilder::new(); + + let (event_id_0, event_0) = new_event(&event_builder, "$ev0"); + let (event_id_1, event_1) = new_event(&event_builder, "$ev1"); + let (event_id_2, event_2) = new_event(&event_builder, "$ev2"); + let (event_id_3, event_3) = new_event(&event_builder, "$ev3"); + + // Push some events. + let mut room_events = RoomEvents::new(); + room_events.push_events([event_0, event_1, event_2, event_3]); + + assert_events_eq!( + room_events.events(), + [ + (event_id_0 at (0, 0)), + (event_id_1 at (0, 1)), + (event_id_2 at (0, 2)), + (event_id_3 at (0, 3)), + ] + ); + + // Remove some events. + room_events.remove_events(vec![event_id_1, event_id_3]); + + assert_events_eq!( + room_events.events(), + [ + (event_id_0 at (0, 0)), + (event_id_2 at (0, 1)), + ] + ); + } + + #[test] + fn test_remove_events_unknown_event() { + let event_builder = EventBuilder::new(); + + let (event_id_0, _event_0) = new_event(&event_builder, "$ev0"); + + // Push ZERO event. + let mut room_events = RoomEvents::new(); + + assert_events_eq!(room_events.events(), []); + + // Remove one undefined event. + // No error is expected. + room_events.remove_events(vec![event_id_0]); + + assert_events_eq!(room_events.events(), []); + + let mut events = room_events.events(); + assert!(events.next().is_none()); + } + + #[test] + fn test_remove_events_and_update_insert_position() { + let event_builder = EventBuilder::new(); + + let (event_id_0, event_0) = new_event(&event_builder, "$ev0"); + let (event_id_1, event_1) = new_event(&event_builder, "$ev1"); + let (event_id_2, event_2) = new_event(&event_builder, "$ev2"); + let (event_id_3, event_3) = new_event(&event_builder, "$ev3"); + let (event_id_4, event_4) = new_event(&event_builder, "$ev4"); + let (event_id_5, event_5) = new_event(&event_builder, "$ev5"); + let (event_id_6, event_6) = new_event(&event_builder, "$ev6"); + let (event_id_7, event_7) = new_event(&event_builder, "$ev7"); + let (event_id_8, event_8) = new_event(&event_builder, "$ev8"); + + // Push some events. + let mut room_events = RoomEvents::new(); + room_events.push_events([event_0, event_1, event_2, event_3, event_4, event_5, event_6]); + room_events.push_gap(Gap { prev_token: "raclette".to_owned() }); + room_events.push_events([event_7, event_8]); + + fn position_of(room_events: &RoomEvents, event_id: &EventId) -> Position { + room_events + .events() + .find_map(|(position, event)| { + (event.event_id().unwrap() == event_id).then_some(position) + }) + .unwrap() + } + + // In the same chunk… + { + // Get the position of `event_4`. + let mut position = position_of(&room_events, &event_id_4); + + // Remove one event BEFORE `event_4`. + // + // The position must move to the left by 1. + { + let previous_position = position; + room_events + .remove_events_and_update_insert_position(vec![event_id_0], &mut position); + + assert_eq!(previous_position.chunk_identifier(), position.chunk_identifier()); + assert_eq!(previous_position.index() - 1, position.index()); + + // It still represents the position of `event_4`. + assert_eq!(position, position_of(&room_events, &event_id_4)); + } + + // Remove one event AFTER `event_4`. + // + // The position must not move. + { + let previous_position = position; + room_events + .remove_events_and_update_insert_position(vec![event_id_5], &mut position); + + assert_eq!(previous_position.chunk_identifier(), position.chunk_identifier()); + assert_eq!(previous_position.index(), position.index()); + + // It still represents the position of `event_4`. + assert_eq!(position, position_of(&room_events, &event_id_4)); + } + + // Remove one event: `event_4`. + // + // The position must not move. + { + let previous_position = position; + room_events + .remove_events_and_update_insert_position(vec![event_id_4], &mut position); + + assert_eq!(previous_position.chunk_identifier(), position.chunk_identifier()); + assert_eq!(previous_position.index(), position.index()); + } + + // Check the events. + assert_events_eq!( + room_events.events(), + [ + (event_id_1 at (0, 0)), + (event_id_2 at (0, 1)), + (event_id_3 at (0, 2)), + (event_id_6 at (0, 3)), + (event_id_7 at (2, 0)), + (event_id_8 at (2, 1)), + ] + ); + } + + // In another chunk… + { + // Get the position of `event_7`. + let mut position = position_of(&room_events, &event_id_7); + + // Remove one event BEFORE `event_7`. + // + // The position must not move because it happens in another chunk. + { + let previous_position = position; + room_events + .remove_events_and_update_insert_position(vec![event_id_1], &mut position); + + assert_eq!(previous_position.chunk_identifier(), position.chunk_identifier()); + assert_eq!(previous_position.index(), position.index()); + + // It still represents the position of `event_7`. + assert_eq!(position, position_of(&room_events, &event_id_7)); + } + + // Check the events. + assert_events_eq!( + room_events.events(), + [ + (event_id_2 at (0, 0)), + (event_id_3 at (0, 1)), + (event_id_6 at (0, 2)), + (event_id_7 at (2, 0)), + (event_id_8 at (2, 1)), + ] + ); + } + + // In the same chunk, but remove multiple events, just for the fun and to ensure + // the loop works correctly. + { + // Get the position of `event_6`. + let mut position = position_of(&room_events, &event_id_6); + + // Remove three events BEFORE `event_6`. + // + // The position must move. + { + let previous_position = position; + room_events.remove_events_and_update_insert_position( + vec![event_id_2, event_id_3, event_id_7], + &mut position, + ); + + assert_eq!(previous_position.chunk_identifier(), position.chunk_identifier()); + assert_eq!(previous_position.index() - 2, position.index()); + + // It still represents the position of `event_6`. + assert_eq!(position, position_of(&room_events, &event_id_6)); + } + + // Check the events. + assert_events_eq!( + room_events.events(), + [ + (event_id_6 at (0, 0)), + (event_id_8 at (2, 0)), + ] + ); + } + } }