create-fluxstack 1.0.13 → 1.0.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +29 -29
- package/app/client/README.md +69 -69
- package/app/client/index.html +14 -13
- package/app/client/src/App.tsx +157 -524
- package/app/client/src/components/ErrorBoundary.tsx +107 -0
- package/app/client/src/components/ErrorDisplay.css +365 -0
- package/app/client/src/components/ErrorDisplay.tsx +258 -0
- package/app/client/src/components/FluxStackConfig.tsx +1321 -0
- package/app/client/src/components/HybridLiveCounter.tsx +140 -0
- package/app/client/src/components/LiveClock.tsx +286 -0
- package/app/client/src/components/MainLayout.tsx +390 -0
- package/app/client/src/components/SidebarNavigation.tsx +391 -0
- package/app/client/src/components/StateDemo.tsx +178 -0
- package/app/client/src/components/SystemMonitor.tsx +1038 -0
- package/app/client/src/components/Teste.tsx +104 -0
- package/app/client/src/components/UserProfile.tsx +809 -0
- package/app/client/src/hooks/useAuth.ts +39 -0
- package/app/client/src/hooks/useNotifications.ts +56 -0
- package/app/client/src/lib/eden-api.ts +189 -53
- package/app/client/src/lib/errors.ts +340 -0
- package/app/client/src/lib/hooks/useErrorHandler.ts +258 -0
- package/app/client/src/lib/index.ts +45 -0
- package/app/client/src/main.tsx +3 -2
- package/app/client/src/pages/ApiDocs.tsx +182 -0
- package/app/client/src/pages/Demo.tsx +174 -0
- package/app/client/src/pages/HybridLive.tsx +263 -0
- package/app/client/src/pages/Overview.tsx +155 -0
- package/app/client/src/store/README.md +43 -0
- package/app/client/src/store/index.ts +16 -0
- package/app/client/src/store/slices/uiSlice.ts +151 -0
- package/app/client/src/store/slices/userSlice.ts +161 -0
- package/app/client/src/test/README.md +257 -0
- package/app/client/src/test/setup.ts +70 -0
- package/app/client/src/test/types.ts +12 -0
- package/app/client/src/vite-env.d.ts +1 -1
- package/app/client/tsconfig.app.json +44 -43
- package/app/client/tsconfig.json +7 -7
- package/app/client/tsconfig.node.json +25 -25
- package/app/client/zustand-setup.md +65 -0
- package/app/server/controllers/users.controller.ts +68 -68
- package/app/server/index.ts +9 -1
- package/app/server/live/CounterComponent.ts +191 -0
- package/app/server/live/FluxStackConfig.ts +529 -0
- package/app/server/live/LiveClockComponent.ts +214 -0
- package/app/server/live/SidebarNavigation.ts +156 -0
- package/app/server/live/SystemMonitor.ts +594 -0
- package/app/server/live/SystemMonitorIntegration.ts +151 -0
- package/app/server/live/TesteComponent.ts +87 -0
- package/app/server/live/UserProfileComponent.ts +135 -0
- package/app/server/live/register-components.ts +28 -0
- package/app/server/middleware/auth.ts +136 -0
- package/app/server/middleware/errorHandling.ts +250 -0
- package/app/server/middleware/index.ts +10 -0
- package/app/server/middleware/rateLimit.ts +193 -0
- package/app/server/middleware/requestLogging.ts +215 -0
- package/app/server/middleware/validation.ts +270 -0
- package/app/server/routes/index.ts +14 -2
- package/app/server/routes/upload.ts +92 -0
- package/app/server/routes/users.routes.ts +2 -9
- package/app/server/services/NotificationService.ts +302 -0
- package/app/server/services/UserService.ts +222 -0
- package/app/server/services/index.ts +46 -0
- package/core/cli/commands/plugin-deps.ts +263 -0
- package/core/cli/generators/README.md +339 -0
- package/core/cli/generators/component.ts +770 -0
- package/core/cli/generators/controller.ts +299 -0
- package/core/cli/generators/index.ts +144 -0
- package/core/cli/generators/interactive.ts +228 -0
- package/core/cli/generators/prompts.ts +83 -0
- package/core/cli/generators/route.ts +513 -0
- package/core/cli/generators/service.ts +465 -0
- package/core/cli/generators/template-engine.ts +154 -0
- package/core/cli/generators/types.ts +71 -0
- package/core/cli/generators/utils.ts +192 -0
- package/core/cli/index.ts +69 -0
- package/core/cli/plugin-discovery.ts +16 -85
- package/core/client/fluxstack.ts +17 -0
- package/core/client/hooks/index.ts +7 -0
- package/core/client/hooks/state-validator.ts +130 -0
- package/core/client/hooks/useAuth.ts +49 -0
- package/core/client/hooks/useChunkedUpload.ts +258 -0
- package/core/client/hooks/useHybridLiveComponent.ts +967 -0
- package/core/client/hooks/useWebSocket.ts +373 -0
- package/core/client/index.ts +47 -0
- package/core/client/state/createStore.ts +193 -0
- package/core/client/state/index.ts +15 -0
- package/core/config/env-dynamic.ts +1 -1
- package/core/config/env.ts +2 -1
- package/core/config/runtime-config.ts +3 -3
- package/core/config/schema.ts +84 -49
- package/core/framework/server.ts +30 -0
- package/core/index.ts +25 -0
- package/core/live/ComponentRegistry.ts +399 -0
- package/core/live/types.ts +164 -0
- package/core/plugins/built-in/live-components/commands/create-live-component.ts +1201 -0
- package/core/plugins/built-in/live-components/index.ts +27 -0
- package/core/plugins/built-in/logger/index.ts +1 -1
- package/core/plugins/built-in/monitoring/index.ts +1 -1
- package/core/plugins/built-in/static/index.ts +1 -1
- package/core/plugins/built-in/swagger/index.ts +1 -1
- package/core/plugins/built-in/vite/index.ts +1 -1
- package/core/plugins/dependency-manager.ts +384 -0
- package/core/plugins/index.ts +5 -1
- package/core/plugins/manager.ts +7 -3
- package/core/plugins/registry.ts +88 -10
- package/core/plugins/types.ts +11 -11
- package/core/server/framework.ts +43 -0
- package/core/server/index.ts +11 -1
- package/core/server/live/ComponentRegistry.ts +1017 -0
- package/core/server/live/FileUploadManager.ts +272 -0
- package/core/server/live/LiveComponentPerformanceMonitor.ts +930 -0
- package/core/server/live/SingleConnectionManager.ts +0 -0
- package/core/server/live/StateSignature.ts +644 -0
- package/core/server/live/WebSocketConnectionManager.ts +688 -0
- package/core/server/live/websocket-plugin.ts +435 -0
- package/core/server/middleware/errorHandling.ts +141 -0
- package/core/server/middleware/index.ts +16 -0
- package/core/server/plugins/static-files-plugin.ts +232 -0
- package/core/server/services/BaseService.ts +95 -0
- package/core/server/services/ServiceContainer.ts +144 -0
- package/core/server/services/index.ts +9 -0
- package/core/templates/create-project.ts +46 -2
- package/core/testing/index.ts +10 -0
- package/core/testing/setup.ts +74 -0
- package/core/types/build.ts +38 -14
- package/core/types/types.ts +319 -0
- package/core/utils/env-runtime.ts +7 -0
- package/core/utils/errors/handlers.ts +264 -39
- package/core/utils/errors/index.ts +528 -18
- package/core/utils/errors/middleware.ts +114 -0
- package/core/utils/logger/formatters.ts +222 -0
- package/core/utils/logger/index.ts +167 -48
- package/core/utils/logger/middleware.ts +253 -0
- package/core/utils/logger/performance.ts +384 -0
- package/core/utils/logger/transports.ts +365 -0
- package/create-fluxstack.ts +296 -296
- package/fluxstack.config.ts +17 -1
- package/package-template.json +66 -66
- package/package.json +31 -6
- package/public/README.md +16 -0
- package/vite.config.ts +29 -14
- package/.claude/settings.local.json +0 -74
- package/.github/workflows/ci-build-tests.yml +0 -480
- package/.github/workflows/dependency-management.yml +0 -324
- package/.github/workflows/release-validation.yml +0 -355
- package/.kiro/specs/fluxstack-architecture-optimization/design.md +0 -700
- package/.kiro/specs/fluxstack-architecture-optimization/requirements.md +0 -127
- package/.kiro/specs/fluxstack-architecture-optimization/tasks.md +0 -330
- package/CLAUDE.md +0 -200
- package/Dockerfile +0 -58
- package/Dockerfile.backend +0 -52
- package/Dockerfile.frontend +0 -54
- package/README-Docker.md +0 -85
- package/ai-context/00-QUICK-START.md +0 -86
- package/ai-context/README.md +0 -88
- package/ai-context/development/eden-treaty-guide.md +0 -362
- package/ai-context/development/patterns.md +0 -382
- package/ai-context/development/plugins-guide.md +0 -572
- package/ai-context/examples/crud-complete.md +0 -626
- package/ai-context/project/architecture.md +0 -399
- package/ai-context/project/overview.md +0 -213
- package/ai-context/recent-changes/eden-treaty-refactor.md +0 -281
- package/ai-context/recent-changes/type-inference-fix.md +0 -223
- package/ai-context/reference/environment-vars.md +0 -384
- package/ai-context/reference/troubleshooting.md +0 -407
- package/app/client/src/components/TestPage.tsx +0 -453
- package/bun.lock +0 -1063
- package/bunfig.toml +0 -16
- package/core/__tests__/integration.test.ts +0 -227
- package/core/build/index.ts +0 -186
- package/core/config/__tests__/config-loader.test.ts +0 -554
- package/core/config/__tests__/config-merger.test.ts +0 -657
- package/core/config/__tests__/env-converter.test.ts +0 -372
- package/core/config/__tests__/env-processor.test.ts +0 -431
- package/core/config/__tests__/env.test.ts +0 -452
- package/core/config/__tests__/integration.test.ts +0 -418
- package/core/config/__tests__/loader.test.ts +0 -331
- package/core/config/__tests__/schema.test.ts +0 -129
- package/core/config/__tests__/validator.test.ts +0 -318
- package/core/framework/__tests__/server.test.ts +0 -233
- package/core/plugins/__tests__/built-in.test.ts.disabled +0 -366
- package/core/plugins/__tests__/manager.test.ts +0 -398
- package/core/plugins/__tests__/monitoring.test.ts +0 -401
- package/core/plugins/__tests__/registry.test.ts +0 -335
- package/core/utils/__tests__/errors.test.ts +0 -139
- package/core/utils/__tests__/helpers.test.ts +0 -297
- package/core/utils/__tests__/logger.test.ts +0 -141
- package/create-test-app.ts +0 -156
- package/docker-compose.microservices.yml +0 -75
- package/docker-compose.simple.yml +0 -57
- package/docker-compose.yml +0 -71
- package/eslint.config.js +0 -23
- package/flux-cli.ts +0 -214
- package/nginx-lb.conf +0 -37
- package/publish.sh +0 -63
- package/run-clean.ts +0 -26
- package/run-env-tests.ts +0 -313
- package/tailwind.config.js +0 -34
- package/tests/__mocks__/api.ts +0 -56
- package/tests/fixtures/users.ts +0 -69
- package/tests/integration/api/users.routes.test.ts +0 -221
- package/tests/setup.ts +0 -29
- package/tests/unit/app/client/App-simple.test.tsx +0 -56
- package/tests/unit/app/client/App.test.tsx.skip +0 -237
- package/tests/unit/app/client/eden-api.test.ts +0 -186
- package/tests/unit/app/client/simple.test.tsx +0 -23
- package/tests/unit/app/controllers/users.controller.test.ts +0 -150
- package/tests/unit/core/create-project.test.ts.skip +0 -95
- package/tests/unit/core/framework.test.ts +0 -144
- package/tests/unit/core/plugins/logger.test.ts.skip +0 -268
- package/tests/unit/core/plugins/vite.test.ts.disabled +0 -188
- package/tests/utils/test-helpers.ts +0 -61
- package/vitest.config.ts +0 -50
- package/workspace.json +0 -6
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
// 🔥 FluxStack Live Components - Component Registry
|
|
2
|
+
|
|
3
|
+
import type {
|
|
4
|
+
LiveComponent,
|
|
5
|
+
LiveMessage,
|
|
6
|
+
BroadcastMessage,
|
|
7
|
+
ComponentDefinition,
|
|
8
|
+
WebSocketData
|
|
9
|
+
} from '../types/types'
|
|
10
|
+
|
|
11
|
+
export class ComponentRegistry {
|
|
12
|
+
private components = new Map<string, LiveComponent>()
|
|
13
|
+
private definitions = new Map<string, ComponentDefinition>()
|
|
14
|
+
private rooms = new Map<string, Set<string>>() // roomId -> componentIds
|
|
15
|
+
private wsConnections = new Map<string, any>() // componentId -> websocket
|
|
16
|
+
private autoDiscoveredComponents = new Map<string, any>() // Auto-discovered component classes
|
|
17
|
+
|
|
18
|
+
// Register component definition
|
|
19
|
+
registerComponent<TState>(definition: ComponentDefinition<TState>) {
|
|
20
|
+
this.definitions.set(definition.name, definition)
|
|
21
|
+
console.log(`📝 Registered component: ${definition.name}`)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Register component class dynamically
|
|
25
|
+
registerComponentClass(name: string, componentClass: any) {
|
|
26
|
+
this.autoDiscoveredComponents.set(name, componentClass)
|
|
27
|
+
console.log(`🔍 Auto-discovered component: ${name}`)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Auto-discover components from directory
|
|
31
|
+
async autoDiscoverComponents(componentsPath: string) {
|
|
32
|
+
try {
|
|
33
|
+
const fs = await import('fs')
|
|
34
|
+
const path = await import('path')
|
|
35
|
+
|
|
36
|
+
if (!fs.existsSync(componentsPath)) {
|
|
37
|
+
console.log(`⚠️ Components path not found: ${componentsPath}`)
|
|
38
|
+
return
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const files = fs.readdirSync(componentsPath)
|
|
42
|
+
|
|
43
|
+
for (const file of files) {
|
|
44
|
+
if (file.endsWith('.ts') || file.endsWith('.js')) {
|
|
45
|
+
try {
|
|
46
|
+
const fullPath = path.join(componentsPath, file)
|
|
47
|
+
const module = await import(/* @vite-ignore */ fullPath)
|
|
48
|
+
|
|
49
|
+
// Look for exported classes that extend LiveComponent
|
|
50
|
+
Object.keys(module).forEach(exportName => {
|
|
51
|
+
const exportedItem = module[exportName]
|
|
52
|
+
if (typeof exportedItem === 'function' &&
|
|
53
|
+
exportedItem.prototype &&
|
|
54
|
+
this.isLiveComponentClass(exportedItem)) {
|
|
55
|
+
|
|
56
|
+
const componentName = exportName.replace(/Component$/, '')
|
|
57
|
+
this.registerComponentClass(componentName, exportedItem)
|
|
58
|
+
}
|
|
59
|
+
})
|
|
60
|
+
} catch (error) {
|
|
61
|
+
console.warn(`⚠️ Failed to load component from ${file}:`, error)
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
} catch (error) {
|
|
66
|
+
console.error('❌ Auto-discovery failed:', error)
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Check if a class extends LiveComponent
|
|
71
|
+
private isLiveComponentClass(cls: any): boolean {
|
|
72
|
+
try {
|
|
73
|
+
let prototype = cls.prototype
|
|
74
|
+
while (prototype) {
|
|
75
|
+
if (prototype.constructor.name === 'LiveComponent') {
|
|
76
|
+
return true
|
|
77
|
+
}
|
|
78
|
+
prototype = Object.getPrototypeOf(prototype)
|
|
79
|
+
}
|
|
80
|
+
return false
|
|
81
|
+
} catch {
|
|
82
|
+
return false
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Mount component instance
|
|
87
|
+
async mountComponent(
|
|
88
|
+
ws: any,
|
|
89
|
+
componentName: string,
|
|
90
|
+
props: any = {},
|
|
91
|
+
options?: { room?: string; userId?: string }
|
|
92
|
+
): Promise<string> {
|
|
93
|
+
// Try to find component definition first
|
|
94
|
+
let definition = this.definitions.get(componentName)
|
|
95
|
+
let ComponentClass: any = null
|
|
96
|
+
let initialState: any = {}
|
|
97
|
+
|
|
98
|
+
if (definition) {
|
|
99
|
+
// Use registered definition
|
|
100
|
+
ComponentClass = definition.component
|
|
101
|
+
initialState = definition.initialState
|
|
102
|
+
} else {
|
|
103
|
+
// Try auto-discovered components
|
|
104
|
+
ComponentClass = this.autoDiscoveredComponents.get(componentName)
|
|
105
|
+
if (!ComponentClass) {
|
|
106
|
+
// Try variations of the name
|
|
107
|
+
const variations = [
|
|
108
|
+
componentName + 'Component',
|
|
109
|
+
componentName.charAt(0).toUpperCase() + componentName.slice(1) + 'Component',
|
|
110
|
+
componentName.charAt(0).toUpperCase() + componentName.slice(1)
|
|
111
|
+
]
|
|
112
|
+
|
|
113
|
+
for (const variation of variations) {
|
|
114
|
+
ComponentClass = this.autoDiscoveredComponents.get(variation)
|
|
115
|
+
if (ComponentClass) break
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (!ComponentClass) {
|
|
120
|
+
const availableComponents = [
|
|
121
|
+
...Array.from(this.definitions.keys()),
|
|
122
|
+
...Array.from(this.autoDiscoveredComponents.keys())
|
|
123
|
+
]
|
|
124
|
+
throw new Error(`Component '${componentName}' not found. Available: ${availableComponents.join(', ')}`)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Create a default initial state for auto-discovered components
|
|
128
|
+
initialState = {}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Create component instance with registry methods
|
|
132
|
+
const component = new ComponentClass(
|
|
133
|
+
{ ...initialState, ...props },
|
|
134
|
+
ws,
|
|
135
|
+
options
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
// Inject registry methods
|
|
139
|
+
component.broadcastToRoom = (message: BroadcastMessage) => {
|
|
140
|
+
this.broadcastToRoom(message, component.id)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Store component and connection
|
|
144
|
+
this.components.set(component.id, component)
|
|
145
|
+
this.wsConnections.set(component.id, ws)
|
|
146
|
+
|
|
147
|
+
// Subscribe to room if specified
|
|
148
|
+
if (options?.room) {
|
|
149
|
+
this.subscribeToRoom(component.id, options.room)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Initialize WebSocket data if needed
|
|
153
|
+
if (!ws.data) {
|
|
154
|
+
ws.data = {
|
|
155
|
+
components: new Map(),
|
|
156
|
+
subscriptions: new Set(),
|
|
157
|
+
userId: options?.userId
|
|
158
|
+
} as WebSocketData
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
ws.data.components.set(component.id, component)
|
|
162
|
+
|
|
163
|
+
console.log(`🚀 Mounted component: ${componentName} (${component.id})`)
|
|
164
|
+
|
|
165
|
+
// Send initial state to client
|
|
166
|
+
component.emit('STATE_UPDATE', { state: component.getSerializableState() })
|
|
167
|
+
|
|
168
|
+
return component.id
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Unmount component
|
|
172
|
+
async unmountComponent(componentId: string) {
|
|
173
|
+
const component = this.components.get(componentId)
|
|
174
|
+
if (!component) return
|
|
175
|
+
|
|
176
|
+
// Cleanup
|
|
177
|
+
component.destroy()
|
|
178
|
+
|
|
179
|
+
// Remove from room subscriptions
|
|
180
|
+
this.unsubscribeFromAllRooms(componentId)
|
|
181
|
+
|
|
182
|
+
// Remove from maps
|
|
183
|
+
this.components.delete(componentId)
|
|
184
|
+
this.wsConnections.delete(componentId)
|
|
185
|
+
|
|
186
|
+
console.log(`🗑️ Unmounted component: ${componentId}`)
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Execute action on component
|
|
190
|
+
async executeAction(componentId: string, action: string, payload: any): Promise<any> {
|
|
191
|
+
const component = this.components.get(componentId)
|
|
192
|
+
if (!component) {
|
|
193
|
+
throw new Error(`Component '${componentId}' not found`)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
try {
|
|
197
|
+
return await component.executeAction(action, payload)
|
|
198
|
+
} catch (error: any) {
|
|
199
|
+
console.error(`❌ Error executing action '${action}' on component '${componentId}':`, error.message)
|
|
200
|
+
throw error
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Update component property
|
|
205
|
+
updateProperty(componentId: string, property: string, value: any) {
|
|
206
|
+
const component = this.components.get(componentId)
|
|
207
|
+
if (!component) {
|
|
208
|
+
throw new Error(`Component '${componentId}' not found`)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Update state
|
|
212
|
+
const updates = { [property]: value }
|
|
213
|
+
component.setState(updates)
|
|
214
|
+
|
|
215
|
+
console.log(`📝 Updated property '${property}' on component '${componentId}'`)
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Subscribe component to room
|
|
219
|
+
subscribeToRoom(componentId: string, roomId: string) {
|
|
220
|
+
if (!this.rooms.has(roomId)) {
|
|
221
|
+
this.rooms.set(roomId, new Set())
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
this.rooms.get(roomId)!.add(componentId)
|
|
225
|
+
console.log(`📡 Component '${componentId}' subscribed to room '${roomId}'`)
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Unsubscribe component from room
|
|
229
|
+
unsubscribeFromRoom(componentId: string, roomId: string) {
|
|
230
|
+
const room = this.rooms.get(roomId)
|
|
231
|
+
if (room) {
|
|
232
|
+
room.delete(componentId)
|
|
233
|
+
if (room.size === 0) {
|
|
234
|
+
this.rooms.delete(roomId)
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
console.log(`📡 Component '${componentId}' unsubscribed from room '${roomId}'`)
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Unsubscribe from all rooms
|
|
241
|
+
private unsubscribeFromAllRooms(componentId: string) {
|
|
242
|
+
for (const [roomId, components] of Array.from(this.rooms.entries())) {
|
|
243
|
+
if (components.has(componentId)) {
|
|
244
|
+
this.unsubscribeFromRoom(componentId, roomId)
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Broadcast message to room
|
|
250
|
+
broadcastToRoom(message: BroadcastMessage, senderComponentId?: string) {
|
|
251
|
+
if (!message.room) return
|
|
252
|
+
|
|
253
|
+
const roomComponents = this.rooms.get(message.room)
|
|
254
|
+
if (!roomComponents) return
|
|
255
|
+
|
|
256
|
+
const broadcastMessage: LiveMessage = {
|
|
257
|
+
type: 'BROADCAST',
|
|
258
|
+
componentId: senderComponentId || 'system',
|
|
259
|
+
payload: {
|
|
260
|
+
type: message.type,
|
|
261
|
+
data: message.payload
|
|
262
|
+
},
|
|
263
|
+
timestamp: Date.now(),
|
|
264
|
+
room: message.room
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
let broadcastCount = 0
|
|
268
|
+
|
|
269
|
+
for (const componentId of Array.from(roomComponents)) {
|
|
270
|
+
// Skip sender if excludeUser is specified
|
|
271
|
+
const component = this.components.get(componentId)
|
|
272
|
+
if (message.excludeUser && component?.userId === message.excludeUser) {
|
|
273
|
+
continue
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const ws = this.wsConnections.get(componentId)
|
|
277
|
+
if (ws && ws.send) {
|
|
278
|
+
ws.send(JSON.stringify(broadcastMessage))
|
|
279
|
+
broadcastCount++
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
console.log(`📡 Broadcast '${message.type}' to room '${message.room}': ${broadcastCount} recipients`)
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Handle WebSocket message
|
|
287
|
+
async handleMessage(ws: any, message: LiveMessage): Promise<any> {
|
|
288
|
+
try {
|
|
289
|
+
switch (message.type) {
|
|
290
|
+
case 'COMPONENT_MOUNT':
|
|
291
|
+
const componentId = await this.mountComponent(
|
|
292
|
+
ws,
|
|
293
|
+
message.payload.component,
|
|
294
|
+
message.payload.props,
|
|
295
|
+
{
|
|
296
|
+
room: message.payload.room,
|
|
297
|
+
userId: message.userId
|
|
298
|
+
}
|
|
299
|
+
)
|
|
300
|
+
return { success: true, result: { componentId } }
|
|
301
|
+
|
|
302
|
+
case 'COMPONENT_UNMOUNT':
|
|
303
|
+
await this.unmountComponent(message.componentId)
|
|
304
|
+
return { success: true }
|
|
305
|
+
|
|
306
|
+
case 'CALL_ACTION':
|
|
307
|
+
// Execute action - response depends on expectResponse flag
|
|
308
|
+
const actionResult = await this.executeAction(
|
|
309
|
+
message.componentId,
|
|
310
|
+
message.action!,
|
|
311
|
+
message.payload
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
// If client expects response, return it
|
|
315
|
+
if (message.expectResponse) {
|
|
316
|
+
return { success: true, result: actionResult }
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Otherwise no return - if state changed, component will emit STATE_UPDATE automatically
|
|
320
|
+
return null
|
|
321
|
+
|
|
322
|
+
case 'PROPERTY_UPDATE':
|
|
323
|
+
this.updateProperty(
|
|
324
|
+
message.componentId,
|
|
325
|
+
message.property!,
|
|
326
|
+
message.payload.value
|
|
327
|
+
)
|
|
328
|
+
return { success: true }
|
|
329
|
+
|
|
330
|
+
default:
|
|
331
|
+
console.warn(`⚠️ Unknown message type: ${message.type}`)
|
|
332
|
+
return { success: false, error: 'Unknown message type' }
|
|
333
|
+
}
|
|
334
|
+
} catch (error: any) {
|
|
335
|
+
console.error('❌ Registry error:', error.message)
|
|
336
|
+
|
|
337
|
+
// Send error back to client
|
|
338
|
+
const errorMessage: LiveMessage = {
|
|
339
|
+
type: 'ERROR',
|
|
340
|
+
componentId: message.componentId || 'system',
|
|
341
|
+
payload: {
|
|
342
|
+
error: error.message,
|
|
343
|
+
action: message.action,
|
|
344
|
+
originalMessage: message.type
|
|
345
|
+
},
|
|
346
|
+
timestamp: Date.now()
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
ws.send(JSON.stringify(errorMessage))
|
|
350
|
+
|
|
351
|
+
return { success: false, error: error.message }
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Cleanup when WebSocket disconnects
|
|
356
|
+
cleanupConnection(ws: any) {
|
|
357
|
+
if (!ws.data?.components) return
|
|
358
|
+
|
|
359
|
+
const componentsToCleanup = Array.from(ws.data.components.keys()) as string[]
|
|
360
|
+
|
|
361
|
+
for (const componentId of componentsToCleanup) {
|
|
362
|
+
this.unmountComponent(componentId)
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
console.log(`🧹 Cleaned up ${componentsToCleanup.length} components from disconnected WebSocket`)
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// Get statistics
|
|
369
|
+
getStats() {
|
|
370
|
+
return {
|
|
371
|
+
components: this.components.size,
|
|
372
|
+
definitions: this.definitions.size,
|
|
373
|
+
rooms: this.rooms.size,
|
|
374
|
+
connections: this.wsConnections.size,
|
|
375
|
+
roomDetails: Object.fromEntries(
|
|
376
|
+
Array.from(this.rooms.entries()).map(([roomId, components]) => [
|
|
377
|
+
roomId,
|
|
378
|
+
components.size
|
|
379
|
+
])
|
|
380
|
+
)
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// Get component by ID
|
|
385
|
+
getComponent(componentId: string): LiveComponent | undefined {
|
|
386
|
+
return this.components.get(componentId)
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// Get all components in room
|
|
390
|
+
getRoomComponents(roomId: string): LiveComponent[] {
|
|
391
|
+
const componentIds = this.rooms.get(roomId) || new Set()
|
|
392
|
+
return Array.from(componentIds)
|
|
393
|
+
.map(id => this.components.get(id))
|
|
394
|
+
.filter(Boolean) as LiveComponent[]
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// Global registry instance
|
|
399
|
+
export const componentRegistry = new ComponentRegistry()
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
// 🔥 FluxStack Live Components - Core Types
|
|
2
|
+
|
|
3
|
+
export interface LiveMessage {
|
|
4
|
+
type: 'COMPONENT_MOUNT' | 'COMPONENT_UNMOUNT' | 'COMPONENT_ACTION' | 'CALL_ACTION' | 'ACTION_RESPONSE' | 'PROPERTY_UPDATE' | 'STATE_UPDATE' | 'ERROR' | 'BROADCAST'
|
|
5
|
+
componentId: string
|
|
6
|
+
action?: string
|
|
7
|
+
property?: string
|
|
8
|
+
payload?: any
|
|
9
|
+
timestamp?: number
|
|
10
|
+
userId?: string
|
|
11
|
+
room?: string
|
|
12
|
+
// Request-Response system
|
|
13
|
+
requestId?: string
|
|
14
|
+
responseId?: string
|
|
15
|
+
expectResponse?: boolean
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface ComponentState {
|
|
19
|
+
[key: string]: any
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface LiveComponentInstance<TState = ComponentState, TActions = Record<string, Function>> {
|
|
23
|
+
id: string
|
|
24
|
+
state: TState
|
|
25
|
+
call: <T extends keyof TActions>(action: T, ...args: any[]) => Promise<any>
|
|
26
|
+
set: <K extends keyof TState>(property: K, value: TState[K]) => void
|
|
27
|
+
loading: boolean
|
|
28
|
+
errors: Record<string, string>
|
|
29
|
+
connected: boolean
|
|
30
|
+
room?: string
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface WebSocketData {
|
|
34
|
+
components: Map<string, any>
|
|
35
|
+
userId?: string
|
|
36
|
+
subscriptions: Set<string>
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface ComponentDefinition<TState = ComponentState> {
|
|
40
|
+
name: string
|
|
41
|
+
initialState: TState
|
|
42
|
+
component: new (initialState: TState, ws: any) => LiveComponent<TState>
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface BroadcastMessage {
|
|
46
|
+
type: string
|
|
47
|
+
payload: any
|
|
48
|
+
room?: string
|
|
49
|
+
excludeUser?: string
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export abstract class LiveComponent<TState = ComponentState> {
|
|
53
|
+
public readonly id: string
|
|
54
|
+
public state: TState
|
|
55
|
+
protected ws: any
|
|
56
|
+
protected room?: string
|
|
57
|
+
protected userId?: string
|
|
58
|
+
public broadcastToRoom: (message: BroadcastMessage) => void = () => {} // Will be injected by registry
|
|
59
|
+
|
|
60
|
+
constructor(initialState: TState, ws: any, options?: { room?: string; userId?: string }) {
|
|
61
|
+
this.id = this.generateId()
|
|
62
|
+
this.state = initialState
|
|
63
|
+
this.ws = ws
|
|
64
|
+
this.room = options?.room
|
|
65
|
+
this.userId = options?.userId
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// State management
|
|
69
|
+
protected setState(updates: Partial<TState> | ((prev: TState) => Partial<TState>)) {
|
|
70
|
+
const newUpdates = typeof updates === 'function' ? updates(this.state) : updates
|
|
71
|
+
this.state = { ...this.state, ...newUpdates }
|
|
72
|
+
this.emit('STATE_UPDATE', { state: this.state })
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Execute action safely
|
|
76
|
+
public async executeAction(action: string, payload: any): Promise<any> {
|
|
77
|
+
try {
|
|
78
|
+
// Check if method exists
|
|
79
|
+
const method = (this as any)[action]
|
|
80
|
+
if (typeof method !== 'function') {
|
|
81
|
+
throw new Error(`Action '${action}' not found on component`)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Execute method
|
|
85
|
+
const result = await method.call(this, payload)
|
|
86
|
+
return result
|
|
87
|
+
} catch (error: any) {
|
|
88
|
+
this.emit('ERROR', {
|
|
89
|
+
action,
|
|
90
|
+
error: error.message,
|
|
91
|
+
stack: error.stack
|
|
92
|
+
})
|
|
93
|
+
throw error
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Send message to client
|
|
98
|
+
protected emit(type: string, payload: any) {
|
|
99
|
+
const message: LiveMessage = {
|
|
100
|
+
type: type as any,
|
|
101
|
+
componentId: this.id,
|
|
102
|
+
payload,
|
|
103
|
+
timestamp: Date.now(),
|
|
104
|
+
userId: this.userId,
|
|
105
|
+
room: this.room
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (this.ws && this.ws.send) {
|
|
109
|
+
this.ws.send(JSON.stringify(message))
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Broadcast to all clients in room
|
|
114
|
+
protected broadcast(type: string, payload: any, excludeCurrentUser = false) {
|
|
115
|
+
const message: BroadcastMessage = {
|
|
116
|
+
type,
|
|
117
|
+
payload,
|
|
118
|
+
room: this.room,
|
|
119
|
+
excludeUser: excludeCurrentUser ? this.userId : undefined
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// This will be handled by the registry
|
|
123
|
+
this.broadcastToRoom(message)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Subscribe to room for multi-user features
|
|
127
|
+
protected async subscribeToRoom(roomId: string) {
|
|
128
|
+
this.room = roomId
|
|
129
|
+
// Registry will handle the actual subscription
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Unsubscribe from room
|
|
133
|
+
protected async unsubscribeFromRoom() {
|
|
134
|
+
this.room = undefined
|
|
135
|
+
// Registry will handle the actual unsubscription
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Generate unique ID
|
|
139
|
+
private generateId(): string {
|
|
140
|
+
return `live-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Cleanup when component is destroyed
|
|
144
|
+
public destroy() {
|
|
145
|
+
this.unsubscribeFromRoom()
|
|
146
|
+
// Override in subclasses for custom cleanup
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Get serializable state for client
|
|
150
|
+
public getSerializableState(): TState {
|
|
151
|
+
return this.state
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Utility types for better TypeScript experience
|
|
156
|
+
export type ComponentActions<T> = {
|
|
157
|
+
[K in keyof T]: T[K] extends (...args: any[]) => any ? T[K] : never
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export type ComponentProps<T extends LiveComponent> = T extends LiveComponent<infer TState> ? TState : never
|
|
161
|
+
|
|
162
|
+
export type ActionParameters<T, K extends keyof T> = T[K] extends (...args: infer P) => any ? P : never
|
|
163
|
+
|
|
164
|
+
export type ActionReturnType<T, K extends keyof T> = T[K] extends (...args: any[]) => infer R ? R : never
|