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/overleafProtocol.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/overleafProtocol.ts')
| -rw-r--r-- | src/main/overleafProtocol.ts | 95 |
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::' +} |
