Container ID
A Container ID is a unique identifier that comes in two forms:
- Root Container: Composed of a type and root name
- Normal Container: Created through user operations, composed of an ID and type
Rust ContainerID
pub enum ContainerID {
Root {
name: InternalString,
container_type: ContainerType,
},
Normal {
peer: PeerID,
counter: Counter,
container_type: ContainerType,
},
}TypeScript ContainerID
export type =
| `cid:root-${string}:${}`
| `cid:${number}@${}:${}`;-
Root Containers
- Created implicitly when accessing a root container for the first time
(e.g., calling
doc.getText("text")). No operation is generated in the history. - Uniquely identified by a string name and container type
- Created implicitly when accessing a root container for the first time
(e.g., calling
-
Normal Containers
- Created explicitly through operations like
insertContainerorcreateNode - Generated automatically when applying operations that create child containers
- Contains the Operation ID of its creation within its Container ID
- Created explicitly through operations like
-
Mergeable Containers
- Created through
LoroMap.ensureMergeableText,ensureMergeableMap,ensureMergeableList, and the otherensureMergeable*methods - Use a special form of Root Container ID. The stable name inside that ID is
derived from the logical position
(parent Map, key, container type), not from the operation that first created the child - Useful when multiple peers may lazily initialize the same child container under the same Map key
- Created through
Container States and IDs
The ContainerID is not a random UUID but is deterministically generated based on the container’s context. To understand how ContainerIDs work, it’s important to first understand container states.
For a comprehensive guide on containers, including attached vs detached states, see Container Concepts.
Key points about ContainerID generation:
- Root containers: Derive their ID from their name (e.g., “text” in
doc.getText("text")) - Normal containers: Derive their ID from the operation (OpID) that created them
- Mergeable containers: Use a special Root Container ID whose stable name is derived from the parent Map, key, and container type
- Detached containers: Have a default placeholder ID until they’re inserted into a document
Container Overwrites
When initializing child containers in parallel, overwrites can occur instead of automatic merging. For example:
const : string = "hello";
const = new ();
const = .("map");
// Parallel initialization of child containers
const = .();
const = .("map").("text", new ());
.(0, "A");
const = .("map").("text", new ());
.(0, "B");
.(.({ : "update" }));
// Result: Either { "meta": { "text": "A" } } or { "meta": { "text": "B" } }This behavior poses a significant risk of data loss if the editing history is not preserved. Even when the complete history is available and allows for data recovery, the recovery process can be complex.
When a container holds substantial data or serves as the primary storage for document content, overwriting it can lead to the unintended hiding/loss of critical information. For this reason, it is essential to implement careful and systematic container initialization practices to prevent such issues.
Mergeable Containers
The overwrite behavior occurs because parallel creation of regular child containers results in different container IDs, preventing automatic merging of their contents. If peers should address the same child by logical Map key, use a mergeable child container:
const text = doc.getMap("map").ensureMergeableText("text");This uses a special Root Container ID for the child. Its stable name is derived from the parent Map, key, and type, rather than from the operation that first created it.
Best Practices
-
Use
ensureMergeable*when a Map child container should be shared by logical key even if peers initialize it concurrently. -
When the structure is fixed and known ahead of time:
- If possible, initialize all child containers during the map container’s initialization
-
Use root containers when the child naturally has a stable top-level name.
-
Use regular child containers when each creation should have a distinct operation-derived identity.