summaryrefslogtreecommitdiff
path: root/src/main/overleafProtocol.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/overleafProtocol.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/overleafProtocol.ts')
-rw-r--r--src/main/overleafProtocol.ts95
1 files changed, 95 insertions, 0 deletions
diff --git a/src/main/overleafProtocol.ts b/src/main/overleafProtocol.ts
new file mode 100644
index 0000000..49b06d7
--- /dev/null
+++ b/src/main/overleafProtocol.ts
@@ -0,0 +1,95 @@
+// Socket.IO v0.9 protocol encoding/decoding
+
+export interface ParsedMessage {
+ type: 'disconnect' | 'connect' | 'heartbeat' | 'event' | 'ack' | 'error' | 'noop'
+ id?: number
+ data?: unknown
+ name?: string
+ args?: unknown[]
+}
+
+/**
+ * Parse a Socket.IO v0.9 message frame.
+ *
+ * Frame format:
+ * 0:: disconnect
+ * 1:: connect
+ * 2:: heartbeat
+ * 5:::{"name":"x","args":[...]} event
+ * 5:N+::{"name":"x","args":[...]} event with ack request
+ * 6:::N+[jsonData] ack response
+ * 8:: noop
+ */
+export function parseSocketMessage(raw: string): ParsedMessage | null {
+ if (!raw || raw.length === 0) return null
+
+ const type = raw[0]
+
+ switch (type) {
+ case '0':
+ return { type: 'disconnect' }
+ case '1':
+ return { type: 'connect' }
+ case '2':
+ return { type: 'heartbeat' }
+ case '8':
+ return { type: 'noop' }
+ case '5': {
+ // Event: 5:::{"name":"x","args":[...]} or 5:N+::{"name":"x","args":[...]}
+ const ackMatch = raw.match(/^5:(\d+)\+::(.*)$/s)
+ if (ackMatch) {
+ try {
+ const payload = JSON.parse(ackMatch[2])
+ return {
+ type: 'event',
+ id: parseInt(ackMatch[1]),
+ name: payload.name,
+ args: payload.args || []
+ }
+ } catch {
+ return null
+ }
+ }
+ const evtMatch = raw.match(/^5:::(.*)$/s)
+ if (evtMatch) {
+ try {
+ const payload = JSON.parse(evtMatch[1])
+ return { type: 'event', name: payload.name, args: payload.args || [] }
+ } catch {
+ return null
+ }
+ }
+ return null
+ }
+ case '6': {
+ // Ack: 6:::N+[jsonData]
+ const ackMatch = raw.match(/^6:::(\d+)\+([\s\S]*)/)
+ if (ackMatch) {
+ try {
+ const data = JSON.parse(ackMatch[2])
+ return { type: 'ack', id: parseInt(ackMatch[1]), data }
+ } catch {
+ return { type: 'ack', id: parseInt(ackMatch[1]), data: null }
+ }
+ }
+ return null
+ }
+ default:
+ return null
+ }
+}
+
+/** Encode a Socket.IO v0.9 event (no ack) */
+export function encodeEvent(name: string, args: unknown[]): string {
+ return '5:::' + JSON.stringify({ name, args })
+}
+
+/** Encode a Socket.IO v0.9 event that expects an ack response */
+export function encodeEventWithAck(ackId: number, name: string, args: unknown[]): string {
+ return `5:${ackId}+::` + JSON.stringify({ name, args })
+}
+
+/** Encode a heartbeat response */
+export function encodeHeartbeat(): string {
+ return '2::'
+}