matrix_sdk/event_cache/room/
events.rs

1// Copyright 2024 The Matrix.org Foundation C.I.C.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use as_variant::as_variant;
16use eyeball_im::VectorDiff;
17pub use matrix_sdk_base::event_cache::{Event, Gap};
18use matrix_sdk_base::{
19    event_cache::store::DEFAULT_CHUNK_CAPACITY,
20    linked_chunk::{
21        ChunkContent, ChunkIdentifierGenerator, ChunkMetadata, OrderTracker, RawChunk,
22        lazy_loader::{self, LazyLoaderError},
23    },
24};
25use matrix_sdk_common::linked_chunk::{
26    AsVector, Chunk, ChunkIdentifier, Error, Iter, IterBackward, LinkedChunk, ObservableUpdates,
27    Position,
28};
29use tracing::trace;
30
31/// This type represents a linked chunk of events for a single room or thread.
32#[derive(Debug)]
33pub(in crate::event_cache) struct EventLinkedChunk {
34    /// The real in-memory storage for all the events.
35    chunks: LinkedChunk<DEFAULT_CHUNK_CAPACITY, Event, Gap>,
36
37    /// Type mapping [`Update`]s from [`Self::chunks`] to [`VectorDiff`]s.
38    ///
39    /// [`Update`]: matrix_sdk_base::linked_chunk::Update
40    chunks_updates_as_vectordiffs: AsVector<Event, Gap>,
41
42    /// Tracker of the events ordering in this room.
43    pub order_tracker: OrderTracker<Event, Gap>,
44}
45
46impl Default for EventLinkedChunk {
47    fn default() -> Self {
48        Self::new()
49    }
50}
51
52impl EventLinkedChunk {
53    /// Build a new [`EventLinkedChunk`] struct with zero events.
54    pub fn new() -> Self {
55        Self::with_initial_linked_chunk(None, None)
56    }
57
58    /// Build a new [`EventLinkedChunk`] struct with prior chunks knowledge.
59    ///
60    /// The provided [`LinkedChunk`] must have been built with update history.
61    pub fn with_initial_linked_chunk(
62        linked_chunk: Option<LinkedChunk<DEFAULT_CHUNK_CAPACITY, Event, Gap>>,
63        full_linked_chunk_metadata: Option<Vec<ChunkMetadata>>,
64    ) -> Self {
65        let mut linked_chunk = linked_chunk.unwrap_or_else(LinkedChunk::new_with_update_history);
66
67        let chunks_updates_as_vectordiffs = linked_chunk
68            .as_vector()
69            .expect("`LinkedChunk` must have been built with `new_with_update_history`");
70
71        let order_tracker = linked_chunk
72            .order_tracker(full_linked_chunk_metadata)
73            .expect("`LinkedChunk` must have been built with `new_with_update_history`");
74
75        Self { chunks: linked_chunk, chunks_updates_as_vectordiffs, order_tracker }
76    }
77
78    /// Clear all events.
79    ///
80    /// All events, all gaps, everything is dropped, move into the void, into
81    /// the ether, forever.
82    pub fn reset(&mut self) {
83        self.chunks.clear();
84    }
85
86    /// Push events after all events or gaps.
87    ///
88    /// The last event in `events` is the most recent one.
89    #[cfg(test)]
90    pub(in crate::event_cache) fn push_events<I>(&mut self, events: I)
91    where
92        I: IntoIterator<Item = Event>,
93        I::IntoIter: ExactSizeIterator,
94    {
95        self.chunks.push_items_back(events);
96    }
97
98    /// Replace the gap identified by `gap_identifier`, by events.
99    ///
100    /// Because the `gap_identifier` can represent non-gap chunk, this method
101    /// returns a `Result`.
102    ///
103    /// This method returns the position of the (first if many) newly created
104    /// `Chunk` that contains the `items`.
105    fn replace_gap_at(
106        &mut self,
107        gap_identifier: ChunkIdentifier,
108        events: Vec<Event>,
109    ) -> Result<Option<Position>, Error> {
110        // As an optimization, we'll remove the chunk if it's a gap that would be
111        // replaced with no events.
112        //
113        // However, our linked chunk requires that it includes at least one chunk in the
114        // in-memory representation. We could tweak this invariant, but in the
115        // meanwhile, don't remove the gap chunk if it's the only one we know
116        // about.
117        let has_only_one_chunk = {
118            let mut it = self.chunks.chunks();
119
120            // If there's no chunks at all, then we won't be able to find the gap chunk.
121            let _ =
122                it.next().ok_or(Error::InvalidChunkIdentifier { identifier: gap_identifier })?;
123
124            // If there's no next chunk, we can conclude there's only one.
125            it.next().is_none()
126        };
127
128        let next_pos = if events.is_empty() && !has_only_one_chunk {
129            // There are no new events, so there's no need to create a new empty items
130            // chunk; instead, remove the gap.
131            self.chunks.remove_empty_chunk_at(gap_identifier)?
132        } else {
133            // Replace the gap by new events.
134            Some(self.chunks.replace_gap_at(events, gap_identifier)?.first_position())
135        };
136
137        Ok(next_pos)
138    }
139
140    /// Remove some events from the linked chunk.
141    ///
142    /// If a chunk becomes empty, it's going to be removed.
143    pub fn remove_events_by_position(&mut self, mut positions: Vec<Position>) -> Result<(), Error> {
144        sort_positions_descending(&mut positions);
145
146        for position in positions {
147            self.chunks.remove_item_at(position)?;
148        }
149
150        Ok(())
151    }
152
153    /// Replace event at a specified position.
154    ///
155    /// `position` must point to a valid item, otherwise the method returns an
156    /// error.
157    pub fn replace_event_at(&mut self, position: Position, event: Event) -> Result<(), Error> {
158        self.chunks.replace_item_at(position, event)
159    }
160
161    /// Search for a chunk, and return its identifier.
162    pub fn chunk_identifier<'a, P>(&'a self, predicate: P) -> Option<ChunkIdentifier>
163    where
164        P: FnMut(&'a Chunk<DEFAULT_CHUNK_CAPACITY, Event, Gap>) -> bool,
165    {
166        self.chunks.chunk_identifier(predicate)
167    }
168
169    /// Iterate over the chunks, forward.
170    ///
171    /// The oldest chunk comes first.
172    pub fn chunks(&self) -> Iter<'_, DEFAULT_CHUNK_CAPACITY, Event, Gap> {
173        self.chunks.chunks()
174    }
175
176    /// Iterate over the chunks, backward.
177    ///
178    /// The most recent chunk comes first.
179    pub fn rchunks(&self) -> IterBackward<'_, DEFAULT_CHUNK_CAPACITY, Event, Gap> {
180        self.chunks.rchunks()
181    }
182
183    /// Iterate over the events, backward.
184    ///
185    /// The most recent event comes first.
186    pub fn revents(&self) -> impl Iterator<Item = (Position, &Event)> {
187        self.chunks.ritems()
188    }
189
190    /// Iterate over the events, forward.
191    ///
192    /// The oldest event comes first.
193    pub fn events(&self) -> impl Iterator<Item = (Position, &Event)> {
194        self.chunks.items()
195    }
196
197    /// Return the order of an event in the room linked chunk.
198    ///
199    /// Can return `None` if the event can't be found in the linked chunk.
200    pub fn event_order(&self, event_pos: Position) -> Option<usize> {
201        self.order_tracker.ordering(event_pos)
202    }
203
204    #[cfg(any(test, debug_assertions))]
205    #[allow(dead_code)] // Temporarily, until we figure out why it's crashing production builds.
206    fn assert_event_ordering(&self) {
207        let mut iter = self.chunks.items().enumerate();
208        let Some((i, (first_event_pos, _))) = iter.next() else {
209            return;
210        };
211
212        // Sanity check.
213        assert_eq!(i, 0);
214
215        // That's the offset in the full linked chunk. Will be 0 if the linked chunk is
216        // entirely loaded, may be non-zero otherwise.
217        let offset =
218            self.event_order(first_event_pos).expect("first event's ordering must be known");
219
220        for (i, (next_pos, _)) in iter {
221            let next_index =
222                self.event_order(next_pos).expect("next event's ordering must be known");
223            assert_eq!(offset + i, next_index, "event ordering must be continuous");
224        }
225    }
226
227    /// Get all updates from the room events as [`VectorDiff`].
228    ///
229    /// Be careful that each `VectorDiff` is returned only once!
230    ///
231    /// See [`AsVector`] to learn more.
232    pub fn updates_as_vector_diffs(&mut self) -> Vec<VectorDiff<Event>> {
233        let updates = self.chunks_updates_as_vectordiffs.take();
234
235        self.order_tracker.flush_updates(false);
236
237        updates
238    }
239
240    /// Get a mutable reference to the [`LinkedChunk`] updates, aka
241    /// [`ObservableUpdates`] to be consumed by the store.
242    ///
243    /// These updates are expected to be *only* forwarded to storage, as they
244    /// might hide some underlying updates to the in-memory chunk; those
245    /// updates should be reflected with manual updates to
246    /// [`Self::chunks_updates_as_vectordiffs`].
247    pub(super) fn store_updates(&mut self) -> &mut ObservableUpdates<Event, Gap> {
248        self.chunks.updates().expect("this is always built with an update history in the ctor")
249    }
250
251    /// Return a nice debug string (a vector of lines) for the linked chunk of
252    /// events for this room.
253    pub fn debug_string(&self) -> Vec<String> {
254        let mut result = Vec::new();
255
256        for chunk in self.chunks.chunks() {
257            let content =
258                chunk_debug_string(chunk.identifier(), chunk.content(), &self.order_tracker);
259            let lazy_previous = if let Some(cid) = chunk.lazy_previous() {
260                format!(" (lazy previous = {})", cid.index())
261            } else {
262                "".to_owned()
263            };
264            let line = format!("chunk #{}{lazy_previous}: {content}", chunk.identifier().index());
265
266            result.push(line);
267        }
268
269        result
270    }
271
272    /// Return the latest gap, if any.
273    ///
274    /// Latest means "closest to the end", or, since events are ordered
275    /// according to the sync ordering, this means "the most recent one".
276    pub fn rgap(&self) -> Option<Gap> {
277        self.rchunks()
278            .find_map(|chunk| as_variant!(chunk.content(), ChunkContent::Gap(gap) => gap.clone()))
279    }
280
281    /// Add the previous back-pagination token (if present), followed by the
282    /// timeline events themselves.
283    pub fn push_live_events(&mut self, new_gap: Option<Gap>, events: &[Event]) {
284        if let Some(new_gap) = new_gap {
285            // As a tiny optimization: remove the last chunk if it's an empty event
286            // one, as it's not useful to keep it before a gap.
287            let prev_chunk_to_remove = self.rchunks().next().and_then(|chunk| {
288                (chunk.is_items() && chunk.num_items() == 0).then_some(chunk.identifier())
289            });
290
291            self.chunks.push_gap_back(new_gap);
292
293            if let Some(prev_chunk_to_remove) = prev_chunk_to_remove {
294                self.chunks
295                    .remove_empty_chunk_at(prev_chunk_to_remove)
296                    .expect("we just checked the chunk is there, and it's an empty item chunk");
297            }
298        }
299
300        self.chunks.push_items_back(events.to_vec());
301    }
302
303    /// Finish a network back-pagination for this linked chunk by updating the
304    /// in-memory linked chunk with the results.
305    ///
306    /// ## Arguments
307    ///
308    /// - `prev_gap_id`: the identifier of the previous gap, if any.
309    /// - `new_gap`: the new gap to insert, if any. If missing, we've likely
310    ///   reached the start of the timeline.
311    /// - `events`: new events to insert, in the topological ordering (i.e. from
312    ///   oldest to most recent).
313    ///
314    /// ## Returns
315    ///
316    /// Returns a boolean indicating whether we've hit the start of the
317    /// timeline/linked chunk.
318    pub fn finish_back_pagination(
319        &mut self,
320        prev_gap_id: Option<ChunkIdentifier>,
321        new_gap: Option<Gap>,
322        events: &[Event],
323    ) -> bool {
324        let first_event_pos = self.events().next().map(|(item_pos, _)| item_pos);
325
326        // First, insert events.
327        let insert_new_gap_pos = if let Some(gap_id) = prev_gap_id {
328            // There is a prior gap, let's replace it with the new events!
329            trace!("replacing previous gap with the back-paginated events");
330
331            // Replace the gap with the events we just deduplicated. This might get rid of
332            // the underlying gap, if the conditions are favorable to
333            // us.
334            self.replace_gap_at(gap_id, events.to_vec())
335                .expect("gap_identifier is a valid chunk id we read previously")
336        } else if let Some(pos) = first_event_pos {
337            // No prior gap, but we had some events: assume we need to prepend events
338            // before those.
339            trace!("inserted events before the first known event");
340
341            self.chunks
342                .insert_items_at(pos, events.to_vec())
343                .expect("pos is a valid position we just read above");
344
345            Some(pos)
346        } else {
347            // No prior gap, and no prior events: push the events.
348            trace!("pushing events received from back-pagination");
349
350            self.chunks.push_items_back(events.to_vec());
351
352            // A new gap may be inserted before the new events, if there are any.
353            self.events().next().map(|(item_pos, _)| item_pos)
354        };
355
356        // And insert the new gap if needs be.
357        //
358        // We only do this when at least one new, non-duplicated event, has been added
359        // to the chunk. Otherwise it means we've back-paginated all the
360        // known events.
361        let has_new_gap = new_gap.is_some();
362        if let Some(new_gap) = new_gap {
363            if let Some(new_pos) = insert_new_gap_pos {
364                self.chunks
365                    .insert_gap_at(new_gap, new_pos)
366                    .expect("events_chunk_pos represents a valid chunk position");
367            } else {
368                self.chunks.push_gap_back(new_gap);
369            }
370        }
371
372        // There could be an inconsistency between the network (which thinks we hit the
373        // start of the timeline) and the disk (which has the initial empty
374        // chunks), so tweak the `reached_start` value so that it reflects the
375        // disk state in priority instead.
376
377        let has_gaps = self.chunks().any(|chunk| chunk.is_gap());
378
379        // Whether the first chunk has no predecessors or not.
380        let first_chunk_is_definitive_head =
381            self.chunks().next().map(|chunk| chunk.is_definitive_head());
382
383        let network_reached_start = !has_new_gap;
384        let reached_start =
385            !has_gaps && first_chunk_is_definitive_head.unwrap_or(network_reached_start);
386
387        trace!(
388            ?network_reached_start,
389            ?has_gaps,
390            ?first_chunk_is_definitive_head,
391            ?reached_start,
392            "finished handling network back-pagination"
393        );
394
395        reached_start
396    }
397}
398
399// Methods related to lazy-loading.
400impl EventLinkedChunk {
401    /// Inhibits all the linked chunk updates caused by the function `f` on the
402    /// ordering tracker.
403    ///
404    /// Updates to the linked chunk that happen because of lazy loading must not
405    /// be taken into account by the order tracker, otherwise the
406    /// fully-loaded state (tracked by the order tracker) wouldn't match
407    /// reality anymore. This provides a facility to help applying such
408    /// updates.
409    fn inhibit_updates_to_ordering_tracker<F: FnOnce(&mut Self) -> R, R>(&mut self, f: F) -> R {
410        // Start by flushing previous pending updates to the chunk ordering, if any.
411        self.order_tracker.flush_updates(false);
412
413        // Call the function.
414        let r = f(self);
415
416        // Now, flush other pending updates which have been caused by the function, and
417        // ignore them.
418        self.order_tracker.flush_updates(true);
419
420        r
421    }
422
423    /// Replace the events with the given last chunk of events and generator.
424    ///
425    /// Happens only during lazy loading.
426    ///
427    /// This clears all the chunks in memory before resetting to the new chunk,
428    /// if provided.
429    pub(super) fn replace_with(
430        &mut self,
431        last_chunk: Option<RawChunk<Event, Gap>>,
432        chunk_identifier_generator: ChunkIdentifierGenerator,
433    ) -> Result<(), LazyLoaderError> {
434        // Since `replace_with` is used only to unload some chunks, we don't want it to
435        // affect the chunk ordering.
436        self.inhibit_updates_to_ordering_tracker(move |this| {
437            lazy_loader::replace_with(&mut this.chunks, last_chunk, chunk_identifier_generator)
438        })
439    }
440
441    /// Prepends a lazily-loaded chunk at the beginning of the linked chunk.
442    pub(super) fn insert_new_chunk_as_first(
443        &mut self,
444        raw_new_first_chunk: RawChunk<Event, Gap>,
445    ) -> Result<(), LazyLoaderError> {
446        // This is only used when reinserting a chunk that was in persisted storage, so
447        // we don't need to touch the chunk ordering for this.
448        self.inhibit_updates_to_ordering_tracker(move |this| {
449            lazy_loader::insert_new_first_chunk(&mut this.chunks, raw_new_first_chunk)
450        })
451    }
452}
453
454/// Create a debug string for a [`ChunkContent`] for an event/gap pair.
455fn chunk_debug_string(
456    chunk_id: ChunkIdentifier,
457    content: &ChunkContent<Event, Gap>,
458    order_tracker: &OrderTracker<Event, Gap>,
459) -> String {
460    match content {
461        ChunkContent::Gap(Gap { prev_token }) => {
462            format!("gap['{prev_token}']")
463        }
464        ChunkContent::Items(vec) => {
465            let items = vec
466                .iter()
467                .enumerate()
468                .map(|(i, event)| {
469                    event.event_id().map_or_else(
470                        || "<no event id>".to_owned(),
471                        |id| {
472                            let pos = Position::new(chunk_id, i);
473                            let order = format!("#{}: ", order_tracker.ordering(pos).unwrap());
474
475                            // Limit event ids to 8 chars *after* the $.
476                            let event_id = id.as_str().chars().take(1 + 8).collect::<String>();
477
478                            format!("{order}{event_id}")
479                        },
480                    )
481                })
482                .collect::<Vec<_>>()
483                .join(", ");
484
485            format!("events[{items}]")
486        }
487    }
488}
489
490/// Sort positions of events so that events can be removed safely without
491/// messing their position.
492///
493/// Events must be sorted by their position index, from greatest to lowest, so
494/// that all positions remain valid inside the same chunk while they are being
495/// removed. For the sake of debugability, we also sort by position chunk
496/// identifier, but this is not required.
497pub(super) fn sort_positions_descending(positions: &mut [Position]) {
498    positions.sort_by(|a, b| {
499        b.chunk_identifier()
500            .cmp(&a.chunk_identifier())
501            .then_with(|| a.index().cmp(&b.index()).reverse())
502    });
503}
504
505#[cfg(test)]
506mod tests {
507    use assert_matches::assert_matches;
508    use assert_matches2::assert_let;
509    use matrix_sdk_test::{ALICE, DEFAULT_TEST_ROOM_ID, event_factory::EventFactory};
510    use ruma::{EventId, OwnedEventId, event_id, user_id};
511
512    use super::*;
513
514    macro_rules! assert_events_eq {
515        ( $events_iterator:expr, [ $( ( $event_id:ident at ( $chunk_identifier:literal, $index:literal ) ) ),* $(,)? ] ) => {
516            {
517                let mut events = $events_iterator;
518
519                $(
520                    assert_let!(Some((position, event)) = events.next());
521                    assert_eq!(position.chunk_identifier(), $chunk_identifier );
522                    assert_eq!(position.index(), $index );
523                    assert_eq!(event.event_id().unwrap(), $event_id );
524                )*
525
526                assert!(events.next().is_none(), "No more events are expected");
527            }
528        };
529    }
530
531    fn new_event(event_id: &str) -> (OwnedEventId, Event) {
532        let event_id = EventId::parse(event_id).unwrap();
533        let event = EventFactory::new()
534            .text_msg("")
535            .sender(user_id!("@mnt_io:matrix.org"))
536            .event_id(&event_id)
537            .into_event();
538
539        (event_id, event)
540    }
541
542    #[test]
543    fn test_new_event_linked_chunk_has_zero_events() {
544        let linked_chunk = EventLinkedChunk::new();
545
546        assert_eq!(linked_chunk.events().count(), 0);
547    }
548
549    #[test]
550    fn test_replace_gap_at() {
551        let (event_id_0, event_0) = new_event("$ev0");
552        let (event_id_1, event_1) = new_event("$ev1");
553        let (event_id_2, event_2) = new_event("$ev2");
554
555        let mut linked_chunk = EventLinkedChunk::new();
556
557        linked_chunk.chunks.push_items_back([event_0]);
558        linked_chunk.chunks.push_gap_back(Gap { prev_token: "hello".to_owned() });
559
560        let gap_chunk_id = linked_chunk
561            .chunks()
562            .find_map(|chunk| chunk.is_gap().then_some(chunk.identifier()))
563            .unwrap();
564
565        linked_chunk.replace_gap_at(gap_chunk_id, vec![event_1, event_2]).unwrap();
566
567        assert_events_eq!(
568            linked_chunk.events(),
569            [
570                (event_id_0 at (0, 0)),
571                (event_id_1 at (2, 0)),
572                (event_id_2 at (2, 1)),
573            ]
574        );
575
576        {
577            let mut chunks = linked_chunk.chunks();
578
579            assert_let!(Some(chunk) = chunks.next());
580            assert!(chunk.is_items());
581
582            assert_let!(Some(chunk) = chunks.next());
583            assert!(chunk.is_items());
584
585            assert!(chunks.next().is_none());
586        }
587    }
588
589    #[test]
590    fn test_replace_gap_at_with_no_new_events() {
591        let (_, event_0) = new_event("$ev0");
592        let (_, event_1) = new_event("$ev1");
593        let (_, event_2) = new_event("$ev2");
594
595        let mut linked_chunk = EventLinkedChunk::new();
596
597        linked_chunk.chunks.push_items_back([event_0, event_1]);
598        linked_chunk.chunks.push_gap_back(Gap { prev_token: "middle".to_owned() });
599        linked_chunk.chunks.push_items_back([event_2]);
600        linked_chunk.chunks.push_gap_back(Gap { prev_token: "end".to_owned() });
601
602        // Remove the first gap.
603        let first_gap_id = linked_chunk
604            .chunks()
605            .find_map(|chunk| chunk.is_gap().then_some(chunk.identifier()))
606            .unwrap();
607
608        // The next insert position is the next chunk's start.
609        let pos = linked_chunk.replace_gap_at(first_gap_id, vec![]).unwrap();
610        assert_eq!(pos, Some(Position::new(ChunkIdentifier::new(2), 0)));
611
612        // Remove the second gap.
613        let second_gap_id = linked_chunk
614            .chunks()
615            .find_map(|chunk| chunk.is_gap().then_some(chunk.identifier()))
616            .unwrap();
617
618        // No next insert position.
619        let pos = linked_chunk.replace_gap_at(second_gap_id, vec![]).unwrap();
620        assert!(pos.is_none());
621    }
622
623    #[test]
624    fn test_remove_events() {
625        let (event_id_0, event_0) = new_event("$ev0");
626        let (event_id_1, event_1) = new_event("$ev1");
627        let (event_id_2, event_2) = new_event("$ev2");
628        let (event_id_3, event_3) = new_event("$ev3");
629
630        // Push some events.
631        let mut linked_chunk = EventLinkedChunk::new();
632        linked_chunk.chunks.push_items_back([event_0, event_1]);
633        linked_chunk.chunks.push_gap_back(Gap { prev_token: "hello".to_owned() });
634        linked_chunk.chunks.push_items_back([event_2, event_3]);
635
636        assert_events_eq!(
637            linked_chunk.events(),
638            [
639                (event_id_0 at (0, 0)),
640                (event_id_1 at (0, 1)),
641                (event_id_2 at (2, 0)),
642                (event_id_3 at (2, 1)),
643            ]
644        );
645        assert_eq!(linked_chunk.chunks().count(), 3);
646
647        // Remove some events.
648        linked_chunk
649            .remove_events_by_position(vec![
650                Position::new(ChunkIdentifier::new(2), 1),
651                Position::new(ChunkIdentifier::new(0), 1),
652            ])
653            .unwrap();
654
655        assert_events_eq!(
656            linked_chunk.events(),
657            [
658                (event_id_0 at (0, 0)),
659                (event_id_2 at (2, 0)),
660            ]
661        );
662
663        // Ensure chunks are removed once empty.
664        linked_chunk
665            .remove_events_by_position(vec![Position::new(ChunkIdentifier::new(2), 0)])
666            .unwrap();
667
668        assert_events_eq!(
669            linked_chunk.events(),
670            [
671                (event_id_0 at (0, 0)),
672            ]
673        );
674        assert_eq!(linked_chunk.chunks().count(), 2);
675    }
676
677    #[test]
678    fn test_remove_events_unknown_event() {
679        // Push ZERO event.
680        let mut linked_chunk = EventLinkedChunk::new();
681
682        assert_events_eq!(linked_chunk.events(), []);
683
684        // Remove one undefined event.
685        // An error is expected.
686        linked_chunk
687            .remove_events_by_position(vec![Position::new(ChunkIdentifier::new(42), 153)])
688            .unwrap_err();
689
690        assert_events_eq!(linked_chunk.events(), []);
691
692        let mut events = linked_chunk.events();
693        assert!(events.next().is_none());
694    }
695
696    #[test]
697    fn test_reset() {
698        let (event_id_0, event_0) = new_event("$ev0");
699        let (event_id_1, event_1) = new_event("$ev1");
700        let (event_id_2, event_2) = new_event("$ev2");
701        let (event_id_3, event_3) = new_event("$ev3");
702
703        // Push some events.
704        let mut linked_chunk = EventLinkedChunk::new();
705        linked_chunk.chunks.push_items_back([event_0, event_1]);
706        linked_chunk.chunks.push_gap_back(Gap { prev_token: "raclette".to_owned() });
707        linked_chunk.chunks.push_items_back([event_2]);
708
709        // Read the updates as `VectorDiff`.
710        let diffs = linked_chunk.updates_as_vector_diffs();
711
712        assert_eq!(diffs.len(), 2);
713
714        assert_matches!(
715            &diffs[0],
716            VectorDiff::Append { values } => {
717                assert_eq!(values.len(), 2);
718                assert_eq!(values[0].event_id(), Some(event_id_0));
719                assert_eq!(values[1].event_id(), Some(event_id_1));
720            }
721        );
722        assert_matches!(
723            &diffs[1],
724            VectorDiff::Append { values } => {
725                assert_eq!(values.len(), 1);
726                assert_eq!(values[0].event_id(), Some(event_id_2));
727            }
728        );
729
730        // Now we can reset and see what happens.
731        linked_chunk.reset();
732        linked_chunk.chunks.push_items_back([event_3]);
733
734        // Read the updates as `VectorDiff`.
735        let diffs = linked_chunk.updates_as_vector_diffs();
736
737        assert_eq!(diffs.len(), 2);
738
739        assert_matches!(&diffs[0], VectorDiff::Clear);
740        assert_matches!(
741            &diffs[1],
742            VectorDiff::Append { values } => {
743                assert_eq!(values.len(), 1);
744                assert_eq!(values[0].event_id(), Some(event_id_3));
745            }
746        );
747    }
748
749    #[test]
750    fn test_debug_string() {
751        let event_factory = EventFactory::new().room(&DEFAULT_TEST_ROOM_ID).sender(*ALICE);
752
753        let mut linked_chunk = EventLinkedChunk::new();
754        linked_chunk.chunks.push_items_back(vec![
755            event_factory
756                .text_msg("hey")
757                .event_id(event_id!("$123456789101112131415617181920"))
758                .into_event(),
759            event_factory.text_msg("you").event_id(event_id!("$2")).into_event(),
760        ]);
761        linked_chunk.chunks.push_gap_back(Gap { prev_token: "raclette".to_owned() });
762
763        // Flush updates to the order tracker.
764        let _ = linked_chunk.updates_as_vector_diffs();
765
766        let output = linked_chunk.debug_string();
767
768        assert_eq!(output.len(), 2);
769        assert_eq!(&output[0], "chunk #0: events[#0: $12345678, #1: $2]");
770        assert_eq!(&output[1], "chunk #1: gap['raclette']");
771    }
772
773    #[test]
774    fn test_sort_positions_descending() {
775        let mut positions = vec![
776            Position::new(ChunkIdentifier::new(2), 1),
777            Position::new(ChunkIdentifier::new(1), 0),
778            Position::new(ChunkIdentifier::new(2), 0),
779            Position::new(ChunkIdentifier::new(1), 1),
780            Position::new(ChunkIdentifier::new(0), 0),
781        ];
782
783        sort_positions_descending(&mut positions);
784
785        assert_eq!(
786            positions,
787            &[
788                Position::new(ChunkIdentifier::new(2), 1),
789                Position::new(ChunkIdentifier::new(2), 0),
790                Position::new(ChunkIdentifier::new(1), 1),
791                Position::new(ChunkIdentifier::new(1), 0),
792                Position::new(ChunkIdentifier::new(0), 0),
793            ]
794        );
795    }
796}