feat: automatic merging for concurrent container inserts in maps#912
feat: automatic merging for concurrent container inserts in maps#912canadaduane wants to merge 1 commit intoloro-dev:mainfrom
Conversation
f857161 to
3395956
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: f8571612a4
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| // Set Parent in Arena | ||
| let child_idx = ans.idx(); | ||
| inner | ||
| .doc | ||
| .arena | ||
| .set_parent(child_idx, Some(inner.container_idx)); |
There was a problem hiding this comment.
Treat mergeable Root IDs as parentless
This explicit parent assignment won’t be observable because mergeable containers use ContainerID::Root and SharedArena::get_parent short‑circuits all root IDs to None. As a result, path/ancestor logic (e.g. DocState::get_path used by getPathToContainer and event path) will still treat mergeable containers as top‑level roots even after this call, so a getMergeableList created under a map will report a root path like cid:root-…/key instead of the map key path. That’s a functional regression for path‑based lookups/subscriptions; consider updating get_parent (and depth/root handling) to honor is_mergeable() root IDs.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Addressed in latest push.
|
Should there also be a EDIT: Added getMergeableCounter |
3395956 to
9348727
Compare
|
I've played around with this a bit now. Here are some questions that arose as an app developer using the new Loro mergeable containers:
Update: validating with some tests, it doesn't look like my map.get claim is holding up. I'll report back. |
9348727 to
6d1f86e
Compare
|
I found a bug, which explains the previous contradictory outcomes I was seeing. The "root container" is now a type that can be encoded and decoded.
Now we have a RootContainerType variant (value 10) to distinguish Root containers from Normal containers during encoding/decoding. |
6d1f86e to
ed17330
Compare
| Map, | ||
| ContainerType, | ||
| /// Root container type - used for mergeable containers | ||
| RootContainerType, |
There was a problem hiding this comment.
I don't think we need to change this. Changing this may break the compatibility with the exiting Loro
|
|
||
| let list2 = map.get_mergeable_list("list").unwrap(); | ||
| assert_eq!(list.id(), list2.id()); | ||
| assert_eq!( |
There was a problem hiding this comment.
The tests here don't feel like test the real things.
In this test case, we should test map.get_deep_value() to include the 'list' entry.
| let list1 = map1.get_mergeable_list("list").unwrap(); | ||
| let list2 = map2.get_mergeable_list("list").unwrap(); | ||
|
|
||
| assert_eq!(list1.id(), list2.id()); |
There was a problem hiding this comment.
we should test that list1 is now actually a field inside map1
| // whether the target is a root container | ||
| return None; | ||
| } | ||
|
|
There was a problem hiding this comment.
If it's root, and it's mergeable, it should return the parent here
| if let Some(c) = C::from_handler(h) { | ||
| return Ok(c); | ||
| } else { | ||
| unreachable!("Container type mismatch for same ID"); |
There was a problem hiding this comment.
We should return an Err here, instead of calling unreachable.
| const list = level2.getMergeableList("list"); | ||
|
|
||
| expect(list.length).toBe(2); | ||
| expect(list.toJSON()).toEqual(expect.arrayContaining(["A", "B"])); |
There was a problem hiding this comment.
The test cases are not testing the real things that could go wrong
This is not the case. Is it produced by Opus? |
Completes #759
Summary of Changes
Core Logic (
crates/loro-internal/src/handler.rs):get_or_create_mergeable_containerandinsert_mergeable_container_with_txninMapHandler.RootContainer ID using the formatparent_id/key. This ensures that concurrent creations by different peers result in the same Container ID, allowing their operations to merge naturally.Serialization (
crates/loro-internal/src/state.rs):get_value,get_deep_value, andget_deep_value_with_idto filter out these mergeable containers from the top-level root container list. They are identified by the reserved/character in their name. This ensures they only appear nested under their parent Map, not as detached root documents.Rust API (
crates/loro-internal/src/handler.rs):get_mergeable_list,get_mergeable_map,get_mergeable_movable_list,get_mergeable_text, andget_mergeable_treetoMapHandler.WASM/JS API (
crates/loro-wasm/src/lib.rs):LoroMap:getMergeableList(key)getMergeableMap(key)getMergeableMovableList(key)getMergeableText(key)getMergeableTree(key)crates/loro-wasm/src/lib.rs.Testing (
crates/loro-internal/tests/mergeable_container.rs):Map -> MergeableMap -> MergeableMap -> MergeableListzipper together correctly.Key Observations
LoroMapchildren, as it relies on stable keys for ID generation./, creating a safe namespace for these system IDs.Why
/is better than:Reserved Namespace:
/character (or\0). This is checked bycheck_root_container_name.:character, however, is allowed in user-created root container names.Preventing Collisions:
:, a user could theoretically create a root container with a name likecid:10@100:Map:myKey. If this matches our internal format, the system might confuse the user's root container with a mergeable child container, leading to unpredictable behavior./(e.g.,cid:10@100:Map/myKey), we are using a character that is strictly forbidden for users. This guarantees that our generated ID will never collide with any valid user-created root container. It effectively creates a private, reserved namespace for these internal containers.Parsing Simplicity:
name.contains('/'). Since users can't use/, any root container with a/in its name must be one of our internal mergeable containers.In summary, using
/leverages an existing system constraint to create a robust, collision-free identifier for this new feature.