Skip to content

Conversation

@canadaduane
Copy link
Contributor

@canadaduane canadaduane commented Jan 25, 2026

Completes #759

Summary of Changes

  1. Core Logic (crates/loro-internal/src/handler.rs):

    • Implemented get_or_create_mergeable_container and insert_mergeable_container_with_txn in MapHandler.
    • Mechanism: Generates a deterministic Root Container ID using the format parent_id/key. This ensures that concurrent creations by different peers result in the same Container ID, allowing their operations to merge naturally.
    • Parentage: Explicitly sets the parent of these "Root" containers in the Arena, allowing them to function as children of a Map.
  2. Serialization (crates/loro-internal/src/state.rs):

    • Updated get_value, get_deep_value, and get_deep_value_with_id to 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.
  3. Rust API (crates/loro-internal/src/handler.rs):

    • Added get_mergeable_list, get_mergeable_map, get_mergeable_movable_list, get_mergeable_text, and get_mergeable_tree to MapHandler.
  4. WASM/JS API (crates/loro-wasm/src/lib.rs):

    • Exposed the new methods on LoroMap:
      • getMergeableList(key)
      • getMergeableMap(key)
      • getMergeableMovableList(key)
      • getMergeableText(key)
      • getMergeableTree(key)
    • Updated TypeScript definitions in crates/loro-wasm/src/lib.rs.
  5. Testing (crates/loro-internal/tests/mergeable_container.rs):

    • Created comprehensive integration tests covering:
      • Concurrent Merging: Verified that two peers creating a list at the same key results in a single merged list containing items from both.
      • Container Types: Verified support for List, Map, Text, Tree, and MovableList.
      • Deep Nesting: Verified that chains like Map -> MergeableMap -> MergeableMap -> MergeableList zipper together correctly.
      • Mixed Types: Verified that different container types at the same key do not merge (LWW applies to the Map entry).
      • Serialization: Verified that mergeable containers are correctly hidden from the root view.
      • TypeScript Tests (crates/loro-wasm/tests/mergeable.test.ts): Added tests verifying the JS API for concurrent merging, deep nesting, and correct JSON representation.

Key Observations

  • Resurrection: Deleting a mergeable container and re-creating it at the same key will restore its history, because the ID is deterministic. This is a feature of the design but should be documented for users.
  • Map-Only: This feature is strictly for LoroMap children, as it relies on stable keys for ID generation.
  • Safety: The implementation relies on the invariant that user-created Root Containers cannot contain /, creating a safe namespace for these system IDs.

Why / is better than :

  1. Reserved Namespace:

    • The Loro system enforces a rule for user-created Root Containers: their names cannot contain the / character (or \0). This is checked by check_root_container_name.
    • The : character, however, is allowed in user-created root container names.
  2. Preventing Collisions:

    • If we used :, a user could theoretically create a root container with a name like cid: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.
    • By using / (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.
  3. Parsing Simplicity:

    • When filtering these containers during serialization (to hide them from the top-level view), we can simply check 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.

@canadaduane canadaduane force-pushed the automatic-merging-concurrent-container-inserts branch from f857161 to 3395956 Compare January 25, 2026 23:21
Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment on lines +4259 to +4274
// Set Parent in Arena
let child_idx = ans.idx();
inner
.doc
.arena
.set_parent(child_idx, Some(inner.container_idx));

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in latest push.

@canadaduane
Copy link
Contributor Author

canadaduane commented Jan 26, 2026

Should there also be a getMergeableCounter? Probably yes?

EDIT: Added getMergeableCounter

@canadaduane canadaduane force-pushed the automatic-merging-concurrent-container-inserts branch from 3395956 to 9348727 Compare January 26, 2026 01:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant