import { useCallback, useEffect, useState } from "react"; export interface EditorSyncOptions { /** WebSocket URL for real-time sync */ wsUrl?: string; /** Document ID for collaborative editing */ documentId?: string; /** User ID */ userId?: string; /** Enable offline support */ enableOffline?: boolean; /** Sync interval in ms */ syncInterval?: number; } export interface EditorSyncState { isConnected: boolean; isOnline: boolean; lastSync: Date | null; pendingChanges: number; collaborators: Collaborator[]; } export interface Collaborator { id: string; name: string; color: string; cursor?: { line: number; column: number }; } export const useEditorSync = ({ wsUrl, documentId, userId, enableOffline = true, syncInterval: _syncInterval = 5000, }: EditorSyncOptions = {}) => { const [isConnected, setIsConnected] = useState(false); const [isOnline, setIsOnline] = useState(navigator.onLine); const [lastSync, setLastSync] = useState(null); const [pendingChanges, setPendingChanges] = useState(0); const [collaborators, setCollaborators] = useState([]); const [ws, setWs] = useState(null); // Monitor online/offline status useEffect(() => { const handleOnline = () => setIsOnline(true); const handleOffline = () => setIsOnline(false); window.addEventListener("online", handleOnline); window.addEventListener("offline", handleOffline); return () => { window.removeEventListener("online", handleOnline); window.removeEventListener("offline", handleOffline); }; }, []); // WebSocket connection useEffect(() => { if (!wsUrl || !documentId) return; const websocket = new WebSocket(wsUrl); websocket.onopen = () => { setIsConnected(true); websocket.send( JSON.stringify({ type: "join", documentId, userId, }), ); }; websocket.onmessage = (event) => { const message = JSON.parse(event.data); switch (message.type) { case "collaborators": setCollaborators(message.data); break; case "sync": setLastSync(new Date()); setPendingChanges(0); break; } }; websocket.onclose = () => { setIsConnected(false); }; websocket.onerror = (error) => { console.error("WebSocket error:", error); setIsConnected(false); }; setWs(websocket); return () => { websocket.close(); }; }, [wsUrl, documentId, userId]); const sendChange = useCallback( (change: any) => { if (!ws || ws.readyState !== WebSocket.OPEN) { if (enableOffline) { setPendingChanges((prev) => prev + 1); // Store in localStorage for offline support const pending = JSON.parse( localStorage.getItem("pendingChanges") || "[]", ); pending.push(change); localStorage.setItem("pendingChanges", JSON.stringify(pending)); } return; } ws.send( JSON.stringify({ type: "change", documentId, userId, change, }), ); }, [ws, documentId, userId, enableOffline], ); const syncPendingChanges = useCallback(async () => { if (!enableOffline || !ws || ws.readyState !== WebSocket.OPEN) return; const pending = JSON.parse(localStorage.getItem("pendingChanges") || "[]"); if (pending.length === 0) return; for (const change of pending) { ws.send( JSON.stringify({ type: "change", documentId, userId, change, }), ); } localStorage.removeItem("pendingChanges"); setPendingChanges(0); }, [ws, documentId, userId, enableOffline]); // Sync pending changes when coming back online useEffect(() => { if (isOnline && isConnected && pendingChanges > 0) { syncPendingChanges(); } }, [isOnline, isConnected, pendingChanges, syncPendingChanges]); return { isConnected, isOnline, lastSync, pendingChanges, collaborators, sendChange, syncPendingChanges, }; };