/** * React Hook for Socket.io Real-Time Updates * * Provides easy-to-use hooks for real-time data in React components. */ import { useState, useEffect, useCallback, useRef } from 'react'; import { getSocketClient, RealtimeSocketClient } from './socketClient'; import type { ConnectionStatus, TransparencyEvent, RoomType, TransparencyEventType, ConnectionMetrics, LiveFeedItem, } from './types'; import { toFeedItem } from './events'; /** * Hook configuration options */ export interface UseSocketOptions { autoConnect?: boolean; rooms?: RoomType[]; eventTypes?: TransparencyEventType[]; userId?: string; maxEvents?: number; } /** * Hook return type */ export interface UseSocketReturn { status: ConnectionStatus; isConnected: boolean; events: TransparencyEvent[]; latestEvent: TransparencyEvent | null; metrics: ConnectionMetrics; connect: () => void; disconnect: () => void; joinRoom: (room: RoomType) => Promise; leaveRoom: (room: RoomType) => Promise; clearEvents: () => void; } /** * Main socket hook for real-time updates */ export function useSocket(options: UseSocketOptions = {}): UseSocketReturn { const { autoConnect = true, rooms = [], eventTypes = [], userId, maxEvents = 100, } = options; const [status, setStatus] = useState('disconnected'); const [events, setEvents] = useState([]); const [latestEvent, setLatestEvent] = useState(null); const [metrics, setMetrics] = useState({ status: 'disconnected', eventsReceived: 0, reconnectAttempts: 0, rooms: [], }); const clientRef = useRef(null); const cleanupRef = useRef<(() => void)[]>([]); // Initialize client useEffect(() => { if (typeof window === 'undefined') return; const client = getSocketClient({ auth: { userId } }); clientRef.current = client; // Set up listeners const unsubStatus = client.onStatusChange((newStatus) => { setStatus(newStatus); setMetrics(client.getMetrics()); }); const unsubEvent = client.onEvent((event) => { setLatestEvent(event); setEvents((prev) => { const updated = [event, ...prev]; return updated.slice(0, maxEvents); }); setMetrics(client.getMetrics()); }); cleanupRef.current = [unsubStatus, unsubEvent]; // Auto connect if (autoConnect) { client.connect(); } // Initial metrics setMetrics(client.getMetrics()); return () => { cleanupRef.current.forEach((cleanup) => cleanup()); }; }, [autoConnect, userId, maxEvents]); // Join initial rooms useEffect(() => { if (!clientRef.current || status !== 'connected') return; rooms.forEach((room) => { clientRef.current?.joinRoom(room); }); }, [status, rooms]); // Subscribe to event types useEffect(() => { if (!clientRef.current || status !== 'connected' || eventTypes.length === 0) return; clientRef.current.subscribeToTypes(eventTypes); }, [status, eventTypes]); const connect = useCallback(() => { clientRef.current?.connect(); }, []); const disconnect = useCallback(() => { clientRef.current?.disconnect(); }, []); const joinRoom = useCallback(async (room: RoomType) => { return clientRef.current?.joinRoom(room) ?? false; }, []); const leaveRoom = useCallback(async (room: RoomType) => { return clientRef.current?.leaveRoom(room) ?? false; }, []); const clearEvents = useCallback(() => { setEvents([]); setLatestEvent(null); }, []); return { status, isConnected: status === 'connected', events, latestEvent, metrics, connect, disconnect, joinRoom, leaveRoom, clearEvents, }; } /** * Hook for live feed display */ export function useLiveFeed(options: UseSocketOptions = {}): { items: LiveFeedItem[]; isConnected: boolean; status: ConnectionStatus; clearFeed: () => void; } { const { events, isConnected, status, clearEvents } = useSocket(options); const items = events.map((event) => toFeedItem(event)); return { items, isConnected, status, clearFeed: clearEvents, }; } /** * Hook for tracking a specific plant's real-time updates */ export function usePlantUpdates(plantId: string): { events: TransparencyEvent[]; isConnected: boolean; } { return useSocket({ rooms: [`plant:${plantId}` as RoomType], eventTypes: [ 'plant.registered', 'plant.cloned', 'plant.transferred', 'plant.updated', 'transport.started', 'transport.completed', ], }) as { events: TransparencyEvent[]; isConnected: boolean }; } /** * Hook for tracking a specific farm's real-time updates */ export function useFarmUpdates(farmId: string): { events: TransparencyEvent[]; isConnected: boolean; } { return useSocket({ rooms: [`farm:${farmId}` as RoomType], eventTypes: [ 'farm.registered', 'farm.updated', 'batch.started', 'batch.harvested', 'agent.alert', ], }) as { events: TransparencyEvent[]; isConnected: boolean }; } /** * Hook for connection status only (lightweight) */ export function useConnectionStatus(): { status: ConnectionStatus; isConnected: boolean; latency: number | undefined; } { const { status, isConnected, metrics } = useSocket({ autoConnect: true }); return { status, isConnected, latency: metrics.latency, }; } /** * Hook for event counts (useful for notification badges) */ export function useEventCount(options: UseSocketOptions = {}): { count: number; unreadCount: number; markAllRead: () => void; } { const { events } = useSocket(options); const [readCount, setReadCount] = useState(0); const markAllRead = useCallback(() => { setReadCount(events.length); }, [events.length]); return { count: events.length, unreadCount: Math.max(0, events.length - readCount), markAllRead, }; } export default useSocket;