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

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;
}