#document #crdt #data-structures #key-value #applications #structure #sync

automerge

一种类似于JSON的数据结构(CRDT),可以被不同用户并发修改,并能自动合并

17个版本

0.5.11 2024年5月7日
0.5.9 2024年3月23日
0.5.6 2023年12月26日
0.5.3 2023年11月24日
0.0.2 2019年12月28日

异步类目下排名第79

Download history 952/week @ 2024-05-02 646/week @ 2024-05-09 791/week @ 2024-05-16 597/week @ 2024-05-23 767/week @ 2024-05-30 569/week @ 2024-06-06 530/week @ 2024-06-13 775/week @ 2024-06-20 614/week @ 2024-06-27 564/week @ 2024-07-04 972/week @ 2024-07-11 858/week @ 2024-07-18 699/week @ 2024-07-25 932/week @ 2024-08-01 814/week @ 2024-08-08 576/week @ 2024-08-15

每月下载量3,209
14个Crate使用(11个直接使用)

采用MIT许可证

1MB
31K SLoC

Automerge

Automerge是一个用于构建协作本地优先应用的库。这是Rust的实现。请参阅automerge.org


lib.rs:

Automerge

Automerge是一个用于构建协作、本地优先应用的库。Automerge的理念是提供一个相当通用——由嵌套的键/值映射和/或列表组成——的数据结构,该数据结构可以在本地完全修改,但可以随时与其他相同数据结构的实例合并。

除了核心数据结构(我们通常称之为“文档”)之外,我们还提供了一个同步协议的实现(在crate::sync中),该协议可以通过任何可靠的有序传输使用;以及一个高效的二进制存储格式。

此库围绕两种文档表示形式组织 - AutomergeAutoCommit。这两者的区别在于 AutoCommit 会为您管理事务。这两种表示形式都实现了 ReadDoc 用于从文档中读取值,以及 sync::SyncDoc 用于参与同步协议。 AutoCommit 直接实现了 transaction::Transactable 以修改文档,而 Automerge 则需要您显式创建一个 transaction::Transaction

注意:该库提供的数据修改API非常底层(某种程度上类似于直接创建JSON值而不是使用 serde derive宏或等效功能)。如果您正在编写使用Automerge的Rust应用程序,可能需要查看 autosurgeon

数据模型

Automerge文档是一个从字符串到值的映射(Value),其中值可以是以下之一:

  • 一个嵌套的复合值,它可以是以下之一:
  • 一个原始值(ScalarValue),它可以是以下之一:
    • 一个字符串
    • 一个64位浮点数
    • 一个有符号64位整数
    • 一个无符号64位整数
    • 一个布尔值
    • 一个计数器对象(一个通过加法合并的64位整数)(ScalarValue::Counter
    • 一个时间戳(自Unix纪元以来的毫秒数)(Timestamp

所有复合值都有一个ID(ObjId),它在将值插入文档时创建,或者是最初对象ID ROOT。文档中的值通过(代码object IDkey)对引用。其中 keyProp 类型表示,对于映射来说是一个字符串,对于序列来说是一个索引。

冲突

有些东西自动合并无法合理合并。例如,两个操作者同时将“名称”键设置为不同的值。在这种情况下,自动合并将以随机但确定性的方式选择获胜值,但冲突的值仍然可以通过 [ReadDoc::get_all()] 方法获取。

更改哈希和历史值

与git类似,文档历史中的点由哈希标识。与git不同,可以存在多个哈希表示特定点(因为自动合并支持并发更改)。这些哈希可以通过 [Automerge::get_heads()] 或 [AutoCommit::get_heads()] 获取(注意这些方法不是ReadDoc的一部分,因为在AutoCommit的情况下,它需要一个对文档的可变引用)。

这些哈希可以用于使用ReadDoc上的各种 *_at() 方法从特定历史点读取文档中的值,这些方法将ChangeHash的切片作为参数。

操作者ID

对自动合并文档的任何更改都是由操作者进行的,操作者由ActorId表示。操作者ID是任何随机字节序列,但同一操作者ID的每个更改必须是顺序的。这通常意味着您将想要为每个设备维护至少一个操作者ID。为每个更改生成新的操作者ID是可以的,但请注意,每个操作者ID都会占用文档中的空间,因此如果您预计文档将长期存在并且/或有很多更改,那么您应该尽可能重用操作者ID。

文本编码

默认情况下使用UTF-8进行编码,但使用wasm目标时使用UTF-16。

同步协议

请参阅sync模块。

补丁,维护实体状态

通常您将有一些表示文档“当前”状态的实体。例如,UI中的一些文本是文档中文本对象的视图。而不是在每次更改发生时重新渲染此文本,您可以使用PatchLog捕获对文档所做的增量更改,然后使用 [Automerge::make_patches()] 获取要应用于实体状态的一组补丁。

许多《Automerge》库中的方法,如 Automergecrate::sync::SyncDoccrate::transaction::Transactable,都有一个 *_log_patches() 变体,它允许你传入一个 PatchLog 来收集这些增量更改。

Serde 序列化

有时你只是想要获取 Automerge 文档的 JSON 值。为此,你可以使用 AutoSerde,它为 Automerge 文档实现了 serde::Serialize

示例

让我们创建一个表示通讯录的文档。

use automerge::{ObjType, AutoCommit, transaction::Transactable, ReadDoc};

let mut doc = AutoCommit::new();

// `put_object` creates a nested object in the root key/value map and
// returns the ID of the new object, in this case a list.
let contacts = doc.put_object(automerge::ROOT, "contacts", ObjType::List)?;

// Now we can insert objects into the list
let alice = doc.insert_object(&contacts, 0, ObjType::Map)?;

// Finally we can set keys in the "alice" map
doc.put(&alice, "name", "Alice")?;
doc.put(&alice, "email", "[email protected]")?;

// Create another contact
let bob = doc.insert_object(&contacts, 1, ObjType::Map)?;
doc.put(&bob, "name", "Bob")?;
doc.put(&bob, "email", "[email protected]")?;

// Now we save the address book, we can put this in a file
let data: Vec<u8> = doc.save();

现在在两个不同的设备上修改这个文档,并合并修改。

use std::borrow::Cow;
use automerge::{ObjType, AutoCommit, transaction::Transactable, ReadDoc};


// Load the document on the first device and change alices email
let mut doc1 = AutoCommit::load(&saved)?;
let contacts = match doc1.get(automerge::ROOT, "contacts")? {
    Some((automerge::Value::Object(ObjType::List), contacts)) => contacts,
    _ => panic!("contacts should be a list"),
};
let alice = match doc1.get(&contacts, 0)? {
   Some((automerge::Value::Object(ObjType::Map), alice)) => alice,
   _ => panic!("alice should be a map"),
};
doc1.put(&alice, "email", "[email protected]")?;


// Load the document on the second device and change bobs name
let mut doc2 = AutoCommit::load(&saved)?;
let contacts = match doc2.get(automerge::ROOT, "contacts")? {
   Some((automerge::Value::Object(ObjType::List), contacts)) => contacts,
   _ => panic!("contacts should be a list"),
};
let bob = match doc2.get(&contacts, 1)? {
  Some((automerge::Value::Object(ObjType::Map), bob)) => bob,
  _ => panic!("bob should be a map"),
};
doc2.put(&bob, "name", "Robert")?;

// Finally, we can merge the changes from the two devices
doc1.merge(&mut doc2)?;
let bobsname: Option<automerge::Value> = doc1.get(&bob, "name")?.map(|(v, _)| v);
assert_eq!(bobsname, Some(automerge::Value::Scalar(Cow::Owned("Robert".into()))));

let alices_email: Option<automerge::Value> = doc1.get(&alice, "email")?.map(|(v, _)| v);
assert_eq!(alices_email, Some(automerge::Value::Scalar(Cow::Owned("[email protected]".into()))));

游标,指向序列中的位置

当处理文本或其他序列时,在合并远程更改时能够引用序列中的特定位置通常很有用。你可以通过维护自己的偏移量并观察补丁来手动完成此操作,但这很容易出错。Cursor 类型提供了一种 API,允许 Automerge 为你进行索引转换。游标通过 [ReadDoc::get_cursor()] 创建,并通过 [ReadDoc::get_cursor_position()] 解引用。

依赖项

~3.5–7MB
~139K SLoC