UNPKG

create-fluxstack

Version:

โšก Revolutionary full-stack TypeScript framework with Declarative Config System, Elysia + React + Bun

399 lines (335 loc) โ€ข 12.7 kB
// ๐Ÿ”ฅ FluxStack Live Components - Component Registry import type { LiveComponent, LiveMessage, BroadcastMessage, ComponentDefinition, WebSocketData } from '../types/types' export class ComponentRegistry { private components = new Map<string, LiveComponent>() private definitions = new Map<string, ComponentDefinition>() private rooms = new Map<string, Set<string>>() // roomId -> componentIds private wsConnections = new Map<string, any>() // componentId -> websocket private autoDiscoveredComponents = new Map<string, any>() // Auto-discovered component classes // Register component definition registerComponent<TState>(definition: ComponentDefinition<TState>) { this.definitions.set(definition.name, definition) console.log(`๐Ÿ“ Registered component: ${definition.name}`) } // Register component class dynamically registerComponentClass(name: string, componentClass: any) { this.autoDiscoveredComponents.set(name, componentClass) console.log(`๐Ÿ” Auto-discovered component: ${name}`) } // Auto-discover components from directory async autoDiscoverComponents(componentsPath: string) { try { const fs = await import('fs') const path = await import('path') if (!fs.existsSync(componentsPath)) { console.log(`โš ๏ธ Components path not found: ${componentsPath}`) return } const files = fs.readdirSync(componentsPath) for (const file of files) { if (file.endsWith('.ts') || file.endsWith('.js')) { try { const fullPath = path.join(componentsPath, file) const module = await import(/* @vite-ignore */ fullPath) // Look for exported classes that extend LiveComponent Object.keys(module).forEach(exportName => { const exportedItem = module[exportName] if (typeof exportedItem === 'function' && exportedItem.prototype && this.isLiveComponentClass(exportedItem)) { const componentName = exportName.replace(/Component$/, '') this.registerComponentClass(componentName, exportedItem) } }) } catch (error) { console.warn(`โš ๏ธ Failed to load component from ${file}:`, error) } } } } catch (error) { console.error('โŒ Auto-discovery failed:', error) } } // Check if a class extends LiveComponent private isLiveComponentClass(cls: any): boolean { try { let prototype = cls.prototype while (prototype) { if (prototype.constructor.name === 'LiveComponent') { return true } prototype = Object.getPrototypeOf(prototype) } return false } catch { return false } } // Mount component instance async mountComponent( ws: any, componentName: string, props: any = {}, options?: { room?: string; userId?: string } ): Promise<string> { // Try to find component definition first let definition = this.definitions.get(componentName) let ComponentClass: any = null let initialState: any = {} if (definition) { // Use registered definition ComponentClass = definition.component initialState = definition.initialState } else { // Try auto-discovered components ComponentClass = this.autoDiscoveredComponents.get(componentName) if (!ComponentClass) { // Try variations of the name const variations = [ componentName + 'Component', componentName.charAt(0).toUpperCase() + componentName.slice(1) + 'Component', componentName.charAt(0).toUpperCase() + componentName.slice(1) ] for (const variation of variations) { ComponentClass = this.autoDiscoveredComponents.get(variation) if (ComponentClass) break } } if (!ComponentClass) { const availableComponents = [ ...Array.from(this.definitions.keys()), ...Array.from(this.autoDiscoveredComponents.keys()) ] throw new Error(`Component '${componentName}' not found. Available: ${availableComponents.join(', ')}`) } // Create a default initial state for auto-discovered components initialState = {} } // Create component instance with registry methods const component = new ComponentClass( { ...initialState, ...props }, ws, options ) // Inject registry methods component.broadcastToRoom = (message: BroadcastMessage) => { this.broadcastToRoom(message, component.id) } // Store component and connection this.components.set(component.id, component) this.wsConnections.set(component.id, ws) // Subscribe to room if specified if (options?.room) { this.subscribeToRoom(component.id, options.room) } // Initialize WebSocket data if needed if (!ws.data) { ws.data = { components: new Map(), subscriptions: new Set(), userId: options?.userId } as WebSocketData } ws.data.components.set(component.id, component) console.log(`๐Ÿš€ Mounted component: ${componentName} (${component.id})`) // Send initial state to client component.emit('STATE_UPDATE', { state: component.getSerializableState() }) return component.id } // Unmount component async unmountComponent(componentId: string) { const component = this.components.get(componentId) if (!component) return // Cleanup component.destroy() // Remove from room subscriptions this.unsubscribeFromAllRooms(componentId) // Remove from maps this.components.delete(componentId) this.wsConnections.delete(componentId) console.log(`๐Ÿ—‘๏ธ Unmounted component: ${componentId}`) } // Execute action on component async executeAction(componentId: string, action: string, payload: any): Promise<any> { const component = this.components.get(componentId) if (!component) { throw new Error(`Component '${componentId}' not found`) } try { return await component.executeAction(action, payload) } catch (error: any) { console.error(`โŒ Error executing action '${action}' on component '${componentId}':`, error.message) throw error } } // Update component property updateProperty(componentId: string, property: string, value: any) { const component = this.components.get(componentId) if (!component) { throw new Error(`Component '${componentId}' not found`) } // Update state const updates = { [property]: value } component.setState(updates) console.log(`๐Ÿ“ Updated property '${property}' on component '${componentId}'`) } // Subscribe component to room subscribeToRoom(componentId: string, roomId: string) { if (!this.rooms.has(roomId)) { this.rooms.set(roomId, new Set()) } this.rooms.get(roomId)!.add(componentId) console.log(`๐Ÿ“ก Component '${componentId}' subscribed to room '${roomId}'`) } // Unsubscribe component from room unsubscribeFromRoom(componentId: string, roomId: string) { const room = this.rooms.get(roomId) if (room) { room.delete(componentId) if (room.size === 0) { this.rooms.delete(roomId) } } console.log(`๐Ÿ“ก Component '${componentId}' unsubscribed from room '${roomId}'`) } // Unsubscribe from all rooms private unsubscribeFromAllRooms(componentId: string) { for (const [roomId, components] of Array.from(this.rooms.entries())) { if (components.has(componentId)) { this.unsubscribeFromRoom(componentId, roomId) } } } // Broadcast message to room broadcastToRoom(message: BroadcastMessage, senderComponentId?: string) { if (!message.room) return const roomComponents = this.rooms.get(message.room) if (!roomComponents) return const broadcastMessage: LiveMessage = { type: 'BROADCAST', componentId: senderComponentId || 'system', payload: { type: message.type, data: message.payload }, timestamp: Date.now(), room: message.room } let broadcastCount = 0 for (const componentId of Array.from(roomComponents)) { // Skip sender if excludeUser is specified const component = this.components.get(componentId) if (message.excludeUser && component?.userId === message.excludeUser) { continue } const ws = this.wsConnections.get(componentId) if (ws && ws.send) { ws.send(JSON.stringify(broadcastMessage)) broadcastCount++ } } console.log(`๐Ÿ“ก Broadcast '${message.type}' to room '${message.room}': ${broadcastCount} recipients`) } // Handle WebSocket message async handleMessage(ws: any, message: LiveMessage): Promise<any> { try { switch (message.type) { case 'COMPONENT_MOUNT': const componentId = await this.mountComponent( ws, message.payload.component, message.payload.props, { room: message.payload.room, userId: message.userId } ) return { success: true, result: { componentId } } case 'COMPONENT_UNMOUNT': await this.unmountComponent(message.componentId) return { success: true } case 'CALL_ACTION': // Execute action - response depends on expectResponse flag const actionResult = await this.executeAction( message.componentId, message.action!, message.payload ) // If client expects response, return it if (message.expectResponse) { return { success: true, result: actionResult } } // Otherwise no return - if state changed, component will emit STATE_UPDATE automatically return null case 'PROPERTY_UPDATE': this.updateProperty( message.componentId, message.property!, message.payload.value ) return { success: true } default: console.warn(`โš ๏ธ Unknown message type: ${message.type}`) return { success: false, error: 'Unknown message type' } } } catch (error: any) { console.error('โŒ Registry error:', error.message) // Send error back to client const errorMessage: LiveMessage = { type: 'ERROR', componentId: message.componentId || 'system', payload: { error: error.message, action: message.action, originalMessage: message.type }, timestamp: Date.now() } ws.send(JSON.stringify(errorMessage)) return { success: false, error: error.message } } } // Cleanup when WebSocket disconnects cleanupConnection(ws: any) { if (!ws.data?.components) return const componentsToCleanup = Array.from(ws.data.components.keys()) as string[] for (const componentId of componentsToCleanup) { this.unmountComponent(componentId) } console.log(`๐Ÿงน Cleaned up ${componentsToCleanup.length} components from disconnected WebSocket`) } // Get statistics getStats() { return { components: this.components.size, definitions: this.definitions.size, rooms: this.rooms.size, connections: this.wsConnections.size, roomDetails: Object.fromEntries( Array.from(this.rooms.entries()).map(([roomId, components]) => [ roomId, components.size ]) ) } } // Get component by ID getComponent(componentId: string): LiveComponent | undefined { return this.components.get(componentId) } // Get all components in room getRoomComponents(roomId: string): LiveComponent[] { const componentIds = this.rooms.get(roomId) || new Set() return Array.from(componentIds) .map(id => this.components.get(id)) .filter(Boolean) as LiveComponent[] } } // Global registry instance export const componentRegistry = new ComponentRegistry()