文档进阶主题容器 ID

容器 ID

容器 ID(Container ID)是容器的唯一标识,分为两种形式:

  • 根容器:由类型与根名称组成;
  • 普通容器:通过用户操作创建,由 ID 与类型组成。

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}@${}:${}`;
  1. 根容器(Root Containers)

    • 首次访问根容器时隐式创建,例如调用 doc.getText("text"),历史中不会生成操作记录;
    • 由字符串名称与容器类型唯一确定。
  2. 普通容器(Normal Containers)

    • 通过 insertContainercreateNode 等操作显式创建;
    • 在执行创建子容器的操作时自动生成;
    • 容器 ID 中包含其创建操作的 Operation ID。

容器状态与 ID

容器 ID 并非随机 UUID,而是基于容器上下文确定性生成。理解容器状态有助于掌握容器 ID 的生成方式。

关于容器的完整介绍(包括 attached / detached 状态),请参阅容器概念

容器 ID 生成的要点:

  • 根容器:ID 来源于名称(例如 doc.getText("text") 中的 “text”);
  • 普通容器:ID 来源于创建它的操作(OpID);
  • Detached 容器:在插入文档前使用默认的占位 ID。

容器覆盖

并行初始化子容器时,Loro 不会自动合并,而是可能发生覆盖。例如:

const : string = "hello";
const  = new ();
const  = .("map");
 
// 并行初始化子容器
const  = .();
const  = .("map").("text", new ());
.(0, "A");
const  = .("map").("text", new ());
.(0, "B");
 
.(.({ : "update" }));
// 结果可能是 { "meta": { "text": "A" } } 或 { "meta": { "text": "B" } }

如果不保留编辑历史,这种行为存在较大的数据丢失风险。即便完整历史仍在、理论上可以恢复数据,恢复过程也会相当复杂。

当容器承载大量数据或是文档的主要存储时,被覆盖可能导致关键信息被隐藏或丢失。因此需要谨慎、系统地初始化容器,以避免此类问题。

最佳实践

  1. 如果容器可能被并行初始化,优先在根层级进行初始化,而不是作为嵌套容器创建。

  2. 使用 Map 容器时:

    • 尽可能在 Map 初始化阶段一次性创建所有子容器;
    • 避免在 Map 中并发创建同名子容器,以免发生覆盖。

覆盖的根源在于并发创建子容器会生成不同的容器 ID,从而无法自动合并其内容。