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 eyeball_im::VectorDiff;
16pub use matrix_sdk_base::event_cache::{Event, Gap};
17use matrix_sdk_base::{
18    apply_redaction,
19    event_cache::store::DEFAULT_CHUNK_CAPACITY,
20    linked_chunk::{
21        lazy_loader::{self, LazyLoaderError},
22        ChunkContent, RawChunk,
23    },
24};
25use matrix_sdk_common::linked_chunk::{
26    AsVector, Chunk, ChunkIdentifier, EmptyChunk, Error, Iter, IterBackward, LinkedChunk,
27    ObservableUpdates, Position,
28};
29use ruma::{
30    events::{room::redaction::SyncRoomRedactionEvent, AnySyncTimelineEvent, MessageLikeEventType},
31    OwnedEventId, RoomVersionId,
32};
33use tracing::{error, instrument, trace, warn};
34
35/// This type represents all events of a single room.
36#[derive(Debug)]
37pub struct RoomEvents {
38    /// The real in-memory storage for all the events.
39    chunks: LinkedChunk<DEFAULT_CHUNK_CAPACITY, Event, Gap>,
40
41    /// Type mapping [`Update`]s from [`Self::chunks`] to [`VectorDiff`]s.
42    ///
43    /// [`Update`]: matrix_sdk_base::linked_chunk::Update
44    chunks_updates_as_vectordiffs: AsVector<Event, Gap>,
45}
46
47impl Default for RoomEvents {
48    fn default() -> Self {
49        Self::new()
50    }
51}
52
53impl RoomEvents {
54    /// Build a new [`RoomEvents`] struct with zero events.
55    pub fn new() -> Self {
56        Self::with_initial_linked_chunk(None)
57    }
58
59    /// Build a new [`RoomEvents`] struct with prior chunks knowledge.
60    ///
61    /// The provided [`LinkedChunk`] must have been built with update history.
62    pub fn with_initial_linked_chunk(
63        linked_chunk: Option<LinkedChunk<DEFAULT_CHUNK_CAPACITY, Event, Gap>>,
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            // SAFETY: The `LinkedChunk` has been built with `new_with_update_history`, so
70            // `as_vector` must return `Some(…)`.
71            .expect("`LinkedChunk` must have been built with `new_with_update_history`");
72
73        Self { chunks: linked_chunk, chunks_updates_as_vectordiffs }
74    }
75
76    /// Returns whether the room has at least one event.
77    pub fn is_empty(&self) -> bool {
78        self.chunks.num_items() == 0
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    /// If the given event is a redaction, try to retrieve the to-be-redacted
90    /// event in the chunk, and replace it by the redacted form.
91    #[instrument(skip_all)]
92    fn maybe_apply_new_redaction(&mut self, room_version: &RoomVersionId, event: &Event) {
93        let raw_event = event.raw();
94
95        // Do not deserialise the entire event if we aren't certain it's a
96        // `m.room.redaction`. It saves a non-negligible amount of computations.
97        let Ok(Some(MessageLikeEventType::RoomRedaction)) =
98            raw_event.get_field::<MessageLikeEventType>("type")
99        else {
100            return;
101        };
102
103        // It is a `m.room.redaction`! We can deserialize it entirely.
104
105        let Ok(AnySyncTimelineEvent::MessageLike(
106            ruma::events::AnySyncMessageLikeEvent::RoomRedaction(redaction),
107        )) = event.raw().deserialize()
108        else {
109            return;
110        };
111
112        let Some(event_id) = redaction.redacts(room_version) else {
113            warn!("missing target event id from the redaction event");
114            return;
115        };
116
117        // Replace the redacted event by a redacted form, if we knew about it.
118        let mut items = self.chunks.items();
119
120        if let Some((pos, target_event)) =
121            items.find(|(_, item)| item.event_id().as_deref() == Some(event_id))
122        {
123            // Don't redact already redacted events.
124            if let Ok(deserialized) = target_event.raw().deserialize() {
125                match deserialized {
126                    AnySyncTimelineEvent::MessageLike(ev) => {
127                        if ev.original_content().is_none() {
128                            // Already redacted.
129                            return;
130                        }
131                    }
132                    AnySyncTimelineEvent::State(ev) => {
133                        if ev.original_content().is_none() {
134                            // Already redacted.
135                            return;
136                        }
137                    }
138                }
139            }
140
141            if let Some(redacted_event) = apply_redaction(
142                target_event.raw(),
143                event.raw().cast_ref::<SyncRoomRedactionEvent>(),
144                room_version,
145            ) {
146                let mut copy = target_event.clone();
147
148                // It's safe to cast `redacted_event` here:
149                // - either the event was an `AnyTimelineEvent` cast to `AnySyncTimelineEvent`
150                //   when calling .raw(), so it's still one under the hood.
151                // - or it wasn't, and it's a plain `AnySyncTimelineEvent` in this case.
152                copy.replace_raw(redacted_event.cast());
153
154                // Get rid of the immutable borrow on self.chunks.
155                drop(items);
156
157                self.chunks
158                    .replace_item_at(pos, copy)
159                    .expect("should have been a valid position of an item");
160            }
161        } else {
162            trace!("redacted event is missing from the linked chunk");
163        }
164
165        // TODO: remove all related events too!
166    }
167
168    /// Callback to call whenever we touch events in the database.
169    pub fn on_new_events<'a>(
170        &mut self,
171        room_version: &RoomVersionId,
172        events: impl Iterator<Item = &'a Event>,
173    ) {
174        for ev in events {
175            self.maybe_apply_new_redaction(room_version, ev);
176        }
177    }
178
179    /// Push events after all events or gaps.
180    ///
181    /// The last event in `events` is the most recent one.
182    pub fn push_events<I>(&mut self, events: I)
183    where
184        I: IntoIterator<Item = Event>,
185        I::IntoIter: ExactSizeIterator,
186    {
187        self.chunks.push_items_back(events);
188    }
189
190    /// Push a gap after all events or gaps.
191    pub fn push_gap(&mut self, gap: Gap) {
192        self.chunks.push_gap_back(gap);
193    }
194
195    /// Insert events at a specified position.
196    pub fn insert_events_at(
197        &mut self,
198        events: Vec<Event>,
199        position: Position,
200    ) -> Result<(), Error> {
201        self.chunks.insert_items_at(events, position)?;
202        Ok(())
203    }
204
205    /// Insert a gap at a specified position.
206    pub fn insert_gap_at(&mut self, gap: Gap, position: Position) -> Result<(), Error> {
207        self.chunks.insert_gap_at(gap, position)
208    }
209
210    /// Remove a gap at the given position.
211    ///
212    /// Returns the next insert position, if any, left after the gap that has
213    /// just been removed.
214    pub fn remove_gap_at(&mut self, gap: ChunkIdentifier) -> Result<Option<Position>, Error> {
215        self.chunks.remove_gap_at(gap)
216    }
217
218    /// Replace the gap identified by `gap_identifier`, by events.
219    ///
220    /// Because the `gap_identifier` can represent non-gap chunk, this method
221    /// returns a `Result`.
222    ///
223    /// This method returns the position of the (first if many) newly created
224    /// `Chunk` that   contains the `items`.
225    pub fn replace_gap_at(
226        &mut self,
227        events: Vec<Event>,
228        gap_identifier: ChunkIdentifier,
229    ) -> Result<Option<Position>, Error> {
230        let next_pos = if events.is_empty() {
231            // There are no new events, so there's no need to create a new empty items
232            // chunk; instead, remove the gap.
233            self.chunks.remove_gap_at(gap_identifier)?
234        } else {
235            // Replace the gap by new events.
236            Some(self.chunks.replace_gap_at(events, gap_identifier)?.first_position())
237        };
238
239        Ok(next_pos)
240    }
241
242    /// Remove some events from the linked chunk.
243    ///
244    /// This method iterates over all event IDs in `event_ids` and removes the
245    /// associated event (if it exists) from `Self::chunks`.
246    pub fn remove_events_by_id(&mut self, event_ids: Vec<OwnedEventId>) {
247        for event_id in event_ids {
248            let Some(event_position) = self.revents().find_map(|(position, event)| {
249                (event.event_id().as_ref() == Some(&event_id)).then_some(position)
250            }) else {
251                error!(?event_id, "Cannot find the event to remove");
252
253                continue;
254            };
255
256            self.chunks
257                .remove_item_at(
258                    event_position,
259                    // If removing an event results in an empty chunk, the empty chunk is removed
260                    // because nothing is going to be inserted in it apparently.
261                    EmptyChunk::Remove,
262                )
263                .expect("Failed to remove an event we have just found");
264        }
265    }
266
267    /// Search for a chunk, and return its identifier.
268    pub fn chunk_identifier<'a, P>(&'a self, predicate: P) -> Option<ChunkIdentifier>
269    where
270        P: FnMut(&'a Chunk<DEFAULT_CHUNK_CAPACITY, Event, Gap>) -> bool,
271    {
272        self.chunks.chunk_identifier(predicate)
273    }
274
275    /// Iterate over the chunks, forward.
276    ///
277    /// The oldest chunk comes first.
278    pub fn chunks(&self) -> Iter<'_, DEFAULT_CHUNK_CAPACITY, Event, Gap> {
279        self.chunks.chunks()
280    }
281
282    /// Iterate over the chunks, backward.
283    ///
284    /// The most recent chunk comes first.
285    pub fn rchunks(&self) -> IterBackward<'_, DEFAULT_CHUNK_CAPACITY, Event, Gap> {
286        self.chunks.rchunks()
287    }
288
289    /// Iterate over the events, backward.
290    ///
291    /// The most recent event comes first.
292    pub fn revents(&self) -> impl Iterator<Item = (Position, &Event)> {
293        self.chunks.ritems()
294    }
295
296    /// Iterate over the events, forward.
297    ///
298    /// The oldest event comes first.
299    pub fn events(&self) -> impl Iterator<Item = (Position, &Event)> {
300        self.chunks.items()
301    }
302
303    /// Get all updates from the room events as [`VectorDiff`].
304    ///
305    /// Be careful that each `VectorDiff` is returned only once!
306    ///
307    /// See [`AsVector`] to learn more.
308    ///
309    /// [`Update`]: matrix_sdk_base::linked_chunk::Update
310    pub fn updates_as_vector_diffs(&mut self) -> Vec<VectorDiff<Event>> {
311        self.chunks_updates_as_vectordiffs.take()
312    }
313
314    /// Get a mutable reference to the [`LinkedChunk`] updates, aka
315    /// [`ObservableUpdates`].
316    pub(super) fn updates(&mut self) -> &mut ObservableUpdates<Event, Gap> {
317        self.chunks.updates().expect("this is always built with an update history in the ctor")
318    }
319
320    /// Return a nice debug string (a vector of lines) for the linked chunk of
321    /// events for this room.
322    pub fn debug_string(&self) -> Vec<String> {
323        let mut result = Vec::new();
324
325        for chunk in self.chunks() {
326            let content = chunk_debug_string(chunk.content());
327            let line = format!("chunk #{}: {content}", chunk.identifier().index());
328
329            result.push(line);
330        }
331
332        result
333    }
334}
335
336// Private implementations, implementation specific.
337impl RoomEvents {
338    pub(super) fn insert_new_chunk_as_first(
339        &mut self,
340        raw_new_first_chunk: RawChunk<Event, Gap>,
341    ) -> Result<(), LazyLoaderError> {
342        lazy_loader::insert_new_first_chunk(&mut self.chunks, raw_new_first_chunk)
343    }
344}
345
346/// Create a debug string for a [`ChunkContent`] for an event/gap pair.
347fn chunk_debug_string(content: &ChunkContent<Event, Gap>) -> String {
348    match content {
349        ChunkContent::Gap(Gap { prev_token }) => {
350            format!("gap['{prev_token}']")
351        }
352        ChunkContent::Items(vec) => {
353            let items = vec
354                .iter()
355                .map(|event| {
356                    // Limit event ids to 8 chars *after* the $.
357                    event.event_id().map_or_else(
358                        || "<no event id>".to_owned(),
359                        |id| id.as_str().chars().take(1 + 8).collect(),
360                    )
361                })
362                .collect::<Vec<_>>()
363                .join(", ");
364
365            format!("events[{items}]")
366        }
367    }
368}
369
370#[cfg(test)]
371mod tests {
372    use assert_matches::assert_matches;
373    use assert_matches2::assert_let;
374    use matrix_sdk_test::{event_factory::EventFactory, ALICE, DEFAULT_TEST_ROOM_ID};
375    use ruma::{event_id, user_id, EventId, OwnedEventId};
376
377    use super::*;
378
379    macro_rules! assert_events_eq {
380        ( $events_iterator:expr, [ $( ( $event_id:ident at ( $chunk_identifier:literal, $index:literal ) ) ),* $(,)? ] ) => {
381            {
382                let mut events = $events_iterator;
383
384                $(
385                    assert_let!(Some((position, event)) = events.next());
386                    assert_eq!(position.chunk_identifier(), $chunk_identifier );
387                    assert_eq!(position.index(), $index );
388                    assert_eq!(event.event_id().unwrap(), $event_id );
389                )*
390
391                assert!(events.next().is_none(), "No more events are expected");
392            }
393        };
394    }
395
396    fn new_event(event_id: &str) -> (OwnedEventId, Event) {
397        let event_id = EventId::parse(event_id).unwrap();
398        let event = EventFactory::new()
399            .text_msg("")
400            .sender(user_id!("@mnt_io:matrix.org"))
401            .event_id(&event_id)
402            .into_event();
403
404        (event_id, event)
405    }
406
407    #[test]
408    fn test_new_room_events_has_zero_events() {
409        let room_events = RoomEvents::new();
410
411        assert_eq!(room_events.events().count(), 0);
412    }
413
414    #[test]
415    fn test_push_events() {
416        let (event_id_0, event_0) = new_event("$ev0");
417        let (event_id_1, event_1) = new_event("$ev1");
418        let (event_id_2, event_2) = new_event("$ev2");
419
420        let mut room_events = RoomEvents::new();
421
422        room_events.push_events([event_0, event_1]);
423        room_events.push_events([event_2]);
424
425        assert_events_eq!(
426            room_events.events(),
427            [
428                (event_id_0 at (0, 0)),
429                (event_id_1 at (0, 1)),
430                (event_id_2 at (0, 2)),
431            ]
432        );
433    }
434
435    #[test]
436    fn test_push_gap() {
437        let (event_id_0, event_0) = new_event("$ev0");
438        let (event_id_1, event_1) = new_event("$ev1");
439
440        let mut room_events = RoomEvents::new();
441
442        room_events.push_events([event_0]);
443        room_events.push_gap(Gap { prev_token: "hello".to_owned() });
444        room_events.push_events([event_1]);
445
446        assert_events_eq!(
447            room_events.events(),
448            [
449                (event_id_0 at (0, 0)),
450                (event_id_1 at (2, 0)),
451            ]
452        );
453
454        {
455            let mut chunks = room_events.chunks();
456
457            assert_let!(Some(chunk) = chunks.next());
458            assert!(chunk.is_items());
459
460            assert_let!(Some(chunk) = chunks.next());
461            assert!(chunk.is_gap());
462
463            assert_let!(Some(chunk) = chunks.next());
464            assert!(chunk.is_items());
465
466            assert!(chunks.next().is_none());
467        }
468    }
469
470    #[test]
471    fn test_insert_events_at() {
472        let (event_id_0, event_0) = new_event("$ev0");
473        let (event_id_1, event_1) = new_event("$ev1");
474        let (event_id_2, event_2) = new_event("$ev2");
475
476        let mut room_events = RoomEvents::new();
477
478        room_events.push_events([event_0, event_1]);
479
480        let position_of_event_1 = room_events
481            .events()
482            .find_map(|(position, event)| {
483                (event.event_id().unwrap() == event_id_1).then_some(position)
484            })
485            .unwrap();
486
487        room_events.insert_events_at(vec![event_2], position_of_event_1).unwrap();
488
489        assert_events_eq!(
490            room_events.events(),
491            [
492                (event_id_0 at (0, 0)),
493                (event_id_2 at (0, 1)),
494                (event_id_1 at (0, 2)),
495            ]
496        );
497    }
498
499    #[test]
500    fn test_insert_gap_at() {
501        let (event_id_0, event_0) = new_event("$ev0");
502        let (event_id_1, event_1) = new_event("$ev1");
503
504        let mut room_events = RoomEvents::new();
505
506        room_events.push_events([event_0, event_1]);
507
508        let position_of_event_1 = room_events
509            .events()
510            .find_map(|(position, event)| {
511                (event.event_id().unwrap() == event_id_1).then_some(position)
512            })
513            .unwrap();
514
515        room_events
516            .insert_gap_at(Gap { prev_token: "hello".to_owned() }, position_of_event_1)
517            .unwrap();
518
519        assert_events_eq!(
520            room_events.events(),
521            [
522                (event_id_0 at (0, 0)),
523                (event_id_1 at (2, 0)),
524            ]
525        );
526
527        {
528            let mut chunks = room_events.chunks();
529
530            assert_let!(Some(chunk) = chunks.next());
531            assert!(chunk.is_items());
532
533            assert_let!(Some(chunk) = chunks.next());
534            assert!(chunk.is_gap());
535
536            assert_let!(Some(chunk) = chunks.next());
537            assert!(chunk.is_items());
538
539            assert!(chunks.next().is_none());
540        }
541    }
542
543    #[test]
544    fn test_replace_gap_at() {
545        let (event_id_0, event_0) = new_event("$ev0");
546        let (event_id_1, event_1) = new_event("$ev1");
547        let (event_id_2, event_2) = new_event("$ev2");
548
549        let mut room_events = RoomEvents::new();
550
551        room_events.push_events([event_0]);
552        room_events.push_gap(Gap { prev_token: "hello".to_owned() });
553
554        let chunk_identifier_of_gap = room_events
555            .chunks()
556            .find_map(|chunk| chunk.is_gap().then_some(chunk.identifier()))
557            .unwrap();
558
559        room_events.replace_gap_at(vec![event_1, event_2], chunk_identifier_of_gap).unwrap();
560
561        assert_events_eq!(
562            room_events.events(),
563            [
564                (event_id_0 at (0, 0)),
565                (event_id_1 at (2, 0)),
566                (event_id_2 at (2, 1)),
567            ]
568        );
569
570        {
571            let mut chunks = room_events.chunks();
572
573            assert_let!(Some(chunk) = chunks.next());
574            assert!(chunk.is_items());
575
576            assert_let!(Some(chunk) = chunks.next());
577            assert!(chunk.is_items());
578
579            assert!(chunks.next().is_none());
580        }
581    }
582
583    #[test]
584    fn test_replace_gap_at_with_no_new_events() {
585        let (_, event_0) = new_event("$ev0");
586        let (_, event_1) = new_event("$ev1");
587        let (_, event_2) = new_event("$ev2");
588
589        let mut room_events = RoomEvents::new();
590
591        room_events.push_events([event_0, event_1]);
592        room_events.push_gap(Gap { prev_token: "middle".to_owned() });
593        room_events.push_events([event_2]);
594        room_events.push_gap(Gap { prev_token: "end".to_owned() });
595
596        // Remove the first gap.
597        let first_gap_id = room_events
598            .chunks()
599            .find_map(|chunk| chunk.is_gap().then_some(chunk.identifier()))
600            .unwrap();
601
602        // The next insert position is the next chunk's start.
603        let pos = room_events.replace_gap_at(vec![], first_gap_id).unwrap();
604        assert_eq!(pos, Some(Position::new(ChunkIdentifier::new(2), 0)));
605
606        // Remove the second gap.
607        let second_gap_id = room_events
608            .chunks()
609            .find_map(|chunk| chunk.is_gap().then_some(chunk.identifier()))
610            .unwrap();
611
612        // No next insert position.
613        let pos = room_events.replace_gap_at(vec![], second_gap_id).unwrap();
614        assert!(pos.is_none());
615    }
616
617    #[test]
618    fn test_remove_events() {
619        let (event_id_0, event_0) = new_event("$ev0");
620        let (event_id_1, event_1) = new_event("$ev1");
621        let (event_id_2, event_2) = new_event("$ev2");
622        let (event_id_3, event_3) = new_event("$ev3");
623
624        // Push some events.
625        let mut room_events = RoomEvents::new();
626        room_events.push_events([event_0, event_1]);
627        room_events.push_gap(Gap { prev_token: "hello".to_owned() });
628        room_events.push_events([event_2, event_3]);
629
630        assert_events_eq!(
631            room_events.events(),
632            [
633                (event_id_0 at (0, 0)),
634                (event_id_1 at (0, 1)),
635                (event_id_2 at (2, 0)),
636                (event_id_3 at (2, 1)),
637            ]
638        );
639        assert_eq!(room_events.chunks().count(), 3);
640
641        // Remove some events.
642        room_events.remove_events_by_id(vec![event_id_1, event_id_3]);
643
644        assert_events_eq!(
645            room_events.events(),
646            [
647                (event_id_0 at (0, 0)),
648                (event_id_2 at (2, 0)),
649            ]
650        );
651
652        // Ensure chunks are removed once empty.
653        room_events.remove_events_by_id(vec![event_id_2]);
654
655        assert_events_eq!(
656            room_events.events(),
657            [
658                (event_id_0 at (0, 0)),
659            ]
660        );
661        assert_eq!(room_events.chunks().count(), 2);
662    }
663
664    #[test]
665    fn test_remove_events_unknown_event() {
666        let (event_id_0, _event_0) = new_event("$ev0");
667
668        // Push ZERO event.
669        let mut room_events = RoomEvents::new();
670
671        assert_events_eq!(room_events.events(), []);
672
673        // Remove one undefined event.
674        // No error is expected.
675        room_events.remove_events_by_id(vec![event_id_0]);
676
677        assert_events_eq!(room_events.events(), []);
678
679        let mut events = room_events.events();
680        assert!(events.next().is_none());
681    }
682
683    #[test]
684    fn test_reset() {
685        let (event_id_0, event_0) = new_event("$ev0");
686        let (event_id_1, event_1) = new_event("$ev1");
687        let (event_id_2, event_2) = new_event("$ev2");
688        let (event_id_3, event_3) = new_event("$ev3");
689
690        // Push some events.
691        let mut room_events = RoomEvents::new();
692        room_events.push_events([event_0, event_1]);
693        room_events.push_gap(Gap { prev_token: "raclette".to_owned() });
694        room_events.push_events([event_2]);
695
696        // Read the updates as `VectorDiff`.
697        let diffs = room_events.updates_as_vector_diffs();
698
699        assert_eq!(diffs.len(), 2);
700
701        assert_matches!(
702            &diffs[0],
703            VectorDiff::Append { values } => {
704                assert_eq!(values.len(), 2);
705                assert_eq!(values[0].event_id(), Some(event_id_0));
706                assert_eq!(values[1].event_id(), Some(event_id_1));
707            }
708        );
709        assert_matches!(
710            &diffs[1],
711            VectorDiff::Append { values } => {
712                assert_eq!(values.len(), 1);
713                assert_eq!(values[0].event_id(), Some(event_id_2));
714            }
715        );
716
717        // Now we can reset and see what happens.
718        room_events.reset();
719        room_events.push_events([event_3]);
720
721        // Read the updates as `VectorDiff`.
722        let diffs = room_events.updates_as_vector_diffs();
723
724        assert_eq!(diffs.len(), 2);
725
726        assert_matches!(&diffs[0], VectorDiff::Clear);
727        assert_matches!(
728            &diffs[1],
729            VectorDiff::Append { values } => {
730                assert_eq!(values.len(), 1);
731                assert_eq!(values[0].event_id(), Some(event_id_3));
732            }
733        );
734    }
735
736    #[test]
737    fn test_debug_string() {
738        let event_factory = EventFactory::new().room(&DEFAULT_TEST_ROOM_ID).sender(*ALICE);
739
740        let mut room_events = RoomEvents::new();
741        room_events.push_events(vec![
742            event_factory
743                .text_msg("hey")
744                .event_id(event_id!("$123456789101112131415617181920"))
745                .into_event(),
746            event_factory.text_msg("you").event_id(event_id!("$2")).into_event(),
747        ]);
748        room_events.push_gap(Gap { prev_token: "raclette".to_owned() });
749
750        let output = room_events.debug_string();
751
752        assert_eq!(output.len(), 2);
753        assert_eq!(&output[0], "chunk #0: events[$12345678, $2]");
754        assert_eq!(&output[1], "chunk #1: gap['raclette']");
755    }
756}