在Hocuspocus服务器恢复Y.js文档快照无法同步至Tiptap客户端
解决Hocuspocus服务器恢复Y.js快照后客户端不同步的问题
问题诊断
客户端能收到restored广播消息但编辑器内容不更新,说明服务器端的文档更新未被Hocuspocus正确捕获并同步给客户端。核心原因是直接操作底层Y.Doc的更新未触发Hocuspocus的同步机制,且清空文档的方式存在逻辑问题。
修复方案
1. 改用Hocuspocus官方API处理更新
放弃直接调用Y.applyUpdate操作Y.Doc,使用Hocuspocus提供的docWrapper.applyUpdate方法,确保服务器端的更新被框架追踪并自动广播给所有客户端。
2. 正确清空现有文档内容
替换原有用空Y.Doc生成update的方式,直接遍历并删除当前文档的所有根节点,避免不必要的状态冲突。
3. 明确更新来源标记
在应用快照时指定更新来源为服务器,确保Hocuspocus不会忽略该更新的同步逻辑。
修改后的代码
async onStateless({ documentName, payload, document }) { const data = JSON.parse(payload as string); logger.info(`Stateless message for ${documentName}:`, data); if (data.type !== 'restore') return; // 从数据库获取快照 const snapshotResult = await pgPool.query( `SELECT id, state, created_by, created_at, label, metadata FROM yjs_snapshot WHERE id = $1`, [data.snapshotId] ); if (snapshotResult.rows.length === 0) { logger.warn(`Snapshot ${data.snapshotId} not found for ${documentName}`); return; } const snapshot = snapshotResult.rows[0]; const docWrapper = hocuspocus.documents.get(documentName); if (!docWrapper) return; try { // 清空当前文档的所有根节点 document.transact(() => { for (const key of Array.from(document.keys())) { document.delete(key); } }); // 解析快照并通过Hocuspocus API应用更新 const snapshotBuffer = new Uint8Array(snapshot.state); await docWrapper.applyUpdate(snapshotBuffer, { origin: 'server' // 标记为服务器发起的全局更新 }); // 发送恢复完成的广播 docWrapper.broadcastStateless( JSON.stringify({ type: 'restored', by: data.by, at: new Date().toISOString(), info: data.info || null, snapshotId: data.snapshotId, }) ); logger.info(`Document ${documentName} restored from snapshot ${data.snapshotId}`); } catch (error) { logger.error(`Failed to restore snapshot ${data.snapshotId} for ${documentName}:`, error); } }
关键修改点说明
- 使用
docWrapper.applyUpdate:Hocuspocus的DocumentWrapper会在应用更新后自动触发同步逻辑,确保所有连接的客户端收到最新文档状态。 - 直接删除根节点清空文档:避免了空Y.Doc update可能带来的状态冲突,更高效且符合Y.js的文档操作规范。
- 指定
origin: 'server':明确标记这是服务器发起的全局更新,Hocuspocus不会跳过该更新的广播流程。 - 增加错误捕获:便于排查恢复过程中出现的异常,提升代码健壮性。
内容的提问来源于stack exchange,提问作者Alex Kleshchevnikov




