Skip to content

Commit ed17330

Browse files
committed
feat: automatic merging for concurrent container inserts in maps
1 parent 08d9939 commit ed17330

File tree

8 files changed

+760
-9
lines changed

8 files changed

+760
-9
lines changed

crates/loro-common/src/lib.rs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,18 @@ pub fn check_root_container_name(name: &str) -> bool {
7373
!name.is_empty() && name.char_indices().all(|(_, x)| x != '/' && x != '\0')
7474
}
7575

76+
/// Return whether the given name indicates a mergeable container.
77+
///
78+
/// Mergeable containers are special containers that use a Root Container ID format
79+
/// but have a parent. They are identified by having a `/` in their name, which is
80+
/// forbidden for user-created root containers.
81+
///
82+
/// The format is: `parent_container_id/key`
83+
#[inline]
84+
pub fn is_mergeable_container_name(name: &str) -> bool {
85+
name.contains('/')
86+
}
87+
7688
impl CompactId {
7789
pub fn new(peer: PeerID, counter: Counter) -> Self {
7890
Self {
@@ -584,6 +596,19 @@ mod container {
584596
pub fn is_unknown(&self) -> bool {
585597
matches!(self.container_type(), ContainerType::Unknown(_))
586598
}
599+
600+
/// Returns true if this is a mergeable container.
601+
///
602+
/// Mergeable containers are special containers that use a Root Container ID format
603+
/// but have a parent. They are identified by having a `/` in their name, which is
604+
/// forbidden for user-created root containers.
605+
#[inline]
606+
pub fn is_mergeable(&self) -> bool {
607+
match self {
608+
ContainerID::Root { name, .. } => crate::is_mergeable_container_name(name),
609+
ContainerID::Normal { .. } => false,
610+
}
611+
}
587612
}
588613

589614
impl TryFrom<&str> for ContainerType {

crates/loro-internal/src/arena.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -330,7 +330,8 @@ impl SharedArena {
330330
}
331331

332332
pub fn get_parent(&self, child: ContainerIdx) -> Option<ContainerIdx> {
333-
if self.get_container_id(child).unwrap().is_root() {
333+
let id = self.get_container_id(child).unwrap();
334+
if id.is_root() && !id.is_mergeable() {
334335
// TODO: PERF: we can speed this up by use a special bit in ContainerIdx to indicate
335336
// whether the target is a root container
336337
return None;

crates/loro-internal/src/encoding/value.rs

Lines changed: 59 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,8 @@ pub enum LoroValueKind {
7070
List,
7171
Map,
7272
ContainerType,
73+
/// Root container type - used for mergeable containers
74+
RootContainerType,
7375
}
7476
impl LoroValueKind {
7577
fn from_u8(kind: u8) -> Self {
@@ -84,6 +86,7 @@ impl LoroValueKind {
8486
7 => LoroValueKind::List,
8587
8 => LoroValueKind::Map,
8688
9 => LoroValueKind::ContainerType,
89+
10 => LoroValueKind::RootContainerType,
8790
_ => unreachable!(),
8891
}
8992
}
@@ -100,6 +103,7 @@ impl LoroValueKind {
100103
LoroValueKind::List => 7,
101104
LoroValueKind::Map => 8,
102105
LoroValueKind::ContainerType => 9,
106+
LoroValueKind::RootContainerType => 10,
103107
}
104108
}
105109
}
@@ -665,6 +669,23 @@ impl<'a> ValueReader<'a> {
665669

666670
LoroValue::Container(container_id)
667671
}
672+
LoroValueKind::RootContainerType => {
673+
// Read container type
674+
let type_u8 = self.read_u8()?;
675+
let container_type =
676+
ContainerType::try_from_u8(type_u8).unwrap_or(ContainerType::Unknown(type_u8));
677+
// Read the root container name from the keys arena
678+
let key_idx = self.read_usize()?;
679+
let name = keys
680+
.get(key_idx)
681+
.ok_or(LoroError::DecodeDataCorruptionError)?
682+
.clone();
683+
let container_id = ContainerID::Root {
684+
name,
685+
container_type,
686+
};
687+
LoroValue::Container(container_id)
688+
}
668689
})
669690
}
670691

@@ -777,6 +798,23 @@ impl<'a> ValueReader<'a> {
777798

778799
LoroValue::Container(container_id)
779800
}
801+
LoroValueKind::RootContainerType => {
802+
// Read container type
803+
let type_u8 = self.read_u8()?;
804+
let container_type = ContainerType::try_from_u8(type_u8)
805+
.unwrap_or(ContainerType::Unknown(type_u8));
806+
// Read the root container name from the keys arena
807+
let name_key_idx = self.read_usize()?;
808+
let name = keys
809+
.get(name_key_idx)
810+
.ok_or(LoroError::DecodeDataCorruptionError)?
811+
.clone();
812+
let container_id = ContainerID::Root {
813+
name,
814+
container_type,
815+
};
816+
LoroValue::Container(container_id)
817+
}
780818
};
781819

782820
task = match task {
@@ -1035,10 +1073,23 @@ impl ValueWriter {
10351073
(LoroValueKind::Map, len)
10361074
}
10371075
LoroValue::Binary(value) => (LoroValueKind::Binary, self.write_binary(value)),
1038-
LoroValue::Container(c) => (
1039-
LoroValueKind::ContainerType,
1040-
self.write_u8(c.container_type().to_u8()),
1041-
),
1076+
LoroValue::Container(c) => match c {
1077+
ContainerID::Normal { container_type, .. } => (
1078+
LoroValueKind::ContainerType,
1079+
self.write_u8(container_type.to_u8()),
1080+
),
1081+
ContainerID::Root {
1082+
name,
1083+
container_type,
1084+
} => {
1085+
// For Root containers, we need to encode:
1086+
// 1. The container type
1087+
// 2. The name (via key register)
1088+
let key_idx = registers.key_mut().register(name);
1089+
let len = self.write_u8(container_type.to_u8()) + self.write_usize(key_idx);
1090+
(LoroValueKind::RootContainerType, len)
1091+
}
1092+
},
10421093
}
10431094
}
10441095

@@ -1145,6 +1196,9 @@ fn get_loro_value_kind(value: &LoroValue) -> LoroValueKind {
11451196
LoroValue::List(_) => LoroValueKind::List,
11461197
LoroValue::Map(_) => LoroValueKind::Map,
11471198
LoroValue::Binary(_) => LoroValueKind::Binary,
1148-
LoroValue::Container(_) => LoroValueKind::ContainerType,
1199+
LoroValue::Container(c) => match c {
1200+
ContainerID::Normal { .. } => LoroValueKind::ContainerType,
1201+
ContainerID::Root { .. } => LoroValueKind::RootContainerType,
1202+
},
11491203
}
11501204
}

crates/loro-internal/src/handler.rs

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4151,6 +4151,141 @@ impl MapHandler {
41514151
}),
41524152
}
41534153
}
4154+
4155+
pub fn get_mergeable_list(&self, key: &str) -> LoroResult<ListHandler> {
4156+
self.get_or_create_mergeable_container(
4157+
key,
4158+
Handler::new_unattached(ContainerType::List)
4159+
.into_list()
4160+
.unwrap(),
4161+
)
4162+
}
4163+
4164+
pub fn get_mergeable_map(&self, key: &str) -> LoroResult<MapHandler> {
4165+
self.get_or_create_mergeable_container(
4166+
key,
4167+
Handler::new_unattached(ContainerType::Map)
4168+
.into_map()
4169+
.unwrap(),
4170+
)
4171+
}
4172+
4173+
pub fn get_mergeable_movable_list(&self, key: &str) -> LoroResult<MovableListHandler> {
4174+
self.get_or_create_mergeable_container(
4175+
key,
4176+
Handler::new_unattached(ContainerType::MovableList)
4177+
.into_movable_list()
4178+
.unwrap(),
4179+
)
4180+
}
4181+
4182+
pub fn get_mergeable_text(&self, key: &str) -> LoroResult<TextHandler> {
4183+
self.get_or_create_mergeable_container(
4184+
key,
4185+
Handler::new_unattached(ContainerType::Text)
4186+
.into_text()
4187+
.unwrap(),
4188+
)
4189+
}
4190+
4191+
pub fn get_mergeable_tree(&self, key: &str) -> LoroResult<TreeHandler> {
4192+
self.get_or_create_mergeable_container(
4193+
key,
4194+
Handler::new_unattached(ContainerType::Tree)
4195+
.into_tree()
4196+
.unwrap(),
4197+
)
4198+
}
4199+
4200+
#[cfg(feature = "counter")]
4201+
pub fn get_mergeable_counter(&self, key: &str) -> LoroResult<counter::CounterHandler> {
4202+
self.get_or_create_mergeable_container(
4203+
key,
4204+
Handler::new_unattached(ContainerType::Counter)
4205+
.into_counter()
4206+
.unwrap(),
4207+
)
4208+
}
4209+
4210+
pub fn get_or_create_mergeable_container<C: HandlerTrait>(
4211+
&self,
4212+
key: &str,
4213+
child: C,
4214+
) -> LoroResult<C> {
4215+
// Extract just the name portion from the parent container ID.
4216+
// For Root containers, we use the name directly (without the "cid:root-" prefix).
4217+
// For Normal containers, we format as "counter@peer:type".
4218+
let parent_name = match self.id() {
4219+
ContainerID::Root { name, .. } => name.to_string(),
4220+
ContainerID::Normal {
4221+
peer,
4222+
counter,
4223+
container_type,
4224+
} => format!("{}@{}:{}", counter, peer, container_type),
4225+
};
4226+
let name = format!("{}/{}", parent_name, key);
4227+
let expected_id = ContainerID::Root {
4228+
name: name.into(),
4229+
container_type: child.kind(),
4230+
};
4231+
4232+
// Check if exists
4233+
if let Some(ValueOrHandler::Handler(h)) = self.get_(key) {
4234+
if h.id() == expected_id {
4235+
if let Some(c) = C::from_handler(h) {
4236+
return Ok(c);
4237+
} else {
4238+
unreachable!("Container type mismatch for same ID");
4239+
}
4240+
}
4241+
}
4242+
4243+
// Create
4244+
match &self.inner {
4245+
MaybeDetached::Detached(_) => Err(LoroError::MisuseDetachedContainer {
4246+
method: "get_or_create_mergeable_container",
4247+
}),
4248+
MaybeDetached::Attached(a) => a.with_txn(|txn| {
4249+
self.insert_mergeable_container_with_txn(txn, key, child, expected_id)
4250+
}),
4251+
}
4252+
}
4253+
4254+
pub fn insert_mergeable_container_with_txn<H: HandlerTrait>(
4255+
&self,
4256+
txn: &mut Transaction,
4257+
key: &str,
4258+
child: H,
4259+
container_id: ContainerID,
4260+
) -> LoroResult<H> {
4261+
let inner = self.inner.try_attached_state()?;
4262+
4263+
// Insert into Map
4264+
txn.apply_local_op(
4265+
inner.container_idx,
4266+
crate::op::RawOpContent::Map(crate::container::map::MapSet {
4267+
key: key.into(),
4268+
value: Some(LoroValue::Container(container_id.clone())),
4269+
}),
4270+
EventHint::Map {
4271+
key: key.into(),
4272+
value: Some(LoroValue::Container(container_id.clone())),
4273+
},
4274+
&inner.doc,
4275+
)?;
4276+
4277+
// Attach
4278+
let ans = child.attach(txn, inner, container_id)?;
4279+
4280+
// Set Parent in Arena
4281+
let child_idx = ans.idx();
4282+
inner
4283+
.doc
4284+
.arena
4285+
.set_parent(child_idx, Some(inner.container_idx));
4286+
4287+
Ok(ans)
4288+
}
41544289
}
41554290

41564291
fn with_txn<R>(doc: &LoroDoc, f: impl FnOnce(&mut Transaction) -> LoroResult<R>) -> LoroResult<R> {

crates/loro-internal/src/state.rs

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -876,7 +876,7 @@ impl DocState {
876876
let roots = self.arena.root_containers(flag);
877877
let ans: loro_common::LoroMapValue = roots
878878
.into_iter()
879-
.map(|idx| {
879+
.filter_map(|idx| {
880880
let id = self.arena.idx_to_id(idx).unwrap();
881881
let ContainerID::Root {
882882
name,
@@ -885,7 +885,11 @@ impl DocState {
885885
else {
886886
unreachable!()
887887
};
888-
(name.to_string(), LoroValue::Container(id))
888+
// Skip mergeable containers - they should not appear at the root level
889+
if id.is_mergeable() {
890+
return None;
891+
}
892+
Some((name.to_string(), LoroValue::Container(id)))
889893
})
890894
.collect();
891895
LoroValue::Map(ans)
@@ -905,6 +909,10 @@ impl DocState {
905909
let id = self.arena.idx_to_id(root_idx).unwrap();
906910
match &id {
907911
loro_common::ContainerID::Root { name, .. } => {
912+
// Skip mergeable containers - they should not appear at the root level
913+
if id.is_mergeable() {
914+
continue;
915+
}
908916
let v = self.get_container_deep_value(root_idx);
909917
if (should_hide_empty_root_container || deleted_root_container.contains(&id))
910918
&& v.is_empty_collection()
@@ -931,6 +939,10 @@ impl DocState {
931939
let id = self.arena.idx_to_id(root_idx).unwrap();
932940
match id.clone() {
933941
loro_common::ContainerID::Root { name, .. } => {
942+
// Skip mergeable containers - they should not appear at the root level
943+
if id.is_mergeable() {
944+
continue;
945+
}
934946
ans.insert(
935947
name.to_string(),
936948
self.get_container_deep_value_with_id(root_idx, Some(id)),

0 commit comments

Comments
 (0)