diff options
| author | haoyuren <13851610112@163.com> | 2026-03-18 08:06:32 +0000 |
|---|---|---|
| committer | haoyuren <13851610112@163.com> | 2026-03-18 08:06:32 +0000 |
| commit | 9b5256718c2117511f0253a656bb8cff7410b92a (patch) | |
| tree | 8ba0fd257f771538874f37b87dcaeb5471185ca5 /src/main/otClient.ts | |
| parent | 69a09baf71798966724d942b93303211516e34c7 (diff) | |
Fix OT sync corruption: match Overleaf ShareJS ack/echo handling
The server broadcasts otUpdateApplied (with ops) to ALL clients including
the sender. Our bridge was treating its own echoed ops as remote ops and
re-applying them, causing text duplication (e.g. "simulatorimulator").
Rewrite OT handling to match Overleaf's ShareJS _onMessage pattern:
- ACK = no ops OR meta.source matches our publicId (own echo)
- REMOTE = ops from a different source
- ACK path calls onAck() without re-applying ops
- OtClient silently drops duplicate acks in synchronized state
- OtClient drops stale remote ops (version < current)
- Remove pendingEchos counter in favor of meta.source detection
Also: refresh MCP comment contexts on new-comment/delete-thread events,
add Overleaf reference repo to .gitignore.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Diffstat (limited to 'src/main/otClient.ts')
| -rw-r--r-- | src/main/otClient.ts | 36 |
1 files changed, 34 insertions, 2 deletions
diff --git a/src/main/otClient.ts b/src/main/otClient.ts index dc6bb4c..2917bcc 100644 --- a/src/main/otClient.ts +++ b/src/main/otClient.ts @@ -1,7 +1,17 @@ // Copyright (c) 2026 Yuren Hao // Licensed under AGPL-3.0 - see LICENSE file -// OT state machine for main process (mirror of renderer otClient) +// OT state machine for main process +// Modeled after Overleaf's ShareJS client (vendor/libs/sharejs.js) +// +// States: +// synchronized — no pending ops, version matches server +// awaitingConfirm — one inflight op awaiting server ack +// awaitingWithBuffer — inflight + buffered local ops +// +// Key invariant: at most ONE inflight op at a time. +// Version increments by 1 on each ack or remote op. + import type { OtOp } from './otTypes' import { transformOps } from './otTransform' @@ -66,6 +76,15 @@ export class OtClient { } } + /** + * Server acknowledged our inflight op. + * Matches Overleaf's ShareJS: both "ack without ops" and "echoed ops from + * our own source" are treated as acks. The echoed ops are NOT re-applied + * because they were already applied optimistically when submitted. + * + * In synchronized state, silently drops (duplicate ack — common when server + * sends both an echo and a separate ack event). + */ onAck() { switch (this.state.name) { case 'awaitingConfirm': @@ -90,12 +109,25 @@ export class OtClient { } case 'synchronized': - console.warn('[OtClient:main] unexpected ack in synchronized state') + // Duplicate ack — silently drop. + // This is expected: server may send both an echoed op (with meta.source) + // and a separate ack event (without ops). The first one transitions us + // to synchronized, the second arrives when we're already there. break } } + /** + * Server sent a remote op from another client. + * Transform against inflight/buffered ops before applying. + */ onRemoteOps(ops: OtOp[], newVersion: number) { + // Stale message detection (matching Overleaf's ShareJS): + // if the server version is behind our version, we already processed this. + if (newVersion < this.state.version) { + return + } + switch (this.state.name) { case 'synchronized': this.state = { ...this.state, version: newVersion } |
