From 11166a63affc4e95450f677d860c8bfdb8211bd9 Mon Sep 17 00:00:00 2001 From: haoyuren <13851610112@163.com> Date: Fri, 13 Mar 2026 18:46:46 -0500 Subject: =?UTF-8?q?Fix=20Claude=20Code=20=E2=86=92=20Overleaf=20sync:=20re?= =?UTF-8?q?move=20hash=20from=20applyOtUpdate,=20fix=20OT=20bugs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Root cause: SHA-1 hash sent with applyOtUpdate didn't match Overleaf's server-side computation, causing "Invalid hash" error, disconnect, and rollback of all synced changes. - Remove hash field from applyOtUpdate to skip server-side hash check - Switch applyOtUpdate from fire-and-forget to emitWithAck for reliable ack - Fix getOldDoc bug: save base doc when changes start accumulating instead of incorrectly using current doc (caused wrong delete ops) - Fix ack handler: only ack when no 'op' field (was acking remote ops too) - Await fileSyncBridge.start() instead of fire-and-forget - Add joinDoc retry logic for transient joinLeaveEpoch mismatch errors - Clear pending ack callbacks on WebSocket close to prevent timeout errors - Add otUpdateError logging for server-side rejections - Add file-based bridge logging for debugging sync issues Co-Authored-By: Claude Opus 4.6 --- src/main/index.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) (limited to 'src/main/index.ts') diff --git a/src/main/index.ts b/src/main/index.ts index 9be1a74..d4d9b2d 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -625,12 +625,15 @@ ipcMain.handle('ot:connect', async (_e, projectId: string) => { }) // otUpdateApplied: server acknowledges our op (ack signal for OT client) + // Only ack when there's no 'op' field — presence of 'op' means it's a remote update, not our ack overleafSock.on('serverEvent', (name: string, args: unknown[]) => { if (name === 'otUpdateApplied') { - const update = args[0] as { doc?: string; v?: number } | undefined - if (update?.doc) { + const update = args[0] as { doc?: string; op?: unknown[]; v?: number } | undefined + if (update?.doc && !update.op) { sendToRenderer('ot:ack', { docId: update.doc }) } + } else if (name === 'otUpdateError') { + console.log(`[ot:error] server rejected update:`, JSON.stringify(args).slice(0, 500)) } }) @@ -662,9 +665,7 @@ ipcMain.handle('ot:connect', async (_e, projectId: string) => { // Set up file sync bridge for bidirectional sync const tmpDir = compilationManager.dir fileSyncBridge = new FileSyncBridge(overleafSock, tmpDir, docPathMap, pathDocMap, fileRefs, mainWindow!, projectId, overleafSessionCookie, overleafCsrfToken) - fileSyncBridge.start().catch((e) => { - console.log('[ot:connect] fileSyncBridge start error:', e) - }) + await fileSyncBridge.start() return { success: true, @@ -703,7 +704,6 @@ ipcMain.handle('ot:joinDoc', async (_e, docId: string) => { try { const result = await overleafSock.joinDoc(docId) const content = (result.docLines || []).join('\n') - // Update compilation manager with doc content if (compilationManager && overleafSock.projectData) { const { docPathMap } = walkRootFolder(overleafSock.projectData.project.rootFolder) -- cgit v1.2.3