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
package/core/types/types.ts
CHANGED
|
@@ -1,186 +1,94 @@
|
|
|
1
|
-
//
|
|
1
|
+
// FluxStack Live Components - Shared Types
|
|
2
|
+
// Re-exports from @fluxstack/live for backward compatibility
|
|
2
3
|
|
|
3
|
-
|
|
4
|
-
|
|
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
|
-
//
|
|
9
|
-
//
|
|
10
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
30
|
-
//
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
|
|
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
|
|
70
|
-
|
|
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
|
|
93
|
-
[
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
}
|