summaryrefslogtreecommitdiff
path: root/src/renderer
diff options
context:
space:
mode:
authorhaoyuren <13851610112@163.com>2026-03-13 18:46:46 -0500
committerhaoyuren <13851610112@163.com>2026-03-13 18:46:46 -0500
commit11166a63affc4e95450f677d860c8bfdb8211bd9 (patch)
tree2ad4b0c81cb4df6e982c0f88fa43b2ed65b972fc /src/renderer
parentfa52c3c4d6c21a26c838fa33de2d7fa447f9499e (diff)
Fix Claude Code → Overleaf sync: remove hash from applyOtUpdate, fix OT bugs
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 <noreply@anthropic.com>
Diffstat (limited to 'src/renderer')
-rw-r--r--src/renderer/src/ot/overleafSync.ts22
1 files changed, 7 insertions, 15 deletions
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<typeof setTimeout> | 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
}
}