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.
Files changed (61) 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 +94 -10
  5. package/LLMD/resources/live-logging.md +95 -33
  6. package/LLMD/resources/live-upload.md +59 -8
  7. package/app/client/index.html +2 -2
  8. package/app/client/public/favicon.svg +46 -0
  9. package/app/client/src/App.tsx +2 -1
  10. package/app/client/src/assets/fluxstack-static.svg +46 -0
  11. package/app/client/src/assets/fluxstack.svg +183 -0
  12. package/app/client/src/components/AppLayout.tsx +138 -9
  13. package/app/client/src/components/BackButton.tsx +13 -13
  14. package/app/client/src/components/DemoPage.tsx +4 -4
  15. package/app/client/src/live/AuthDemo.tsx +23 -21
  16. package/app/client/src/live/ChatDemo.tsx +2 -2
  17. package/app/client/src/live/CounterDemo.tsx +12 -12
  18. package/app/client/src/live/FormDemo.tsx +2 -2
  19. package/app/client/src/live/LiveDebuggerPanel.tsx +779 -0
  20. package/app/client/src/live/RoomChatDemo.tsx +24 -16
  21. package/app/client/src/main.tsx +13 -13
  22. package/app/client/src/pages/ApiTestPage.tsx +6 -6
  23. package/app/client/src/pages/HomePage.tsx +80 -52
  24. package/app/server/live/LiveAdminPanel.ts +1 -0
  25. package/app/server/live/LiveChat.ts +78 -77
  26. package/app/server/live/LiveCounter.ts +1 -1
  27. package/app/server/live/LiveForm.ts +1 -0
  28. package/app/server/live/LiveLocalCounter.ts +38 -37
  29. package/app/server/live/LiveProtectedChat.ts +1 -0
  30. package/app/server/live/LiveRoomChat.ts +1 -0
  31. package/app/server/live/LiveUpload.ts +1 -0
  32. package/app/server/live/register-components.ts +19 -19
  33. package/config/system/runtime.config.ts +4 -0
  34. package/core/build/optimizer.ts +235 -235
  35. package/core/client/components/Live.tsx +17 -11
  36. package/core/client/components/LiveDebugger.tsx +1324 -0
  37. package/core/client/hooks/AdaptiveChunkSizer.ts +215 -215
  38. package/core/client/hooks/useLiveComponent.ts +11 -1
  39. package/core/client/hooks/useLiveDebugger.ts +392 -0
  40. package/core/client/index.ts +14 -0
  41. package/core/plugins/built-in/index.ts +134 -134
  42. package/core/plugins/built-in/live-components/commands/create-live-component.ts +4 -0
  43. package/core/plugins/built-in/vite/index.ts +75 -21
  44. package/core/server/index.ts +15 -15
  45. package/core/server/live/ComponentRegistry.ts +55 -26
  46. package/core/server/live/FileUploadManager.ts +188 -24
  47. package/core/server/live/LiveDebugger.ts +462 -0
  48. package/core/server/live/LiveLogger.ts +38 -5
  49. package/core/server/live/LiveRoomManager.ts +17 -1
  50. package/core/server/live/StateSignature.ts +87 -27
  51. package/core/server/live/WebSocketConnectionManager.ts +11 -10
  52. package/core/server/live/auto-generated-components.ts +1 -1
  53. package/core/server/live/websocket-plugin.ts +233 -8
  54. package/core/server/plugins/static-files-plugin.ts +179 -69
  55. package/core/types/build.ts +219 -219
  56. package/core/types/plugin.ts +107 -107
  57. package/core/types/types.ts +145 -9
  58. package/core/utils/logger/startup-banner.ts +82 -82
  59. package/core/utils/version.ts +6 -6
  60. package/package.json +1 -1
  61. 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
- // Global (non-component) logs controlled by LIVE_LOGGING env var:
18
- // LIVE_LOGGING=true → all global logs
19
- // LIVE_LOGGING=lifecycle,rooms → only these categories globally
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
- room.state = { ...room.state, ...updates }
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