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