1mod str_arena;
2use self::str_arena::{StrArena, StrArenaCheckpoint};
3use crate::sync::{Mutex, MutexGuard, RwLock, RwLockWriteGuard};
4use crate::{
5 change::Lamport,
6 container::{
7 idx::ContainerIdx,
8 list::list_op::{InnerListOp, ListOp},
9 map::MapSet,
10 ContainerID,
11 },
12 id::Counter,
13 op::{InnerContent, ListSlice, Op, RawOp, RawOpContent, SliceRange},
14 LoroValue,
15};
16use append_only_bytes::BytesSlice;
17use loro_common::PeerID;
18use rustc_hash::FxHashMap;
19use std::fmt;
20use std::{
21 num::NonZeroU16,
22 ops::{Range, RangeBounds},
23 sync::Arc,
24};
25
26pub(crate) struct LoadAllFlag;
27type ParentResolver = dyn Fn(ContainerID) -> Option<ContainerID> + Send + Sync + 'static;
28
29#[derive(Default)]
30struct ArenaContainers {
31 container_idx_to_id: Vec<ContainerID>,
32 depth: Vec<Option<NonZeroU16>>,
35 container_id_to_idx: FxHashMap<ContainerID, ContainerIdx>,
36 parents: FxHashMap<ContainerIdx, Option<ContainerIdx>>,
38 root_c_idx: Vec<ContainerIdx>,
41 top_level_root_c_idx: Vec<ContainerIdx>,
46 parent_resolver: Option<Arc<ParentResolver>>,
52}
53
54#[derive(Default)]
55struct InnerSharedArena {
56 containers: RwLock<ArenaContainers>,
60 values: Mutex<Vec<LoroValue>>,
61 str: Arc<Mutex<StrArena>>,
62}
63
64impl fmt::Debug for InnerSharedArena {
65 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
66 f.debug_struct("InnerSharedArena")
67 .field("containers", &"<Mutex<_>>")
68 .field("values", &"<Mutex<_>>")
69 .field("str", &"<Arc<Mutex<_>>>")
70 .finish()
71 }
72}
73
74#[derive(Debug, Clone)]
77pub struct SharedArena {
78 inner: Arc<InnerSharedArena>,
79}
80
81pub(crate) struct SharedArenaRollback {
82 container_len: usize,
83 root_len: usize,
84 top_level_root_len: usize,
85 values_len: usize,
86 str: StrArenaCheckpoint,
87}
88
89#[derive(Debug)]
90pub struct StrAllocResult {
91 pub start: usize,
93 pub end: usize,
95}
96
97impl ArenaContainers {
98 fn push_root(&mut self, idx: ContainerIdx, is_mergeable: bool) {
107 self.root_c_idx.push(idx);
108 if !is_mergeable {
109 self.top_level_root_c_idx.push(idx);
110 }
111 }
112
113 fn register_container(&mut self, id: &ContainerID) -> ContainerIdx {
114 if let Some(&idx) = self.container_id_to_idx.get(id) {
115 return idx;
116 }
117
118 let idx = self.container_idx_to_id.len();
119 self.container_idx_to_id.push(id.clone());
120 let idx = ContainerIdx::from_index_and_type(idx as u32, id.container_type());
121 self.container_id_to_idx.insert(id.clone(), idx);
122 let mergeable_parts = if id.is_root() {
125 id.parse_mergeable()
126 } else {
127 None
128 };
129 match (id.is_root(), mergeable_parts) {
130 (true, None) => {
131 self.push_root(idx, false);
132 self.parents.insert(idx, None);
133 self.depth.push(NonZeroU16::new(1));
134 }
135 (true, Some((parent_id, _key, _kind))) => {
136 self.push_root(idx, true);
143 self.depth.push(None);
144 let parent_idx = self.register_container(&parent_id);
145 self.set_parent(idx, Some(parent_idx));
146 }
147 (false, _) => {
148 self.depth.push(None);
149 }
150 }
151 idx
152 }
153
154 fn set_parent(&mut self, child: ContainerIdx, parent: Option<ContainerIdx>) {
155 self.parents.insert(child, parent);
156
157 match parent {
158 Some(p) => {
159 if let Some(d) = self.depth[p.to_index() as usize] {
164 self.depth[child.to_index() as usize] = NonZeroU16::new(d.get() + 1);
165 } else {
166 self.depth[child.to_index() as usize] = None;
167 }
168 }
169 None => {
170 self.depth[child.to_index() as usize] = NonZeroU16::new(1);
171 }
172 }
173 }
174
175 fn get_depth(&mut self, target: ContainerIdx) -> Option<NonZeroU16> {
176 if let Some(d) = self.depth[target.to_index() as usize] {
177 return Some(d);
178 }
179
180 let parent: Option<ContainerIdx> = if let Some(p) = self.parents.get(&target) {
181 *p
182 } else {
183 let id = self
184 .container_idx_to_id
185 .get(target.to_index() as usize)
186 .unwrap()
187 .clone();
188 if id.is_root() && !id.is_mergeable() {
189 None
190 } else {
191 let resolver = self.parent_resolver.clone()?;
196 let parent_id = resolver(id)?;
197 let parent_idx =
202 if let Some(idx) = self.container_id_to_idx.get(&parent_id).copied() {
203 if parent_id.is_root() && !parent_id.is_mergeable() {
206 self.parents.entry(idx).or_insert(None);
207 if self.depth[idx.to_index() as usize].is_none() {
208 self.depth[idx.to_index() as usize] = NonZeroU16::new(1);
209 }
210 }
211 idx
212 } else {
213 self.register_container(&parent_id)
214 };
215
216 Some(parent_idx)
217 }
218 };
219
220 let d = match parent {
221 Some(p) => NonZeroU16::new(self.get_depth(p)?.get() + 1),
222 None => NonZeroU16::new(1),
223 };
224 self.depth[target.to_index() as usize] = d;
225 d
226 }
227
228 fn container_id(&self, idx: ContainerIdx) -> Option<ContainerID> {
229 self.container_idx_to_id
230 .get(idx.to_index() as usize)
231 .cloned()
232 }
233}
234
235pub(crate) struct ArenaGuards<'a> {
236 containers: RwLockWriteGuard<'a, ArenaContainers>,
237}
238
239impl ArenaGuards<'_> {
240 pub fn register_container(&mut self, id: &ContainerID) -> ContainerIdx {
241 self.containers.register_container(id)
242 }
243
244 pub fn set_parent(&mut self, child: ContainerIdx, parent: Option<ContainerIdx>) {
245 self.containers.set_parent(child, parent);
246 }
247}
248
249impl SharedArena {
250 #[allow(clippy::new_without_default)]
251 pub fn new() -> Self {
252 Self {
253 inner: Arc::new(InnerSharedArena::default()),
254 }
255 }
256
257 pub fn fork(&self) -> Self {
258 Self {
259 inner: Arc::new(InnerSharedArena {
260 containers: RwLock::new({
261 let containers = self.inner.containers.read();
262 ArenaContainers {
263 container_idx_to_id: containers.container_idx_to_id.clone(),
264 depth: containers.depth.clone(),
265 container_id_to_idx: containers.container_id_to_idx.clone(),
266 parents: containers.parents.clone(),
267 root_c_idx: containers.root_c_idx.clone(),
268 top_level_root_c_idx: containers.top_level_root_c_idx.clone(),
269 parent_resolver: containers.parent_resolver.clone(),
270 }
271 }),
272 values: Mutex::new(self.inner.values.lock().clone()),
273 str: self.inner.str.clone(),
274 }),
275 }
276 }
277
278 pub(crate) fn checkpoint_for_rollback(&self) -> SharedArenaRollback {
279 let containers = self.inner.containers.read();
280 let container_len = containers.container_idx_to_id.len();
281 let root_len = containers.root_c_idx.len();
282 let top_level_root_len = containers.top_level_root_c_idx.len();
283 drop(containers);
284 let values_len = self.inner.values.lock().len();
285 let str = self.inner.str.lock().checkpoint();
286 SharedArenaRollback {
287 container_len,
288 root_len,
289 top_level_root_len,
290 values_len,
291 str,
292 }
293 }
294
295 pub(crate) fn rollback(&self, checkpoint: SharedArenaRollback) {
296 let mut containers = self.inner.containers.write();
297 let removed_ids = containers
298 .container_idx_to_id
299 .split_off(checkpoint.container_len);
300 for id in removed_ids {
301 containers.container_id_to_idx.remove(&id);
302 }
303 containers.depth.truncate(checkpoint.container_len);
304 containers.root_c_idx.truncate(checkpoint.root_len);
305 containers
306 .top_level_root_c_idx
307 .truncate(checkpoint.top_level_root_len);
308 containers.parents.retain(|child, parent| {
309 let child_is_kept = (child.to_index() as usize) < checkpoint.container_len;
310 let parent_is_kept = parent
311 .map(|p| (p.to_index() as usize) < checkpoint.container_len)
312 .unwrap_or(true);
313 child_is_kept && parent_is_kept
314 });
315 drop(containers);
316
317 self.inner.values.lock().truncate(checkpoint.values_len);
318 self.inner.str.lock().rollback(checkpoint.str);
319 }
320
321 pub(crate) fn with_guards(&self, f: impl FnOnce(&mut ArenaGuards)) {
322 let mut guards = self.get_arena_guards();
323 f(&mut guards);
324 }
325
326 fn get_arena_guards(&self) -> ArenaGuards<'_> {
327 ArenaGuards {
328 containers: self.inner.containers.write(),
329 }
330 }
331
332 pub fn register_container(&self, id: &ContainerID) -> ContainerIdx {
333 self.inner.containers.write().register_container(id)
334 }
335
336 pub fn get_container_id(&self, idx: ContainerIdx) -> Option<ContainerID> {
337 self.inner.containers.read().container_id(idx)
338 }
339
340 pub fn id_to_idx(&self, id: &ContainerID) -> Option<ContainerIdx> {
350 self.inner
351 .containers
352 .read()
353 .container_id_to_idx
354 .get(id)
355 .copied()
356 }
357
358 #[inline]
359 pub fn idx_to_id(&self, id: ContainerIdx) -> Option<ContainerID> {
360 self.inner.containers.read().container_id(id)
361 }
362
363 #[inline]
364 pub fn with_idx_to_id<R>(&self, f: impl FnOnce(&Vec<ContainerID>) -> R) -> R {
365 let containers = self.inner.containers.read();
366 f(&containers.container_idx_to_id)
367 }
368
369 pub fn alloc_str(&self, str: &str) -> StrAllocResult {
370 let mut text_lock = self.inner.str.lock();
371 _alloc_str(&mut text_lock, str)
372 }
373
374 pub fn alloc_str_with_slice(&self, str: &str) -> (BytesSlice, StrAllocResult) {
376 let mut text_lock = self.inner.str.lock();
377 _alloc_str_with_slice(&mut text_lock, str)
378 }
379
380 pub fn alloc_str_fast(&self, bytes: &[u8]) {
382 let mut text_lock = self.inner.str.lock();
383 text_lock.alloc(std::str::from_utf8(bytes).unwrap());
384 }
385
386 #[inline]
387 pub fn utf16_len(&self) -> usize {
388 self.inner.str.lock().len_utf16()
389 }
390
391 #[inline]
392 pub fn alloc_value(&self, value: LoroValue) -> usize {
393 let mut values_lock = self.inner.values.lock();
394 _alloc_value(&mut values_lock, value)
395 }
396
397 #[inline]
398 pub fn alloc_values(&self, values: impl Iterator<Item = LoroValue>) -> std::ops::Range<usize> {
399 let mut values_lock = self.inner.values.lock();
400 _alloc_values(&mut values_lock, values)
401 }
402
403 #[inline]
404 pub fn set_parent(&self, child: ContainerIdx, parent: Option<ContainerIdx>) {
405 self.inner.containers.write().set_parent(child, parent);
406 }
407
408 pub fn log_hierarchy(&self) {
409 if cfg!(debug_assertions) {
410 let containers = self.inner.containers.read();
411 for (c, p) in containers.parents.iter() {
412 tracing::info!(
413 "container {:?} {:?} {:?}",
414 c,
415 containers.container_id(*c),
416 p.and_then(|x| containers.container_id(x))
417 );
418 }
419 }
420 }
421
422 pub fn log_all_containers(&self) {
423 let containers = self.inner.containers.read();
424 containers.container_id_to_idx.iter().for_each(|(id, idx)| {
425 tracing::info!("container {:?} {:?}", id, idx);
426 });
427 containers
428 .container_idx_to_id
429 .iter()
430 .enumerate()
431 .for_each(|(i, id)| {
432 tracing::info!("container {} {:?}", i, id);
433 });
434 }
435
436 pub fn get_parent(&self, child: ContainerIdx) -> Option<ContainerIdx> {
437 let (child_id, resolver) = {
438 let containers = self.inner.containers.read();
439 let child_id = containers.container_id(child).unwrap();
440 if child_id.is_root() && !child_id.is_mergeable() {
441 return None;
444 }
445
446 if let Some(p) = containers.parents.get(&child).copied() {
448 return p;
449 }
450
451 (child_id, containers.parent_resolver.clone())
453 };
454 if let Some(resolver) = resolver {
455 if let Some(parent_id) = resolver(child_id.clone()) {
456 let parent_idx = self.register_container(&parent_id);
457 self.set_parent(child, Some(parent_idx));
458 return Some(parent_idx);
459 }
460 }
461
462 panic!("InternalError: Parent is not registered")
463 }
464
465 pub(crate) fn get_registered_parent(
469 &self,
470 child: ContainerIdx,
471 ) -> Option<Option<ContainerIdx>> {
472 self.inner.containers.read().parents.get(&child).copied()
473 }
474
475 pub fn with_ancestors(&self, container: ContainerIdx, mut f: impl FnMut(ContainerIdx, bool)) {
479 let mut container = Some(container);
480 let mut is_first = true;
481 while let Some(c) = container {
482 f(c, is_first);
483 is_first = false;
484 container = self.get_parent(c)
485 }
486 }
487
488 #[inline]
489 pub fn slice_by_unicode(&self, range: impl RangeBounds<usize>) -> BytesSlice {
490 self.inner.str.lock().slice_by_unicode(range)
491 }
492
493 #[inline]
494 pub fn slice_by_utf8(&self, range: impl RangeBounds<usize>) -> BytesSlice {
495 self.inner.str.lock().slice_bytes(range)
496 }
497
498 #[inline]
499 pub fn slice_str_by_unicode_range(&self, range: Range<usize>) -> String {
500 let mut s = self.inner.str.lock();
501 let s: &mut StrArena = &mut s;
502 let mut ans = String::with_capacity(range.len());
503 ans.push_str(s.slice_str_by_unicode(range));
504 ans
505 }
506
507 #[inline]
508 pub fn with_text_slice(&self, range: Range<usize>, mut f: impl FnMut(&str)) {
509 f(self.inner.str.lock().slice_str_by_unicode(range))
510 }
511
512 #[inline]
513 pub fn get_value(&self, idx: usize) -> Option<LoroValue> {
514 self.inner.values.lock().get(idx).cloned()
515 }
516
517 #[inline]
518 pub fn get_values(&self, range: Range<usize>) -> Vec<LoroValue> {
519 (self.inner.values.lock()[range]).to_vec()
520 }
521
522 #[inline]
525 pub fn with_values<R>(&self, range: Range<usize>, f: impl FnOnce(&[LoroValue]) -> R) -> R {
526 f(&self.inner.values.lock()[range])
527 }
528
529 pub fn convert_single_op(
530 &self,
531 container: &ContainerID,
532 peer: PeerID,
533 counter: Counter,
534 lamport: Lamport,
535 content: RawOpContent,
536 ) -> Op {
537 let container = self.register_container(container);
538 self.inner_convert_op(content, peer, counter, lamport, container)
539 }
540
541 pub fn can_import_snapshot(&self) -> bool {
542 let str_empty = self.inner.str.lock().is_empty();
543 let values_empty = self.inner.values.lock().is_empty();
544 str_empty && values_empty
545 }
546
547 fn inner_convert_op(
548 &self,
549 content: RawOpContent<'_>,
550 _peer: PeerID,
551 counter: i32,
552 _lamport: Lamport,
553 container: ContainerIdx,
554 ) -> Op {
555 match content {
556 crate::op::RawOpContent::Map(MapSet { key, value }) => Op {
557 counter,
558 container,
559 content: crate::op::InnerContent::Map(MapSet { key, value }),
560 },
561 crate::op::RawOpContent::List(list) => match list {
562 ListOp::Insert { slice, pos } => match slice {
563 ListSlice::RawData(values) => {
564 let range = self.alloc_values(values.iter().cloned());
565 Op {
566 counter,
567 container,
568 content: crate::op::InnerContent::List(InnerListOp::Insert {
569 slice: SliceRange::from(range.start as u32..range.end as u32),
570 pos,
571 }),
572 }
573 }
574 ListSlice::RawStr { str, unicode_len } => {
575 let (slice, info) = self.alloc_str_with_slice(&str);
576 Op {
577 counter,
578 container,
579 content: crate::op::InnerContent::List(InnerListOp::InsertText {
580 slice,
581 unicode_start: info.start as u32,
582 unicode_len: unicode_len as u32,
583 pos: pos as u32,
584 }),
585 }
586 }
587 },
588 ListOp::Delete(span) => Op {
589 counter,
590 container,
591 content: crate::op::InnerContent::List(InnerListOp::Delete(span)),
592 },
593 ListOp::StyleStart {
594 start,
595 end,
596 info,
597 key,
598 value,
599 } => Op {
600 counter,
601 container,
602 content: InnerContent::List(InnerListOp::StyleStart {
603 start,
604 end,
605 key,
606 info,
607 value,
608 }),
609 },
610 ListOp::StyleEnd => Op {
611 counter,
612 container,
613 content: InnerContent::List(InnerListOp::StyleEnd),
614 },
615 ListOp::Move {
616 from,
617 to,
618 elem_id: from_id,
619 } => Op {
620 counter,
621 container,
622 content: InnerContent::List(InnerListOp::Move {
623 from,
624 to,
625 elem_id: from_id,
626 }),
627 },
628 ListOp::Set { elem_id, value } => Op {
629 counter,
630 container,
631 content: InnerContent::List(InnerListOp::Set { elem_id, value }),
632 },
633 },
634 crate::op::RawOpContent::Tree(tree) => Op {
635 counter,
636 container,
637 content: crate::op::InnerContent::Tree(tree.clone()),
638 },
639 #[cfg(feature = "counter")]
640 crate::op::RawOpContent::Counter(c) => Op {
641 counter,
642 container,
643 content: crate::op::InnerContent::Future(crate::op::FutureInnerContent::Counter(c)),
644 },
645 crate::op::RawOpContent::Unknown { prop, value } => Op {
646 counter,
647 container,
648 content: crate::op::InnerContent::Future(crate::op::FutureInnerContent::Unknown {
649 prop,
650 value: Box::new(value),
651 }),
652 },
653 }
654 }
655
656 #[inline]
657 pub fn convert_raw_op(&self, op: &RawOp) -> Op {
658 self.inner_convert_op(
659 op.content.clone(),
660 op.id.peer,
661 op.id.counter,
662 op.lamport,
663 op.container,
664 )
665 }
666
667 #[inline]
668 pub fn export_containers(&self) -> Vec<ContainerID> {
669 self.inner.containers.read().container_idx_to_id.clone()
670 }
671
672 pub fn export_parents(&self) -> Vec<Option<ContainerIdx>> {
673 let containers = self.inner.containers.read();
674 containers
675 .container_idx_to_id
676 .iter()
677 .enumerate()
678 .map(|(x, id)| {
679 let idx = ContainerIdx::from_index_and_type(x as u32, id.container_type());
680 let parent_idx = containers.parents.get(&idx)?;
681 *parent_idx
682 })
683 .collect()
684 }
685
686 #[inline]
694 pub(crate) fn root_containers(&self, _f: LoadAllFlag) -> Vec<ContainerIdx> {
695 self.inner.containers.read().root_c_idx.clone()
696 }
697
698 #[inline]
704 pub(crate) fn top_level_root_containers(&self, _f: LoadAllFlag) -> Vec<ContainerIdx> {
705 self.inner.containers.read().top_level_root_c_idx.clone()
706 }
707
708 pub(crate) fn get_depth(&self, container: ContainerIdx) -> Option<NonZeroU16> {
710 self.inner.containers.write().get_depth(container)
711 }
712
713 pub(crate) fn iter_value_slice(
714 &self,
715 range: Range<usize>,
716 ) -> impl Iterator<Item = LoroValue> + '_ {
717 let values = self.inner.values.lock();
718 range
719 .into_iter()
720 .map(move |i| values.get(i).unwrap().clone())
721 }
722
723 #[allow(unused)]
724 pub(crate) fn log_all_values(&self) {
725 let values = self.inner.values.lock();
726 for (i, v) in values.iter().enumerate() {
727 loro_common::debug!("value {} {:?}", i, v);
728 }
729 }
730}
731
732fn _alloc_str_with_slice(
733 text_lock: &mut MutexGuard<'_, StrArena>,
734 str: &str,
735) -> (BytesSlice, StrAllocResult) {
736 let start = text_lock.len_bytes();
737 let ans = _alloc_str(text_lock, str);
738 (text_lock.slice_bytes(start..), ans)
739}
740
741fn _alloc_values(
742 values_lock: &mut MutexGuard<'_, Vec<LoroValue>>,
743 values: impl Iterator<Item = LoroValue>,
744) -> Range<usize> {
745 values_lock.reserve(values.size_hint().0);
746 let start = values_lock.len();
747 for value in values {
748 values_lock.push(value);
749 }
750
751 start..values_lock.len()
752}
753
754fn _alloc_value(values_lock: &mut MutexGuard<'_, Vec<LoroValue>>, value: LoroValue) -> usize {
755 values_lock.push(value);
756 values_lock.len() - 1
757}
758
759fn _alloc_str(text_lock: &mut MutexGuard<'_, StrArena>, str: &str) -> StrAllocResult {
760 let start = text_lock.len_unicode();
761 text_lock.alloc(str);
762 StrAllocResult {
763 start,
764 end: text_lock.len_unicode(),
765 }
766}
767
768fn _slice_str(range: Range<usize>, s: &mut StrArena) -> String {
769 let mut ans = String::with_capacity(range.len());
770 ans.push_str(s.slice_str_by_unicode(range));
771 ans
772}
773
774impl SharedArena {
775 pub fn set_parent_resolver<F>(&self, resolver: Option<F>)
781 where
782 F: Fn(ContainerID) -> Option<ContainerID> + Send + Sync + 'static,
783 {
784 self.inner.containers.write().parent_resolver =
785 resolver.map(|f| Arc::new(f) as Arc<ParentResolver>);
786 }
787}
788
789#[cfg(test)]
790mod tests {
791 use super::*;
792 use loro_common::ContainerType;
793 use std::sync::{
794 atomic::{AtomicBool, Ordering},
795 Arc,
796 };
797
798 #[test]
805 fn get_depth_resolver_registers_mergeable_parent_as_retention_root() {
806 let arena = SharedArena::new();
807
808 let top_root = ContainerID::new_root("state", ContainerType::Map);
809 let mergeable_parent = ContainerID::new_mergeable(&top_root, "profile", ContainerType::Map);
810 let child = ContainerID::new_normal(loro_common::ID::new(1, 0), ContainerType::List);
811
812 let mergeable_parent_for_resolver = mergeable_parent.clone();
813 let child_for_resolver = child.clone();
814 arena.set_parent_resolver(Some(move |q: ContainerID| {
815 if q == child_for_resolver {
816 Some(mergeable_parent_for_resolver.clone())
817 } else {
818 None
819 }
820 }));
821
822 let child_idx = arena.register_container(&child);
823 let _ = arena.get_depth(child_idx);
824
825 let flag = LoadAllFlag;
826 let mergeable_idx = arena
827 .id_to_idx(&mergeable_parent)
828 .expect("resolver should have registered the mergeable parent");
829 assert!(
830 arena.root_containers(flag).contains(&mergeable_idx),
831 "mergeable parent discovered via resolver must be a retention root"
832 );
833
834 let flag = LoadAllFlag;
835 assert!(
836 !arena
837 .top_level_root_containers(flag)
838 .contains(&mergeable_idx),
839 "mergeable parent must not appear in user-facing top-level enumeration"
840 );
841
842 let flag = LoadAllFlag;
843 let top_root_idx = arena
844 .id_to_idx(&top_root)
845 .expect("mergeable parent's own grandparent must also be registered");
846 assert!(
847 arena
848 .top_level_root_containers(flag)
849 .contains(&top_root_idx),
850 "the mergeable parent's grandparent (a top-level root) must be in the top-level list"
851 );
852 }
853
854 #[test]
855 fn set_parent_does_not_resolve_missing_parent_depth() {
856 let arena = SharedArena::new();
857 let parent = ContainerID::new_normal(loro_common::ID::new(1, 0), ContainerType::Map);
858 let child = ContainerID::new_mergeable(&parent, "field", ContainerType::Text);
859 let resolver_called = Arc::new(AtomicBool::new(false));
860 let called = resolver_called.clone();
861 arena.set_parent_resolver(Some(move |_| {
862 called.store(true, Ordering::SeqCst);
863 None
864 }));
865
866 arena.register_container(&child);
867
868 assert!(
869 !resolver_called.load(Ordering::SeqCst),
870 "registering a mergeable child must not invoke the lazy parent resolver"
871 );
872 }
873}