多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

loro.js 分离文档快照导出修复:编码最新状态、版本与前沿并保持检出不变

loro.js 分离文档快照导出修复:编码最新状态、版本与前沿并保持检出不变 后端【免费下载链接】loroMake your JSON data collaborative and version-controlled with CRDTs项目地址https://gitcode.com/gh_mirrors/lo/loro点击查看免费下载导读本篇文章围绕 loro.js 仓库中.changeset/loro-js-detached-snapshot.md记录的修复展开当调用方对一个处于detached分离/检出到历史版本状态的LoroDoc执行export({ mode: snapshot })时导出内容现在会携带最新状态latest state、版本version与前沿frontiers与 Rust 端及loro-crdt行为保持一致。读完本文你将理解 detached 文档的快照导出语义、修复前后的行为差异、#encodeSnapshot/#encodeLatestState的底层实现路径以及如何用单元测试与差分测试验证这一行为。背景什么是 detached 文档在 loro.js 中一个文档可以通过以下两种方式进入 detached 状态调用checkout(frontiers)将文档状态回退到某个历史前沿此时#detached true#checkoutVersion被设置为目标版本见 document.ts调用detach()将当前版本固化为检出版本并标记 detached见 document.ts。detached 状态下的文档只能读、不能写document.ts中多处编辑入口会抛出cannot edit a detached document; call attach() first见 document.ts。通过isDetached()可以查询状态checkoutToLatest()/attach()则用于回到最新版本并重新挂载。关键点在于detached 文档的“状态”与其“历史oplog”是两个不同概念。文档可能检出于历史版本V1但其oplog中已经包含甚至是在 detached 期间通过import()接收到的更新最新版本是V2。快照导出如何在这种错位下保持正确正是本次修复的核心。问题修复前的快照导出行为本次变更说明明确指出修复前的问题Before, the export wrote the checked-out state and version next to the full history, so an importer saw an old state as the latest version and a later checkout failed.即修复前对 detached 文档执行export({ mode: snapshot })时导出的**状态部分state是当前检出版本checked-out state**下的状态导出的**版本version**是当前检出版本对应的版本向量但**历史full history**是完整的包含了检出版本之后的所有更新。这样产生的快照文件内部自相矛盾导入方拿到快照后会看到“旧状态 完整历史”的组合。由于快照头部声明的版本是旧的检出版本导入方会把旧状态误认为最新版本而历史中实际存在更新的变更。此时若导入方尝试checkout(latestFrontiers)到达历史真实的最新前沿会因状态与版本不一致而失败造成后续 checkout 无法正确进行。修复后的行为快照始终携带最新状态修复后的语义可以概括为一句话snapshot 模式导出的永远是文档的“最新世界”无论当前是否 detached。源码#encodeSnapshot()的注释直接给出了这条契约见 document.ts// A snapshot always carries the latest state, as in Rust. A detached // documents state (lazy or not) may be at an older version or may miss // updates imported while detached. const latestVersion this.#historyVersion(); const detachedState this.version().compare(latestVersion) ! 0;具体来说修复后导出快照包含三部分全部取历史的最新值快照组成修复前修复后状态state检出版本的状态最新版本的状态#encodeLatestState()版本versionVERSION_KEY检出版本的版本向量历史最新版本向量latestVersion前沿frontiersFRONTIERS_KEY检出版本的前沿历史最新前沿排序后的#historyFrontiers同时变更说明强调“The exporting document keeps its checkout”导出过程不会把源文档“带走”源文档的 detached 状态与检出版本在导出后保持不变。这是通过#encodeLatestState()的“转移后恢复”机制实现的详见下文。源码级实现剖析1.ExportMode与export()分派ExportMode定义了四种导出模式见 types.tsexport type ExportMode | { readonly mode: update; readonly from?: VersionVector } | { readonly mode: snapshot } | { readonly mode: shallow-snapshot; readonly frontiers: Frontiers } | { readonly mode: updates-in-range; readonly spans: readonly { readonly id: OpId; readonly len: number }[]; };export()在commit之后按模式分派见 document.tsupdate走增量更新编码shallow-snapshot走浅快照默认的snapshot模式最终调用#encodeSnapshot()。本次修复影响的正是最后这条路径。2.#encodeSnapshot()统一按最新历史编码#encodeSnapshot()内部有两条分支非 detached 且存在 deferred snapshot惰性快照直接从#deferredSnapshotHistory/#deferredSnapshotState组装FastSnapshot把VERSION_KEY与FRONTIERS_KEY覆盖为最新值其他情况含 detached从#sortedHistory()重建历史条目SSTable 形式随后写入VERSION_KEYlatestVersion的 postcard 版本向量与FRONTIERS_KEY排序后的最新前沿并据此决定状态来源state: detachedState ? this.#encodeLatestState() : this.#encodeDeferredSnapshotState(),也就是说一旦检测到“状态版本 ≠ 历史最新版本”detachedState为真状态部分就会由专门的#encodeLatestState()生成而不是沿用当前检出版本的状态。3.#encodeLatestState()临时前进、编码、再恢复#encodeLatestState()见 document.ts是实现“导出最新状态但不改变检出”的关键记录当前版本current与历史最新版本latest计算两者之间的前进记录allForward与回退记录allRetreat优先采用版本增量转移方式在文档上临时应用版本转移至latest复杂度为 O(delta)惰性编码且未被增量触及的容器保持其编码条目不动然后编码状态若增量转移不可行例如跨越了不可重放的状态则退化为在隔离的 staging 文档上重放一次完整历史forkAt(this.oplogFrontiers())后构建状态存储最后通过finally恢复成功时转移回current失败时#rebuildFromHistory(current)保证不留下部分更新的脏状态。由于#applyVersionTransition在转移前后会被调用两次前进后再回退源文档最终停留在原来的检出版本上——这就是“exporting document keeps its checkout”的机制来源。API 使用场景与注意事项本次修复使得以下典型工作流可以安全运行历史版本快照归档检出到旧版本查看 / 审计随后直接export({ mode: snapshot })生成归档导入方可正确到达最新状态离线 / 懒加载文档的 detached 期导入一个从快照懒加载的文档在 detached 期间通过import()接收更新导出快照时这些更新不会丢失跨端一致性loro.js与 Rust /loro-crdt的快照导出语义对齐混合使用多个实现的客户端不会因“旧状态被当作最新版本”而产生 checkout 失败。注意事项快照导出包含完整历史若只需增量同步应优先使用export({ mode: update, from })浅快照不受影响shallow-snapshot模式仍按指定的frontiers裁剪历史与状态见 document.ts源文档不被移动导出前后isDetached()与检出版本保持不变可放心在 detached 状态下反复导出。测试验证本次修复在 detached-snapshot.test.ts 中通过三个用例覆盖encodes the latest state and keeps the checkout构建两轮提交的历史checkout(first)后导出快照断言导出后源文档仍 detached、前沿与 JSON 值仍停留在first导入方toJSON()与frontiers()/oplogFrontiers()均为最新值且可checkout(latest)成功includes updates imported while detached into a lazily imported document目标文档从快照导入后detach()在 detached 期间接收增量更新再导出快照导入方得到最新值{ m: { x: 2 } }且源文档仍保持 detached、仍显示旧值{ m: { x: 1 } }includes updates imported while detached普通文档检出到旧版本后detached 期间导入其他 peer 的更新导出快照后导入方能读到与对方完全一致的最新 JSON 与前沿。此外差分测试也在持续把关这一语义差分 harness 会在每次操作后比对 Rust 与 JS 两侧的isDetached()状态见 harness.ts而 differential.ts 的模糊测试脚本覆盖了detach/checkout/ 快照导出等组合场景确保loro.js的 detached 快照语义与 Rust 实现长期保持一致。小结本次loro.js的 patch 修复了一个容易踩坑的语义缺陷detached 文档的快照导出不再暴露检出版本的“旧状态 完整历史”矛盾组合而是统一编码最新状态、版本与前沿。从实现上看#encodeSnapshot()负责组装最新历史与元数据#encodeLatestState()通过“临时前进—编码—恢复”的方式导出最新状态且不扰动源文档的检出。若你的应用依赖“历史检视 快照归档 跨端同步”组合升级到包含该修复的版本后快照导入与后续 checkout 的行为将更加稳健、与 Rust 端一致。延伸阅读变更记录.changeset/loro-js-detached-snapshot.md导出实现document.ts 与#encodeSnapshot/#encodeLatestStatedocument.ts导出模式类型types.ts单元测试detached-snapshot.test.ts差分一致性测试harness.ts赞分享后端【免费下载链接】loroMake your JSON data collaborative and version-controlled with CRDTs项目地址https://gitcode.com/gh_mirrors/lo/loro点击查看免费下载相关推荐Road Architect for Unity完全指南从零开始创建专业游戏道路系统Road Architect for Unity完全指南从零开始创建专业游戏道路系统 Road Architect for Unity是一款强大的游戏道路系统游戏开发开发工具如何快速导出飞书文档2025最新飞书文档一键导出工具支持Markdown/PDF/Word全格式如何快速导出飞书文档2025最新飞书文档一键导出工具支持Markdown/PDF/Word全格式 飞书文档导出工具feishu doc export是CLI企业应用如何快速导出飞书文档2025最新飞书文档导出工具全攻略 如何快速导出飞书文档2025最新飞书文档导出工具全攻略 飞书文档导出服务是一款高效的开源工具能帮助用户将飞书文档一键导出为Markdown、PDF和DCLI企业应用上一篇Anarlog 1.4.23 版本深度解析会话洞察、徽章成就与转录稳定性全面升级下一篇Worktrunk 扩展机制完全指南Hooks 生命周期钩子、Aliases 别名与自定义子命令实战创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表