Skip to content

Commit 3395956

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

6 files changed

Lines changed: 564 additions & 3 deletions

File tree

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/handler.rs

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4151,6 +4151,120 @@ 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+
pub fn get_or_create_mergeable_container<C: HandlerTrait>(
4201+
&self,
4202+
key: &str,
4203+
child: C,
4204+
) -> LoroResult<C> {
4205+
let name = format!("{}/{}", self.id(), key);
4206+
let expected_id = ContainerID::Root {
4207+
name: name.into(),
4208+
container_type: child.kind(),
4209+
};
4210+
4211+
// Check if exists
4212+
if let Some(ValueOrHandler::Handler(h)) = self.get_(key) {
4213+
if h.id() == expected_id {
4214+
if let Some(c) = C::from_handler(h) {
4215+
return Ok(c);
4216+
} else {
4217+
unreachable!("Container type mismatch for same ID");
4218+
}
4219+
}
4220+
}
4221+
4222+
// Create
4223+
match &self.inner {
4224+
MaybeDetached::Detached(_) => Err(LoroError::MisuseDetachedContainer {
4225+
method: "get_or_create_mergeable_container",
4226+
}),
4227+
MaybeDetached::Attached(a) => a.with_txn(|txn| {
4228+
self.insert_mergeable_container_with_txn(txn, key, child, expected_id)
4229+
}),
4230+
}
4231+
}
4232+
4233+
pub fn insert_mergeable_container_with_txn<H: HandlerTrait>(
4234+
&self,
4235+
txn: &mut Transaction,
4236+
key: &str,
4237+
child: H,
4238+
container_id: ContainerID,
4239+
) -> LoroResult<H> {
4240+
let inner = self.inner.try_attached_state()?;
4241+
4242+
// Insert into Map
4243+
txn.apply_local_op(
4244+
inner.container_idx,
4245+
crate::op::RawOpContent::Map(crate::container::map::MapSet {
4246+
key: key.into(),
4247+
value: Some(LoroValue::Container(container_id.clone())),
4248+
}),
4249+
EventHint::Map {
4250+
key: key.into(),
4251+
value: Some(LoroValue::Container(container_id.clone())),
4252+
},
4253+
&inner.doc,
4254+
)?;
4255+
4256+
// Attach
4257+
let ans = child.attach(txn, inner, container_id)?;
4258+
4259+
// Set Parent in Arena
4260+
let child_idx = ans.idx();
4261+
inner
4262+
.doc
4263+
.arena
4264+
.set_parent(child_idx, Some(inner.container_idx));
4265+
4266+
Ok(ans)
4267+
}
41544268
}
41554269

41564270
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)