「这是我参与2022首次更文挑战的第 17 天,活动详情查看:2022首次更文挑战」。
删除
在Yjs中,删除的处理方式与插入的处理方式非常不同。插入是作为一个基于顺序操作的CRDT实现的,但删除是作为一个简单的基于状态的CRDT处理的。
当一个item在历史上的任何时刻被任意对端删除时,该item就会被标记为删除。(Yjs内部使用info bitfield)Yjs不会记录关于删除的元数据:
- 当一个item被删除时,或者哪个用户删除了它,不会保留任何数据。
- struct存储不包含删除记录。
- clientID 的
clock
没有增加
如果在Yjs中启用了GC,当一个对象被删除时,它的内容也就会被丢弃。如果一个被删除的对象包含子对象(例如一个字段在一个对象中被删除),内容将被替换为一个GC对象(src/structs/GC.js)。这是一个非常轻量级的结构 —— 因为它只存储删除内容的长度。
Yjs有一些特殊的逻辑来共享文档中哪些内容被删除:
- 当发生删除时,除了标记项目外,删除的id会在事务中本地列出。(关于事务的更多信息请参见下文。)在本地事务提交时,将删除的项目集附加到事务的更新消息中。
- 快照(在Yjs发生历史中标记的时间点)是使用
<clientID, clock>
和所有删除项目id集合指定的。被删除的数据集是O(n),但因为删除通常发生在运行中,所以这个数据集在实践中通常很小。(来自B4基准文档的真实编辑跟踪:其中包含182k个插入和77k个删除字符。删除的快照设置大小仅为4.5Kb)。
事务
Yjs中的所有更新都发生在一个事务中。(src/utils/Transaction.js)。
事务收集一组对Yjs文档的更新,这些更新将原子地应用到远程对端上。一旦事务在本地提交,它会生成一个压缩的更新消息,这个消息会广播给同步的远端,以通知他们本地的更改。更新消息包含:
- 新插入的item集合
- 事务中删除的item集合
网络协议
网络协议并不是Yjs的一部分。不过有几个相关的概念可以用来创建一个自定义的网络协议:
- update:可以将Yjs文档编码为一个 update object,该对象可以被解析来重组文档。此外,文档上的每一个更改都会触发一个增量文档更新,从而允许客户端彼此同步。更新对象是一个
Uint8Array
,它可以有效地编码Item objects
和delete set
。 - state vector:状态集定义每个用户(一组元组
(client, clock)
)的已知状态。这个对象也被有效地编码为Uint8Array
。
分为两个步骤:
- 客户端可以通过发送他们的状态集(sync步骤1)来向远程客户端询问缺失的文件更新。
- 远程对端可以使用各自客户端的
clock
来计算缺失的Item对象,并计算出反映所有缺失更新的最小更新消息(sync步骤2)。
同步过程的实现:github.com/yjs/y-proto…
快照
快照可以用来恢复旧的文档状态。它是一个状态集+删除集。客户端可以通过遍历序列CRDT并忽略所有 id.clock>stateVector[id.client].clock
的item恢复一个旧的文档状态。客户端不再使用 item.deleted
,而是使用 delete set
来确定一个条目是否被删除。
但是我们不建议使用快照恢复旧文档状态,尽管这肯定是可行的。相反,应该通过迭代最新的状态并使用状态集合的额外信息来计算旧的状态。