/** * Socket.io Context Provider for LocalGreenChain * * Provides socket connection state to the entire application. */ import React, { createContext, useContext, useEffect, useState, useCallback, useRef, ReactNode } from 'react'; import { getSocketClient, RealtimeSocketClient } from './socketClient'; import type { ConnectionStatus, TransparencyEvent, RoomType, TransparencyEventType, ConnectionMetrics, RealtimeNotification, } from './types'; import { toFeedItem } from './events'; /** * Socket context value type */ interface SocketContextValue { // Connection state status: ConnectionStatus; isConnected: boolean; metrics: ConnectionMetrics; // Events events: TransparencyEvent[]; latestEvent: TransparencyEvent | null; // Notifications notifications: RealtimeNotification[]; unreadCount: number; // Actions connect: () => void; disconnect: () => void; joinRoom: (room: RoomType) => Promise; leaveRoom: (room: RoomType) => Promise; subscribeToTypes: (types: TransparencyEventType[]) => Promise; clearEvents: () => void; markNotificationRead: (id: string) => void; dismissNotification: (id: string) => void; markAllRead: () => void; } const SocketContext = createContext(null); /** * Provider props */ interface SocketProviderProps { children: ReactNode; userId?: string; autoConnect?: boolean; maxEvents?: number; maxNotifications?: number; } /** * Convert event to notification */ function eventToNotification(event: TransparencyEvent): RealtimeNotification { const feedItem = toFeedItem(event); let notificationType: RealtimeNotification['type'] = 'info'; if (event.priority === 'CRITICAL') notificationType = 'error'; else if (event.priority === 'HIGH') notificationType = 'warning'; else if (event.type.includes('error')) notificationType = 'error'; else if (event.type.includes('completed') || event.type.includes('verified')) notificationType = 'success'; return { id: event.id, type: notificationType, title: feedItem.formatted.title, message: feedItem.formatted.description, timestamp: feedItem.timestamp, eventType: event.type, data: event.data, read: false, dismissed: false, }; } /** * Socket Provider component */ export function SocketProvider({ children, userId, autoConnect = true, maxEvents = 100, maxNotifications = 50, }: SocketProviderProps) { const [status, setStatus] = useState('disconnected'); const [events, setEvents] = useState([]); const [latestEvent, setLatestEvent] = useState(null); const [notifications, setNotifications] = useState([]); const [metrics, setMetrics] = useState({ status: 'disconnected', eventsReceived: 0, reconnectAttempts: 0, rooms: [], }); const clientRef = useRef(null); // 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) => [event, ...prev].slice(0, maxEvents)); setMetrics(client.getMetrics()); // Create notification for important events if (event.priority === 'HIGH' || event.priority === 'CRITICAL') { const notification = eventToNotification(event); setNotifications((prev) => [notification, ...prev].slice(0, maxNotifications)); } }); // Auto connect if (autoConnect) { client.connect(); } // Initial metrics setMetrics(client.getMetrics()); return () => { unsubStatus(); unsubEvent(); }; }, [autoConnect, userId, maxEvents, maxNotifications]); 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 subscribeToTypes = useCallback(async (types: TransparencyEventType[]) => { return clientRef.current?.subscribeToTypes(types) ?? false; }, []); const clearEvents = useCallback(() => { setEvents([]); setLatestEvent(null); }, []); const markNotificationRead = useCallback((id: string) => { setNotifications((prev) => prev.map((n) => (n.id === id ? { ...n, read: true } : n)) ); }, []); const dismissNotification = useCallback((id: string) => { setNotifications((prev) => prev.map((n) => (n.id === id ? { ...n, dismissed: true } : n)) ); }, []); const markAllRead = useCallback(() => { setNotifications((prev) => prev.map((n) => ({ ...n, read: true }))); }, []); const unreadCount = notifications.filter((n) => !n.read && !n.dismissed).length; const value: SocketContextValue = { status, isConnected: status === 'connected', metrics, events, latestEvent, notifications: notifications.filter((n) => !n.dismissed), unreadCount, connect, disconnect, joinRoom, leaveRoom, subscribeToTypes, clearEvents, markNotificationRead, dismissNotification, markAllRead, }; return ( {children} ); } /** * Hook to use socket context */ export function useSocketContext(): SocketContextValue { const context = useContext(SocketContext); if (!context) { throw new Error('useSocketContext must be used within a SocketProvider'); } return context; } /** * Hook to optionally use socket context (returns null if not in provider) */ export function useOptionalSocketContext(): SocketContextValue | null { return useContext(SocketContext); } export default SocketContext;