create-fluxstack 1.13.0 → 1.15.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 (96) hide show
  1. package/LLMD/patterns/anti-patterns.md +100 -0
  2. package/LLMD/reference/routing.md +39 -39
  3. package/LLMD/resources/live-auth.md +20 -2
  4. package/LLMD/resources/live-components.md +300 -21
  5. package/LLMD/resources/live-logging.md +95 -33
  6. package/LLMD/resources/live-upload.md +59 -8
  7. package/app/client/.live-stubs/LiveAdminPanel.js +5 -0
  8. package/app/client/.live-stubs/LiveChat.js +7 -0
  9. package/app/client/.live-stubs/LiveCounter.js +9 -0
  10. package/app/client/.live-stubs/LiveForm.js +11 -0
  11. package/app/client/.live-stubs/LiveLocalCounter.js +8 -0
  12. package/app/client/.live-stubs/LiveRoomChat.js +10 -0
  13. package/app/client/.live-stubs/LiveTodoList.js +9 -0
  14. package/app/client/.live-stubs/LiveUpload.js +15 -0
  15. package/app/client/index.html +2 -2
  16. package/app/client/public/favicon.svg +46 -0
  17. package/app/client/src/App.tsx +13 -1
  18. package/app/client/src/assets/fluxstack-static.svg +46 -0
  19. package/app/client/src/assets/fluxstack.svg +183 -0
  20. package/app/client/src/components/AppLayout.tsx +146 -9
  21. package/app/client/src/components/BackButton.tsx +13 -13
  22. package/app/client/src/components/DemoPage.tsx +4 -4
  23. package/app/client/src/live/AuthDemo.tsx +23 -21
  24. package/app/client/src/live/ChatDemo.tsx +2 -2
  25. package/app/client/src/live/CounterDemo.tsx +12 -12
  26. package/app/client/src/live/FormDemo.tsx +2 -2
  27. package/app/client/src/live/LiveDebuggerPanel.tsx +779 -0
  28. package/app/client/src/live/RoomChatDemo.tsx +24 -16
  29. package/app/client/src/live/TodoListDemo.tsx +158 -0
  30. package/app/client/src/main.tsx +13 -13
  31. package/app/client/src/pages/ApiTestPage.tsx +6 -6
  32. package/app/client/src/pages/HomePage.tsx +80 -52
  33. package/app/server/auth/DevAuthProvider.ts +2 -2
  34. package/app/server/auth/JWTAuthProvider.example.ts +2 -2
  35. package/app/server/index.ts +2 -2
  36. package/app/server/live/LiveAdminPanel.ts +2 -1
  37. package/app/server/live/LiveChat.ts +78 -77
  38. package/app/server/live/LiveCounter.ts +1 -1
  39. package/app/server/live/LiveForm.ts +1 -0
  40. package/app/server/live/LiveLocalCounter.ts +38 -37
  41. package/app/server/live/LiveProtectedChat.ts +2 -1
  42. package/app/server/live/LiveRoomChat.ts +1 -0
  43. package/app/server/live/LiveTodoList.ts +110 -0
  44. package/app/server/live/LiveUpload.ts +1 -0
  45. package/app/server/live/register-components.ts +19 -19
  46. package/app/server/routes/room.routes.ts +1 -2
  47. package/config/system/runtime.config.ts +4 -0
  48. package/core/build/live-components-generator.ts +1 -1
  49. package/core/build/optimizer.ts +235 -235
  50. package/core/build/vite-plugins.ts +28 -0
  51. package/core/client/components/LiveDebugger.tsx +1324 -0
  52. package/core/client/hooks/useLiveUpload.ts +3 -4
  53. package/core/client/index.ts +41 -21
  54. package/core/framework/server.ts +1 -1
  55. package/core/plugins/built-in/index.ts +134 -134
  56. package/core/plugins/built-in/live-components/commands/create-live-component.ts +4 -0
  57. package/core/plugins/built-in/vite/index.ts +75 -21
  58. package/core/server/index.ts +14 -15
  59. package/core/server/live/auto-generated-components.ts +6 -3
  60. package/core/server/live/index.ts +95 -21
  61. package/core/server/live/websocket-plugin.ts +27 -862
  62. package/core/server/plugins/static-files-plugin.ts +179 -69
  63. package/core/types/build.ts +219 -219
  64. package/core/types/plugin.ts +107 -107
  65. package/core/types/types.ts +77 -890
  66. package/core/utils/logger/startup-banner.ts +82 -82
  67. package/core/utils/version.ts +6 -6
  68. package/create-fluxstack.ts +1 -1
  69. package/package.json +5 -1
  70. package/plugins/crypto-auth/index.ts +1 -1
  71. package/plugins/crypto-auth/server/CryptoAuthLiveProvider.ts +2 -2
  72. package/vite.config.ts +40 -12
  73. package/app/client/src/assets/react.svg +0 -1
  74. package/core/client/LiveComponentsProvider.tsx +0 -531
  75. package/core/client/components/Live.tsx +0 -105
  76. package/core/client/hooks/AdaptiveChunkSizer.ts +0 -215
  77. package/core/client/hooks/state-validator.ts +0 -130
  78. package/core/client/hooks/useChunkedUpload.ts +0 -359
  79. package/core/client/hooks/useLiveChunkedUpload.ts +0 -87
  80. package/core/client/hooks/useLiveComponent.ts +0 -843
  81. package/core/client/hooks/useRoom.ts +0 -409
  82. package/core/client/hooks/useRoomProxy.ts +0 -382
  83. package/core/server/live/ComponentRegistry.ts +0 -1099
  84. package/core/server/live/FileUploadManager.ts +0 -282
  85. package/core/server/live/LiveComponentPerformanceMonitor.ts +0 -931
  86. package/core/server/live/LiveLogger.ts +0 -111
  87. package/core/server/live/LiveRoomManager.ts +0 -262
  88. package/core/server/live/RoomEventBus.ts +0 -234
  89. package/core/server/live/RoomStateManager.ts +0 -172
  90. package/core/server/live/SingleConnectionManager.ts +0 -0
  91. package/core/server/live/StateSignature.ts +0 -645
  92. package/core/server/live/WebSocketConnectionManager.ts +0 -709
  93. package/core/server/live/auth/LiveAuthContext.ts +0 -71
  94. package/core/server/live/auth/LiveAuthManager.ts +0 -304
  95. package/core/server/live/auth/index.ts +0 -19
  96. package/core/server/live/auth/types.ts +0 -179
@@ -1,883 +1,48 @@
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 } from '@fluxstack/live'
4
+ import type { LiveAuthProvider } 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
8
 
16
- // ===== Response Schemas for Live Components Routes =====
9
+ // Expose the LiveServer instance so other parts of FluxStack can access it
10
+ export let liveServer: LiveServer | null = null
17
11
 
18
- const LiveWebSocketInfoSchema = t.Object({
19
- success: t.Boolean(),
20
- message: t.String(),
21
- endpoint: t.String(),
22
- status: t.String(),
23
- connectionManager: t.Any()
24
- }, {
25
- description: 'WebSocket connection information and system statistics'
26
- })
27
-
28
- const LiveStatsSchema = t.Object({
29
- success: t.Boolean(),
30
- stats: t.Any(),
31
- timestamp: t.String()
32
- }, {
33
- description: 'Live Components statistics including registered components and instances'
34
- })
35
-
36
- const LiveHealthSchema = t.Object({
37
- success: t.Boolean(),
38
- service: t.String(),
39
- status: t.String(),
40
- components: t.Number(),
41
- connections: t.Any(),
42
- uptime: t.Number(),
43
- timestamp: t.String()
44
- }, {
45
- description: 'Health status of Live Components service'
46
- })
47
-
48
- const LiveConnectionsSchema = t.Object({
49
- success: t.Boolean(),
50
- connections: t.Array(t.Any()),
51
- systemStats: t.Any(),
52
- timestamp: t.String()
53
- }, {
54
- description: 'List of all active WebSocket connections with metrics'
55
- })
56
-
57
- const LiveConnectionDetailsSchema = t.Union([
58
- t.Object({
59
- success: t.Literal(true),
60
- connection: t.Any(),
61
- timestamp: t.String()
62
- }),
63
- t.Object({
64
- success: t.Literal(false),
65
- error: t.String()
66
- })
67
- ], {
68
- description: 'Detailed metrics for a specific connection'
69
- })
70
-
71
- const LivePoolStatsSchema = t.Union([
72
- t.Object({
73
- success: t.Literal(true),
74
- pool: t.String(),
75
- stats: t.Any(),
76
- timestamp: t.String()
77
- }),
78
- t.Object({
79
- success: t.Literal(false),
80
- error: t.String()
81
- })
82
- ], {
83
- description: 'Statistics for a specific connection pool'
84
- })
85
-
86
- const LivePerformanceDashboardSchema = t.Object({
87
- success: t.Boolean(),
88
- dashboard: t.Any(),
89
- timestamp: t.String()
90
- }, {
91
- description: 'Performance monitoring dashboard data'
92
- })
93
-
94
- const LiveComponentMetricsSchema = t.Union([
95
- t.Object({
96
- success: t.Literal(true),
97
- component: t.String(),
98
- metrics: t.Any(),
99
- alerts: t.Array(t.Any()),
100
- suggestions: t.Array(t.Any()),
101
- timestamp: t.String()
102
- }),
103
- t.Object({
104
- success: t.Literal(false),
105
- error: t.String()
106
- })
107
- ], {
108
- description: 'Performance metrics, alerts and suggestions for a specific component'
109
- })
110
-
111
- const LiveAlertResolveSchema = t.Object({
112
- success: t.Boolean(),
113
- message: t.String(),
114
- timestamp: t.String()
115
- }, {
116
- description: 'Result of alert resolution operation'
117
- })
12
+ // Queue for auth providers registered before LiveServer is created
13
+ export const pendingAuthProviders: LiveAuthProvider[] = []
118
14
 
119
15
  export const liveComponentsPlugin: Plugin = {
120
16
  name: 'live-components',
121
- version: '1.0.0',
122
- description: 'Real-time Live Components with Elysia native WebSocket support',
17
+ version: '2.0.0',
18
+ description: 'Real-time Live Components powered by @fluxstack/live',
123
19
  author: 'FluxStack Team',
124
20
  priority: 'normal',
125
21
  category: 'core',
126
22
  tags: ['websocket', 'real-time', 'live-components'],
127
-
23
+
128
24
  setup: async (context: PluginContext) => {
129
- context.logger.debug('🔌 Setting up Live Components plugin with Elysia WebSocket...')
130
-
131
- // Auto-discover components from app/server/live directory
25
+ const transport = new ElysiaTransport(context.app)
132
26
  const componentsPath = path.join(process.cwd(), 'app', 'server', 'live')
133
- await componentRegistry.autoDiscoverComponents(componentsPath)
134
- context.logger.debug('🔍 Component auto-discovery completed')
135
-
136
- // Create grouped routes for Live Components with documentation
137
- const liveRoutes = new Elysia({ prefix: '/api/live', tags: ['Live Components'] })
138
- // WebSocket route - supports both JSON and binary messages
139
- .ws('/ws', {
140
- // Use t.Any() to allow both JSON objects and binary data
141
- // Binary messages will be ArrayBuffer/Uint8Array, JSON will be parsed objects
142
- body: t.Any(),
143
-
144
- async open(ws) {
145
- const socket = ws as unknown as FluxStackWebSocket
146
- const connectionId = `ws-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
147
- liveLog('websocket', null, `🔌 Live Components WebSocket connected: ${connectionId}`)
148
-
149
- // Register connection with enhanced connection manager
150
- connectionManager.registerConnection(ws as unknown as FluxStackWebSocket, connectionId, 'live-components')
151
-
152
- // Initialize and store connection data in ws.data
153
- const wsData: FluxStackWSData = {
154
- connectionId,
155
- components: new Map(),
156
- subscriptions: new Set(),
157
- connectedAt: new Date()
158
- }
159
-
160
- // Assign data to websocket (Elysia creates ws.data from context)
161
- if (!socket.data) {
162
- (socket as { data: FluxStackWSData }).data = wsData
163
- } else {
164
- socket.data.connectionId = connectionId
165
- socket.data.components = new Map()
166
- socket.data.subscriptions = new Set()
167
- socket.data.connectedAt = new Date()
168
- }
169
-
170
- // 🔒 Try to authenticate from query params (token=xxx)
171
- try {
172
- const query = (ws as any).data?.query as Record<string, string> | undefined
173
- const token = query?.token
174
- if (token && liveAuthManager.hasProviders()) {
175
- const authContext = await liveAuthManager.authenticate({ token })
176
- socket.data.authContext = authContext
177
- if (authContext.authenticated) {
178
- socket.data.userId = authContext.user?.id
179
- liveLog('websocket', null, `🔒 WebSocket authenticated via query: user=${authContext.user?.id}`)
180
- }
181
- }
182
- } catch {
183
- // Query param auth is optional - continue without auth
184
- }
185
-
186
- // Send connection confirmation
187
- ws.send(JSON.stringify({
188
- type: 'CONNECTION_ESTABLISHED',
189
- connectionId,
190
- timestamp: Date.now(),
191
- authenticated: socket.data.authContext?.authenticated ?? false,
192
- userId: socket.data.authContext?.user?.id,
193
- features: {
194
- compression: true,
195
- encryption: true,
196
- offlineQueue: true,
197
- loadBalancing: true,
198
- auth: liveAuthManager.hasProviders()
199
- }
200
- }))
201
- },
202
-
203
- async message(ws: unknown, rawMessage: LiveMessage | ArrayBuffer | Uint8Array) {
204
- const socket = ws as FluxStackWebSocket
205
- try {
206
- let message: LiveMessage
207
- let binaryChunkData: Buffer | null = null
208
-
209
- // Check if this is a binary message (file upload chunk)
210
- if (rawMessage instanceof ArrayBuffer || rawMessage instanceof Uint8Array) {
211
- // Binary protocol: [4 bytes header length][JSON header][binary data]
212
- const buffer = rawMessage instanceof ArrayBuffer
213
- ? Buffer.from(rawMessage)
214
- : Buffer.from(rawMessage.buffer, rawMessage.byteOffset, rawMessage.byteLength)
215
-
216
- // Read header length (first 4 bytes, little-endian)
217
- const headerLength = buffer.readUInt32LE(0)
218
-
219
- // Extract and parse JSON header
220
- const headerJson = buffer.slice(4, 4 + headerLength).toString('utf-8')
221
- const header = JSON.parse(headerJson) as BinaryChunkHeader
222
-
223
- // Extract binary chunk data
224
- binaryChunkData = buffer.slice(4 + headerLength)
225
-
226
- liveLog('messages', null, `📦 Binary chunk received: ${binaryChunkData.length} bytes for upload ${header.uploadId}`)
227
-
228
- // Create message with binary data attached
229
- message = {
230
- ...header,
231
- data: binaryChunkData, // Buffer instead of base64 string
232
- timestamp: Date.now()
233
- } as unknown as LiveMessage
234
- } else {
235
- // Regular JSON message
236
- message = rawMessage as LiveMessage
237
- message.timestamp = Date.now()
238
- }
239
-
240
- liveLog('messages', message.componentId || null, `📨 Received message:`, {
241
- type: message.type,
242
- componentId: message.componentId,
243
- action: message.action,
244
- requestId: message.requestId,
245
- isBinary: binaryChunkData !== null
246
- })
247
-
248
- // Handle different message types
249
- switch (message.type) {
250
- case 'COMPONENT_MOUNT':
251
- await handleComponentMount(socket, message)
252
- break
253
- case 'COMPONENT_REHYDRATE':
254
- await handleComponentRehydrate(socket, message)
255
- break
256
- case 'COMPONENT_UNMOUNT':
257
- await handleComponentUnmount(socket, message)
258
- break
259
- case 'CALL_ACTION':
260
- await handleActionCall(socket, message)
261
- break
262
- case 'PROPERTY_UPDATE':
263
- await handlePropertyUpdate(socket, message)
264
- break
265
- case 'COMPONENT_PING':
266
- await handleComponentPing(socket, message)
267
- break
268
- case 'AUTH':
269
- await handleAuth(socket, message)
270
- break
271
- case 'FILE_UPLOAD_START':
272
- await handleFileUploadStart(socket, message as FileUploadStartMessage)
273
- break
274
- case 'FILE_UPLOAD_CHUNK':
275
- await handleFileUploadChunk(socket, message as FileUploadChunkMessage, binaryChunkData)
276
- break
277
- case 'FILE_UPLOAD_COMPLETE':
278
- await handleFileUploadComplete(socket, message as unknown as FileUploadCompleteMessage)
279
- break
280
- // Room system messages
281
- case 'ROOM_JOIN':
282
- await handleRoomJoin(socket, message as unknown as RoomMessage)
283
- break
284
- case 'ROOM_LEAVE':
285
- await handleRoomLeave(socket, message as unknown as RoomMessage)
286
- break
287
- case 'ROOM_EMIT':
288
- await handleRoomEmit(socket, message as unknown as RoomMessage)
289
- break
290
- case 'ROOM_STATE_SET':
291
- await handleRoomStateSet(socket, message as unknown as RoomMessage)
292
- break
293
- default:
294
- console.warn(`❌ Unknown message type: ${message.type}`)
295
- socket.send(JSON.stringify({
296
- type: 'ERROR',
297
- error: `Unknown message type: ${message.type}`,
298
- timestamp: Date.now()
299
- }))
300
- }
301
- } catch (error) {
302
- console.error('❌ WebSocket message error:', error)
303
- socket.send(JSON.stringify({
304
- type: 'ERROR',
305
- error: error instanceof Error ? error.message : 'Unknown error',
306
- timestamp: Date.now()
307
- }))
308
- }
309
- },
310
-
311
- close(ws) {
312
- const socket = ws as unknown as FluxStackWebSocket
313
- const connectionId = socket.data?.connectionId
314
- liveLog('websocket', null, `🔌 Live Components WebSocket disconnected: ${connectionId}`)
315
-
316
- // Cleanup connection in connection manager
317
- if (connectionId) {
318
- connectionManager.cleanupConnection(connectionId)
319
- }
320
-
321
- // Cleanup components for this connection
322
- componentRegistry.cleanupConnection(socket)
323
- }
324
- })
325
-
326
- // ===== Live Components Information Routes =====
327
- .get('/websocket-info', () => {
328
- return {
329
- success: true,
330
- message: 'Live Components WebSocket available via Elysia',
331
- endpoint: 'ws://localhost:3000/api/live/ws',
332
- status: 'running',
333
- connectionManager: connectionManager.getSystemStats()
334
- }
335
- }, {
336
- detail: {
337
- summary: 'Get WebSocket Information',
338
- description: 'Returns WebSocket endpoint information and connection manager statistics',
339
- tags: ['Live Components', 'WebSocket']
340
- },
341
- response: LiveWebSocketInfoSchema
342
- })
343
-
344
- .get('/stats', () => {
345
- const stats = componentRegistry.getStats()
346
- return {
347
- success: true,
348
- stats,
349
- timestamp: new Date().toISOString()
350
- }
351
- }, {
352
- detail: {
353
- summary: 'Get Live Components Statistics',
354
- description: 'Returns statistics about registered components and active instances',
355
- tags: ['Live Components', 'Monitoring']
356
- },
357
- response: LiveStatsSchema
358
- })
359
-
360
- .get('/health', () => {
361
- return {
362
- success: true,
363
- service: 'FluxStack Live Components',
364
- status: 'operational',
365
- components: componentRegistry.getStats().components,
366
- connections: connectionManager.getSystemStats(),
367
- uptime: process.uptime(),
368
- timestamp: new Date().toISOString()
369
- }
370
- }, {
371
- detail: {
372
- summary: 'Health Check',
373
- description: 'Returns the health status of the Live Components service',
374
- tags: ['Live Components', 'Health']
375
- },
376
- response: LiveHealthSchema
377
- })
378
-
379
- // ===== Connection Management Routes =====
380
- .get('/connections', () => {
381
- return {
382
- success: true,
383
- connections: connectionManager.getAllConnectionMetrics(),
384
- systemStats: connectionManager.getSystemStats(),
385
- timestamp: new Date().toISOString()
386
- }
387
- }, {
388
- detail: {
389
- summary: 'List All Connections',
390
- description: 'Returns all active WebSocket connections with their metrics',
391
- tags: ['Live Components', 'Connections']
392
- },
393
- response: LiveConnectionsSchema
394
- })
395
-
396
- .get('/connections/:connectionId', ({ params }) => {
397
- const metrics = connectionManager.getConnectionMetrics(params.connectionId)
398
- if (!metrics) {
399
- return {
400
- success: false,
401
- error: 'Connection not found'
402
- }
403
- }
404
- return {
405
- success: true,
406
- connection: metrics,
407
- timestamp: new Date().toISOString()
408
- }
409
- }, {
410
- detail: {
411
- summary: 'Get Connection Details',
412
- description: 'Returns detailed metrics for a specific WebSocket connection',
413
- tags: ['Live Components', 'Connections']
414
- },
415
- params: t.Object({
416
- connectionId: t.String({ description: 'The unique connection identifier' })
417
- }),
418
- response: LiveConnectionDetailsSchema
419
- })
420
-
421
- .get('/pools/:poolId/stats', ({ params }) => {
422
- const stats = connectionManager.getPoolStats(params.poolId)
423
- if (!stats) {
424
- return {
425
- success: false,
426
- error: 'Pool not found'
427
- }
428
- }
429
- return {
430
- success: true,
431
- pool: params.poolId,
432
- stats,
433
- timestamp: new Date().toISOString()
434
- }
435
- }, {
436
- detail: {
437
- summary: 'Get Pool Statistics',
438
- description: 'Returns statistics for a specific connection pool',
439
- tags: ['Live Components', 'Connections', 'Pools']
440
- },
441
- params: t.Object({
442
- poolId: t.String({ description: 'The unique pool identifier' })
443
- }),
444
- response: LivePoolStatsSchema
445
- })
446
-
447
- // ===== Performance Monitoring Routes =====
448
- .get('/performance/dashboard', () => {
449
- return {
450
- success: true,
451
- dashboard: performanceMonitor.generateDashboard(),
452
- timestamp: new Date().toISOString()
453
- }
454
- }, {
455
- detail: {
456
- summary: 'Performance Dashboard',
457
- description: 'Returns comprehensive performance monitoring dashboard data',
458
- tags: ['Live Components', 'Performance']
459
- },
460
- response: LivePerformanceDashboardSchema
461
- })
462
27
 
463
- .get('/performance/components/:componentId', ({ params }) => {
464
- const metrics = performanceMonitor.getComponentMetrics(params.componentId)
465
- if (!metrics) {
466
- return {
467
- success: false,
468
- error: 'Component metrics not found'
469
- }
470
- }
471
-
472
- const alerts = performanceMonitor.getComponentAlerts(params.componentId)
473
- const suggestions = performanceMonitor.getComponentSuggestions(params.componentId)
474
-
475
- return {
476
- success: true,
477
- component: params.componentId,
478
- metrics,
479
- alerts,
480
- suggestions,
481
- timestamp: new Date().toISOString()
482
- }
483
- }, {
484
- detail: {
485
- summary: 'Get Component Performance Metrics',
486
- description: 'Returns performance metrics, alerts, and optimization suggestions for a specific component',
487
- tags: ['Live Components', 'Performance']
488
- },
489
- params: t.Object({
490
- componentId: t.String({ description: 'The unique component identifier' })
491
- }),
492
- response: LiveComponentMetricsSchema
493
- })
494
-
495
- .post('/performance/alerts/:alertId/resolve', ({ params }) => {
496
- const resolved = performanceMonitor.resolveAlert(params.alertId)
497
- return {
498
- success: resolved,
499
- message: resolved ? 'Alert resolved' : 'Alert not found',
500
- timestamp: new Date().toISOString()
501
- }
502
- }, {
503
- detail: {
504
- summary: 'Resolve Performance Alert',
505
- description: 'Marks a performance alert as resolved',
506
- tags: ['Live Components', 'Performance', 'Alerts']
507
- },
508
- params: t.Object({
509
- alertId: t.String({ description: 'The unique alert identifier' })
510
- }),
511
- response: LiveAlertResolveSchema
512
- })
513
-
514
- // Register the grouped routes with the main app
515
- context.app.use(liveRoutes)
516
- },
517
-
518
- onServerStart: async (context: PluginContext) => {
519
- context.logger.debug('🔌 Live Components WebSocket ready on /api/live/ws')
520
- }
521
- }
522
-
523
- // Handler functions for WebSocket messages
524
- async function handleComponentMount(ws: FluxStackWebSocket, message: LiveMessage) {
525
- const result = await componentRegistry.handleMessage(ws, message)
526
-
527
- if (result !== null) {
528
- const response = {
529
- type: 'COMPONENT_MOUNTED',
530
- componentId: message.componentId,
531
- success: result.success,
532
- result: result.result,
533
- error: result.error,
534
- requestId: message.requestId,
535
- timestamp: Date.now()
536
- }
537
- ws.send(JSON.stringify(response))
538
- }
539
- }
540
-
541
- async function handleComponentRehydrate(ws: FluxStackWebSocket, message: LiveMessage) {
542
- liveLog('lifecycle', message.componentId, '🔄 Processing component re-hydration request:', {
543
- componentId: message.componentId,
544
- payload: message.payload
545
- })
546
-
547
- try {
548
- const { componentName, signedState, room, userId } = message.payload || {}
549
-
550
- if (!componentName || !signedState) {
551
- throw new Error('Missing componentName or signedState in rehydration payload')
552
- }
553
-
554
- const result = await componentRegistry.rehydrateComponent(
555
- message.componentId,
556
- componentName,
557
- signedState,
558
- ws,
559
- { room, userId }
560
- )
561
-
562
- const response = {
563
- type: 'COMPONENT_REHYDRATED',
564
- componentId: message.componentId,
565
- success: result.success,
566
- result: {
567
- newComponentId: result.newComponentId,
568
- ...result
569
- },
570
- error: result.error,
571
- requestId: message.requestId,
572
- timestamp: Date.now()
573
- }
574
-
575
- liveLog('lifecycle', message.componentId, '📤 Sending COMPONENT_REHYDRATED response:', {
576
- type: response.type,
577
- success: response.success,
578
- newComponentId: response.result?.newComponentId,
579
- requestId: response.requestId
28
+ liveServer = new LiveServer({
29
+ transport,
30
+ componentsPath,
31
+ wsPath: '/api/live/ws',
32
+ httpPrefix: '/api/live',
580
33
  })
581
-
582
- ws.send(JSON.stringify(response))
583
-
584
- } catch (error: any) {
585
- console.error('❌ Re-hydration handler error:', error.message)
586
-
587
- const errorResponse = {
588
- type: 'COMPONENT_REHYDRATED',
589
- componentId: message.componentId,
590
- success: false,
591
- error: error.message,
592
- requestId: message.requestId,
593
- timestamp: Date.now()
594
- }
595
-
596
- ws.send(JSON.stringify(errorResponse))
597
- }
598
- }
599
-
600
- async function handleComponentUnmount(ws: FluxStackWebSocket, message: LiveMessage) {
601
- const result = await componentRegistry.handleMessage(ws, message)
602
-
603
- if (result !== null) {
604
- const response = {
605
- type: 'COMPONENT_UNMOUNTED',
606
- componentId: message.componentId,
607
- success: result.success,
608
- requestId: message.requestId,
609
- timestamp: Date.now()
610
- }
611
- ws.send(JSON.stringify(response))
612
- }
613
- }
614
-
615
- async function handleActionCall(ws: FluxStackWebSocket, message: LiveMessage) {
616
- const result = await componentRegistry.handleMessage(ws, message)
617
-
618
- if (result !== null) {
619
- const response = {
620
- type: message.expectResponse ? 'ACTION_RESPONSE' : 'MESSAGE_RESPONSE',
621
- originalType: message.type,
622
- componentId: message.componentId,
623
- success: result.success,
624
- result: result.result,
625
- error: result.error,
626
- requestId: message.requestId,
627
- timestamp: Date.now()
628
- }
629
- ws.send(JSON.stringify(response))
630
- }
631
- }
632
-
633
- async function handlePropertyUpdate(ws: FluxStackWebSocket, message: LiveMessage) {
634
- const result = await componentRegistry.handleMessage(ws, message)
635
-
636
- if (result !== null) {
637
- const response = {
638
- type: 'PROPERTY_UPDATED',
639
- componentId: message.componentId,
640
- success: result.success,
641
- result: result.result,
642
- error: result.error,
643
- requestId: message.requestId,
644
- timestamp: Date.now()
645
- }
646
- ws.send(JSON.stringify(response))
647
- }
648
- }
649
-
650
- async function handleComponentPing(ws: FluxStackWebSocket, message: LiveMessage) {
651
- // Update component's last activity timestamp
652
- const updated = componentRegistry.updateComponentActivity(message.componentId)
653
-
654
- // Send pong response
655
- const response = {
656
- type: 'COMPONENT_PONG',
657
- componentId: message.componentId,
658
- success: updated,
659
- requestId: message.requestId,
660
- timestamp: Date.now()
661
- }
662
-
663
- ws.send(JSON.stringify(response))
664
- }
665
-
666
- // ===== Auth Handler =====
667
-
668
- async function handleAuth(ws: FluxStackWebSocket, message: LiveMessage) {
669
- liveLog('websocket', null, '🔒 Processing WebSocket authentication request')
670
-
671
- try {
672
- const credentials = message.payload || {}
673
- const providerName = credentials.provider as string | undefined
674
34
 
675
- if (!liveAuthManager.hasProviders()) {
676
- ws.send(JSON.stringify({
677
- type: 'AUTH_RESPONSE',
678
- success: false,
679
- error: 'No auth providers configured',
680
- requestId: message.requestId,
681
- timestamp: Date.now()
682
- }))
683
- return
35
+ // Replay any auth providers that were registered before setup()
36
+ for (const provider of pendingAuthProviders) {
37
+ liveServer.useAuth(provider)
684
38
  }
39
+ pendingAuthProviders.length = 0
685
40
 
686
- const authContext = await liveAuthManager.authenticate(credentials, providerName)
687
-
688
- // Store auth context on the WebSocket connection
689
- ws.data.authContext = authContext
690
-
691
- if (authContext.authenticated) {
692
- ws.data.userId = authContext.user?.id
693
- liveLog('websocket', null, `🔒 WebSocket authenticated: user=${authContext.user?.id}`)
694
- }
695
-
696
- ws.send(JSON.stringify({
697
- type: 'AUTH_RESPONSE',
698
- success: authContext.authenticated,
699
- authenticated: authContext.authenticated,
700
- userId: authContext.user?.id,
701
- roles: authContext.user?.roles,
702
- requestId: message.requestId,
703
- timestamp: Date.now()
704
- }))
705
- } catch (error: any) {
706
- console.error('🔒 WebSocket auth error:', error.message)
707
- ws.send(JSON.stringify({
708
- type: 'AUTH_RESPONSE',
709
- success: false,
710
- error: error.message,
711
- requestId: message.requestId,
712
- timestamp: Date.now()
713
- }))
714
- }
715
- }
716
-
717
- // File Upload Handler Functions
718
- async function handleFileUploadStart(ws: FluxStackWebSocket, message: FileUploadStartMessage) {
719
- liveLog('messages', message.componentId || null, '📤 Starting file upload:', message.uploadId)
720
-
721
- const result = await fileUploadManager.startUpload(message)
722
-
723
- const response = {
724
- type: 'FILE_UPLOAD_START_RESPONSE',
725
- componentId: message.componentId,
726
- uploadId: message.uploadId,
727
- success: result.success,
728
- error: result.error,
729
- requestId: message.requestId,
730
- timestamp: Date.now()
731
- }
732
-
733
- ws.send(JSON.stringify(response))
734
- }
735
-
736
- async function handleFileUploadChunk(ws: FluxStackWebSocket, message: FileUploadChunkMessage, binaryData: Buffer | null = null) {
737
- liveLog('messages', message.componentId || null, `📦 Receiving chunk ${message.chunkIndex + 1} for upload ${message.uploadId}${binaryData ? ' (binary)' : ' (base64)'}`)
738
-
739
- const progressResponse = await fileUploadManager.receiveChunk(message, ws, binaryData)
740
-
741
- if (progressResponse) {
742
- // Add requestId to response so client can correlate it
743
- const responseWithRequestId = {
744
- ...progressResponse,
745
- requestId: message.requestId,
746
- success: true
747
- }
748
- ws.send(JSON.stringify(responseWithRequestId))
749
- } else {
750
- // Send error response
751
- const errorResponse = {
752
- type: 'FILE_UPLOAD_ERROR',
753
- componentId: message.componentId,
754
- uploadId: message.uploadId,
755
- error: 'Failed to process chunk',
756
- requestId: message.requestId,
757
- success: false,
758
- timestamp: Date.now()
759
- }
760
- ws.send(JSON.stringify(errorResponse))
761
- }
762
- }
763
-
764
- async function handleFileUploadComplete(ws: FluxStackWebSocket, message: FileUploadCompleteMessage) {
765
- liveLog('messages', null, '✅ Completing file upload:', message.uploadId)
766
-
767
- const completeResponse = await fileUploadManager.completeUpload(message)
768
-
769
- // Add requestId to response so client can correlate it
770
- const responseWithRequestId = {
771
- ...completeResponse,
772
- requestId: message.requestId
773
- }
774
-
775
- ws.send(JSON.stringify(responseWithRequestId))
776
- }
777
-
778
- // ===== Room System Handlers =====
779
-
780
- async function handleRoomJoin(ws: FluxStackWebSocket, message: RoomMessage) {
781
- liveLog('rooms', message.componentId, `🚪 Component ${message.componentId} joining room ${message.roomId}`)
782
-
783
- try {
784
- const result = liveRoomManager.joinRoom(
785
- message.componentId,
786
- message.roomId,
787
- ws,
788
- message.data?.initialState
789
- )
790
-
791
- const response = {
792
- type: 'ROOM_JOINED',
793
- componentId: message.componentId,
794
- roomId: message.roomId,
795
- success: true,
796
- state: result.state,
797
- requestId: message.requestId,
798
- timestamp: Date.now()
799
- }
800
-
801
- ws.send(JSON.stringify(response))
802
- } catch (error: any) {
803
- ws.send(JSON.stringify({
804
- type: 'ERROR',
805
- componentId: message.componentId,
806
- roomId: message.roomId,
807
- error: error.message,
808
- requestId: message.requestId,
809
- timestamp: Date.now()
810
- }))
811
- }
812
- }
813
-
814
- async function handleRoomLeave(ws: FluxStackWebSocket, message: RoomMessage) {
815
- liveLog('rooms', message.componentId, `🚶 Component ${message.componentId} leaving room ${message.roomId}`)
816
-
817
- try {
818
- liveRoomManager.leaveRoom(message.componentId, message.roomId)
819
-
820
- const response = {
821
- type: 'ROOM_LEFT',
822
- componentId: message.componentId,
823
- roomId: message.roomId,
824
- success: true,
825
- requestId: message.requestId,
826
- timestamp: Date.now()
827
- }
828
-
829
- ws.send(JSON.stringify(response))
830
- } catch (error: any) {
831
- ws.send(JSON.stringify({
832
- type: 'ERROR',
833
- componentId: message.componentId,
834
- roomId: message.roomId,
835
- error: error.message,
836
- requestId: message.requestId,
837
- timestamp: Date.now()
838
- }))
839
- }
840
- }
841
-
842
- async function handleRoomEmit(ws: FluxStackWebSocket, message: RoomMessage) {
843
- liveLog('rooms', message.componentId, `📡 Component ${message.componentId} emitting '${message.event}' to room ${message.roomId}`)
844
-
845
- try {
846
- const count = liveRoomManager.emitToRoom(
847
- message.roomId,
848
- message.event!,
849
- message.data,
850
- message.componentId // Excluir quem enviou
851
- )
852
-
853
- liveLog('rooms', message.componentId, ` → Notified ${count} components`)
854
- } catch (error: any) {
855
- ws.send(JSON.stringify({
856
- type: 'ERROR',
857
- componentId: message.componentId,
858
- roomId: message.roomId,
859
- error: error.message,
860
- timestamp: Date.now()
861
- }))
862
- }
863
- }
864
-
865
- async function handleRoomStateSet(ws: FluxStackWebSocket, message: RoomMessage) {
866
- liveLog('rooms', message.componentId, `📝 Component ${message.componentId} updating state in room ${message.roomId}`)
41
+ await liveServer.start()
42
+ context.logger.debug('Live Components started via @fluxstack/live')
43
+ },
867
44
 
868
- try {
869
- liveRoomManager.setRoomState(
870
- message.roomId,
871
- message.data ?? {},
872
- message.componentId // Excluir quem enviou
873
- )
874
- } catch (error: any) {
875
- ws.send(JSON.stringify({
876
- type: 'ERROR',
877
- componentId: message.componentId,
878
- roomId: message.roomId,
879
- error: error.message,
880
- timestamp: Date.now()
881
- }))
45
+ onServerStart: async (context: PluginContext) => {
46
+ context.logger.debug('Live Components WebSocket ready on /api/live/ws')
882
47
  }
883
48
  }