create-fluxstack 1.14.0 → 1.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LLMD/INDEX.md +4 -3
- package/LLMD/resources/live-binary-delta.md +507 -0
- package/LLMD/resources/live-components.md +208 -12
- package/LLMD/resources/live-rooms.md +731 -333
- package/app/client/.live-stubs/LiveAdminPanel.js +5 -0
- package/app/client/.live-stubs/LiveCounter.js +9 -0
- package/app/client/.live-stubs/LiveForm.js +11 -0
- package/app/client/.live-stubs/LiveLocalCounter.js +8 -0
- package/app/client/.live-stubs/LivePingPong.js +10 -0
- package/app/client/.live-stubs/LiveRoomChat.js +11 -0
- package/app/client/.live-stubs/LiveSharedCounter.js +10 -0
- package/app/client/.live-stubs/LiveUpload.js +15 -0
- package/app/client/src/App.tsx +19 -7
- package/app/client/src/components/AppLayout.tsx +18 -10
- package/app/client/src/live/PingPongDemo.tsx +199 -0
- package/app/client/src/live/RoomChatDemo.tsx +187 -22
- package/app/client/src/live/SharedCounterDemo.tsx +142 -0
- package/app/server/auth/DevAuthProvider.ts +2 -2
- package/app/server/auth/JWTAuthProvider.example.ts +2 -2
- package/app/server/index.ts +2 -2
- package/app/server/live/LiveAdminPanel.ts +1 -1
- package/app/server/live/LivePingPong.ts +61 -0
- package/app/server/live/LiveProtectedChat.ts +1 -1
- package/app/server/live/LiveRoomChat.ts +106 -38
- package/app/server/live/LiveSharedCounter.ts +73 -0
- package/app/server/live/rooms/ChatRoom.ts +68 -0
- package/app/server/live/rooms/CounterRoom.ts +51 -0
- package/app/server/live/rooms/DirectoryRoom.ts +42 -0
- package/app/server/live/rooms/PingRoom.ts +40 -0
- package/app/server/routes/room.routes.ts +1 -2
- package/core/build/live-components-generator.ts +11 -2
- package/core/build/vite-plugins.ts +28 -0
- package/core/client/hooks/useLiveUpload.ts +3 -4
- package/core/client/index.ts +25 -35
- package/core/framework/server.ts +1 -1
- package/core/server/index.ts +1 -2
- package/core/server/live/auto-generated-components.ts +5 -8
- package/core/server/live/index.ts +90 -21
- package/core/server/live/websocket-plugin.ts +54 -1079
- package/core/types/types.ts +76 -1025
- package/core/utils/version.ts +1 -1
- package/create-fluxstack.ts +1 -1
- package/package.json +100 -95
- package/plugins/crypto-auth/index.ts +1 -1
- package/plugins/crypto-auth/server/CryptoAuthLiveProvider.ts +2 -2
- package/tsconfig.json +4 -1
- package/vite.config.ts +40 -12
- package/app/client/src/live/ChatDemo.tsx +0 -107
- package/app/client/src/live/LiveDebuggerPanel.tsx +0 -779
- package/app/server/live/LiveChat.ts +0 -78
- package/core/client/LiveComponentsProvider.tsx +0 -531
- package/core/client/components/Live.tsx +0 -111
- package/core/client/components/LiveDebugger.tsx +0 -1324
- package/core/client/hooks/AdaptiveChunkSizer.ts +0 -215
- package/core/client/hooks/state-validator.ts +0 -130
- package/core/client/hooks/useChunkedUpload.ts +0 -359
- package/core/client/hooks/useLiveChunkedUpload.ts +0 -87
- package/core/client/hooks/useLiveComponent.ts +0 -853
- package/core/client/hooks/useLiveDebugger.ts +0 -392
- package/core/client/hooks/useRoom.ts +0 -409
- package/core/client/hooks/useRoomProxy.ts +0 -382
- package/core/server/live/ComponentRegistry.ts +0 -1128
- package/core/server/live/FileUploadManager.ts +0 -446
- package/core/server/live/LiveComponentPerformanceMonitor.ts +0 -931
- package/core/server/live/LiveDebugger.ts +0 -462
- package/core/server/live/LiveLogger.ts +0 -144
- package/core/server/live/LiveRoomManager.ts +0 -278
- package/core/server/live/RoomEventBus.ts +0 -234
- package/core/server/live/RoomStateManager.ts +0 -172
- package/core/server/live/SingleConnectionManager.ts +0 -0
- package/core/server/live/StateSignature.ts +0 -705
- package/core/server/live/WebSocketConnectionManager.ts +0 -710
- package/core/server/live/auth/LiveAuthContext.ts +0 -71
- package/core/server/live/auth/LiveAuthManager.ts +0 -304
- package/core/server/live/auth/index.ts +0 -19
- package/core/server/live/auth/types.ts +0 -179
|
@@ -1,278 +0,0 @@
|
|
|
1
|
-
// 🔥 FluxStack Live Room Manager - Gerencia salas para LiveComponents
|
|
2
|
-
|
|
3
|
-
import { roomEvents } from './RoomEventBus'
|
|
4
|
-
import type { FluxStackWebSocket } from '@core/types/types'
|
|
5
|
-
import { liveLog } from './LiveLogger'
|
|
6
|
-
|
|
7
|
-
export interface RoomMessage {
|
|
8
|
-
type: 'ROOM_JOIN' | 'ROOM_LEAVE' | 'ROOM_EMIT' | 'ROOM_STATE_SET' | 'ROOM_STATE_GET'
|
|
9
|
-
componentId: string
|
|
10
|
-
roomId: string
|
|
11
|
-
event?: string
|
|
12
|
-
data?: any
|
|
13
|
-
requestId?: string
|
|
14
|
-
timestamp: number
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
interface RoomMember {
|
|
18
|
-
componentId: string
|
|
19
|
-
ws: FluxStackWebSocket
|
|
20
|
-
joinedAt: number
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
interface Room<TState = any> {
|
|
24
|
-
id: string
|
|
25
|
-
state: TState
|
|
26
|
-
members: Map<string, RoomMember>
|
|
27
|
-
createdAt: number
|
|
28
|
-
lastActivity: number
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
class LiveRoomManager {
|
|
32
|
-
private rooms = new Map<string, Room>()
|
|
33
|
-
private componentRooms = new Map<string, Set<string>>() // componentId -> roomIds
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Componente entra em uma sala
|
|
37
|
-
*/
|
|
38
|
-
joinRoom<TState = any>(componentId: string, roomId: string, ws: FluxStackWebSocket, initialState?: TState): { state: TState } {
|
|
39
|
-
// 🔒 Validate room name format
|
|
40
|
-
if (!roomId || !/^[a-zA-Z0-9_:.-]{1,64}$/.test(roomId)) {
|
|
41
|
-
throw new Error('Invalid room name. Must be 1-64 alphanumeric characters, hyphens, underscores, dots, or colons.')
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
// Criar sala se não existir
|
|
45
|
-
if (!this.rooms.has(roomId)) {
|
|
46
|
-
this.rooms.set(roomId, {
|
|
47
|
-
id: roomId,
|
|
48
|
-
state: initialState || {},
|
|
49
|
-
members: new Map(),
|
|
50
|
-
createdAt: Date.now(),
|
|
51
|
-
lastActivity: Date.now()
|
|
52
|
-
})
|
|
53
|
-
liveLog('rooms', componentId, `🏠 Room '${roomId}' created`)
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
const room = this.rooms.get(roomId)!
|
|
57
|
-
|
|
58
|
-
// Adicionar membro
|
|
59
|
-
room.members.set(componentId, {
|
|
60
|
-
componentId,
|
|
61
|
-
ws,
|
|
62
|
-
joinedAt: Date.now()
|
|
63
|
-
})
|
|
64
|
-
room.lastActivity = Date.now()
|
|
65
|
-
|
|
66
|
-
// Rastrear salas do componente
|
|
67
|
-
if (!this.componentRooms.has(componentId)) {
|
|
68
|
-
this.componentRooms.set(componentId, new Set())
|
|
69
|
-
}
|
|
70
|
-
this.componentRooms.get(componentId)!.add(roomId)
|
|
71
|
-
|
|
72
|
-
liveLog('rooms', componentId, `👋 Component '${componentId}' joined room '${roomId}' (${room.members.size} members)`)
|
|
73
|
-
|
|
74
|
-
// Notificar outros membros
|
|
75
|
-
this.broadcastToRoom(roomId, {
|
|
76
|
-
type: 'ROOM_SYSTEM',
|
|
77
|
-
componentId,
|
|
78
|
-
roomId,
|
|
79
|
-
event: '$sub:join',
|
|
80
|
-
data: {
|
|
81
|
-
subscriberId: componentId,
|
|
82
|
-
count: room.members.size
|
|
83
|
-
},
|
|
84
|
-
timestamp: Date.now()
|
|
85
|
-
}, componentId)
|
|
86
|
-
|
|
87
|
-
return { state: room.state }
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* Componente sai de uma sala
|
|
92
|
-
*/
|
|
93
|
-
leaveRoom(componentId: string, roomId: string): void {
|
|
94
|
-
const room = this.rooms.get(roomId)
|
|
95
|
-
if (!room) return
|
|
96
|
-
|
|
97
|
-
// Remover membro
|
|
98
|
-
room.members.delete(componentId)
|
|
99
|
-
room.lastActivity = Date.now()
|
|
100
|
-
|
|
101
|
-
// Remover do rastreamento
|
|
102
|
-
this.componentRooms.get(componentId)?.delete(roomId)
|
|
103
|
-
|
|
104
|
-
liveLog('rooms', componentId, `🚶 Component '${componentId}' left room '${roomId}' (${room.members.size} members)`)
|
|
105
|
-
|
|
106
|
-
// Notificar outros membros
|
|
107
|
-
this.broadcastToRoom(roomId, {
|
|
108
|
-
type: 'ROOM_SYSTEM',
|
|
109
|
-
componentId,
|
|
110
|
-
roomId,
|
|
111
|
-
event: '$sub:leave',
|
|
112
|
-
data: {
|
|
113
|
-
subscriberId: componentId,
|
|
114
|
-
count: room.members.size
|
|
115
|
-
},
|
|
116
|
-
timestamp: Date.now()
|
|
117
|
-
})
|
|
118
|
-
|
|
119
|
-
// Cleanup sala vazia após delay
|
|
120
|
-
if (room.members.size === 0) {
|
|
121
|
-
setTimeout(() => {
|
|
122
|
-
const currentRoom = this.rooms.get(roomId)
|
|
123
|
-
if (currentRoom && currentRoom.members.size === 0) {
|
|
124
|
-
this.rooms.delete(roomId)
|
|
125
|
-
liveLog('rooms', null, `🗑️ Room '${roomId}' destroyed (empty)`)
|
|
126
|
-
}
|
|
127
|
-
}, 5 * 60 * 1000) // 5 minutos
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
/**
|
|
132
|
-
* Componente desconecta - sai de todas as salas
|
|
133
|
-
*/
|
|
134
|
-
cleanupComponent(componentId: string): void {
|
|
135
|
-
const rooms = this.componentRooms.get(componentId)
|
|
136
|
-
if (!rooms) return
|
|
137
|
-
|
|
138
|
-
for (const roomId of rooms) {
|
|
139
|
-
this.leaveRoom(componentId, roomId)
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
this.componentRooms.delete(componentId)
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
/**
|
|
146
|
-
* Emitir evento para todos na sala
|
|
147
|
-
* - Envia via WebSocket para frontends dos outros membros
|
|
148
|
-
* - Também dispara eventos no RoomEventBus para handlers server-side
|
|
149
|
-
*/
|
|
150
|
-
emitToRoom(roomId: string, event: string, data: any, excludeComponentId?: string): number {
|
|
151
|
-
const room = this.rooms.get(roomId)
|
|
152
|
-
if (!room) return 0
|
|
153
|
-
|
|
154
|
-
room.lastActivity = Date.now()
|
|
155
|
-
|
|
156
|
-
// 1. Emitir no RoomEventBus para handlers server-side (outros LiveComponents)
|
|
157
|
-
// Isso permite que componentes do servidor reajam a eventos de outros componentes
|
|
158
|
-
// Usa 'room' como tipo genérico (mesmo usado em $room.on)
|
|
159
|
-
roomEvents.emit('room', roomId, event, data, excludeComponentId)
|
|
160
|
-
|
|
161
|
-
// 2. Broadcast via WebSocket para frontends
|
|
162
|
-
return this.broadcastToRoom(roomId, {
|
|
163
|
-
type: 'ROOM_EVENT',
|
|
164
|
-
componentId: '',
|
|
165
|
-
roomId,
|
|
166
|
-
event,
|
|
167
|
-
data,
|
|
168
|
-
timestamp: Date.now()
|
|
169
|
-
}, excludeComponentId)
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
// 🔒 Maximum room state size (10MB) to prevent memory exhaustion attacks
|
|
173
|
-
private readonly MAX_ROOM_STATE_SIZE = 10 * 1024 * 1024
|
|
174
|
-
|
|
175
|
-
/**
|
|
176
|
-
* Atualizar estado da sala
|
|
177
|
-
*/
|
|
178
|
-
setRoomState(roomId: string, updates: any, excludeComponentId?: string): void {
|
|
179
|
-
const room = this.rooms.get(roomId)
|
|
180
|
-
if (!room) return
|
|
181
|
-
|
|
182
|
-
// Merge estado
|
|
183
|
-
const newState = { ...room.state, ...updates }
|
|
184
|
-
|
|
185
|
-
// 🔒 Validate state size to prevent memory exhaustion
|
|
186
|
-
const stateSize = Buffer.byteLength(JSON.stringify(newState), 'utf8')
|
|
187
|
-
if (stateSize > this.MAX_ROOM_STATE_SIZE) {
|
|
188
|
-
throw new Error('Room state exceeds maximum size limit')
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
room.state = newState
|
|
192
|
-
room.lastActivity = Date.now()
|
|
193
|
-
|
|
194
|
-
// Notificar todos os membros
|
|
195
|
-
this.broadcastToRoom(roomId, {
|
|
196
|
-
type: 'ROOM_STATE',
|
|
197
|
-
componentId: '',
|
|
198
|
-
roomId,
|
|
199
|
-
event: '$state:update',
|
|
200
|
-
data: { state: updates },
|
|
201
|
-
timestamp: Date.now()
|
|
202
|
-
}, excludeComponentId)
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
/**
|
|
206
|
-
* Obter estado da sala
|
|
207
|
-
*/
|
|
208
|
-
getRoomState<TState = any>(roomId: string): TState {
|
|
209
|
-
return (this.rooms.get(roomId)?.state || {}) as TState
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
/**
|
|
213
|
-
* Broadcast para todos os membros da sala
|
|
214
|
-
*/
|
|
215
|
-
private broadcastToRoom(roomId: string, message: any, excludeComponentId?: string): number {
|
|
216
|
-
const room = this.rooms.get(roomId)
|
|
217
|
-
if (!room) return 0
|
|
218
|
-
|
|
219
|
-
let sent = 0
|
|
220
|
-
for (const [componentId, member] of room.members) {
|
|
221
|
-
if (excludeComponentId && componentId === excludeComponentId) continue
|
|
222
|
-
|
|
223
|
-
try {
|
|
224
|
-
if (member.ws && member.ws.readyState === 1) {
|
|
225
|
-
member.ws.send(JSON.stringify({
|
|
226
|
-
...message,
|
|
227
|
-
componentId
|
|
228
|
-
}))
|
|
229
|
-
sent++
|
|
230
|
-
}
|
|
231
|
-
} catch (error) {
|
|
232
|
-
console.error(`Failed to send to ${componentId}:`, error)
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
return sent
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
/**
|
|
240
|
-
* Verificar se componente está em uma sala
|
|
241
|
-
*/
|
|
242
|
-
isInRoom(componentId: string, roomId: string): boolean {
|
|
243
|
-
return this.rooms.get(roomId)?.members.has(componentId) ?? false
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
/**
|
|
247
|
-
* Obter salas de um componente
|
|
248
|
-
*/
|
|
249
|
-
getComponentRooms(componentId: string): string[] {
|
|
250
|
-
return Array.from(this.componentRooms.get(componentId) || [])
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
/**
|
|
254
|
-
* Estatísticas
|
|
255
|
-
*/
|
|
256
|
-
getStats(): {
|
|
257
|
-
totalRooms: number
|
|
258
|
-
rooms: Record<string, { members: number; createdAt: number; lastActivity: number }>
|
|
259
|
-
} {
|
|
260
|
-
const rooms: Record<string, { members: number; createdAt: number; lastActivity: number }> = {}
|
|
261
|
-
|
|
262
|
-
for (const [id, room] of this.rooms) {
|
|
263
|
-
rooms[id] = {
|
|
264
|
-
members: room.members.size,
|
|
265
|
-
createdAt: room.createdAt,
|
|
266
|
-
lastActivity: room.lastActivity
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
return {
|
|
271
|
-
totalRooms: this.rooms.size,
|
|
272
|
-
rooms
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
export const liveRoomManager = new LiveRoomManager()
|
|
278
|
-
export type { Room, RoomMember }
|
|
@@ -1,234 +0,0 @@
|
|
|
1
|
-
// 🔥 FluxStack Live - Room Event Bus (Pub/Sub server-side)
|
|
2
|
-
|
|
3
|
-
type EventHandler<T = any> = (data: T) => void
|
|
4
|
-
|
|
5
|
-
interface RoomSubscription {
|
|
6
|
-
roomType: string
|
|
7
|
-
roomId: string
|
|
8
|
-
event: string
|
|
9
|
-
handler: EventHandler
|
|
10
|
-
componentId: string
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export function createTypedRoomEventBus<TRoomEvents extends Record<string, Record<string, any>>>() {
|
|
14
|
-
const subscriptions = new Map<string, Set<RoomSubscription>>()
|
|
15
|
-
|
|
16
|
-
const getKey = (roomType: string, roomId: string, event: string) =>
|
|
17
|
-
`${roomType}:${roomId}:${event}`
|
|
18
|
-
|
|
19
|
-
const getRoomKey = (roomType: string, roomId: string) =>
|
|
20
|
-
`${roomType}:${roomId}`
|
|
21
|
-
|
|
22
|
-
return {
|
|
23
|
-
on<K extends keyof TRoomEvents, E extends keyof TRoomEvents[K]>(
|
|
24
|
-
roomType: K,
|
|
25
|
-
roomId: string,
|
|
26
|
-
event: E,
|
|
27
|
-
componentId: string,
|
|
28
|
-
handler: EventHandler<TRoomEvents[K][E]>
|
|
29
|
-
): () => void {
|
|
30
|
-
const key = getKey(roomType as string, roomId, event as string)
|
|
31
|
-
|
|
32
|
-
if (!subscriptions.has(key)) {
|
|
33
|
-
subscriptions.set(key, new Set())
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
const subscription: RoomSubscription = {
|
|
37
|
-
roomType: roomType as string,
|
|
38
|
-
roomId,
|
|
39
|
-
event: event as string,
|
|
40
|
-
handler,
|
|
41
|
-
componentId
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
subscriptions.get(key)!.add(subscription)
|
|
45
|
-
|
|
46
|
-
return () => {
|
|
47
|
-
subscriptions.get(key)?.delete(subscription)
|
|
48
|
-
if (subscriptions.get(key)?.size === 0) {
|
|
49
|
-
subscriptions.delete(key)
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
},
|
|
53
|
-
|
|
54
|
-
emit<K extends keyof TRoomEvents, E extends keyof TRoomEvents[K]>(
|
|
55
|
-
roomType: K,
|
|
56
|
-
roomId: string,
|
|
57
|
-
event: E,
|
|
58
|
-
data: TRoomEvents[K][E],
|
|
59
|
-
excludeComponentId?: string
|
|
60
|
-
): number {
|
|
61
|
-
const key = getKey(roomType as string, roomId, event as string)
|
|
62
|
-
const subs = subscriptions.get(key)
|
|
63
|
-
|
|
64
|
-
if (!subs || subs.size === 0) return 0
|
|
65
|
-
|
|
66
|
-
let notified = 0
|
|
67
|
-
for (const sub of subs) {
|
|
68
|
-
if (excludeComponentId && sub.componentId === excludeComponentId) continue
|
|
69
|
-
|
|
70
|
-
try {
|
|
71
|
-
sub.handler(data)
|
|
72
|
-
notified++
|
|
73
|
-
} catch (error) {
|
|
74
|
-
console.error(`❌ RoomEventBus error [${key}]:`, error)
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
return notified
|
|
79
|
-
},
|
|
80
|
-
|
|
81
|
-
unsubscribeAll(componentId: string): number {
|
|
82
|
-
let removed = 0
|
|
83
|
-
|
|
84
|
-
for (const [key, subs] of subscriptions) {
|
|
85
|
-
for (const sub of subs) {
|
|
86
|
-
if (sub.componentId === componentId) {
|
|
87
|
-
subs.delete(sub)
|
|
88
|
-
removed++
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
if (subs.size === 0) {
|
|
92
|
-
subscriptions.delete(key)
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
return removed
|
|
97
|
-
},
|
|
98
|
-
|
|
99
|
-
clearRoom<K extends keyof TRoomEvents>(roomType: K, roomId: string): number {
|
|
100
|
-
const prefix = getRoomKey(roomType as string, roomId)
|
|
101
|
-
let removed = 0
|
|
102
|
-
|
|
103
|
-
for (const key of subscriptions.keys()) {
|
|
104
|
-
if (key.startsWith(prefix)) {
|
|
105
|
-
removed += subscriptions.get(key)?.size ?? 0
|
|
106
|
-
subscriptions.delete(key)
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
return removed
|
|
111
|
-
},
|
|
112
|
-
|
|
113
|
-
getStats(): { totalSubscriptions: number; rooms: Record<string, { events: Record<string, number> }> } {
|
|
114
|
-
const rooms: Record<string, { events: Record<string, number> }> = {}
|
|
115
|
-
let total = 0
|
|
116
|
-
|
|
117
|
-
for (const [key, subs] of subscriptions) {
|
|
118
|
-
const [roomType, roomId, event] = key.split(':')
|
|
119
|
-
const roomKey = `${roomType}:${roomId}`
|
|
120
|
-
|
|
121
|
-
if (!rooms[roomKey]) {
|
|
122
|
-
rooms[roomKey] = { events: {} }
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
rooms[roomKey].events[event] = subs.size
|
|
126
|
-
total += subs.size
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
return { totalSubscriptions: total, rooms }
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
class RoomEventBus {
|
|
135
|
-
private subscriptions = new Map<string, Set<RoomSubscription>>()
|
|
136
|
-
|
|
137
|
-
private getKey(roomType: string, roomId: string, event: string): string {
|
|
138
|
-
return `${roomType}:${roomId}:${event}`
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
on(roomType: string, roomId: string, event: string, componentId: string, handler: EventHandler): () => void {
|
|
142
|
-
const key = this.getKey(roomType, roomId, event)
|
|
143
|
-
|
|
144
|
-
if (!this.subscriptions.has(key)) {
|
|
145
|
-
this.subscriptions.set(key, new Set())
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
const subscription: RoomSubscription = { roomType, roomId, event, handler, componentId }
|
|
149
|
-
this.subscriptions.get(key)!.add(subscription)
|
|
150
|
-
|
|
151
|
-
return () => {
|
|
152
|
-
this.subscriptions.get(key)?.delete(subscription)
|
|
153
|
-
if (this.subscriptions.get(key)?.size === 0) {
|
|
154
|
-
this.subscriptions.delete(key)
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
emit(roomType: string, roomId: string, event: string, data: any, excludeComponentId?: string): number {
|
|
160
|
-
const key = this.getKey(roomType, roomId, event)
|
|
161
|
-
const subs = this.subscriptions.get(key)
|
|
162
|
-
|
|
163
|
-
if (!subs || subs.size === 0) return 0
|
|
164
|
-
|
|
165
|
-
let notified = 0
|
|
166
|
-
for (const sub of subs) {
|
|
167
|
-
if (excludeComponentId && sub.componentId === excludeComponentId) continue
|
|
168
|
-
|
|
169
|
-
try {
|
|
170
|
-
sub.handler(data)
|
|
171
|
-
notified++
|
|
172
|
-
} catch (error) {
|
|
173
|
-
console.error(`❌ RoomEventBus error [${key}]:`, error)
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
return notified
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
unsubscribeAll(componentId: string): number {
|
|
181
|
-
let removed = 0
|
|
182
|
-
|
|
183
|
-
for (const [key, subs] of this.subscriptions) {
|
|
184
|
-
for (const sub of subs) {
|
|
185
|
-
if (sub.componentId === componentId) {
|
|
186
|
-
subs.delete(sub)
|
|
187
|
-
removed++
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
if (subs.size === 0) {
|
|
191
|
-
this.subscriptions.delete(key)
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
return removed
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
clearRoom(roomType: string, roomId: string): number {
|
|
199
|
-
const prefix = `${roomType}:${roomId}`
|
|
200
|
-
let removed = 0
|
|
201
|
-
|
|
202
|
-
for (const key of this.subscriptions.keys()) {
|
|
203
|
-
if (key.startsWith(prefix)) {
|
|
204
|
-
removed += this.subscriptions.get(key)?.size ?? 0
|
|
205
|
-
this.subscriptions.delete(key)
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
return removed
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
getStats() {
|
|
213
|
-
const rooms: Record<string, { events: Record<string, number> }> = {}
|
|
214
|
-
let total = 0
|
|
215
|
-
|
|
216
|
-
for (const [key, subs] of this.subscriptions) {
|
|
217
|
-
const [roomType, roomId, event] = key.split(':')
|
|
218
|
-
const roomKey = `${roomType}:${roomId}`
|
|
219
|
-
|
|
220
|
-
if (!rooms[roomKey]) {
|
|
221
|
-
rooms[roomKey] = { events: {} }
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
rooms[roomKey].events[event] = subs.size
|
|
225
|
-
total += subs.size
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
return { totalSubscriptions: total, rooms }
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
export const roomEvents = new RoomEventBus()
|
|
233
|
-
|
|
234
|
-
export type { EventHandler, RoomSubscription }
|
|
@@ -1,172 +0,0 @@
|
|
|
1
|
-
// 🔥 FluxStack Live - Room State Manager (In-memory storage per room)
|
|
2
|
-
|
|
3
|
-
type RoomStateData = Record<string, any>
|
|
4
|
-
|
|
5
|
-
interface RoomInfo {
|
|
6
|
-
state: RoomStateData
|
|
7
|
-
componentCount: number
|
|
8
|
-
createdAt: number
|
|
9
|
-
lastUpdate: number
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export function createTypedRoomState<TRoomTypes extends Record<string, RoomStateData>>() {
|
|
13
|
-
const rooms = new Map<string, RoomInfo>()
|
|
14
|
-
const getKey = (type: string, roomId: string) => `${type}:${roomId}`
|
|
15
|
-
|
|
16
|
-
return {
|
|
17
|
-
get<K extends keyof TRoomTypes>(type: K, roomId: string, defaultState: TRoomTypes[K]): TRoomTypes[K] {
|
|
18
|
-
const key = getKey(type as string, roomId)
|
|
19
|
-
const room = rooms.get(key)
|
|
20
|
-
|
|
21
|
-
if (room) return room.state as TRoomTypes[K]
|
|
22
|
-
|
|
23
|
-
rooms.set(key, { state: defaultState, componentCount: 0, createdAt: Date.now(), lastUpdate: Date.now() })
|
|
24
|
-
return defaultState
|
|
25
|
-
},
|
|
26
|
-
|
|
27
|
-
update<K extends keyof TRoomTypes>(type: K, roomId: string, updates: Partial<TRoomTypes[K]>): TRoomTypes[K] {
|
|
28
|
-
const key = getKey(type as string, roomId)
|
|
29
|
-
const room = rooms.get(key)
|
|
30
|
-
|
|
31
|
-
if (room) {
|
|
32
|
-
room.state = { ...room.state, ...updates }
|
|
33
|
-
room.lastUpdate = Date.now()
|
|
34
|
-
return room.state as TRoomTypes[K]
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
const newState = updates as TRoomTypes[K]
|
|
38
|
-
rooms.set(key, { state: newState, componentCount: 0, createdAt: Date.now(), lastUpdate: Date.now() })
|
|
39
|
-
return newState
|
|
40
|
-
},
|
|
41
|
-
|
|
42
|
-
set<K extends keyof TRoomTypes>(type: K, roomId: string, state: TRoomTypes[K]): void {
|
|
43
|
-
const key = getKey(type as string, roomId)
|
|
44
|
-
const room = rooms.get(key)
|
|
45
|
-
|
|
46
|
-
if (room) {
|
|
47
|
-
room.state = state
|
|
48
|
-
room.lastUpdate = Date.now()
|
|
49
|
-
} else {
|
|
50
|
-
rooms.set(key, { state, componentCount: 0, createdAt: Date.now(), lastUpdate: Date.now() })
|
|
51
|
-
}
|
|
52
|
-
},
|
|
53
|
-
|
|
54
|
-
join<K extends keyof TRoomTypes>(type: K, roomId: string): void {
|
|
55
|
-
const room = rooms.get(getKey(type as string, roomId))
|
|
56
|
-
if (room) room.componentCount++
|
|
57
|
-
},
|
|
58
|
-
|
|
59
|
-
leave<K extends keyof TRoomTypes>(type: K, roomId: string): void {
|
|
60
|
-
const key = getKey(type as string, roomId)
|
|
61
|
-
const room = rooms.get(key)
|
|
62
|
-
if (room) {
|
|
63
|
-
room.componentCount--
|
|
64
|
-
if (room.componentCount <= 0) {
|
|
65
|
-
setTimeout(() => {
|
|
66
|
-
const current = rooms.get(key)
|
|
67
|
-
if (current && current.componentCount <= 0) {
|
|
68
|
-
rooms.delete(key)
|
|
69
|
-
}
|
|
70
|
-
}, 5 * 60 * 1000)
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
},
|
|
74
|
-
|
|
75
|
-
has<K extends keyof TRoomTypes>(type: K, roomId: string): boolean {
|
|
76
|
-
return rooms.has(getKey(type as string, roomId))
|
|
77
|
-
},
|
|
78
|
-
|
|
79
|
-
delete<K extends keyof TRoomTypes>(type: K, roomId: string): boolean {
|
|
80
|
-
return rooms.delete(getKey(type as string, roomId))
|
|
81
|
-
},
|
|
82
|
-
|
|
83
|
-
getStats(): { totalRooms: number; rooms: Record<string, { componentCount: number; stateKeys: string[] }> } {
|
|
84
|
-
const roomStats: Record<string, { componentCount: number; stateKeys: string[] }> = {}
|
|
85
|
-
for (const [key, info] of rooms) {
|
|
86
|
-
roomStats[key] = { componentCount: info.componentCount, stateKeys: Object.keys(info.state) }
|
|
87
|
-
}
|
|
88
|
-
return { totalRooms: rooms.size, rooms: roomStats }
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
class RoomStateManager {
|
|
94
|
-
private rooms = new Map<string, RoomInfo>()
|
|
95
|
-
|
|
96
|
-
get<T extends RoomStateData>(roomId: string, defaultState?: T): T {
|
|
97
|
-
const room = this.rooms.get(roomId)
|
|
98
|
-
if (room) return room.state as T
|
|
99
|
-
|
|
100
|
-
if (defaultState) {
|
|
101
|
-
this.rooms.set(roomId, { state: defaultState, componentCount: 0, createdAt: Date.now(), lastUpdate: Date.now() })
|
|
102
|
-
return defaultState
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
return {} as T
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
update<T extends RoomStateData>(roomId: string, updates: Partial<T>): T {
|
|
109
|
-
const room = this.rooms.get(roomId)
|
|
110
|
-
|
|
111
|
-
if (room) {
|
|
112
|
-
room.state = { ...room.state, ...updates }
|
|
113
|
-
room.lastUpdate = Date.now()
|
|
114
|
-
return room.state as T
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
const newState = updates as T
|
|
118
|
-
this.rooms.set(roomId, { state: newState, componentCount: 0, createdAt: Date.now(), lastUpdate: Date.now() })
|
|
119
|
-
return newState
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
set<T extends RoomStateData>(roomId: string, state: T): void {
|
|
123
|
-
const room = this.rooms.get(roomId)
|
|
124
|
-
|
|
125
|
-
if (room) {
|
|
126
|
-
room.state = state
|
|
127
|
-
room.lastUpdate = Date.now()
|
|
128
|
-
} else {
|
|
129
|
-
this.rooms.set(roomId, { state, componentCount: 0, createdAt: Date.now(), lastUpdate: Date.now() })
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
join(roomId: string): void {
|
|
134
|
-
const room = this.rooms.get(roomId)
|
|
135
|
-
if (room) room.componentCount++
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
leave(roomId: string): void {
|
|
139
|
-
const room = this.rooms.get(roomId)
|
|
140
|
-
if (room) {
|
|
141
|
-
room.componentCount--
|
|
142
|
-
if (room.componentCount <= 0) {
|
|
143
|
-
setTimeout(() => {
|
|
144
|
-
const current = this.rooms.get(roomId)
|
|
145
|
-
if (current && current.componentCount <= 0) {
|
|
146
|
-
this.rooms.delete(roomId)
|
|
147
|
-
}
|
|
148
|
-
}, 5 * 60 * 1000)
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
has(roomId: string): boolean {
|
|
154
|
-
return this.rooms.has(roomId)
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
delete(roomId: string): boolean {
|
|
158
|
-
return this.rooms.delete(roomId)
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
getStats(): { totalRooms: number; rooms: Record<string, { componentCount: number; stateKeys: string[] }> } {
|
|
162
|
-
const rooms: Record<string, { componentCount: number; stateKeys: string[] }> = {}
|
|
163
|
-
for (const [roomId, info] of this.rooms) {
|
|
164
|
-
rooms[roomId] = { componentCount: info.componentCount, stateKeys: Object.keys(info.state) }
|
|
165
|
-
}
|
|
166
|
-
return { totalRooms: this.rooms.size, rooms }
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
export const roomState = new RoomStateManager()
|
|
171
|
-
|
|
172
|
-
export type { RoomStateData, RoomInfo }
|
|
File without changes
|