create-fluxstack 1.13.0 → 1.14.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/patterns/anti-patterns.md +100 -0
- package/LLMD/reference/routing.md +39 -39
- package/LLMD/resources/live-auth.md +20 -2
- package/LLMD/resources/live-components.md +94 -10
- package/LLMD/resources/live-logging.md +95 -33
- package/LLMD/resources/live-upload.md +59 -8
- package/app/client/index.html +2 -2
- package/app/client/public/favicon.svg +46 -0
- package/app/client/src/App.tsx +2 -1
- package/app/client/src/assets/fluxstack-static.svg +46 -0
- package/app/client/src/assets/fluxstack.svg +183 -0
- package/app/client/src/components/AppLayout.tsx +138 -9
- package/app/client/src/components/BackButton.tsx +13 -13
- package/app/client/src/components/DemoPage.tsx +4 -4
- package/app/client/src/live/AuthDemo.tsx +23 -21
- package/app/client/src/live/ChatDemo.tsx +2 -2
- package/app/client/src/live/CounterDemo.tsx +12 -12
- package/app/client/src/live/FormDemo.tsx +2 -2
- package/app/client/src/live/LiveDebuggerPanel.tsx +779 -0
- package/app/client/src/live/RoomChatDemo.tsx +24 -16
- package/app/client/src/main.tsx +13 -13
- package/app/client/src/pages/ApiTestPage.tsx +6 -6
- package/app/client/src/pages/HomePage.tsx +80 -52
- package/app/server/live/LiveAdminPanel.ts +1 -0
- package/app/server/live/LiveChat.ts +78 -77
- package/app/server/live/LiveCounter.ts +1 -1
- package/app/server/live/LiveForm.ts +1 -0
- package/app/server/live/LiveLocalCounter.ts +38 -37
- package/app/server/live/LiveProtectedChat.ts +1 -0
- package/app/server/live/LiveRoomChat.ts +1 -0
- package/app/server/live/LiveUpload.ts +1 -0
- package/app/server/live/register-components.ts +19 -19
- package/config/system/runtime.config.ts +4 -0
- package/core/build/optimizer.ts +235 -235
- package/core/client/components/Live.tsx +17 -11
- package/core/client/components/LiveDebugger.tsx +1324 -0
- package/core/client/hooks/AdaptiveChunkSizer.ts +215 -215
- package/core/client/hooks/useLiveComponent.ts +11 -1
- package/core/client/hooks/useLiveDebugger.ts +392 -0
- package/core/client/index.ts +14 -0
- package/core/plugins/built-in/index.ts +134 -134
- package/core/plugins/built-in/live-components/commands/create-live-component.ts +4 -0
- package/core/plugins/built-in/vite/index.ts +75 -21
- package/core/server/index.ts +15 -15
- package/core/server/live/ComponentRegistry.ts +55 -26
- package/core/server/live/FileUploadManager.ts +188 -24
- package/core/server/live/LiveDebugger.ts +462 -0
- package/core/server/live/LiveLogger.ts +38 -5
- package/core/server/live/LiveRoomManager.ts +17 -1
- package/core/server/live/StateSignature.ts +87 -27
- package/core/server/live/WebSocketConnectionManager.ts +11 -10
- package/core/server/live/auto-generated-components.ts +1 -1
- package/core/server/live/websocket-plugin.ts +233 -8
- package/core/server/plugins/static-files-plugin.ts +179 -69
- package/core/types/build.ts +219 -219
- package/core/types/plugin.ts +107 -107
- package/core/types/types.ts +145 -9
- package/core/utils/logger/startup-banner.ts +82 -82
- package/core/utils/version.ts +6 -6
- package/package.json +1 -1
- package/app/client/src/assets/react.svg +0 -1
|
@@ -0,0 +1,462 @@
|
|
|
1
|
+
// 🔍 FluxStack Live Component Debugger - Server-Side Event Bus
|
|
2
|
+
//
|
|
3
|
+
// Captures and streams debug events from the Live Components system.
|
|
4
|
+
// Controlled by debugLive in config/system/runtime.config.ts (DEBUG_LIVE env var).
|
|
5
|
+
//
|
|
6
|
+
// Events captured:
|
|
7
|
+
// - Component mount/unmount/rehydrate
|
|
8
|
+
// - State changes (setState, proxy mutations)
|
|
9
|
+
// - Action calls (name, payload, result, duration, errors)
|
|
10
|
+
// - Room events (join, leave, emit)
|
|
11
|
+
// - WebSocket connections/disconnections
|
|
12
|
+
// - Errors
|
|
13
|
+
//
|
|
14
|
+
// Usage:
|
|
15
|
+
// import { liveDebugger } from './LiveDebugger'
|
|
16
|
+
// liveDebugger.emit('STATE_CHANGE', componentId, componentName, { delta, fullState })
|
|
17
|
+
|
|
18
|
+
import type { FluxStackWebSocket } from '@core/types/types'
|
|
19
|
+
import { appRuntimeConfig } from '@config'
|
|
20
|
+
|
|
21
|
+
// ===== Types =====
|
|
22
|
+
|
|
23
|
+
export type DebugEventType =
|
|
24
|
+
| 'COMPONENT_MOUNT'
|
|
25
|
+
| 'COMPONENT_UNMOUNT'
|
|
26
|
+
| 'COMPONENT_REHYDRATE'
|
|
27
|
+
| 'STATE_CHANGE'
|
|
28
|
+
| 'ACTION_CALL'
|
|
29
|
+
| 'ACTION_RESULT'
|
|
30
|
+
| 'ACTION_ERROR'
|
|
31
|
+
| 'ROOM_JOIN'
|
|
32
|
+
| 'ROOM_LEAVE'
|
|
33
|
+
| 'ROOM_EMIT'
|
|
34
|
+
| 'ROOM_EVENT_RECEIVED'
|
|
35
|
+
| 'WS_CONNECT'
|
|
36
|
+
| 'WS_DISCONNECT'
|
|
37
|
+
| 'ERROR'
|
|
38
|
+
| 'LOG'
|
|
39
|
+
|
|
40
|
+
export interface DebugEvent {
|
|
41
|
+
id: string
|
|
42
|
+
timestamp: number
|
|
43
|
+
type: DebugEventType
|
|
44
|
+
componentId: string | null
|
|
45
|
+
componentName: string | null
|
|
46
|
+
data: Record<string, unknown>
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface ComponentSnapshot {
|
|
50
|
+
componentId: string
|
|
51
|
+
componentName: string
|
|
52
|
+
/** Developer-defined label for easier identification in the debugger */
|
|
53
|
+
debugLabel?: string
|
|
54
|
+
state: Record<string, unknown>
|
|
55
|
+
rooms: string[]
|
|
56
|
+
mountedAt: number
|
|
57
|
+
lastActivity: number
|
|
58
|
+
actionCount: number
|
|
59
|
+
stateChangeCount: number
|
|
60
|
+
errorCount: number
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface DebugSnapshot {
|
|
64
|
+
components: ComponentSnapshot[]
|
|
65
|
+
connections: number
|
|
66
|
+
uptime: number
|
|
67
|
+
totalEvents: number
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ===== Debug Message Types (sent to debug clients) =====
|
|
71
|
+
|
|
72
|
+
export interface DebugWsMessage {
|
|
73
|
+
type: 'DEBUG_EVENT' | 'DEBUG_SNAPSHOT' | 'DEBUG_WELCOME' | 'DEBUG_DISABLED'
|
|
74
|
+
event?: DebugEvent
|
|
75
|
+
snapshot?: DebugSnapshot
|
|
76
|
+
enabled?: boolean
|
|
77
|
+
timestamp: number
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ===== LiveDebugger =====
|
|
81
|
+
|
|
82
|
+
const MAX_EVENTS = 500
|
|
83
|
+
const MAX_STATE_SIZE = 50_000 // Truncate large states in debug events
|
|
84
|
+
|
|
85
|
+
class LiveDebugger {
|
|
86
|
+
private events: DebugEvent[] = []
|
|
87
|
+
private componentSnapshots = new Map<string, ComponentSnapshot>()
|
|
88
|
+
private debugClients = new Set<FluxStackWebSocket>()
|
|
89
|
+
private _enabled = false
|
|
90
|
+
private startTime = Date.now()
|
|
91
|
+
private eventCounter = 0
|
|
92
|
+
|
|
93
|
+
constructor() {
|
|
94
|
+
// Read from FluxStack config system (config/system/runtime.config.ts)
|
|
95
|
+
// Defaults to true in development, false otherwise.
|
|
96
|
+
// Override via DEBUG_LIVE env var or config reload.
|
|
97
|
+
this._enabled = appRuntimeConfig.values.debugLive ?? false
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
get enabled(): boolean {
|
|
101
|
+
return this._enabled
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
set enabled(value: boolean) {
|
|
105
|
+
this._enabled = value
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ===== Event Emission =====
|
|
109
|
+
|
|
110
|
+
emit(
|
|
111
|
+
type: DebugEventType,
|
|
112
|
+
componentId: string | null,
|
|
113
|
+
componentName: string | null,
|
|
114
|
+
data: Record<string, unknown> = {}
|
|
115
|
+
): void {
|
|
116
|
+
if (!this._enabled) return
|
|
117
|
+
|
|
118
|
+
const event: DebugEvent = {
|
|
119
|
+
id: `dbg-${++this.eventCounter}`,
|
|
120
|
+
timestamp: Date.now(),
|
|
121
|
+
type,
|
|
122
|
+
componentId,
|
|
123
|
+
componentName,
|
|
124
|
+
data: this.sanitizeData(data)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Store in circular buffer
|
|
128
|
+
this.events.push(event)
|
|
129
|
+
if (this.events.length > MAX_EVENTS) {
|
|
130
|
+
this.events.shift()
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Update component snapshot
|
|
134
|
+
if (componentId) {
|
|
135
|
+
this.updateSnapshot(event)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Broadcast to debug clients
|
|
139
|
+
this.broadcastEvent(event)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// ===== Component Tracking =====
|
|
143
|
+
|
|
144
|
+
trackComponentMount(
|
|
145
|
+
componentId: string,
|
|
146
|
+
componentName: string,
|
|
147
|
+
initialState: Record<string, unknown>,
|
|
148
|
+
room?: string,
|
|
149
|
+
debugLabel?: string
|
|
150
|
+
): void {
|
|
151
|
+
if (!this._enabled) return
|
|
152
|
+
|
|
153
|
+
const snapshot: ComponentSnapshot = {
|
|
154
|
+
componentId,
|
|
155
|
+
componentName,
|
|
156
|
+
debugLabel,
|
|
157
|
+
state: this.sanitizeState(initialState),
|
|
158
|
+
rooms: room ? [room] : [],
|
|
159
|
+
mountedAt: Date.now(),
|
|
160
|
+
lastActivity: Date.now(),
|
|
161
|
+
actionCount: 0,
|
|
162
|
+
stateChangeCount: 0,
|
|
163
|
+
errorCount: 0
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
this.componentSnapshots.set(componentId, snapshot)
|
|
167
|
+
|
|
168
|
+
this.emit('COMPONENT_MOUNT', componentId, componentName, {
|
|
169
|
+
initialState: snapshot.state,
|
|
170
|
+
room: room ?? null,
|
|
171
|
+
debugLabel: debugLabel ?? null
|
|
172
|
+
})
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
trackComponentUnmount(componentId: string): void {
|
|
176
|
+
if (!this._enabled) return
|
|
177
|
+
|
|
178
|
+
const snapshot = this.componentSnapshots.get(componentId)
|
|
179
|
+
const componentName = snapshot?.componentName ?? null
|
|
180
|
+
|
|
181
|
+
this.emit('COMPONENT_UNMOUNT', componentId, componentName, {
|
|
182
|
+
lifetime: snapshot ? Date.now() - snapshot.mountedAt : 0,
|
|
183
|
+
totalActions: snapshot?.actionCount ?? 0,
|
|
184
|
+
totalStateChanges: snapshot?.stateChangeCount ?? 0,
|
|
185
|
+
totalErrors: snapshot?.errorCount ?? 0
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
this.componentSnapshots.delete(componentId)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
trackStateChange(
|
|
192
|
+
componentId: string,
|
|
193
|
+
delta: Record<string, unknown>,
|
|
194
|
+
fullState: Record<string, unknown>,
|
|
195
|
+
source: 'proxy' | 'setState' | 'rehydrate' = 'setState'
|
|
196
|
+
): void {
|
|
197
|
+
if (!this._enabled) return
|
|
198
|
+
|
|
199
|
+
const snapshot = this.componentSnapshots.get(componentId)
|
|
200
|
+
if (snapshot) {
|
|
201
|
+
snapshot.state = this.sanitizeState(fullState)
|
|
202
|
+
snapshot.stateChangeCount++
|
|
203
|
+
snapshot.lastActivity = Date.now()
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
this.emit('STATE_CHANGE', componentId, snapshot?.componentName ?? null, {
|
|
207
|
+
delta,
|
|
208
|
+
fullState: this.sanitizeState(fullState),
|
|
209
|
+
source
|
|
210
|
+
})
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
trackActionCall(
|
|
214
|
+
componentId: string,
|
|
215
|
+
action: string,
|
|
216
|
+
payload: unknown
|
|
217
|
+
): void {
|
|
218
|
+
if (!this._enabled) return
|
|
219
|
+
|
|
220
|
+
const snapshot = this.componentSnapshots.get(componentId)
|
|
221
|
+
if (snapshot) {
|
|
222
|
+
snapshot.actionCount++
|
|
223
|
+
snapshot.lastActivity = Date.now()
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
this.emit('ACTION_CALL', componentId, snapshot?.componentName ?? null, {
|
|
227
|
+
action,
|
|
228
|
+
payload: this.sanitizeData({ payload }).payload
|
|
229
|
+
})
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
trackActionResult(
|
|
233
|
+
componentId: string,
|
|
234
|
+
action: string,
|
|
235
|
+
result: unknown,
|
|
236
|
+
duration: number
|
|
237
|
+
): void {
|
|
238
|
+
if (!this._enabled) return
|
|
239
|
+
|
|
240
|
+
const snapshot = this.componentSnapshots.get(componentId)
|
|
241
|
+
|
|
242
|
+
this.emit('ACTION_RESULT', componentId, snapshot?.componentName ?? null, {
|
|
243
|
+
action,
|
|
244
|
+
result: this.sanitizeData({ result }).result,
|
|
245
|
+
duration
|
|
246
|
+
})
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
trackActionError(
|
|
250
|
+
componentId: string,
|
|
251
|
+
action: string,
|
|
252
|
+
error: string,
|
|
253
|
+
duration: number
|
|
254
|
+
): void {
|
|
255
|
+
if (!this._enabled) return
|
|
256
|
+
|
|
257
|
+
const snapshot = this.componentSnapshots.get(componentId)
|
|
258
|
+
if (snapshot) {
|
|
259
|
+
snapshot.errorCount++
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
this.emit('ACTION_ERROR', componentId, snapshot?.componentName ?? null, {
|
|
263
|
+
action,
|
|
264
|
+
error,
|
|
265
|
+
duration
|
|
266
|
+
})
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
trackRoomJoin(componentId: string, roomId: string): void {
|
|
270
|
+
if (!this._enabled) return
|
|
271
|
+
|
|
272
|
+
const snapshot = this.componentSnapshots.get(componentId)
|
|
273
|
+
if (snapshot && !snapshot.rooms.includes(roomId)) {
|
|
274
|
+
snapshot.rooms.push(roomId)
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
this.emit('ROOM_JOIN', componentId, snapshot?.componentName ?? null, { roomId })
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
trackRoomLeave(componentId: string, roomId: string): void {
|
|
281
|
+
if (!this._enabled) return
|
|
282
|
+
|
|
283
|
+
const snapshot = this.componentSnapshots.get(componentId)
|
|
284
|
+
if (snapshot) {
|
|
285
|
+
snapshot.rooms = snapshot.rooms.filter(r => r !== roomId)
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
this.emit('ROOM_LEAVE', componentId, snapshot?.componentName ?? null, { roomId })
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
trackRoomEmit(componentId: string, roomId: string, event: string, data: unknown): void {
|
|
292
|
+
if (!this._enabled) return
|
|
293
|
+
|
|
294
|
+
const snapshot = this.componentSnapshots.get(componentId)
|
|
295
|
+
|
|
296
|
+
this.emit('ROOM_EMIT', componentId, snapshot?.componentName ?? null, {
|
|
297
|
+
roomId,
|
|
298
|
+
event,
|
|
299
|
+
data: this.sanitizeData({ data }).data
|
|
300
|
+
})
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
trackConnection(connectionId: string): void {
|
|
304
|
+
if (!this._enabled) return
|
|
305
|
+
this.emit('WS_CONNECT', null, null, { connectionId })
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
trackDisconnection(connectionId: string, componentCount: number): void {
|
|
309
|
+
if (!this._enabled) return
|
|
310
|
+
this.emit('WS_DISCONNECT', null, null, { connectionId, componentCount })
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
trackError(componentId: string | null, error: string, context?: Record<string, unknown>): void {
|
|
314
|
+
if (!this._enabled) return
|
|
315
|
+
|
|
316
|
+
const snapshot = componentId ? this.componentSnapshots.get(componentId) : null
|
|
317
|
+
if (snapshot) {
|
|
318
|
+
snapshot.errorCount++
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
this.emit('ERROR', componentId, snapshot?.componentName ?? null, {
|
|
322
|
+
error,
|
|
323
|
+
...context
|
|
324
|
+
})
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// ===== Debug Client Management =====
|
|
328
|
+
|
|
329
|
+
registerDebugClient(ws: FluxStackWebSocket): void {
|
|
330
|
+
// If debugging is disabled, tell the client and close
|
|
331
|
+
if (!this._enabled) {
|
|
332
|
+
const disabled: DebugWsMessage = {
|
|
333
|
+
type: 'DEBUG_DISABLED',
|
|
334
|
+
enabled: false,
|
|
335
|
+
timestamp: Date.now()
|
|
336
|
+
}
|
|
337
|
+
ws.send(JSON.stringify(disabled))
|
|
338
|
+
ws.close()
|
|
339
|
+
return
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
this.debugClients.add(ws)
|
|
343
|
+
|
|
344
|
+
// Send welcome with current snapshot
|
|
345
|
+
const welcome: DebugWsMessage = {
|
|
346
|
+
type: 'DEBUG_WELCOME',
|
|
347
|
+
enabled: true,
|
|
348
|
+
snapshot: this.getSnapshot(),
|
|
349
|
+
timestamp: Date.now()
|
|
350
|
+
}
|
|
351
|
+
ws.send(JSON.stringify(welcome))
|
|
352
|
+
|
|
353
|
+
// Send recent events
|
|
354
|
+
for (const event of this.events.slice(-100)) {
|
|
355
|
+
const msg: DebugWsMessage = {
|
|
356
|
+
type: 'DEBUG_EVENT',
|
|
357
|
+
event,
|
|
358
|
+
timestamp: Date.now()
|
|
359
|
+
}
|
|
360
|
+
ws.send(JSON.stringify(msg))
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
unregisterDebugClient(ws: FluxStackWebSocket): void {
|
|
365
|
+
this.debugClients.delete(ws)
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// ===== Snapshot =====
|
|
369
|
+
|
|
370
|
+
getSnapshot(): DebugSnapshot {
|
|
371
|
+
return {
|
|
372
|
+
components: Array.from(this.componentSnapshots.values()),
|
|
373
|
+
connections: this.debugClients.size,
|
|
374
|
+
uptime: Date.now() - this.startTime,
|
|
375
|
+
totalEvents: this.eventCounter
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
getComponentState(componentId: string): ComponentSnapshot | null {
|
|
380
|
+
return this.componentSnapshots.get(componentId) ?? null
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
getEvents(filter?: {
|
|
384
|
+
componentId?: string
|
|
385
|
+
type?: DebugEventType
|
|
386
|
+
limit?: number
|
|
387
|
+
}): DebugEvent[] {
|
|
388
|
+
let filtered = this.events
|
|
389
|
+
|
|
390
|
+
if (filter?.componentId) {
|
|
391
|
+
filtered = filtered.filter(e => e.componentId === filter.componentId)
|
|
392
|
+
}
|
|
393
|
+
if (filter?.type) {
|
|
394
|
+
filtered = filtered.filter(e => e.type === filter.type)
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
const limit = filter?.limit ?? 100
|
|
398
|
+
return filtered.slice(-limit)
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
clearEvents(): void {
|
|
402
|
+
this.events = []
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// ===== Internal =====
|
|
406
|
+
|
|
407
|
+
private broadcastEvent(event: DebugEvent): void {
|
|
408
|
+
if (this.debugClients.size === 0) return
|
|
409
|
+
|
|
410
|
+
const msg: DebugWsMessage = {
|
|
411
|
+
type: 'DEBUG_EVENT',
|
|
412
|
+
event,
|
|
413
|
+
timestamp: Date.now()
|
|
414
|
+
}
|
|
415
|
+
const json = JSON.stringify(msg)
|
|
416
|
+
|
|
417
|
+
for (const client of this.debugClients) {
|
|
418
|
+
try {
|
|
419
|
+
client.send(json)
|
|
420
|
+
} catch {
|
|
421
|
+
// Client disconnected, will be cleaned up
|
|
422
|
+
this.debugClients.delete(client)
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
private sanitizeData(data: Record<string, unknown>): Record<string, unknown> {
|
|
428
|
+
try {
|
|
429
|
+
const json = JSON.stringify(data)
|
|
430
|
+
if (json.length > MAX_STATE_SIZE) {
|
|
431
|
+
return { _truncated: true, _size: json.length, _preview: json.slice(0, 500) + '...' }
|
|
432
|
+
}
|
|
433
|
+
return JSON.parse(json) // Deep clone to avoid mutation
|
|
434
|
+
} catch {
|
|
435
|
+
return { _serialization_error: true }
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
private sanitizeState(state: Record<string, unknown>): Record<string, unknown> {
|
|
440
|
+
try {
|
|
441
|
+
const json = JSON.stringify(state)
|
|
442
|
+
if (json.length > MAX_STATE_SIZE) {
|
|
443
|
+
return { _truncated: true, _size: json.length }
|
|
444
|
+
}
|
|
445
|
+
return JSON.parse(json)
|
|
446
|
+
} catch {
|
|
447
|
+
return { _serialization_error: true }
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
private updateSnapshot(event: DebugEvent): void {
|
|
452
|
+
if (!event.componentId) return
|
|
453
|
+
|
|
454
|
+
const snapshot = this.componentSnapshots.get(event.componentId)
|
|
455
|
+
if (snapshot) {
|
|
456
|
+
snapshot.lastActivity = event.timestamp
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// Global singleton
|
|
462
|
+
export const liveDebugger = new LiveDebugger()
|
|
@@ -14,10 +14,16 @@
|
|
|
14
14
|
// rooms — room create/join/leave, emit, broadcast
|
|
15
15
|
// websocket — connection open/close, auth
|
|
16
16
|
//
|
|
17
|
-
//
|
|
18
|
-
// LIVE_LOGGING=true → all global logs
|
|
19
|
-
// LIVE_LOGGING=lifecycle,rooms → only these categories
|
|
20
|
-
// (unset or 'false') → silent (default)
|
|
17
|
+
// Console output controlled by LIVE_LOGGING env var:
|
|
18
|
+
// LIVE_LOGGING=true → all global logs to console
|
|
19
|
+
// LIVE_LOGGING=lifecycle,rooms → only these categories to console
|
|
20
|
+
// (unset or 'false') → silent console (default)
|
|
21
|
+
//
|
|
22
|
+
// Debug panel: All liveLog/liveWarn calls are always forwarded to the Live Debugger
|
|
23
|
+
// (when DEBUG_LIVE is enabled) as LOG events, regardless of LIVE_LOGGING setting.
|
|
24
|
+
// This way the console stays clean but all details are visible in the debug panel.
|
|
25
|
+
|
|
26
|
+
import { liveDebugger } from './LiveDebugger'
|
|
21
27
|
|
|
22
28
|
export type LiveLogCategory = 'lifecycle' | 'messages' | 'state' | 'performance' | 'rooms' | 'websocket'
|
|
23
29
|
|
|
@@ -79,8 +85,26 @@ function shouldLog(componentId: string | null, category: LiveLogCategory): boole
|
|
|
79
85
|
return cfg.includes(category)
|
|
80
86
|
}
|
|
81
87
|
|
|
88
|
+
/**
|
|
89
|
+
* Forward a log entry to the Live Debugger as a LOG event.
|
|
90
|
+
* Always emits when the debugger is enabled, regardless of console logging config.
|
|
91
|
+
*/
|
|
92
|
+
function emitToDebugger(category: LiveLogCategory, level: 'info' | 'warn', componentId: string | null, message: string, args: unknown[]): void {
|
|
93
|
+
if (!liveDebugger.enabled) return
|
|
94
|
+
|
|
95
|
+
const data: Record<string, unknown> = { category, level, message }
|
|
96
|
+
if (args.length === 1 && typeof args[0] === 'object' && args[0] !== null) {
|
|
97
|
+
data.details = args[0]
|
|
98
|
+
} else if (args.length > 0) {
|
|
99
|
+
data.details = args
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
liveDebugger.emit('LOG', componentId, null, data)
|
|
103
|
+
}
|
|
104
|
+
|
|
82
105
|
/**
|
|
83
106
|
* Log a message gated by the component's logging config.
|
|
107
|
+
* Always forwarded to the Live Debugger when active (DEBUG_LIVE).
|
|
84
108
|
*
|
|
85
109
|
* @param category - Log category
|
|
86
110
|
* @param componentId - Component ID, or null for global logs
|
|
@@ -88,6 +112,10 @@ function shouldLog(componentId: string | null, category: LiveLogCategory): boole
|
|
|
88
112
|
* @param args - Extra arguments (objects, etc.)
|
|
89
113
|
*/
|
|
90
114
|
export function liveLog(category: LiveLogCategory, componentId: string | null, message: string, ...args: unknown[]): void {
|
|
115
|
+
// Always forward to debug panel
|
|
116
|
+
emitToDebugger(category, 'info', componentId, message, args)
|
|
117
|
+
|
|
118
|
+
// Console output gated by config
|
|
91
119
|
if (shouldLog(componentId, category)) {
|
|
92
120
|
if (args.length > 0) {
|
|
93
121
|
console.log(message, ...args)
|
|
@@ -98,9 +126,14 @@ export function liveLog(category: LiveLogCategory, componentId: string | null, m
|
|
|
98
126
|
}
|
|
99
127
|
|
|
100
128
|
/**
|
|
101
|
-
* Warn-level log gated by config (for non-error informational warnings like perf alerts)
|
|
129
|
+
* Warn-level log gated by config (for non-error informational warnings like perf alerts).
|
|
130
|
+
* Always forwarded to the Live Debugger when active (DEBUG_LIVE).
|
|
102
131
|
*/
|
|
103
132
|
export function liveWarn(category: LiveLogCategory, componentId: string | null, message: string, ...args: unknown[]): void {
|
|
133
|
+
// Always forward to debug panel
|
|
134
|
+
emitToDebugger(category, 'warn', componentId, message, args)
|
|
135
|
+
|
|
136
|
+
// Console output gated by config
|
|
104
137
|
if (shouldLog(componentId, category)) {
|
|
105
138
|
if (args.length > 0) {
|
|
106
139
|
console.warn(message, ...args)
|
|
@@ -36,6 +36,11 @@ class LiveRoomManager {
|
|
|
36
36
|
* Componente entra em uma sala
|
|
37
37
|
*/
|
|
38
38
|
joinRoom<TState = any>(componentId: string, roomId: string, ws: FluxStackWebSocket, initialState?: TState): { state: TState } {
|
|
39
|
+
// 🔒 Validate room name format
|
|
40
|
+
if (!roomId || !/^[a-zA-Z0-9_:.-]{1,64}$/.test(roomId)) {
|
|
41
|
+
throw new Error('Invalid room name. Must be 1-64 alphanumeric characters, hyphens, underscores, dots, or colons.')
|
|
42
|
+
}
|
|
43
|
+
|
|
39
44
|
// Criar sala se não existir
|
|
40
45
|
if (!this.rooms.has(roomId)) {
|
|
41
46
|
this.rooms.set(roomId, {
|
|
@@ -164,6 +169,9 @@ class LiveRoomManager {
|
|
|
164
169
|
}, excludeComponentId)
|
|
165
170
|
}
|
|
166
171
|
|
|
172
|
+
// 🔒 Maximum room state size (10MB) to prevent memory exhaustion attacks
|
|
173
|
+
private readonly MAX_ROOM_STATE_SIZE = 10 * 1024 * 1024
|
|
174
|
+
|
|
167
175
|
/**
|
|
168
176
|
* Atualizar estado da sala
|
|
169
177
|
*/
|
|
@@ -172,7 +180,15 @@ class LiveRoomManager {
|
|
|
172
180
|
if (!room) return
|
|
173
181
|
|
|
174
182
|
// Merge estado
|
|
175
|
-
|
|
183
|
+
const newState = { ...room.state, ...updates }
|
|
184
|
+
|
|
185
|
+
// 🔒 Validate state size to prevent memory exhaustion
|
|
186
|
+
const stateSize = Buffer.byteLength(JSON.stringify(newState), 'utf8')
|
|
187
|
+
if (stateSize > this.MAX_ROOM_STATE_SIZE) {
|
|
188
|
+
throw new Error('Room state exceeds maximum size limit')
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
room.state = newState
|
|
176
192
|
room.lastActivity = Date.now()
|
|
177
193
|
|
|
178
194
|
// Notificar todos os membros
|