matrix_sdk_common/linked_chunk/
lazy_loader.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::marker::PhantomData;
16
17use super::{
18    Chunk, ChunkContent, ChunkIdentifier, ChunkIdentifierGenerator, Ends, LinkedChunk,
19    ObservableUpdates, RawChunk, Update,
20};
21
22/// Build a new `LinkedChunk` with a single chunk that is supposed to be the
23/// last one.
24pub fn from_last_chunk<const CAP: usize, Item, Gap>(
25    chunk: Option<RawChunk<Item, Gap>>,
26    chunk_identifier_generator: ChunkIdentifierGenerator,
27) -> Result<Option<LinkedChunk<CAP, Item, Gap>>, LazyLoaderError> {
28    let Some(mut chunk) = chunk else {
29        return Ok(None);
30    };
31
32    // Check consistency before creating the `LinkedChunk`.
33    {
34        // The number of items is not too large.
35        if let ChunkContent::Items(items) = &chunk.content
36            && items.len() > CAP
37        {
38            return Err(LazyLoaderError::ChunkTooLarge { id: chunk.identifier });
39        }
40
41        // Chunk has no next chunk.
42        if chunk.next.is_some() {
43            return Err(LazyLoaderError::ChunkIsNotLast { id: chunk.identifier });
44        }
45    }
46
47    // Create the `LinkedChunk` from a single chunk.
48    {
49        // Take the `previous` chunk and consider it becomes the `lazy_previous`.
50        let lazy_previous = chunk.previous.take();
51
52        // Transform the `RawChunk` into a `Chunk`.
53        let mut chunk_ptr = Chunk::new_leaked(chunk.identifier, chunk.content);
54
55        // Set the `lazy_previous` value!
56        //
57        // SAFETY: Pointer is convertible to a reference.
58        unsafe { chunk_ptr.as_mut() }.lazy_previous = lazy_previous;
59
60        Ok(Some(LinkedChunk {
61            links: Ends { first: chunk_ptr, last: None },
62            chunk_identifier_generator,
63            updates: Some(ObservableUpdates::new()),
64            marker: PhantomData,
65        }))
66    }
67}
68
69/// Insert a new chunk at the front of a `LinkedChunk`.
70pub fn insert_new_first_chunk<const CAP: usize, Item, Gap>(
71    linked_chunk: &mut LinkedChunk<CAP, Item, Gap>,
72    mut new_first_chunk: RawChunk<Item, Gap>,
73) -> Result<(), LazyLoaderError>
74where
75    Item: Clone,
76    Gap: Clone,
77{
78    // Check `LinkedChunk` is going to be consistent after the insertion.
79    {
80        // The number of items is not too large.
81        if let ChunkContent::Items(items) = &new_first_chunk.content
82            && items.len() > CAP
83        {
84            return Err(LazyLoaderError::ChunkTooLarge { id: new_first_chunk.identifier });
85        }
86
87        // New chunk doesn't create a cycle.
88        if let Some(previous_chunk) = new_first_chunk.previous
89            && linked_chunk.chunks().any(|chunk| chunk.identifier() == previous_chunk)
90        {
91            return Err(LazyLoaderError::Cycle {
92                new_chunk: new_first_chunk.identifier,
93                with_chunk: previous_chunk,
94            });
95        }
96
97        let first_chunk = linked_chunk.links.first_chunk();
98        let expected_next_chunk = first_chunk.identifier();
99
100        // New chunk has a next chunk.
101        let Some(next_chunk) = new_first_chunk.next else {
102            return Err(LazyLoaderError::MissingNextChunk { id: new_first_chunk.identifier });
103        };
104
105        // New chunk has a next chunk, and it is the first chunk of the `LinkedChunk`.
106        if next_chunk != expected_next_chunk {
107            return Err(LazyLoaderError::CannotConnectTwoChunks {
108                new_chunk: new_first_chunk.identifier,
109                with_chunk: expected_next_chunk,
110            });
111        }
112
113        // Same check as before, but in reverse: the first chunk has a `lazy_previous`
114        // to the new first chunk.
115        if first_chunk.lazy_previous() != Some(new_first_chunk.identifier) {
116            return Err(LazyLoaderError::CannotConnectTwoChunks {
117                new_chunk: first_chunk.identifier,
118                with_chunk: new_first_chunk.identifier,
119            });
120        }
121
122        // Alright. All checks are made.
123    }
124
125    // Insert the new first chunk.
126    {
127        // Transform the `RawChunk` into a `Chunk`.
128        let lazy_previous = new_first_chunk.previous.take();
129        let mut new_first_chunk =
130            Chunk::new_leaked(new_first_chunk.identifier, new_first_chunk.content);
131
132        let links = &mut linked_chunk.links;
133
134        // Update the first chunk.
135        {
136            let first_chunk = links.first_chunk_mut();
137
138            debug_assert!(
139                first_chunk.previous.is_none(),
140                "The first chunk is not supposed to have a previous chunk"
141            );
142
143            // Move the `lazy_previous` if any.
144            first_chunk.lazy_previous = None;
145            unsafe { new_first_chunk.as_mut() }.lazy_previous = lazy_previous;
146
147            // Link one way: `new_first_chunk` becomes the previous chunk of the first
148            // chunk.
149            first_chunk.previous = Some(new_first_chunk);
150        }
151
152        // Update `links`.
153        {
154            // Remember the pointer to the `first_chunk`.
155            let old_first_chunk = links.first;
156
157            // `new_first_chunk` becomes the new first chunk.
158            links.first = new_first_chunk;
159
160            // Link the other way: `old_first_chunk` becomes the next chunk of the first
161            // chunk.
162            links.first_chunk_mut().next = Some(old_first_chunk);
163
164            debug_assert!(
165                links.first_chunk().previous.is_none(),
166                "The new first chunk is not supposed to have a previous chunk"
167            );
168
169            // Update the last chunk. If it's `Some(_)`, no need to update the last chunk
170            // pointer. If it's `None`, it means we had only one chunk; now we have two, the
171            // last chunk is the `old_first_chunk`.
172            if links.last.is_none() {
173                links.last = Some(old_first_chunk);
174            }
175        }
176    }
177
178    // Emit the updates.
179    if let Some(updates) = linked_chunk.updates.as_mut() {
180        let first_chunk = linked_chunk.links.first_chunk();
181        emit_new_first_chunk_updates(first_chunk, updates);
182    }
183
184    Ok(())
185}
186
187/// Emit updates whenever a new first chunk is inserted at the front of a
188/// `LinkedChunk`.
189fn emit_new_first_chunk_updates<const CAP: usize, Item, Gap>(
190    chunk: &Chunk<CAP, Item, Gap>,
191    updates: &mut ObservableUpdates<Item, Gap>,
192) where
193    Item: Clone,
194    Gap: Clone,
195{
196    let previous = chunk.previous().map(Chunk::identifier).or(chunk.lazy_previous);
197    let new = chunk.identifier();
198    let next = chunk.next().map(Chunk::identifier);
199
200    match chunk.content() {
201        ChunkContent::Gap(gap) => {
202            updates.push(Update::NewGapChunk { previous, new, next, gap: gap.clone() });
203        }
204        ChunkContent::Items(items) => {
205            updates.push(Update::NewItemsChunk { previous, new, next });
206            updates.push(Update::PushItems { at: chunk.first_position(), items: items.clone() });
207        }
208    }
209}
210
211/// Replace the items with the given last chunk of items and generator.
212///
213/// This clears all the chunks in memory before resetting to the new chunk,
214/// if provided.
215pub fn replace_with<const CAP: usize, Item, Gap>(
216    linked_chunk: &mut LinkedChunk<CAP, Item, Gap>,
217    chunk: Option<RawChunk<Item, Gap>>,
218    chunk_identifier_generator: ChunkIdentifierGenerator,
219) -> Result<(), LazyLoaderError>
220where
221    Item: Clone,
222    Gap: Clone,
223{
224    let Some(mut chunk) = chunk else {
225        // This is equivalent to clearing the linked chunk, and overriding the chunk ID
226        // generator afterwards. But, if there was no chunks in the DB, the generator
227        // should be reset too, so it's entirely equivalent to a clear.
228        linked_chunk.clear();
229        return Ok(());
230    };
231
232    // Check consistency before replacing the `LinkedChunk`.
233    // The number of items is not too large.
234    if let ChunkContent::Items(items) = &chunk.content
235        && items.len() > CAP
236    {
237        return Err(LazyLoaderError::ChunkTooLarge { id: chunk.identifier });
238    }
239
240    // Chunk has no next chunk.
241    if chunk.next.is_some() {
242        return Err(LazyLoaderError::ChunkIsNotLast { id: chunk.identifier });
243    }
244
245    // The last chunk is now valid.
246    linked_chunk.chunk_identifier_generator = chunk_identifier_generator;
247
248    // Take the `previous` chunk and consider it becomes the `lazy_previous`.
249    let lazy_previous = chunk.previous.take();
250
251    // Transform the `RawChunk` into a `Chunk`.
252    let mut chunk_ptr = Chunk::new_leaked(chunk.identifier, chunk.content);
253
254    // Set the `lazy_previous` value!
255    //
256    // SAFETY: Pointer is convertible to a reference.
257    unsafe { chunk_ptr.as_mut() }.lazy_previous = lazy_previous;
258
259    // Replace the first link with the new pointer.
260    linked_chunk.links.replace_with(chunk_ptr);
261
262    if let Some(updates) = linked_chunk.updates.as_mut() {
263        // Clear the previous updates, as we're about to insert a clear they would be
264        // useless.
265        updates.clear_pending();
266        updates.push(Update::Clear);
267
268        emit_new_first_chunk_updates(linked_chunk.links.first_chunk(), updates);
269    }
270
271    Ok(())
272}
273
274/// A pretty inefficient, test-only, function to rebuild a full `LinkedChunk`.
275#[doc(hidden)]
276pub fn from_all_chunks<const CAP: usize, Item, Gap>(
277    mut chunks: Vec<RawChunk<Item, Gap>>,
278) -> Result<Option<LinkedChunk<CAP, Item, Gap>>, LazyLoaderError>
279where
280    Item: Clone,
281    Gap: Clone,
282{
283    if chunks.is_empty() {
284        return Ok(None);
285    }
286
287    // Sort by `next` so that the search for the next chunk is faster (it should
288    // come first). The chunk with the biggest next chunk identifier comes first.
289    // Chunk with no next chunk comes last.
290    chunks.sort_by(|a, b| b.next.cmp(&a.next));
291
292    let last_chunk = chunks
293        .pop()
294        // SAFETY: `chunks` is guaranteed to not be empty, `pop` cannot fail.
295        .expect("`chunks` is supposed to not be empty, we must be able to `pop` an item");
296    let last_chunk_identifier = last_chunk.identifier;
297    let chunk_identifier_generator =
298        ChunkIdentifierGenerator::new_from_previous_chunk_identifier(last_chunk_identifier);
299
300    let Some(mut linked_chunk) = from_last_chunk(Some(last_chunk), chunk_identifier_generator)?
301    else {
302        return Ok(None);
303    };
304
305    let mut next_chunk = last_chunk_identifier;
306
307    while let Some(chunk) = chunks
308        .iter()
309        .position(|chunk| chunk.next == Some(next_chunk))
310        .map(|index| chunks.remove(index))
311    {
312        next_chunk = chunk.identifier;
313        insert_new_first_chunk(&mut linked_chunk, chunk)?;
314    }
315
316    let first_chunk = linked_chunk.links.first_chunk();
317
318    // It is expected that **all chunks** are passed to this function. If there was
319    // a previous chunk, `insert_new_first_chunk` has erased it and moved it to
320    // `lazy_previous`. Hence, let's check both (the former condition isn't
321    // necessary, but better be robust).
322    if first_chunk.previous().is_some() || first_chunk.lazy_previous.is_some() {
323        return Err(LazyLoaderError::ChunkIsNotFirst { id: first_chunk.identifier() });
324    }
325
326    if !chunks.is_empty() {
327        return Err(LazyLoaderError::MultipleConnectedComponents);
328    }
329
330    Ok(Some(linked_chunk))
331}
332
333#[derive(thiserror::Error, Debug)]
334pub enum LazyLoaderError {
335    #[error("chunk with id {} has a next chunk, it is supposed to be the last chunk", id.index())]
336    ChunkIsNotLast { id: ChunkIdentifier },
337
338    #[error("chunk with id {} forms a cycle with chunk with id {}", new_chunk.index(), with_chunk.index())]
339    Cycle { new_chunk: ChunkIdentifier, with_chunk: ChunkIdentifier },
340
341    #[error("chunk with id {} is supposed to have a next chunk", id.index())]
342    MissingNextChunk { id: ChunkIdentifier },
343
344    #[error(
345        "chunk with id {} cannot be connected to chunk with id {} because the identifiers do not match",
346        new_chunk.index(),
347        with_chunk.index()
348    )]
349    CannotConnectTwoChunks { new_chunk: ChunkIdentifier, with_chunk: ChunkIdentifier },
350
351    #[error("chunk with id {} is too large", id.index())]
352    ChunkTooLarge { id: ChunkIdentifier },
353
354    #[doc(hidden)]
355    #[error("the last chunk is missing")]
356    MissingLastChunk,
357
358    #[doc(hidden)]
359    #[error("chunk with id {} has a previous chunk, it is supposed to be the first chunk", id.index())]
360    ChunkIsNotFirst { id: ChunkIdentifier },
361
362    #[doc(hidden)]
363    #[error("multiple connected components")]
364    MultipleConnectedComponents,
365}
366
367#[cfg(test)]
368mod tests {
369    use assert_matches::assert_matches;
370
371    use super::{
372        super::Position, ChunkContent, ChunkIdentifier, ChunkIdentifierGenerator, LazyLoaderError,
373        LinkedChunk, RawChunk, Update, from_all_chunks, from_last_chunk, insert_new_first_chunk,
374        replace_with,
375    };
376
377    #[test]
378    fn test_from_last_chunk_err_too_much_items() {
379        let last_chunk = RawChunk {
380            previous: None,
381            identifier: ChunkIdentifier::new(0),
382            next: None,
383            content: ChunkContent::Items(vec!['a', 'b', 'c']),
384        };
385        let chunk_identifier_generator =
386            ChunkIdentifierGenerator::new_from_previous_chunk_identifier(ChunkIdentifier::new(0));
387
388        let maybe_linked_chunk =
389            from_last_chunk::<2, char, ()>(Some(last_chunk), chunk_identifier_generator);
390
391        assert_matches!(
392            maybe_linked_chunk,
393            Err(LazyLoaderError::ChunkTooLarge { id }) => {
394                assert_eq!(id, 0);
395            }
396        );
397    }
398
399    #[test]
400    fn test_from_last_chunk_err_is_not_last_chunk() {
401        let last_chunk = RawChunk {
402            previous: None,
403            identifier: ChunkIdentifier::new(0),
404            next: Some(ChunkIdentifier::new(42)),
405            content: ChunkContent::Items(vec!['a']),
406        };
407        let chunk_identifier_generator =
408            ChunkIdentifierGenerator::new_from_previous_chunk_identifier(ChunkIdentifier::new(0));
409
410        let maybe_linked_chunk =
411            from_last_chunk::<2, char, ()>(Some(last_chunk), chunk_identifier_generator);
412
413        assert_matches!(
414            maybe_linked_chunk,
415            Err(LazyLoaderError::ChunkIsNotLast { id }) => {
416                assert_eq!(id, 0);
417            }
418        );
419    }
420
421    #[test]
422    fn test_from_last_chunk_none() {
423        let chunk_identifier_generator =
424            ChunkIdentifierGenerator::new_from_previous_chunk_identifier(ChunkIdentifier::new(0));
425
426        let maybe_linked_chunk =
427            from_last_chunk::<2, char, ()>(None, chunk_identifier_generator).unwrap();
428
429        assert!(maybe_linked_chunk.is_none());
430    }
431
432    #[test]
433    fn test_from_last_chunk() {
434        let last_chunk = RawChunk {
435            previous: Some(ChunkIdentifier::new(42)),
436            identifier: ChunkIdentifier::new(0),
437            next: None,
438            content: ChunkContent::Items(vec!['a']),
439        };
440        let chunk_identifier_generator =
441            ChunkIdentifierGenerator::new_from_previous_chunk_identifier(ChunkIdentifier::new(0));
442
443        let maybe_linked_chunk =
444            from_last_chunk::<2, char, ()>(Some(last_chunk), chunk_identifier_generator).unwrap();
445
446        assert_matches!(maybe_linked_chunk, Some(mut linked_chunk) => {
447            let mut chunks = linked_chunk.chunks();
448
449            assert_matches!(chunks.next(), Some(chunk) => {
450                assert_eq!(chunk.identifier(), 0);
451                // The chunk's previous has been set to `None`
452                assert!(chunk.previous().is_none());
453            });
454            assert!(chunks.next().is_none());
455
456            // It has updates enabled.
457            assert!(linked_chunk.updates().is_some());
458        });
459    }
460
461    #[test]
462    fn test_insert_new_first_chunk_err_too_much_items() {
463        let new_first_chunk = RawChunk {
464            previous: None,
465            identifier: ChunkIdentifier::new(0),
466            next: None,
467            content: ChunkContent::Items(vec!['a', 'b', 'c']),
468        };
469
470        let mut linked_chunk = LinkedChunk::<2, char, ()>::new();
471
472        let result = insert_new_first_chunk(&mut linked_chunk, new_first_chunk);
473
474        assert_matches!(result, Err(LazyLoaderError::ChunkTooLarge { id }) => {
475            assert_eq!(id, 0);
476        });
477    }
478
479    #[test]
480    fn test_insert_new_first_chunk_err_cycle() {
481        let new_first_chunk = RawChunk {
482            previous: Some(ChunkIdentifier::new(0)),
483            identifier: ChunkIdentifier::new(1),
484            next: Some(ChunkIdentifier::new(0)),
485            content: ChunkContent::Gap(()),
486        };
487
488        let mut linked_chunk = LinkedChunk::<2, char, ()>::new();
489        let result = insert_new_first_chunk(&mut linked_chunk, new_first_chunk);
490
491        assert_matches!(result, Err(LazyLoaderError::Cycle { new_chunk, with_chunk }) => {
492            assert_eq!(new_chunk, 1);
493            assert_eq!(with_chunk, 0);
494        });
495    }
496
497    #[test]
498    fn test_insert_new_first_chunk_err_missing_next_chunk() {
499        let new_first_chunk = RawChunk {
500            previous: None,
501            identifier: ChunkIdentifier::new(0),
502            next: None,
503            content: ChunkContent::Gap(()),
504        };
505
506        let mut linked_chunk = LinkedChunk::<2, char, ()>::new();
507
508        let result = insert_new_first_chunk(&mut linked_chunk, new_first_chunk);
509
510        assert_matches!(result, Err(LazyLoaderError::MissingNextChunk { id }) => {
511            assert_eq!(id, 0);
512        });
513    }
514
515    #[test]
516    fn test_insert_new_first_chunk_err_cannot_connect_two_chunks() {
517        let new_first_chunk = RawChunk {
518            previous: None,
519            identifier: ChunkIdentifier::new(1),
520            next: Some(ChunkIdentifier::new(42)),
521            content: ChunkContent::Gap(()),
522        };
523
524        let mut linked_chunk = LinkedChunk::<2, char, ()>::new();
525        linked_chunk.push_gap_back(());
526
527        let result = insert_new_first_chunk(&mut linked_chunk, new_first_chunk);
528
529        assert_matches!(result, Err(LazyLoaderError::CannotConnectTwoChunks { new_chunk, with_chunk }) => {
530            assert_eq!(new_chunk, 1);
531            assert_eq!(with_chunk, 0);
532        });
533    }
534
535    #[test]
536    fn test_insert_new_first_chunk_err_cannot_connect_two_chunks_before_no_lazy_previous() {
537        let new_first_chunk = RawChunk {
538            previous: None,
539            identifier: ChunkIdentifier::new(1),
540            next: Some(ChunkIdentifier::new(0)),
541            content: ChunkContent::Gap(()),
542        };
543
544        let mut linked_chunk = LinkedChunk::<2, char, ()>::new();
545        linked_chunk.push_gap_back(());
546
547        let result = insert_new_first_chunk(&mut linked_chunk, new_first_chunk);
548
549        assert_matches!(result, Err(LazyLoaderError::CannotConnectTwoChunks { new_chunk, with_chunk }) => {
550            assert_eq!(new_chunk, 0);
551            assert_eq!(with_chunk, 1);
552        });
553    }
554
555    #[test]
556    fn test_insert_new_first_chunk_gap() {
557        let new_first_chunk = RawChunk {
558            previous: None,
559            identifier: ChunkIdentifier::new(1),
560            next: Some(ChunkIdentifier::new(0)),
561            content: ChunkContent::Gap(()),
562        };
563
564        let mut linked_chunk = LinkedChunk::<5, char, ()>::new_with_update_history();
565        linked_chunk.push_items_back(vec!['a', 'b']);
566        linked_chunk.links.first_chunk_mut().lazy_previous = Some(ChunkIdentifier::new(1));
567
568        // Drain initial updates.
569        let _ = linked_chunk.updates().unwrap().take();
570
571        insert_new_first_chunk(&mut linked_chunk, new_first_chunk).unwrap();
572
573        // Iterate forwards to ensure forwards links are okay.
574        {
575            let mut chunks = linked_chunk.chunks();
576
577            assert_matches!(chunks.next(), Some(chunk) => {
578                assert_eq!(chunk.identifier(), 1);
579                assert!(chunk.is_gap());
580            });
581            assert_matches!(chunks.next(), Some(chunk) => {
582                assert_eq!(chunk.identifier(), 0);
583                assert!(chunk.is_items());
584            });
585            assert!(chunks.next().is_none());
586        }
587
588        // Iterate backwards to ensure backwards links are okay.
589        {
590            let mut rchunks = linked_chunk.rchunks();
591
592            assert_eq!(rchunks.next().unwrap().identifier(), 0);
593            assert_eq!(rchunks.next().unwrap().identifier(), 1);
594            assert!(rchunks.next().is_none());
595        }
596
597        // Check updates.
598        {
599            let updates = linked_chunk.updates().unwrap().take();
600
601            assert_eq!(updates.len(), 1);
602            assert_eq!(
603                updates,
604                [Update::NewGapChunk {
605                    previous: None,
606                    new: ChunkIdentifier::new(1),
607                    next: Some(ChunkIdentifier::new(0)),
608                    gap: (),
609                }]
610            );
611        }
612    }
613
614    #[test]
615    fn test_insert_new_first_chunk_items() {
616        let new_first_chunk = RawChunk {
617            previous: None,
618            identifier: ChunkIdentifier::new(1),
619            next: Some(ChunkIdentifier::new(0)),
620            content: ChunkContent::Items(vec!['c', 'd']),
621        };
622
623        let mut linked_chunk = LinkedChunk::<5, char, ()>::new_with_update_history();
624        linked_chunk.push_items_back(vec!['a', 'b']);
625        linked_chunk.links.first_chunk_mut().lazy_previous = Some(ChunkIdentifier::new(1));
626
627        // Drain initial updates.
628        let _ = linked_chunk.updates().unwrap().take();
629
630        insert_new_first_chunk(&mut linked_chunk, new_first_chunk).unwrap();
631
632        // Iterate forwards to ensure forwards links are okay.
633        {
634            let mut chunks = linked_chunk.chunks();
635
636            assert_matches!(chunks.next(), Some(chunk) => {
637                assert_eq!(chunk.identifier(), 1);
638                assert!(chunk.is_items());
639            });
640            assert_matches!(chunks.next(), Some(chunk) => {
641                assert_eq!(chunk.identifier(), 0);
642                assert!(chunk.is_items());
643            });
644            assert!(chunks.next().is_none());
645        }
646
647        // Iterate backwards to ensure backwards links are okay.
648        {
649            let mut rchunks = linked_chunk.rchunks();
650
651            assert_eq!(rchunks.next().unwrap().identifier(), 0);
652            assert_eq!(rchunks.next().unwrap().identifier(), 1);
653            assert!(rchunks.next().is_none());
654        }
655
656        // Check updates.
657        {
658            let updates = linked_chunk.updates().unwrap().take();
659
660            assert_eq!(updates.len(), 2);
661            assert_eq!(
662                updates,
663                [
664                    Update::NewItemsChunk {
665                        previous: None,
666                        new: ChunkIdentifier::new(1),
667                        next: Some(ChunkIdentifier::new(0)),
668                    },
669                    Update::PushItems {
670                        at: Position::new(ChunkIdentifier::new(1), 0),
671                        items: vec!['c', 'd']
672                    }
673                ]
674            );
675        }
676    }
677
678    #[test]
679    fn test_replace_with_chunk_too_large() {
680        // Start with a linked chunk with 3 chunks: one item, one gap, one item.
681        let mut linked_chunk = LinkedChunk::<2, char, ()>::new();
682        linked_chunk.push_items_back(vec!['a', 'b']);
683        linked_chunk.push_gap_back(());
684        linked_chunk.push_items_back(vec!['c', 'd']);
685
686        // Try to replace it with a last chunk that has too many items.
687        let chunk_identifier_generator = ChunkIdentifierGenerator::new_from_scratch();
688
689        let chunk_id = ChunkIdentifier::new(1);
690        let raw_chunk = RawChunk {
691            previous: Some(ChunkIdentifier::new(0)),
692            identifier: chunk_id,
693            next: None,
694            content: ChunkContent::Items(vec!['e', 'f', 'g', 'h']),
695        };
696
697        let err = replace_with(&mut linked_chunk, Some(raw_chunk), chunk_identifier_generator)
698            .unwrap_err();
699        assert_matches!(err, LazyLoaderError::ChunkTooLarge { id } => {
700            assert_eq!(chunk_id, id);
701        });
702    }
703
704    #[test]
705    fn test_replace_with_next_chunk() {
706        // Start with a linked chunk with 3 chunks: one item, one gap, one item.
707        let mut linked_chunk = LinkedChunk::<2, char, ()>::new();
708        linked_chunk.push_items_back(vec!['a', 'b']);
709        linked_chunk.push_gap_back(());
710        linked_chunk.push_items_back(vec!['c', 'd']);
711
712        // Try to replace it with a last chunk that has too many items.
713        let chunk_identifier_generator = ChunkIdentifierGenerator::new_from_scratch();
714
715        let chunk_id = ChunkIdentifier::new(1);
716        let raw_chunk = RawChunk {
717            previous: Some(ChunkIdentifier::new(0)),
718            identifier: chunk_id,
719            next: Some(ChunkIdentifier::new(2)),
720            content: ChunkContent::Items(vec!['e', 'f']),
721        };
722
723        let err = replace_with(&mut linked_chunk, Some(raw_chunk), chunk_identifier_generator)
724            .unwrap_err();
725        assert_matches!(err, LazyLoaderError::ChunkIsNotLast { id } => {
726            assert_eq!(chunk_id, id);
727        });
728    }
729
730    #[test]
731    fn test_replace_with_empty() {
732        // Start with a linked chunk with 3 chunks: one item, one gap, one item.
733        let mut linked_chunk = LinkedChunk::<2, char, ()>::new_with_update_history();
734        linked_chunk.push_items_back(vec!['a', 'b']);
735        linked_chunk.push_gap_back(());
736        linked_chunk.push_items_back(vec!['c', 'd']);
737
738        // Drain initial updates.
739        let _ = linked_chunk.updates().unwrap().take();
740
741        // Replace it with… you know, nothing (jon snow).
742        let chunk_identifier_generator =
743            ChunkIdentifierGenerator::new_from_previous_chunk_identifier(
744                ChunkIdentifierGenerator::FIRST_IDENTIFIER,
745            );
746        replace_with(&mut linked_chunk, None, chunk_identifier_generator).unwrap();
747
748        // The linked chunk still has updates enabled.
749        assert!(linked_chunk.updates().is_some());
750
751        // Check the linked chunk only contains the default empty events chunk.
752        let mut it = linked_chunk.chunks();
753
754        assert_matches!(it.next(), Some(chunk) => {
755            assert_eq!(chunk.identifier(), ChunkIdentifier::new(0));
756            assert!(chunk.is_items());
757            assert!(chunk.next().is_none());
758            assert_matches!(chunk.content(), ChunkContent::Items(items) => {
759                assert!(items.is_empty());
760            });
761        });
762
763        // And there's no other chunk.
764        assert_matches!(it.next(), None);
765
766        // Check updates.
767        {
768            let updates = linked_chunk.updates().unwrap().take();
769
770            assert_eq!(updates.len(), 2);
771            assert_eq!(
772                updates,
773                [
774                    Update::Clear,
775                    Update::NewItemsChunk {
776                        previous: None,
777                        new: ChunkIdentifier::new(0),
778                        next: None,
779                    },
780                ]
781            );
782        }
783    }
784
785    #[test]
786    fn test_replace_with_non_empty() {
787        // Start with a linked chunk with 3 chunks: one item, one gap, one item.
788        let mut linked_chunk = LinkedChunk::<2, char, ()>::new_with_update_history();
789        linked_chunk.push_items_back(vec!['a', 'b']);
790        linked_chunk.push_gap_back(());
791        linked_chunk.push_items_back(vec!['c', 'd']);
792
793        // Drain initial updates.
794        let _ = linked_chunk.updates().unwrap().take();
795
796        // Replace it with a single chunk (sorry, jon).
797        let chunk_identifier_generator =
798            ChunkIdentifierGenerator::new_from_previous_chunk_identifier(ChunkIdentifier::new(42));
799
800        let chunk_id = ChunkIdentifier::new(1);
801        let chunk = RawChunk {
802            previous: Some(ChunkIdentifier::new(0)),
803            identifier: chunk_id,
804            next: None,
805            content: ChunkContent::Items(vec!['e', 'f']),
806        };
807        replace_with(&mut linked_chunk, Some(chunk), chunk_identifier_generator).unwrap();
808
809        // The linked chunk still has updates enabled.
810        assert!(linked_chunk.updates().is_some());
811
812        let mut it = linked_chunk.chunks();
813
814        // The first chunk is an event chunks with the expected items.
815        assert_matches!(it.next(), Some(chunk) => {
816            assert_eq!(chunk.identifier(), chunk_id);
817            assert!(chunk.next().is_none());
818            assert_matches!(chunk.content(), ChunkContent::Items(items) => {
819                assert_eq!(*items, vec!['e', 'f']);
820            });
821        });
822
823        // Nothing more.
824        assert!(it.next().is_none());
825
826        // Check updates.
827        {
828            let updates = linked_chunk.updates().unwrap().take();
829
830            assert_eq!(updates.len(), 3);
831            assert_eq!(
832                updates,
833                [
834                    Update::Clear,
835                    Update::NewItemsChunk {
836                        previous: Some(ChunkIdentifier::new(0)),
837                        new: chunk_id,
838                        next: None,
839                    },
840                    Update::PushItems {
841                        at: Position::new(ChunkIdentifier::new(1), 0),
842                        items: vec!['e', 'f']
843                    }
844                ]
845            );
846        }
847    }
848
849    #[test]
850    fn test_from_all_chunks_empty() {
851        // Building an empty linked chunk works, and returns `None`.
852        let lc = from_all_chunks::<3, char, ()>(vec![]).unwrap();
853        assert!(lc.is_none());
854    }
855
856    #[test]
857    fn test_from_all_chunks_success() {
858        let cid0 = ChunkIdentifier::new(0);
859        let cid1 = ChunkIdentifier::new(1);
860        // Note: cid2 is missing on purpose, to confirm that it's fine to have holes in
861        // the chunk id space.
862        let cid3 = ChunkIdentifier::new(3);
863
864        // Check that we can successfully create a linked chunk, independently of the
865        // order in which chunks are added.
866        //
867        // The final chunk will contain [cid0 <-> cid1 <-> cid3], in this order.
868
869        let chunks = vec![
870            // Adding chunk cid0.
871            RawChunk {
872                previous: None,
873                identifier: cid0,
874                next: Some(cid1),
875                content: ChunkContent::Items(vec!['a', 'b', 'c']),
876            },
877            // Adding chunk cid3.
878            RawChunk {
879                previous: Some(cid1),
880                identifier: cid3,
881                next: None,
882                content: ChunkContent::Items(vec!['d', 'e']),
883            },
884            // Adding chunk cid1.
885            RawChunk {
886                previous: Some(cid0),
887                identifier: cid1,
888                next: Some(cid3),
889                content: ChunkContent::Gap('g'),
890            },
891        ];
892
893        let mut lc = from_all_chunks::<3, _, _>(chunks)
894            .expect("building works")
895            .expect("returns a non-empty linked chunk");
896
897        // Check the entire content first.
898        assert_items_eq!(lc, ['a', 'b', 'c'] [-] ['d', 'e']);
899
900        // Run checks on the first chunk.
901        let mut chunks = lc.chunks();
902        let first_chunk = chunks.next().unwrap();
903        {
904            assert!(first_chunk.previous().is_none());
905            assert_eq!(first_chunk.identifier(), cid0);
906        }
907
908        // Run checks on the second chunk.
909        let second_chunk = chunks.next().unwrap();
910        {
911            assert_eq!(second_chunk.identifier(), first_chunk.next().unwrap().identifier());
912            assert_eq!(second_chunk.previous().unwrap().identifier(), first_chunk.identifier());
913            assert_eq!(second_chunk.identifier(), cid1);
914        }
915
916        // Run checks on the third chunk.
917        let third_chunk = chunks.next().unwrap();
918        {
919            assert_eq!(third_chunk.identifier(), second_chunk.next().unwrap().identifier());
920            assert_eq!(third_chunk.previous().unwrap().identifier(), second_chunk.identifier());
921            assert!(third_chunk.next().is_none());
922            assert_eq!(third_chunk.identifier(), cid3);
923        }
924
925        // There's no more chunk.
926        assert!(chunks.next().is_none());
927
928        // The linked chunk had 5 items.
929        assert_eq!(lc.num_items(), 5);
930
931        // Now, if we add a new chunk, its identifier should be the previous one we used
932        // + 1.
933        lc.push_gap_back('h');
934
935        let last_chunk = lc.chunks().last().unwrap();
936        assert_eq!(last_chunk.identifier(), ChunkIdentifier::new(cid3.index() + 1));
937    }
938
939    #[test]
940    fn test_from_all_chunks_chunk_too_large() {
941        let cid0 = ChunkIdentifier::new(0);
942
943        // Adding a chunk with 4 items will fail, because the max capacity specified in
944        // the builder generics is 3.
945        let res = from_all_chunks::<3, char, ()>(vec![RawChunk {
946            previous: None,
947            identifier: cid0,
948            next: None,
949            content: ChunkContent::Items(vec!['a', 'b', 'c', 'd']),
950        }]);
951        assert_matches!(res, Err(LazyLoaderError::ChunkTooLarge { id }) => {
952            assert_eq!(id, cid0);
953        });
954    }
955
956    #[test]
957    fn test_from_all_chunks_missing_first_chunk() {
958        let cid0 = ChunkIdentifier::new(0);
959        let cid1 = ChunkIdentifier::new(1);
960        let cid2 = ChunkIdentifier::new(2);
961
962        let res = from_all_chunks::<3, char, char>(vec![
963            RawChunk {
964                previous: Some(cid2),
965                identifier: cid0,
966                next: Some(cid1),
967                content: ChunkContent::Gap('g'),
968            },
969            RawChunk {
970                previous: Some(cid0),
971                identifier: cid1,
972                next: None,
973                content: ChunkContent::Items(vec!['a', 'b', 'c']),
974            },
975        ]);
976        assert_matches!(res, Err(LazyLoaderError::ChunkIsNotFirst { id }) => {
977            assert_eq!(id, cid0);
978        });
979    }
980
981    #[test]
982    fn test_from_all_chunks_multiple_first_chunks() {
983        let cid0 = ChunkIdentifier::new(0);
984        let cid1 = ChunkIdentifier::new(1);
985
986        let res = from_all_chunks::<3, char, char>(vec![
987            RawChunk {
988                previous: None,
989                identifier: cid0,
990                next: None,
991                content: ChunkContent::Gap('g'),
992            },
993            // Second chunk lies and pretends to be the first too.
994            RawChunk {
995                previous: None,
996                identifier: cid1,
997                next: None,
998                content: ChunkContent::Gap('G'),
999            },
1000        ]);
1001
1002        assert_matches!(res, Err(LazyLoaderError::MultipleConnectedComponents));
1003    }
1004
1005    #[test]
1006    fn test_from_all_chunks_cycle() {
1007        let cid0 = ChunkIdentifier::new(0);
1008        let cid1 = ChunkIdentifier::new(1);
1009
1010        let res = from_all_chunks::<3, char, char>(vec![
1011            RawChunk {
1012                previous: None,
1013                identifier: cid0,
1014                next: None,
1015                content: ChunkContent::Gap('g'),
1016            },
1017            RawChunk {
1018                previous: Some(cid0),
1019                identifier: cid1,
1020                next: Some(cid0),
1021                content: ChunkContent::Gap('G'),
1022            },
1023        ]);
1024
1025        assert_matches!(res, Err(LazyLoaderError::Cycle { new_chunk, with_chunk }) => {
1026            assert_eq!(new_chunk, cid1);
1027            assert_eq!(with_chunk, cid0);
1028        });
1029    }
1030
1031    #[test]
1032    fn test_from_all_chunks_multiple_connected_components() {
1033        let cid0 = ChunkIdentifier::new(0);
1034        let cid1 = ChunkIdentifier::new(1);
1035        let cid2 = ChunkIdentifier::new(2);
1036
1037        let res = from_all_chunks::<3, char, char>(vec![
1038            // cid0 and cid1 are linked to each other.
1039            RawChunk {
1040                previous: None,
1041                identifier: cid0,
1042                next: Some(cid1),
1043                content: ChunkContent::Gap('g'),
1044            },
1045            RawChunk {
1046                previous: Some(cid0),
1047                identifier: cid1,
1048                next: None,
1049                content: ChunkContent::Gap('G'),
1050            },
1051            // cid2 stands on its own.
1052            RawChunk {
1053                previous: None,
1054                identifier: cid2,
1055                next: None,
1056                content: ChunkContent::Gap('h'),
1057            },
1058        ]);
1059
1060        assert_matches!(res, Err(LazyLoaderError::MultipleConnectedComponents));
1061    }
1062}