「这是我参与2022首次更文挑战的第 27 天,活动详情查看:2022首次更文挑战」。
本文旨在解释 automerge 内部数据结构的存储细节。你不应该只是为了在你的应用程序中使用 automerge 而需要阅读它,但是如果你想深入 automerge 代码本身,你会发现它很有用。
Document/changes/operations
你可以通过调用 Automerge.init()
(创建一个新的空文档)或 Automerge.load()
(加载一个现有的文档,通常是从磁盘上的一个文件)来获得一个Automerge实例。
默认情况下,这个文件只存在于单个设备(当前client)的内存中,你不需要任何网络通信来进行读写访问。也许会有一个单独的网络层,异步地将变化从一个设备传播到另一个设备,但该网络层不在automerge 本身的讨论范围内。
automerge 实例代表你的应用程序的当前状态(或它的某些部分)。该状态是不可变的,永远不会在原地更新。相反,每当你想做一些改变状态的操作时,你需要调用函数,该函数将旧状态作为第一个参数,并返回一个反映变化的新状态。有两种方式可以改变状态:
一个是个本地修改文档状态,一个是远程修改文档状态
- Local change → 一般是由用户改变用户界面中的某些应用数据而触发的。用户的这种编辑是通过调用
Automerge.change()
,它把应该作为一个原子单元应用的操作块组合起来。在变更回调中,你可以访问Automerge文档的可变版本,以 Proxy 的形式实现。该代理记录了你所做的任何突变操作(例如,改变一个特定对象的特定属性的值)。change()
返回一个应用了这些操作的新状态的副本(相对之前的来说是副本)。 - Remote change → 另一个设备上的用户编辑了他们的文档副本,该更改通过网络发送给你,现在你想把它应用到你自己的文档副本中。远程操作使用
Automerge.applyChanges()
来应用,它返回一个新的状态副本。出于测试的目的,还有Automerge.merge()
,这是一个捷径,用于所谓 "远程"文档 实际上只是同一进程中Automerge的另一个实例的情况。
解释一下文章会用的术语:
- operation → 对单个修改的细粒度描述。例如,设置一个特定对象的特定属性的值,或在一个列表中插入一个元素。用户通常不会看到 operation,因为它们是比较底层的实现细节。
- change → operations 的集合,它被分组为一个单元,被原子化地应用(有点像数据库事务)。对
Automerge.change()
的每次调用都会产生一个 change,而在这个 change 中可能有任何数量的操作。一个 change 也是通过网络传输给其他设备的最小单位。 - document → 单个Automerge实例的状态。一个文件的状态是由应用于它的所有变更的集合决定的。Automerge确保,只要任何两个文档看到了相同的更改集,即使这些更改是以不同的顺序应用的,那么这些文档就处于相同的状态。这意味着Automerge文档是一个CRDT。
Automerge.getChanges()
返回在一个文档状态和另一个状态之间发生的所有变化,这样它们就可以被编码并通过网络发送给其他设备。在接收方的一端,Automerge.applyChanges()更新相应的文档以纳入这些变化。
你可以使用 Automerge.save()
将 document 保存到磁盘。这个函数实际上只是把文档中曾经发生过的所有变化,编码为一个字符串。相反,Automerge.load()
会对这个字符串进行解码,并将所有的变化应用到一个新的空白文档。这样做是因为我们总是可以从变化的集合中重建文档的状态。出于这个原因,一个文档保留了它的整个编辑历史,甚至跨越了保存和重建(有点像Git仓库)。
有一天,我们可能需要丢弃这段历史,以节省磁盘空间。存储整个历史记录还涉及到隐私问题:任何一个新上线的协同者在访问一个文档时都可以看到该文档过去的所有状态,包括现在被删除的任何内容。然而,现在我们选择保留所有的历史记录,因为它使同步更容易(想象一下,一个设备已经离线了很长时间,然后需要赶上其他用户在离线时改变的一切)。此外,能够检查编辑历史本身就是一个有用的功能。