文档教程时间回溯

在 Loro 中使用时光旅行

在 Loro 中,可以调用 doc.checkout(frontiers) 跳转到指定 Frontiers 所代表的版本(了解 Frontiers)。

需要注意:调用 doc.checkout(frontiers) 后,文档会进入 detached 状态,此时无法继续编辑。详情参见 Attached/Detached 状态。若要继续编辑,需要调用 doc.attach() 回到最新版本。此设计是临时方案,未来会随着版本控制 API 的完善而调整。

只读时光旅行

下面演示如何实现一个简单的只读时光旅行功能,例如结合 UI 的滑块,让用户查看不同时间点的文档。

启用时间戳

在运行示例之前,需要为文档启用时间戳存储

doc.setRecordTimestamp(true);

这样每次变更都会带有时间戳,我们可以据此排序,符合用户直觉。

实现时光旅行

第一步,加载文档。假设你从数据库或 API 得到了一个快照:

// 从数据库 / API 获取文档快照
let snapshot = fetchSnapshot();
 
// 导入到新文档
const doc = new LoroDoc();
doc.import(snapshot);

接下来收集并排序文档中每次变更的时间戳,供滑块选择:

// 收集文档的全部变更
const changes = doc.getAllChanges();
 
// 获取所有变更的时间戳
const timestamps = Array.from(
  new Set(
    [...changes.values()]
      .flat() // 将不同 peer 的变更展平成一个列表
      .map((x) => x.timestamp) // 提取时间戳
      .filter((x) => !!x),
  ),
);
 
// 排序
timestamps.sort((a, b) => a - b);

然后实现一个辅助函数,用于根据时间戳生成对应的 Frontiers

对于每个参与编辑的 peer,都有一组按顺序排列的变更。每条变更包含 counterlengthcounter 是起始版本号,length 表示此次变更占用了多少计数值。

Frontiers 是我们希望 checkout 的各 peer 计数列表。为了实现时间轴,需要找到每个 peer 在目标时间之前的最大计数值。

const getFrontiersForTimestamp = (
  changes: Map<string, Change[]>,
  ts: number,
): { peer: string; counter: number }[] => {
  const frontiers = [] as { peer: string; counter: number }[];
 
  // 记录每个 peer 在目标时间之前的最高计数
  changes.forEach((changes, peer) => {
    let counter = -1;
    for (const change of changes) {
      if (change.timestamp <= ts) {
        counter = Math.max(counter, change.counter + change.length - 1);
      }
    }
    if (counter > -1) {
      frontiers.push({ counter, peer });
    }
  });
  return frontiers;
};

最后,根据滑块索引获取时间戳,计算 frontiers 并执行 checkout:

let sliderIdx = 3;
const timestamp = timestamps[sliderIdx - 1];
const frontiers = getFrontiersForTimestamp(changes, timestamp);
 
doc.checkout(frontiers);

可编辑的时光旅行

下方展示了结合节点编辑器的完整时光旅行示例。