失败后重试,只能救下还活着的页面
浏览器侧采集到一批学习记录,准备同步时恰好断网。如果只是捕获异常,再用一个定时器重试,那么关闭标签页以后,定时器和内存里的记录就都消失了。
洛谷学习工具因此先把待同步批次保存在 userscript 的 GM 存储里,再尝试发送。下一次打开页面,仍能找到没有获得确认的批次。前提是本地存储确实写入成功,且没有被用户清除。
这篇只讨论同步机制。真实学生资料、源代码和提交记录不用于公开举例。
服务端写入了,客户端却不知道
把过程分成五步,会更容易看到风险:
记录入箱 -> 发出请求 -> 服务端入库 -> 收到确认 -> 删除该批
如果中断发生在服务端入库之后、确认到达之前,客户端仍会保留这批记录,以后再次发送。这时重复请求是恢复过程中的正常情况。
项目后端按记录标识执行幂等更新,并核对当前用户归属;客户端只在 accepted === true 后删除本批。持久待发箱与服务端幂等必须配合,不能单靠“发送成功一次”保证记录不会重复影响业务。
面试研习社里关于“确认丢失怎么办”的追问,用这一条链路就能讲清楚:不确定时保留待办,重复到达时由接收端识别同一实体。
每批一个键,避开共享数组覆盖
两个标签页如果都读取一个共享数组,各自追加再写回,后写入的那个可能覆盖另一个标签页刚加的内容。
当前客户端为每个批次生成独立存储键,包含时间、标签页标识和序号。发送前固定本轮的批次列表;收到确认只删除对应批次的键,发送期间新入箱的批次交给下一轮。
例如 A 发出批次 X 时,B 加入批次 Y。X 的确认回来后,只删除 X,不能把当时整个待发箱一并清空。这个规则比“请求成功后清掉缓存”具体得多,也更容易写出回归场景。
它仍然不是跨标签页的一次全局事务。不同标签页可能重复发送同一批,执行锁或领取策略可以减少重复工作,最终仍要由后端幂等承受重复输入。
账号和服务器都属于命名空间
一台设备可能切换账号,也可能从本地测试服务切换到另一台服务器。旧账号的待发记录、令牌和同步水位,不能直接拿给新的目标使用。
客户端因此按环境和账号区分相关存储键。重试回调触发时,也要确认当前账号仍然匹配。身份已经变化时,继续发送旧批次可能比暂时停下更危险。
服务端核对归属同样不能省略。客户端说某条记录属于谁,不是可信身份的唯一依据。
退避不是把失败藏起来
当前失败处理保存重试次数、下一次重试时间和错误说明;等待逐步增加,并带少量随机抖动。鉴权失败或需要重新登记时,则停止普通补投,提示恢复身份条件。
如果本地存储配额不足,批次连入箱都没成功,就应该明确告知用户,不能继续显示“一切已经妥善暂存”。界面提示与持久化状态需要一致。
我会按四种情况检查恢复逻辑:入箱后刷新、发送期间关闭页面、确认丢失后重发、切换账号后重试。再增加一个双标签页同时入箱的场景,检查是否有批次被整体清理误删。
对应实现集中在 client/collector.user.js 的 queueOutbox、sendRecords 和 flushOutbox。读完以后,再回看 FlashFlow 的持久化任务,会发现浏览器与服务端虽然存储方式不同,都要回答同一个问题:工作完成之前,谁负责记住它还没完成?