Skip to main content

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