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.
137 lines
3.3 KiB
TypeScript
137 lines
3.3 KiB
TypeScript
/**
|
|
* Room Management for Socket.io
|
|
*
|
|
* Manages room subscriptions for targeted event delivery.
|
|
*/
|
|
|
|
import type { RoomType, TransparencyEventType } from './types';
|
|
import { EventCategory, getEventCategory } from './events';
|
|
|
|
/**
|
|
* Parse a room type to extract its components
|
|
*/
|
|
export function parseRoom(room: RoomType): { type: string; id?: string } {
|
|
if (room.includes(':')) {
|
|
const [type, id] = room.split(':');
|
|
return { type, id };
|
|
}
|
|
return { type: room };
|
|
}
|
|
|
|
/**
|
|
* Create a room name for a specific entity
|
|
*/
|
|
export function createRoom(type: 'plant' | 'farm' | 'user', id: string): RoomType {
|
|
return `${type}:${id}` as RoomType;
|
|
}
|
|
|
|
/**
|
|
* Get the default rooms for a user based on their role
|
|
*/
|
|
export function getDefaultRooms(userId?: string): RoomType[] {
|
|
const rooms: RoomType[] = ['global'];
|
|
if (userId) {
|
|
rooms.push(`user:${userId}` as RoomType);
|
|
}
|
|
return rooms;
|
|
}
|
|
|
|
/**
|
|
* Get category-based room for an event type
|
|
*/
|
|
export function getCategoryRoom(type: TransparencyEventType): RoomType {
|
|
const category = getEventCategory(type);
|
|
|
|
switch (category) {
|
|
case EventCategory.PLANT:
|
|
return 'plants';
|
|
case EventCategory.TRANSPORT:
|
|
return 'transport';
|
|
case EventCategory.FARM:
|
|
return 'farms';
|
|
case EventCategory.DEMAND:
|
|
return 'demand';
|
|
case EventCategory.SYSTEM:
|
|
case EventCategory.AGENT:
|
|
case EventCategory.BLOCKCHAIN:
|
|
case EventCategory.AUDIT:
|
|
return 'system';
|
|
default:
|
|
return 'global';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Determine which rooms should receive an event
|
|
*/
|
|
export function getEventRooms(
|
|
type: TransparencyEventType,
|
|
data: Record<string, unknown>
|
|
): RoomType[] {
|
|
const rooms: RoomType[] = ['global'];
|
|
|
|
// Add category room
|
|
const categoryRoom = getCategoryRoom(type);
|
|
if (categoryRoom !== 'global') {
|
|
rooms.push(categoryRoom);
|
|
}
|
|
|
|
// Add entity-specific rooms
|
|
if (data.plantId && typeof data.plantId === 'string') {
|
|
rooms.push(`plant:${data.plantId}` as RoomType);
|
|
}
|
|
if (data.farmId && typeof data.farmId === 'string') {
|
|
rooms.push(`farm:${data.farmId}` as RoomType);
|
|
}
|
|
if (data.userId && typeof data.userId === 'string') {
|
|
rooms.push(`user:${data.userId}` as RoomType);
|
|
}
|
|
|
|
return rooms;
|
|
}
|
|
|
|
/**
|
|
* Check if a room is valid
|
|
*/
|
|
export function isValidRoom(room: string): room is RoomType {
|
|
const validPrefixes = ['global', 'plants', 'transport', 'farms', 'demand', 'system'];
|
|
const validEntityPrefixes = ['plant:', 'farm:', 'user:'];
|
|
|
|
if (validPrefixes.includes(room)) {
|
|
return true;
|
|
}
|
|
|
|
return validEntityPrefixes.some((prefix) => room.startsWith(prefix));
|
|
}
|
|
|
|
/**
|
|
* Room subscription limits per connection
|
|
*/
|
|
export const ROOM_LIMITS = {
|
|
maxRooms: 50,
|
|
maxEntityRooms: 20,
|
|
maxGlobalRooms: 10,
|
|
};
|
|
|
|
/**
|
|
* Check if a connection can join another room
|
|
*/
|
|
export function canJoinRoom(currentRooms: RoomType[], newRoom: RoomType): boolean {
|
|
if (currentRooms.length >= ROOM_LIMITS.maxRooms) {
|
|
return false;
|
|
}
|
|
|
|
const parsed = parseRoom(newRoom);
|
|
const entityRooms = currentRooms.filter((r) => r.includes(':')).length;
|
|
const globalRooms = currentRooms.filter((r) => !r.includes(':')).length;
|
|
|
|
if (parsed.id && entityRooms >= ROOM_LIMITS.maxEntityRooms) {
|
|
return false;
|
|
}
|
|
|
|
if (!parsed.id && globalRooms >= ROOM_LIMITS.maxGlobalRooms) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|