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/renderer/src/ot/overleafSync.ts | 22 +++++++--------------- 1 file changed, 7 insertions(+), 15 deletions(-) (limited to 'src/renderer') diff --git a/src/renderer/src/ot/overleafSync.ts b/src/renderer/src/ot/overleafSync.ts index 1c6672b..e288c56 100644 --- a/src/renderer/src/ot/overleafSync.ts +++ b/src/renderer/src/ot/overleafSync.ts @@ -19,6 +19,7 @@ export class OverleafDocSync { private view: EditorView | null = null private docId: string private pendingChanges: ChangeSet | null = null + private pendingBaseDoc: Text | null = null // doc before pendingChanges private debounceTimer: ReturnType | null = null private debounceMs = 150 @@ -46,6 +47,7 @@ export class OverleafDocSync { this.pendingChanges = this.pendingChanges.compose(changes) } else { this.pendingChanges = changes + this.pendingBaseDoc = oldDoc // save the base doc for correct OT op generation } // Debounce send @@ -54,29 +56,17 @@ export class OverleafDocSync { } private flushLocalChanges() { - if (!this.pendingChanges || !this.view) return + if (!this.pendingChanges || !this.view || !this.pendingBaseDoc) return - const oldDoc = this.view.state.doc - // We need the doc state BEFORE the pending changes were applied - // Since we composed changes incrementally, we work backward - // Actually, we stored the ChangeSet which maps old positions, so we convert directly - const ops = changeSetToOtOps(this.pendingChanges, this.getOldDoc()) + const ops = changeSetToOtOps(this.pendingChanges, this.pendingBaseDoc) this.pendingChanges = null + this.pendingBaseDoc = null if (ops.length > 0) { this.otClient.onLocalOps(ops) } } - private getOldDoc(): Text { - // The "old doc" is the current doc minus pending local changes - // Since pendingChanges is null at send time (we just cleared it), - // and the ChangeSet was already composed against the old doc, - // we just use the doc that was current when changes started accumulating. - // For simplicity, we pass the doc at change time via changeSetToOtOps - return this.view!.state.doc - } - /** Send ops to server via IPC */ private handleSend(ops: OtOp[], version: number) { const docText = this.view?.state.doc.toString() || '' @@ -114,6 +104,7 @@ export class OverleafDocSync { reset(version: number, docContent: string) { this.otClient.reset(version) this.pendingChanges = null + this.pendingBaseDoc = null if (this.debounceTimer) { clearTimeout(this.debounceTimer) this.debounceTimer = null @@ -164,5 +155,6 @@ export class OverleafDocSync { if (this.debounceTimer) clearTimeout(this.debounceTimer) this.view = null this.pendingChanges = null + this.pendingBaseDoc = null } } -- cgit v1.2.3