summaryrefslogtreecommitdiff
path: root/src/main/otClient.ts
diff options
context:
space:
mode:
authorhaoyuren <13851610112@163.com>2026-03-12 17:52:53 -0500
committerhaoyuren <13851610112@163.com>2026-03-12 17:52:53 -0500
commitb116335f9dbde4f483c0b2b8e7bfca5d321c5dfc (patch)
tree8bd84b0f4a54eb879c8cc5a158002e999b23d57e /src/main/otClient.ts
parentebec1a1073f9cc5b69e125d5b284669545ea3d9f (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.ts131
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 }
+ }
+}