matrix_sdk_common/linked_chunk/
as_vector.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 std::{
16    collections::VecDeque,
17    ops::{ControlFlow, Not},
18    sync::{Arc, RwLock},
19};
20
21use eyeball_im::VectorDiff;
22
23use super::{
24    updates::{ReaderToken, Update, UpdatesInner},
25    ChunkContent, ChunkIdentifier, Iter, Position,
26};
27
28/// A type alias to represent a chunk's length. This is purely for commodity.
29type ChunkLength = usize;
30
31/// A type that transforms a `Vec<Update<Item, Gap>>` (given by
32/// [`ObservableUpdates::take`](super::ObservableUpdates::take)) into a
33/// `Vec<VectorDiff<Item>>` (this type). Basically, it helps to consume a
34/// [`LinkedChunk<CAP, Item, Gap>`](super::LinkedChunk) as if it was an
35/// [`eyeball_im::ObservableVector<Item>`].
36#[derive(Debug)]
37pub struct AsVector<Item, Gap> {
38    /// Strong reference to [`UpdatesInner`].
39    updates: Arc<RwLock<UpdatesInner<Item, Gap>>>,
40
41    /// The token to read the updates.
42    token: ReaderToken,
43
44    /// Mapper from `Update` to `VectorDiff`.
45    mapper: UpdateToVectorDiff,
46}
47
48impl<Item, Gap> AsVector<Item, Gap> {
49    /// Create a new [`AsVector`].
50    ///
51    /// `updates` is the inner value of
52    /// [`ObservableUpdates`][super::updates::ObservableUpdates].
53    /// It's required to read the new [`Update`]s. `token` is the
54    /// [`ReaderToken`] necessary for this type to read the [`Update`]s.
55    /// `chunk_iterator` is the iterator of all [`Chunk`](super::Chunk)s, used
56    /// to set up its internal state.
57    pub(super) fn new<const CAP: usize>(
58        updates: Arc<RwLock<UpdatesInner<Item, Gap>>>,
59        token: ReaderToken,
60        chunk_iterator: Iter<'_, CAP, Item, Gap>,
61    ) -> Self {
62        // Drain previous updates so that this type is synced with `Updates`.
63        {
64            let mut updates = updates.write().unwrap();
65            let _ = updates.take_with_token(token);
66        }
67
68        Self { updates, token, mapper: UpdateToVectorDiff::new(chunk_iterator) }
69    }
70
71    /// Take the new updates as [`VectorDiff`].
72    ///
73    /// It returns an empty `Vec` if there is no new `VectorDiff` for the
74    /// moment.
75    pub fn take(&mut self) -> Vec<VectorDiff<Item>>
76    where
77        Item: Clone,
78    {
79        let mut updates = self.updates.write().unwrap();
80
81        self.mapper.map(updates.take_with_token(self.token))
82    }
83}
84
85/// Internal type that converts [`Update`] into [`VectorDiff`].
86#[derive(Debug)]
87struct UpdateToVectorDiff {
88    /// Pairs of all known chunks and their respective length. This is the only
89    /// required data for this algorithm.
90    chunks: VecDeque<(ChunkIdentifier, ChunkLength)>,
91}
92
93impl UpdateToVectorDiff {
94    /// Construct [`UpdateToVectorDiff`], based on an iterator of
95    /// [`Chunk`](super::Chunk)s, used to set up its own internal state.
96    ///
97    /// See [`Self::map`] to learn more about the algorithm.
98    fn new<const CAP: usize, Item, Gap>(chunk_iterator: Iter<'_, CAP, Item, Gap>) -> Self {
99        let mut initial_chunk_lengths = VecDeque::new();
100
101        for chunk in chunk_iterator {
102            initial_chunk_lengths.push_back((
103                chunk.identifier(),
104                match chunk.content() {
105                    ChunkContent::Gap(_) => 0,
106                    ChunkContent::Items(items) => items.len(),
107                },
108            ))
109        }
110
111        Self { chunks: initial_chunk_lengths }
112    }
113
114    /// Map several [`Update`] into [`VectorDiff`].
115    ///
116    /// How does this type transform `Update` into `VectorDiff`? There is no
117    /// internal buffer of kind [`eyeball_im::ObservableVector<Item>`],
118    /// which could have been used to generate the `VectorDiff`s. They are
119    /// computed manually.
120    ///
121    /// The only buffered data is pairs of [`ChunkIdentifier`] and
122    /// [`ChunkLength`]. The following rules must be respected (they are defined
123    /// in [`Self::new`]):
124    ///
125    /// * A chunk of kind [`ChunkContent::Gap`] has a length of 0,
126    /// * A chunk of kind [`ChunkContent::Items`] has a length equals to its
127    ///   number of items,
128    /// * The pairs must be ordered exactly like the chunks in [`LinkedChunk`],
129    ///   i.e. the first pair must represent the first chunk, the last pair must
130    ///   represent the last chunk.
131    ///
132    /// The only thing this algorithm does is maintaining the pairs:
133    ///
134    /// * [`Update::NewItemsChunk`] and [`Update::NewGapChunk`] are inserting a
135    ///   new pair with a chunk length of 0 at the appropriate index,
136    /// * [`Update::RemoveChunk`] is removing a pair,
137    /// * [`Update::PushItems`] is increasing the length of the appropriate pair
138    ///   by the number of new items, and is potentially emitting
139    ///   [`VectorDiff`],
140    /// * [`Update::DetachLastItems`] is decreasing the length of the
141    ///   appropriate pair by the number of items to be detached; no
142    ///   [`VectorDiff`] is emitted,
143    /// * [`Update::StartReattachItems`] and [`Update::EndReattachItems`] are
144    ///   respectively muting or unmuting the emission of [`VectorDiff`] by
145    ///   [`Update::PushItems`],
146    /// * [`Update::Clear`] reinitialises the state.
147    ///
148    /// The only `VectorDiff` that are emitted are [`VectorDiff::Insert`],
149    /// [`VectorDiff::Append`], [`VectorDiff::Remove`] and
150    /// [`VectorDiff::Clear`].
151    ///
152    /// `VectorDiff::Append` is an optimisation when numerous
153    /// `VectorDiff::Insert`s have to be emitted at the last position.
154    ///
155    /// `VectorDiff::Insert` needs an index. To compute this index, the
156    /// algorithm will iterate over all pairs to accumulate each chunk length
157    /// until it finds the appropriate pair (given by
158    /// [`Update::PushItems::at`]). This is _the offset_. To this offset, the
159    /// algorithm adds the position's index of the new items (still given by
160    /// [`Update::PushItems::at`]). This is _the index_. This logic works
161    /// for all cases as long as pairs are maintained according to the rules
162    /// hereinabove.
163    ///
164    /// That's a pretty memory compact and computation efficient way to map a
165    /// `Vec<Update<Item, Gap>>` into a `Vec<VectorDiff<Item>>`. The larger the
166    /// `LinkedChunk` capacity is, the fewer pairs the algorithm will have
167    /// to handle, e.g. for 1'000 items and a `LinkedChunk` capacity of 128,
168    /// it's only 8 pairs, that is 256 bytes.
169    ///
170    /// [`LinkedChunk`]: super::LinkedChunk
171    /// [`ChunkContent::Gap`]: super::ChunkContent::Gap
172    /// [`ChunkContent::Content`]: super::ChunkContent::Content
173    fn map<Item, Gap>(&mut self, updates: &[Update<Item, Gap>]) -> Vec<VectorDiff<Item>>
174    where
175        Item: Clone,
176    {
177        let mut diffs = Vec::with_capacity(updates.len());
178
179        // A flag specifying when updates are reattaching detached items.
180        //
181        // Why is it useful?
182        //
183        // Imagine a `LinkedChunk::<3, char, ()>` containing `['a', 'b', 'c'] ['d']`. If
184        // one wants to insert [`w`, x`, 'y', 'z'] at position
185        // `Position(ChunkIdentifier(0), 1)`, i.e. at the position of `b`, here is what
186        // happens:
187        //
188        // 1. `LinkedChunk` will split off `['a', 'b', 'c']` at index 1, the chunk
189        //    becomes `['a']` and `b` and `c` are _detached_, thus we have:
190        //
191        //     ['a'] ['d']
192        //
193        // 2. `LinkedChunk` will then insert `w`, `x`, `y` and `z` to get:
194        //
195        //     ['a', 'w', 'x'] ['y', 'z'] ['d']
196        //
197        // 3. `LinkedChunk` will now reattach `b` and `c` after `z`, like so:
198        //
199        //     ['a', 'w', 'x'] ['y', 'z', 'b'] ['c'] ['d']
200        //
201        // This detaching/reattaching approach makes it reliable and safe. Good. Now,
202        // what updates are we going to receive for each step?
203        //
204        // Step 1, detaching last items:
205        //
206        // ```
207        // Update::DetachLastItems { at: Position(ChunkIdentifier(0), 1) }
208        // ```
209        //
210        // Step 2, inserting new items:
211        //
212        // ```
213        // Update::PushItems {
214        //     at: Position(ChunkIdentifier(0), 1),
215        //     items: vec!['w', 'x'],
216        // }
217        // Update::NewItemsChunk {
218        //     previous: Some(ChunkIdentifier(0)),
219        //     new: ChunkIdentifier(2),
220        //     next: Some(ChunkIdentifier(1)),
221        // }
222        // Update::PushItems {
223        //     at: Position(ChunkIdentifier(2), 0),
224        //     items: vec!['y', 'z'],
225        // }
226        // ```
227        //
228        // Step 3, reattaching detached items:
229        //
230        // ```
231        // Update::StartReattachItems
232        // Update::PushItems {
233        //     at: Position(ChunkIdentifier(2), 2),
234        //     items: vec!['b']
235        // }
236        // Update::NewItemsChunk {
237        //     previous: Some(ChunkIdentifier(2)),
238        //     new: ChunkIdentifier(3),
239        //     next: Some(ChunkIdentifier(1)),
240        // }
241        // Update::PushItems {
242        //     at: Position(ChunkIdentifier(3), 0),
243        //     items: vec!['c'],
244        // }
245        // Update::EndReattachItems
246        // ```
247        //
248        // To ensure an optimised behaviour of this algorithm:
249        //
250        // * `Update::DetachLastItems` must not emit `VectorDiff::Remove`,
251        //
252        // * `Update::PushItems` must not emit `VectorDiff::Insert`s or
253        //   `VectorDiff::Append`s if it happens after `StartReattachItems` and before
254        //   `EndReattachItems`. However, `Self::chunks` must always be updated.
255        //
256        // From the `VectorDiff` “point of view”, this optimisation aims at avoiding
257        // removing items to push them again later.
258        let mut reattaching = false;
259        let mut detaching = false;
260
261        for update in updates {
262            match update {
263                Update::NewItemsChunk { previous, new, next }
264                | Update::NewGapChunk { previous, new, next, .. } => {
265                    match (previous, next) {
266                        // New chunk at the end.
267                        (Some(previous), None) => {
268                            debug_assert!(
269                                matches!(self.chunks.back(), Some((p, _)) if p == previous),
270                                "Inserting new chunk at the end: The previous chunk is invalid"
271                            );
272
273                            self.chunks.push_back((*new, 0));
274                        }
275
276                        // New chunk at the beginning.
277                        (None, Some(next)) => {
278                            debug_assert!(
279                                matches!(self.chunks.front(), Some((n, _)) if n == next),
280                                "Inserting new chunk at the end: The previous chunk is invalid"
281                            );
282
283                            self.chunks.push_front((*new, 0));
284                        }
285
286                        // New chunk is inserted between 2 chunks.
287                        (Some(previous), Some(next)) => {
288                            let next_chunk_index = self
289                                .chunks
290                                .iter()
291                                .position(|(chunk_identifier, _)| chunk_identifier == next)
292                                // SAFETY: Assuming `LinkedChunk` and `ObservableUpdates` are not
293                                // buggy, and assuming `Self::chunks` is correctly initialized, it
294                                // is not possible to insert a chunk between two chunks where one
295                                // does not exist. If this predicate fails, it means `LinkedChunk`
296                                // or `ObservableUpdates` contain a bug.
297                                .expect("Inserting new chunk: The chunk is not found");
298
299                            debug_assert!(
300                                matches!(self.chunks.get(next_chunk_index - 1), Some((p, _)) if p == previous),
301                                "Inserting new chunk: The previous chunk is invalid"
302                            );
303
304                            self.chunks.insert(next_chunk_index, (*new, 0));
305                        }
306
307                        // First chunk!
308                        (None, None) if self.chunks.is_empty() => {
309                            self.chunks.push_back((*new, 0));
310                        }
311
312                        // Impossible state.
313                        (None, None) => {
314                            unreachable!(
315                                "Inserting new chunk with no previous nor next chunk identifiers \
316                                is impossible"
317                            );
318                        }
319                    }
320                }
321
322                Update::RemoveChunk(expected_chunk_identifier) => {
323                    let chunk_index = self
324                        .chunks
325                        .iter()
326                        .position(|(chunk_identifier, _)| {
327                            chunk_identifier == expected_chunk_identifier
328                        })
329                        // SAFETY: Assuming `LinkedChunk` and `ObservableUpdates` are not buggy, and
330                        // assuming `Self::chunks` is correctly initialized, it is not possible to
331                        // remove a chunk that does not exist. If this predicate fails, it means
332                        // `LinkedChunk` or `ObservableUpdates` contain a bug.
333                        .expect("Removing a chunk: The chunk is not found");
334
335                    // It's OK to ignore the result. The `chunk_index` exists because it's been
336                    // found, and we don't care about its associated value.
337                    let _ = self.chunks.remove(chunk_index);
338                }
339
340                Update::PushItems { at: position, items } => {
341                    let number_of_chunks = self.chunks.len();
342                    let (offset, (chunk_index, chunk_length)) = self.map_to_offset(position);
343
344                    let is_pushing_back =
345                        chunk_index + 1 == number_of_chunks && position.index() >= *chunk_length;
346
347                    // Add the number of items to the chunk in `self.chunks`.
348                    *chunk_length += items.len();
349
350                    // See `reattaching` to learn more.
351                    if reattaching {
352                        continue;
353                    }
354
355                    // Optimisation: we can emit a `VectorDiff::Append` in this particular case.
356                    if is_pushing_back && detaching.not() {
357                        diffs.push(VectorDiff::Append { values: items.into() });
358                    }
359                    // No optimisation: let's emit `VectorDiff::Insert`.
360                    else {
361                        diffs.extend(items.iter().enumerate().map(|(nth, item)| {
362                            VectorDiff::Insert { index: offset + nth, value: item.clone() }
363                        }));
364                    }
365                }
366
367                Update::ReplaceItem { at: position, item } => {
368                    let (offset, (_chunk_index, _chunk_length)) = self.map_to_offset(position);
369
370                    // The chunk length doesn't change.
371
372                    diffs.push(VectorDiff::Set { index: offset, value: item.clone() });
373                }
374
375                Update::RemoveItem { at: position } => {
376                    let (offset, (_chunk_index, chunk_length)) = self.map_to_offset(position);
377
378                    // Remove one item to the chunk in `self.chunks`.
379                    *chunk_length -= 1;
380
381                    // See `reattaching` to learn more.
382                    if reattaching {
383                        continue;
384                    }
385
386                    // Let's emit a `VectorDiff::Remove`.
387                    diffs.push(VectorDiff::Remove { index: offset });
388                }
389
390                Update::DetachLastItems { at: position } => {
391                    let expected_chunk_identifier = position.chunk_identifier();
392                    let new_length = position.index();
393
394                    let chunk_length = self
395                        .chunks
396                        .iter_mut()
397                        .find_map(|(chunk_identifier, chunk_length)| {
398                            (*chunk_identifier == expected_chunk_identifier).then_some(chunk_length)
399                        })
400                        // SAFETY: Assuming `LinkedChunk` and `ObservableUpdates` are not buggy, and
401                        // assuming `Self::chunks` is correctly initialized, it is not possible to
402                        // detach items from a chunk that does not exist. If this predicate fails,
403                        // it means `LinkedChunk` or `ObservableUpdates` contain a bug.
404                        .expect("Detach last items: The chunk is not found");
405
406                    *chunk_length = new_length;
407
408                    // Entering the _detaching_ mode.
409                    detaching = true;
410                }
411
412                Update::StartReattachItems => {
413                    // Entering the _reattaching_ mode.
414                    reattaching = true;
415                }
416
417                Update::EndReattachItems => {
418                    // Exiting the _reattaching_ mode.
419                    reattaching = false;
420
421                    // Exiting the _detaching_ mode.
422                    detaching = false;
423                }
424
425                Update::Clear => {
426                    // Clean `self.chunks`.
427                    self.chunks.clear();
428
429                    // Let's straightforwardly emit a `VectorDiff::Clear`.
430                    diffs.push(VectorDiff::Clear);
431                }
432            }
433        }
434
435        diffs
436    }
437
438    fn map_to_offset(&mut self, position: &Position) -> (usize, (usize, &mut usize)) {
439        let expected_chunk_identifier = position.chunk_identifier();
440
441        let (offset, (chunk_index, chunk_length)) = {
442            let control_flow = self.chunks.iter_mut().enumerate().try_fold(
443                position.index(),
444                |offset, (chunk_index, (chunk_identifier, chunk_length))| {
445                    if chunk_identifier == &expected_chunk_identifier {
446                        ControlFlow::Break((offset, (chunk_index, chunk_length)))
447                    } else {
448                        ControlFlow::Continue(offset + *chunk_length)
449                    }
450                },
451            );
452
453            match control_flow {
454                // Chunk has been found, and all values have been calculated as
455                // expected.
456                ControlFlow::Break(values) => values,
457
458                // Chunk has not been found.
459                ControlFlow::Continue(..) => {
460                    // SAFETY: Assuming `LinkedChunk` and `ObservableUpdates` are not buggy, and
461                    // assuming `Self::chunks` is correctly initialized, it is not possible to work
462                    // on a chunk that does not exist. If this predicate fails, it means
463                    // `LinkedChunk` or `ObservableUpdates` contain a bug.
464                    panic!("The chunk is not found");
465                }
466            }
467        };
468
469        (offset, (chunk_index, chunk_length))
470    }
471}
472
473#[cfg(test)]
474mod tests {
475    use std::fmt::Debug;
476
477    use assert_matches::assert_matches;
478    use imbl::{vector, Vector};
479
480    use super::{
481        super::{ChunkIdentifierGenerator, EmptyChunk, LinkedChunk},
482        VectorDiff,
483    };
484
485    fn apply_and_assert_eq<Item>(
486        accumulator: &mut Vector<Item>,
487        diffs: Vec<VectorDiff<Item>>,
488        expected_diffs: &[VectorDiff<Item>],
489    ) where
490        Item: PartialEq + Clone + Debug,
491    {
492        assert_eq!(diffs, expected_diffs);
493
494        for diff in diffs {
495            diff.apply(accumulator);
496        }
497    }
498
499    #[test]
500    fn test_as_vector() {
501        let mut linked_chunk = LinkedChunk::<3, char, ()>::new_with_update_history();
502        let mut as_vector = linked_chunk.as_vector().unwrap();
503
504        let mut accumulator = Vector::new();
505
506        assert!(as_vector.take().is_empty());
507
508        linked_chunk.push_items_back(['a', 'b', 'c', 'd']);
509        #[rustfmt::skip]
510        assert_items_eq!(linked_chunk, ['a', 'b', 'c'] ['d']);
511
512        // From an `ObservableVector` point of view, it would look like:
513        //
514        // 0   1   2   3   4
515        // +---+---+---+---+
516        // | a | b | c | d |
517        // +---+---+---+---+
518        // ^^^^^^^^^^^^^^^^
519        // |
520        // new
521        apply_and_assert_eq(
522            &mut accumulator,
523            as_vector.take(),
524            &[
525                VectorDiff::Append { values: vector!['a', 'b', 'c'] },
526                VectorDiff::Append { values: vector!['d'] },
527            ],
528        );
529
530        linked_chunk
531            .insert_items_at(
532                ['w', 'x', 'y', 'z'],
533                linked_chunk.item_position(|item| *item == 'b').unwrap(),
534            )
535            .unwrap();
536        assert_items_eq!(linked_chunk, ['a', 'w', 'x'] ['y', 'z', 'b'] ['c'] ['d']);
537
538        // From an `ObservableVector` point of view, it would look like:
539        //
540        // 0   1   2   3   4   5   6   7   8
541        // +---+---+---+---+---+---+---+---+
542        // | a | w | x | y | z | b | c | d |
543        // +---+---+---+---+---+---+---+---+
544        //     ^^^^^^^^^^^^^^^^
545        //     |
546        //     new
547        apply_and_assert_eq(
548            &mut accumulator,
549            as_vector.take(),
550            &[
551                VectorDiff::Insert { index: 1, value: 'w' },
552                VectorDiff::Insert { index: 2, value: 'x' },
553                VectorDiff::Insert { index: 3, value: 'y' },
554                VectorDiff::Insert { index: 4, value: 'z' },
555            ],
556        );
557
558        linked_chunk.push_gap_back(());
559        linked_chunk.push_items_back(['e', 'f', 'g', 'h']);
560        assert_items_eq!(
561            linked_chunk,
562            ['a', 'w', 'x'] ['y', 'z', 'b'] ['c'] ['d'] [-] ['e', 'f', 'g'] ['h']
563        );
564
565        // From an `ObservableVector` point of view, it would look like:
566        //
567        // 0   1   2   3   4   5   6   7   8   9   10  11  12
568        // +---+---+---+---+---+---+---+---+---+---+---+---+
569        // | a | w | x | y | z | b | c | d | e | f | g | h |
570        // +---+---+---+---+---+---+---+---+---+---+---+---+
571        //                                 ^^^^^^^^^^^^^^^^
572        //                                 |
573        //                                 new
574        apply_and_assert_eq(
575            &mut accumulator,
576            as_vector.take(),
577            &[
578                VectorDiff::Append { values: vector!['e', 'f', 'g'] },
579                VectorDiff::Append { values: vector!['h'] },
580            ],
581        );
582
583        linked_chunk
584            .replace_gap_at(
585                ['i', 'j', 'k', 'l'],
586                linked_chunk.chunk_identifier(|chunk| chunk.is_gap()).unwrap(),
587            )
588            .unwrap();
589        assert_items_eq!(
590            linked_chunk,
591            ['a', 'w', 'x'] ['y', 'z', 'b'] ['c'] ['d'] ['i', 'j', 'k'] ['l'] ['e', 'f', 'g'] ['h']
592        );
593
594        // From an `ObservableVector` point of view, it would look like:
595        //
596        // 0   1   2   3   4   5   6   7   8   9   10  11  12  13  14  15  16
597        // +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
598        // | a | w | x | y | z | b | c | d | i | j | k | l | e | f | g | h |
599        // +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
600        //                                 ^^^^^^^^^^^^^^^^
601        //                                 |
602        //                                 new
603        apply_and_assert_eq(
604            &mut accumulator,
605            as_vector.take(),
606            &[
607                VectorDiff::Insert { index: 8, value: 'i' },
608                VectorDiff::Insert { index: 9, value: 'j' },
609                VectorDiff::Insert { index: 10, value: 'k' },
610                VectorDiff::Insert { index: 11, value: 'l' },
611            ],
612        );
613
614        linked_chunk
615            .insert_items_at(['m'], linked_chunk.item_position(|item| *item == 'a').unwrap())
616            .unwrap();
617        assert_items_eq!(
618            linked_chunk,
619            ['m', 'a', 'w'] ['x'] ['y', 'z', 'b'] ['c'] ['d'] ['i', 'j', 'k'] ['l'] ['e', 'f', 'g'] ['h']
620        );
621
622        // From an `ObservableVector` point of view, it would look like:
623        //
624        // 0   1   2   3   4   5   6   7   8   9   10  11  12  13  14  15  16  17
625        // +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
626        // | m | a | w | x | y | z | b | c | d | i | j | k | l | e | f | g | h |
627        // +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
628        // ^^^^
629        // |
630        // new
631        apply_and_assert_eq(
632            &mut accumulator,
633            as_vector.take(),
634            &[VectorDiff::Insert { index: 0, value: 'm' }],
635        );
636
637        let removed_item = linked_chunk
638            .remove_item_at(
639                linked_chunk.item_position(|item| *item == 'c').unwrap(),
640                EmptyChunk::Remove,
641            )
642            .unwrap();
643        assert_eq!(removed_item, 'c');
644        assert_items_eq!(
645            linked_chunk,
646            ['m', 'a', 'w'] ['x'] ['y', 'z', 'b'] ['d'] ['i', 'j', 'k'] ['l'] ['e', 'f', 'g'] ['h']
647        );
648
649        // From an `ObservableVector` point of view, it would look like:
650        //
651        // 0   1   2   3   4   5   6   7   8   9   10  11  12  13  14  15  16
652        // +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
653        // | m | a | w | x | y | z | b | d | i | j | k | l | e | f | g | h |
654        // +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
655        //                             ^
656        //                             |
657        //                             `c` has been removed
658        apply_and_assert_eq(&mut accumulator, as_vector.take(), &[VectorDiff::Remove { index: 7 }]);
659
660        let removed_item = linked_chunk
661            .remove_item_at(
662                linked_chunk.item_position(|item| *item == 'z').unwrap(),
663                EmptyChunk::Remove,
664            )
665            .unwrap();
666        assert_eq!(removed_item, 'z');
667        assert_items_eq!(
668            linked_chunk,
669            ['m', 'a', 'w'] ['x'] ['y', 'b'] ['d'] ['i', 'j', 'k'] ['l'] ['e', 'f', 'g'] ['h']
670        );
671
672        // From an `ObservableVector` point of view, it would look like:
673        //
674        // 0   1   2   3   4   5   6   7   8   9   10  11  12  13  14  15
675        // +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
676        // | m | a | w | x | y | b | d | i | j | k | l | e | f | g | h |
677        // +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
678        //                     ^
679        //                     |
680        //                     `z` has been removed
681        apply_and_assert_eq(&mut accumulator, as_vector.take(), &[VectorDiff::Remove { index: 5 }]);
682
683        linked_chunk
684            .insert_items_at(['z'], linked_chunk.item_position(|item| *item == 'h').unwrap())
685            .unwrap();
686
687        assert_items_eq!(
688            linked_chunk,
689            ['m', 'a', 'w'] ['x'] ['y', 'b'] ['d'] ['i', 'j', 'k'] ['l'] ['e', 'f', 'g'] ['z', 'h']
690        );
691
692        // From an `ObservableVector` point of view, it would look like:
693        //
694        // 0   1   2   3   4   5   6   7   8   9   10  11  12  13  14  15  16
695        // +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
696        // | m | a | w | x | y | b | d | i | j | k | l | e | f | g | z | h |
697        // +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
698        //                                                         ^^^^
699        //                                                         |
700        //                                                         new!
701        apply_and_assert_eq(
702            &mut accumulator,
703            as_vector.take(),
704            &[VectorDiff::Insert { index: 14, value: 'z' }],
705        );
706
707        // Ensure the “reconstitued” vector is the one expected.
708        assert_eq!(
709            accumulator,
710            vector!['m', 'a', 'w', 'x', 'y', 'b', 'd', 'i', 'j', 'k', 'l', 'e', 'f', 'g', 'z', 'h']
711        );
712
713        // Replace element 8 by an uppercase J.
714        linked_chunk
715            .replace_item_at(linked_chunk.item_position(|item| *item == 'j').unwrap(), 'J')
716            .unwrap();
717
718        assert_items_eq!(
719            linked_chunk,
720            ['m', 'a', 'w'] ['x'] ['y', 'b'] ['d'] ['i', 'J', 'k'] ['l'] ['e', 'f', 'g'] ['z', 'h']
721        );
722
723        // From an `ObservableVector` point of view, it would look like:
724        //
725        // 0   1   2   3   4   5   6   7   8   9   10  11  12  13  14  15  16
726        // +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
727        // | m | a | w | x | y | b | d | i | J | k | l | e | f | g | z | h |
728        // +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
729        //                                 ^^^^
730        //                                 |
731        //                                 new!
732        apply_and_assert_eq(
733            &mut accumulator,
734            as_vector.take(),
735            &[VectorDiff::Set { index: 8, value: 'J' }],
736        );
737
738        // Let's try to clear the linked chunk now.
739        linked_chunk.clear();
740
741        apply_and_assert_eq(&mut accumulator, as_vector.take(), &[VectorDiff::Clear]);
742        assert!(accumulator.is_empty());
743
744        drop(linked_chunk);
745        assert!(as_vector.take().is_empty());
746    }
747
748    #[test]
749    fn test_as_vector_with_update_clear() {
750        let mut linked_chunk = LinkedChunk::<3, char, ()>::new_with_update_history();
751        let mut as_vector = linked_chunk.as_vector().unwrap();
752
753        {
754            // 1 initial chunk in the `UpdateToVectorDiff` mapper.
755            let chunks = &as_vector.mapper.chunks;
756            assert_eq!(chunks.len(), 1);
757            assert_eq!(chunks[0].0, ChunkIdentifierGenerator::FIRST_IDENTIFIER);
758            assert_eq!(chunks[0].1, 0);
759
760            assert!(as_vector.take().is_empty());
761        }
762
763        linked_chunk.push_items_back(['a', 'b', 'c', 'd']);
764
765        {
766            let diffs = as_vector.take();
767            assert_eq!(diffs.len(), 2);
768            assert_matches!(&diffs[0], VectorDiff::Append { .. });
769            assert_matches!(&diffs[1], VectorDiff::Append { .. });
770
771            // 2 chunks in the `UpdateToVectorDiff` mapper.
772            assert_eq!(as_vector.mapper.chunks.len(), 2);
773        }
774
775        linked_chunk.clear();
776
777        {
778            let diffs = as_vector.take();
779            assert_eq!(diffs.len(), 1);
780            assert_matches!(&diffs[0], VectorDiff::Clear);
781
782            // 1 chunk in the `UpdateToVectorDiff` mapper.
783            let chunks = &as_vector.mapper.chunks;
784            assert_eq!(chunks.len(), 1);
785            assert_eq!(chunks[0].0, ChunkIdentifierGenerator::FIRST_IDENTIFIER);
786            assert_eq!(chunks[0].1, 0);
787        }
788
789        // And we can push again.
790        linked_chunk.push_items_back(['a', 'b', 'c', 'd']);
791
792        {
793            let diffs = as_vector.take();
794            assert_eq!(diffs.len(), 2);
795            assert_matches!(&diffs[0], VectorDiff::Append { .. });
796            assert_matches!(&diffs[1], VectorDiff::Append { .. });
797        }
798    }
799
800    #[test]
801    fn test_updates_are_drained_when_constructing_as_vector() {
802        let mut linked_chunk = LinkedChunk::<10, char, ()>::new_with_update_history();
803
804        linked_chunk.push_items_back(['a']);
805
806        let mut as_vector = linked_chunk.as_vector().unwrap();
807        let diffs = as_vector.take();
808
809        // `diffs` are empty because `AsVector` is built _after_ `LinkedChunk`
810        // has been updated.
811        assert!(diffs.is_empty());
812
813        linked_chunk.push_items_back(['b']);
814
815        let diffs = as_vector.take();
816
817        // `diffs` is not empty because new updates are coming.
818        assert_eq!(diffs.len(), 1);
819    }
820
821    #[test]
822    fn test_as_vector_with_initial_content() {
823        // Fill the linked chunk with some initial items.
824        let mut linked_chunk = LinkedChunk::<3, char, ()>::new_with_update_history();
825        linked_chunk.push_items_back(['a', 'b', 'c', 'd']);
826
827        #[rustfmt::skip]
828        assert_items_eq!(linked_chunk, ['a', 'b', 'c'] ['d']);
829
830        // Empty updates first.
831        let _ = linked_chunk.updates().unwrap().take();
832
833        // Start observing future updates.
834        let mut as_vector = linked_chunk.as_vector().unwrap();
835
836        assert!(as_vector.take().is_empty());
837
838        // It's important to cause a change that will create new chunks, like pushing
839        // enough items.
840        linked_chunk.push_items_back(['e', 'f', 'g']);
841        #[rustfmt::skip]
842        assert_items_eq!(linked_chunk, ['a', 'b', 'c'] ['d', 'e', 'f'] ['g']);
843
844        // And the vector diffs can be computed without crashing.
845        let diffs = as_vector.take();
846        assert_eq!(diffs.len(), 2);
847        assert_matches!(&diffs[0], VectorDiff::Append { values } => {
848            assert_eq!(*values, ['e', 'f'].into());
849        });
850        assert_matches!(&diffs[1], VectorDiff::Append { values } => {
851            assert_eq!(*values, ['g'].into());
852        });
853    }
854
855    #[cfg(not(target_arch = "wasm32"))]
856    mod proptests {
857        use proptest::prelude::*;
858
859        use super::*;
860
861        #[derive(Debug, Clone)]
862        enum AsVectorOperation {
863            PushItems { items: Vec<char> },
864            PushGap,
865            ReplaceLastGap { items: Vec<char> },
866            RemoveItem { item: char },
867        }
868
869        fn as_vector_operation_strategy() -> impl Strategy<Value = AsVectorOperation> {
870            prop_oneof![
871                3 => prop::collection::vec(prop::char::ranges(vec!['a'..='z', 'A'..='Z'].into()), 0..=25)
872                    .prop_map(|items| AsVectorOperation::PushItems { items }),
873
874                2 => Just(AsVectorOperation::PushGap),
875
876                1 => prop::collection::vec(prop::char::ranges(vec!['a'..='z', 'A'..='Z'].into()), 0..=25)
877                    .prop_map(|items| AsVectorOperation::ReplaceLastGap { items }),
878
879                1 => prop::char::ranges(vec!['a'..='z', 'A'..='Z'].into())
880                    .prop_map(|item| AsVectorOperation::RemoveItem { item }),
881            ]
882        }
883
884        proptest! {
885            #[test]
886            fn test_as_vector_is_correct(
887                operations in prop::collection::vec(as_vector_operation_strategy(), 50..=200)
888            ) {
889                let mut linked_chunk = LinkedChunk::<10, char, ()>::new_with_update_history();
890                let mut as_vector = linked_chunk.as_vector().unwrap();
891
892                for operation in operations {
893                    match operation {
894                        AsVectorOperation::PushItems { items } => {
895                            linked_chunk.push_items_back(items);
896                        }
897
898                        AsVectorOperation::PushGap => {
899                            linked_chunk.push_gap_back(());
900                        }
901
902                        AsVectorOperation::ReplaceLastGap { items } => {
903                            let Some(gap_identifier) = linked_chunk
904                                .rchunks()
905                                .find_map(|chunk| chunk.is_gap().then_some(chunk.identifier()))
906                            else {
907                                continue;
908                            };
909
910                            linked_chunk.replace_gap_at(items, gap_identifier).expect("Failed to replace a gap");
911                        }
912
913                        AsVectorOperation::RemoveItem { item: expected_item } => {
914                            let Some(position) = linked_chunk
915                                .items().find_map(|(position, item)| (*item == expected_item).then_some(position))
916                            else {
917                                continue;
918                            };
919
920                            linked_chunk.remove_item_at(position, EmptyChunk::Remove).expect("Failed to remove an item");
921                        }
922                    }
923                }
924
925                let mut vector_from_diffs = Vec::new();
926
927                // Read all updates as `VectorDiff` and rebuild a `Vec<char>`.
928                for diff in as_vector.take() {
929                    match diff {
930                        VectorDiff::Insert { index, value } => vector_from_diffs.insert(index, value),
931                        VectorDiff::Append { values } => {
932                            let mut values = values.iter().copied().collect();
933
934                            vector_from_diffs.append(&mut values);
935                        }
936                        VectorDiff::Remove { index } => {
937                            vector_from_diffs.remove(index);
938                        }
939                        _ => unreachable!(),
940                    }
941                }
942
943                // Iterate over all chunks and collect items as `Vec<char>`.
944                let vector_from_chunks = linked_chunk.items().map(|(_, item)| *item).collect::<Vec<_>>();
945
946                // Compare both `Vec`s.
947                assert_eq!(vector_from_diffs, vector_from_chunks);
948            }
949        }
950    }
951}