- 后端
【免费下载链接】automerge
A JSON-like data structure (a CRDT) that can be modified concurrently by different users, and merged again automatically.
导读
本文基于 Automerge JavaScript 实现(@automerge/automerge)的 3.5.0 版本变更记录(javascript/CHANGELOG.md),系统解读该版本相对 3.4.1 的全部变化:新增的 change 作者元数据能力(getAuthor/getAuthors)、对__proto__键赋值的RangeError安全加固,以及针对大列表插入、富文本块补丁、性能回归等多处关键修复。通过对照 javascript/src 与 rust/automerge-wasm/src/lib.rs 的源码实现和 javascript/test 中的测试用例,读者可以理解每一项变更的底层原理、正确用法与升级注意事项。
版本背景:3.5.0 相对 3.4.1 的变更范围
3.5.0 是 Automerge JavaScript 包的一个功能与修复并重的版本。变更记录以三项分类组织:
- Added:新增 change 作者(author)元数据写入与查询能力;
- Changed:
__proto__键赋值行为收紧,改为抛出RangeError; - Fixed:一批与补丁应用(patch application)、大变更性能、富文本与片段 API 相关的修复。
下文逐一展开,并在每个条目下补充对应的源码与测试证据。
新增:为 change 附加作者元数据
核心能力
3.5.0 起,可以在创建文档或产生变更时附加一个"作者"标识,它被记录在 change 的元数据中。从 Automerge 的角度看,作者是一个不透明(opaque)的十六进制字符串——库本身不解释其语义,你可以用它承载用户 ID、会话 ID 或任意自定义标识。
对应的 JS 层 API 定义在 javascript/src/implementation.ts:
getAuthor<T>(doc: Doc<T>): Author | null:返回文档当前头部 change 的作者,未设置时为null;getAuthors<T>(doc: Doc<T>): Author[]:返回文档变更历史中出现过的全部作者;getAuthorForActor<T>(doc: Doc<T>, actor: ActorId): Author | null:按 actor 查询其关联作者;getActorsForAuthor<T>(doc: Doc<T>, author: Author): Actor[]:反向查询某个作者对应的所有 actor。
作者信息在变更历史(change history)中持久记录,因此merge之后依然可以追溯到各分支的作者来源。
WASM 绑定层实现
这些 JS API 最终转发到 WASM 绑定层,实现在 rust/automerge-wasm/src/lib.rs:
getAuthor:Some(self.doc.get_author()?.to_string()),作者未设置时返回null;getAuthors:遍历doc.get_authors()收集为数组;getAuthorForActor:先将十六进制字符串解码为ActorId,再查询映射;getActorsForAuthor:先通过Author::try_from校验格式,非法时返回BadAuthor错误;- 配套的
setAuthor(author)用于写入作者元数据。
值得注意的是,getAuthorForActor/getActorsForAuthor在 JS 层的implementation.ts中均有对应封装(见 javascript/src/implementation.ts),说明作者与 actor 之间存在可逆的双向映射,可用于在多人协作中把"物理 actor"与"业务作者身份"关联起来。
测试用例验证
作者 API 的完整行为在 javascript/test/basic_test.ts 中被覆盖,可作为最佳实践参考:
- 未设置作者时
getAuthor返回null; from({...}, { author: "aabbcc" })与init({ author: "ff00ff" })均可在初始化时指定作者;clone(doc, { author: "ffaa00" })支持在克隆时更换作者;- 两个带不同作者的分支
merge后,getAuthors返回两个作者的有序集合(如["aabbcc", "ffaa00"]); getAuthorForActor(doc, actor)返回当前 actor 对应的作者,getActorsForAuthor(doc, author)返回该作者的全部 actor。
变更:__proto__键赋值现在抛出 RangeError
变更内容
3.5.0 起,向 Automerge 文档(或其嵌套对象)赋值为__proto__键会抛出RangeError,消息为The key "__proto__" is not allowed in Automerge documents。该保护不仅作用于change回调中的赋值,也覆盖:
- 传给
from的初始状态对象; - 在
change中赋值的嵌套对象(批量插入路径)。
这一收紧是为了防止 JavaScript 原型链污染:__proto__是对象原型访问器,若被当作普通键写入文档,可能意外修改原型或造成赋值语义混乱。
源码实现
保护逻辑分两层:
- 代理层赋值拦截:文档对象的赋值经由 Proxy 拦截(见 javascript/src/proxies.ts),当键为
__proto__时直接抛出RangeError; - 批量插入校验:
validateForBatchInsert(见 javascript/src/proxies.ts)在递归校验批量插入值时,对每个键名检查k === "__proto__"。
关于"批量校验只遍历自身可枚举属性(own enumerable properties)"这一细节:validateForBatchInsert使用Object.keys(value)遍历对象键,Object.keys只返回自身可枚举属性、不包含继承属性,因此继承链上的属性不会被误检或误写入,避免了校验逻辑自身触发原型链访问。
测试覆盖
javascript/test/proxies.ts 中专门设有__proto__ handling测试组,覆盖四种场景:
- 在 change 回调中给
d["__proto__"]赋标量值 → 抛RangeError; - 赋对象值
{ x: 1 }→ 抛错且不污染原型; - 初始状态
from({ ["__proto__"]: false })→ 抛错; - 嵌套对象
d["nested"] = { ["__proto__"]: { x: 1 } }→ 抛错。
修复:补丁应用与性能问题
大列表插入不再触发 "Maximum call stack size exceeded"
此前,应用合并后的补丁(consolidated patches)时,若一次性向列表插入大量元素,会把每个元素逐一传给单个splice调用,导致递归或参数展开过深而触发 V8 的调用栈上限错误。3.5.0 的修复思路是:WASM 绑定层按有界分块(bounded chunks)应用插入,而不是把全部元素塞进一次splice。
JS 侧的补丁应用入口见 javascript/src/apply_patches.ts:applyInsertPatch对数组目标使用原生parent.splice(prop, 0, ...patch.values),而对文本目标则在 Automerge 文档内逐块调用splitBlock;分块策略确保单次调用规模受限,从而避免调用栈溢出。
applyPatch/applyPatches支持富文本块
文本中的嵌入块(embedded rich-text blocks)此前在补丁应用时无法正确处理,3.5.0 起:
- 应用于普通 JavaScript 字符串时:块以对象替换字符(U+FFFC,
\ufffc)表示,对块内容的更新被忽略。见 javascript/src/apply_patches.ts,其中用"\ufffc".repeat(patch.values.length)填充插入位置; - 应用于 Automerge
change回调内时:块数据被完整保留。applyPatch首先通过resolveEmbeddedBlock(见 javascript/src/apply_patches.ts)解析补丁路径是否命中块标记:命中则先在块的旧值上应用补丁,再调用updateBlock写回新值,从而实现块内容的可编辑更新。
这套机制使外部 diff / patch 管线能够安全地表示和更新富文本块,而不会丢失块的结构信息。
暴露既有文本对象的 marks 与块内容
此前生成的补丁若涉及一个已存在的文本对象,可能丢失其 marks(标记)、嵌入块与块内容。3.5.0 修复了这一点:补丁现在会携带这些富文本细节,从而修复了"恢复已删除富文本时 diff 不完整"的问题——也就是说,删除后又恢复的富文本内容能够被正确重建。
大变更中的性能回归
修复了因反复扫描 pending operations 来检查对象可见性而导致的性能回归。在变更规模很大时,这种重复扫描会让时间复杂度退化;修复后可见性检查的扫描路径得到优化,大变更的补丁生成不再出现可感知的退化。
计数器增量与尾随插入的定位修复
修复了一个较隐蔽的排序问题:在 change 应用过程中,当计数器增量(counter increment)紧跟一个尾随插入(trailing insert)之前时,列表插入的定位可能出错,进而破坏操作分组(operation grouping),导致后续 change 重建(change reconstruction)失败。该修复保证了操作排序的正确性,避免由此引发的数据重建错误。
实验性getFragmentsAPI 的 checkpoints 调整
getFragments与 fragment 元数据 API(仍为实验性)中,checkpoints不再包含 fragment 自身的 head:checkpoints 现在同时排除 head 与边界哈希(boundary hashes),而 head 仍然保留在members中。这使 checkpoints 的语义更精确,避免与成员列表信息重复。
文档与示例修正
- 将文档示例中不存在的
forkAPI 更正为clone(fork 并非公开 API); - 修正了 TypeScript 示例的格式问题。
升级与迁移建议
面向从 3.4.1 升级到 3.5.0 的用户,总结如下注意事项:
- 新增能力可直接使用:作者元数据是纯增量 API,
from/init/clone的author选项与getAuthor/getAuthors/getAuthorForActor/getActorsForAuthor查询函数不会破坏现有代码; __proto__是破坏性行为变更:如果现有代码依赖向文档写入__proto__键(应属极少见情况),升级后会抛出RangeError,需改用其他键名;- 富文本补丁行为增强:
applyPatch/applyPatches现在会保留嵌入块数据,依赖"块更新被忽略"这一旧行为的代码需要重新审视; - 大列表插入更健壮:过去以"单次超大 splice"方式触发调用栈溢出的场景已修复,大列表批量插入可放心使用;
- 实验性
getFragments/ fragment 元数据 API 的checkpoints语义有调整,使用方请按新语义核对输出。
总结
Automerge JavaScript 3.5.0 是一个兼顾功能与健壮性的版本:作者元数据把"变更归属"带入了协作数据模型,__proto__安全加固堵住了原型链污染入口,而大列表分块插入、富文本块补丁、计数器排序与性能扫描等一系列修复则提升了补丁应用的正确性和大变更场景下的可用性。配合 javascript/src/implementation.ts 与 rust/automerge-wasm/src/lib.rs 的源码实现,以及 javascript/test/basic_test.ts 与 javascript/test/proxies.ts 中的测试用例,开发者可以完整把握每一项变更的边界与正确用法。
- 后端
【免费下载链接】automerge
A JSON-like data structure (a CRDT) that can be modified concurrently by different users, and merged again automatically.
相关推荐
Vitess v24.0.2 补丁版本发布解读:安全修复、连接池与 VReplication 稳定性加固
Vitess v24.0.2 补丁版本发布解读:安全修复、连接池与 VReplication 稳定性加固 Vitess v24.0.2 是 v24.0 系列的一
数据库分布式数据库云原生后端数据存储NumPy 1.18.1 补丁版本全解析:整数梯度修复、构建链修复与 CI 加固
NumPy 1.18.1 补丁版本全解析:整数梯度修复、构建链修复与 CI 加固 导读 本文基于仓库内的官方发布记录 doc/changelog/1.18.1
科学计算数据分析Vitess v22.0.3 补丁版本发布全解析:40 个 PR 的稳定性修复与安全加固
Vitess v22.0.3 补丁版本发布全解析:40 个 PR 的稳定性修复与安全加固 导读 Vitess v22.0.3 是 v22 系列的第 3 个补丁版
数据库分布式数据库云原生后端数据存储
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考