工程实践

离线优先的多人协作:本地操作队列如何安全收敛

从一次家务记录出发,解释离线写入、重放与冲突处理如何组成可恢复的协作链路。

共同生活里的记录往往发生在网络最不可靠的时候:电梯里补记一次采购,地下车库里勾掉倒垃圾,或在信号刚恢复时连续完成几项任务。对这类产品来说,“离线可用”不能只意味着页面还能打开,而要意味着用户做出的动作不会因为连接状态而消失。

先保存意图,再等待网络

treetree 把一次本地操作看成一条有身份的意图。界面先把结果写进本地状态,同时生成稳定的操作标识、目标对象、动作类型和发生时间。网络请求随后执行,但它不再决定按钮是否“真的生效”。

这样做的价值不是追求零延迟,而是把失败的边界收窄:连接失败时,我们需要重试的是一条已知操作,而不是猜测用户刚才是否点过按钮。

队列不是请求缓存

如果只是把 HTTP 请求原样缓存,客户端会把当时的鉴权头、临时地址甚至已经失效的上下文一并保存。操作队列保存的应当是领域动作,例如“完成任务 A”或“为记录 B 添加备注”,在重放时再根据当前会话组装请求。

每条操作至少需要回答四个问题:它是谁、它作用于谁、它依赖什么,以及重复执行是否安全。稳定的操作标识让服务端可以去重;目标版本帮助发现竞争写入;明确的依赖关系避免先提交子记录再创建父记录。

用幂等收住重复提交

移动网络会制造一种常见的不确定性:服务端已经成功,客户端却没有收到响应。如果客户端只能“再试一次”,服务端就必须把同一个操作标识视为同一次意图,而不是第二次业务动作。

幂等并不等于所有冲突都自动解决。对“完成任务”这类单向状态,可以接受重复提交;对备注文本或时间调整,则要保留服务端版本,并让客户端重新基于最新数据提交。我们不会把最后写入者胜出当作万能答案,因为它可能安静地覆盖另一个成员刚刚补充的信息。

收敛发生在可解释的状态里

队列会经历等待、提交中、已确认和需要处理几种状态。界面不必暴露工程术语,但应当让人知道记录是否仍在本机、是否正在同步,以及是否需要重新确认。用户能理解“这条记录还在设备上”,却很难从一个永远旋转的加载图标里推断发生了什么。

真正的离线优先不是让错误消失,而是让每一次意图都有归处、每一次重放可以辨认、每一次冲突能够解释。等网络恢复后,系统的目标也不是尽快清空队列,而是在不制造重复、不吞掉修改的前提下安全收敛。