Skip to content

Commit 4ce1dfa

Browse files
committed
feat(linked chunk): allow replacing a linked chunk's content with a raw chunk
1 parent 9019449 commit 4ce1dfa

File tree

3 files changed

+282
-42
lines changed

3 files changed

+282
-42
lines changed

crates/matrix-sdk-common/src/linked_chunk/as_vector.rs

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -266,11 +266,10 @@ impl UpdateToVectorDiff {
266266
| Update::NewGapChunk { previous, new, next, .. } => {
267267
match (previous, next) {
268268
// New chunk at the end.
269-
(Some(previous), None) => {
270-
debug_assert!(
271-
matches!(self.chunks.back(), Some((p, _)) if p == previous),
272-
"Inserting new chunk at the end: The previous chunk is invalid"
273-
);
269+
(Some(_previous), None) => {
270+
// No need to check `previous`. It's possible that the linked chunk is
271+
// lazily loaded, chunk by chunk. The `next` is always reliable, but the
272+
// `previous` might not exist in-memory yet.
274273

275274
self.chunks.push_back((*new, 0));
276275
}

crates/matrix-sdk-common/src/linked_chunk/lazy_loader.rs

Lines changed: 255 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -170,26 +170,93 @@ where
170170
// Emit the updates.
171171
if let Some(updates) = linked_chunk.updates.as_mut() {
172172
let first_chunk = linked_chunk.links.first_chunk();
173+
emit_new_first_chunk_updates(first_chunk, updates);
174+
}
173175

174-
let previous = first_chunk.previous().map(Chunk::identifier).or(first_chunk.lazy_previous);
175-
let new = first_chunk.identifier();
176-
let next = first_chunk.next().map(Chunk::identifier);
176+
Ok(())
177+
}
177178

178-
match first_chunk.content() {
179-
ChunkContent::Gap(gap) => {
180-
updates.push(Update::NewGapChunk { previous, new, next, gap: gap.clone() });
181-
}
179+
/// Emit updates whenever a new first chunk is inserted at the front of a
180+
/// `LinkedChunk`.
181+
fn emit_new_first_chunk_updates<const CAP: usize, Item, Gap>(
182+
chunk: &Chunk<CAP, Item, Gap>,
183+
updates: &mut ObservableUpdates<Item, Gap>,
184+
) where
185+
Item: Clone,
186+
Gap: Clone,
187+
{
188+
let previous = chunk.previous().map(Chunk::identifier).or(chunk.lazy_previous);
189+
let new = chunk.identifier();
190+
let next = chunk.next().map(Chunk::identifier);
182191

183-
ChunkContent::Items(items) => {
184-
updates.push(Update::NewItemsChunk { previous, new, next });
185-
updates.push(Update::PushItems {
186-
at: first_chunk.first_position(),
187-
items: items.clone(),
188-
});
189-
}
192+
match chunk.content() {
193+
ChunkContent::Gap(gap) => {
194+
updates.push(Update::NewGapChunk { previous, new, next, gap: gap.clone() });
195+
}
196+
ChunkContent::Items(items) => {
197+
updates.push(Update::NewItemsChunk { previous, new, next });
198+
updates.push(Update::PushItems { at: chunk.first_position(), items: items.clone() });
199+
}
200+
}
201+
}
202+
203+
/// Replace the items with the given last chunk of items and generator.
204+
///
205+
/// This clears all the chunks in memory before resetting to the new chunk,
206+
/// if provided.
207+
pub fn replace_with<const CAP: usize, Item, Gap>(
208+
linked_chunk: &mut LinkedChunk<CAP, Item, Gap>,
209+
chunk: Option<RawChunk<Item, Gap>>,
210+
chunk_identifier_generator: ChunkIdentifierGenerator,
211+
) -> Result<(), LazyLoaderError>
212+
where
213+
Item: Clone,
214+
Gap: Clone,
215+
{
216+
let Some(mut chunk) = chunk else {
217+
// This is equivalent to clearing the linked chunk, and overriding the chunk ID
218+
// generator afterwards. But, if there was no chunks in the DB, the generator
219+
// should be reset too, so it's entirely equivalent to a clear.
220+
linked_chunk.clear();
221+
return Ok(());
222+
};
223+
224+
// Check consistency before replacing the `LinkedChunk`.
225+
// The number of items is not too large.
226+
if let ChunkContent::Items(items) = &chunk.content {
227+
if items.len() > CAP {
228+
return Err(LazyLoaderError::ChunkTooLarge { id: chunk.identifier });
190229
}
191230
}
192231

232+
// Chunk has no next chunk.
233+
if chunk.next.is_some() {
234+
return Err(LazyLoaderError::ChunkIsNotLast { id: chunk.identifier });
235+
}
236+
237+
// The last chunk is now valid.
238+
linked_chunk.chunk_identifier_generator = chunk_identifier_generator;
239+
240+
// Take the `previous` chunk and consider it becomes the `lazy_previous`.
241+
let lazy_previous = chunk.previous.take();
242+
243+
// Transform the `RawChunk` into a `Chunk`.
244+
let mut chunk_ptr = Chunk::new_leaked(chunk.identifier, chunk.content);
245+
246+
// Set the `lazy_previous` value!
247+
//
248+
// SAFETY: Pointer is convertible to a reference.
249+
unsafe { chunk_ptr.as_mut() }.lazy_previous = lazy_previous;
250+
251+
// Replace the first link with the new pointer.
252+
linked_chunk.links.replace_with(chunk_ptr);
253+
254+
if let Some(updates) = linked_chunk.updates.as_mut() {
255+
// TODO: clear updates first? (see same comment in `clear`).
256+
updates.push(Update::Clear);
257+
emit_new_first_chunk_updates(linked_chunk.links.first_chunk(), updates);
258+
}
259+
193260
Ok(())
194261
}
195262

@@ -291,8 +358,9 @@ mod tests {
291358
use assert_matches::assert_matches;
292359

293360
use super::{
294-
super::Position, from_all_chunks, from_last_chunk, insert_new_first_chunk, ChunkContent,
295-
ChunkIdentifier, ChunkIdentifierGenerator, LazyLoaderError, LinkedChunk, RawChunk, Update,
361+
super::Position, from_all_chunks, from_last_chunk, insert_new_first_chunk, replace_with,
362+
ChunkContent, ChunkIdentifier, ChunkIdentifierGenerator, LazyLoaderError, LinkedChunk,
363+
RawChunk, Update,
296364
};
297365

298366
#[test]
@@ -574,6 +642,177 @@ mod tests {
574642
}
575643
}
576644

645+
#[test]
646+
fn test_replace_with_chunk_too_large() {
647+
// Start with a linked chunk with 3 chunks: one item, one gap, one item.
648+
let mut linked_chunk = LinkedChunk::<2, char, ()>::new();
649+
linked_chunk.push_items_back(vec!['a', 'b']);
650+
linked_chunk.push_gap_back(());
651+
linked_chunk.push_items_back(vec!['c', 'd']);
652+
653+
// Try to replace it with a last chunk that has too many items.
654+
let chunk_identifier_generator = ChunkIdentifierGenerator::new_from_scratch();
655+
656+
let chunk_id = ChunkIdentifier::new(1);
657+
let raw_chunk = RawChunk {
658+
previous: Some(ChunkIdentifier::new(0)),
659+
identifier: chunk_id,
660+
next: None,
661+
content: ChunkContent::Items(vec!['e', 'f', 'g', 'h']),
662+
};
663+
664+
let err = replace_with(&mut linked_chunk, Some(raw_chunk), chunk_identifier_generator)
665+
.unwrap_err();
666+
assert_matches!(err, LazyLoaderError::ChunkTooLarge { id } => {
667+
assert_eq!(chunk_id, id);
668+
});
669+
}
670+
671+
#[test]
672+
fn test_replace_with_next_chunk() {
673+
// Start with a linked chunk with 3 chunks: one item, one gap, one item.
674+
let mut linked_chunk = LinkedChunk::<2, char, ()>::new();
675+
linked_chunk.push_items_back(vec!['a', 'b']);
676+
linked_chunk.push_gap_back(());
677+
linked_chunk.push_items_back(vec!['c', 'd']);
678+
679+
// Try to replace it with a last chunk that has too many items.
680+
let chunk_identifier_generator = ChunkIdentifierGenerator::new_from_scratch();
681+
682+
let chunk_id = ChunkIdentifier::new(1);
683+
let raw_chunk = RawChunk {
684+
previous: Some(ChunkIdentifier::new(0)),
685+
identifier: chunk_id,
686+
next: Some(ChunkIdentifier::new(2)),
687+
content: ChunkContent::Items(vec!['e', 'f']),
688+
};
689+
690+
let err = replace_with(&mut linked_chunk, Some(raw_chunk), chunk_identifier_generator)
691+
.unwrap_err();
692+
assert_matches!(err, LazyLoaderError::ChunkIsNotLast { id } => {
693+
assert_eq!(chunk_id, id);
694+
});
695+
}
696+
697+
#[test]
698+
fn test_replace_with_empty() {
699+
// Start with a linked chunk with 3 chunks: one item, one gap, one item.
700+
let mut linked_chunk = LinkedChunk::<2, char, ()>::new_with_update_history();
701+
linked_chunk.push_items_back(vec!['a', 'b']);
702+
linked_chunk.push_gap_back(());
703+
linked_chunk.push_items_back(vec!['c', 'd']);
704+
705+
// Drain initial updates.
706+
let _ = linked_chunk.updates().unwrap().take();
707+
708+
// Replace it with… you know, nothing (jon snow).
709+
let chunk_identifier_generator =
710+
ChunkIdentifierGenerator::new_from_previous_chunk_identifier(
711+
ChunkIdentifierGenerator::FIRST_IDENTIFIER,
712+
);
713+
replace_with(&mut linked_chunk, None, chunk_identifier_generator).unwrap();
714+
715+
// The linked chunk still has updates enabled.
716+
assert!(linked_chunk.updates().is_some());
717+
718+
// Check the linked chunk only contains the default empty events chunk.
719+
let mut it = linked_chunk.chunks();
720+
721+
assert_matches!(it.next(), Some(chunk) => {
722+
assert_eq!(chunk.identifier(), ChunkIdentifier::new(0));
723+
assert!(chunk.is_items());
724+
assert!(chunk.next().is_none());
725+
assert_matches!(chunk.content(), ChunkContent::Items(items) => {
726+
assert!(items.is_empty());
727+
});
728+
});
729+
730+
// And there's no other chunk.
731+
assert_matches!(it.next(), None);
732+
733+
// Check updates.
734+
{
735+
let updates = linked_chunk.updates().unwrap().take();
736+
737+
assert_eq!(updates.len(), 2);
738+
assert_eq!(
739+
updates,
740+
[
741+
Update::Clear,
742+
Update::NewItemsChunk {
743+
previous: None,
744+
new: ChunkIdentifier::new(0),
745+
next: None,
746+
},
747+
]
748+
);
749+
}
750+
}
751+
752+
#[test]
753+
fn test_replace_with_non_empty() {
754+
// Start with a linked chunk with 3 chunks: one item, one gap, one item.
755+
let mut linked_chunk = LinkedChunk::<2, char, ()>::new_with_update_history();
756+
linked_chunk.push_items_back(vec!['a', 'b']);
757+
linked_chunk.push_gap_back(());
758+
linked_chunk.push_items_back(vec!['c', 'd']);
759+
760+
// Drain initial updates.
761+
let _ = linked_chunk.updates().unwrap().take();
762+
763+
// Replace it with a single chunk (sorry, jon).
764+
let chunk_identifier_generator =
765+
ChunkIdentifierGenerator::new_from_previous_chunk_identifier(ChunkIdentifier::new(42));
766+
767+
let chunk_id = ChunkIdentifier::new(1);
768+
let chunk = RawChunk {
769+
previous: Some(ChunkIdentifier::new(0)),
770+
identifier: chunk_id,
771+
next: None,
772+
content: ChunkContent::Items(vec!['e', 'f']),
773+
};
774+
replace_with(&mut linked_chunk, Some(chunk), chunk_identifier_generator).unwrap();
775+
776+
// The linked chunk still has updates enabled.
777+
assert!(linked_chunk.updates().is_some());
778+
779+
let mut it = linked_chunk.chunks();
780+
781+
// The first chunk is an event chunks with the expected items.
782+
assert_matches!(it.next(), Some(chunk) => {
783+
assert_eq!(chunk.identifier(), chunk_id);
784+
assert!(chunk.next().is_none());
785+
assert_matches!(chunk.content(), ChunkContent::Items(items) => {
786+
assert_eq!(*items, vec!['e', 'f']);
787+
});
788+
});
789+
790+
// Nothing more.
791+
assert!(it.next().is_none());
792+
793+
// Check updates.
794+
{
795+
let updates = linked_chunk.updates().unwrap().take();
796+
797+
assert_eq!(updates.len(), 3);
798+
assert_eq!(
799+
updates,
800+
[
801+
Update::Clear,
802+
Update::NewItemsChunk {
803+
previous: Some(ChunkIdentifier::new(0)),
804+
new: chunk_id,
805+
next: None,
806+
},
807+
Update::PushItems {
808+
at: Position::new(ChunkIdentifier::new(1), 0),
809+
items: vec!['e', 'f']
810+
}
811+
]
812+
);
813+
}
814+
}
815+
577816
#[test]
578817
fn test_from_all_chunks_empty() {
579818
// Building an empty linked chunk works, and returns `None`.

crates/matrix-sdk-common/src/linked_chunk/mod.rs

Lines changed: 23 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -197,35 +197,37 @@ impl<const CAP: usize, Item, Gap> Ends<CAP, Item, Gap> {
197197
}
198198
}
199199

200-
/// Drop all chunks, and re-create the first one.
201-
fn clear(&mut self) {
200+
/// Drop all chunks, and replace the first one with the one provided as an
201+
/// argument.
202+
fn replace_with(&mut self, first_chunk: NonNull<Chunk<CAP, Item, Gap>>) {
202203
// Loop over all chunks, from the last to the first chunk, and drop them.
203-
{
204-
// Take the latest chunk.
205-
let mut current_chunk_ptr = self.last.or(Some(self.first));
206-
207-
// As long as we have another chunk…
208-
while let Some(chunk_ptr) = current_chunk_ptr {
209-
// Fetch the previous chunk pointer.
210-
let previous_ptr = unsafe { chunk_ptr.as_ref() }.previous;
204+
// Take the latest chunk.
205+
let mut current_chunk_ptr = self.last.or(Some(self.first));
211206

212-
// Re-box the chunk, and let Rust does its job.
213-
let _chunk_boxed = unsafe { Box::from_raw(chunk_ptr.as_ptr()) };
207+
// As long as we have another chunk…
208+
while let Some(chunk_ptr) = current_chunk_ptr {
209+
// Fetch the previous chunk pointer.
210+
let previous_ptr = unsafe { chunk_ptr.as_ref() }.previous;
214211

215-
// Update the `current_chunk_ptr`.
216-
current_chunk_ptr = previous_ptr;
217-
}
212+
// Re-box the chunk, and let Rust does its job.
213+
let _chunk_boxed = unsafe { Box::from_raw(chunk_ptr.as_ptr()) };
218214

219-
// At this step, all chunks have been dropped, including
220-
// `self.first`.
215+
// Update the `current_chunk_ptr`.
216+
current_chunk_ptr = previous_ptr;
221217
}
222218

223-
// Recreate the first chunk.
224-
self.first = Chunk::new_items_leaked(ChunkIdentifierGenerator::FIRST_IDENTIFIER);
225-
226-
// Reset the last chunk.
219+
// At this step, all chunks have been dropped, including `self.first`.
220+
self.first = first_chunk;
227221
self.last = None;
228222
}
223+
224+
/// Drop all chunks, and re-create the default first one.
225+
///
226+
/// The default first chunk is an empty items chunk, with the identifier
227+
/// [`ChunkIdentifierGenerator::FIRST_IDENTIFIER`].
228+
fn clear(&mut self) {
229+
self.replace_with(Chunk::new_items_leaked(ChunkIdentifierGenerator::FIRST_IDENTIFIER));
230+
}
229231
}
230232

231233
/// The [`LinkedChunk`] structure.

0 commit comments

Comments
 (0)