diff options
| author | haoyuren <13851610112@163.com> | 2026-03-12 17:52:53 -0500 |
|---|---|---|
| committer | haoyuren <13851610112@163.com> | 2026-03-12 17:52:53 -0500 |
| commit | b116335f9dbde4f483c0b2b8e7bfca5d321c5dfc (patch) | |
| tree | 8bd84b0f4a54eb879c8cc5a158002e999b23d57e /src/main/otClient.ts | |
| parent | ebec1a1073f9cc5b69e125d5b284669545ea3d9f (diff) | |
Add bidirectional file sync, OT system, comments, and real-time collaboration
Implement full Overleaf integration with Socket.IO v0.9 real-time sync:
- FileSyncBridge for bidirectional temp dir ↔ Overleaf sync via chokidar + diff-match-patch
- OT state machine, transform functions, and CM6 adapter for collaborative editing
- Comment system with highlights, tooltips, and review panel
- Project list, file tree management, and socket-based compilation
- 3-layer loop prevention (write guards, content equality, debounce)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Diffstat (limited to 'src/main/otClient.ts')
| -rw-r--r-- | src/main/otClient.ts | 131 |
1 files changed, 131 insertions, 0 deletions
diff --git a/src/main/otClient.ts b/src/main/otClient.ts new file mode 100644 index 0000000..7985c66 --- /dev/null +++ b/src/main/otClient.ts @@ -0,0 +1,131 @@ +// OT state machine for main process (mirror of renderer otClient) +import type { OtOp } from './otTypes' +import { transformOps } from './otTransform' + +export type SendFn = (ops: OtOp[], version: number) => void +export type ApplyFn = (ops: OtOp[]) => void + +interface OtState { + name: 'synchronized' | 'awaitingConfirm' | 'awaitingWithBuffer' + inflight: OtOp[] | null + buffer: OtOp[] | null + version: number +} + +export class OtClient { + private state: OtState + private sendFn: SendFn + private applyFn: ApplyFn + + constructor(version: number, sendFn: SendFn, applyFn: ApplyFn) { + this.state = { name: 'synchronized', inflight: null, buffer: null, version } + this.sendFn = sendFn + this.applyFn = applyFn + } + + get version(): number { + return this.state.version + } + + get stateName(): string { + return this.state.name + } + + onLocalOps(ops: OtOp[]) { + if (ops.length === 0) return + + switch (this.state.name) { + case 'synchronized': + this.state = { + name: 'awaitingConfirm', + inflight: ops, + buffer: null, + version: this.state.version + } + this.sendFn(ops, this.state.version) + break + + case 'awaitingConfirm': + this.state = { + name: 'awaitingWithBuffer', + inflight: this.state.inflight, + buffer: ops, + version: this.state.version + } + break + + case 'awaitingWithBuffer': + this.state = { + ...this.state, + buffer: [...(this.state.buffer || []), ...ops] + } + break + } + } + + onAck() { + switch (this.state.name) { + case 'awaitingConfirm': + this.state = { + name: 'synchronized', + inflight: null, + buffer: null, + version: this.state.version + 1 + } + break + + case 'awaitingWithBuffer': { + const bufferOps = this.state.buffer || [] + this.state = { + name: 'awaitingConfirm', + inflight: bufferOps, + buffer: null, + version: this.state.version + 1 + } + this.sendFn(bufferOps, this.state.version) + break + } + + case 'synchronized': + console.warn('[OtClient:main] unexpected ack in synchronized state') + break + } + } + + onRemoteOps(ops: OtOp[], newVersion: number) { + switch (this.state.name) { + case 'synchronized': + this.state = { ...this.state, version: newVersion } + this.applyFn(ops) + break + + case 'awaitingConfirm': { + const { left: transformedRemote, right: transformedInflight } = transformOps(ops, this.state.inflight || []) + this.state = { + ...this.state, + inflight: transformedInflight, + version: newVersion + } + this.applyFn(transformedRemote) + break + } + + case 'awaitingWithBuffer': { + const { left: remoteAfterInflight, right: inflightAfterRemote } = transformOps(ops, this.state.inflight || []) + const { left: remoteAfterBuffer, right: bufferAfterRemote } = transformOps(remoteAfterInflight, this.state.buffer || []) + this.state = { + ...this.state, + inflight: inflightAfterRemote, + buffer: bufferAfterRemote, + version: newVersion + } + this.applyFn(remoteAfterBuffer) + break + } + } + } + + reset(version: number) { + this.state = { name: 'synchronized', inflight: null, buffer: null, version } + } +} |
