同步

两个存在并发编辑的文档,只需交换两次消息即可完成同步。

下面是两个文档同步的示例:

const  = new ();
const  = new ();
const :  = .("list");
.(0, "A");
.(1, "B");
.(2, "C");
// B 导入来自 A 的操作
const :  = .({ : "update" });
// 将数据通过网络发送给 B
.();
(.()).({
  : ["A", "B", "C"],
});
 
const :  = .("list");
.(1, 1);
 
// `doc.export({ mode: "update", from: version })` 会打包自某个版本以来的全部操作
// `version` 即另一文档的版本向量
const  = .({
  : "update",
  : .(),
});
.();
 
(.()).({
  : ["A", "C"],
});
(.()).(.());

实时协作

得益于 CRDT 的特性,只要各节点最终接收到相同的更新,顺序、重复或延迟都不会影响一致性。

同步策略

  1. 首次同步(节点初次连线):

    • 新节点可以与其他节点交换版本向量,判断缺失的更新
    • 使用 doc.export({ mode: "update", from: versionVector }) 获取对方已知版本之后的增量,也可以像示例那样直接发送整个历史 doc.export({ mode: "update" })
    • 上述示例展示了基础的首次同步流程
  2. 实时同步(持续更新):

    • 订阅本地更新
    • 将更新直接广播给其他节点
    • 首次同步完成后无需再次比较版本
    • 只要所有更新都能送达,就能保持一致性

示例

以下示例展示了当一位带有离线修改的用户重新上线时,两节点如何建立实时同步:

  1. 双方交换版本信息
  2. 互相补齐缺失的更新:
    • doc2 获取 doc1 中自己缺少的更新
    • doc1 获取 doc2 中自己缺少的更新
  3. 建立实时同步,保持连接
const  = new ();
.("text").(0, "Hello");
// Peer2 加入网络
const  = new ();
// ... doc2 可能先导入本地快照
 
// 1. 交换版本信息
const  = .();
const  = .();
 
// 2. 向现有节点请求缺失更新
const  = .({
  : "update",
  : ,
});
.();
const  = .({
  : "update",
  : ,
});
.();
 
// 3. 建立实时同步
.(() => {
  // websocket.send(update);
});
.(() => {
  // websocket.send(update);
});
 
// 现在两个节点已同步,可继续协作

理解 import() 的返回值

在 Loro 的 JavaScript/WASM 绑定中,import() 会返回一个对象,告知导入结果。该对象(下文称 ImportStatusJS)结构如下:

interface ImportStatusJS {
  : PeerVersionRange;
  ?: PeerVersionRange; // 可选字段:存在待处理操作时才返回
}
 
interface PeerVersionRange {
  [: string]: {
    : number; // 起始计数(包含)
    : number; // 结束计数(不包含)
  };
}

字段说明

  1. success(必填,PeerVersionRange

    • 定义:描述哪些操作(变更)已成功导入并应用。
    • 结构:键为 PeerID,值为 { start, end },表示该 peer 从 startend - 1 的操作已处理。
    • 用途:用于确认本次导入包含了哪些更新。
    • 示例
      console.log(importResult.success);
      // 可能输出:
      // {
      //   "clientA_peerId": { start: 0, end: 50 },
      //   "server_peerId": { start: 120, end: 150 }
      // }
      // 表示成功导入了 clientA 的 0-49 号操作,server 的 120-149 号操作。
  2. pending(可选,PeerVersionRange

    • 定义:当导入的数据依赖尚未到达的操作时,会记录这些“暂挂”操作的范围。
    • 结构:与 success 相同,表示因缺失依赖而暂未应用的操作区间。
    • 用途:提示应用需要进一步获取缺失的先决操作,以保持一致性。
    • 示例
      if (importResult.pending) {
        console.log(importResult.pending);
        // 可能输出:
        // {
        //   "clientA_peerId": { start: 50, end: 60 },
        //   "clientB_peerId": { start: 10, end: 25 }
        // }
        // 表示 clientA 的 50-59、clientB 的 10-24 号操作因缺少依赖被暂挂。
      }

如何利用这些信息

  • 查看 success 了解已成功应用的更新。
  • pending 存在且非空,说明仍有操作等待依赖;应用应继续请求或同步缺少的更新,以完成整合。