/** * useYjsBinding – React hook that ties YjsDocument, WebSocketProvider, * and AwarenessManager together for real-time collaboration. */ import { useEffect, useRef, useState, useCallback } from 'react'; import * as Y from 'yjs'; import { YjsDocument } from './YjsDocument'; import { WebSocketProvider, type ConnectionStatus } from './WebSocketProvider'; import { AwarenessManager, type UserCursor, type UserSelection } from './AwarenessManager'; import type { CADElement, CADLayer, BlockDefinition } from '../types/cad.types'; import type { ElementGroup } from '../tools/modification/GroupTool'; import type { BackgroundConfig } from '../services/backgroundService'; export interface UseYjsBindingOptions { docName: string; wsUrl?: string; userId: string; userName: string; userColor: string; enabled?: boolean; token?: string; } export interface UseYjsBindingResult { yjsDoc: YjsDocument | null; provider: WebSocketProvider | null; awareness: AwarenessManager | null; status: ConnectionStatus; elements: CADElement[]; layers: CADLayer[]; blocks: BlockDefinition[]; groups: ElementGroup[]; bgConfigs: BackgroundConfig[]; cursors: UserCursor[]; selections: UserSelection[]; setElement: (el: CADElement) => void; deleteElement: (id: string) => void; setLayer: (layer: CADLayer) => void; deleteLayer: (id: string) => void; setBlock: (block: BlockDefinition) => void; deleteBlock: (id: string) => void; setGroup: (group: ElementGroup) => void; deleteGroup: (id: string) => void; setBgConfig: (id: string, config: BackgroundConfig) => void; deleteBgConfig: (id: string) => void; loadFromState: (state: { elements: CADElement[]; layers: CADLayer[]; blocks: BlockDefinition[]; groups?: ElementGroup[]; bgConfigs?: BackgroundConfig[]; }) => void; setCursor: (x: number, y: number) => void; hideCursor: () => void; setSelection: (elementIds: string[]) => void; clearSelection: () => void; } const DEFAULT_WS_URL = `wss://${window.location.host}`; export function useYjsBinding(opts: UseYjsBindingOptions): UseYjsBindingResult { const { docName, wsUrl = DEFAULT_WS_URL, userId, userName, userColor, enabled = true, token } = opts; // Refs for stable values that don't need to trigger effect re-runs const wsUrlRef = useRef(wsUrl); wsUrlRef.current = wsUrl; const enabledRef = useRef(enabled); enabledRef.current = enabled; const yjsDocRef = useRef(null); const providerRef = useRef(null); const awarenessRef = useRef(null); const unbindRef = useRef<(() => void) | null>(null); const [status, setStatus] = useState('disconnected'); const [elements, setElements] = useState([]); const [layers, setLayers] = useState([]); const [blocks, setBlocks] = useState([]); const [groups, setGroups] = useState([]); const [bgConfigs, setBgConfigs] = useState([]); const [cursors, setCursors] = useState([]); const [selections, setSelections] = useState([]); // Initialize Yjs document, provider, and awareness useEffect(() => { const currentWsUrl = wsUrlRef.current; const currentEnabled = enabledRef.current; if (!currentEnabled || !docName) return; const doc = new YjsDocument(); yjsDocRef.current = doc; const awareness = new AwarenessManager(doc, userId); awarenessRef.current = awareness; const provider = new WebSocketProvider({ url: currentWsUrl, docName, yjsDoc: doc, awareness, onStatusChange: (s) => setStatus(s), }); providerRef.current = provider; // Sync local state from Y.Doc on changes const syncState = () => { setElements(doc.getElements()); setLayers(doc.getLayers()); setBlocks(doc.getBlocks()); setGroups(doc.getGroups()); setBgConfigs(doc.getBgConfigs()); setCursors(awareness.getAllCursors()); setSelections(awareness.getAllSelections()); }; const unbindDoc = doc.onChange(syncState); const unbindAwareness = awareness.onChange(syncState); // Dedicated observers for groups and bgConfig maps const groupsMap = doc.data.groups; const bgConfigMap = doc.data.bgConfig; const groupsObserver = () => { setGroups(Array.from(groupsMap.values())); }; const bgConfigObserver = () => { setBgConfigs(Array.from(bgConfigMap.values())); }; groupsMap.observe(groupsObserver); bgConfigMap.observe(bgConfigObserver); // Forward local updates to server const unbindLocal = provider.bindLocalUpdates(); // Forward local awareness updates to server const unbindAwarenessUpdates = provider.bindAwarenessUpdates(); // Connect provider.connect(token); syncState(); // Cleanup return () => { groupsMap.unobserve(groupsObserver); bgConfigMap.unobserve(bgConfigObserver); unbindDoc(); unbindAwareness(); unbindLocal(); unbindAwarenessUpdates(); awareness.removeSelf(); awareness.destroy(); provider.disconnect(); doc.destroy(); yjsDocRef.current = null; providerRef.current = null; awarenessRef.current = null; }; }, [docName, userId, token]); // Mutators const setElement = useCallback((el: CADElement) => { yjsDocRef.current?.setElement(el); }, []); const deleteElement = useCallback((id: string) => { yjsDocRef.current?.deleteElement(id); }, []); const setLayer = useCallback((layer: CADLayer) => { yjsDocRef.current?.setLayer(layer); }, []); const deleteLayer = useCallback((id: string) => { yjsDocRef.current?.deleteLayer(id); }, []); const setBlock = useCallback((block: BlockDefinition) => { yjsDocRef.current?.setBlock(block); }, []); const deleteBlock = useCallback((id: string) => { yjsDocRef.current?.deleteBlock(id); }, []); const setGroup = useCallback((group: ElementGroup) => { yjsDocRef.current?.setGroup(group); }, []); const deleteGroup = useCallback((id: string) => { yjsDocRef.current?.deleteGroup(id); }, []); const setBgConfig = useCallback((id: string, config: BackgroundConfig) => { yjsDocRef.current?.setBgConfig(id, config); }, []); const deleteBgConfig = useCallback((id: string) => { yjsDocRef.current?.deleteBgConfig(id); }, []); const loadFromState = useCallback((state: { elements: CADElement[]; layers: CADLayer[]; blocks: BlockDefinition[]; groups?: ElementGroup[]; bgConfigs?: BackgroundConfig[]; }) => { yjsDocRef.current?.loadFromState(state); }, []); const setCursor = useCallback((x: number, y: number) => { awarenessRef.current?.setCursor(x, y, userName, userColor); }, [userName, userColor]); const hideCursor = useCallback(() => { awarenessRef.current?.hideCursor(); }, []); const setSelection = useCallback((elementIds: string[]) => { awarenessRef.current?.setSelection(elementIds); }, []); const clearSelection = useCallback(() => { awarenessRef.current?.clearSelection(); }, []); return { yjsDoc: yjsDocRef.current, provider: providerRef.current, awareness: awarenessRef.current, status, elements, layers, blocks, groups, bgConfigs, cursors, selections, setElement, deleteElement, setLayer, deleteLayer, setBlock, deleteBlock, setGroup, deleteGroup, setBgConfig, deleteBgConfig, loadFromState, setCursor, hideCursor, setSelection, clearSelection, }; }