localgreenchain/lib/realtime/SocketContext.tsx
Claude 7098335ce7
Add real-time updates system with Socket.io
Implement Agent 6: Real-Time Updates feature for LocalGreenChain:

- Add Socket.io server with room-based subscriptions
- Create client-side hooks (useSocket, useLiveFeed, usePlantUpdates)
- Add SocketProvider context for application-wide state
- Implement UI components:
  - ConnectionStatus: Shows WebSocket connection state
  - LiveFeed: Real-time event feed display
  - NotificationToast: Toast notifications with auto-dismiss
  - LiveChart: Real-time data visualization
- Add event type definitions and formatting utilities
- Create socket API endpoint for WebSocket initialization
- Add socket stats endpoint for monitoring
- Extend tailwind with fadeIn/slideIn animations

Integrates with existing EventStream SSE system for fallback.
2025-11-23 03:51:51 +00:00

235 lines
6.1 KiB
TypeScript

/**
* 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<boolean>;
leaveRoom: (room: RoomType) => Promise<boolean>;
subscribeToTypes: (types: TransparencyEventType[]) => Promise<boolean>;
clearEvents: () => void;
markNotificationRead: (id: string) => void;
dismissNotification: (id: string) => void;
markAllRead: () => void;
}
const SocketContext = createContext<SocketContextValue | null>(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<ConnectionStatus>('disconnected');
const [events, setEvents] = useState<TransparencyEvent[]>([]);
const [latestEvent, setLatestEvent] = useState<TransparencyEvent | null>(null);
const [notifications, setNotifications] = useState<RealtimeNotification[]>([]);
const [metrics, setMetrics] = useState<ConnectionMetrics>({
status: 'disconnected',
eventsReceived: 0,
reconnectAttempts: 0,
rooms: [],
});
const clientRef = useRef<RealtimeSocketClient | null>(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 (
<SocketContext.Provider value={value}>
{children}
</SocketContext.Provider>
);
}
/**
* 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;