Automerge同步场景下,如何延迟WebSocket关闭直至文档同步完成?
解决方案:WebSocket关闭前确保CRDT变更同步完成
核心思路
拦截WebSocket的关闭触发场景(页面刷新/关闭、主动调用关闭),阻止默认关闭流程,先确认前端所有未同步的CRDT变更已被后端接收持久化,再手动完成关闭操作。
具体实现方案
1. 拦截关闭触发事件
针对不同关闭场景做拦截,进入同步校验流程:
- 页面刷新/关闭:监听
beforeunload事件 - 主动关闭WebSocket:封装安全关闭函数,替代直接调用
ws.close()
// 拦截页面刷新或关闭操作 window.addEventListener('beforeunload', async (e) => { const hasUnsynced = await hasUnsyncedChanges(); if (hasUnsynced) { e.preventDefault(); e.returnValue = '存在未同步的文档变更,确认离开?'; // 等待同步完成后移除拦截,允许关闭 await syncPendingChanges(); window.removeEventListener('beforeunload', arguments.callee); } }); // 封装安全关闭WebSocket的方法 async function safeCloseWebSocket() { const hasUnsynced = await hasUnsyncedChanges(); if (hasUnsynced) { await syncPendingChanges(); } ws.close(); }
2. 高效校验未同步变更(替代全量遍历)
放弃反复调用getAllChanges遍历所有变更的方式,通过维护本地状态优化:
- 前端修改文档时,记录生成的变更并标记为「未同步」
- 后端接收变更后,返回包含变更哈希的确认消息,前端移除对应未同步标记
- 校验时只需检查未同步队列是否为空,无需全量遍历
// 本地维护未同步变更队列 let pendingChanges = []; // 前端修改文档时记录变更 const updatedDoc = Automerge.change(originalDoc, (doc) => { // 执行文档修改操作 }); const changes = Automerge.getChanges(originalDoc, updatedDoc); pendingChanges.push(...changes); // 发送变更到后端 ws.send(JSON.stringify({ type: 'sync', changes })); // 监听后端的同步确认消息 ws.addEventListener('message', (event) => { const data = JSON.parse(event.data); if (data.type === 'sync_ack' && data.hashes) { // 过滤已确认的变更 pendingChanges = pendingChanges.filter(change => !data.hashes.includes(Automerge.getChangeHash(change)) ); } }); // 检查是否存在未同步变更 function hasUnsyncedChanges() { return Promise.resolve(pendingChanges.length > 0); } // 同步所有未完成的变更 async function syncPendingChanges() { if (pendingChanges.length === 0) return; // 发送剩余变更 ws.send(JSON.stringify({ type: 'sync', changes: pendingChanges })); // 等待后端确认全部变更 await new Promise((resolve) => { const ackHandler = (event) => { const data = JSON.parse(event.data); if (data.type === 'sync_ack' && data.hashes.length === pendingChanges.length) { ws.removeEventListener('message', ackHandler); pendingChanges = []; resolve(); } }; ws.addEventListener('message', ackHandler); }); }
3. 超时兜底处理
避免因后端故障导致无限等待同步,设置超时时间,超时后提示用户并允许关闭:
async function syncPendingChanges() { if (pendingChanges.length === 0) return; const syncPromise = new Promise((resolve) => { const ackHandler = (event) => { const data = JSON.parse(event.data); if (data.type === 'sync_ack' && data.hashes.length === pendingChanges.length) { ws.removeEventListener('message', ackHandler); pendingChanges = []; resolve(true); } }; ws.addEventListener('message', ackHandler); ws.send(JSON.stringify({ type: 'sync', changes: pendingChanges })); }); // 5秒超时兜底 const timeoutPromise = new Promise((resolve) => setTimeout(() => resolve(false), 5000)); const syncSuccess = await Promise.race([syncPromise, timeoutPromise]); if (!syncSuccess) { alert('变更同步超时,部分内容可能未保存'); } }
对临时方案的优化说明
临时方案中遍历全量变更的方式在文档较大时性能损耗明显,通过维护未同步变更队列+后端确认机制,将校验复杂度从O(n)降为O(1),同时避免了不必要的全量变更遍历操作。
内容的提问来源于stack exchange,提问作者yasmanets
相关产品推荐
相关产品推荐

