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}