create-fluxstack 1.1.0 → 1.4.1
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/app/server/backend-only.ts +5 -5
- package/app/server/index.ts +63 -54
- package/app/server/live/FluxStackConfig.ts +43 -39
- package/app/server/live/SystemMonitorIntegration.ts +2 -2
- package/app/server/live/register-components.ts +1 -1
- package/app/server/middleware/errorHandling.ts +6 -4
- package/app/server/routes/config.ts +145 -0
- package/app/server/routes/index.ts +5 -3
- package/config/app.config.ts +113 -0
- package/config/build.config.ts +24 -0
- package/config/database.config.ts +99 -0
- package/config/index.ts +68 -0
- package/config/logger.config.ts +27 -0
- package/config/runtime.config.ts +92 -0
- package/config/server.config.ts +46 -0
- package/config/services.config.ts +130 -0
- package/config/system.config.ts +105 -0
- package/core/build/index.ts +10 -4
- package/core/cli/generators/index.ts +5 -2
- package/core/cli/generators/plugin.ts +290 -0
- package/core/cli/index.ts +117 -15
- package/core/config/env.ts +37 -95
- package/core/config/runtime-config.ts +61 -58
- package/core/config/schema.ts +4 -0
- package/core/framework/server.ts +22 -10
- package/core/plugins/built-in/index.ts +7 -17
- package/core/plugins/built-in/swagger/index.ts +228 -228
- package/core/plugins/built-in/vite/index.ts +374 -358
- package/core/plugins/dependency-manager.ts +5 -5
- package/core/plugins/manager.ts +12 -12
- package/core/plugins/registry.ts +3 -3
- package/core/server/index.ts +0 -1
- package/core/server/live/ComponentRegistry.ts +34 -8
- package/core/server/live/LiveComponentPerformanceMonitor.ts +1 -1
- package/core/server/live/websocket-plugin.ts +434 -434
- package/core/server/middleware/README.md +488 -0
- package/core/server/middleware/elysia-helpers.ts +227 -0
- package/core/server/middleware/index.ts +25 -9
- package/core/server/plugins/static-files-plugin.ts +231 -231
- package/core/utils/config-schema.ts +484 -0
- package/core/utils/env.ts +306 -0
- package/core/utils/helpers.ts +4 -4
- package/core/utils/logger/colors.ts +114 -0
- package/core/utils/logger/config.ts +35 -0
- package/core/utils/logger/formatter.ts +82 -0
- package/core/utils/logger/group-logger.ts +101 -0
- package/core/utils/logger/index.ts +199 -250
- package/core/utils/logger/stack-trace.ts +92 -0
- package/core/utils/logger/startup-banner.ts +92 -0
- package/core/utils/logger/winston-logger.ts +152 -0
- package/core/utils/version.ts +5 -0
- package/create-fluxstack.ts +118 -8
- package/fluxstack.config.ts +2 -2
- package/package.json +117 -115
- package/core/config/env-dynamic.ts +0 -326
- package/core/plugins/built-in/logger/index.ts +0 -180
- package/core/server/plugins/logger.ts +0 -47
- package/core/utils/env-runtime-v2.ts +0 -232
- package/core/utils/env-runtime.ts +0 -259
- package/core/utils/logger/formatters.ts +0 -222
- package/core/utils/logger/middleware.ts +0 -253
- package/core/utils/logger/performance.ts +0 -384
- package/core/utils/logger/transports.ts +0 -365
- package/core/utils/logger.ts +0 -106
|
@@ -1,435 +1,435 @@
|
|
|
1
|
-
// 🔥 FluxStack Live Components - Enhanced WebSocket Plugin with Connection Management
|
|
2
|
-
|
|
3
|
-
import { componentRegistry } from './ComponentRegistry'
|
|
4
|
-
import { fileUploadManager } from './FileUploadManager'
|
|
5
|
-
import { connectionManager } from './WebSocketConnectionManager'
|
|
6
|
-
import { performanceMonitor } from './LiveComponentPerformanceMonitor'
|
|
7
|
-
import type { LiveMessage, FileUploadStartMessage, FileUploadChunkMessage, FileUploadCompleteMessage } from '../../types/types'
|
|
8
|
-
import type { Plugin, PluginContext } from '../../plugins/types'
|
|
9
|
-
import { t } from 'elysia'
|
|
10
|
-
import path from 'path'
|
|
11
|
-
|
|
12
|
-
export const liveComponentsPlugin: Plugin = {
|
|
13
|
-
name: 'live-components',
|
|
14
|
-
version: '1.0.0',
|
|
15
|
-
description: 'Real-time Live Components with Elysia native WebSocket support',
|
|
16
|
-
author: 'FluxStack Team',
|
|
17
|
-
priority: 'normal',
|
|
18
|
-
category: 'core',
|
|
19
|
-
tags: ['websocket', 'real-time', 'live-components'],
|
|
20
|
-
|
|
21
|
-
setup: async (context: PluginContext) => {
|
|
22
|
-
context.logger.
|
|
23
|
-
|
|
24
|
-
// Auto-discover components from app/server/live directory
|
|
25
|
-
const componentsPath = path.join(process.cwd(), 'app', 'server', 'live')
|
|
26
|
-
await componentRegistry.autoDiscoverComponents(componentsPath)
|
|
27
|
-
context.logger.
|
|
28
|
-
|
|
29
|
-
// Add WebSocket route for Live Components
|
|
30
|
-
context.app
|
|
31
|
-
.ws('/api/live/ws', {
|
|
32
|
-
body: t.Object({
|
|
33
|
-
type: t.String(),
|
|
34
|
-
componentId: t.String(),
|
|
35
|
-
action: t.Optional(t.String()),
|
|
36
|
-
payload: t.Optional(t.Any()),
|
|
37
|
-
timestamp: t.Optional(t.Number()),
|
|
38
|
-
userId: t.Optional(t.String()),
|
|
39
|
-
room: t.Optional(t.String()),
|
|
40
|
-
requestId: t.Optional(t.String()),
|
|
41
|
-
expectResponse: t.Optional(t.Boolean()),
|
|
42
|
-
// File upload specific fields
|
|
43
|
-
uploadId: t.Optional(t.String()),
|
|
44
|
-
filename: t.Optional(t.String()),
|
|
45
|
-
fileType: t.Optional(t.String()),
|
|
46
|
-
fileSize: t.Optional(t.Number()),
|
|
47
|
-
chunkSize: t.Optional(t.Number()),
|
|
48
|
-
chunkIndex: t.Optional(t.Number()),
|
|
49
|
-
totalChunks: t.Optional(t.Number()),
|
|
50
|
-
data: t.Optional(t.String()),
|
|
51
|
-
hash: t.Optional(t.String())
|
|
52
|
-
}),
|
|
53
|
-
|
|
54
|
-
open(ws) {
|
|
55
|
-
const connectionId = `ws-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
|
|
56
|
-
console.log(`🔌 Live Components WebSocket connected: ${connectionId}`)
|
|
57
|
-
|
|
58
|
-
// Register connection with enhanced connection manager
|
|
59
|
-
connectionManager.registerConnection(ws, connectionId, 'live-components')
|
|
60
|
-
|
|
61
|
-
// Initialize and store connection data in ws.data
|
|
62
|
-
if (!ws.data) {
|
|
63
|
-
ws.data = {}
|
|
64
|
-
}
|
|
65
|
-
ws.data.connectionId = connectionId
|
|
66
|
-
ws.data.components = new Map()
|
|
67
|
-
ws.data.subscriptions = new Set()
|
|
68
|
-
ws.data.connectedAt = new Date()
|
|
69
|
-
|
|
70
|
-
// Send connection confirmation
|
|
71
|
-
ws.send(JSON.stringify({
|
|
72
|
-
type: 'CONNECTION_ESTABLISHED',
|
|
73
|
-
connectionId,
|
|
74
|
-
timestamp: Date.now(),
|
|
75
|
-
features: {
|
|
76
|
-
compression: true,
|
|
77
|
-
encryption: true,
|
|
78
|
-
offlineQueue: true,
|
|
79
|
-
loadBalancing: true
|
|
80
|
-
}
|
|
81
|
-
}))
|
|
82
|
-
},
|
|
83
|
-
|
|
84
|
-
async message(ws, message: LiveMessage) {
|
|
85
|
-
try {
|
|
86
|
-
// Add connection metadata
|
|
87
|
-
message.timestamp = Date.now()
|
|
88
|
-
|
|
89
|
-
console.log(`📨 Received message:`, {
|
|
90
|
-
type: message.type,
|
|
91
|
-
componentId: message.componentId,
|
|
92
|
-
action: message.action,
|
|
93
|
-
requestId: message.requestId
|
|
94
|
-
})
|
|
95
|
-
|
|
96
|
-
// Handle different message types
|
|
97
|
-
switch (message.type) {
|
|
98
|
-
case 'COMPONENT_MOUNT':
|
|
99
|
-
await handleComponentMount(ws, message)
|
|
100
|
-
break
|
|
101
|
-
case 'COMPONENT_REHYDRATE':
|
|
102
|
-
await handleComponentRehydrate(ws, message)
|
|
103
|
-
break
|
|
104
|
-
case 'COMPONENT_UNMOUNT':
|
|
105
|
-
await handleComponentUnmount(ws, message)
|
|
106
|
-
break
|
|
107
|
-
case 'CALL_ACTION':
|
|
108
|
-
await handleActionCall(ws, message)
|
|
109
|
-
break
|
|
110
|
-
case 'PROPERTY_UPDATE':
|
|
111
|
-
await handlePropertyUpdate(ws, message)
|
|
112
|
-
break
|
|
113
|
-
case 'FILE_UPLOAD_START':
|
|
114
|
-
await handleFileUploadStart(ws, message as FileUploadStartMessage)
|
|
115
|
-
break
|
|
116
|
-
case 'FILE_UPLOAD_CHUNK':
|
|
117
|
-
await handleFileUploadChunk(ws, message as FileUploadChunkMessage)
|
|
118
|
-
break
|
|
119
|
-
case 'FILE_UPLOAD_COMPLETE':
|
|
120
|
-
await handleFileUploadComplete(ws, message as FileUploadCompleteMessage)
|
|
121
|
-
break
|
|
122
|
-
default:
|
|
123
|
-
console.warn(`❌ Unknown message type: ${message.type}`)
|
|
124
|
-
ws.send(JSON.stringify({
|
|
125
|
-
type: 'ERROR',
|
|
126
|
-
error: `Unknown message type: ${message.type}`,
|
|
127
|
-
timestamp: Date.now()
|
|
128
|
-
}))
|
|
129
|
-
}
|
|
130
|
-
} catch (error) {
|
|
131
|
-
console.error('❌ WebSocket message error:', error)
|
|
132
|
-
ws.send(JSON.stringify({
|
|
133
|
-
type: 'ERROR',
|
|
134
|
-
error: error instanceof Error ? error.message : 'Unknown error',
|
|
135
|
-
timestamp: Date.now()
|
|
136
|
-
}))
|
|
137
|
-
}
|
|
138
|
-
},
|
|
139
|
-
|
|
140
|
-
close(ws) {
|
|
141
|
-
const connectionId = ws.data?.connectionId
|
|
142
|
-
console.log(`🔌 Live Components WebSocket disconnected: ${connectionId}`)
|
|
143
|
-
|
|
144
|
-
// Cleanup connection in connection manager
|
|
145
|
-
if (connectionId) {
|
|
146
|
-
connectionManager.cleanupConnection(connectionId)
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
// Cleanup components for this connection
|
|
150
|
-
componentRegistry.cleanupConnection(ws)
|
|
151
|
-
}
|
|
152
|
-
})
|
|
153
|
-
|
|
154
|
-
// Add Live Components info routes
|
|
155
|
-
.get('/api/live/websocket-info', () => {
|
|
156
|
-
return {
|
|
157
|
-
success: true,
|
|
158
|
-
message: 'Live Components WebSocket available via Elysia',
|
|
159
|
-
endpoint: 'ws://localhost:3000/api/live/ws',
|
|
160
|
-
status: 'running',
|
|
161
|
-
connectionManager: connectionManager.getSystemStats()
|
|
162
|
-
}
|
|
163
|
-
})
|
|
164
|
-
.get('/api/live/stats', () => {
|
|
165
|
-
const stats = componentRegistry.getStats()
|
|
166
|
-
return {
|
|
167
|
-
success: true,
|
|
168
|
-
stats,
|
|
169
|
-
timestamp: new Date().toISOString()
|
|
170
|
-
}
|
|
171
|
-
})
|
|
172
|
-
.get('/api/live/health', () => {
|
|
173
|
-
return {
|
|
174
|
-
success: true,
|
|
175
|
-
service: 'FluxStack Live Components',
|
|
176
|
-
status: 'operational',
|
|
177
|
-
components: componentRegistry.getStats().components,
|
|
178
|
-
connections: connectionManager.getSystemStats(),
|
|
179
|
-
uptime: process.uptime(),
|
|
180
|
-
timestamp: new Date().toISOString()
|
|
181
|
-
}
|
|
182
|
-
})
|
|
183
|
-
.get('/api/live/connections', () => {
|
|
184
|
-
return {
|
|
185
|
-
success: true,
|
|
186
|
-
connections: connectionManager.getAllConnectionMetrics(),
|
|
187
|
-
systemStats: connectionManager.getSystemStats(),
|
|
188
|
-
timestamp: new Date().toISOString()
|
|
189
|
-
}
|
|
190
|
-
})
|
|
191
|
-
.get('/api/live/connections/:connectionId', ({ params }) => {
|
|
192
|
-
const metrics = connectionManager.getConnectionMetrics(params.connectionId)
|
|
193
|
-
if (!metrics) {
|
|
194
|
-
return {
|
|
195
|
-
success: false,
|
|
196
|
-
error: 'Connection not found'
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
return {
|
|
200
|
-
success: true,
|
|
201
|
-
connection: metrics,
|
|
202
|
-
timestamp: new Date().toISOString()
|
|
203
|
-
}
|
|
204
|
-
})
|
|
205
|
-
.get('/api/live/pools/:poolId/stats', ({ params }) => {
|
|
206
|
-
const stats = connectionManager.getPoolStats(params.poolId)
|
|
207
|
-
if (!stats) {
|
|
208
|
-
return {
|
|
209
|
-
success: false,
|
|
210
|
-
error: 'Pool not found'
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
return {
|
|
214
|
-
success: true,
|
|
215
|
-
pool: params.poolId,
|
|
216
|
-
stats,
|
|
217
|
-
timestamp: new Date().toISOString()
|
|
218
|
-
}
|
|
219
|
-
})
|
|
220
|
-
.get('/api/live/performance/dashboard', () => {
|
|
221
|
-
return {
|
|
222
|
-
success: true,
|
|
223
|
-
dashboard: performanceMonitor.generateDashboard(),
|
|
224
|
-
timestamp: new Date().toISOString()
|
|
225
|
-
}
|
|
226
|
-
})
|
|
227
|
-
.get('/api/live/performance/components/:componentId', ({ params }) => {
|
|
228
|
-
const metrics = performanceMonitor.getComponentMetrics(params.componentId)
|
|
229
|
-
if (!metrics) {
|
|
230
|
-
return {
|
|
231
|
-
success: false,
|
|
232
|
-
error: 'Component metrics not found'
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
const alerts = performanceMonitor.getComponentAlerts(params.componentId)
|
|
237
|
-
const suggestions = performanceMonitor.getComponentSuggestions(params.componentId)
|
|
238
|
-
|
|
239
|
-
return {
|
|
240
|
-
success: true,
|
|
241
|
-
component: params.componentId,
|
|
242
|
-
metrics,
|
|
243
|
-
alerts,
|
|
244
|
-
suggestions,
|
|
245
|
-
timestamp: new Date().toISOString()
|
|
246
|
-
}
|
|
247
|
-
})
|
|
248
|
-
.post('/api/live/performance/alerts/:alertId/resolve', ({ params }) => {
|
|
249
|
-
const resolved = performanceMonitor.resolveAlert(params.alertId)
|
|
250
|
-
return {
|
|
251
|
-
success: resolved,
|
|
252
|
-
message: resolved ? 'Alert resolved' : 'Alert not found',
|
|
253
|
-
timestamp: new Date().toISOString()
|
|
254
|
-
}
|
|
255
|
-
})
|
|
256
|
-
},
|
|
257
|
-
|
|
258
|
-
onServerStart: async (context: PluginContext) => {
|
|
259
|
-
context.logger.
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
// Handler functions for WebSocket messages
|
|
264
|
-
async function handleComponentMount(ws: any, message: LiveMessage) {
|
|
265
|
-
const result = await componentRegistry.handleMessage(ws, message)
|
|
266
|
-
|
|
267
|
-
if (result !== null) {
|
|
268
|
-
const response = {
|
|
269
|
-
type: 'COMPONENT_MOUNTED',
|
|
270
|
-
componentId: message.componentId,
|
|
271
|
-
success: result.success,
|
|
272
|
-
result: result.result,
|
|
273
|
-
error: result.error,
|
|
274
|
-
requestId: message.requestId,
|
|
275
|
-
timestamp: Date.now()
|
|
276
|
-
}
|
|
277
|
-
ws.send(JSON.stringify(response))
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
async function handleComponentRehydrate(ws: any, message: LiveMessage) {
|
|
282
|
-
console.log('🔄 Processing component re-hydration request:', {
|
|
283
|
-
componentId: message.componentId,
|
|
284
|
-
payload: message.payload
|
|
285
|
-
})
|
|
286
|
-
|
|
287
|
-
try {
|
|
288
|
-
const { componentName, signedState, room, userId } = message.payload || {}
|
|
289
|
-
|
|
290
|
-
if (!componentName || !signedState) {
|
|
291
|
-
throw new Error('Missing componentName or signedState in rehydration payload')
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
const result = await componentRegistry.rehydrateComponent(
|
|
295
|
-
message.componentId,
|
|
296
|
-
componentName,
|
|
297
|
-
signedState,
|
|
298
|
-
ws,
|
|
299
|
-
{ room, userId }
|
|
300
|
-
)
|
|
301
|
-
|
|
302
|
-
const response = {
|
|
303
|
-
type: 'COMPONENT_REHYDRATED',
|
|
304
|
-
componentId: message.componentId,
|
|
305
|
-
success: result.success,
|
|
306
|
-
result: {
|
|
307
|
-
newComponentId: result.newComponentId,
|
|
308
|
-
...result
|
|
309
|
-
},
|
|
310
|
-
error: result.error,
|
|
311
|
-
requestId: message.requestId,
|
|
312
|
-
timestamp: Date.now()
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
console.log('📤 Sending COMPONENT_REHYDRATED response:', {
|
|
316
|
-
type: response.type,
|
|
317
|
-
success: response.success,
|
|
318
|
-
newComponentId: response.result?.newComponentId,
|
|
319
|
-
requestId: response.requestId
|
|
320
|
-
})
|
|
321
|
-
|
|
322
|
-
ws.send(JSON.stringify(response))
|
|
323
|
-
|
|
324
|
-
} catch (error: any) {
|
|
325
|
-
console.error('❌ Re-hydration handler error:', error.message)
|
|
326
|
-
|
|
327
|
-
const errorResponse = {
|
|
328
|
-
type: 'COMPONENT_REHYDRATED',
|
|
329
|
-
componentId: message.componentId,
|
|
330
|
-
success: false,
|
|
331
|
-
error: error.message,
|
|
332
|
-
requestId: message.requestId,
|
|
333
|
-
timestamp: Date.now()
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
ws.send(JSON.stringify(errorResponse))
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
async function handleComponentUnmount(ws: any, message: LiveMessage) {
|
|
341
|
-
const result = await componentRegistry.handleMessage(ws, message)
|
|
342
|
-
|
|
343
|
-
if (result !== null) {
|
|
344
|
-
const response = {
|
|
345
|
-
type: 'COMPONENT_UNMOUNTED',
|
|
346
|
-
componentId: message.componentId,
|
|
347
|
-
success: result.success,
|
|
348
|
-
requestId: message.requestId,
|
|
349
|
-
timestamp: Date.now()
|
|
350
|
-
}
|
|
351
|
-
ws.send(JSON.stringify(response))
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
async function handleActionCall(ws: any, message: LiveMessage) {
|
|
356
|
-
const result = await componentRegistry.handleMessage(ws, message)
|
|
357
|
-
|
|
358
|
-
if (result !== null) {
|
|
359
|
-
const response = {
|
|
360
|
-
type: message.expectResponse ? 'ACTION_RESPONSE' : 'MESSAGE_RESPONSE',
|
|
361
|
-
originalType: message.type,
|
|
362
|
-
componentId: message.componentId,
|
|
363
|
-
success: result.success,
|
|
364
|
-
result: result.result,
|
|
365
|
-
error: result.error,
|
|
366
|
-
requestId: message.requestId,
|
|
367
|
-
timestamp: Date.now()
|
|
368
|
-
}
|
|
369
|
-
ws.send(JSON.stringify(response))
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
async function handlePropertyUpdate(ws: any, message: LiveMessage) {
|
|
374
|
-
const result = await componentRegistry.handleMessage(ws, message)
|
|
375
|
-
|
|
376
|
-
if (result !== null) {
|
|
377
|
-
const response = {
|
|
378
|
-
type: 'PROPERTY_UPDATED',
|
|
379
|
-
componentId: message.componentId,
|
|
380
|
-
success: result.success,
|
|
381
|
-
result: result.result,
|
|
382
|
-
error: result.error,
|
|
383
|
-
requestId: message.requestId,
|
|
384
|
-
timestamp: Date.now()
|
|
385
|
-
}
|
|
386
|
-
ws.send(JSON.stringify(response))
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
// File Upload Handler Functions
|
|
391
|
-
async function handleFileUploadStart(ws: any, message: FileUploadStartMessage) {
|
|
392
|
-
console.log('📤 Starting file upload:', message.uploadId)
|
|
393
|
-
|
|
394
|
-
const result = await fileUploadManager.startUpload(message)
|
|
395
|
-
|
|
396
|
-
const response = {
|
|
397
|
-
type: 'FILE_UPLOAD_START_RESPONSE',
|
|
398
|
-
componentId: message.componentId,
|
|
399
|
-
uploadId: message.uploadId,
|
|
400
|
-
success: result.success,
|
|
401
|
-
error: result.error,
|
|
402
|
-
requestId: message.requestId,
|
|
403
|
-
timestamp: Date.now()
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
ws.send(JSON.stringify(response))
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
async function handleFileUploadChunk(ws: any, message: FileUploadChunkMessage) {
|
|
410
|
-
console.log(`📦 Receiving chunk ${message.chunkIndex + 1} for upload ${message.uploadId}`)
|
|
411
|
-
|
|
412
|
-
const progressResponse = await fileUploadManager.receiveChunk(message, ws)
|
|
413
|
-
|
|
414
|
-
if (progressResponse) {
|
|
415
|
-
ws.send(JSON.stringify(progressResponse))
|
|
416
|
-
} else {
|
|
417
|
-
// Send error response
|
|
418
|
-
const errorResponse = {
|
|
419
|
-
type: 'FILE_UPLOAD_ERROR',
|
|
420
|
-
componentId: message.componentId,
|
|
421
|
-
uploadId: message.uploadId,
|
|
422
|
-
error: 'Failed to process chunk',
|
|
423
|
-
requestId: message.requestId,
|
|
424
|
-
timestamp: Date.now()
|
|
425
|
-
}
|
|
426
|
-
ws.send(JSON.stringify(errorResponse))
|
|
427
|
-
}
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
async function handleFileUploadComplete(ws: any, message: FileUploadCompleteMessage) {
|
|
431
|
-
console.log('✅ Completing file upload:', message.uploadId)
|
|
432
|
-
|
|
433
|
-
const completeResponse = await fileUploadManager.completeUpload(message)
|
|
434
|
-
ws.send(JSON.stringify(completeResponse))
|
|
1
|
+
// 🔥 FluxStack Live Components - Enhanced WebSocket Plugin with Connection Management
|
|
2
|
+
|
|
3
|
+
import { componentRegistry } from './ComponentRegistry'
|
|
4
|
+
import { fileUploadManager } from './FileUploadManager'
|
|
5
|
+
import { connectionManager } from './WebSocketConnectionManager'
|
|
6
|
+
import { performanceMonitor } from './LiveComponentPerformanceMonitor'
|
|
7
|
+
import type { LiveMessage, FileUploadStartMessage, FileUploadChunkMessage, FileUploadCompleteMessage } from '../../types/types'
|
|
8
|
+
import type { Plugin, PluginContext } from '../../plugins/types'
|
|
9
|
+
import { t } from 'elysia'
|
|
10
|
+
import path from 'path'
|
|
11
|
+
|
|
12
|
+
export const liveComponentsPlugin: Plugin = {
|
|
13
|
+
name: 'live-components',
|
|
14
|
+
version: '1.0.0',
|
|
15
|
+
description: 'Real-time Live Components with Elysia native WebSocket support',
|
|
16
|
+
author: 'FluxStack Team',
|
|
17
|
+
priority: 'normal',
|
|
18
|
+
category: 'core',
|
|
19
|
+
tags: ['websocket', 'real-time', 'live-components'],
|
|
20
|
+
|
|
21
|
+
setup: async (context: PluginContext) => {
|
|
22
|
+
context.logger.debug('🔌 Setting up Live Components plugin with Elysia WebSocket...')
|
|
23
|
+
|
|
24
|
+
// Auto-discover components from app/server/live directory
|
|
25
|
+
const componentsPath = path.join(process.cwd(), 'app', 'server', 'live')
|
|
26
|
+
await componentRegistry.autoDiscoverComponents(componentsPath)
|
|
27
|
+
context.logger.debug('🔍 Component auto-discovery completed')
|
|
28
|
+
|
|
29
|
+
// Add WebSocket route for Live Components
|
|
30
|
+
context.app
|
|
31
|
+
.ws('/api/live/ws', {
|
|
32
|
+
body: t.Object({
|
|
33
|
+
type: t.String(),
|
|
34
|
+
componentId: t.String(),
|
|
35
|
+
action: t.Optional(t.String()),
|
|
36
|
+
payload: t.Optional(t.Any()),
|
|
37
|
+
timestamp: t.Optional(t.Number()),
|
|
38
|
+
userId: t.Optional(t.String()),
|
|
39
|
+
room: t.Optional(t.String()),
|
|
40
|
+
requestId: t.Optional(t.String()),
|
|
41
|
+
expectResponse: t.Optional(t.Boolean()),
|
|
42
|
+
// File upload specific fields
|
|
43
|
+
uploadId: t.Optional(t.String()),
|
|
44
|
+
filename: t.Optional(t.String()),
|
|
45
|
+
fileType: t.Optional(t.String()),
|
|
46
|
+
fileSize: t.Optional(t.Number()),
|
|
47
|
+
chunkSize: t.Optional(t.Number()),
|
|
48
|
+
chunkIndex: t.Optional(t.Number()),
|
|
49
|
+
totalChunks: t.Optional(t.Number()),
|
|
50
|
+
data: t.Optional(t.String()),
|
|
51
|
+
hash: t.Optional(t.String())
|
|
52
|
+
}),
|
|
53
|
+
|
|
54
|
+
open(ws) {
|
|
55
|
+
const connectionId = `ws-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
|
|
56
|
+
console.log(`🔌 Live Components WebSocket connected: ${connectionId}`)
|
|
57
|
+
|
|
58
|
+
// Register connection with enhanced connection manager
|
|
59
|
+
connectionManager.registerConnection(ws, connectionId, 'live-components')
|
|
60
|
+
|
|
61
|
+
// Initialize and store connection data in ws.data
|
|
62
|
+
if (!ws.data) {
|
|
63
|
+
ws.data = {}
|
|
64
|
+
}
|
|
65
|
+
ws.data.connectionId = connectionId
|
|
66
|
+
ws.data.components = new Map()
|
|
67
|
+
ws.data.subscriptions = new Set()
|
|
68
|
+
ws.data.connectedAt = new Date()
|
|
69
|
+
|
|
70
|
+
// Send connection confirmation
|
|
71
|
+
ws.send(JSON.stringify({
|
|
72
|
+
type: 'CONNECTION_ESTABLISHED',
|
|
73
|
+
connectionId,
|
|
74
|
+
timestamp: Date.now(),
|
|
75
|
+
features: {
|
|
76
|
+
compression: true,
|
|
77
|
+
encryption: true,
|
|
78
|
+
offlineQueue: true,
|
|
79
|
+
loadBalancing: true
|
|
80
|
+
}
|
|
81
|
+
}))
|
|
82
|
+
},
|
|
83
|
+
|
|
84
|
+
async message(ws, message: LiveMessage) {
|
|
85
|
+
try {
|
|
86
|
+
// Add connection metadata
|
|
87
|
+
message.timestamp = Date.now()
|
|
88
|
+
|
|
89
|
+
console.log(`📨 Received message:`, {
|
|
90
|
+
type: message.type,
|
|
91
|
+
componentId: message.componentId,
|
|
92
|
+
action: message.action,
|
|
93
|
+
requestId: message.requestId
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
// Handle different message types
|
|
97
|
+
switch (message.type) {
|
|
98
|
+
case 'COMPONENT_MOUNT':
|
|
99
|
+
await handleComponentMount(ws, message)
|
|
100
|
+
break
|
|
101
|
+
case 'COMPONENT_REHYDRATE':
|
|
102
|
+
await handleComponentRehydrate(ws, message)
|
|
103
|
+
break
|
|
104
|
+
case 'COMPONENT_UNMOUNT':
|
|
105
|
+
await handleComponentUnmount(ws, message)
|
|
106
|
+
break
|
|
107
|
+
case 'CALL_ACTION':
|
|
108
|
+
await handleActionCall(ws, message)
|
|
109
|
+
break
|
|
110
|
+
case 'PROPERTY_UPDATE':
|
|
111
|
+
await handlePropertyUpdate(ws, message)
|
|
112
|
+
break
|
|
113
|
+
case 'FILE_UPLOAD_START':
|
|
114
|
+
await handleFileUploadStart(ws, message as FileUploadStartMessage)
|
|
115
|
+
break
|
|
116
|
+
case 'FILE_UPLOAD_CHUNK':
|
|
117
|
+
await handleFileUploadChunk(ws, message as FileUploadChunkMessage)
|
|
118
|
+
break
|
|
119
|
+
case 'FILE_UPLOAD_COMPLETE':
|
|
120
|
+
await handleFileUploadComplete(ws, message as FileUploadCompleteMessage)
|
|
121
|
+
break
|
|
122
|
+
default:
|
|
123
|
+
console.warn(`❌ Unknown message type: ${message.type}`)
|
|
124
|
+
ws.send(JSON.stringify({
|
|
125
|
+
type: 'ERROR',
|
|
126
|
+
error: `Unknown message type: ${message.type}`,
|
|
127
|
+
timestamp: Date.now()
|
|
128
|
+
}))
|
|
129
|
+
}
|
|
130
|
+
} catch (error) {
|
|
131
|
+
console.error('❌ WebSocket message error:', error)
|
|
132
|
+
ws.send(JSON.stringify({
|
|
133
|
+
type: 'ERROR',
|
|
134
|
+
error: error instanceof Error ? error.message : 'Unknown error',
|
|
135
|
+
timestamp: Date.now()
|
|
136
|
+
}))
|
|
137
|
+
}
|
|
138
|
+
},
|
|
139
|
+
|
|
140
|
+
close(ws) {
|
|
141
|
+
const connectionId = ws.data?.connectionId
|
|
142
|
+
console.log(`🔌 Live Components WebSocket disconnected: ${connectionId}`)
|
|
143
|
+
|
|
144
|
+
// Cleanup connection in connection manager
|
|
145
|
+
if (connectionId) {
|
|
146
|
+
connectionManager.cleanupConnection(connectionId)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Cleanup components for this connection
|
|
150
|
+
componentRegistry.cleanupConnection(ws)
|
|
151
|
+
}
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
// Add Live Components info routes
|
|
155
|
+
.get('/api/live/websocket-info', () => {
|
|
156
|
+
return {
|
|
157
|
+
success: true,
|
|
158
|
+
message: 'Live Components WebSocket available via Elysia',
|
|
159
|
+
endpoint: 'ws://localhost:3000/api/live/ws',
|
|
160
|
+
status: 'running',
|
|
161
|
+
connectionManager: connectionManager.getSystemStats()
|
|
162
|
+
}
|
|
163
|
+
})
|
|
164
|
+
.get('/api/live/stats', () => {
|
|
165
|
+
const stats = componentRegistry.getStats()
|
|
166
|
+
return {
|
|
167
|
+
success: true,
|
|
168
|
+
stats,
|
|
169
|
+
timestamp: new Date().toISOString()
|
|
170
|
+
}
|
|
171
|
+
})
|
|
172
|
+
.get('/api/live/health', () => {
|
|
173
|
+
return {
|
|
174
|
+
success: true,
|
|
175
|
+
service: 'FluxStack Live Components',
|
|
176
|
+
status: 'operational',
|
|
177
|
+
components: componentRegistry.getStats().components,
|
|
178
|
+
connections: connectionManager.getSystemStats(),
|
|
179
|
+
uptime: process.uptime(),
|
|
180
|
+
timestamp: new Date().toISOString()
|
|
181
|
+
}
|
|
182
|
+
})
|
|
183
|
+
.get('/api/live/connections', () => {
|
|
184
|
+
return {
|
|
185
|
+
success: true,
|
|
186
|
+
connections: connectionManager.getAllConnectionMetrics(),
|
|
187
|
+
systemStats: connectionManager.getSystemStats(),
|
|
188
|
+
timestamp: new Date().toISOString()
|
|
189
|
+
}
|
|
190
|
+
})
|
|
191
|
+
.get('/api/live/connections/:connectionId', ({ params }) => {
|
|
192
|
+
const metrics = connectionManager.getConnectionMetrics(params.connectionId)
|
|
193
|
+
if (!metrics) {
|
|
194
|
+
return {
|
|
195
|
+
success: false,
|
|
196
|
+
error: 'Connection not found'
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return {
|
|
200
|
+
success: true,
|
|
201
|
+
connection: metrics,
|
|
202
|
+
timestamp: new Date().toISOString()
|
|
203
|
+
}
|
|
204
|
+
})
|
|
205
|
+
.get('/api/live/pools/:poolId/stats', ({ params }) => {
|
|
206
|
+
const stats = connectionManager.getPoolStats(params.poolId)
|
|
207
|
+
if (!stats) {
|
|
208
|
+
return {
|
|
209
|
+
success: false,
|
|
210
|
+
error: 'Pool not found'
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return {
|
|
214
|
+
success: true,
|
|
215
|
+
pool: params.poolId,
|
|
216
|
+
stats,
|
|
217
|
+
timestamp: new Date().toISOString()
|
|
218
|
+
}
|
|
219
|
+
})
|
|
220
|
+
.get('/api/live/performance/dashboard', () => {
|
|
221
|
+
return {
|
|
222
|
+
success: true,
|
|
223
|
+
dashboard: performanceMonitor.generateDashboard(),
|
|
224
|
+
timestamp: new Date().toISOString()
|
|
225
|
+
}
|
|
226
|
+
})
|
|
227
|
+
.get('/api/live/performance/components/:componentId', ({ params }) => {
|
|
228
|
+
const metrics = performanceMonitor.getComponentMetrics(params.componentId)
|
|
229
|
+
if (!metrics) {
|
|
230
|
+
return {
|
|
231
|
+
success: false,
|
|
232
|
+
error: 'Component metrics not found'
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const alerts = performanceMonitor.getComponentAlerts(params.componentId)
|
|
237
|
+
const suggestions = performanceMonitor.getComponentSuggestions(params.componentId)
|
|
238
|
+
|
|
239
|
+
return {
|
|
240
|
+
success: true,
|
|
241
|
+
component: params.componentId,
|
|
242
|
+
metrics,
|
|
243
|
+
alerts,
|
|
244
|
+
suggestions,
|
|
245
|
+
timestamp: new Date().toISOString()
|
|
246
|
+
}
|
|
247
|
+
})
|
|
248
|
+
.post('/api/live/performance/alerts/:alertId/resolve', ({ params }) => {
|
|
249
|
+
const resolved = performanceMonitor.resolveAlert(params.alertId)
|
|
250
|
+
return {
|
|
251
|
+
success: resolved,
|
|
252
|
+
message: resolved ? 'Alert resolved' : 'Alert not found',
|
|
253
|
+
timestamp: new Date().toISOString()
|
|
254
|
+
}
|
|
255
|
+
})
|
|
256
|
+
},
|
|
257
|
+
|
|
258
|
+
onServerStart: async (context: PluginContext) => {
|
|
259
|
+
context.logger.debug('🔌 Live Components WebSocket ready on /api/live/ws')
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Handler functions for WebSocket messages
|
|
264
|
+
async function handleComponentMount(ws: any, message: LiveMessage) {
|
|
265
|
+
const result = await componentRegistry.handleMessage(ws, message)
|
|
266
|
+
|
|
267
|
+
if (result !== null) {
|
|
268
|
+
const response = {
|
|
269
|
+
type: 'COMPONENT_MOUNTED',
|
|
270
|
+
componentId: message.componentId,
|
|
271
|
+
success: result.success,
|
|
272
|
+
result: result.result,
|
|
273
|
+
error: result.error,
|
|
274
|
+
requestId: message.requestId,
|
|
275
|
+
timestamp: Date.now()
|
|
276
|
+
}
|
|
277
|
+
ws.send(JSON.stringify(response))
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
async function handleComponentRehydrate(ws: any, message: LiveMessage) {
|
|
282
|
+
console.log('🔄 Processing component re-hydration request:', {
|
|
283
|
+
componentId: message.componentId,
|
|
284
|
+
payload: message.payload
|
|
285
|
+
})
|
|
286
|
+
|
|
287
|
+
try {
|
|
288
|
+
const { componentName, signedState, room, userId } = message.payload || {}
|
|
289
|
+
|
|
290
|
+
if (!componentName || !signedState) {
|
|
291
|
+
throw new Error('Missing componentName or signedState in rehydration payload')
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const result = await componentRegistry.rehydrateComponent(
|
|
295
|
+
message.componentId,
|
|
296
|
+
componentName,
|
|
297
|
+
signedState,
|
|
298
|
+
ws,
|
|
299
|
+
{ room, userId }
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
const response = {
|
|
303
|
+
type: 'COMPONENT_REHYDRATED',
|
|
304
|
+
componentId: message.componentId,
|
|
305
|
+
success: result.success,
|
|
306
|
+
result: {
|
|
307
|
+
newComponentId: result.newComponentId,
|
|
308
|
+
...result
|
|
309
|
+
},
|
|
310
|
+
error: result.error,
|
|
311
|
+
requestId: message.requestId,
|
|
312
|
+
timestamp: Date.now()
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
console.log('📤 Sending COMPONENT_REHYDRATED response:', {
|
|
316
|
+
type: response.type,
|
|
317
|
+
success: response.success,
|
|
318
|
+
newComponentId: response.result?.newComponentId,
|
|
319
|
+
requestId: response.requestId
|
|
320
|
+
})
|
|
321
|
+
|
|
322
|
+
ws.send(JSON.stringify(response))
|
|
323
|
+
|
|
324
|
+
} catch (error: any) {
|
|
325
|
+
console.error('❌ Re-hydration handler error:', error.message)
|
|
326
|
+
|
|
327
|
+
const errorResponse = {
|
|
328
|
+
type: 'COMPONENT_REHYDRATED',
|
|
329
|
+
componentId: message.componentId,
|
|
330
|
+
success: false,
|
|
331
|
+
error: error.message,
|
|
332
|
+
requestId: message.requestId,
|
|
333
|
+
timestamp: Date.now()
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
ws.send(JSON.stringify(errorResponse))
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
async function handleComponentUnmount(ws: any, message: LiveMessage) {
|
|
341
|
+
const result = await componentRegistry.handleMessage(ws, message)
|
|
342
|
+
|
|
343
|
+
if (result !== null) {
|
|
344
|
+
const response = {
|
|
345
|
+
type: 'COMPONENT_UNMOUNTED',
|
|
346
|
+
componentId: message.componentId,
|
|
347
|
+
success: result.success,
|
|
348
|
+
requestId: message.requestId,
|
|
349
|
+
timestamp: Date.now()
|
|
350
|
+
}
|
|
351
|
+
ws.send(JSON.stringify(response))
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
async function handleActionCall(ws: any, message: LiveMessage) {
|
|
356
|
+
const result = await componentRegistry.handleMessage(ws, message)
|
|
357
|
+
|
|
358
|
+
if (result !== null) {
|
|
359
|
+
const response = {
|
|
360
|
+
type: message.expectResponse ? 'ACTION_RESPONSE' : 'MESSAGE_RESPONSE',
|
|
361
|
+
originalType: message.type,
|
|
362
|
+
componentId: message.componentId,
|
|
363
|
+
success: result.success,
|
|
364
|
+
result: result.result,
|
|
365
|
+
error: result.error,
|
|
366
|
+
requestId: message.requestId,
|
|
367
|
+
timestamp: Date.now()
|
|
368
|
+
}
|
|
369
|
+
ws.send(JSON.stringify(response))
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
async function handlePropertyUpdate(ws: any, message: LiveMessage) {
|
|
374
|
+
const result = await componentRegistry.handleMessage(ws, message)
|
|
375
|
+
|
|
376
|
+
if (result !== null) {
|
|
377
|
+
const response = {
|
|
378
|
+
type: 'PROPERTY_UPDATED',
|
|
379
|
+
componentId: message.componentId,
|
|
380
|
+
success: result.success,
|
|
381
|
+
result: result.result,
|
|
382
|
+
error: result.error,
|
|
383
|
+
requestId: message.requestId,
|
|
384
|
+
timestamp: Date.now()
|
|
385
|
+
}
|
|
386
|
+
ws.send(JSON.stringify(response))
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// File Upload Handler Functions
|
|
391
|
+
async function handleFileUploadStart(ws: any, message: FileUploadStartMessage) {
|
|
392
|
+
console.log('📤 Starting file upload:', message.uploadId)
|
|
393
|
+
|
|
394
|
+
const result = await fileUploadManager.startUpload(message)
|
|
395
|
+
|
|
396
|
+
const response = {
|
|
397
|
+
type: 'FILE_UPLOAD_START_RESPONSE',
|
|
398
|
+
componentId: message.componentId,
|
|
399
|
+
uploadId: message.uploadId,
|
|
400
|
+
success: result.success,
|
|
401
|
+
error: result.error,
|
|
402
|
+
requestId: message.requestId,
|
|
403
|
+
timestamp: Date.now()
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
ws.send(JSON.stringify(response))
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
async function handleFileUploadChunk(ws: any, message: FileUploadChunkMessage) {
|
|
410
|
+
console.log(`📦 Receiving chunk ${message.chunkIndex + 1} for upload ${message.uploadId}`)
|
|
411
|
+
|
|
412
|
+
const progressResponse = await fileUploadManager.receiveChunk(message, ws)
|
|
413
|
+
|
|
414
|
+
if (progressResponse) {
|
|
415
|
+
ws.send(JSON.stringify(progressResponse))
|
|
416
|
+
} else {
|
|
417
|
+
// Send error response
|
|
418
|
+
const errorResponse = {
|
|
419
|
+
type: 'FILE_UPLOAD_ERROR',
|
|
420
|
+
componentId: message.componentId,
|
|
421
|
+
uploadId: message.uploadId,
|
|
422
|
+
error: 'Failed to process chunk',
|
|
423
|
+
requestId: message.requestId,
|
|
424
|
+
timestamp: Date.now()
|
|
425
|
+
}
|
|
426
|
+
ws.send(JSON.stringify(errorResponse))
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
async function handleFileUploadComplete(ws: any, message: FileUploadCompleteMessage) {
|
|
431
|
+
console.log('✅ Completing file upload:', message.uploadId)
|
|
432
|
+
|
|
433
|
+
const completeResponse = await fileUploadManager.completeUpload(message)
|
|
434
|
+
ws.send(JSON.stringify(completeResponse))
|
|
435
435
|
}
|