/** * Task Socket Hook - Simplified real-time task synchronization * * This hook provides a clean interface to the task socket service, * replacing the complex useOptimisticUpdates pattern with a simpler * approach that avoids conflicts and connection issues. */ import { useEffect, useRef, useCallback, useState } from 'react'; import { taskSocketService, TaskSocketEvents } from '../services/taskSocketService'; import { WebSocketState } from '../services/socketIOService'; export interface UseTaskSocketOptions { projectId: string; onTaskCreated?: (task: any) => void; onTaskUpdated?: (task: any) => void; onTaskDeleted?: (task: any) => void; onTaskArchived?: (task: any) => void; onTasksReordered?: (data: any) => void; onInitialTasks?: (tasks: any[]) => void; onConnectionStateChange?: (state: WebSocketState) => void; } export function useTaskSocket(options: UseTaskSocketOptions) { const { projectId, onTaskCreated, onTaskUpdated, onTaskDeleted, onTaskArchived, onTasksReordered, onInitialTasks, onConnectionStateChange } = options; const componentIdRef = useRef(`task-socket-${Math.random().toString(36).substring(7)}`); const currentProjectIdRef = useRef(null); const isInitializedRef = useRef(false); // Add reactive state for connection status const [isConnected, setIsConnected] = useState(false); const [connectionState, setConnectionState] = useState(WebSocketState.DISCONNECTED); // Memoized handlers to prevent unnecessary re-registrations const memoizedHandlers = useCallback((): Partial => { return { onTaskCreated, onTaskUpdated, onTaskDeleted, onTaskArchived, onTasksReordered, onInitialTasks, onConnectionStateChange }; }, [ onTaskCreated, onTaskUpdated, onTaskDeleted, onTaskArchived, onTasksReordered, onInitialTasks, onConnectionStateChange ]); // Subscribe to connection state changes useEffect(() => { const checkConnection = () => { const connected = taskSocketService.isConnected(); const state = taskSocketService.getConnectionState(); setIsConnected(connected); setConnectionState(state); }; // Check initial state checkConnection(); // Poll for connection state changes (since the service doesn't expose event emitters) const interval = setInterval(checkConnection, 500); // Also trigger when connection state handler is called const wrappedOnConnectionStateChange = onConnectionStateChange ? (state: WebSocketState) => { setConnectionState(state); setIsConnected(state === WebSocketState.CONNECTED); onConnectionStateChange(state); } : (state: WebSocketState) => { setConnectionState(state); setIsConnected(state === WebSocketState.CONNECTED); }; // Update the handler if (componentIdRef.current && taskSocketService) { taskSocketService.registerHandlers(componentIdRef.current, { ...memoizedHandlers(), onConnectionStateChange: wrappedOnConnectionStateChange }); } return () => { clearInterval(interval); }; }, [onConnectionStateChange, memoizedHandlers]); // Initialize connection once and register handlers useEffect(() => { if (!projectId || isInitializedRef.current) return; const initializeConnection = async () => { try { console.log(`[USE_TASK_SOCKET] Initializing connection for project: ${projectId}`); setConnectionState(WebSocketState.CONNECTING); // Register handlers first taskSocketService.registerHandlers(componentIdRef.current, memoizedHandlers()); // Connect to project (singleton service will handle deduplication) await taskSocketService.connectToProject(projectId); currentProjectIdRef.current = projectId; isInitializedRef.current = true; console.log(`[USE_TASK_SOCKET] Successfully initialized for project: ${projectId}`); // Update connection state after successful connection setIsConnected(taskSocketService.isConnected()); setConnectionState(taskSocketService.getConnectionState()); } catch (error) { console.error(`[USE_TASK_SOCKET] Failed to initialize for project ${projectId}:`, error); setConnectionState(WebSocketState.DISCONNECTED); setIsConnected(false); } }; initializeConnection(); }, [projectId, memoizedHandlers]); // Update handlers when they change (without reconnecting) useEffect(() => { if (isInitializedRef.current && currentProjectIdRef.current === projectId) { console.log(`[USE_TASK_SOCKET] Updating handlers for component: ${componentIdRef.current}`); taskSocketService.registerHandlers(componentIdRef.current, memoizedHandlers()); } }, [memoizedHandlers, projectId]); // Handle project change (different project) useEffect(() => { if (!projectId) return; // If project changed, reconnect if (isInitializedRef.current && currentProjectIdRef.current !== projectId) { console.log(`[USE_TASK_SOCKET] Project changed from ${currentProjectIdRef.current} to ${projectId}`); const switchProject = async () => { try { setConnectionState(WebSocketState.CONNECTING); // Update handlers for new project taskSocketService.registerHandlers(componentIdRef.current, memoizedHandlers()); // Connect to new project (service handles disconnecting from old) await taskSocketService.connectToProject(projectId); currentProjectIdRef.current = projectId; console.log(`[USE_TASK_SOCKET] Successfully switched to project: ${projectId}`); // Update connection state setIsConnected(taskSocketService.isConnected()); setConnectionState(taskSocketService.getConnectionState()); } catch (error) { console.error(`[USE_TASK_SOCKET] Failed to switch to project ${projectId}:`, error); setConnectionState(WebSocketState.DISCONNECTED); setIsConnected(false); } }; switchProject(); } }, [projectId, memoizedHandlers]); // Cleanup on unmount useEffect(() => { const componentId = componentIdRef.current; return () => { console.log(`[USE_TASK_SOCKET] Cleaning up component: ${componentId}`); taskSocketService.unregisterHandlers(componentId); isInitializedRef.current = false; }; }, []); // Return reactive state and utility functions return { isConnected, // Now reactive! connectionState, // Now reactive! reconnect: taskSocketService.reconnect.bind(taskSocketService), getCurrentProjectId: taskSocketService.getCurrentProjectId.bind(taskSocketService) }; }