Skip to main content

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