选择合并方式前,先说明离线编辑冲突

使用 AI 辅助编写与复核。

本文目录

离线编辑器需要的不只是“联网后同步”。本地草稿遇到较新的已保存记录时,应用必须有明确决定:保留本地文字、展示两个版本、提出组合建议,还是拒绝写入?

先描述历史,再选择合并算法。把三件事分开:编辑开始时的版本、尚未保存的草稿,以及最近从服务读取的版本。随后规定被拒绝的写入和结果不确定的保存应如何显示。

在 HTTP 设计中,If-Match 可以让写入以实体标签匹配为前提。RFC 9110 要求使用强比较;条件不满足时,不得执行所请求的方法。如果能够确认修改此前已应用,RFC 允许返回成功响应;否则,前置条件失败可以产生 412 Precondition Failed。它没有定义字段合并策略。RFC 9110,第 13.1.1 节

建立一条虚构记录

两台设备 Laptop A(笔记本 A)和 Tablet B(平板 B)编辑虚构笔记 N17。本练习假设两者都有所需权限。完整可编辑记录恰好有两个字段:

{"title":"Release checklist","body":"Check links."}

服务随每个版本返回一个强实体标签。下文的 "n7"、"n8" 等标签是虚构的不透明标签。只比较它们是否精确相等,不要解析其中的数字,也不要把它们当作时间戳。

这些示例采用一份简单服务契约:替换写入同时提交两个字段,以及它所基于的标签。匹配时,写入一起替换两个字段,并获得一个新标签。不匹配时,返回 412,两个字段都不改变。这个虚构服务不采用 RFC 中“此前已应用则返回成功”的选项。按本应用的策略,缺少标签也会被拒绝。

实现时,比较和写入必须组成一次条件更新。先读取标签、在客户端检查,再发出无条件写入,不符合这份契约。

每台设备都保留一个编辑资料包,包含记录 ID、基线标签、完整基线记录和完整本地草稿。刷新远端版本不能覆盖这份资料包。以下历史是手工推导的预期,不是发送到服务器的流量。

情况一:两台设备修改了不同字段

两台设备最初都读取 "n7"。笔记本 A 离线修改标题,平板 B 在线修改正文。

步骤 设备事件与写入依据 最近保存的标签 完整已保存标题 完整已保存正文
D0 A 和 B 读取初始笔记 "n7" Release checklist Check links.
D1 A 离线编辑;没有写入 "n7" Release checklist Check links.
D2 B 以 If-Match: "n7" 写入正文修改,收到成功响应 "n8" Release checklist Check links and spelling.
D3 A 重新连接,以 If-Match: "n7" 提交完整本地草稿 "n8" 不变;412 Release checklist Check links and spelling.
D4 A 明确读取最新笔记;没有写入 "n8" Release checklist Check links and spelling.
D5 A 复查组合提案,并确认一次基于 "n8" 的写入 "n9";成功 Release checklist v2 Check links and spelling.

D3 时,A 保留的编辑资料包仍是:

{
  "recordId": "N17",
  "baseTag": "\"n7\"",
  "base": {
    "title": "Release checklist",
    "body": "Check links."
  },
  "localDraft": {
    "title": "Release checklist v2",
    "body": "Check links."
  }
}

服务没有接受这份本地草稿,此时显示“已保存”是不对的。用最新远端记录替换草稿,也会丢掉 A 尚未保存的标题。

D4 之后,逐字段与原基线比较:

字段 "n7" 时的基线 A 的草稿 "n8" 时的远端值 选定提案
标题 Release checklist Release checklist v2 Release checklist 采用 A 修改的标题
正文 Check links. Check links. Check links and spelling. 采用远端修改的正文

因为每个字段都只在一侧改变,这个应用会提供组合提案。这是应用规则,不是 HTTP 功能。D5 前仍由人复查组合结果。对于更大的记录,假设字段相互独立可能并不合适:即使键不同,标题与正文也可能互相依赖。

由此得到的示意请求为:

PUT /notes/N17
If-Match: "n8"
Content-Type: application/json

{"title":"Release checklist v2","body":"Check links and spelling."}

这些请求形状属于虚构服务,不是在调用 CueSpec API。

较新的编辑可能使已复查的提案失效

考虑 D4 之后的另一条分支,用它替换 D5。平板 B 以 If-Match: "n8" 把标题改为 Urgent release checklist;服务将这条完整笔记保存为 "n9",正文仍为 Check links and spelling.。随后 A 仍以 "n8" 提交上面的提案。

服务再次返回 412,保留 B 的完整 "n9" 记录。A 保留草稿,并把提案标为已过时。它必须重新读取并展示新出现的标题冲突,不能只把请求中的 "n8" 换成 "n9" 再发送:那会把对先前组合的复查,变成覆盖后来决定的授权。

A 尚未基于新记录作出新选择时,再按 Save 只会打开冲突复查,不会发送写入。这个明确暂停的步骤,是所选界面策略的一部分。

情况二:两台设备修改了同一字段

从相同记录值开始一段独立历史,这次标签是 "s7",不是接着 "n9" 的历史继续。

步骤 事件与写入依据 已保存标签 完整已保存标题 完整已保存正文
S0 两台设备读取初始笔记 "s7" Release checklist Check links.
S1 A 把离线标题改为 Ready for review;没有写入 "s7" Release checklist Check links.
S2 B 使用 "s7" 保存标题 Needs wording review "s8" Needs wording review Check links.
S3 A 使用 "s7" 提交本地标题和未修改的正文 "s8" 不变;412 Needs wording review Check links.
S4 A 读取 "s8" 并比较三个版本 "s8" 不变 Needs wording review Check links.
S5 用户明确输入 Ready for wording review,并以 "s8" 保存 "s9";成功 Ready for wording review Check links.

S4 时,标题比较包含三个不同值:基线是 Release checklist,本地草稿是 Ready for review,远端值是 Needs wording review。任何一项修改都不能从另一项推导出来。应用把三者放在一起展示,并在用户决定期间保留本地文字。

S5 使用用户新写的标题。应用没有擅自认定,组合两个短语就保留了任一作者的意图。其他明确选择也可以成立:保留远端标题,或选用本地标题,并以最近复查的标签提交。应用应清楚说明这种覆盖。

没有作出选择,就不会出现新的写入。关闭冲突面板会让草稿继续处于未解决状态,不代表已接受远端值作为解决方案。能否在崩溃或应用重启后保留该草稿,是另一项需要设计和测试的存储要求。

保存响应丢失,需要不同提示

超时不能证明条件更新被拒绝。再看一条独立分支:

步骤 事件与写入依据 这段虚构历史中实际保存的标签 完整已保存标题 完整已保存正文 A 知道什么
L0 两台设备读取初始笔记 "l7" Release checklist Check links. 已知基线
L1 A 以 "l7" 写入标题 Release checklist v2;服务已应用,但响应丢失 "l8" Release checklist v2 Check links. 保存结果未知
L2 B 读取 "l8",修改正文,并以 "l8" 保存 "l9" Release checklist v2 Check links and spelling. A 尚未读取这个版本
L3 A 明确刷新并收到 "l9" "l9" 不变 Release checklist v2 Check links and spelling. 当前标题与草稿一致;当前正文有较晚修改

L3 前显示“保存结果未知,请检查最新版本”,并保留编辑资料包。如果读取也失败,这种不确定状态继续保留。

L3 后,A 可以说最新记录包含自己期望的标题。但这个观察不能确定是哪项请求或哪位作者产生了它。本示例没有操作回执端点,所以即使期望标题已经存在,原请求的回执仍未确认。让用户明确确认当前记录,再主动清除本地待定草稿;不要重发旧的完整记录,覆盖 B 较新的正文。

先选择结果,再选择算法

情况 选定的下一步 现在允许写入吗? 继续保留什么?
旧标签被拒绝 读取最新内容,展示冲突复查 不自动写入 基线和本地草稿
不同字段被修改 提供组合提案 只有复查后,以最近复查的标签写入 确认成功前保留本地草稿
同一字段出现不同值 展示基线、本地和远端 只有明确选择后才允许 解决前保留尚未选定的本地文字
复查后又出现新远端版本 使提案失效,重新读取并决定 不重放过时提案 草稿和未解决的决定
保存响应丢失 标为结果未知,读取当前记录 不自动发起替换写入 待定编辑资料包
解决冲突前按 Save 回到冲突复查 不允许 未解决草稿

复制一份离线冲突 brief

使用本文完整的 D、S、L 历史,为虚构笔记 N17 准备一份冲突处理设计。
不要调用 API,也不要实现生产同步。

可编辑记录恰好包含 title 和 body。把带引号的标签视为不透明的强验证器。
我们选择的服务只在提交标签匹配时一起替换两个字段;
不匹配则返回 412,不应用修改。不采用“此前已应用则返回成功”的优化。

把记录 ID、基线标签、完整基线和本地草稿,与最新远端读取分开保留。
刷新不能抹去本地编辑。D 情况提供需复查的组合;
S 情况要求对同字段冲突作出明确选择。
如果拟议保存之前出现较新版本,使提案失效并取得新决定。
不要更新标签后默默重放旧的完整替换请求。

对于 L,区分保存回执未知、写入被拒绝,以及观察到期望的当前标题。
保留较晚的正文修改。冲突解决前按 Save 不发送写入。
指出仍需实现工作的重启后保留草稿及原子条件更新要求,
不要假设这些机制已经存在。

返回完整结果记录、UI 状态和决策步骤。
把纸面预期与之后任何实际执行的检查分开。

明确预期结果后,再选择自动合并、冲突面板或更简单的单写入者策略。真正有用的起点是一段历史:它说清谁的未保存工作仍被保留、写入依赖哪个版本,以及凭什么证据,界面才可以显示“已保存”。