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),仅供参考

相关新闻

caveman极简工作流:用命令行Vim与tmux替代IDE的实操指南

caveman极简工作流:用命令行Vim与tmux替代IDE的实操指南

去年我把自己的工作环境从一套两百多MB的IDE配置,迁到一台只有终端模拟器的轻量服务器上,发现一件很反直觉的事:活照样能干,而且干得更利索了。这个被我私下叫做caveman的方案,本质上就是把程序员工具箱里那个入口门槛…

2026/10/14 14:56:44 阅读更多 →
开发工具链实战:终端、浏览器与数据库的高效用法

开发工具链实战:终端、浏览器与数据库的高效用法

1. 从“一个终端走天下”说起:三类工具的定位与关系先聊个现象。很多开发者电脑里装的工具数量,可能比手机里的App还多。但真正常用的,翻来覆去就那么几个。终端、浏览器、数据库工具,这三样几乎是每个搞技术的人每天都躲不开的。…

2026/10/9 14:44:19 阅读更多 →
Emacs 中让 AI 真正干活的 agent-shell:从配置到工作流实战

Emacs 中让 AI 真正干活的 agent-shell:从配置到工作流实战

1. 为什么我在 Emacs 里给 AI 开了一个“shell”这两年 AI 编程工具铺天盖地,从 IDE 里的自动补全到独立 AI 编辑器,每个人都想给 AI 找个合适的“工位”。我试过各种 Copilot 系的补全插件,也折腾过 Cursor 这类全家桶,但总有一种…

2026/10/12 10:34:32 阅读更多 →

最新新闻

Docker优缺点深度剖析:从环境一致性到生产级实践避坑指南

Docker优缺点深度剖析:从环境一致性到生产级实践避坑指南

1. 从一次线上事故说起:为什么我又把Docker翻出来重新审视前阵子帮一个朋友排查他那个小团队的项目部署问题,四个人开发,测试环境跑得好好的,一上生产就各种“在我电脑上明明是好的”。这种场景你应该不陌生,我这些年听…

2026/10/14 15:13:01 阅读更多 →
CMAK 2.0.0.2 部署与实战:Kafka 可视化运维核心指南

CMAK 2.0.0.2 部署与实战:Kafka 可视化运维核心指南

简介:本资源为Kafka-Manager(CMAK)2.0.0.2官方预编译版,面向Kafka运维工程师、中间件开发人员及分布式系统学习者,专为简化多集群监控、主题与消费者组管理、在线配置调整等高频运维场景而设计。压缩包共780个文件&…

2026/10/14 15:13:01 阅读更多 →
驾驶员疲劳检测数据集实战:从XML转YOLO到模型训练与调优

驾驶员疲劳检测数据集实战:从XML转YOLO到模型训练与调优

简介:这份资源是面向计算机视觉学习者与智能驾驶方向研究者的YOLO驾驶员疲劳检测数据集,可用于训练和验证闭眼、打哈欠等疲劳行为的实时识别模型,适合具备一定目标检测基础、希望落地疲劳预警项目的读者。压缩包共8744个文件,约25…

2026/10/14 15:13:01 阅读更多 →
POSIX共享内存实战:高实时性IPC的零拷贝设计与避坑指南

POSIX共享内存实战:高实时性IPC的零拷贝设计与避坑指南

1. 共享内存到底是什么,为什么它在高性能场景里不可替代共享内存不是什么新概念,但很多人一听到就下意识觉得“这得是系统级开发才碰得到”,其实完全不是。我最早在做某图像处理Demo时,需要让两个独立进程——一个负责实时采集摄像…

2026/10/14 15:13:01 阅读更多 →
论文党收藏!2026AI写作辅助平台全攻略 含详细免费额度说明

论文党收藏!2026AI写作辅助平台全攻略 含详细免费额度说明

写期刊论文是不是总让你感觉头大?面对繁杂的文献资料、严格的格式要求,还有一遍又一遍的修改,很多学术朋友都遇到写作效率低下的问题。其实,利用AI论文写作工具可以帮你省不少力气。现在,无论是查找资料,还…

2026/10/14 15:13:01 阅读更多 →
M4A转MP3批量转换工具:FFmpeg参数调优与避坑指南

M4A转MP3批量转换工具:FFmpeg参数调优与避坑指南

简介:这是一款面向音频爱好者与日常办公用户的M4A转MP3格式转换工具,主要解决M4A文件在非Apple设备或较旧播放器上兼容性不足的问题,让音乐库能在更多终端顺畅播放。压缩包共2个文件,包含1个exe可执行程序与1个htm说明文档&#x…

2026/10/14 15:11:59 阅读更多 →

日新闻

AnyPS5通用适配层设计:兼容性与性能的工程取舍

AnyPS5通用适配层设计:兼容性与性能的工程取舍

1. 从"AnyPS5"这个名字说起:它到底想解决什么问题第一次看到"AnyPS5"这个标题,我脑子里冒出来的第一个念头是:这大概率是一个围绕"跨平台运行"或者"通用适配"做文章的项目。名字里的"Any"…

2026/10/14 0:00:41 阅读更多 →
Flutter跨平台开发实战:二手交易应用与HarmonyOS适配

Flutter跨平台开发实战:二手交易应用与HarmonyOS适配

1. 项目概述:Flutter跨平台二手交易应用开发作为一名长期从事移动应用开发的工程师,我最近完成了一个基于Flutter框架的二手物品交易平台项目。这个项目最吸引我的地方在于它完美结合了Flutter的跨平台优势与HarmonyOS的生态特性,实现了"…

2026/10/14 0:00:41 阅读更多 →
DFS算法解决飞机降落调度问题实战

DFS算法解决飞机降落调度问题实战

1. 项目背景与核心思路飞机降落问题是一个经典的调度优化场景,也是算法竞赛和面试中的高频题目。这个题目要求我们合理安排多架飞机的降落顺序和时间,确保所有飞机都能安全着陆而不发生冲突。在实际航空管制中,这类问题直接关系到飞行安全和机…

2026/10/14 0:00:41 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/14 3:30:28 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/13 16:22:04 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/14 3:29:58 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/13 22:01:27 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/13 14:57:39 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/14 7:53:34 阅读更多 →