create-fluxstack
Version:
โก Revolutionary full-stack TypeScript framework with Declarative Config System, Elysia + React + Bun
399 lines (335 loc) โข 12.7 kB
text/typescript
// ๐ฅ 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()