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