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.
- package/LLMD/INDEX.md +4 -3
- package/LLMD/resources/live-binary-delta.md +507 -0
- package/LLMD/resources/live-components.md +208 -12
- package/LLMD/resources/live-rooms.md +731 -333
- package/app/client/.live-stubs/LiveAdminPanel.js +5 -0
- package/app/client/.live-stubs/LiveCounter.js +9 -0
- package/app/client/.live-stubs/LiveForm.js +11 -0
- package/app/client/.live-stubs/LiveLocalCounter.js +8 -0
- package/app/client/.live-stubs/LivePingPong.js +10 -0
- package/app/client/.live-stubs/LiveRoomChat.js +11 -0
- package/app/client/.live-stubs/LiveSharedCounter.js +10 -0
- package/app/client/.live-stubs/LiveUpload.js +15 -0
- package/app/client/src/App.tsx +19 -7
- package/app/client/src/components/AppLayout.tsx +18 -10
- package/app/client/src/live/PingPongDemo.tsx +199 -0
- package/app/client/src/live/RoomChatDemo.tsx +187 -22
- package/app/client/src/live/SharedCounterDemo.tsx +142 -0
- package/app/server/auth/DevAuthProvider.ts +2 -2
- package/app/server/auth/JWTAuthProvider.example.ts +2 -2
- package/app/server/index.ts +2 -2
- package/app/server/live/LiveAdminPanel.ts +1 -1
- package/app/server/live/LivePingPong.ts +61 -0
- package/app/server/live/LiveProtectedChat.ts +1 -1
- package/app/server/live/LiveRoomChat.ts +106 -38
- package/app/server/live/LiveSharedCounter.ts +73 -0
- package/app/server/live/rooms/ChatRoom.ts +68 -0
- package/app/server/live/rooms/CounterRoom.ts +51 -0
- package/app/server/live/rooms/DirectoryRoom.ts +42 -0
- package/app/server/live/rooms/PingRoom.ts +40 -0
- package/app/server/routes/room.routes.ts +1 -2
- package/core/build/live-components-generator.ts +11 -2
- package/core/build/vite-plugins.ts +28 -0
- package/core/client/hooks/useLiveUpload.ts +3 -4
- package/core/client/index.ts +25 -35
- package/core/framework/server.ts +1 -1
- package/core/server/index.ts +1 -2
- package/core/server/live/auto-generated-components.ts +5 -8
- package/core/server/live/index.ts +90 -21
- package/core/server/live/websocket-plugin.ts +54 -1079
- package/core/types/types.ts +76 -1025
- package/core/utils/version.ts +1 -1
- package/create-fluxstack.ts +1 -1
- package/package.json +100 -95
- package/plugins/crypto-auth/index.ts +1 -1
- package/plugins/crypto-auth/server/CryptoAuthLiveProvider.ts +2 -2
- package/tsconfig.json +4 -1
- package/vite.config.ts +40 -12
- package/app/client/src/live/ChatDemo.tsx +0 -107
- package/app/client/src/live/LiveDebuggerPanel.tsx +0 -779
- package/app/server/live/LiveChat.ts +0 -78
- package/core/client/LiveComponentsProvider.tsx +0 -531
- package/core/client/components/Live.tsx +0 -111
- package/core/client/components/LiveDebugger.tsx +0 -1324
- package/core/client/hooks/AdaptiveChunkSizer.ts +0 -215
- package/core/client/hooks/state-validator.ts +0 -130
- package/core/client/hooks/useChunkedUpload.ts +0 -359
- package/core/client/hooks/useLiveChunkedUpload.ts +0 -87
- package/core/client/hooks/useLiveComponent.ts +0 -853
- package/core/client/hooks/useLiveDebugger.ts +0 -392
- package/core/client/hooks/useRoom.ts +0 -409
- package/core/client/hooks/useRoomProxy.ts +0 -382
- package/core/server/live/ComponentRegistry.ts +0 -1128
- package/core/server/live/FileUploadManager.ts +0 -446
- package/core/server/live/LiveComponentPerformanceMonitor.ts +0 -931
- package/core/server/live/LiveDebugger.ts +0 -462
- package/core/server/live/LiveLogger.ts +0 -144
- package/core/server/live/LiveRoomManager.ts +0 -278
- package/core/server/live/RoomEventBus.ts +0 -234
- package/core/server/live/RoomStateManager.ts +0 -172
- package/core/server/live/SingleConnectionManager.ts +0 -0
- package/core/server/live/StateSignature.ts +0 -705
- package/core/server/live/WebSocketConnectionManager.ts +0 -710
- package/core/server/live/auth/LiveAuthContext.ts +0 -71
- package/core/server/live/auth/LiveAuthManager.ts +0 -304
- package/core/server/live/auth/index.ts +0 -19
- package/core/server/live/auth/types.ts +0 -179
|
@@ -1,1108 +1,83 @@
|
|
|
1
|
-
//
|
|
1
|
+
// FluxStack Live Components Plugin — delegates to @fluxstack/live
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
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 {
|
|
15
|
-
import { liveDebugger } from './LiveDebugger'
|
|
8
|
+
import { readdirSync, existsSync } from 'fs'
|
|
16
9
|
|
|
17
|
-
//
|
|
10
|
+
// Expose the LiveServer instance so other parts of FluxStack can access it
|
|
11
|
+
export let liveServer: LiveServer | null = null
|
|
18
12
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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: '
|
|
156
|
-
description: 'Real-time Live Components
|
|
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
|
-
|
|
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
|
-
|
|
758
|
-
|
|
759
|
-
|
|
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
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
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
|
-
|
|
780
|
-
|
|
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
|
-
|
|
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
|
-
|
|
917
|
-
|
|
918
|
-
|
|
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
|
-
|
|
1011
|
-
|
|
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
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
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
|
-
|
|
1052
|
-
|
|
66
|
+
const rooms: LiveRoomClass[] = []
|
|
67
|
+
const files = readdirSync(dir).filter(f => f.endsWith('.ts') && !f.endsWith('.d.ts'))
|
|
1053
68
|
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
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
|
-
|
|
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
|
}
|