资讯详情

资讯详情

Automerge-Wasm Patch 机制完全指南:路径定位、动作类型与增量同步实战

后端【免费下载链接】automergeA JSON-like data structure (a CRDT) that can be modified concurrently by different users, and merged again automatically.项目地址https://gitcode.com/gh_mirrors/au/automerge点击查看免费下载Automerge 是一个基于 CRDT无冲突复制数据类型的 JSON-like 数据结构允许多个用户并发修改同一份文档并自动合并。当文档发生变化时Automerge 并不要求你重新物化整棵文档树而是以Patch补丁的形式输出一组最小化的增量变更描述。本文以仓库中的 PATCH.md 为骨架结合 automerge-wasm 与 automerge 的源码实现完整讲解 Patch 的路径Path、值Value与全部动作类型put / insert / del / inc / splice / conflict / mark并演示如何通过diffIncremental、diff、applyPatches等 API 在 JavaScript / TypeScript 中消费这些补丁实现 UI 增量更新与本地合并状态重建。读完本文你将掌握Patch 的精确 JSON 结构与每类动作的字段语义、字段缺省时的默认行为、底层 RustPatchAction与 JS 对象的映射关系以及一套可直接复用的增量同步实战代码模式。Patch 从哪来增量差异的产生机制在深入动作类型之前先明确 Patch 在 Automerge 中的来源。从 lib.rs 的 WASM 绑定可以看到Patch 主要通过三条路径产生diffIncremental()自上次调用以来的增量补丁。WASM 层调用self.doc.diff_incremental()取得 Rust 侧的Vecautomerge::Patch再经由interop::export_patches转成 JS 数组。这通常配合updateDiffCursor()将当前 diff 游标推进到最新 headslib.rs使用。diff(before, after)给定两组 heads计算两者之间的全部差异是离线对比式的补丁生成方式。applyAndReturnPatches(obj, meta)/applyPatches(obj, meta)把补丁直接应用到一个普通的 JS 对象上并可选返回本次应用的补丁列表是增量物化的核心入口lib.rs。底层类型定义位于 lib.rs 的 TypeScript 自定义片段中统一以Patch联合类型收口export type Patch | PutPatch | DelPatch | SpliceTextPatch | IncPatch | InsertPatch | MarkPatch | UnmarkPatch | ConflictPatch;Path定位目标的属性链所有 Patch 都携带一个path。这个路径描述了从文档根节点出发、需要依次遍历的一组属性才能到达补丁真正作用的目标位置。path的每个元素类型为Prop即string | number字符串表示 map对象中的键数字表示 list数组或 text文本中的索引。例如假设文档当前结构为{ animals: [ { name: Lion, genus: Panthera, }, { name: Chimpanzee, genus: Panthera, } ] }若要把 Chimpanzee 的genus改为Pan对应的补丁路径就是[animals, 1, genus]——先进入animals列表取索引1的对象再读写其genus键{ action: put, path: [ animals, 1, genus ], value: Pan, }这一语义在 Rust 底层也有对应automerge::Patch结构体中的path: Vec(ObjId, Prop)记录了到达目标对象所经过的每一层(对象 ID, 属性)对见 patch.rs而 WASM 导出层在 interop.rs 中只取Prop部分、去掉对象 ID 后转换为string | number混合数组从而在 JS 侧保持纯数据形态、便于直接驱动状态管理。PatchValue补丁可携带的值类型Patch 中的value/values字段取值类型为type PatchValue string | number | boolean | null | Date | Uint8Array | {} | []这些值分别对应 Automerge 中的各类标量类型字符串、整数、浮点数、布尔、null、时间戳、字节数组外加一个空对象{}表示 map 对象占位和空数组[]表示 list 对象占位。需要注意它与ScalarValue的区别。在 lib.rs 中export type ScalarValue string | number | boolean | null | Date | Uint8Array;PatchValue在ScalarValue的基础上额外增加了{}与[]。这两者用于**对象暴露object exposure**场景当你在 map 或 list 中新建了一个子对象时补丁先以空对象/空数组占位该位置随后紧跟一批针对该子对象内部属性的补丁。这一点在 diff.mts 的测试中有直接体现{ action: put, path: [ map, foo ], value: {} }, { action: put, path: [ map, foo, from ], value: doc1 }, { action: put, path: [ map, foo, other ], value: 1 }第一条补丁把foo暴露为{}一个空的 map后续补丁再逐键填充。put写入单个键或索引put用于改变 map 中的单个键或 list 中的单个索引。它不会改变 list 的长度是原地更新语义。type PutPatch { action: put path: Prop[], value: PatchValue, conflict?: boolean } let patch : PutPatch { action: put, path: [ config, enabled ], value: true, conflict: true, // optional }conflict 字段的含义conflict字段表明该位置同时存在另一个竞争值concurrent value而当前补丁中的value是合并后的获胜值winning value。如果字段缺失默认视为false。在 Rust 底层PutMap/PutSeq均携带conflict: bool见 patch.rs其文档注释明确说明冲突的其余候选值可通过ReadDoc::get_all查询得到。WASM 导出层在 interop.rs 中也遵循仅在冲突为真时才输出conflict: true的规则——这正是字段缺失即默认为 false的底层实现。diff.mts 给出了一个完整的冲突补丁样例两个副本对同一 map 键写入不同对象后 merge生成的增量补丁中会携带conflict: true{ action: put, path: [ map, foo ], conflict: true, value: {} }, { action: put, path: [ map, foo, from ], value: doc2 }, { action: put, path: [ map, foo, something ], value: 2 }insert向 list 插入一个或多个值insert用于向 list 中插入一个或多个值插入后 list 的长度会相应增加。interface MarkSet { [name : string]: Value; } type InsertPatch { action: insert path: Prop[], values: PatchValue[], marks?: MarkSet, conflicts?: boolean[] } let patch : InsertPatch { action: insert, path: [ emoji, 3 ], values: [ , , ], conflicts: [ false, true, false ] // optional marks: { size: 24 }, }values被插入的值数组按其在序列中的顺序排列conflicts与values一一对应的布尔数组标记每个插入值是否处于冲突状态缺失时默认全部为falsemarks插入的文本片段所携带的标记仅对 text 场景有意义没有标记时该字段会缺失。底层导出逻辑与这一默认约定完全一致interop.rs 中Insert动作会逐个检查插入值的冲突标志仅当存在任一冲突为true时才输出conflicts数组。del删除键或序列元素del用于删除 map 中的键或删除 list / text 对象中的一个或多个连续元素。type DelPatch { action: del path: Prop[], length?: number, } let patch : DelPatch { action: del, path: [ items, 3 ], length: 10, // optional }length仅出现在**序列list / text上且仅当存在一段连续删除run of consecutive deletes**时携带表示从path最后一级索引起连续删除的元素个数。缺失时默认视为删除单个元素length 1。对 map 的删除DeleteMap永远不携带lengthRust 侧 patch.rs 区分了DeleteMap { key }与DeleteSeq { index, length }两个动作而 WASM 导出层在 interop.rs 中只对length 1的序列删除输出length字段。一个 text 中连续删除的实测输出见 diff.mts{ action: splice, path: [text, 10], value: cow }, { action: del, path: [text, 13], length: 3 },inc递增计数器inc用于将目标位置的值Counter 或普通数字按value递增value可以是负数递减。type IncPatch { action: inc path: Prop[], value: number } let patch : IncPatch { action: inc, path: [ config, logins ], value: 1 }Rust 底层Increment { prop: Prop, value: i64 }见 patch.rs明确注释the amount incremented, may be negative即递增量可以为负。导出时value被转为浮点数输出interop.rs。splice向 text 对象拼接字符串splice用于向 text 对象拼接一段字符串。注意向普通 list 插入多个元素用的是insert而向 text 文本对象插入字符用的是splice。type SpliceTextPatch { action: splice path: Prop[], value: string, marks?: MarkSet, } let patch : SpliceTextPatch { action: splice, path: [ text, 123 ], value: and so it was done marks: { bold: true }, }value插入的字符串本身marks该文本片段上当前生效的标记集合没有标记时该字段缺失。底层实现同样遵循仅在存在标记时输出的原则interop.rs 中只有marks存在且num_marks() 0时才构建marks对象。Rust 侧对应动作SpliceText { index, value: ConcreteTextValue, marks: OptionMarkSet }patch.rs。补充说明PATCH.md 中该示例的action字段笔误为inc正确值应为splice本文已按实际语义与类型定义修正。conflict冲突出现但值未变化conflict补丁用于标记某个字段进入了冲突状态但其值本身没有发生变化。它不携带值只携带路径type ConflictPatch { action: conflict path: Prop[], } let patch : ConflictPatch { action: conflict, path: [ keys, 11 ] }在 Rust 底层Conflict { prop: Prop }的文档注释为A new conflict has appearedpatch.rs即冲突是新出现的事件。它与put携带的conflict: true的区别在于put conflict: true表示值发生了写入且该位置有冲突候选而conflict动作表示字段已经存在冲突、但这次差异中没有值变化仅通知上层此处产生了并发冲突。这类补丁对需要高亮冲突字段的编辑器 UI 尤为重要。mark文本标记的添加与移除mark补丁表示一个或多个标记marks被添加到文档的文本对象上。每个标记包含名称、值、起始与结束位置当value为null时表示该标记被移除。type Mark { name: string, value: Value, start: number, end: number, } type MarkPatch { action: mark path: Prop[], marks: Mark[] } let patch : MarkPatch { action: mark, path: [ keys, 11 ] marks: [ { name: font-weight, value: bold, start: 0, end: 5 }, { name: color, value: blue, start: 8, end: 11 }, ] }name标记名称如bold、color、font-weightvalue标记值null表示该标记在此区间被删除start/end标记覆盖的文本区间字符索引end为开区间边界path指向包含这些标记的 text 对象本身而非具体字符位置。在导出实现 interop.rs 中Mark { marks: VecMark }被展开为marks数组每个元素包含name、value、start、end四个字段与上述类型完全对应。WASM 层还额外提供了独立的unmark动作UnmarkPatch { name, start, end }见 lib.rs用于显式表达标记移除二者语义互补。diff.mts 中的实测样例同时覆盖了添加与移除两种形态// 添加标记 { action: mark, path: [text], marks: [ { start: 3, end: 6, name: bold, value: true } ] }, // 移除标记value 为 null { action: mark, path: [text], marks: [ { start: 3, end: 6, name: bold, value: null } ] },消费补丁从增量 Patch 到 UI 状态掌握了动作类型之后接下来是在应用中真正消费这些 Patch。核心 API 定义在 lib.rs 的接口声明中applyPatchesDoc(obj: Doc, meta?: unknown): Doc; applyAndReturnPatchesDoc(obj: Doc, meta?: unknown): { value: Doc; patches: Patch[] };两者都会把自 diff 游标以来的增量补丁逐一应用到传入的普通 JS 对象上并返回新的对象区别在于applyAndReturnPatches额外返回本次应用的patches数组方便驱动每个补丁对应一次 UI 更新的渲染模型。典型用法一增量物化 应用补丁// 初始同步把完整文档物化到本地状态 let state doc.materialize(/); // 后续文档更新后 const { value, patches } doc.applyAndReturnPatches(state); state value; // 此时可以用 patches 逐条驱动 UI 更新例如 for (const p of patches) { // p.action 为 put / insert / del / inc / splice / mark / conflict console.log(p.action, p.path, p.value ?? p.values ?? p.length); } doc.updateDiffCursor(); // 推进 diff 游标避免下次重复输出这正是 apply.mts 所验证的工作流先applyPatches({})从空对象开始重建状态之后每次本地变更再applyPatches(base)增量更新并断言结果与materialize(/)完全一致。典型用法二heads 之间的显式对比当需要对比两个历史快照时使用diff(before, after)它返回两个 heads 之间的完整补丁列表diff.mtsconst heads1 doc.getHeads(); doc.put(/, key1, value2); const heads2 doc.getHeads(); const patches doc.diff(heads1, heads2); // [{ action: put, path: [key1], value: value2 }]增量补丁的字段规约应用方约定综合类型定义与导出实现interop.rs消费补丁时应遵循以下规约否则容易踩坑字段缺省行为出现条件conflictput视为false仅存在竞争值时输出trueconflictsinsert视为全false仅当任一插入值冲突时输出lengthdel视为1仅序列连续删除且length 1时输出marksinsert / splice视为无标记仅当标记集合非空时输出valuemark—null表示标记移除与 Rust 底层PatchAction的对应关系最后把 JS 侧的 8 种动作与 Rust 核心库的PatchAction枚举patch.rs做一张对照表便于深入源码时快速定位JS 动作RustPatchAction变体适用对象putPutMap/PutSeqmap 键 / list 索引insertInsertlistdelDeleteMap/DeleteSeqmap 键 / list、text 区间incIncrementCounter / 数字spliceSpliceTexttextmarkMark含移除语义text 标记unmarkWASM 层补充动作text 标记显式移除conflictConflict任意冲突字段映射逻辑集中在 interop.rs 的export_patch函数中它读取 Rust 侧Patch的path与action将Prop::Map(key)导出为字符串、Prop::Seq(index)导出为数字再按上表为每个动作组装出对应的 JS 对象。理解这条映射链你在排查为什么生成的补丁长这样时就能直接定位到对应源码。小结Automerge-Wasm 的 Patch 机制是一套设计紧凑的增量变更协议统一的path负责定位PatchValue负责承载标量与对象占位put / insert / del / inc / splice / conflict / mark七类核心动作外加 WASM 层补充的unmark覆盖了 map、list、text、Counter 与富文本标记的全部变更形态。配合diffIncremental/diff/applyPatches/applyAndReturnPatches等 API你可以在不重新物化整棵文档树的前提下把 CRDT 合并结果高效地同步到任意前端状态管理体系中——这正是 Automerge 支撑实时协作编辑的基石能力。赞分享后端【免费下载链接】automergeA JSON-like data structure (a CRDT) that can be modified concurrently by different users, and merged again automatically.项目地址https://gitcode.com/gh_mirrors/au/automerge点击查看免费下载相关推荐Tess-4-27B-OptiQ-4bit实战案例构建智能代理应用的10个示例Tess 4 27B OptiQ 4bit实战案例构建智能代理应用的10个示例 Tess 4 27B OptiQ 4bit是一款基于Qwen3.6 27B构建大模型多模态模型量化本地部署Detox 同步问题排查指南自动同步机制、调试定位与手动回退实战Detox 同步问题排查指南自动同步机制、调试定位与手动回退实战 端到端测试中最棘手的问题之一是让测试脚本与应用保持同步应用内部的复杂操作访问服务器、播测试移动开发质量保障开发工具VitePress 路由机制完全指南文件路由、动态路由与路径重写实战VitePress 路由机制完全指南文件路由、动态路由与路径重写实战 本篇技术指南系统讲解 VitePressVite Vue 驱动的静态站点生成器的前端文档上一篇如何快速配置碧蓝航线智能助手面向新手的完整教程下一篇AzurLaneAutoScript碧蓝航线自动化脚本的架构解析与实战应用指南创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →