create-fluxstack 1.14.0 → 1.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/LLMD/INDEX.md +4 -3
  2. package/LLMD/resources/live-binary-delta.md +507 -0
  3. package/LLMD/resources/live-components.md +208 -12
  4. package/LLMD/resources/live-rooms.md +731 -333
  5. package/app/client/.live-stubs/LiveAdminPanel.js +5 -0
  6. package/app/client/.live-stubs/LiveCounter.js +9 -0
  7. package/app/client/.live-stubs/LiveForm.js +11 -0
  8. package/app/client/.live-stubs/LiveLocalCounter.js +8 -0
  9. package/app/client/.live-stubs/LivePingPong.js +10 -0
  10. package/app/client/.live-stubs/LiveRoomChat.js +11 -0
  11. package/app/client/.live-stubs/LiveSharedCounter.js +10 -0
  12. package/app/client/.live-stubs/LiveUpload.js +15 -0
  13. package/app/client/src/App.tsx +19 -7
  14. package/app/client/src/components/AppLayout.tsx +18 -10
  15. package/app/client/src/live/PingPongDemo.tsx +199 -0
  16. package/app/client/src/live/RoomChatDemo.tsx +187 -22
  17. package/app/client/src/live/SharedCounterDemo.tsx +142 -0
  18. package/app/server/auth/DevAuthProvider.ts +2 -2
  19. package/app/server/auth/JWTAuthProvider.example.ts +2 -2
  20. package/app/server/index.ts +2 -2
  21. package/app/server/live/LiveAdminPanel.ts +1 -1
  22. package/app/server/live/LivePingPong.ts +61 -0
  23. package/app/server/live/LiveProtectedChat.ts +1 -1
  24. package/app/server/live/LiveRoomChat.ts +106 -38
  25. package/app/server/live/LiveSharedCounter.ts +73 -0
  26. package/app/server/live/rooms/ChatRoom.ts +68 -0
  27. package/app/server/live/rooms/CounterRoom.ts +51 -0
  28. package/app/server/live/rooms/DirectoryRoom.ts +42 -0
  29. package/app/server/live/rooms/PingRoom.ts +40 -0
  30. package/app/server/routes/room.routes.ts +1 -2
  31. package/core/build/live-components-generator.ts +11 -2
  32. package/core/build/vite-plugins.ts +28 -0
  33. package/core/client/hooks/useLiveUpload.ts +3 -4
  34. package/core/client/index.ts +25 -35
  35. package/core/framework/server.ts +1 -1
  36. package/core/server/index.ts +1 -2
  37. package/core/server/live/auto-generated-components.ts +5 -8
  38. package/core/server/live/index.ts +90 -21
  39. package/core/server/live/websocket-plugin.ts +54 -1079
  40. package/core/types/types.ts +76 -1025
  41. package/core/utils/version.ts +1 -1
  42. package/create-fluxstack.ts +1 -1
  43. package/package.json +100 -95
  44. package/plugins/crypto-auth/index.ts +1 -1
  45. package/plugins/crypto-auth/server/CryptoAuthLiveProvider.ts +2 -2
  46. package/tsconfig.json +4 -1
  47. package/vite.config.ts +40 -12
  48. package/app/client/src/live/ChatDemo.tsx +0 -107
  49. package/app/client/src/live/LiveDebuggerPanel.tsx +0 -779
  50. package/app/server/live/LiveChat.ts +0 -78
  51. package/core/client/LiveComponentsProvider.tsx +0 -531
  52. package/core/client/components/Live.tsx +0 -111
  53. package/core/client/components/LiveDebugger.tsx +0 -1324
  54. package/core/client/hooks/AdaptiveChunkSizer.ts +0 -215
  55. package/core/client/hooks/state-validator.ts +0 -130
  56. package/core/client/hooks/useChunkedUpload.ts +0 -359
  57. package/core/client/hooks/useLiveChunkedUpload.ts +0 -87
  58. package/core/client/hooks/useLiveComponent.ts +0 -853
  59. package/core/client/hooks/useLiveDebugger.ts +0 -392
  60. package/core/client/hooks/useRoom.ts +0 -409
  61. package/core/client/hooks/useRoomProxy.ts +0 -382
  62. package/core/server/live/ComponentRegistry.ts +0 -1128
  63. package/core/server/live/FileUploadManager.ts +0 -446
  64. package/core/server/live/LiveComponentPerformanceMonitor.ts +0 -931
  65. package/core/server/live/LiveDebugger.ts +0 -462
  66. package/core/server/live/LiveLogger.ts +0 -144
  67. package/core/server/live/LiveRoomManager.ts +0 -278
  68. package/core/server/live/RoomEventBus.ts +0 -234
  69. package/core/server/live/RoomStateManager.ts +0 -172
  70. package/core/server/live/SingleConnectionManager.ts +0 -0
  71. package/core/server/live/StateSignature.ts +0 -705
  72. package/core/server/live/WebSocketConnectionManager.ts +0 -710
  73. package/core/server/live/auth/LiveAuthContext.ts +0 -71
  74. package/core/server/live/auth/LiveAuthManager.ts +0 -304
  75. package/core/server/live/auth/index.ts +0 -19
  76. package/core/server/live/auth/types.ts +0 -179
@@ -1,186 +1,94 @@
1
- // 🔥 FluxStack Live Components - Shared Types
1
+ // FluxStack Live Components - Shared Types
2
+ // Re-exports from @fluxstack/live for backward compatibility
2
3
 
3
- import { roomEvents } from '@core/server/live/RoomEventBus'
4
- import { liveRoomManager } from '@core/server/live/LiveRoomManager'
5
- import { ANONYMOUS_CONTEXT } from '@core/server/live/auth/LiveAuthContext'
6
- import { liveLog, liveWarn } from '@core/server/live/LiveLogger'
4
+ // LiveComponent base class
5
+ export { LiveComponent } from '@fluxstack/live'
7
6
 
8
- // ===== Debug Instrumentation (injectable to avoid client-side import) =====
9
- // The real debugger is injected by ComponentRegistry at server startup.
10
- // This avoids importing server-only LiveDebugger.ts from this shared types file.
11
- interface LiveDebuggerInterface {
12
- trackStateChange(componentId: string, delta: Record<string, unknown>, fullState: Record<string, unknown>, source?: string): void
13
- trackActionCall(componentId: string, action: string, payload: unknown): void
14
- trackActionResult(componentId: string, action: string, result: unknown, duration: number): void
15
- trackActionError(componentId: string, action: string, error: string, duration: number): void
16
- trackRoomEmit(componentId: string, roomId: string, event: string, data: unknown): void
17
- }
7
+ // EMIT_OVERRIDE_KEY: uses Symbol.for() for cross-module compatibility
8
+ // Not yet exported from @fluxstack/live runtime, so we define it here
9
+ export const EMIT_OVERRIDE_KEY = Symbol.for('fluxstack:emitOverride')
18
10
 
19
- let _liveDebugger: LiveDebuggerInterface | null = null
11
+ // WebSocket types FluxStack aliases
12
+ export type { GenericWebSocket as FluxStackWebSocket } from '@fluxstack/live'
13
+ export type { LiveWSData as FluxStackWSData } from '@fluxstack/live'
20
14
 
21
- /** @internal Called by ComponentRegistry to inject the debugger instance */
22
- export function _setLiveDebugger(dbg: LiveDebuggerInterface): void {
23
- _liveDebugger = dbg
24
- }
25
- import type { LiveAuthContext, LiveComponentAuth, LiveActionAuthMap } from '@core/server/live/auth/types'
15
+ // For Bun-specific raw WS (FluxStack-specific)
26
16
  import type { ServerWebSocket } from 'bun'
27
-
28
- // ============================================
29
- // 🔌 WebSocket Types for Server-Side
30
- // ============================================
31
-
32
- /**
33
- * WebSocket data stored on each connection
34
- * This is attached to ws.data by the WebSocket plugin
35
- */
36
- export interface FluxStackWSData {
37
- connectionId: string
38
- components: Map<string, LiveComponent>
39
- subscriptions: Set<string>
40
- connectedAt: Date
41
- userId?: string
42
- /** Contexto de autenticação da conexão WebSocket */
43
- authContext?: LiveAuthContext
44
- }
45
-
46
- /**
47
- * Type-safe WebSocket interface for FluxStack Live Components
48
- * Compatible with both Elysia's ElysiaWS and Bun's ServerWebSocket
49
- */
50
- export interface FluxStackWebSocket {
51
- /** Send data to the client */
52
- send(data: string | BufferSource, compress?: boolean): number
53
- /** Close the connection */
54
- close(code?: number, reason?: string): void
55
- /** Connection data storage */
56
- data: FluxStackWSData
57
- /** Remote address of the client */
58
- readonly remoteAddress: string
59
- /** Current ready state */
60
- readonly readyState: 0 | 1 | 2 | 3
17
+ import type { LiveWSData } from '@fluxstack/live'
18
+ export type FluxStackServerWebSocket = ServerWebSocket<LiveWSData>
19
+
20
+ // Protocol messages
21
+ export type {
22
+ LiveMessage,
23
+ ComponentState,
24
+ LiveComponentInstance,
25
+ ComponentDefinition,
26
+ BroadcastMessage,
27
+ WebSocketMessage,
28
+ WebSocketResponse,
29
+ HybridState,
30
+ HybridComponentOptions,
31
+ ServerRoomHandle,
32
+ ServerRoomProxy,
33
+ FileChunkData,
34
+ FileUploadStartMessage,
35
+ FileUploadChunkMessage,
36
+ FileUploadCompleteMessage,
37
+ FileUploadProgressResponse,
38
+ FileUploadCompleteResponse,
39
+ BinaryChunkHeader,
40
+ ActiveUpload,
41
+ } from '@fluxstack/live'
42
+
43
+ // Auth types
44
+ export type {
45
+ LiveAuthContext,
46
+ LiveComponentAuth,
47
+ LiveActionAuth,
48
+ LiveActionAuthMap,
49
+ LiveAuthProvider,
50
+ LiveAuthCredentials,
51
+ LiveAuthUser,
52
+ LiveAuthResult,
53
+ } from '@fluxstack/live'
54
+
55
+ // Type inference utilities
56
+ export type {
57
+ ExtractActions,
58
+ ActionNames,
59
+ ActionPayload,
60
+ ActionReturn,
61
+ InferComponentState,
62
+ InferPrivateState,
63
+ TypedCall,
64
+ TypedCallAndWait,
65
+ TypedSetValue,
66
+ UseTypedLiveComponentReturn,
67
+ } from '@fluxstack/live'
68
+
69
+ // Utility types (FluxStack-specific aliases)
70
+ export type ComponentActions<T> = {
71
+ [K in keyof T]: T[K] extends (...args: any[]) => any ? T[K] : never
61
72
  }
62
73
 
63
- /**
64
- * Raw ServerWebSocket from Bun with FluxStack data
65
- * Use this when you need access to all Bun WebSocket methods
66
- */
67
- export type FluxStackServerWebSocket = ServerWebSocket<FluxStackWSData>
74
+ export type ComponentProps<T extends import('@fluxstack/live').LiveComponent> =
75
+ T extends import('@fluxstack/live').LiveComponent<infer TState> ? TState : never
68
76
 
69
- export interface LiveMessage {
70
- type: 'COMPONENT_MOUNT' | 'COMPONENT_UNMOUNT' |
71
- 'COMPONENT_REHYDRATE' | 'COMPONENT_ACTION' | 'CALL_ACTION' |
72
- 'ACTION_RESPONSE' | 'PROPERTY_UPDATE' | 'STATE_UPDATE' | 'STATE_DELTA' | 'STATE_REHYDRATED' |
73
- 'ERROR' | 'BROADCAST' | 'FILE_UPLOAD_START' | 'FILE_UPLOAD_CHUNK' | 'FILE_UPLOAD_COMPLETE' |
74
- 'COMPONENT_PING' | 'COMPONENT_PONG' |
75
- // Auth system message
76
- 'AUTH' |
77
- // Room system messages
78
- 'ROOM_JOIN' | 'ROOM_LEAVE' | 'ROOM_EMIT' | 'ROOM_STATE_SET' | 'ROOM_STATE_GET'
79
- componentId: string
80
- action?: string
81
- property?: string
82
- payload?: any
83
- timestamp?: number
84
- userId?: string
85
- room?: string
86
- // Request-Response system
87
- requestId?: string
88
- responseId?: string
89
- expectResponse?: boolean
90
- }
77
+ export type ActionParameters<T, K extends keyof T> =
78
+ T[K] extends (...args: infer P) => any ? P : never
91
79
 
92
- export interface ComponentState {
93
- [key: string]: any
94
- }
95
-
96
- export interface LiveComponentInstance<TState = ComponentState, TActions = Record<string, Function>> {
97
- id: string
98
- state: TState
99
- call: <T extends keyof TActions>(action: T, ...args: any[]) => Promise<any>
100
- set: <K extends keyof TState>(property: K, value: TState[K]) => void
101
- loading: boolean
102
- errors: Record<string, string>
103
- connected: boolean
104
- room?: string
105
- }
80
+ export type ActionReturnType<T, K extends keyof T> =
81
+ T[K] extends (...args: any[]) => infer R ? R : never
106
82
 
107
- /**
108
- * @deprecated Use FluxStackWSData instead
109
- */
83
+ // Deprecated types (backward compat)
84
+ /** @deprecated Use FluxStackWSData instead */
110
85
  export interface WebSocketData {
111
86
  components: Map<string, any>
112
87
  userId?: string
113
88
  subscriptions: Set<string>
114
89
  }
115
90
 
116
- export interface ComponentDefinition<TState = ComponentState> {
117
- name: string
118
- initialState: TState
119
- component: new (initialState: TState, ws: FluxStackWebSocket, options?: { room?: string; userId?: string }) => LiveComponent<TState>
120
- }
121
-
122
- export interface BroadcastMessage {
123
- type: string
124
- payload: any
125
- room?: string
126
- excludeUser?: string
127
- }
128
-
129
- // WebSocket Types for Client
130
- export interface WebSocketMessage {
131
- type: string
132
- componentId?: string
133
- action?: string
134
- payload?: any
135
- timestamp?: number
136
- userId?: string
137
- room?: string
138
- // Request-Response system
139
- requestId?: string
140
- responseId?: string
141
- expectResponse?: boolean
142
- }
143
-
144
- export interface WebSocketResponse {
145
- type: 'MESSAGE_RESPONSE' | 'CONNECTION_ESTABLISHED' | 'ERROR' | 'BROADCAST' | 'ACTION_RESPONSE' | 'COMPONENT_MOUNTED' | 'COMPONENT_REHYDRATED' | 'STATE_UPDATE' | 'STATE_DELTA' | 'STATE_REHYDRATED' | 'FILE_UPLOAD_PROGRESS' | 'FILE_UPLOAD_COMPLETE' | 'FILE_UPLOAD_ERROR' | 'FILE_UPLOAD_START_RESPONSE' | 'COMPONENT_PONG' |
146
- // Auth system response
147
- 'AUTH_RESPONSE' |
148
- // Room system responses
149
- 'ROOM_EVENT' | 'ROOM_STATE' | 'ROOM_SYSTEM' | 'ROOM_JOINED' | 'ROOM_LEFT'
150
- originalType?: string
151
- componentId?: string
152
- success?: boolean
153
- result?: any
154
- // Request-Response system
155
- requestId?: string
156
- responseId?: string
157
- error?: string
158
- timestamp?: number
159
- connectionId?: string
160
- payload?: any
161
- // File upload specific fields
162
- uploadId?: string
163
- chunkIndex?: number
164
- totalChunks?: number
165
- bytesUploaded?: number
166
- totalBytes?: number
167
- progress?: number
168
- filename?: string
169
- fileUrl?: string
170
- // Re-hydration specific fields
171
- signedState?: any
172
- oldComponentId?: string
173
- newComponentId?: string
174
- }
175
-
176
- // Hybrid Live Component Types
177
- export interface HybridState<T> {
178
- data: T
179
- validation: StateValidation
180
- conflicts: StateConflict[]
181
- status: 'synced' | 'conflict' | 'disconnected'
182
- }
183
-
91
+ /** @deprecated Not used in current protocol */
184
92
  export interface StateValidation {
185
93
  checksum: string
186
94
  version: number
@@ -188,6 +96,7 @@ export interface StateValidation {
188
96
  timestamp: number
189
97
  }
190
98
 
99
+ /** @deprecated Not used in current protocol */
191
100
  export interface StateConflict {
192
101
  property: string
193
102
  clientValue: any
@@ -195,861 +104,3 @@ export interface StateConflict {
195
104
  timestamp: number
196
105
  resolved: boolean
197
106
  }
198
-
199
- export interface HybridComponentOptions {
200
- fallbackToLocal?: boolean
201
- room?: string
202
- userId?: string
203
- autoMount?: boolean
204
- debug?: boolean
205
-
206
- // Component lifecycle callbacks
207
- onConnect?: () => void // Called when WebSocket connects (can happen multiple times on reconnect)
208
- onMount?: () => void // Called after fresh mount (no prior state)
209
- onRehydrate?: () => void // Called after successful rehydration (restoring prior state)
210
- onDisconnect?: () => void // Called when WebSocket disconnects
211
- onError?: (error: string) => void
212
- onStateChange?: (newState: any, oldState: any) => void
213
- }
214
-
215
- // Interface para handle de sala no servidor
216
- export interface ServerRoomHandle<TState = any, TEvents extends Record<string, any> = Record<string, any>> {
217
- readonly id: string
218
- readonly state: TState
219
- join: (initialState?: TState) => void
220
- leave: () => void
221
- emit: <K extends keyof TEvents>(event: K, data: TEvents[K]) => number
222
- on: <K extends keyof TEvents>(event: K, handler: (data: TEvents[K]) => void) => () => void
223
- setState: (updates: Partial<TState>) => void
224
- }
225
-
226
- // Proxy para $room no servidor
227
- export interface ServerRoomProxy<TState = any, TEvents extends Record<string, any> = Record<string, any>> {
228
- (roomId: string): ServerRoomHandle<TState, TEvents>
229
- readonly id: string | undefined
230
- readonly state: TState
231
- join: (initialState?: TState) => void
232
- leave: () => void
233
- emit: <K extends keyof TEvents>(event: K, data: TEvents[K]) => number
234
- on: <K extends keyof TEvents>(event: K, handler: (data: TEvents[K]) => void) => () => void
235
- setState: (updates: Partial<TState>) => void
236
- }
237
-
238
- export abstract class LiveComponent<TState = ComponentState, TPrivate extends Record<string, any> = Record<string, any>> {
239
- /** Component name for registry lookup - must be defined in subclasses */
240
- static componentName: string
241
- /** Default state - must be defined in subclasses */
242
- static defaultState: any
243
-
244
- /**
245
- * Per-component logging control. Silent by default.
246
- *
247
- * @example
248
- * // Enable all log categories
249
- * static logging = true
250
- *
251
- * // Enable specific categories only
252
- * static logging = ['lifecycle', 'messages'] as const
253
- *
254
- * // Disabled (default — omit or set false)
255
- * static logging = false
256
- *
257
- * Categories: 'lifecycle' | 'messages' | 'state' | 'performance' | 'rooms' | 'websocket'
258
- */
259
- static logging?: boolean | readonly ('lifecycle' | 'messages' | 'state' | 'performance' | 'rooms' | 'websocket')[]
260
-
261
- /**
262
- * Configuração de autenticação do componente.
263
- * Define se auth é obrigatória e quais roles/permissions são necessárias.
264
- *
265
- * @example
266
- * static auth: LiveComponentAuth = {
267
- * required: true,
268
- * roles: ['admin', 'moderator'],
269
- * permissions: ['chat.read'],
270
- * }
271
- */
272
- static auth?: LiveComponentAuth
273
-
274
- /**
275
- * Configuração de autenticação por action.
276
- * Permite controle granular de permissões por método.
277
- *
278
- * @example
279
- * static actionAuth: LiveActionAuthMap = {
280
- * deleteMessage: { permissions: ['chat.admin'] },
281
- * sendMessage: { permissions: ['chat.write'] },
282
- * }
283
- */
284
- static actionAuth?: LiveActionAuthMap
285
-
286
- public readonly id: string
287
- private _state: TState
288
- public state: TState // Proxy wrapper
289
- protected ws: FluxStackWebSocket
290
- public room?: string
291
- public userId?: string
292
- public broadcastToRoom: (message: BroadcastMessage) => void = () => {} // Will be injected by registry
293
-
294
- // 🔒 Server-only private state (NEVER sent to client)
295
- private _privateState: TPrivate = {} as TPrivate
296
-
297
- // Auth context (injected by registry during mount)
298
- private _authContext: LiveAuthContext = ANONYMOUS_CONTEXT
299
-
300
- // Room event subscriptions (cleaned up on destroy)
301
- private roomEventUnsubscribers: (() => void)[] = []
302
- private joinedRooms: Set<string> = new Set()
303
-
304
- // Room type for typed events (override in subclass)
305
- protected roomType: string = 'default'
306
-
307
- // Cached room handles
308
- private roomHandles: Map<string, ServerRoomHandle> = new Map()
309
-
310
- constructor(initialState: Partial<TState>, ws: FluxStackWebSocket, options?: { room?: string; userId?: string }) {
311
- this.id = this.generateId()
312
- // Merge defaultState with initialState - subclass defaultState takes precedence for missing fields
313
- const ctor = this.constructor as typeof LiveComponent
314
- this._state = { ...ctor.defaultState, ...initialState } as TState
315
-
316
- // Create reactive proxy that auto-syncs on mutation
317
- this.state = this.createStateProxy(this._state)
318
-
319
- this.ws = ws
320
- this.room = options?.room
321
- this.userId = options?.userId
322
-
323
- // Auto-join default room if specified
324
- if (this.room) {
325
- this.joinedRooms.add(this.room)
326
- liveRoomManager.joinRoom(this.id, this.room, this.ws)
327
- }
328
-
329
- // 🔥 Create direct property accessors (this.count instead of this.state.count)
330
- this.createDirectStateAccessors()
331
- }
332
-
333
- // Create getters/setters for each state property directly on `this`
334
- private createDirectStateAccessors() {
335
- // Properties that should NOT become state accessors
336
- const forbidden = new Set([
337
- // Instance properties
338
- ...Object.keys(this),
339
- // Prototype methods
340
- ...Object.getOwnPropertyNames(Object.getPrototypeOf(this)),
341
- // Known internal properties
342
- 'state', '_state', 'ws', 'id', 'room', 'userId', 'broadcastToRoom',
343
- '$private', '_privateState',
344
- '$room', '$rooms', 'roomType', 'roomHandles', 'joinedRooms', 'roomEventUnsubscribers'
345
- ])
346
-
347
- // Create accessor for each state key
348
- for (const key of Object.keys(this._state as object)) {
349
- if (!forbidden.has(key)) {
350
- Object.defineProperty(this, key, {
351
- get: () => (this._state as any)[key],
352
- set: (value) => { (this.state as any)[key] = value }, // Uses proxy for auto-sync
353
- enumerable: true,
354
- configurable: true
355
- })
356
- }
357
- }
358
- }
359
-
360
- // Create a Proxy that auto-emits STATE_DELTA on any mutation
361
- private createStateProxy(state: TState): TState {
362
- const self = this
363
- return new Proxy(state as object, {
364
- set(target, prop, value) {
365
- const oldValue = (target as any)[prop]
366
- if (oldValue !== value) {
367
- (target as any)[prop] = value
368
- // Delta sync - send only the changed property
369
- self.emit('STATE_DELTA', { delta: { [prop]: value } })
370
- // Debug: track proxy mutation
371
- _liveDebugger?.trackStateChange(
372
- self.id,
373
- { [prop]: value } as Record<string, unknown>,
374
- target as Record<string, unknown>,
375
- 'proxy'
376
- )
377
- }
378
- return true
379
- },
380
- get(target, prop) {
381
- return (target as any)[prop]
382
- }
383
- }) as TState
384
- }
385
-
386
- // ========================================
387
- // 🔒 $private - Server-Only State
388
- // ========================================
389
-
390
- /**
391
- * Server-only state that is NEVER synchronized with the client.
392
- * Use this for sensitive data like tokens, API keys, internal IDs, etc.
393
- *
394
- * Unlike `this.state`, mutations to `$private` do NOT trigger
395
- * STATE_DELTA or STATE_UPDATE messages.
396
- *
397
- * ⚠️ Private state is lost on rehydration (since it's never sent to client).
398
- * Re-populate it in your action handlers as needed.
399
- *
400
- * @example
401
- * async connect(payload: { token: string }) {
402
- * this.$private.token = payload.token
403
- * this.$private.apiKey = await getKey()
404
- *
405
- * // Only UI-relevant data goes to state (synced with client)
406
- * this.state.messages = await fetchMessages(this.$private.token)
407
- * }
408
- */
409
- public get $private(): TPrivate {
410
- return this._privateState
411
- }
412
-
413
- // ========================================
414
- // 🔥 $room - Sistema de Salas Unificado
415
- // ========================================
416
-
417
- /**
418
- * Acessa uma sala específica ou a sala padrão
419
- * @example
420
- * // Sala padrão
421
- * this.$room.emit('typing', { user: 'João' })
422
- * this.$room.on('message:new', handler)
423
- *
424
- * // Outra sala
425
- * this.$room('sala-vip').join()
426
- * this.$room('sala-vip').emit('typing', { user: 'João' })
427
- */
428
- public get $room(): ServerRoomProxy {
429
- const self = this
430
-
431
- const createHandle = (roomId: string): ServerRoomHandle => {
432
- // Retornar handle cacheado
433
- if (this.roomHandles.has(roomId)) {
434
- return this.roomHandles.get(roomId)!
435
- }
436
-
437
- const handle: ServerRoomHandle = {
438
- get id() { return roomId },
439
- get state() { return liveRoomManager.getRoomState(roomId) },
440
-
441
- join: (initialState?: any) => {
442
- if (self.joinedRooms.has(roomId)) return
443
- self.joinedRooms.add(roomId)
444
- liveRoomManager.joinRoom(self.id, roomId, self.ws, initialState)
445
- },
446
-
447
- leave: () => {
448
- if (!self.joinedRooms.has(roomId)) return
449
- self.joinedRooms.delete(roomId)
450
- liveRoomManager.leaveRoom(self.id, roomId)
451
- },
452
-
453
- emit: (event: string, data: any): number => {
454
- return liveRoomManager.emitToRoom(roomId, event, data, self.id)
455
- },
456
-
457
- on: (event: string, handler: (data: any) => void): (() => void) => {
458
- // Usar 'room' como tipo genérico e roomId como identificador
459
- // Isso permite que emitToRoom encontre os handlers corretamente
460
- const unsubscribe = roomEvents.on(
461
- 'room', // Tipo genérico para todas as salas
462
- roomId,
463
- event,
464
- self.id,
465
- handler
466
- )
467
- self.roomEventUnsubscribers.push(unsubscribe)
468
- return unsubscribe
469
- },
470
-
471
- setState: (updates: any) => {
472
- liveRoomManager.setRoomState(roomId, updates, self.id)
473
- }
474
- }
475
-
476
- this.roomHandles.set(roomId, handle)
477
- return handle
478
- }
479
-
480
- // Criar proxy que funciona como função e objeto
481
- const proxyFn = ((roomId: string) => createHandle(roomId)) as ServerRoomProxy
482
-
483
- const defaultHandle = this.room ? createHandle(this.room) : null
484
-
485
- Object.defineProperties(proxyFn, {
486
- id: { get: () => self.room },
487
- state: { get: () => defaultHandle?.state ?? {} },
488
- join: {
489
- value: (initialState?: any) => {
490
- if (!defaultHandle) throw new Error('No default room set')
491
- defaultHandle.join(initialState)
492
- }
493
- },
494
- leave: {
495
- value: () => {
496
- if (!defaultHandle) throw new Error('No default room set')
497
- defaultHandle.leave()
498
- }
499
- },
500
- emit: {
501
- value: (event: string, data: any) => {
502
- if (!defaultHandle) throw new Error('No default room set')
503
- return defaultHandle.emit(event, data)
504
- }
505
- },
506
- on: {
507
- value: (event: string, handler: (data: any) => void) => {
508
- if (!defaultHandle) throw new Error('No default room set')
509
- return defaultHandle.on(event, handler)
510
- }
511
- },
512
- setState: {
513
- value: (updates: any) => {
514
- if (!defaultHandle) throw new Error('No default room set')
515
- defaultHandle.setState(updates)
516
- }
517
- }
518
- })
519
-
520
- return proxyFn
521
- }
522
-
523
- /**
524
- * Lista de IDs das salas que este componente está participando
525
- */
526
- public get $rooms(): string[] {
527
- return Array.from(this.joinedRooms)
528
- }
529
-
530
- // ========================================
531
- // 🔒 $auth - Contexto de Autenticação
532
- // ========================================
533
-
534
- /**
535
- * Acessa o contexto de autenticação do usuário atual.
536
- * Disponível após o mount do componente.
537
- *
538
- * @example
539
- * async sendMessage(payload: { text: string }) {
540
- * if (!this.$auth.authenticated) {
541
- * throw new Error('Login required')
542
- * }
543
- *
544
- * const userId = this.$auth.user!.id
545
- * const isAdmin = this.$auth.hasRole('admin')
546
- * const canDelete = this.$auth.hasPermission('chat.admin')
547
- * }
548
- */
549
- public get $auth(): LiveAuthContext {
550
- return this._authContext
551
- }
552
-
553
- /**
554
- * Injeta o contexto de autenticação no componente.
555
- * Chamado internamente pelo ComponentRegistry durante o mount.
556
- * @internal
557
- */
558
- public setAuthContext(context: LiveAuthContext): void {
559
- this._authContext = context
560
- // Atualiza userId se disponível no auth context
561
- if (context.authenticated && context.user?.id && !this.userId) {
562
- this.userId = context.user.id
563
- }
564
- }
565
-
566
- // State management (batch update - single emit with delta)
567
- public setState(updates: Partial<TState> | ((prev: TState) => Partial<TState>)) {
568
- const newUpdates = typeof updates === 'function' ? updates(this._state) : updates
569
- Object.assign(this._state as object, newUpdates)
570
- // Delta sync - send only the changed properties
571
- this.emit('STATE_DELTA', { delta: newUpdates })
572
- // Debug: track state change
573
- _liveDebugger?.trackStateChange(
574
- this.id,
575
- newUpdates as Record<string, unknown>,
576
- this._state as Record<string, unknown>,
577
- 'setState'
578
- )
579
- }
580
-
581
- // Generic setValue action - set any state key with type safety
582
- public async setValue<K extends keyof TState>(payload: { key: K; value: TState[K] }): Promise<{ success: true; key: K; value: TState[K] }> {
583
- const { key, value } = payload
584
- const update = { [key]: value } as unknown as Partial<TState>
585
- this.setState(update)
586
- return { success: true, key, value }
587
- }
588
-
589
- /**
590
- * 🔒 REQUIRED: List of methods that are explicitly callable from the client.
591
- * ONLY these methods can be called via CALL_ACTION.
592
- * Components without publicActions will deny ALL remote actions (secure by default).
593
- *
594
- * @example
595
- * static publicActions = ['sendMessage', 'deleteMessage', 'join'] as const
596
- */
597
- static publicActions?: readonly string[]
598
-
599
- // Internal methods that must NEVER be callable from the client
600
- private static readonly BLOCKED_ACTIONS: ReadonlySet<string> = new Set([
601
- // Lifecycle & internal
602
- 'constructor', 'destroy', 'executeAction', 'getSerializableState',
603
- // State management internals
604
- 'setState', 'emit', 'broadcast', 'broadcastToRoom',
605
- 'createStateProxy', 'createDirectStateAccessors', 'generateId',
606
- // Auth internals
607
- 'setAuthContext', '$auth',
608
- // Private state internals
609
- '$private', '_privateState',
610
- // Room internals
611
- '$room', '$rooms', 'subscribeToRoom', 'unsubscribeFromRoom',
612
- 'emitRoomEvent', 'onRoomEvent', 'emitRoomEventWithState',
613
- ])
614
-
615
- // Execute action safely with security validation
616
- public async executeAction(action: string, payload: any): Promise<any> {
617
- const actionStart = Date.now()
618
- try {
619
- // 🔒 Security: Block internal/protected methods from being called remotely
620
- if ((LiveComponent.BLOCKED_ACTIONS as Set<string>).has(action)) {
621
- throw new Error(`Action '${action}' is not callable`)
622
- }
623
-
624
- // 🔒 Security: Block private methods (prefixed with _ or #)
625
- if (action.startsWith('_') || action.startsWith('#')) {
626
- throw new Error(`Action '${action}' is not callable`)
627
- }
628
-
629
- // 🔒 Security: publicActions whitelist is MANDATORY
630
- // Components without publicActions deny ALL remote actions (secure by default)
631
- const componentClass = this.constructor as typeof LiveComponent
632
- const publicActions = componentClass.publicActions
633
- if (!publicActions) {
634
- console.warn(`🔒 [SECURITY] Component '${componentClass.componentName || componentClass.name}' has no publicActions defined. All remote actions are blocked. Define static publicActions to allow specific actions.`)
635
- throw new Error(`Action '${action}' is not callable - component has no publicActions defined`)
636
- }
637
- if (!publicActions.includes(action)) {
638
- throw new Error(`Action '${action}' is not callable`)
639
- }
640
-
641
- // Check if method exists on the instance
642
- const method = (this as any)[action]
643
- if (typeof method !== 'function') {
644
- throw new Error(`Action '${action}' not found on component`)
645
- }
646
-
647
- // 🔒 Security: Block inherited Object.prototype methods
648
- if (Object.prototype.hasOwnProperty.call(Object.prototype, action)) {
649
- throw new Error(`Action '${action}' is not callable`)
650
- }
651
-
652
- // Debug: track action call
653
- _liveDebugger?.trackActionCall(this.id, action, payload)
654
-
655
- // Execute method
656
- const result = await method.call(this, payload)
657
-
658
- // Debug: track action result
659
- _liveDebugger?.trackActionResult(this.id, action, result, Date.now() - actionStart)
660
-
661
- return result
662
- } catch (error: any) {
663
- // Debug: track action error
664
- _liveDebugger?.trackActionError(this.id, action, error.message, Date.now() - actionStart)
665
-
666
- this.emit('ERROR', {
667
- action,
668
- error: error.message
669
- })
670
- throw error
671
- }
672
- }
673
-
674
- // Send message to client
675
- protected emit(type: string, payload: any) {
676
- const message: LiveMessage = {
677
- type: type as any,
678
- componentId: this.id,
679
- payload,
680
- timestamp: Date.now(),
681
- userId: this.userId,
682
- room: this.room
683
- }
684
-
685
- if (this.ws && this.ws.send) {
686
- this.ws.send(JSON.stringify(message))
687
- }
688
- }
689
-
690
- // Broadcast to all clients in room (via WebSocket)
691
- protected broadcast(type: string, payload: any, excludeCurrentUser = false) {
692
- if (!this.room) {
693
- liveWarn('rooms', this.id, `⚠️ [${this.id}] Cannot broadcast '${type}' - no room set`)
694
- return
695
- }
696
-
697
- const message: BroadcastMessage = {
698
- type,
699
- payload,
700
- room: this.room,
701
- excludeUser: excludeCurrentUser ? this.userId : undefined
702
- }
703
-
704
- liveLog('rooms', this.id, `📤 [${this.id}] Broadcasting '${type}' to room '${this.room}'`)
705
-
706
- // This will be handled by the registry
707
- this.broadcastToRoom(message)
708
- }
709
-
710
- // ========================================
711
- // 🔥 Room Events - Internal Server Events
712
- // ========================================
713
-
714
- /**
715
- * Emite um evento para todos os componentes da sala (server-side)
716
- * Cada componente inscrito pode reagir e atualizar seu próprio cliente
717
- *
718
- * @param event - Nome do evento
719
- * @param data - Dados do evento
720
- * @param notifySelf - Se true, este componente também recebe (default: false)
721
- */
722
- protected emitRoomEvent(event: string, data: any, notifySelf = false): number {
723
- if (!this.room) {
724
- liveWarn('rooms', this.id, `⚠️ [${this.id}] Cannot emit room event '${event}' - no room set`)
725
- return 0
726
- }
727
-
728
- const excludeId = notifySelf ? undefined : this.id
729
- const notified = roomEvents.emit(this.roomType, this.room, event, data, excludeId)
730
-
731
- liveLog('rooms', this.id, `📡 [${this.id}] Room event '${event}' → ${notified} components`)
732
-
733
- // Debug: track room emit
734
- _liveDebugger?.trackRoomEmit(this.id, this.room, event, data)
735
-
736
- return notified
737
- }
738
-
739
- /**
740
- * Inscreve este componente em um evento da sala
741
- * Handler é chamado quando outro componente emite o evento
742
- *
743
- * @param event - Nome do evento para escutar
744
- * @param handler - Função chamada quando evento é recebido
745
- */
746
- protected onRoomEvent<T = any>(event: string, handler: (data: T) => void): void {
747
- if (!this.room) {
748
- liveWarn('rooms', this.id, `⚠️ [${this.id}] Cannot subscribe to room event '${event}' - no room set`)
749
- return
750
- }
751
-
752
- const unsubscribe = roomEvents.on(
753
- this.roomType,
754
- this.room,
755
- event,
756
- this.id,
757
- handler
758
- )
759
-
760
- // Guardar para cleanup no destroy
761
- this.roomEventUnsubscribers.push(unsubscribe)
762
-
763
- liveLog('rooms', this.id, `👂 [${this.id}] Subscribed to room event '${event}'`)
764
- }
765
-
766
- /**
767
- * Helper: Emite evento E atualiza estado local + envia pro cliente
768
- * Útil para o componente que origina a ação
769
- *
770
- * @param event - Nome do evento
771
- * @param data - Dados do evento
772
- * @param stateUpdates - Atualizações de estado para aplicar localmente
773
- */
774
- protected emitRoomEventWithState(
775
- event: string,
776
- data: any,
777
- stateUpdates: Partial<TState>
778
- ): number {
779
- // 1. Atualiza estado local (envia pro cliente deste componente)
780
- this.setState(stateUpdates)
781
-
782
- // 2. Emite evento para outros componentes da sala
783
- return this.emitRoomEvent(event, data, false)
784
- }
785
-
786
- // Subscribe to room for multi-user features
787
- protected async subscribeToRoom(roomId: string) {
788
- this.room = roomId
789
- // Registry will handle the actual subscription
790
- }
791
-
792
- // Unsubscribe from room
793
- protected async unsubscribeFromRoom() {
794
- this.room = undefined
795
- // Registry will handle the actual unsubscription
796
- }
797
-
798
- // Generate unique ID using cryptographically secure randomness
799
- private generateId(): string {
800
- return `live-${crypto.randomUUID()}`
801
- }
802
-
803
- // Cleanup when component is destroyed
804
- public destroy() {
805
- // Limpa todas as inscrições de room events
806
- for (const unsubscribe of this.roomEventUnsubscribers) {
807
- unsubscribe()
808
- }
809
- this.roomEventUnsubscribers = []
810
-
811
- // Sai de todas as salas
812
- for (const roomId of this.joinedRooms) {
813
- liveRoomManager.leaveRoom(this.id, roomId)
814
- }
815
- this.joinedRooms.clear()
816
- this.roomHandles.clear()
817
- this._privateState = {} as TPrivate
818
-
819
- this.unsubscribeFromRoom()
820
- // Override in subclasses for custom cleanup
821
- }
822
-
823
- // Get serializable state for client
824
- public getSerializableState(): TState {
825
- return this.state
826
- }
827
- }
828
-
829
- // Utility types for better TypeScript experience
830
- export type ComponentActions<T> = {
831
- [K in keyof T]: T[K] extends (...args: any[]) => any ? T[K] : never
832
- }
833
-
834
- export type ComponentProps<T extends LiveComponent> = T extends LiveComponent<infer TState> ? TState : never
835
-
836
- export type ActionParameters<T, K extends keyof T> = T[K] extends (...args: infer P) => any ? P : never
837
-
838
- export type ActionReturnType<T, K extends keyof T> = T[K] extends (...args: any[]) => infer R ? R : never
839
-
840
- // 🔥 Type Inference System for Live Components
841
- // Similar to Eden Treaty - automatic type inference for actions
842
-
843
- /**
844
- * Extract all public action methods from a LiveComponent class
845
- * Excludes constructor, destroy, lifecycle methods, and inherited methods
846
- */
847
- export type ExtractActions<T extends LiveComponent<any>> = {
848
- [K in keyof T as K extends string
849
- ? T[K] extends (payload?: any) => Promise<any>
850
- ? K extends 'executeAction' | 'destroy' | 'getSerializableState' | 'setState'
851
- ? never
852
- : K
853
- : never
854
- : never]: T[K]
855
- }
856
-
857
- /**
858
- * Get all action names from a component
859
- */
860
- export type ActionNames<T extends LiveComponent<any>> = keyof ExtractActions<T>
861
-
862
- /**
863
- * Get the payload type for a specific action
864
- * Extracts the first parameter type from the action method
865
- */
866
- export type ActionPayload<
867
- T extends LiveComponent<any>,
868
- K extends ActionNames<T>
869
- > = ExtractActions<T>[K] extends (payload: infer P) => any
870
- ? P
871
- : ExtractActions<T>[K] extends () => any
872
- ? undefined
873
- : never
874
-
875
- /**
876
- * Get the return type for a specific action (unwrapped from Promise)
877
- */
878
- export type ActionReturn<
879
- T extends LiveComponent<any>,
880
- K extends ActionNames<T>
881
- > = ExtractActions<T>[K] extends (...args: any[]) => Promise<infer R>
882
- ? R
883
- : ExtractActions<T>[K] extends (...args: any[]) => infer R
884
- ? R
885
- : never
886
-
887
- /**
888
- * Get the state type from a LiveComponent class
889
- */
890
- export type InferComponentState<T extends LiveComponent<any>> = T extends LiveComponent<infer S> ? S : never
891
-
892
- /**
893
- * Get the private state type from a LiveComponent class
894
- */
895
- export type InferPrivateState<T extends LiveComponent<any, any>> = T extends LiveComponent<any, infer P> ? P : never
896
-
897
- /**
898
- * Type-safe call signature for a component
899
- * Provides autocomplete for action names and validates payload types
900
- */
901
- export type TypedCall<T extends LiveComponent<any>> = <K extends ActionNames<T>>(
902
- action: K,
903
- ...args: ActionPayload<T, K> extends undefined
904
- ? []
905
- : [payload: ActionPayload<T, K>]
906
- ) => Promise<void>
907
-
908
- /**
909
- * Type-safe callAndWait signature for a component
910
- * Provides autocomplete and returns the correct type
911
- */
912
- export type TypedCallAndWait<T extends LiveComponent<any>> = <K extends ActionNames<T>>(
913
- action: K,
914
- ...args: ActionPayload<T, K> extends undefined
915
- ? [payload?: undefined, timeout?: number]
916
- : [payload: ActionPayload<T, K>, timeout?: number]
917
- ) => Promise<ActionReturn<T, K>>
918
-
919
- /**
920
- * Type-safe setValue signature for a component
921
- * Convenience helper for setting individual state values
922
- */
923
- export type TypedSetValue<T extends LiveComponent<any>> = <K extends keyof InferComponentState<T>>(
924
- key: K,
925
- value: InferComponentState<T>[K]
926
- ) => Promise<void>
927
-
928
- /**
929
- * Return type for useTypedLiveComponent hook
930
- * Provides full type inference for state and actions
931
- */
932
- export interface UseTypedLiveComponentReturn<T extends LiveComponent<any>> {
933
- // Server-driven state (read-only from frontend perspective)
934
- state: InferComponentState<T>
935
-
936
- // Status information
937
- loading: boolean
938
- error: string | null
939
- connected: boolean
940
- componentId: string | null
941
-
942
- // Connection status with all possible states
943
- status: 'synced' | 'disconnected' | 'connecting' | 'reconnecting' | 'loading' | 'mounting' | 'error'
944
-
945
- // Type-safe actions
946
- call: TypedCall<T>
947
- callAndWait: TypedCallAndWait<T>
948
-
949
- // Convenience helper for setting individual state values
950
- setValue: TypedSetValue<T>
951
-
952
- // Lifecycle
953
- mount: () => Promise<void>
954
- unmount: () => Promise<void>
955
-
956
- // Helper for temporary input state
957
- useControlledField: <K extends keyof InferComponentState<T>>(field: K, action?: string) => {
958
- value: InferComponentState<T>[K]
959
- setValue: (value: InferComponentState<T>[K]) => void
960
- commit: (value?: InferComponentState<T>[K]) => Promise<void>
961
- isDirty: boolean
962
- }
963
- }
964
-
965
- // File Upload Types for Chunked WebSocket Upload
966
- export interface FileChunkData {
967
- uploadId: string
968
- filename: string
969
- fileType: string
970
- fileSize: number
971
- chunkIndex: number
972
- totalChunks: number
973
- chunkSize: number
974
- data: string // Base64 encoded chunk data
975
- hash?: string // Optional chunk hash for verification
976
- }
977
-
978
- export interface FileUploadStartMessage {
979
- type: 'FILE_UPLOAD_START'
980
- componentId: string
981
- uploadId: string
982
- filename: string
983
- fileType: string
984
- fileSize: number
985
- chunkSize?: number // Optional, defaults to 64KB
986
- requestId?: string
987
- }
988
-
989
- export interface FileUploadChunkMessage {
990
- type: 'FILE_UPLOAD_CHUNK'
991
- componentId: string
992
- uploadId: string
993
- chunkIndex: number
994
- totalChunks: number
995
- data: string | Buffer // Base64 string (JSON) or Buffer (binary protocol)
996
- hash?: string
997
- requestId?: string
998
- }
999
-
1000
- // Binary protocol header for chunk uploads
1001
- export interface BinaryChunkHeader {
1002
- type: 'FILE_UPLOAD_CHUNK'
1003
- componentId: string
1004
- uploadId: string
1005
- chunkIndex: number
1006
- totalChunks: number
1007
- requestId?: string
1008
- }
1009
-
1010
- export interface FileUploadCompleteMessage {
1011
- type: 'FILE_UPLOAD_COMPLETE'
1012
- componentId: string
1013
- uploadId: string
1014
- requestId?: string
1015
- }
1016
-
1017
- export interface FileUploadProgressResponse {
1018
- type: 'FILE_UPLOAD_PROGRESS'
1019
- componentId: string
1020
- uploadId: string
1021
- chunkIndex: number
1022
- totalChunks: number
1023
- bytesUploaded: number
1024
- totalBytes: number
1025
- progress: number // 0-100
1026
- requestId?: string
1027
- timestamp: number
1028
- }
1029
-
1030
- export interface FileUploadCompleteResponse {
1031
- type: 'FILE_UPLOAD_COMPLETE'
1032
- componentId: string
1033
- uploadId: string
1034
- success: boolean
1035
- filename?: string
1036
- fileUrl?: string
1037
- error?: string
1038
- requestId?: string
1039
- timestamp: number
1040
- }
1041
-
1042
- // File Upload Manager for handling uploads
1043
- export interface ActiveUpload {
1044
- uploadId: string
1045
- componentId: string
1046
- filename: string
1047
- fileType: string
1048
- fileSize: number
1049
- totalChunks: number
1050
- receivedChunks: Map<number, string | Buffer> // Base64 string or raw Buffer (binary protocol)
1051
- bytesReceived: number // Track actual bytes received for adaptive chunking
1052
- startTime: number
1053
- lastChunkTime: number
1054
- tempFilePath?: string
1055
- }