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

379 lines
8.8 KiB
TypeScript

/**
* Socket.io Client for LocalGreenChain
*
* Provides a client-side wrapper for Socket.io connections.
*/
import { io, Socket } from 'socket.io-client';
import type {
ClientToServerEvents,
ServerToClientEvents,
ConnectionStatus,
RoomType,
TransparencyEventType,
TransparencyEvent,
ConnectionMetrics,
} from './types';
type TypedSocket = Socket<ServerToClientEvents, ClientToServerEvents>;
/**
* Client configuration options
*/
export interface SocketClientConfig {
url?: string;
path?: string;
autoConnect?: boolean;
auth?: {
userId?: string;
token?: string;
sessionId?: string;
};
reconnection?: boolean;
reconnectionAttempts?: number;
reconnectionDelay?: number;
}
/**
* Event listener types
*/
export type EventListener = (event: TransparencyEvent) => void;
export type StatusListener = (status: ConnectionStatus) => void;
export type ErrorListener = (error: { code: string; message: string }) => void;
/**
* Socket.io client wrapper
*/
class RealtimeSocketClient {
private socket: TypedSocket | null = null;
private config: SocketClientConfig;
private status: ConnectionStatus = 'disconnected';
private eventListeners: Set<EventListener> = new Set();
private statusListeners: Set<StatusListener> = new Set();
private errorListeners: Set<ErrorListener> = new Set();
private metrics: ConnectionMetrics;
private pingInterval: NodeJS.Timeout | null = null;
constructor(config: SocketClientConfig = {}) {
this.config = {
url: typeof window !== 'undefined' ? window.location.origin : '',
path: '/api/socket',
autoConnect: true,
reconnection: true,
reconnectionAttempts: 10,
reconnectionDelay: 1000,
...config,
};
this.metrics = {
status: 'disconnected',
eventsReceived: 0,
reconnectAttempts: 0,
rooms: [],
};
}
/**
* Connect to the server
*/
connect(): void {
if (this.socket?.connected) {
return;
}
this.updateStatus('connecting');
this.socket = io(this.config.url!, {
path: this.config.path,
autoConnect: this.config.autoConnect,
auth: this.config.auth,
reconnection: this.config.reconnection,
reconnectionAttempts: this.config.reconnectionAttempts,
reconnectionDelay: this.config.reconnectionDelay,
transports: ['websocket', 'polling'],
});
this.setupEventHandlers();
}
/**
* Set up socket event handlers
*/
private setupEventHandlers(): void {
if (!this.socket) return;
// Connection events
this.socket.on('connect', () => {
this.updateStatus('connected');
this.metrics.connectedAt = Date.now();
this.metrics.reconnectAttempts = 0;
this.startPingInterval();
});
this.socket.on('disconnect', () => {
this.updateStatus('disconnected');
this.stopPingInterval();
});
this.socket.on('connect_error', () => {
this.updateStatus('error');
this.metrics.reconnectAttempts++;
});
// Server events
this.socket.on('connection:established', (data) => {
console.log('[SocketClient] Connected:', data.socketId);
});
this.socket.on('connection:error', (error) => {
this.errorListeners.forEach((listener) => listener(error));
});
// Real-time events
this.socket.on('event', (event) => {
this.metrics.eventsReceived++;
this.metrics.lastEventAt = Date.now();
this.eventListeners.forEach((listener) => listener(event));
});
this.socket.on('event:batch', (events) => {
this.metrics.eventsReceived += events.length;
this.metrics.lastEventAt = Date.now();
events.forEach((event) => {
this.eventListeners.forEach((listener) => listener(event));
});
});
// Room events
this.socket.on('room:joined', (room) => {
if (!this.metrics.rooms.includes(room)) {
this.metrics.rooms.push(room);
}
});
this.socket.on('room:left', (room) => {
this.metrics.rooms = this.metrics.rooms.filter((r) => r !== room);
});
// System events
this.socket.on('system:message', (message) => {
console.log(`[SocketClient] System ${message.type}: ${message.text}`);
});
this.socket.on('system:heartbeat', () => {
// Heartbeat received - connection is alive
});
// Reconnection events
this.socket.io.on('reconnect_attempt', () => {
this.updateStatus('reconnecting');
this.metrics.reconnectAttempts++;
});
this.socket.io.on('reconnect', () => {
this.updateStatus('connected');
});
}
/**
* Start ping interval for latency measurement
*/
private startPingInterval(): void {
this.stopPingInterval();
this.pingInterval = setInterval(() => {
if (this.socket?.connected) {
const start = Date.now();
this.socket.emit('ping', (serverTime) => {
this.metrics.latency = Date.now() - start;
});
}
}, 10000); // Every 10 seconds
}
/**
* Stop ping interval
*/
private stopPingInterval(): void {
if (this.pingInterval) {
clearInterval(this.pingInterval);
this.pingInterval = null;
}
}
/**
* Update connection status and notify listeners
*/
private updateStatus(status: ConnectionStatus): void {
this.status = status;
this.metrics.status = status;
this.statusListeners.forEach((listener) => listener(status));
}
/**
* Disconnect from the server
*/
disconnect(): void {
this.stopPingInterval();
if (this.socket) {
this.socket.disconnect();
this.socket = null;
}
this.updateStatus('disconnected');
}
/**
* Join a room
*/
joinRoom(room: RoomType): Promise<boolean> {
return new Promise((resolve) => {
if (!this.socket?.connected) {
resolve(false);
return;
}
this.socket.emit('room:join', room, (success) => {
resolve(success);
});
});
}
/**
* Leave a room
*/
leaveRoom(room: RoomType): Promise<boolean> {
return new Promise((resolve) => {
if (!this.socket?.connected) {
resolve(false);
return;
}
this.socket.emit('room:leave', room, (success) => {
resolve(success);
});
});
}
/**
* Subscribe to specific event types
*/
subscribeToTypes(types: TransparencyEventType[]): Promise<boolean> {
return new Promise((resolve) => {
if (!this.socket?.connected) {
resolve(false);
return;
}
this.socket.emit('subscribe:types', types, (success) => {
resolve(success);
});
});
}
/**
* Unsubscribe from specific event types
*/
unsubscribeFromTypes(types: TransparencyEventType[]): Promise<boolean> {
return new Promise((resolve) => {
if (!this.socket?.connected) {
resolve(false);
return;
}
this.socket.emit('unsubscribe:types', types, (success) => {
resolve(success);
});
});
}
/**
* Get recent events
*/
getRecentEvents(limit: number = 50): Promise<TransparencyEvent[]> {
return new Promise((resolve) => {
if (!this.socket?.connected) {
resolve([]);
return;
}
this.socket.emit('events:recent', limit, (events) => {
resolve(events);
});
});
}
/**
* Add an event listener
*/
onEvent(listener: EventListener): () => void {
this.eventListeners.add(listener);
return () => this.eventListeners.delete(listener);
}
/**
* Add a status listener
*/
onStatusChange(listener: StatusListener): () => void {
this.statusListeners.add(listener);
return () => this.statusListeners.delete(listener);
}
/**
* Add an error listener
*/
onError(listener: ErrorListener): () => void {
this.errorListeners.add(listener);
return () => this.errorListeners.delete(listener);
}
/**
* Get current connection status
*/
getStatus(): ConnectionStatus {
return this.status;
}
/**
* Get connection metrics
*/
getMetrics(): ConnectionMetrics {
return { ...this.metrics };
}
/**
* Check if connected
*/
isConnected(): boolean {
return this.socket?.connected ?? false;
}
/**
* Get socket ID
*/
getSocketId(): string | undefined {
return this.socket?.id;
}
}
// Singleton instance for client-side use
let clientInstance: RealtimeSocketClient | null = null;
/**
* Get the singleton socket client instance
*/
export function getSocketClient(config?: SocketClientConfig): RealtimeSocketClient {
if (!clientInstance) {
clientInstance = new RealtimeSocketClient(config);
}
return clientInstance;
}
/**
* Create a new socket client instance
*/
export function createSocketClient(config?: SocketClientConfig): RealtimeSocketClient {
return new RealtimeSocketClient(config);
}
export { RealtimeSocketClient };
export default RealtimeSocketClient;