create-fluxstack 1.0.13 → 1.0.15
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 +196 -33
- 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,1017 @@
|
|
|
1
|
+
// 🔥 FluxStack Live Components - Enhanced Component Registry
|
|
2
|
+
|
|
3
|
+
import type {
|
|
4
|
+
LiveComponent,
|
|
5
|
+
LiveMessage,
|
|
6
|
+
BroadcastMessage,
|
|
7
|
+
ComponentDefinition,
|
|
8
|
+
WebSocketData
|
|
9
|
+
} from '../../types/types'
|
|
10
|
+
import { stateSignature, type SignedState } from './StateSignature'
|
|
11
|
+
import { performanceMonitor } from './LiveComponentPerformanceMonitor'
|
|
12
|
+
|
|
13
|
+
// Enhanced interfaces for registry improvements
|
|
14
|
+
export interface ComponentMetadata {
|
|
15
|
+
id: string
|
|
16
|
+
name: string
|
|
17
|
+
version: string
|
|
18
|
+
mountedAt: Date
|
|
19
|
+
lastActivity: Date
|
|
20
|
+
state: 'mounting' | 'active' | 'inactive' | 'error' | 'destroying'
|
|
21
|
+
healthStatus: 'healthy' | 'degraded' | 'unhealthy'
|
|
22
|
+
dependencies: string[]
|
|
23
|
+
services: Map<string, any>
|
|
24
|
+
metrics: ComponentMetrics
|
|
25
|
+
migrationHistory: StateMigration[]
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface ComponentMetrics {
|
|
29
|
+
renderCount: number
|
|
30
|
+
actionCount: number
|
|
31
|
+
errorCount: number
|
|
32
|
+
averageRenderTime: number
|
|
33
|
+
memoryUsage: number
|
|
34
|
+
lastRenderTime?: number
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface StateMigration {
|
|
38
|
+
fromVersion: string
|
|
39
|
+
toVersion: string
|
|
40
|
+
migratedAt: Date
|
|
41
|
+
success: boolean
|
|
42
|
+
error?: string
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface ComponentDependency {
|
|
46
|
+
name: string
|
|
47
|
+
version: string
|
|
48
|
+
required: boolean
|
|
49
|
+
factory: () => any
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface ComponentHealthCheck {
|
|
53
|
+
componentId: string
|
|
54
|
+
status: 'healthy' | 'degraded' | 'unhealthy'
|
|
55
|
+
lastCheck: Date
|
|
56
|
+
issues: string[]
|
|
57
|
+
metrics: ComponentMetrics
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface ServiceContainer {
|
|
61
|
+
register<T>(name: string, factory: () => T): void
|
|
62
|
+
resolve<T>(name: string): T
|
|
63
|
+
has(name: string): boolean
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export class ComponentRegistry {
|
|
67
|
+
private components = new Map<string, LiveComponent>()
|
|
68
|
+
private definitions = new Map<string, ComponentDefinition>()
|
|
69
|
+
private metadata = new Map<string, ComponentMetadata>()
|
|
70
|
+
private rooms = new Map<string, Set<string>>() // roomId -> componentIds
|
|
71
|
+
private wsConnections = new Map<string, any>() // componentId -> websocket
|
|
72
|
+
private autoDiscoveredComponents = new Map<string, any>() // Auto-discovered component classes
|
|
73
|
+
private dependencies = new Map<string, ComponentDependency[]>()
|
|
74
|
+
private services: ServiceContainer
|
|
75
|
+
private healthCheckInterval: NodeJS.Timeout
|
|
76
|
+
private recoveryStrategies = new Map<string, () => Promise<void>>()
|
|
77
|
+
|
|
78
|
+
constructor() {
|
|
79
|
+
this.services = this.createServiceContainer()
|
|
80
|
+
this.setupHealthMonitoring()
|
|
81
|
+
this.setupRecoveryStrategies()
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
private createServiceContainer(): ServiceContainer {
|
|
85
|
+
const services = new Map<string, any>()
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
register<T>(name: string, factory: () => T): void {
|
|
89
|
+
services.set(name, factory)
|
|
90
|
+
},
|
|
91
|
+
resolve<T>(name: string): T {
|
|
92
|
+
const factory = services.get(name)
|
|
93
|
+
if (!factory) {
|
|
94
|
+
throw new Error(`Service '${name}' not found`)
|
|
95
|
+
}
|
|
96
|
+
return factory()
|
|
97
|
+
},
|
|
98
|
+
has(name: string): boolean {
|
|
99
|
+
return services.has(name)
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
private setupHealthMonitoring(): void {
|
|
105
|
+
// Health check every 30 seconds
|
|
106
|
+
this.healthCheckInterval = setInterval(() => {
|
|
107
|
+
this.performHealthChecks()
|
|
108
|
+
}, 30000)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
private setupRecoveryStrategies(): void {
|
|
112
|
+
// Default recovery strategy for unhealthy components
|
|
113
|
+
this.recoveryStrategies.set('default', async () => {
|
|
114
|
+
console.log('🔄 Executing default recovery strategy')
|
|
115
|
+
// Restart unhealthy components
|
|
116
|
+
for (const [componentId, metadata] of this.metadata) {
|
|
117
|
+
if (metadata.healthStatus === 'unhealthy') {
|
|
118
|
+
await this.recoverComponent(componentId)
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
})
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Register component definition with versioning support
|
|
125
|
+
registerComponent<TState>(definition: ComponentDefinition<TState>, version: string = '1.0.0') {
|
|
126
|
+
// Store version separately in metadata when component is mounted
|
|
127
|
+
this.definitions.set(definition.name, definition)
|
|
128
|
+
console.log(`📝 Registered component: ${definition.name} v${version}`)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Register component dependencies
|
|
132
|
+
registerDependencies(componentName: string, dependencies: ComponentDependency[]): void {
|
|
133
|
+
this.dependencies.set(componentName, dependencies)
|
|
134
|
+
console.log(`🔗 Registered dependencies for ${componentName}:`, dependencies.map(d => d.name))
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Register service in DI container
|
|
138
|
+
registerService<T>(name: string, factory: () => T): void {
|
|
139
|
+
this.services.register(name, factory)
|
|
140
|
+
console.log(`🔧 Registered service: ${name}`)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Register component class dynamically
|
|
144
|
+
registerComponentClass(name: string, componentClass: any) {
|
|
145
|
+
this.autoDiscoveredComponents.set(name, componentClass)
|
|
146
|
+
console.log(`🔍 Auto-discovered component: ${name}`)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Auto-discover components from directory
|
|
150
|
+
async autoDiscoverComponents(componentsPath: string) {
|
|
151
|
+
try {
|
|
152
|
+
const fs = await import('fs')
|
|
153
|
+
const path = await import('path')
|
|
154
|
+
|
|
155
|
+
if (!fs.existsSync(componentsPath)) {
|
|
156
|
+
console.log(`⚠️ Components path not found: ${componentsPath}`)
|
|
157
|
+
return
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const files = fs.readdirSync(componentsPath)
|
|
161
|
+
|
|
162
|
+
for (const file of files) {
|
|
163
|
+
if (file.endsWith('.ts') || file.endsWith('.js')) {
|
|
164
|
+
try {
|
|
165
|
+
const fullPath = path.join(componentsPath, file)
|
|
166
|
+
const module = await import(/* @vite-ignore */ fullPath)
|
|
167
|
+
|
|
168
|
+
// Look for exported classes that extend LiveComponent
|
|
169
|
+
Object.keys(module).forEach(exportName => {
|
|
170
|
+
const exportedItem = module[exportName]
|
|
171
|
+
if (typeof exportedItem === 'function' &&
|
|
172
|
+
exportedItem.prototype &&
|
|
173
|
+
this.isLiveComponentClass(exportedItem)) {
|
|
174
|
+
|
|
175
|
+
const componentName = exportName.replace(/Component$/, '')
|
|
176
|
+
this.registerComponentClass(componentName, exportedItem)
|
|
177
|
+
}
|
|
178
|
+
})
|
|
179
|
+
} catch (error) {
|
|
180
|
+
console.warn(`⚠️ Failed to load component from ${file}:`, error)
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
} catch (error) {
|
|
185
|
+
console.error('❌ Auto-discovery failed:', error)
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Check if a class extends LiveComponent
|
|
190
|
+
private isLiveComponentClass(cls: any): boolean {
|
|
191
|
+
try {
|
|
192
|
+
let prototype = cls.prototype
|
|
193
|
+
while (prototype) {
|
|
194
|
+
if (prototype.constructor.name === 'LiveComponent') {
|
|
195
|
+
return true
|
|
196
|
+
}
|
|
197
|
+
prototype = Object.getPrototypeOf(prototype)
|
|
198
|
+
}
|
|
199
|
+
return false
|
|
200
|
+
} catch {
|
|
201
|
+
return false
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Enhanced component mounting with lifecycle management
|
|
206
|
+
async mountComponent(
|
|
207
|
+
ws: any,
|
|
208
|
+
componentName: string,
|
|
209
|
+
props: any = {},
|
|
210
|
+
options?: { room?: string; userId?: string; version?: string }
|
|
211
|
+
): Promise<{ componentId: string; initialState: any; signedState: any }> {
|
|
212
|
+
const startTime = Date.now()
|
|
213
|
+
|
|
214
|
+
try {
|
|
215
|
+
// Validate dependencies
|
|
216
|
+
await this.validateDependencies(componentName)
|
|
217
|
+
|
|
218
|
+
// Try to find component definition first
|
|
219
|
+
let definition = this.definitions.get(componentName)
|
|
220
|
+
let ComponentClass: any = null
|
|
221
|
+
let initialState: any = {}
|
|
222
|
+
|
|
223
|
+
if (definition) {
|
|
224
|
+
// Use registered definition
|
|
225
|
+
ComponentClass = definition.component
|
|
226
|
+
initialState = definition.initialState
|
|
227
|
+
} else {
|
|
228
|
+
// Try auto-discovered components
|
|
229
|
+
ComponentClass = this.autoDiscoveredComponents.get(componentName)
|
|
230
|
+
if (!ComponentClass) {
|
|
231
|
+
// Try variations of the name
|
|
232
|
+
const variations = [
|
|
233
|
+
componentName + 'Component',
|
|
234
|
+
componentName.charAt(0).toUpperCase() + componentName.slice(1) + 'Component',
|
|
235
|
+
componentName.charAt(0).toUpperCase() + componentName.slice(1)
|
|
236
|
+
]
|
|
237
|
+
|
|
238
|
+
for (const variation of variations) {
|
|
239
|
+
ComponentClass = this.autoDiscoveredComponents.get(variation)
|
|
240
|
+
if (ComponentClass) break
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (!ComponentClass) {
|
|
245
|
+
const availableComponents = [
|
|
246
|
+
...Array.from(this.definitions.keys()),
|
|
247
|
+
...Array.from(this.autoDiscoveredComponents.keys())
|
|
248
|
+
]
|
|
249
|
+
throw new Error(`Component '${componentName}' not found. Available: ${availableComponents.join(', ')}`)
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Create a default initial state for auto-discovered components
|
|
253
|
+
initialState = {}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Create component instance with registry methods
|
|
257
|
+
const component = new ComponentClass(
|
|
258
|
+
{ ...initialState, ...props },
|
|
259
|
+
ws,
|
|
260
|
+
options
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
// Inject registry methods
|
|
264
|
+
component.broadcastToRoom = (message: BroadcastMessage) => {
|
|
265
|
+
this.broadcastToRoom(message, component.id)
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Create and store component metadata
|
|
269
|
+
const metadata = this.createComponentMetadata(component.id, componentName, options?.version)
|
|
270
|
+
this.metadata.set(component.id, metadata)
|
|
271
|
+
|
|
272
|
+
// Inject services into component
|
|
273
|
+
const dependencies = this.dependencies.get(componentName) || []
|
|
274
|
+
for (const dep of dependencies) {
|
|
275
|
+
if (this.services.has(dep.name)) {
|
|
276
|
+
const service = this.services.resolve(dep.name)
|
|
277
|
+
metadata.services.set(dep.name, service)
|
|
278
|
+
// Inject service into component if it has a setter
|
|
279
|
+
if (typeof (component as any)[`set${dep.name}`] === 'function') {
|
|
280
|
+
(component as any)[`set${dep.name}`](service)
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Store component and connection
|
|
286
|
+
this.components.set(component.id, component)
|
|
287
|
+
this.wsConnections.set(component.id, ws)
|
|
288
|
+
|
|
289
|
+
// Subscribe to room if specified
|
|
290
|
+
if (options?.room) {
|
|
291
|
+
this.subscribeToRoom(component.id, options.room)
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Initialize WebSocket data if needed
|
|
295
|
+
if (!ws || typeof ws !== 'object') {
|
|
296
|
+
throw new Error('Invalid WebSocket object provided')
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
if (!ws.data) {
|
|
300
|
+
ws.data = {
|
|
301
|
+
components: new Map(),
|
|
302
|
+
subscriptions: new Set(),
|
|
303
|
+
userId: options?.userId
|
|
304
|
+
} as WebSocketData
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Ensure components map exists
|
|
308
|
+
if (!ws.data.components) {
|
|
309
|
+
ws.data.components = new Map()
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
ws.data.components.set(component.id, component)
|
|
313
|
+
|
|
314
|
+
// Update metadata state
|
|
315
|
+
metadata.state = 'active'
|
|
316
|
+
const renderTime = Date.now() - startTime
|
|
317
|
+
this.recordComponentMetrics(component.id, renderTime)
|
|
318
|
+
|
|
319
|
+
// Initialize performance monitoring
|
|
320
|
+
performanceMonitor.initializeComponent(component.id, componentName)
|
|
321
|
+
performanceMonitor.recordRenderTime(component.id, renderTime)
|
|
322
|
+
|
|
323
|
+
console.log(`🚀 Mounted component: ${componentName} (${component.id}) in ${renderTime}ms`)
|
|
324
|
+
|
|
325
|
+
// Send initial state to client with signature
|
|
326
|
+
const signedState = await stateSignature.signState(component.id, component.getSerializableState(), 1, {
|
|
327
|
+
compress: true,
|
|
328
|
+
backup: true
|
|
329
|
+
})
|
|
330
|
+
;(component as any).emit('STATE_UPDATE', {
|
|
331
|
+
state: component.getSerializableState(),
|
|
332
|
+
signedState
|
|
333
|
+
})
|
|
334
|
+
|
|
335
|
+
// Return component ID with signed state for immediate persistence
|
|
336
|
+
return {
|
|
337
|
+
componentId: component.id,
|
|
338
|
+
initialState: component.getSerializableState(),
|
|
339
|
+
signedState
|
|
340
|
+
}
|
|
341
|
+
} catch (error: any) {
|
|
342
|
+
console.error(`❌ Failed to mount component ${componentName}:`, error)
|
|
343
|
+
throw error
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Re-hydrate component with signed client state
|
|
348
|
+
async rehydrateComponent(
|
|
349
|
+
componentId: string,
|
|
350
|
+
componentName: string,
|
|
351
|
+
signedState: SignedState,
|
|
352
|
+
ws: any,
|
|
353
|
+
options?: { room?: string; userId?: string }
|
|
354
|
+
): Promise<{ success: boolean; newComponentId?: string; error?: string }> {
|
|
355
|
+
console.log('🔄 Attempting component re-hydration:', {
|
|
356
|
+
oldComponentId: componentId,
|
|
357
|
+
componentName,
|
|
358
|
+
signedState: {
|
|
359
|
+
timestamp: signedState.timestamp,
|
|
360
|
+
version: signedState.version,
|
|
361
|
+
signature: signedState.signature.substring(0, 16) + '...'
|
|
362
|
+
}
|
|
363
|
+
})
|
|
364
|
+
|
|
365
|
+
try {
|
|
366
|
+
// Validate signed state integrity
|
|
367
|
+
const validation = await stateSignature.validateState(signedState)
|
|
368
|
+
if (!validation.valid) {
|
|
369
|
+
console.warn('❌ State signature validation failed:', validation.error)
|
|
370
|
+
return {
|
|
371
|
+
success: false,
|
|
372
|
+
error: validation.error || 'Invalid state signature'
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// Try to find component definition (same logic as mountComponent)
|
|
377
|
+
let definition = this.definitions.get(componentName)
|
|
378
|
+
let ComponentClass: any = null
|
|
379
|
+
let initialState: any = {}
|
|
380
|
+
|
|
381
|
+
if (definition) {
|
|
382
|
+
// Use registered definition
|
|
383
|
+
ComponentClass = definition.component
|
|
384
|
+
initialState = definition.initialState
|
|
385
|
+
} else {
|
|
386
|
+
// Try auto-discovered components
|
|
387
|
+
ComponentClass = this.autoDiscoveredComponents.get(componentName)
|
|
388
|
+
if (!ComponentClass) {
|
|
389
|
+
// Try variations of the name
|
|
390
|
+
const variations = [
|
|
391
|
+
componentName + 'Component',
|
|
392
|
+
componentName.charAt(0).toUpperCase() + componentName.slice(1) + 'Component',
|
|
393
|
+
componentName.charAt(0).toUpperCase() + componentName.slice(1)
|
|
394
|
+
]
|
|
395
|
+
|
|
396
|
+
for (const variation of variations) {
|
|
397
|
+
ComponentClass = this.autoDiscoveredComponents.get(variation)
|
|
398
|
+
if (ComponentClass) break
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
if (!ComponentClass) {
|
|
403
|
+
const availableComponents = [
|
|
404
|
+
...Array.from(this.definitions.keys()),
|
|
405
|
+
...Array.from(this.autoDiscoveredComponents.keys())
|
|
406
|
+
]
|
|
407
|
+
return {
|
|
408
|
+
success: false,
|
|
409
|
+
error: `Component '${componentName}' not found. Available: ${availableComponents.join(', ')}`
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// Extract validated state
|
|
415
|
+
const clientState = await stateSignature.extractData(signedState)
|
|
416
|
+
|
|
417
|
+
// Create new component instance with client state (merge with initial state if from definition)
|
|
418
|
+
const finalState = definition ? { ...initialState, ...clientState } : clientState
|
|
419
|
+
const component = new ComponentClass(finalState, ws, options)
|
|
420
|
+
|
|
421
|
+
// Store component
|
|
422
|
+
this.components.set(component.id, component)
|
|
423
|
+
this.wsConnections.set(component.id, ws)
|
|
424
|
+
|
|
425
|
+
// Setup room if specified
|
|
426
|
+
if (options?.room) {
|
|
427
|
+
this.subscribeToRoom(component.id, options.room)
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// Initialize WebSocket data
|
|
431
|
+
if (!ws.data) {
|
|
432
|
+
ws.data = {
|
|
433
|
+
components: new Map(),
|
|
434
|
+
subscriptions: new Set(),
|
|
435
|
+
userId: options?.userId
|
|
436
|
+
} as WebSocketData
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// Ensure components map exists
|
|
440
|
+
if (!ws.data.components) {
|
|
441
|
+
ws.data.components = new Map()
|
|
442
|
+
}
|
|
443
|
+
ws.data.components.set(component.id, component)
|
|
444
|
+
|
|
445
|
+
console.log('✅ Component re-hydrated successfully:', {
|
|
446
|
+
oldComponentId: componentId,
|
|
447
|
+
newComponentId: component.id,
|
|
448
|
+
componentName,
|
|
449
|
+
stateVersion: signedState.version
|
|
450
|
+
})
|
|
451
|
+
|
|
452
|
+
// Send updated state to client (with new signature)
|
|
453
|
+
const newSignedState = await stateSignature.signState(
|
|
454
|
+
component.id,
|
|
455
|
+
component.getSerializableState(),
|
|
456
|
+
signedState.version + 1
|
|
457
|
+
)
|
|
458
|
+
|
|
459
|
+
component.emit('STATE_REHYDRATED', {
|
|
460
|
+
state: component.getSerializableState(),
|
|
461
|
+
signedState: newSignedState,
|
|
462
|
+
oldComponentId: componentId,
|
|
463
|
+
newComponentId: component.id
|
|
464
|
+
})
|
|
465
|
+
|
|
466
|
+
return {
|
|
467
|
+
success: true,
|
|
468
|
+
newComponentId: component.id
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
} catch (error: any) {
|
|
472
|
+
console.error('❌ Component re-hydration failed:', error.message)
|
|
473
|
+
return {
|
|
474
|
+
success: false,
|
|
475
|
+
error: error.message
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// Unmount component
|
|
481
|
+
async unmountComponent(componentId: string) {
|
|
482
|
+
const component = this.components.get(componentId)
|
|
483
|
+
if (!component) return
|
|
484
|
+
|
|
485
|
+
// Cleanup
|
|
486
|
+
component.destroy()
|
|
487
|
+
|
|
488
|
+
// Remove from room subscriptions
|
|
489
|
+
this.unsubscribeFromAllRooms(componentId)
|
|
490
|
+
|
|
491
|
+
// Remove from maps
|
|
492
|
+
this.components.delete(componentId)
|
|
493
|
+
this.wsConnections.delete(componentId)
|
|
494
|
+
|
|
495
|
+
console.log(`🗑️ Unmounted component: ${componentId}`)
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// Execute action on component
|
|
499
|
+
async executeAction(componentId: string, action: string, payload: any): Promise<any> {
|
|
500
|
+
const component = this.components.get(componentId)
|
|
501
|
+
if (!component) {
|
|
502
|
+
console.log(`🔄 Component '${componentId}' not found - triggering re-hydration request`)
|
|
503
|
+
// Return special error that triggers re-hydration on client
|
|
504
|
+
throw new Error(`COMPONENT_REHYDRATION_REQUIRED:${componentId}`)
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
try {
|
|
508
|
+
return await component.executeAction(action, payload)
|
|
509
|
+
} catch (error: any) {
|
|
510
|
+
console.error(`❌ Error executing action '${action}' on component '${componentId}':`, error.message)
|
|
511
|
+
throw error
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// Update component property
|
|
516
|
+
updateProperty(componentId: string, property: string, value: any) {
|
|
517
|
+
const component = this.components.get(componentId)
|
|
518
|
+
if (!component) {
|
|
519
|
+
throw new Error(`Component '${componentId}' not found`)
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// Update state
|
|
523
|
+
const updates = { [property]: value }
|
|
524
|
+
component.setState(updates)
|
|
525
|
+
|
|
526
|
+
console.log(`📝 Updated property '${property}' on component '${componentId}'`)
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// Subscribe component to room
|
|
530
|
+
subscribeToRoom(componentId: string, roomId: string) {
|
|
531
|
+
if (!this.rooms.has(roomId)) {
|
|
532
|
+
this.rooms.set(roomId, new Set())
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
this.rooms.get(roomId)!.add(componentId)
|
|
536
|
+
console.log(`📡 Component '${componentId}' subscribed to room '${roomId}'`)
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// Unsubscribe component from room
|
|
540
|
+
unsubscribeFromRoom(componentId: string, roomId: string) {
|
|
541
|
+
const room = this.rooms.get(roomId)
|
|
542
|
+
if (room) {
|
|
543
|
+
room.delete(componentId)
|
|
544
|
+
if (room.size === 0) {
|
|
545
|
+
this.rooms.delete(roomId)
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
console.log(`📡 Component '${componentId}' unsubscribed from room '${roomId}'`)
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
// Unsubscribe from all rooms
|
|
552
|
+
private unsubscribeFromAllRooms(componentId: string) {
|
|
553
|
+
for (const [roomId, components] of Array.from(this.rooms.entries())) {
|
|
554
|
+
if (components.has(componentId)) {
|
|
555
|
+
this.unsubscribeFromRoom(componentId, roomId)
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
// Broadcast message to room
|
|
561
|
+
broadcastToRoom(message: BroadcastMessage, senderComponentId?: string) {
|
|
562
|
+
if (!message.room) return
|
|
563
|
+
|
|
564
|
+
const roomComponents = this.rooms.get(message.room)
|
|
565
|
+
if (!roomComponents) return
|
|
566
|
+
|
|
567
|
+
const broadcastMessage: LiveMessage = {
|
|
568
|
+
type: 'BROADCAST',
|
|
569
|
+
componentId: senderComponentId || 'system',
|
|
570
|
+
payload: {
|
|
571
|
+
type: message.type,
|
|
572
|
+
data: message.payload
|
|
573
|
+
},
|
|
574
|
+
timestamp: Date.now(),
|
|
575
|
+
room: message.room
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
let broadcastCount = 0
|
|
579
|
+
|
|
580
|
+
for (const componentId of Array.from(roomComponents)) {
|
|
581
|
+
// Skip sender if excludeUser is specified
|
|
582
|
+
const component = this.components.get(componentId)
|
|
583
|
+
if (message.excludeUser && component?.userId === message.excludeUser) {
|
|
584
|
+
continue
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
const ws = this.wsConnections.get(componentId)
|
|
588
|
+
if (ws && ws.send) {
|
|
589
|
+
ws.send(JSON.stringify(broadcastMessage))
|
|
590
|
+
broadcastCount++
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
console.log(`📡 Broadcast '${message.type}' to room '${message.room}': ${broadcastCount} recipients`)
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// Handle WebSocket message with enhanced metrics and lifecycle tracking
|
|
598
|
+
async handleMessage(ws: any, message: LiveMessage): Promise<any> {
|
|
599
|
+
const startTime = Date.now()
|
|
600
|
+
|
|
601
|
+
try {
|
|
602
|
+
// Update component activity
|
|
603
|
+
if (message.componentId) {
|
|
604
|
+
this.updateComponentActivity(message.componentId)
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
switch (message.type) {
|
|
608
|
+
case 'COMPONENT_MOUNT':
|
|
609
|
+
const mountResult = await this.mountComponent(
|
|
610
|
+
ws,
|
|
611
|
+
message.payload.component,
|
|
612
|
+
message.payload.props,
|
|
613
|
+
{
|
|
614
|
+
room: message.payload.room,
|
|
615
|
+
userId: message.userId
|
|
616
|
+
}
|
|
617
|
+
)
|
|
618
|
+
return { success: true, result: mountResult }
|
|
619
|
+
|
|
620
|
+
case 'COMPONENT_UNMOUNT':
|
|
621
|
+
await this.unmountComponent(message.componentId)
|
|
622
|
+
return { success: true }
|
|
623
|
+
|
|
624
|
+
case 'CALL_ACTION':
|
|
625
|
+
// Record action metrics
|
|
626
|
+
this.recordComponentMetrics(message.componentId, undefined, message.action)
|
|
627
|
+
|
|
628
|
+
// Execute action with performance monitoring
|
|
629
|
+
const actionStartTime = Date.now()
|
|
630
|
+
let actionError: Error | undefined
|
|
631
|
+
|
|
632
|
+
try {
|
|
633
|
+
const actionResult = await this.executeAction(
|
|
634
|
+
message.componentId,
|
|
635
|
+
message.action!,
|
|
636
|
+
message.payload
|
|
637
|
+
)
|
|
638
|
+
|
|
639
|
+
// Record successful action performance
|
|
640
|
+
const actionTime = Date.now() - actionStartTime
|
|
641
|
+
performanceMonitor.recordActionTime(message.componentId, message.action!, actionTime)
|
|
642
|
+
|
|
643
|
+
// If client expects response, return it
|
|
644
|
+
if (message.expectResponse) {
|
|
645
|
+
return { success: true, result: actionResult }
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
// Otherwise no return - if state changed, component will emit STATE_UPDATE automatically
|
|
649
|
+
return null
|
|
650
|
+
} catch (error) {
|
|
651
|
+
actionError = error as Error
|
|
652
|
+
const actionTime = Date.now() - actionStartTime
|
|
653
|
+
performanceMonitor.recordActionTime(message.componentId, message.action!, actionTime, actionError)
|
|
654
|
+
throw error
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
|
|
658
|
+
case 'PROPERTY_UPDATE':
|
|
659
|
+
this.updateProperty(
|
|
660
|
+
message.componentId,
|
|
661
|
+
message.property!,
|
|
662
|
+
message.payload.value
|
|
663
|
+
)
|
|
664
|
+
return { success: true }
|
|
665
|
+
|
|
666
|
+
default:
|
|
667
|
+
console.warn(`⚠️ Unknown message type: ${message.type}`)
|
|
668
|
+
return { success: false, error: 'Unknown message type' }
|
|
669
|
+
}
|
|
670
|
+
} catch (error: any) {
|
|
671
|
+
console.error('❌ Registry error:', error.message)
|
|
672
|
+
|
|
673
|
+
// Record error metrics if component ID is available
|
|
674
|
+
if (message.componentId) {
|
|
675
|
+
this.recordComponentError(message.componentId, error)
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
// Return error for handleActionCall to process
|
|
679
|
+
return { success: false, error: error.message }
|
|
680
|
+
} finally {
|
|
681
|
+
// Record processing time
|
|
682
|
+
const processingTime = Date.now() - startTime
|
|
683
|
+
if (message.componentId && processingTime > 0) {
|
|
684
|
+
this.recordComponentMetrics(message.componentId, processingTime)
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
// Cleanup when WebSocket disconnects
|
|
690
|
+
cleanupConnection(ws: any) {
|
|
691
|
+
if (!ws.data?.components) return
|
|
692
|
+
|
|
693
|
+
const componentsToCleanup = Array.from(ws.data.components.keys()) as string[]
|
|
694
|
+
|
|
695
|
+
console.log(`🧹 Cleaning up ${componentsToCleanup.length} components for disconnected WebSocket`)
|
|
696
|
+
|
|
697
|
+
for (const componentId of componentsToCleanup) {
|
|
698
|
+
this.cleanupComponent(componentId)
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
// Clear the WebSocket's component map
|
|
702
|
+
ws.data.components.clear()
|
|
703
|
+
|
|
704
|
+
console.log(`🧹 Cleaned up ${componentsToCleanup.length} components from disconnected WebSocket`)
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
// Get statistics
|
|
708
|
+
getStats() {
|
|
709
|
+
return {
|
|
710
|
+
components: this.components.size,
|
|
711
|
+
definitions: this.definitions.size,
|
|
712
|
+
rooms: this.rooms.size,
|
|
713
|
+
connections: this.wsConnections.size,
|
|
714
|
+
roomDetails: Object.fromEntries(
|
|
715
|
+
Array.from(this.rooms.entries()).map(([roomId, components]) => [
|
|
716
|
+
roomId,
|
|
717
|
+
components.size
|
|
718
|
+
])
|
|
719
|
+
)
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
// Get component by ID
|
|
724
|
+
getComponent(componentId: string): LiveComponent | undefined {
|
|
725
|
+
return this.components.get(componentId)
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
// Get all components in room
|
|
729
|
+
getRoomComponents(roomId: string): LiveComponent[] {
|
|
730
|
+
const componentIds = this.rooms.get(roomId) || new Set()
|
|
731
|
+
return Array.from(componentIds)
|
|
732
|
+
.map(id => this.components.get(id))
|
|
733
|
+
.filter(Boolean) as LiveComponent[]
|
|
734
|
+
}
|
|
735
|
+
// Validate component dependencies
|
|
736
|
+
private async validateDependencies(componentName: string): Promise<void> {
|
|
737
|
+
const dependencies = this.dependencies.get(componentName)
|
|
738
|
+
if (!dependencies) return
|
|
739
|
+
|
|
740
|
+
for (const dep of dependencies) {
|
|
741
|
+
if (dep.required && !this.services.has(dep.name)) {
|
|
742
|
+
throw new Error(`Required dependency '${dep.name}' not found for component '${componentName}'`)
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
// Create component metadata
|
|
748
|
+
private createComponentMetadata(componentId: string, componentName: string, version: string = '1.0.0'): ComponentMetadata {
|
|
749
|
+
return {
|
|
750
|
+
id: componentId,
|
|
751
|
+
name: componentName,
|
|
752
|
+
version,
|
|
753
|
+
mountedAt: new Date(),
|
|
754
|
+
lastActivity: new Date(),
|
|
755
|
+
state: 'mounting',
|
|
756
|
+
healthStatus: 'healthy',
|
|
757
|
+
dependencies: this.dependencies.get(componentName)?.map(d => d.name) || [],
|
|
758
|
+
services: new Map(),
|
|
759
|
+
metrics: {
|
|
760
|
+
renderCount: 0,
|
|
761
|
+
actionCount: 0,
|
|
762
|
+
errorCount: 0,
|
|
763
|
+
averageRenderTime: 0,
|
|
764
|
+
memoryUsage: 0
|
|
765
|
+
},
|
|
766
|
+
migrationHistory: []
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
// Update component activity
|
|
771
|
+
updateComponentActivity(componentId: string): void {
|
|
772
|
+
const metadata = this.metadata.get(componentId)
|
|
773
|
+
if (metadata) {
|
|
774
|
+
metadata.lastActivity = new Date()
|
|
775
|
+
metadata.state = 'active'
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
// Record component metrics
|
|
780
|
+
recordComponentMetrics(componentId: string, renderTime?: number, action?: string): void {
|
|
781
|
+
const metadata = this.metadata.get(componentId)
|
|
782
|
+
if (!metadata) return
|
|
783
|
+
|
|
784
|
+
if (renderTime) {
|
|
785
|
+
metadata.metrics.renderCount++
|
|
786
|
+
metadata.metrics.averageRenderTime =
|
|
787
|
+
(metadata.metrics.averageRenderTime * (metadata.metrics.renderCount - 1) + renderTime) / metadata.metrics.renderCount
|
|
788
|
+
metadata.metrics.lastRenderTime = renderTime
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
if (action) {
|
|
792
|
+
metadata.metrics.actionCount++
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
this.updateComponentActivity(componentId)
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
// Record component error
|
|
799
|
+
recordComponentError(componentId: string, error: Error): void {
|
|
800
|
+
const metadata = this.metadata.get(componentId)
|
|
801
|
+
if (metadata) {
|
|
802
|
+
metadata.metrics.errorCount++
|
|
803
|
+
metadata.healthStatus = metadata.metrics.errorCount > 5 ? 'unhealthy' : 'degraded'
|
|
804
|
+
console.error(`❌ Component ${componentId} error:`, error.message)
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
// Perform health checks on all components
|
|
809
|
+
private async performHealthChecks(): Promise<void> {
|
|
810
|
+
const healthChecks: ComponentHealthCheck[] = []
|
|
811
|
+
|
|
812
|
+
for (const [componentId, metadata] of this.metadata) {
|
|
813
|
+
const component = this.components.get(componentId)
|
|
814
|
+
if (!component) continue
|
|
815
|
+
|
|
816
|
+
const issues: string[] = []
|
|
817
|
+
let status: 'healthy' | 'degraded' | 'unhealthy' = 'healthy'
|
|
818
|
+
|
|
819
|
+
// Check if component is responsive
|
|
820
|
+
const timeSinceLastActivity = Date.now() - metadata.lastActivity.getTime()
|
|
821
|
+
if (timeSinceLastActivity > 300000) { // 5 minutes
|
|
822
|
+
issues.push('Component inactive for more than 5 minutes')
|
|
823
|
+
status = 'degraded'
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
// Check error rate
|
|
827
|
+
if (metadata.metrics.errorCount > 10) {
|
|
828
|
+
issues.push('High error rate detected')
|
|
829
|
+
status = 'unhealthy'
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
// Check memory usage (if available)
|
|
833
|
+
if (metadata.metrics.memoryUsage > 100 * 1024 * 1024) { // 100MB
|
|
834
|
+
issues.push('High memory usage detected')
|
|
835
|
+
status = 'degraded'
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
metadata.healthStatus = status
|
|
839
|
+
|
|
840
|
+
healthChecks.push({
|
|
841
|
+
componentId,
|
|
842
|
+
status,
|
|
843
|
+
lastCheck: new Date(),
|
|
844
|
+
issues,
|
|
845
|
+
metrics: { ...metadata.metrics }
|
|
846
|
+
})
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
// Log unhealthy components
|
|
850
|
+
const unhealthyComponents = healthChecks.filter(hc => hc.status === 'unhealthy')
|
|
851
|
+
if (unhealthyComponents.length > 0) {
|
|
852
|
+
console.warn(`⚠️ Found ${unhealthyComponents.length} unhealthy components:`,
|
|
853
|
+
unhealthyComponents.map(hc => hc.componentId))
|
|
854
|
+
|
|
855
|
+
// Trigger recovery if needed
|
|
856
|
+
await this.triggerRecovery()
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
// Trigger recovery for unhealthy components
|
|
861
|
+
private async triggerRecovery(): Promise<void> {
|
|
862
|
+
const defaultStrategy = this.recoveryStrategies.get('default')
|
|
863
|
+
if (defaultStrategy) {
|
|
864
|
+
try {
|
|
865
|
+
await defaultStrategy()
|
|
866
|
+
} catch (error) {
|
|
867
|
+
console.error('❌ Recovery strategy failed:', error)
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
// Recover a specific component
|
|
873
|
+
private async recoverComponent(componentId: string): Promise<void> {
|
|
874
|
+
const metadata = this.metadata.get(componentId)
|
|
875
|
+
const component = this.components.get(componentId)
|
|
876
|
+
|
|
877
|
+
if (!metadata || !component) return
|
|
878
|
+
|
|
879
|
+
try {
|
|
880
|
+
console.log(`🔄 Recovering component ${componentId}`)
|
|
881
|
+
|
|
882
|
+
// Reset error count
|
|
883
|
+
metadata.metrics.errorCount = 0
|
|
884
|
+
metadata.healthStatus = 'healthy'
|
|
885
|
+
metadata.state = 'active'
|
|
886
|
+
|
|
887
|
+
// Emit recovery event to client using the protected emit method
|
|
888
|
+
;(component as any).emit('COMPONENT_RECOVERED', {
|
|
889
|
+
componentId,
|
|
890
|
+
timestamp: Date.now()
|
|
891
|
+
})
|
|
892
|
+
|
|
893
|
+
} catch (error) {
|
|
894
|
+
console.error(`❌ Failed to recover component ${componentId}:`, error)
|
|
895
|
+
metadata.state = 'error'
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
// Migrate component state to new version
|
|
900
|
+
async migrateComponentState(componentId: string, fromVersion: string, toVersion: string, migrationFn: (state: any) => any): Promise<boolean> {
|
|
901
|
+
const component = this.components.get(componentId)
|
|
902
|
+
const metadata = this.metadata.get(componentId)
|
|
903
|
+
|
|
904
|
+
if (!component || !metadata) return false
|
|
905
|
+
|
|
906
|
+
try {
|
|
907
|
+
console.log(`🔄 Migrating component ${componentId} from v${fromVersion} to v${toVersion}`)
|
|
908
|
+
|
|
909
|
+
const oldState = component.getSerializableState()
|
|
910
|
+
const newState = migrationFn(oldState)
|
|
911
|
+
|
|
912
|
+
// Update component state
|
|
913
|
+
component.setState(newState)
|
|
914
|
+
|
|
915
|
+
// Record migration
|
|
916
|
+
const migration: StateMigration = {
|
|
917
|
+
fromVersion,
|
|
918
|
+
toVersion,
|
|
919
|
+
migratedAt: new Date(),
|
|
920
|
+
success: true
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
metadata.migrationHistory.push(migration)
|
|
924
|
+
metadata.version = toVersion
|
|
925
|
+
|
|
926
|
+
console.log(`✅ Successfully migrated component ${componentId}`)
|
|
927
|
+
return true
|
|
928
|
+
|
|
929
|
+
} catch (error: any) {
|
|
930
|
+
console.error(`❌ Migration failed for component ${componentId}:`, error)
|
|
931
|
+
|
|
932
|
+
const migration: StateMigration = {
|
|
933
|
+
fromVersion,
|
|
934
|
+
toVersion,
|
|
935
|
+
migratedAt: new Date(),
|
|
936
|
+
success: false,
|
|
937
|
+
error: error.message
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
metadata?.migrationHistory.push(migration)
|
|
941
|
+
return false
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
// Get component health status
|
|
946
|
+
getComponentHealth(componentId: string): ComponentHealthCheck | null {
|
|
947
|
+
const metadata = this.metadata.get(componentId)
|
|
948
|
+
if (!metadata) return null
|
|
949
|
+
|
|
950
|
+
return {
|
|
951
|
+
componentId,
|
|
952
|
+
status: metadata.healthStatus,
|
|
953
|
+
lastCheck: new Date(),
|
|
954
|
+
issues: [],
|
|
955
|
+
metrics: { ...metadata.metrics }
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
// Get all component health statuses
|
|
960
|
+
getAllComponentHealth(): ComponentHealthCheck[] {
|
|
961
|
+
return Array.from(this.metadata.values()).map(metadata => ({
|
|
962
|
+
componentId: metadata.id,
|
|
963
|
+
status: metadata.healthStatus,
|
|
964
|
+
lastCheck: new Date(),
|
|
965
|
+
issues: [],
|
|
966
|
+
metrics: { ...metadata.metrics }
|
|
967
|
+
}))
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
// Cleanup method to be called on shutdown
|
|
971
|
+
cleanup(): void {
|
|
972
|
+
if (this.healthCheckInterval) {
|
|
973
|
+
clearInterval(this.healthCheckInterval)
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
// Cleanup all components
|
|
977
|
+
for (const [componentId] of this.components) {
|
|
978
|
+
this.cleanupComponent(componentId)
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
// Enhanced cleanup for individual components
|
|
983
|
+
private cleanupComponent(componentId: string): void {
|
|
984
|
+
const component = this.components.get(componentId)
|
|
985
|
+
const metadata = this.metadata.get(componentId)
|
|
986
|
+
|
|
987
|
+
if (component) {
|
|
988
|
+
try {
|
|
989
|
+
component.destroy()
|
|
990
|
+
} catch (error) {
|
|
991
|
+
console.error(`❌ Error destroying component ${componentId}:`, error)
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
if (metadata) {
|
|
996
|
+
metadata.state = 'destroying'
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
// Remove from performance monitoring
|
|
1000
|
+
performanceMonitor.removeComponent(componentId)
|
|
1001
|
+
|
|
1002
|
+
this.components.delete(componentId)
|
|
1003
|
+
this.metadata.delete(componentId)
|
|
1004
|
+
this.wsConnections.delete(componentId)
|
|
1005
|
+
|
|
1006
|
+
// Remove from rooms
|
|
1007
|
+
for (const [roomId, componentIds] of this.rooms) {
|
|
1008
|
+
componentIds.delete(componentId)
|
|
1009
|
+
if (componentIds.size === 0) {
|
|
1010
|
+
this.rooms.delete(roomId)
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
// Global registry instance
|
|
1017
|
+
export const componentRegistry = new ComponentRegistry()
|