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