localgreenchain/lib/realtime/useSocket.ts
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

258 lines
5.9 KiB
TypeScript

/**
* 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<boolean>;
leaveRoom: (room: RoomType) => Promise<boolean>;
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<ConnectionStatus>('disconnected');
const [events, setEvents] = useState<TransparencyEvent[]>([]);
const [latestEvent, setLatestEvent] = useState<TransparencyEvent | null>(null);
const [metrics, setMetrics] = useState<ConnectionMetrics>({
status: 'disconnected',
eventsReceived: 0,
reconnectAttempts: 0,
rooms: [],
});
const clientRef = useRef<RealtimeSocketClient | null>(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;