'use client'; import React, { createContext, useContext, useEffect, useState, useCallback, useRef } from 'react'; import { HubConnection } from '@microsoft/signalr'; import { SignalRConnectionManager, ConnectionState } from './ConnectionManager'; import { SIGNALR_CONFIG } from './config'; import { useAuthStore } from '@/stores/authStore'; import { toast } from 'sonner'; import { logger } from '@/lib/utils/logger'; // ============================================ // TYPE DEFINITIONS // ============================================ interface SignalRContextValue { // Connection management connection: HubConnection | null; connectionState: ConnectionState; isConnected: boolean; // Event handlers registry service: SignalREventService | null; // Manual connection control (optional) connect: () => Promise; disconnect: () => Promise; } // Event subscription service interface SignalREventService { subscribe: (eventName: string, handler: (...args: any[]) => void) => () => void; unsubscribe: (eventName: string, handler?: (...args: any[]) => void) => void; getEventHandlers: () => SignalREventService | null; } // ============================================ // CONTEXT CREATION // ============================================ const SignalRContext = createContext(null); // ============================================ // PROVIDER COMPONENT // ============================================ interface SignalRProviderProps { children: React.ReactNode; hubUrl?: string; // Optional: custom hub URL (defaults to PROJECT hub) autoConnect?: boolean; // Auto-connect when authenticated (default: true) showToasts?: boolean; // Show connection status toasts (default: true) } export function SignalRProvider({ children, hubUrl = SIGNALR_CONFIG.HUB_URLS.PROJECT, autoConnect = true, showToasts = true, }: SignalRProviderProps) { const isAuthenticated = useAuthStore((state) => state.isAuthenticated); const [connectionState, setConnectionState] = useState('disconnected'); const managerRef = useRef(null); const eventHandlersRef = useRef void>>>(new Map()); // ============================================ // EVENT SERVICE IMPLEMENTATION // ============================================ const eventService: SignalREventService = { subscribe: (eventName: string, handler: (...args: any[]) => void) => { // Register handler if (!eventHandlersRef.current.has(eventName)) { eventHandlersRef.current.set(eventName, new Set()); } eventHandlersRef.current.get(eventName)?.add(handler); // Subscribe to SignalR event if (managerRef.current) { managerRef.current.on(eventName, handler); } // Return unsubscribe function return () => { eventHandlersRef.current.get(eventName)?.delete(handler); if (managerRef.current) { managerRef.current.off(eventName, handler); } }; }, unsubscribe: (eventName: string, handler?: (...args: any[]) => void) => { if (handler) { eventHandlersRef.current.get(eventName)?.delete(handler); if (managerRef.current) { managerRef.current.off(eventName, handler); } } else { // Unsubscribe all handlers for this event eventHandlersRef.current.delete(eventName); if (managerRef.current) { managerRef.current.off(eventName); } } }, getEventHandlers: () => eventService, }; // ============================================ // CONNECTION MANAGEMENT // ============================================ const connect = useCallback(async () => { if (!isAuthenticated) { logger.warn('[SignalRContext] Cannot connect: user not authenticated'); return; } if (managerRef.current?.state === 'connected') { logger.debug('[SignalRContext] Already connected'); return; } const manager = new SignalRConnectionManager(hubUrl); managerRef.current = manager; // Subscribe to state changes manager.onStateChange((state) => { setConnectionState(state); // Show toast notifications if (showToasts) { if (state === 'connected') { toast.success('Connected to real-time updates'); } else if (state === 'disconnected') { toast.error('Disconnected from real-time updates'); } else if (state === 'reconnecting') { toast.info('Reconnecting...'); } } }); // Re-subscribe all registered event handlers eventHandlersRef.current.forEach((handlers, eventName) => { handlers.forEach((handler) => { manager.on(eventName, handler); }); }); try { await manager.start(); } catch (error) { logger.error('[SignalRContext] Connection error:', error); if (showToasts) { toast.error('Failed to connect to real-time updates'); } } }, [isAuthenticated, hubUrl, showToasts]); const disconnect = useCallback(async () => { if (managerRef.current) { await managerRef.current.stop(); managerRef.current = null; } }, []); // ============================================ // AUTO-CONNECT ON AUTHENTICATION // ============================================ useEffect(() => { if (autoConnect && isAuthenticated) { connect(); } else if (!isAuthenticated) { disconnect(); } return () => { disconnect(); }; }, [isAuthenticated, autoConnect, connect, disconnect]); // ============================================ // CONTEXT VALUE // ============================================ const contextValue: SignalRContextValue = { connection: managerRef.current?.['connection'] ?? null, connectionState, isConnected: connectionState === 'connected', service: eventService, connect, disconnect, }; return ( {children} ); } // ============================================ // CUSTOM HOOKS // ============================================ /** * Access SignalR context */ export function useSignalRContext(): SignalRContextValue { const context = useContext(SignalRContext); if (!context) { throw new Error('useSignalRContext must be used within SignalRProvider'); } return context; } /** * Subscribe to a specific SignalR event (simplified hook) * * @example * useSignalREvent('TaskStatusChanged', (taskId, newStatus) => { * console.log('Task status changed:', taskId, newStatus); * }); */ export function useSignalREvent( eventName: string, handler: (...args: any[]) => void, deps: React.DependencyList = [] ) { const { service, isConnected } = useSignalRContext(); useEffect(() => { if (!isConnected || !service) { return; } const unsubscribe = service.subscribe(eventName, handler); return () => { unsubscribe(); }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [eventName, isConnected, service, ...deps]); } /** * Subscribe to multiple SignalR events * * @example * useSignalREvents({ * 'TaskCreated': (task) => console.log('Task created:', task), * 'TaskUpdated': (task) => console.log('Task updated:', task), * }); */ export function useSignalREvents( events: Record void>, deps: React.DependencyList = [] ) { const { service, isConnected } = useSignalRContext(); useEffect(() => { if (!isConnected || !service) { return; } const unsubscribers = Object.entries(events).map(([eventName, handler]) => service.subscribe(eventName, handler) ); return () => { unsubscribers.forEach((unsubscribe) => unsubscribe()); }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [isConnected, service, ...deps]); } /** * Get connection state and status */ export function useSignalRConnection() { const { connectionState, isConnected, connect, disconnect } = useSignalRContext(); return { connectionState, isConnected, connect, disconnect, }; }