create-fluxstack 1.14.0 → 1.16.0

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.
Files changed (76) hide show
  1. package/LLMD/INDEX.md +4 -3
  2. package/LLMD/resources/live-binary-delta.md +507 -0
  3. package/LLMD/resources/live-components.md +208 -12
  4. package/LLMD/resources/live-rooms.md +731 -333
  5. package/app/client/.live-stubs/LiveAdminPanel.js +5 -0
  6. package/app/client/.live-stubs/LiveCounter.js +9 -0
  7. package/app/client/.live-stubs/LiveForm.js +11 -0
  8. package/app/client/.live-stubs/LiveLocalCounter.js +8 -0
  9. package/app/client/.live-stubs/LivePingPong.js +10 -0
  10. package/app/client/.live-stubs/LiveRoomChat.js +11 -0
  11. package/app/client/.live-stubs/LiveSharedCounter.js +10 -0
  12. package/app/client/.live-stubs/LiveUpload.js +15 -0
  13. package/app/client/src/App.tsx +19 -7
  14. package/app/client/src/components/AppLayout.tsx +18 -10
  15. package/app/client/src/live/PingPongDemo.tsx +199 -0
  16. package/app/client/src/live/RoomChatDemo.tsx +187 -22
  17. package/app/client/src/live/SharedCounterDemo.tsx +142 -0
  18. package/app/server/auth/DevAuthProvider.ts +2 -2
  19. package/app/server/auth/JWTAuthProvider.example.ts +2 -2
  20. package/app/server/index.ts +2 -2
  21. package/app/server/live/LiveAdminPanel.ts +1 -1
  22. package/app/server/live/LivePingPong.ts +61 -0
  23. package/app/server/live/LiveProtectedChat.ts +1 -1
  24. package/app/server/live/LiveRoomChat.ts +106 -38
  25. package/app/server/live/LiveSharedCounter.ts +73 -0
  26. package/app/server/live/rooms/ChatRoom.ts +68 -0
  27. package/app/server/live/rooms/CounterRoom.ts +51 -0
  28. package/app/server/live/rooms/DirectoryRoom.ts +42 -0
  29. package/app/server/live/rooms/PingRoom.ts +40 -0
  30. package/app/server/routes/room.routes.ts +1 -2
  31. package/core/build/live-components-generator.ts +11 -2
  32. package/core/build/vite-plugins.ts +28 -0
  33. package/core/client/hooks/useLiveUpload.ts +3 -4
  34. package/core/client/index.ts +25 -35
  35. package/core/framework/server.ts +1 -1
  36. package/core/server/index.ts +1 -2
  37. package/core/server/live/auto-generated-components.ts +5 -8
  38. package/core/server/live/index.ts +90 -21
  39. package/core/server/live/websocket-plugin.ts +54 -1079
  40. package/core/types/types.ts +76 -1025
  41. package/core/utils/version.ts +1 -1
  42. package/create-fluxstack.ts +1 -1
  43. package/package.json +100 -95
  44. package/plugins/crypto-auth/index.ts +1 -1
  45. package/plugins/crypto-auth/server/CryptoAuthLiveProvider.ts +2 -2
  46. package/tsconfig.json +4 -1
  47. package/vite.config.ts +40 -12
  48. package/app/client/src/live/ChatDemo.tsx +0 -107
  49. package/app/client/src/live/LiveDebuggerPanel.tsx +0 -779
  50. package/app/server/live/LiveChat.ts +0 -78
  51. package/core/client/LiveComponentsProvider.tsx +0 -531
  52. package/core/client/components/Live.tsx +0 -111
  53. package/core/client/components/LiveDebugger.tsx +0 -1324
  54. package/core/client/hooks/AdaptiveChunkSizer.ts +0 -215
  55. package/core/client/hooks/state-validator.ts +0 -130
  56. package/core/client/hooks/useChunkedUpload.ts +0 -359
  57. package/core/client/hooks/useLiveChunkedUpload.ts +0 -87
  58. package/core/client/hooks/useLiveComponent.ts +0 -853
  59. package/core/client/hooks/useLiveDebugger.ts +0 -392
  60. package/core/client/hooks/useRoom.ts +0 -409
  61. package/core/client/hooks/useRoomProxy.ts +0 -382
  62. package/core/server/live/ComponentRegistry.ts +0 -1128
  63. package/core/server/live/FileUploadManager.ts +0 -446
  64. package/core/server/live/LiveComponentPerformanceMonitor.ts +0 -931
  65. package/core/server/live/LiveDebugger.ts +0 -462
  66. package/core/server/live/LiveLogger.ts +0 -144
  67. package/core/server/live/LiveRoomManager.ts +0 -278
  68. package/core/server/live/RoomEventBus.ts +0 -234
  69. package/core/server/live/RoomStateManager.ts +0 -172
  70. package/core/server/live/SingleConnectionManager.ts +0 -0
  71. package/core/server/live/StateSignature.ts +0 -705
  72. package/core/server/live/WebSocketConnectionManager.ts +0 -710
  73. package/core/server/live/auth/LiveAuthContext.ts +0 -71
  74. package/core/server/live/auth/LiveAuthManager.ts +0 -304
  75. package/core/server/live/auth/index.ts +0 -19
  76. package/core/server/live/auth/types.ts +0 -179
@@ -1,1108 +1,83 @@
1
- // 🔥 FluxStack Live Components - Enhanced WebSocket Plugin with Connection Management
1
+ // FluxStack Live Components Plugin delegates to @fluxstack/live
2
2
 
3
- import { componentRegistry } from './ComponentRegistry'
4
- import { fileUploadManager } from './FileUploadManager'
5
- import { connectionManager } from './WebSocketConnectionManager'
6
- import { performanceMonitor } from './LiveComponentPerformanceMonitor'
7
- import { liveRoomManager, type RoomMessage } from './LiveRoomManager'
8
- import { liveAuthManager } from './auth/LiveAuthManager'
9
- import { ANONYMOUS_CONTEXT } from './auth/LiveAuthContext'
10
- import type { LiveMessage, FileUploadStartMessage, FileUploadChunkMessage, FileUploadCompleteMessage, BinaryChunkHeader, FluxStackWebSocket, FluxStackWSData } from '@core/types/types'
11
- import type { Plugin, PluginContext } from '@core/index'
12
- import { t, Elysia } from 'elysia'
3
+ import { LiveServer, RoomRegistry } from '@fluxstack/live'
4
+ import type { LiveAuthProvider, LiveRoomClass } from '@fluxstack/live'
5
+ import { ElysiaTransport } from '@fluxstack/live-elysia'
6
+ import type { Plugin, PluginContext } from '@core/plugins/types'
13
7
  import path from 'path'
14
- import { liveLog } from './LiveLogger'
15
- import { liveDebugger } from './LiveDebugger'
8
+ import { readdirSync, existsSync } from 'fs'
16
9
 
17
- // ===== Response Schemas for Live Components Routes =====
10
+ // Expose the LiveServer instance so other parts of FluxStack can access it
11
+ export let liveServer: LiveServer | null = null
18
12
 
19
- const LiveWebSocketInfoSchema = t.Object({
20
- success: t.Boolean(),
21
- message: t.String(),
22
- endpoint: t.String(),
23
- status: t.String(),
24
- connectionManager: t.Any()
25
- }, {
26
- description: 'WebSocket connection information and system statistics'
27
- })
28
-
29
- const LiveStatsSchema = t.Object({
30
- success: t.Boolean(),
31
- stats: t.Any(),
32
- timestamp: t.String()
33
- }, {
34
- description: 'Live Components statistics including registered components and instances'
35
- })
36
-
37
- const LiveHealthSchema = t.Object({
38
- success: t.Boolean(),
39
- service: t.String(),
40
- status: t.String(),
41
- components: t.Number(),
42
- connections: t.Any(),
43
- uptime: t.Number(),
44
- timestamp: t.String()
45
- }, {
46
- description: 'Health status of Live Components service'
47
- })
48
-
49
- const LiveConnectionsSchema = t.Object({
50
- success: t.Boolean(),
51
- connections: t.Array(t.Any()),
52
- systemStats: t.Any(),
53
- timestamp: t.String()
54
- }, {
55
- description: 'List of all active WebSocket connections with metrics'
56
- })
57
-
58
- const LiveConnectionDetailsSchema = t.Union([
59
- t.Object({
60
- success: t.Literal(true),
61
- connection: t.Any(),
62
- timestamp: t.String()
63
- }),
64
- t.Object({
65
- success: t.Literal(false),
66
- error: t.String()
67
- })
68
- ], {
69
- description: 'Detailed metrics for a specific connection'
70
- })
71
-
72
- const LivePoolStatsSchema = t.Union([
73
- t.Object({
74
- success: t.Literal(true),
75
- pool: t.String(),
76
- stats: t.Any(),
77
- timestamp: t.String()
78
- }),
79
- t.Object({
80
- success: t.Literal(false),
81
- error: t.String()
82
- })
83
- ], {
84
- description: 'Statistics for a specific connection pool'
85
- })
86
-
87
- const LivePerformanceDashboardSchema = t.Object({
88
- success: t.Boolean(),
89
- dashboard: t.Any(),
90
- timestamp: t.String()
91
- }, {
92
- description: 'Performance monitoring dashboard data'
93
- })
94
-
95
- const LiveComponentMetricsSchema = t.Union([
96
- t.Object({
97
- success: t.Literal(true),
98
- component: t.String(),
99
- metrics: t.Any(),
100
- alerts: t.Array(t.Any()),
101
- suggestions: t.Array(t.Any()),
102
- timestamp: t.String()
103
- }),
104
- t.Object({
105
- success: t.Literal(false),
106
- error: t.String()
107
- })
108
- ], {
109
- description: 'Performance metrics, alerts and suggestions for a specific component'
110
- })
111
-
112
- const LiveAlertResolveSchema = t.Object({
113
- success: t.Boolean(),
114
- message: t.String(),
115
- timestamp: t.String()
116
- }, {
117
- description: 'Result of alert resolution operation'
118
- })
119
-
120
- // 🔒 Per-connection rate limiter to prevent WebSocket message flooding
121
- class ConnectionRateLimiter {
122
- private tokens: number
123
- private lastRefill: number
124
- private readonly maxTokens: number
125
- private readonly refillRate: number // tokens per second
126
-
127
- constructor(maxTokens = 100, refillRate = 50) {
128
- this.maxTokens = maxTokens
129
- this.tokens = maxTokens
130
- this.refillRate = refillRate
131
- this.lastRefill = Date.now()
132
- }
133
-
134
- tryConsume(count = 1): boolean {
135
- this.refill()
136
- if (this.tokens >= count) {
137
- this.tokens -= count
138
- return true
139
- }
140
- return false
141
- }
142
-
143
- private refill(): void {
144
- const now = Date.now()
145
- const elapsed = (now - this.lastRefill) / 1000
146
- this.tokens = Math.min(this.maxTokens, this.tokens + elapsed * this.refillRate)
147
- this.lastRefill = now
148
- }
149
- }
150
-
151
- const connectionRateLimiters = new Map<string, ConnectionRateLimiter>()
13
+ // Queue for auth providers registered before LiveServer is created
14
+ export const pendingAuthProviders: LiveAuthProvider[] = []
15
+ // Queue for room classes registered before LiveServer is created
16
+ export const pendingRoomClasses: LiveRoomClass[] = []
152
17
 
153
18
  export const liveComponentsPlugin: Plugin = {
154
19
  name: 'live-components',
155
- version: '1.0.0',
156
- description: 'Real-time Live Components with Elysia native WebSocket support',
20
+ version: '2.0.0',
21
+ description: 'Real-time Live Components powered by @fluxstack/live',
157
22
  author: 'FluxStack Team',
158
23
  priority: 'normal',
159
24
  category: 'core',
160
25
  tags: ['websocket', 'real-time', 'live-components'],
161
-
26
+
162
27
  setup: async (context: PluginContext) => {
163
- context.logger.debug('🔌 Setting up Live Components plugin with Elysia WebSocket...')
164
-
165
- // Auto-discover components from app/server/live directory
28
+ const transport = new ElysiaTransport(context.app)
166
29
  const componentsPath = path.join(process.cwd(), 'app', 'server', 'live')
167
- await componentRegistry.autoDiscoverComponents(componentsPath)
168
- context.logger.debug('🔍 Component auto-discovery completed')
169
-
170
- // Create grouped routes for Live Components with documentation
171
- const liveRoutes = new Elysia({ prefix: '/api/live', tags: ['Live Components'] })
172
- // WebSocket route - supports both JSON and binary messages
173
- .ws('/ws', {
174
- // Use t.Any() to allow both JSON objects and binary data
175
- // Binary messages will be ArrayBuffer/Uint8Array, JSON will be parsed objects
176
- body: t.Any(),
177
-
178
- async open(ws) {
179
- const socket = ws as unknown as FluxStackWebSocket
180
- const connectionId = `ws-${crypto.randomUUID()}`
181
- liveLog('websocket', null, `🔌 Live Components WebSocket connected: ${connectionId}`)
182
-
183
- // 🔒 Initialize rate limiter for this connection
184
- connectionRateLimiters.set(connectionId, new ConnectionRateLimiter())
185
-
186
- // Register connection with enhanced connection manager
187
- connectionManager.registerConnection(ws as unknown as FluxStackWebSocket, connectionId, 'live-components')
188
-
189
- // Initialize and store connection data in ws.data
190
- const wsData: FluxStackWSData = {
191
- connectionId,
192
- components: new Map(),
193
- subscriptions: new Set(),
194
- connectedAt: new Date()
195
- }
196
-
197
- // Assign data to websocket (Elysia creates ws.data from context)
198
- if (!socket.data) {
199
- (socket as { data: FluxStackWSData }).data = wsData
200
- } else {
201
- socket.data.connectionId = connectionId
202
- socket.data.components = new Map()
203
- socket.data.subscriptions = new Set()
204
- socket.data.connectedAt = new Date()
205
- }
206
-
207
- // 🔒 Try to authenticate from query params (token=xxx)
208
- try {
209
- const query = (ws as any).data?.query as Record<string, string> | undefined
210
- const token = query?.token
211
- if (token && liveAuthManager.hasProviders()) {
212
- const authContext = await liveAuthManager.authenticate({ token })
213
- socket.data.authContext = authContext
214
- if (authContext.authenticated) {
215
- socket.data.userId = authContext.user?.id
216
- liveLog('websocket', null, `🔒 WebSocket authenticated via query: user=${authContext.user?.id}`)
217
- } else {
218
- // 🔒 Log failed auth attempts (token was provided but auth failed)
219
- liveLog('websocket', null, `🔒 WebSocket authentication failed via query token`)
220
- }
221
- }
222
- } catch (authError) {
223
- // 🔒 Log auth errors instead of silently ignoring them
224
- console.warn('🔒 WebSocket query auth error:', authError instanceof Error ? authError.message : 'Unknown error')
225
- }
226
-
227
- // Debug: track connection
228
- liveDebugger.trackConnection(connectionId)
229
-
230
- // Send connection confirmation
231
- ws.send(JSON.stringify({
232
- type: 'CONNECTION_ESTABLISHED',
233
- connectionId,
234
- timestamp: Date.now(),
235
- authenticated: socket.data.authContext?.authenticated ?? false,
236
- userId: socket.data.authContext?.user?.id,
237
- features: {
238
- compression: true,
239
- encryption: true,
240
- offlineQueue: true,
241
- loadBalancing: true,
242
- auth: liveAuthManager.hasProviders()
243
- }
244
- }))
245
- },
246
-
247
- async message(ws: unknown, rawMessage: LiveMessage | ArrayBuffer | Uint8Array) {
248
- const socket = ws as FluxStackWebSocket
249
- try {
250
- // 🔒 Rate limiting: reject messages if connection exceeds rate limit
251
- const connId = socket.data?.connectionId
252
- if (connId) {
253
- const limiter = connectionRateLimiters.get(connId)
254
- if (limiter && !limiter.tryConsume()) {
255
- socket.send(JSON.stringify({
256
- type: 'ERROR',
257
- error: 'Rate limit exceeded. Please slow down.',
258
- timestamp: Date.now()
259
- }))
260
- return
261
- }
262
- }
263
-
264
- let message: LiveMessage
265
- let binaryChunkData: Buffer | null = null
266
-
267
- // Check if this is a binary message (file upload chunk)
268
- if (rawMessage instanceof ArrayBuffer || rawMessage instanceof Uint8Array) {
269
- // Binary protocol: [4 bytes header length][JSON header][binary data]
270
- const buffer = rawMessage instanceof ArrayBuffer
271
- ? Buffer.from(rawMessage)
272
- : Buffer.from(rawMessage.buffer, rawMessage.byteOffset, rawMessage.byteLength)
273
-
274
- // Read header length (first 4 bytes, little-endian)
275
- const headerLength = buffer.readUInt32LE(0)
276
-
277
- // Extract and parse JSON header
278
- const headerJson = buffer.slice(4, 4 + headerLength).toString('utf-8')
279
- const header = JSON.parse(headerJson) as BinaryChunkHeader
280
-
281
- // Extract binary chunk data
282
- binaryChunkData = buffer.slice(4 + headerLength)
283
-
284
- liveLog('messages', null, `📦 Binary chunk received: ${binaryChunkData.length} bytes for upload ${header.uploadId}`)
285
-
286
- // Create message with binary data attached
287
- message = {
288
- ...header,
289
- data: binaryChunkData, // Buffer instead of base64 string
290
- timestamp: Date.now()
291
- } as unknown as LiveMessage
292
- } else {
293
- // Regular JSON message
294
- message = rawMessage as LiveMessage
295
- message.timestamp = Date.now()
296
- }
297
-
298
- liveLog('messages', message.componentId || null, `📨 Received message:`, {
299
- type: message.type,
300
- componentId: message.componentId,
301
- action: message.action,
302
- requestId: message.requestId,
303
- isBinary: binaryChunkData !== null
304
- })
305
-
306
- // Handle different message types
307
- switch (message.type) {
308
- case 'COMPONENT_MOUNT':
309
- await handleComponentMount(socket, message)
310
- break
311
- case 'COMPONENT_REHYDRATE':
312
- await handleComponentRehydrate(socket, message)
313
- break
314
- case 'COMPONENT_UNMOUNT':
315
- await handleComponentUnmount(socket, message)
316
- break
317
- case 'CALL_ACTION':
318
- await handleActionCall(socket, message)
319
- break
320
- case 'PROPERTY_UPDATE':
321
- await handlePropertyUpdate(socket, message)
322
- break
323
- case 'COMPONENT_PING':
324
- await handleComponentPing(socket, message)
325
- break
326
- case 'AUTH':
327
- await handleAuth(socket, message)
328
- break
329
- case 'FILE_UPLOAD_START':
330
- await handleFileUploadStart(socket, message as FileUploadStartMessage)
331
- break
332
- case 'FILE_UPLOAD_CHUNK':
333
- await handleFileUploadChunk(socket, message as FileUploadChunkMessage, binaryChunkData)
334
- break
335
- case 'FILE_UPLOAD_COMPLETE':
336
- await handleFileUploadComplete(socket, message as unknown as FileUploadCompleteMessage)
337
- break
338
- // Room system messages
339
- case 'ROOM_JOIN':
340
- await handleRoomJoin(socket, message as unknown as RoomMessage)
341
- break
342
- case 'ROOM_LEAVE':
343
- await handleRoomLeave(socket, message as unknown as RoomMessage)
344
- break
345
- case 'ROOM_EMIT':
346
- await handleRoomEmit(socket, message as unknown as RoomMessage)
347
- break
348
- case 'ROOM_STATE_SET':
349
- await handleRoomStateSet(socket, message as unknown as RoomMessage)
350
- break
351
- default:
352
- console.warn(`❌ Unknown message type: ${message.type}`)
353
- socket.send(JSON.stringify({
354
- type: 'ERROR',
355
- error: `Unknown message type: ${message.type}`,
356
- timestamp: Date.now()
357
- }))
358
- }
359
- } catch (error) {
360
- console.error('❌ WebSocket message error:', error)
361
- socket.send(JSON.stringify({
362
- type: 'ERROR',
363
- error: error instanceof Error ? error.message : 'Unknown error',
364
- timestamp: Date.now()
365
- }))
366
- }
367
- },
368
-
369
- close(ws) {
370
- const socket = ws as unknown as FluxStackWebSocket
371
- const connectionId = socket.data?.connectionId
372
- liveLog('websocket', null, `🔌 Live Components WebSocket disconnected: ${connectionId}`)
373
-
374
- // Debug: track disconnection
375
- const componentCount = socket.data?.components?.size ?? 0
376
- if (connectionId) {
377
- liveDebugger.trackDisconnection(connectionId, componentCount)
378
- }
379
-
380
- // 🔒 Cleanup rate limiter
381
- if (connectionId) {
382
- connectionRateLimiters.delete(connectionId)
383
- }
384
-
385
- // Cleanup connection in connection manager
386
- if (connectionId) {
387
- connectionManager.cleanupConnection(connectionId)
388
- }
389
-
390
- // Cleanup components for this connection
391
- componentRegistry.cleanupConnection(socket)
392
- }
393
- })
394
-
395
- // ===== Live Components Information Routes =====
396
- .get('/websocket-info', () => {
397
- return {
398
- success: true,
399
- message: 'Live Components WebSocket available via Elysia',
400
- endpoint: 'ws://localhost:3000/api/live/ws',
401
- status: 'running',
402
- connectionManager: connectionManager.getSystemStats()
403
- }
404
- }, {
405
- detail: {
406
- summary: 'Get WebSocket Information',
407
- description: 'Returns WebSocket endpoint information and connection manager statistics',
408
- tags: ['Live Components', 'WebSocket']
409
- },
410
- response: LiveWebSocketInfoSchema
411
- })
412
-
413
- .get('/stats', () => {
414
- const stats = componentRegistry.getStats()
415
- return {
416
- success: true,
417
- stats,
418
- timestamp: new Date().toISOString()
419
- }
420
- }, {
421
- detail: {
422
- summary: 'Get Live Components Statistics',
423
- description: 'Returns statistics about registered components and active instances',
424
- tags: ['Live Components', 'Monitoring']
425
- },
426
- response: LiveStatsSchema
427
- })
428
-
429
- .get('/health', () => {
430
- return {
431
- success: true,
432
- service: 'FluxStack Live Components',
433
- status: 'operational',
434
- components: componentRegistry.getStats().components,
435
- connections: connectionManager.getSystemStats(),
436
- uptime: process.uptime(),
437
- timestamp: new Date().toISOString()
438
- }
439
- }, {
440
- detail: {
441
- summary: 'Health Check',
442
- description: 'Returns the health status of the Live Components service',
443
- tags: ['Live Components', 'Health']
444
- },
445
- response: LiveHealthSchema
446
- })
447
-
448
- // ===== Connection Management Routes =====
449
- .get('/connections', () => {
450
- return {
451
- success: true,
452
- connections: connectionManager.getAllConnectionMetrics(),
453
- systemStats: connectionManager.getSystemStats(),
454
- timestamp: new Date().toISOString()
455
- }
456
- }, {
457
- detail: {
458
- summary: 'List All Connections',
459
- description: 'Returns all active WebSocket connections with their metrics',
460
- tags: ['Live Components', 'Connections']
461
- },
462
- response: LiveConnectionsSchema
463
- })
464
-
465
- .get('/connections/:connectionId', ({ params }) => {
466
- const metrics = connectionManager.getConnectionMetrics(params.connectionId)
467
- if (!metrics) {
468
- return {
469
- success: false,
470
- error: 'Connection not found'
471
- }
472
- }
473
- return {
474
- success: true,
475
- connection: metrics,
476
- timestamp: new Date().toISOString()
477
- }
478
- }, {
479
- detail: {
480
- summary: 'Get Connection Details',
481
- description: 'Returns detailed metrics for a specific WebSocket connection',
482
- tags: ['Live Components', 'Connections']
483
- },
484
- params: t.Object({
485
- connectionId: t.String({ description: 'The unique connection identifier' })
486
- }),
487
- response: LiveConnectionDetailsSchema
488
- })
489
-
490
- .get('/pools/:poolId/stats', ({ params }) => {
491
- const stats = connectionManager.getPoolStats(params.poolId)
492
- if (!stats) {
493
- return {
494
- success: false,
495
- error: 'Pool not found'
496
- }
497
- }
498
- return {
499
- success: true,
500
- pool: params.poolId,
501
- stats,
502
- timestamp: new Date().toISOString()
503
- }
504
- }, {
505
- detail: {
506
- summary: 'Get Pool Statistics',
507
- description: 'Returns statistics for a specific connection pool',
508
- tags: ['Live Components', 'Connections', 'Pools']
509
- },
510
- params: t.Object({
511
- poolId: t.String({ description: 'The unique pool identifier' })
512
- }),
513
- response: LivePoolStatsSchema
514
- })
515
-
516
- // ===== Performance Monitoring Routes =====
517
- .get('/performance/dashboard', () => {
518
- return {
519
- success: true,
520
- dashboard: performanceMonitor.generateDashboard(),
521
- timestamp: new Date().toISOString()
522
- }
523
- }, {
524
- detail: {
525
- summary: 'Performance Dashboard',
526
- description: 'Returns comprehensive performance monitoring dashboard data',
527
- tags: ['Live Components', 'Performance']
528
- },
529
- response: LivePerformanceDashboardSchema
530
- })
531
-
532
- .get('/performance/components/:componentId', ({ params }) => {
533
- const metrics = performanceMonitor.getComponentMetrics(params.componentId)
534
- if (!metrics) {
535
- return {
536
- success: false,
537
- error: 'Component metrics not found'
538
- }
539
- }
540
-
541
- const alerts = performanceMonitor.getComponentAlerts(params.componentId)
542
- const suggestions = performanceMonitor.getComponentSuggestions(params.componentId)
543
-
544
- return {
545
- success: true,
546
- component: params.componentId,
547
- metrics,
548
- alerts,
549
- suggestions,
550
- timestamp: new Date().toISOString()
551
- }
552
- }, {
553
- detail: {
554
- summary: 'Get Component Performance Metrics',
555
- description: 'Returns performance metrics, alerts, and optimization suggestions for a specific component',
556
- tags: ['Live Components', 'Performance']
557
- },
558
- params: t.Object({
559
- componentId: t.String({ description: 'The unique component identifier' })
560
- }),
561
- response: LiveComponentMetricsSchema
562
- })
563
-
564
- .post('/performance/alerts/:alertId/resolve', ({ params }) => {
565
- const resolved = performanceMonitor.resolveAlert(params.alertId)
566
- return {
567
- success: resolved,
568
- message: resolved ? 'Alert resolved' : 'Alert not found',
569
- timestamp: new Date().toISOString()
570
- }
571
- }, {
572
- detail: {
573
- summary: 'Resolve Performance Alert',
574
- description: 'Marks a performance alert as resolved',
575
- tags: ['Live Components', 'Performance', 'Alerts']
576
- },
577
- params: t.Object({
578
- alertId: t.String({ description: 'The unique alert identifier' })
579
- }),
580
- response: LiveAlertResolveSchema
581
- })
582
-
583
- // ===== Live Component Debugger Routes =====
584
-
585
- // Debug WebSocket - streams debug events in real-time
586
- .ws('/debug/ws', {
587
- body: t.Any(),
588
-
589
- open(ws) {
590
- const socket = ws as unknown as FluxStackWebSocket
591
- liveLog('websocket', null, '🔍 Debug client connected')
592
- liveDebugger.registerDebugClient(socket)
593
- },
594
-
595
- message() {
596
- // Debug clients are read-only, no incoming messages to handle
597
- },
598
-
599
- close(ws) {
600
- const socket = ws as unknown as FluxStackWebSocket
601
- liveLog('websocket', null, '🔍 Debug client disconnected')
602
- liveDebugger.unregisterDebugClient(socket)
603
- }
604
- })
605
-
606
- // Debug snapshot - current state of all components
607
- .get('/debug/snapshot', () => {
608
- return {
609
- success: true,
610
- snapshot: liveDebugger.getSnapshot(),
611
- timestamp: new Date().toISOString()
612
- }
613
- }, {
614
- detail: {
615
- summary: 'Debug Snapshot',
616
- description: 'Returns current state of all active Live Components for debugging',
617
- tags: ['Live Components', 'Debug']
618
- }
619
- })
620
-
621
- // Debug events - recent event history
622
- .get('/debug/events', ({ query }) => {
623
- const filter: { componentId?: string; type?: any; limit?: number } = {}
624
- if (query.componentId) filter.componentId = query.componentId as string
625
- if (query.type) filter.type = query.type
626
- if (query.limit) filter.limit = parseInt(query.limit as string, 10)
627
-
628
- return {
629
- success: true,
630
- events: liveDebugger.getEvents(filter),
631
- timestamp: new Date().toISOString()
632
- }
633
- }, {
634
- detail: {
635
- summary: 'Debug Events',
636
- description: 'Returns recent debug events, optionally filtered by component or type',
637
- tags: ['Live Components', 'Debug']
638
- },
639
- query: t.Object({
640
- componentId: t.Optional(t.String()),
641
- type: t.Optional(t.String()),
642
- limit: t.Optional(t.String())
643
- })
644
- })
645
-
646
- // Debug toggle - enable/disable debugger at runtime
647
- .post('/debug/toggle', ({ body }) => {
648
- const enabled = (body as any)?.enabled
649
- if (typeof enabled === 'boolean') {
650
- liveDebugger.enabled = enabled
651
- } else {
652
- liveDebugger.enabled = !liveDebugger.enabled
653
- }
654
- return {
655
- success: true,
656
- enabled: liveDebugger.enabled,
657
- timestamp: new Date().toISOString()
658
- }
659
- }, {
660
- detail: {
661
- summary: 'Toggle Debugger',
662
- description: 'Enable or disable the Live Component debugger at runtime',
663
- tags: ['Live Components', 'Debug']
664
- }
665
- })
666
-
667
- // Debug component state - get specific component state
668
- .get('/debug/components/:componentId', ({ params }) => {
669
- const snapshot = liveDebugger.getComponentState(params.componentId)
670
- if (!snapshot) {
671
- return { success: false, error: 'Component not found' }
672
- }
673
- return {
674
- success: true,
675
- component: snapshot,
676
- events: liveDebugger.getEvents({
677
- componentId: params.componentId,
678
- limit: 50
679
- }),
680
- timestamp: new Date().toISOString()
681
- }
682
- }, {
683
- detail: {
684
- summary: 'Debug Component State',
685
- description: 'Returns current state and recent events for a specific component',
686
- tags: ['Live Components', 'Debug']
687
- },
688
- params: t.Object({
689
- componentId: t.String()
690
- })
691
- })
692
-
693
- // Clear debug events
694
- .post('/debug/clear', () => {
695
- liveDebugger.clearEvents()
696
- return {
697
- success: true,
698
- message: 'Debug events cleared',
699
- timestamp: new Date().toISOString()
700
- }
701
- }, {
702
- detail: {
703
- summary: 'Clear Debug Events',
704
- description: 'Clears the debug event history buffer',
705
- tags: ['Live Components', 'Debug']
706
- }
707
- })
708
-
709
- // Register the grouped routes with the main app
710
- context.app.use(liveRoutes)
711
- },
712
-
713
- onServerStart: async (context: PluginContext) => {
714
- context.logger.debug('🔌 Live Components WebSocket ready on /api/live/ws')
715
- }
716
- }
717
-
718
- // Handler functions for WebSocket messages
719
- async function handleComponentMount(ws: FluxStackWebSocket, message: LiveMessage) {
720
- const result = await componentRegistry.handleMessage(ws, message)
721
-
722
- if (result !== null) {
723
- const response = {
724
- type: 'COMPONENT_MOUNTED',
725
- componentId: message.componentId,
726
- success: result.success,
727
- result: result.result,
728
- error: result.error,
729
- requestId: message.requestId,
730
- timestamp: Date.now()
731
- }
732
- ws.send(JSON.stringify(response))
733
- }
734
- }
735
-
736
- async function handleComponentRehydrate(ws: FluxStackWebSocket, message: LiveMessage) {
737
- liveLog('lifecycle', message.componentId, '🔄 Processing component re-hydration request:', {
738
- componentId: message.componentId,
739
- payload: message.payload
740
- })
741
-
742
- try {
743
- const { componentName, signedState, room, userId } = message.payload || {}
744
-
745
- if (!componentName || !signedState) {
746
- throw new Error('Missing componentName or signedState in rehydration payload')
747
- }
748
-
749
- const result = await componentRegistry.rehydrateComponent(
750
- message.componentId,
751
- componentName,
752
- signedState,
753
- ws,
754
- { room, userId }
755
- )
756
30
 
757
- const response = {
758
- type: 'COMPONENT_REHYDRATED',
759
- componentId: message.componentId,
760
- success: result.success,
761
- result: {
762
- newComponentId: result.newComponentId,
763
- ...result
764
- },
765
- error: result.error,
766
- requestId: message.requestId,
767
- timestamp: Date.now()
768
- }
31
+ // Auto-discover LiveRoom classes from rooms/ directory
32
+ const roomsPath = path.join(componentsPath, 'rooms')
33
+ const discoveredRooms = await discoverRoomClasses(roomsPath)
769
34
 
770
- liveLog('lifecycle', message.componentId, '📤 Sending COMPONENT_REHYDRATED response:', {
771
- type: response.type,
772
- success: response.success,
773
- newComponentId: response.result?.newComponentId,
774
- requestId: response.requestId
35
+ liveServer = new LiveServer({
36
+ transport,
37
+ componentsPath,
38
+ wsPath: '/api/live/ws',
39
+ httpPrefix: '/api/live',
40
+ rooms: [...discoveredRooms, ...pendingRoomClasses],
775
41
  })
776
-
777
- ws.send(JSON.stringify(response))
778
42
 
779
- } catch (error: any) {
780
- console.error('❌ Re-hydration handler error:', error.message)
781
-
782
- const errorResponse = {
783
- type: 'COMPONENT_REHYDRATED',
784
- componentId: message.componentId,
785
- success: false,
786
- error: error.message,
787
- requestId: message.requestId,
788
- timestamp: Date.now()
43
+ // Replay any auth providers that were registered before setup()
44
+ for (const provider of pendingAuthProviders) {
45
+ liveServer.useAuth(provider)
789
46
  }
790
-
791
- ws.send(JSON.stringify(errorResponse))
792
- }
793
- }
794
-
795
- async function handleComponentUnmount(ws: FluxStackWebSocket, message: LiveMessage) {
796
- const result = await componentRegistry.handleMessage(ws, message)
797
-
798
- if (result !== null) {
799
- const response = {
800
- type: 'COMPONENT_UNMOUNTED',
801
- componentId: message.componentId,
802
- success: result.success,
803
- requestId: message.requestId,
804
- timestamp: Date.now()
805
- }
806
- ws.send(JSON.stringify(response))
807
- }
808
- }
809
-
810
- async function handleActionCall(ws: FluxStackWebSocket, message: LiveMessage) {
811
- const result = await componentRegistry.handleMessage(ws, message)
812
-
813
- if (result !== null) {
814
- const response = {
815
- type: message.expectResponse ? 'ACTION_RESPONSE' : 'MESSAGE_RESPONSE',
816
- originalType: message.type,
817
- componentId: message.componentId,
818
- success: result.success,
819
- result: result.result,
820
- error: result.error,
821
- requestId: message.requestId,
822
- timestamp: Date.now()
823
- }
824
- ws.send(JSON.stringify(response))
825
- }
826
- }
827
-
828
- async function handlePropertyUpdate(ws: FluxStackWebSocket, message: LiveMessage) {
829
- const result = await componentRegistry.handleMessage(ws, message)
830
-
831
- if (result !== null) {
832
- const response = {
833
- type: 'PROPERTY_UPDATED',
834
- componentId: message.componentId,
835
- success: result.success,
836
- result: result.result,
837
- error: result.error,
838
- requestId: message.requestId,
839
- timestamp: Date.now()
840
- }
841
- ws.send(JSON.stringify(response))
842
- }
843
- }
844
-
845
- async function handleComponentPing(ws: FluxStackWebSocket, message: LiveMessage) {
846
- // Update component's last activity timestamp
847
- const updated = componentRegistry.updateComponentActivity(message.componentId)
848
-
849
- // Send pong response
850
- const response = {
851
- type: 'COMPONENT_PONG',
852
- componentId: message.componentId,
853
- success: updated,
854
- requestId: message.requestId,
855
- timestamp: Date.now()
856
- }
857
-
858
- ws.send(JSON.stringify(response))
859
- }
860
-
861
- // ===== Auth Handler =====
862
-
863
- async function handleAuth(ws: FluxStackWebSocket, message: LiveMessage) {
864
- liveLog('websocket', null, '🔒 Processing WebSocket authentication request')
865
-
866
- try {
867
- const credentials = message.payload || {}
868
- const providerName = credentials.provider as string | undefined
869
-
870
- if (!liveAuthManager.hasProviders()) {
871
- ws.send(JSON.stringify({
872
- type: 'AUTH_RESPONSE',
873
- success: false,
874
- error: 'No auth providers configured',
875
- requestId: message.requestId,
876
- timestamp: Date.now()
877
- }))
878
- return
879
- }
880
-
881
- const authContext = await liveAuthManager.authenticate(credentials, providerName)
882
-
883
- // Store auth context on the WebSocket connection
884
- ws.data.authContext = authContext
885
-
886
- if (authContext.authenticated) {
887
- ws.data.userId = authContext.user?.id
888
- liveLog('websocket', null, `🔒 WebSocket authenticated: user=${authContext.user?.id}`)
889
- }
890
-
891
- ws.send(JSON.stringify({
892
- type: 'AUTH_RESPONSE',
893
- success: authContext.authenticated,
894
- authenticated: authContext.authenticated,
895
- userId: authContext.user?.id,
896
- roles: authContext.user?.roles,
897
- requestId: message.requestId,
898
- timestamp: Date.now()
899
- }))
900
- } catch (error: any) {
901
- console.error('🔒 WebSocket auth error:', error.message)
902
- ws.send(JSON.stringify({
903
- type: 'AUTH_RESPONSE',
904
- success: false,
905
- error: error.message,
906
- requestId: message.requestId,
907
- timestamp: Date.now()
908
- }))
909
- }
910
- }
911
-
912
- // File Upload Handler Functions
913
- async function handleFileUploadStart(ws: FluxStackWebSocket, message: FileUploadStartMessage) {
914
- liveLog('messages', message.componentId || null, '📤 Starting file upload:', message.uploadId)
47
+ pendingAuthProviders.length = 0
48
+ pendingRoomClasses.length = 0
915
49
 
916
- // 🔒 Pass userId for per-user upload quota enforcement
917
- const userId = ws.data?.userId || ws.data?.authContext?.user?.id
918
- const result = await fileUploadManager.startUpload(message, userId)
919
-
920
- const response = {
921
- type: 'FILE_UPLOAD_START_RESPONSE',
922
- componentId: message.componentId,
923
- uploadId: message.uploadId,
924
- success: result.success,
925
- error: result.error,
926
- requestId: message.requestId,
927
- timestamp: Date.now()
928
- }
929
-
930
- ws.send(JSON.stringify(response))
931
- }
932
-
933
- async function handleFileUploadChunk(ws: FluxStackWebSocket, message: FileUploadChunkMessage, binaryData: Buffer | null = null) {
934
- liveLog('messages', message.componentId || null, `📦 Receiving chunk ${message.chunkIndex + 1} for upload ${message.uploadId}${binaryData ? ' (binary)' : ' (base64)'}`)
935
-
936
- const progressResponse = await fileUploadManager.receiveChunk(message, ws, binaryData)
937
-
938
- if (progressResponse) {
939
- // Add requestId to response so client can correlate it
940
- const responseWithRequestId = {
941
- ...progressResponse,
942
- requestId: message.requestId,
943
- success: true
944
- }
945
- ws.send(JSON.stringify(responseWithRequestId))
946
- } else {
947
- // Send error response
948
- const errorResponse = {
949
- type: 'FILE_UPLOAD_ERROR',
950
- componentId: message.componentId,
951
- uploadId: message.uploadId,
952
- error: 'Failed to process chunk',
953
- requestId: message.requestId,
954
- success: false,
955
- timestamp: Date.now()
956
- }
957
- ws.send(JSON.stringify(errorResponse))
958
- }
959
- }
960
-
961
- async function handleFileUploadComplete(ws: FluxStackWebSocket, message: FileUploadCompleteMessage) {
962
- liveLog('messages', null, '✅ Completing file upload:', message.uploadId)
963
-
964
- const completeResponse = await fileUploadManager.completeUpload(message)
965
-
966
- // Add requestId to response so client can correlate it
967
- const responseWithRequestId = {
968
- ...completeResponse,
969
- requestId: message.requestId
970
- }
971
-
972
- ws.send(JSON.stringify(responseWithRequestId))
973
- }
974
-
975
- // ===== Room System Handlers =====
976
-
977
- async function handleRoomJoin(ws: FluxStackWebSocket, message: RoomMessage) {
978
- liveLog('rooms', message.componentId, `🚪 Component ${message.componentId} joining room ${message.roomId}`)
979
-
980
- try {
981
- // 🔒 Validate room name format (alphanumeric, hyphens, underscores, max 64 chars)
982
- if (!message.roomId || !/^[a-zA-Z0-9_:.-]{1,64}$/.test(message.roomId)) {
983
- throw new Error('Invalid room name. Must be 1-64 alphanumeric characters, hyphens, underscores, dots, or colons.')
984
- }
985
-
986
- // 🔒 Room authorization check
987
- const authContext = ws.data?.authContext || ANONYMOUS_CONTEXT
988
- const authResult = await liveAuthManager.authorizeRoom(authContext, message.roomId)
989
- if (!authResult.allowed) {
990
- throw new Error(`Room access denied: ${authResult.reason}`)
991
- }
992
-
993
- const result = liveRoomManager.joinRoom(
994
- message.componentId,
995
- message.roomId,
996
- ws,
997
- undefined // 🔒 Don't allow client to set initial room state - server controls this
998
- )
999
-
1000
- const response = {
1001
- type: 'ROOM_JOINED',
1002
- componentId: message.componentId,
1003
- roomId: message.roomId,
1004
- success: true,
1005
- state: result.state,
1006
- requestId: message.requestId,
1007
- timestamp: Date.now()
1008
- }
50
+ await liveServer.start()
51
+ context.logger.debug('Live Components started via @fluxstack/live')
52
+ },
1009
53
 
1010
- ws.send(JSON.stringify(response))
1011
- } catch (error: any) {
1012
- ws.send(JSON.stringify({
1013
- type: 'ERROR',
1014
- componentId: message.componentId,
1015
- roomId: message.roomId,
1016
- error: error.message,
1017
- requestId: message.requestId,
1018
- timestamp: Date.now()
1019
- }))
54
+ onServerStart: async (context: PluginContext) => {
55
+ context.logger.debug('Live Components WebSocket ready on /api/live/ws')
1020
56
  }
1021
57
  }
1022
58
 
1023
- async function handleRoomLeave(ws: FluxStackWebSocket, message: RoomMessage) {
1024
- liveLog('rooms', message.componentId, `🚶 Component ${message.componentId} leaving room ${message.roomId}`)
1025
-
1026
- try {
1027
- liveRoomManager.leaveRoom(message.componentId, message.roomId)
1028
-
1029
- const response = {
1030
- type: 'ROOM_LEFT',
1031
- componentId: message.componentId,
1032
- roomId: message.roomId,
1033
- success: true,
1034
- requestId: message.requestId,
1035
- timestamp: Date.now()
1036
- }
1037
-
1038
- ws.send(JSON.stringify(response))
1039
- } catch (error: any) {
1040
- ws.send(JSON.stringify({
1041
- type: 'ERROR',
1042
- componentId: message.componentId,
1043
- roomId: message.roomId,
1044
- error: error.message,
1045
- requestId: message.requestId,
1046
- timestamp: Date.now()
1047
- }))
1048
- }
1049
- }
59
+ /**
60
+ * Auto-discover LiveRoom classes from a directory.
61
+ * Scans all .ts files, imports them, and checks for LiveRoom subclasses.
62
+ */
63
+ async function discoverRoomClasses(dir: string): Promise<LiveRoomClass[]> {
64
+ if (!existsSync(dir)) return []
1050
65
 
1051
- async function handleRoomEmit(ws: FluxStackWebSocket, message: RoomMessage) {
1052
- liveLog('rooms', message.componentId, `📡 Component ${message.componentId} emitting '${message.event}' to room ${message.roomId}`)
66
+ const rooms: LiveRoomClass[] = []
67
+ const files = readdirSync(dir).filter(f => f.endsWith('.ts') && !f.endsWith('.d.ts'))
1053
68
 
1054
- try {
1055
- // 🔒 Validate room name
1056
- if (!message.roomId || !/^[a-zA-Z0-9_:.-]{1,64}$/.test(message.roomId)) {
1057
- throw new Error('Invalid room name')
69
+ for (const file of files) {
70
+ try {
71
+ const mod = await import(path.join(dir, file))
72
+ for (const exported of Object.values(mod)) {
73
+ if (RoomRegistry.isLiveRoomClass(exported)) {
74
+ rooms.push(exported as LiveRoomClass)
75
+ }
76
+ }
77
+ } catch {
78
+ // Skip files that fail to import
1058
79
  }
1059
-
1060
- const count = liveRoomManager.emitToRoom(
1061
- message.roomId,
1062
- message.event!,
1063
- message.data,
1064
- message.componentId // Excluir quem enviou
1065
- )
1066
-
1067
- liveLog('rooms', message.componentId, ` → Notified ${count} components`)
1068
- } catch (error: any) {
1069
- ws.send(JSON.stringify({
1070
- type: 'ERROR',
1071
- componentId: message.componentId,
1072
- roomId: message.roomId,
1073
- error: error.message,
1074
- timestamp: Date.now()
1075
- }))
1076
80
  }
1077
- }
1078
-
1079
- async function handleRoomStateSet(ws: FluxStackWebSocket, message: RoomMessage) {
1080
- liveLog('rooms', message.componentId, `📝 Component ${message.componentId} updating state in room ${message.roomId}`)
1081
81
 
1082
- try {
1083
- // 🔒 Validate room name
1084
- if (!message.roomId || !/^[a-zA-Z0-9_:.-]{1,64}$/.test(message.roomId)) {
1085
- throw new Error('Invalid room name')
1086
- }
1087
-
1088
- // 🔒 Validate state size (max 1MB per update to prevent memory attacks)
1089
- const stateStr = JSON.stringify(message.data ?? {})
1090
- if (stateStr.length > 1024 * 1024) {
1091
- throw new Error('Room state update too large (max 1MB)')
1092
- }
1093
-
1094
- liveRoomManager.setRoomState(
1095
- message.roomId,
1096
- message.data ?? {},
1097
- message.componentId // Excluir quem enviou
1098
- )
1099
- } catch (error: any) {
1100
- ws.send(JSON.stringify({
1101
- type: 'ERROR',
1102
- componentId: message.componentId,
1103
- roomId: message.roomId,
1104
- error: error.message,
1105
- timestamp: Date.now()
1106
- }))
1107
- }
82
+ return rooms
1108
83
  }