import { useState, useCallback, useRef, useEffect } from "react"; import { FILE_SUBSCRIBE_ENDPOINT } from "../lib/api"; export interface FileUpdateEvent { fileId: string; version: number; updatedFields: string[]; updatedBy: "user" | "llm" | "system"; } interface UseFileSubscriptionOptions { fileId: string | null; onUpdate?: (event: FileUpdateEvent) => void; onError?: (error: string) => void; } export function useFileSubscription(options: UseFileSubscriptionOptions) { const { fileId, onUpdate, onError } = options; const [connected, setConnected] = useState(false); const wsRef = useRef(null); const reconnectTimeoutRef = useRef(null); const subscribedFileRef = useRef(null); // Store callbacks in refs to avoid re-connecting when callbacks change const callbacksRef = useRef({ onUpdate, onError }); useEffect(() => { callbacksRef.current = { onUpdate, onError }; }, [onUpdate, onError]); const connect = useCallback(() => { if (wsRef.current?.readyState === WebSocket.OPEN) return; try { const ws = new WebSocket(FILE_SUBSCRIBE_ENDPOINT); wsRef.current = ws; ws.onopen = () => { setConnected(true); // Re-subscribe if we had a subscription if (subscribedFileRef.current) { ws.send( JSON.stringify({ type: "subscribe", fileId: subscribedFileRef.current, }) ); } }; ws.onmessage = (event) => { try { const message = JSON.parse(event.data); if (message.type === "fileUpdated") { callbacksRef.current.onUpdate?.({ fileId: message.fileId, version: message.version, updatedFields: message.updatedFields, updatedBy: message.updatedBy, }); } else if (message.type === "error") { callbacksRef.current.onError?.(message.message); } } catch (e) { console.error("Failed to parse file subscription message:", e); } }; ws.onerror = () => { callbacksRef.current.onError?.("WebSocket connection error"); }; ws.onclose = () => { setConnected(false); wsRef.current = null; // Attempt reconnection after 3 seconds if we still have a subscription if (subscribedFileRef.current) { reconnectTimeoutRef.current = window.setTimeout(() => { connect(); }, 3000); } }; } catch (e) { callbacksRef.current.onError?.( e instanceof Error ? e.message : "Failed to connect" ); } }, []); const subscribe = useCallback( (id: string) => { subscribedFileRef.current = id; if (wsRef.current?.readyState === WebSocket.OPEN) { wsRef.current.send( JSON.stringify({ type: "subscribe", fileId: id, }) ); } else { connect(); } }, [connect] ); const unsubscribe = useCallback(() => { if ( subscribedFileRef.current && wsRef.current?.readyState === WebSocket.OPEN ) { wsRef.current.send( JSON.stringify({ type: "unsubscribe", fileId: subscribedFileRef.current, }) ); } subscribedFileRef.current = null; }, []); // Auto-subscribe when fileId changes useEffect(() => { if (fileId) { subscribe(fileId); } else { unsubscribe(); } return () => { unsubscribe(); }; }, [fileId, subscribe, unsubscribe]); // Cleanup on unmount useEffect(() => { return () => { if (reconnectTimeoutRef.current) { clearTimeout(reconnectTimeoutRef.current); } if (wsRef.current) { wsRef.current.close(); } }; }, []); return { connected, subscribe, unsubscribe, }; }