create-fluxstack 1.15.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 (35) 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 +1 -0
  4. package/LLMD/resources/live-rooms.md +731 -333
  5. package/app/client/.live-stubs/LivePingPong.js +10 -0
  6. package/app/client/.live-stubs/LiveRoomChat.js +3 -2
  7. package/app/client/.live-stubs/LiveSharedCounter.js +10 -0
  8. package/app/client/src/App.tsx +15 -14
  9. package/app/client/src/components/AppLayout.tsx +4 -4
  10. package/app/client/src/live/PingPongDemo.tsx +199 -0
  11. package/app/client/src/live/RoomChatDemo.tsx +187 -22
  12. package/app/client/src/live/SharedCounterDemo.tsx +142 -0
  13. package/app/server/live/LivePingPong.ts +61 -0
  14. package/app/server/live/LiveRoomChat.ts +106 -38
  15. package/app/server/live/LiveSharedCounter.ts +73 -0
  16. package/app/server/live/rooms/ChatRoom.ts +68 -0
  17. package/app/server/live/rooms/CounterRoom.ts +51 -0
  18. package/app/server/live/rooms/DirectoryRoom.ts +42 -0
  19. package/app/server/live/rooms/PingRoom.ts +40 -0
  20. package/core/build/live-components-generator.ts +10 -1
  21. package/core/client/index.ts +0 -16
  22. package/core/server/live/auto-generated-components.ts +3 -9
  23. package/core/server/live/index.ts +0 -5
  24. package/core/server/live/websocket-plugin.ts +37 -2
  25. package/core/utils/version.ts +1 -1
  26. package/package.json +100 -99
  27. package/tsconfig.json +4 -1
  28. package/app/client/.live-stubs/LiveChat.js +0 -7
  29. package/app/client/.live-stubs/LiveTodoList.js +0 -9
  30. package/app/client/src/live/ChatDemo.tsx +0 -107
  31. package/app/client/src/live/LiveDebuggerPanel.tsx +0 -779
  32. package/app/client/src/live/TodoListDemo.tsx +0 -158
  33. package/app/server/live/LiveChat.ts +0 -78
  34. package/app/server/live/LiveTodoList.ts +0 -110
  35. package/core/client/components/LiveDebugger.tsx +0 -1324
@@ -1,341 +1,779 @@
1
1
  # Live Room System
2
2
 
3
- **Version:** 1.11.0 | **Updated:** 2025-02-09
3
+ **Version:** 2.0.0 | **Updated:** 2025-03-11
4
4
 
5
5
  ## Quick Facts
6
6
 
7
- - Server-side room management for real-time communication
8
- - Multiple rooms per component supported
7
+ - **Two APIs:** Typed `$room(ChatRoom, 'lobby')` and untyped `$room('room-id')` — coexist
8
+ - **Typed rooms** have lifecycle hooks, private metadata, custom methods, join rejection
9
+ - **Untyped rooms** still work for simple pub/sub (backward compatible)
10
+ - Server-side room management — client cannot join typed rooms directly
9
11
  - Events propagate to all room members automatically
10
12
  - HTTP API integration for external systems (webhooks, bots)
11
- - Type-safe event handling with TypeScript
13
+ - Powered by `@fluxstack/live` package
12
14
 
13
15
  ## Overview
14
16
 
15
- The Room System enables real-time communication between Live Components. It's **server-side first**, meaning:
17
+ The Room System enables real-time communication between Live Components. There are two levels:
16
18
 
17
- 1. Server controls room membership and event routing
18
- 2. Each component updates its own client via `setState()`
19
- 3. External systems can emit events via HTTP API
19
+ 1. **Typed Rooms (LiveRoom)** Class-based rooms with lifecycle hooks, private state (`meta`), custom methods, and join validation. Ideal for rooms with business logic (chat with passwords, game rooms with rules).
20
20
 
21
- ## Core API
21
+ 2. **Untyped Rooms** — String-based `$room('id')` for simple pub/sub without a room class. Ideal for notifications, presence, simple event broadcasting.
22
22
 
23
- ### Server-Side ($room)
23
+ Both are **server-side first**: the server controls membership, event routing, and state.
24
+
25
+ ---
26
+
27
+ ## Typed Rooms (LiveRoom)
28
+
29
+ ### Creating a Room Class
30
+
31
+ Room classes live in `app/server/live/rooms/` and are auto-discovered on startup.
24
32
 
25
33
  ```typescript
26
- // app/server/live/MyComponent.ts
27
- import { LiveComponent } from '@core/types/types'
34
+ // app/server/live/rooms/ChatRoom.ts
35
+ import { LiveRoom } from '@fluxstack/live'
36
+ import type { RoomJoinContext, RoomLeaveContext } from '@fluxstack/live'
28
37
 
29
- export class MyComponent extends LiveComponent<typeof defaultState> {
38
+ export interface ChatMessage {
39
+ id: string
40
+ user: string
41
+ text: string
42
+ timestamp: number
43
+ }
44
+
45
+ // Public state — synced to all room members via WebSocket
46
+ interface ChatState {
47
+ messages: ChatMessage[]
48
+ onlineCount: number
49
+ isPrivate: boolean
50
+ }
30
51
 
31
- // Join a room
32
- this.$room('room-id').join()
52
+ // Private metadata — NEVER leaves the server
53
+ interface ChatMeta {
54
+ password: string | null
55
+ createdBy: string | null
56
+ }
33
57
 
34
- // Leave a room
35
- this.$room('room-id').leave()
58
+ // Typed events emitted within the room
59
+ interface ChatEvents {
60
+ 'chat:message': ChatMessage
61
+ }
36
62
 
37
- // Emit event to all members (except self)
38
- this.$room('room-id').emit('event-name', { data: 'value' })
63
+ export class ChatRoom extends LiveRoom<ChatState, ChatMeta, ChatEvents> {
64
+ // Required: unique room type name (used as prefix in compound IDs)
65
+ static roomName = 'chat'
39
66
 
40
- // Listen for events from other members
41
- this.$room('room-id').on('event-name', (data) => {
42
- // Handle event, update local state
43
- this.setState({ ... })
44
- })
67
+ // Initial state templates (cloned per instance)
68
+ static defaultState: ChatState = { messages: [], onlineCount: 0, isPrivate: false }
69
+ static defaultMeta: ChatMeta = { password: null, createdBy: null }
45
70
 
46
- // Get room state
47
- const state = this.$room('room-id').state
71
+ // Room options
72
+ static $options = { maxMembers: 100 }
48
73
 
49
- // Set room state (broadcasts to all)
50
- this.$room('room-id').setState({ key: 'value' })
74
+ // === Lifecycle Hooks ===
51
75
 
52
- // List all joined rooms
53
- const rooms = this.$rooms // ['room-1', 'room-2']
76
+ onJoin(ctx: RoomJoinContext) {
77
+ // Validate password if room is protected
78
+ if (this.meta.password) {
79
+ if (ctx.payload?.password !== this.meta.password) {
80
+ return false // Reject join
81
+ }
82
+ }
83
+ this.setState({ onlineCount: this.state.onlineCount + 1 })
84
+ }
85
+
86
+ onLeave(_ctx: RoomLeaveContext) {
87
+ this.setState({ onlineCount: Math.max(0, this.state.onlineCount - 1) })
88
+ }
89
+
90
+ // === Custom Methods ===
91
+
92
+ setPassword(password: string | null) {
93
+ this.meta.password = password
94
+ this.setState({ isPrivate: password !== null })
95
+ }
96
+
97
+ addMessage(user: string, text: string) {
98
+ const msg: ChatMessage = {
99
+ id: `msg-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
100
+ user,
101
+ text,
102
+ timestamp: Date.now(),
103
+ }
104
+ this.setState({ messages: [...this.state.messages.slice(-99), msg] })
105
+ this.emit('chat:message', msg)
106
+ return msg
107
+ }
54
108
  }
55
109
  ```
56
110
 
57
- ### Default Room
111
+ ### LiveRoom Base Class API
112
+
113
+ ```typescript
114
+ abstract class LiveRoom<TState, TMeta, TEvents> {
115
+ // === Static Fields (required in subclass) ===
116
+ static roomName: string // Unique type name (e.g. 'chat')
117
+ static defaultState: Record<string, any> // Initial public state
118
+ static defaultMeta: Record<string, any> // Initial private metadata
119
+ static $options?: LiveRoomOptions // { maxMembers?, deepDiff?, deepDiffDepth? }
120
+
121
+ // === Instance Properties ===
122
+ readonly id: string // Compound ID (e.g. 'chat:lobby')
123
+ state: TState // Public state — synced to all members
124
+ meta: TMeta // Private metadata — NEVER sent to clients
125
+
126
+ // === Framework Methods ===
127
+ setState(updates: Partial<TState>): void // Update & broadcast state
128
+ emit<K extends keyof TEvents>(event: K, data: TEvents[K]): number // Emit typed event
129
+ get memberCount(): number // Current member count
130
+
131
+ // === Lifecycle Hooks (override in subclass) ===
132
+ onJoin(ctx: RoomJoinContext): void | false // Return false to reject
133
+ onLeave(ctx: RoomLeaveContext): void
134
+ onEvent(event: string, data: any, ctx: RoomEventContext): void
135
+ onCreate(): void // First member joined
136
+ onDestroy(): void | false // Last member left (return false to keep alive)
137
+ }
138
+ ```
58
139
 
59
- If component has a default room (via `options.room`), you can use shorthand:
140
+ ### Lifecycle Context Types
60
141
 
61
142
  ```typescript
62
- // Using default room
63
- this.$room.emit('event', data)
64
- this.$room.on('event', handler)
143
+ interface RoomJoinContext {
144
+ componentId: string
145
+ userId?: string
146
+ payload?: any // Arbitrary data passed from room.join({ ... })
147
+ }
65
148
 
66
- // Equivalent to:
67
- this.$room('default-room-id').emit('event', data)
149
+ interface RoomLeaveContext {
150
+ componentId: string
151
+ userId?: string
152
+ reason: 'leave' | 'disconnect' | 'cleanup'
153
+ }
154
+
155
+ interface RoomEventContext {
156
+ componentId: string
157
+ userId?: string
158
+ }
68
159
  ```
69
160
 
70
- ## Complete Example: Chat Component
161
+ ### Public State vs Private Meta
71
162
 
72
- ### Server Component
163
+ | | `state` | `meta` |
164
+ |---|---|---|
165
+ | **Visibility** | Synced to all room members | Server-only, never leaves |
166
+ | **Access** | `this.state.x` / `this.setState({})` | `this.meta.x` (direct mutation) |
167
+ | **Use for** | Messages, counts, flags | Passwords, secrets, internal data |
168
+ | **Broadcast** | Yes, via deep diff | Never |
169
+
170
+ ### Using Typed Rooms in Components
73
171
 
74
172
  ```typescript
75
173
  // app/server/live/LiveRoomChat.ts
76
- import { LiveComponent } from '@core/types/types'
174
+ import { ChatRoom } from './rooms/ChatRoom'
77
175
 
78
- export interface ChatMessage {
79
- id: string
80
- user: string
81
- text: string
82
- timestamp: number
83
- }
176
+ export class LiveRoomChat extends LiveComponent<typeof LiveRoomChat.defaultState> {
177
+ static publicActions = ['joinRoom', 'sendMessage'] as const
178
+
179
+ async joinRoom(payload: { roomId: string; password?: string }) {
180
+ // $room(Class, instanceId) — returns typed handle with custom methods
181
+ const room = this.$room(ChatRoom, payload.roomId)
182
+
183
+ // Join with payload (passed to onJoin lifecycle hook)
184
+ const result = room.join({ password: payload.password })
185
+
186
+ // Check rejection
187
+ if ('rejected' in result && result.rejected) {
188
+ return { success: false, error: result.reason }
189
+ }
190
+
191
+ // Access room state (public)
192
+ const messages = room.state.messages
193
+
194
+ // Access room meta (private, server-only)
195
+ const createdBy = room.meta.createdBy
196
+
197
+ // Call custom methods
198
+ room.addMessage('System', 'Welcome!')
199
+ room.setPassword('new-password')
84
200
 
85
- export const defaultState = {
86
- username: '',
87
- activeRoom: null as string | null,
88
- rooms: [] as { id: string; name: string }[],
89
- messages: {} as Record<string, ChatMessage[]>,
90
- typingUsers: {} as Record<string, string[]>
201
+ // Listen for typed events
202
+ room.on('chat:message', (msg) => {
203
+ // Update component state to sync with frontend
204
+ this.setState({ messages: [...this.state.messages, msg] })
205
+ })
206
+
207
+ // Emit typed events
208
+ room.emit('chat:message', { id: '1', user: 'Bot', text: 'Hi', timestamp: Date.now() })
209
+
210
+ // Framework properties
211
+ console.log(room.id) // 'chat:lobby'
212
+ console.log(room.memberCount) // 5
213
+ console.log(room.state) // { messages: [...], onlineCount: 5, isPrivate: true }
214
+
215
+ return { success: true }
216
+ }
91
217
  }
218
+ ```
219
+
220
+ ### Compound Room IDs
221
+
222
+ Typed rooms use compound IDs: `${roomName}:${instanceId}`
223
+
224
+ ```
225
+ ChatRoom + 'lobby' → 'chat:lobby'
226
+ ChatRoom + 'vip' → 'chat:vip'
227
+ GameRoom + 'match1' → 'game:match1'
228
+ ```
229
+
230
+ This allows multiple instances of the same room type. The `RoomRegistry` resolves the class from the compound ID automatically.
231
+
232
+ ### Room Auto-Discovery
233
+
234
+ Room classes in `app/server/live/rooms/` are auto-discovered on startup. Any exported class that extends `LiveRoom` is registered automatically.
235
+
236
+ ```
237
+ app/server/live/rooms/
238
+ ChatRoom.ts → registered as 'chat'
239
+ DirectoryRoom.ts → registered as 'directory'
240
+ GameRoom.ts → registered as 'game'
241
+ ```
242
+
243
+ No manual registration needed. The `websocket-plugin.ts` scans the directory and passes discovered rooms to `LiveServer`.
92
244
 
93
- export class LiveRoomChat extends LiveComponent<typeof defaultState> {
94
- static defaultState = defaultState
245
+ ### Join Rejection
95
246
 
96
- constructor(initialState: Partial<typeof defaultState>, ws: any, options?: { room?: string; userId?: string }) {
97
- super({ ...defaultState, ...initialState }, ws, options)
247
+ Typed rooms can reject joins in the `onJoin` hook:
248
+
249
+ ```typescript
250
+ class VIPRoom extends LiveRoom<State, Meta, Events> {
251
+ static roomName = 'vip'
252
+
253
+ onJoin(ctx: RoomJoinContext) {
254
+ // Reject if no password
255
+ if (this.meta.password && ctx.payload?.password !== this.meta.password) {
256
+ return false
257
+ }
258
+
259
+ // Reject if room is full (also handled automatically by maxMembers)
260
+ if (this.memberCount >= 10) {
261
+ return false
262
+ }
263
+
264
+ // Accept (return void)
98
265
  }
266
+ }
267
+ ```
268
+
269
+ On the component side:
99
270
 
100
- // Join a chat room
101
- async joinRoom(payload: { roomId: string; roomName?: string }) {
102
- const { roomId, roomName } = payload
271
+ ```typescript
272
+ const room = this.$room(VIPRoom, 'exclusive')
273
+ const result = room.join({ password: '1234' })
103
274
 
104
- // 1. Join the room on server
105
- this.$room(roomId).join()
275
+ if ('rejected' in result && result.rejected) {
276
+ return { success: false, error: result.reason }
277
+ }
278
+ ```
106
279
 
107
- // 2. Listen for messages from OTHER users
108
- this.$room(roomId).on('message:new', (msg: ChatMessage) => {
109
- this.addMessageToState(roomId, msg)
110
- })
280
+ ### Server-Only Join Enforcement
281
+
282
+ Clients cannot join typed rooms directly via WebSocket. The join MUST happen through a component action on the server. If a client attempts to send a `ROOM_JOIN` message for a typed room, it receives an error:
283
+
284
+ ```
285
+ "Room requires server-side join via component action"
286
+ ```
287
+
288
+ This ensures all join logic (password validation, authorization, etc.) runs on the server.
289
+
290
+ ---
291
+
292
+ ## Shared Room Directory Pattern
293
+
294
+ When users can create rooms dynamically, other users need to discover them. The **DirectoryRoom** pattern solves this:
295
+
296
+ ```typescript
297
+ // app/server/live/rooms/DirectoryRoom.ts
298
+ import { LiveRoom } from '@fluxstack/live'
299
+
300
+ export interface DirectoryEntry {
301
+ id: string
302
+ name: string
303
+ isPrivate: boolean
304
+ createdBy: string
305
+ }
306
+
307
+ interface DirectoryState {
308
+ rooms: DirectoryEntry[]
309
+ }
310
+
311
+ interface DirectoryEvents {
312
+ 'room:added': DirectoryEntry
313
+ 'room:removed': { id: string }
314
+ }
315
+
316
+ export class DirectoryRoom extends LiveRoom<DirectoryState, {}, DirectoryEvents> {
317
+ static roomName = 'directory'
318
+ static defaultState: DirectoryState = { rooms: [] }
319
+ static defaultMeta = {}
111
320
 
112
- // 3. Listen for typing events
113
- this.$room(roomId).on('user:typing', (data: { user: string; typing: boolean }) => {
114
- this.updateTypingUsers(roomId, data.user, data.typing)
321
+ addRoom(entry: DirectoryEntry) {
322
+ this.setState({
323
+ rooms: [...this.state.rooms.filter(r => r.id !== entry.id), entry]
115
324
  })
325
+ this.emit('room:added', entry)
326
+ }
116
327
 
117
- // 4. Update local state (syncs to frontend)
328
+ removeRoom(id: string) {
118
329
  this.setState({
119
- activeRoom: roomId,
120
- rooms: [...this.state.rooms, { id: roomId, name: roomName || roomId }],
121
- messages: { ...this.state.messages, [roomId]: [] },
122
- typingUsers: { ...this.state.typingUsers, [roomId]: [] }
330
+ rooms: this.state.rooms.filter(r => r.id !== id)
123
331
  })
332
+ this.emit('room:removed', { id })
333
+ }
334
+ }
335
+ ```
124
336
 
125
- return { success: true, roomId }
337
+ **Usage in component:**
338
+
339
+ ```typescript
340
+ export class LiveRoomChat extends LiveComponent<...> {
341
+ constructor(initialState, ws, options) {
342
+ super(initialState, ws, options)
343
+
344
+ // All instances auto-join the directory
345
+ const dir = this.$room(DirectoryRoom, 'main')
346
+ dir.join()
347
+
348
+ // Load existing rooms
349
+ this.setState({ customRooms: dir.state.rooms || [] })
350
+
351
+ // Listen for real-time updates
352
+ dir.on('room:added', (entry) => {
353
+ this.setState({ customRooms: [...this.state.customRooms, entry] })
354
+ })
126
355
  }
127
356
 
128
- // Send message
129
- async sendMessage(payload: { text: string }) {
130
- const roomId = this.state.activeRoom
131
- if (!roomId) throw new Error('No active room')
357
+ async createRoom(payload: { roomId: string; roomName: string; password?: string }) {
358
+ const room = this.$room(ChatRoom, payload.roomId)
359
+ room.join()
132
360
 
133
- const message: ChatMessage = {
134
- id: `msg-${Date.now()}`,
135
- user: this.state.username,
136
- text: payload.text,
137
- timestamp: Date.now()
138
- }
361
+ if (payload.password) room.setPassword(payload.password)
139
362
 
140
- // 1. Add to MY state (syncs to MY frontend)
141
- this.addMessageToState(roomId, message)
363
+ // Register in directory all connected users see it immediately
364
+ this.$room(DirectoryRoom, 'main').addRoom({
365
+ id: payload.roomId,
366
+ name: payload.roomName,
367
+ isPrivate: !!payload.password,
368
+ createdBy: this.state.username
369
+ })
370
+ }
371
+ }
372
+ ```
142
373
 
143
- // 2. Emit to OTHERS (they receive via $room.on)
144
- this.$room(roomId).emit('message:new', message)
374
+ **Flow:**
145
375
 
146
- return { success: true, message }
376
+ ```
377
+ User A creates room → DirectoryRoom.addRoom()
378
+ → emit('room:added', entry)
379
+ → All LiveRoomChat instances receive event
380
+ → Each updates customRooms in component state
381
+ → Each frontend sees the new room in sidebar
382
+ ```
383
+
384
+ ---
385
+
386
+ ## Password-Protected Rooms
387
+
388
+ Complete implementation using `meta` (server-only) and `onJoin` validation:
389
+
390
+ ### 1. Room Class
391
+
392
+ ```typescript
393
+ // ChatMeta.password is NEVER sent to clients
394
+ interface ChatMeta {
395
+ password: string | null
396
+ createdBy: string | null
397
+ }
398
+
399
+ class ChatRoom extends LiveRoom<ChatState, ChatMeta, ChatEvents> {
400
+ static defaultMeta: ChatMeta = { password: null, createdBy: null }
401
+
402
+ setPassword(password: string | null) {
403
+ this.meta.password = password // Server-only
404
+ this.setState({ isPrivate: password !== null }) // Visible to clients
147
405
  }
148
406
 
149
- // Helper: add message to state
150
- private addMessageToState(roomId: string, msg: ChatMessage) {
151
- const messages = this.state.messages[roomId] || []
152
- this.setState({
153
- messages: {
154
- ...this.state.messages,
155
- [roomId]: [...messages, msg].slice(-100) // Keep last 100
407
+ onJoin(ctx: RoomJoinContext) {
408
+ if (this.meta.password) {
409
+ if (ctx.payload?.password !== this.meta.password) {
410
+ return false // Wrong password → rejected
156
411
  }
157
- })
412
+ }
413
+ // Password correct or no password → accepted
158
414
  }
415
+ }
416
+ ```
159
417
 
160
- // Helper: update typing users
161
- private updateTypingUsers(roomId: string, user: string, typing: boolean) {
162
- const current = this.state.typingUsers[roomId] || []
163
- const updated = typing
164
- ? [...current.filter(u => u !== user), user]
165
- : current.filter(u => u !== user)
418
+ ### 2. Component Action
166
419
 
167
- this.setState({
168
- typingUsers: { ...this.state.typingUsers, [roomId]: updated }
420
+ ```typescript
421
+ async joinRoom(payload: { roomId: string; password?: string }) {
422
+ const room = this.$room(ChatRoom, payload.roomId)
423
+ const result = room.join({ password: payload.password })
424
+ // ^^^^^^^^^ passed to onJoin ctx.payload
425
+
426
+ if ('rejected' in result && result.rejected) {
427
+ return { success: false, error: 'Senha incorreta' }
428
+ }
429
+ return { success: true }
430
+ }
431
+
432
+ async createRoom(payload: { roomId: string; roomName: string; password?: string }) {
433
+ const room = this.$room(ChatRoom, payload.roomId)
434
+ room.join() // Creator joins without password (first join, no password set yet)
435
+
436
+ if (payload.password) {
437
+ room.setPassword(payload.password) // Now set password for future joiners
438
+ }
439
+ room.meta.createdBy = this.state.username
440
+ }
441
+ ```
442
+
443
+ ### 3. Frontend
444
+
445
+ ```tsx
446
+ // Password prompt when clicking a private room
447
+ const handleJoinRoom = async (roomId: string, roomName: string, isPrivate?: boolean) => {
448
+ if (isPrivate) {
449
+ showPasswordModal(roomId, roomName)
450
+ return
451
+ }
452
+ const result = await chat.joinRoom({ roomId, roomName })
453
+ if (result && !result.success) {
454
+ // Rejected — maybe password-protected, show prompt
455
+ showPasswordModal(roomId, roomName)
456
+ }
457
+ }
458
+
459
+ // Submit with password
460
+ const handlePasswordSubmit = async () => {
461
+ const result = await chat.joinRoom({
462
+ roomId: prompt.roomId,
463
+ roomName: prompt.roomName,
464
+ password: passwordInput
465
+ })
466
+ if (result && !result.success) {
467
+ showError('Senha incorreta')
468
+ }
469
+ }
470
+ ```
471
+
472
+ ---
473
+
474
+ ## Untyped Rooms (Legacy)
475
+
476
+ The string-based API still works for simple pub/sub without a room class:
477
+
478
+ ```typescript
479
+ // Join/leave
480
+ this.$room('notifications').join()
481
+ this.$room('notifications').leave()
482
+
483
+ // Emit/listen
484
+ this.$room('notifications').emit('alert', { msg: 'hey' })
485
+ this.$room('notifications').on('alert', (data) => {
486
+ this.setState({ alerts: [...this.state.alerts, data] })
487
+ })
488
+
489
+ // Room state
490
+ this.$room('notifications').setState({ lastAlert: Date.now() })
491
+ const state = this.$room('notifications').state
492
+
493
+ // Default room shorthand (via options.room)
494
+ this.$room.emit('event', data)
495
+ this.$room.on('event', handler)
496
+
497
+ // List all joined rooms
498
+ const rooms = this.$rooms // ['notifications', 'chat:lobby']
499
+ ```
500
+
501
+ ### Typed vs Untyped Comparison
502
+
503
+ | Feature | Typed `$room(Class, id)` | Untyped `$room('id')` |
504
+ |---|---|---|
505
+ | Lifecycle hooks | onJoin, onLeave, onCreate, onDestroy | None |
506
+ | Private metadata | `meta` (server-only) | None |
507
+ | Custom methods | addMessage(), setPassword(), etc. | None |
508
+ | Join rejection | `return false` in onJoin | Not possible |
509
+ | Type safety | Full (state, events, methods) | None |
510
+ | Server-only join | Enforced | Client can join directly |
511
+ | Max members | `$options.maxMembers` | No limit |
512
+ | Auto-discovery | Yes (rooms/ directory) | N/A |
513
+ | Use case | Complex room logic | Simple pub/sub |
514
+
515
+ **Both can be used in the same component:**
516
+
517
+ ```typescript
518
+ // Typed room for chat
519
+ const chat = this.$room(ChatRoom, 'lobby')
520
+ chat.addMessage('user', 'hello')
521
+
522
+ // Untyped room for presence
523
+ this.$room('presence').join()
524
+ this.$room('presence').emit('online', { user: 'John' })
525
+ ```
526
+
527
+ ---
528
+
529
+ ## Complete Example: Chat with Password Rooms
530
+
531
+ ### Server — Room Classes
532
+
533
+ ```
534
+ app/server/live/rooms/
535
+ ChatRoom.ts — Chat room with messages, password, lifecycle
536
+ DirectoryRoom.ts — Shared room directory for room discovery
537
+ ```
538
+
539
+ ### Server — Component
540
+
541
+ ```typescript
542
+ // app/server/live/LiveRoomChat.ts
543
+ import { LiveComponent, type FluxStackWebSocket } from '@core/types/types'
544
+ import { ChatRoom } from './rooms/ChatRoom'
545
+ import { DirectoryRoom } from './rooms/DirectoryRoom'
546
+ import type { ChatMessage } from './rooms/ChatRoom'
547
+ import type { DirectoryEntry } from './rooms/DirectoryRoom'
548
+
549
+ export class LiveRoomChat extends LiveComponent<typeof LiveRoomChat.defaultState> {
550
+ static componentName = 'LiveRoomChat'
551
+ static publicActions = ['createRoom', 'joinRoom', 'leaveRoom', 'switchRoom', 'sendMessage', 'setUsername'] as const
552
+ static defaultState = {
553
+ username: '',
554
+ activeRoom: null as string | null,
555
+ rooms: [] as { id: string; name: string; isPrivate: boolean }[],
556
+ messages: {} as Record<string, ChatMessage[]>,
557
+ customRooms: [] as DirectoryEntry[]
558
+ }
559
+
560
+ private roomListeners = new Map<string, (() => void)[]>()
561
+ private directoryUnsubs: (() => void)[] = []
562
+
563
+ constructor(initialState: Partial<typeof LiveRoomChat.defaultState>, ws: FluxStackWebSocket, options?: { room?: string; userId?: string }) {
564
+ super(initialState, ws, options)
565
+
566
+ // Auto-join directory for room discovery
567
+ const dir = this.$room(DirectoryRoom, 'main')
568
+ dir.join()
569
+ this.setState({ customRooms: dir.state.rooms || [] })
570
+
571
+ const unsubAdd = dir.on('room:added', (entry: DirectoryEntry) => {
572
+ const current = this.state.customRooms.filter(r => r.id !== entry.id)
573
+ this.setState({ customRooms: [...current, entry] })
574
+ })
575
+ const unsubRemove = dir.on('room:removed', (data: { id: string }) => {
576
+ this.setState({ customRooms: this.state.customRooms.filter(r => r.id !== data.id) })
169
577
  })
578
+ this.directoryUnsubs = [unsubAdd, unsubRemove]
170
579
  }
171
580
 
172
- // Start typing indicator
173
- async startTyping() {
174
- const roomId = this.state.activeRoom
175
- if (!roomId) return { success: false }
581
+ async createRoom(payload: { roomId: string; roomName: string; password?: string }) {
582
+ const room = this.$room(ChatRoom, payload.roomId)
583
+ const result = room.join()
584
+ if ('rejected' in result && result.rejected) return { success: false, error: result.reason }
585
+
586
+ if (payload.password) room.setPassword(payload.password)
587
+ room.meta.createdBy = this.state.username || 'Anonymous'
176
588
 
177
- this.$room(roomId).emit('user:typing', {
178
- user: this.state.username,
179
- typing: true
589
+ // Register in directory so all users see it
590
+ this.$room(DirectoryRoom, 'main').addRoom({
591
+ id: payload.roomId, name: payload.roomName,
592
+ isPrivate: !!payload.password, createdBy: this.state.username || 'Anonymous'
180
593
  })
181
594
 
182
- return { success: true }
595
+ const unsub = room.on('chat:message', (msg: ChatMessage) => {
596
+ const msgs = this.state.messages[payload.roomId] || []
597
+ this.setState({ messages: { ...this.state.messages, [payload.roomId]: [...msgs, msg].slice(-100) } })
598
+ })
599
+ this.roomListeners.set(payload.roomId, [unsub])
600
+
601
+ this.setState({
602
+ activeRoom: payload.roomId,
603
+ rooms: [...this.state.rooms.filter(r => r.id !== payload.roomId), { id: payload.roomId, name: payload.roomName, isPrivate: !!payload.password }],
604
+ messages: { ...this.state.messages, [payload.roomId]: [] }
605
+ })
606
+ return { success: true, roomId: payload.roomId }
183
607
  }
184
608
 
185
- // Leave room
186
- async leaveRoom(payload: { roomId: string }) {
187
- const { roomId } = payload
609
+ async joinRoom(payload: { roomId: string; roomName?: string; password?: string }) {
610
+ if (this.roomListeners.has(payload.roomId)) {
611
+ this.state.activeRoom = payload.roomId
612
+ return { success: true, roomId: payload.roomId }
613
+ }
188
614
 
189
- this.$room(roomId).leave()
615
+ const room = this.$room(ChatRoom, payload.roomId)
616
+ const result = room.join({ password: payload.password })
617
+ if ('rejected' in result && result.rejected) return { success: false, error: 'Senha incorreta' }
190
618
 
191
- const { [roomId]: _, ...restMessages } = this.state.messages
192
- const { [roomId]: __, ...restTyping } = this.state.typingUsers
619
+ const unsub = room.on('chat:message', (msg: ChatMessage) => {
620
+ const msgs = this.state.messages[payload.roomId] || []
621
+ this.setState({ messages: { ...this.state.messages, [payload.roomId]: [...msgs, msg].slice(-100) } })
622
+ })
623
+ this.roomListeners.set(payload.roomId, [unsub])
193
624
 
194
625
  this.setState({
195
- rooms: this.state.rooms.filter(r => r.id !== roomId),
196
- activeRoom: this.state.activeRoom === roomId ? null : this.state.activeRoom,
197
- messages: restMessages,
198
- typingUsers: restTyping
626
+ activeRoom: payload.roomId,
627
+ rooms: [...this.state.rooms.filter(r => r.id !== payload.roomId), { id: payload.roomId, name: payload.roomName || payload.roomId, isPrivate: room.state.isPrivate }],
628
+ messages: { ...this.state.messages, [payload.roomId]: room.state.messages || [] }
199
629
  })
630
+ return { success: true, roomId: payload.roomId }
631
+ }
200
632
 
201
- return { success: true }
633
+ async sendMessage(payload: { text: string }) {
634
+ const roomId = this.state.activeRoom
635
+ if (!roomId) throw new Error('No active room')
636
+ // Custom method on ChatRoom — emits 'chat:message' event,
637
+ // which the handler above catches and updates component state
638
+ const room = this.$room(ChatRoom, roomId)
639
+ const message = room.addMessage(this.state.username || 'Anonymous', payload.text.trim())
640
+ return { success: true, message }
641
+ }
642
+
643
+ destroy() {
644
+ for (const fns of this.roomListeners.values()) fns.forEach(fn => fn())
645
+ this.roomListeners.clear()
646
+ this.directoryUnsubs.forEach(fn => fn())
647
+ super.destroy()
202
648
  }
203
649
  }
204
650
  ```
205
651
 
206
- ### Frontend Component
652
+ ### Frontend
207
653
 
208
- ```typescript
654
+ ```tsx
209
655
  // app/client/src/live/RoomChatDemo.tsx
210
656
  import { Live } from '@/core/client'
211
- import { LiveRoomChat, defaultState } from '@server/live/LiveRoomChat'
657
+ import { LiveRoomChat } from '@server/live/LiveRoomChat'
212
658
 
213
659
  export function RoomChatDemo() {
214
- const [text, setText] = useState('')
215
-
216
- // Connect to Live Component
217
660
  const chat = Live.use(LiveRoomChat, {
218
- initialState: { ...defaultState, username: 'User123' }
661
+ initialState: { ...LiveRoomChat.defaultState, username: 'User123' }
219
662
  })
220
663
 
221
- // State comes directly from server
222
- const activeRoom = chat.$state.activeRoom
223
- const messages = activeRoom ? (chat.$state.messages[activeRoom] || []) : []
224
- const typingUsers = activeRoom ? (chat.$state.typingUsers[activeRoom] || []) : []
664
+ // Joined rooms from component state
665
+ const joinedRoomIds = chat.$state.rooms.map(r => r.id)
225
666
 
226
- // Join room
227
- const handleJoinRoom = async (roomId: string) => {
228
- await chat.joinRoom({ roomId, roomName: roomId })
229
- }
667
+ // Custom rooms from shared directory (visible to ALL users)
668
+ const customRooms = chat.$state.customRooms || []
669
+
670
+ // Combine default + custom rooms for sidebar
671
+ const allRooms = [
672
+ ...DEFAULT_ROOMS,
673
+ ...customRooms.filter(r => !DEFAULT_ROOMS.some(d => d.id === r.id))
674
+ ]
230
675
 
231
- // Send message
232
- const handleSend = async () => {
233
- if (!text.trim()) return
234
- await chat.sendMessage({ text })
235
- setText('')
676
+ // Join with password handling
677
+ const handleJoinRoom = async (roomId: string, roomName: string, isPrivate?: boolean) => {
678
+ if (joinedRoomIds.includes(roomId)) {
679
+ await chat.switchRoom({ roomId })
680
+ return
681
+ }
682
+ if (isPrivate) {
683
+ showPasswordPrompt(roomId, roomName)
684
+ return
685
+ }
686
+ const result = await chat.joinRoom({ roomId, roomName })
687
+ if (result && !result.success) {
688
+ showPasswordPrompt(roomId, roomName) // Might be password-protected
689
+ }
236
690
  }
237
691
 
238
- return (
239
- <div>
240
- {/* Room list */}
241
- <div>
242
- {['geral', 'tech', 'random'].map(roomId => (
243
- <button key={roomId} onClick={() => handleJoinRoom(roomId)}>
244
- {roomId} {chat.$rooms.includes(roomId) && '(joined)'}
245
- </button>
246
- ))}
247
- </div>
248
-
249
- {/* Messages */}
250
- <div>
251
- {messages.map(msg => (
252
- <div key={msg.id}>
253
- <strong>{msg.user}:</strong> {msg.text}
254
- </div>
255
- ))}
256
- </div>
257
-
258
- {/* Typing indicator */}
259
- {typingUsers.length > 0 && (
260
- <div>{typingUsers.join(', ')} typing...</div>
261
- )}
262
-
263
- {/* Input */}
264
- <input
265
- value={text}
266
- onChange={e => {
267
- setText(e.target.value)
268
- chat.startTyping()
269
- }}
270
- onKeyDown={e => e.key === 'Enter' && handleSend()}
271
- />
272
- <button onClick={handleSend}>Send</button>
273
- </div>
274
- )
692
+ // Create room with optional password
693
+ const handleCreateRoom = async (name: string, password?: string) => {
694
+ const roomId = name.toLowerCase().replace(/\s+/g, '-').replace(/[^a-z0-9-]/g, '')
695
+ await chat.createRoom({ roomId, roomName: name, password })
696
+ }
275
697
  }
276
698
  ```
277
699
 
278
- ## HTTP API Integration
279
-
280
- Send messages from external systems via REST API:
700
+ ---
281
701
 
282
- ### Routes
702
+ ## Event Flow Diagrams
283
703
 
284
- ```typescript
285
- // app/server/routes/room.routes.ts
286
- import { Elysia, t } from 'elysia'
287
- import { liveRoomManager } from '@core/server/live/LiveRoomManager'
704
+ ### Message Flow (Typed Room)
288
705
 
289
- export const roomRoutes = new Elysia({ prefix: '/rooms' })
706
+ ```
707
+ Frontend A Server Frontend B
708
+ │ │ │
709
+ │ sendMessage() │ │
710
+ │─────────────────────>│ │
711
+ │ │ │
712
+ │ │ ChatRoom.addMessage()
713
+ │ │ ├─ setState(messages) // room state updated
714
+ │ │ └─ emit('chat:message') // event to all members
715
+ │ │ │
716
+ │ │ A's on('chat:message')
717
+ │ │ └─ component.setState()
718
+ │ <state update> │ │
719
+ │<─────────────────────│ │
720
+ │ │ │
721
+ │ │ B's on('chat:message')
722
+ │ │ └─ component.setState()
723
+ │ │ <state update> │
724
+ │ │─────────────────────>│
725
+ ```
290
726
 
291
- // Send message to room
292
- .post('/:roomId/messages', ({ params, body }) => {
293
- const message = {
294
- id: `api-${Date.now()}`,
295
- user: body.user || 'API Bot',
296
- text: body.text,
297
- timestamp: Date.now()
298
- }
727
+ ### Password Join Flow
299
728
 
300
- const notified = liveRoomManager.emitToRoom(
301
- params.roomId,
302
- 'message:new',
303
- message
304
- )
305
-
306
- return { success: true, message, notified }
307
- }, {
308
- params: t.Object({ roomId: t.String() }),
309
- body: t.Object({
310
- user: t.Optional(t.String()),
311
- text: t.String()
312
- })
313
- })
729
+ ```
730
+ Frontend Component ChatRoom
731
+ │ │ │
732
+ │ joinRoom(password) │ │
733
+ │─────────────────────>│ │
734
+ │ │ │
735
+ │ │ room.join({password})
736
+ │ │─────────────────────>│
737
+ │ │ │
738
+ │ │ onJoin(ctx)
739
+ │ │ ctx.payload.password
740
+ │ │ vs meta.password
741
+ │ │ │
742
+ │ │ {rejected: true} │ (wrong password)
743
+ │ │<─────────────────────│
744
+ │ │ │
745
+ │ {success: false} │ │
746
+ │<─────────────────────│ │
747
+ │ │ │
748
+ │ Show error toast │ │
749
+ ```
314
750
 
315
- // Emit custom event
316
- .post('/:roomId/emit', ({ params, body }) => {
317
- const notified = liveRoomManager.emitToRoom(
318
- params.roomId,
319
- body.event,
320
- body.data
321
- )
322
- return { success: true, notified }
323
- }, {
324
- params: t.Object({ roomId: t.String() }),
325
- body: t.Object({
326
- event: t.String(),
327
- data: t.Any()
328
- })
329
- })
751
+ ### Room Discovery Flow
330
752
 
331
- // Get stats
332
- .get('/stats', () => liveRoomManager.getStats())
333
753
  ```
754
+ User A DirectoryRoom User B
755
+ │ │ │
756
+ │ createRoom() │ │
757
+ │ dir.addRoom(entry) │ │
758
+ │─────────────────────>│ │
759
+ │ │ │
760
+ │ │ emit('room:added') │
761
+ │ │─────────────────────>│
762
+ │ │ │
763
+ │ │ B's on('room:added')│
764
+ │ │ setState(customRooms)
765
+ │ │ │
766
+ │ │ New room appears in B's sidebar
767
+ ```
768
+
769
+ ---
770
+
771
+ ## HTTP API Integration
334
772
 
335
- ### Usage Examples
773
+ Send messages from external systems via REST API:
336
774
 
337
775
  ```bash
338
- # Send message via curl
776
+ # Send message to room
339
777
  curl -X POST http://localhost:3000/api/rooms/geral/messages \
340
778
  -H "Content-Type: application/json" \
341
779
  -d '{"user": "Webhook Bot", "text": "New deployment completed!"}'
@@ -343,140 +781,100 @@ curl -X POST http://localhost:3000/api/rooms/geral/messages \
343
781
  # Emit custom event
344
782
  curl -X POST http://localhost:3000/api/rooms/tech/emit \
345
783
  -H "Content-Type: application/json" \
346
- -d '{"event": "notification", "data": {"type": "alert", "message": "Server restarted"}}'
784
+ -d '{"event": "notification", "data": {"type": "alert"}}'
347
785
 
348
786
  # Get room stats
349
787
  curl http://localhost:3000/api/rooms/stats
350
788
  ```
351
789
 
352
- ## Event Flow Diagram
353
-
354
- ```
355
- ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
356
- │ Frontend A │ │ Server │ │ Frontend B │
357
- └──────┬──────┘ └──────┬──────┘ └──────┬──────┘
358
- │ │ │
359
- │ sendMessage() │ │
360
- │──────────────────>│ │
361
- │ │ │
362
- │ │ 1. setState() │
363
- │ │ (sync to A) │
364
- │<──────────────────│ │
365
- │ │ │
366
- │ │ 2. $room.emit() │
367
- │ │ (to others) │
368
- │ │ │
369
- │ │ 3. B's handler │
370
- │ │ receives event │
371
- │ │ │
372
- │ │ 4. B's setState() │
373
- │ │ (sync to B) │
374
- │ │──────────────────>│
375
- │ │ │
376
- ```
790
+ ---
377
791
 
378
792
  ## Room Manager API
379
793
 
380
- Direct access to room manager for advanced use cases:
794
+ Direct access for advanced use cases:
381
795
 
382
796
  ```typescript
383
797
  import { liveRoomManager } from '@core/server/live/LiveRoomManager'
384
798
 
385
- // Join component to room
386
- liveRoomManager.joinRoom(componentId, roomId, ws, initialState)
387
-
388
- // Leave room
389
- liveRoomManager.leaveRoom(componentId, roomId)
390
-
391
- // Emit event to room
392
- const count = liveRoomManager.emitToRoom(roomId, event, data, excludeComponentId)
393
-
394
- // Update room state
395
- liveRoomManager.setRoomState(roomId, updates, excludeComponentId)
396
-
397
- // Get room state
398
- const state = liveRoomManager.getRoomState(roomId)
399
-
400
- // Check membership
401
- const isIn = liveRoomManager.isInRoom(componentId, roomId)
402
-
403
- // Get component's rooms
404
- const rooms = liveRoomManager.getComponentRooms(componentId)
405
-
406
- // Cleanup on disconnect
799
+ // Membership
800
+ liveRoomManager.joinRoom(componentId, roomId, ws, initialState, options, joinContext)
801
+ liveRoomManager.leaveRoom(componentId, roomId, leaveReason)
407
802
  liveRoomManager.cleanupComponent(componentId)
408
803
 
409
- // Get statistics
410
- const stats = liveRoomManager.getStats()
804
+ // Events & State
805
+ liveRoomManager.emitToRoom(roomId, event, data, excludeComponentId)
806
+ liveRoomManager.setRoomState(roomId, updates, excludeComponentId)
807
+ liveRoomManager.getRoomState(roomId)
808
+
809
+ // Queries
810
+ liveRoomManager.isInRoom(componentId, roomId)
811
+ liveRoomManager.getComponentRooms(componentId)
812
+ liveRoomManager.getMemberCount(roomId)
813
+ liveRoomManager.getRoomInstance(roomId) // Get LiveRoom instance (typed rooms only)
814
+ liveRoomManager.getStats()
411
815
  ```
412
816
 
413
- ## Room Event Bus
817
+ ---
414
818
 
415
- For server-side event handling:
819
+ ## Best Practices
416
820
 
417
- ```typescript
418
- import { roomEvents } from '@core/server/live/RoomEventBus'
419
-
420
- // Subscribe to events
421
- const unsubscribe = roomEvents.on(
422
- 'room', // type
423
- 'geral', // roomId
424
- 'message', // event
425
- componentId, // subscriber
426
- (data) => { // handler
427
- console.log('Received:', data)
428
- }
429
- )
821
+ **DO:**
822
+ - Use typed rooms for rooms with business logic (auth, validation, custom methods)
823
+ - Use `meta` for sensitive data (passwords, tokens, internal flags)
824
+ - Use `onJoin` to validate/reject joins
825
+ - Register event handlers with `room.on()` in joinRoom actions
826
+ - Use the DirectoryRoom pattern when users create rooms dynamically
827
+ - Clean up listeners in `destroy()`
828
+ - Load existing room state on join: `room.state.messages`
430
829
 
431
- // Emit events
432
- roomEvents.emit('room', 'geral', 'message', { text: 'Hello' }, excludeId)
830
+ **DON'T:**
831
+ - Store sensitive data in `state` (it's synced to all clients)
832
+ - Forget to check `result.rejected` after `room.join()`
833
+ - Update component state directly in `sendMessage` when using room events (causes duplicates)
834
+ - Rely on untyped rooms for rooms with security requirements
835
+ - Skip the DirectoryRoom when users need to discover dynamically-created rooms
433
836
 
434
- // Cleanup
435
- roomEvents.unsubscribeAll(componentId)
436
- roomEvents.clearRoom('room', 'geral')
837
+ **Common Pitfall — Duplicate Messages:**
838
+ ```typescript
839
+ // WRONG: event handler + manual setState = duplicate
840
+ async sendMessage(payload: { text: string }) {
841
+ const room = this.$room(ChatRoom, roomId)
842
+ room.addMessage(user, text) // emits 'chat:message' → handler updates state
843
+ // DON'T also do: this.setState({ messages: [...msgs, msg] })
844
+ }
437
845
 
438
- // Stats
439
- const stats = roomEvents.getStats()
846
+ // CORRECT: let the event handler do all the work
847
+ async sendMessage(payload: { text: string }) {
848
+ const room = this.$room(ChatRoom, roomId)
849
+ room.addMessage(user, text) // event handler catches it for ALL members
850
+ }
440
851
  ```
441
852
 
442
- ## Use Cases
443
-
444
- | Use Case | Description |
445
- |----------|-------------|
446
- | **Chat** | Multi-room chat with typing indicators |
447
- | **Notifications** | Send alerts to specific groups |
448
- | **Collaboration** | Real-time document editing |
449
- | **Gaming** | Multiplayer game state sync |
450
- | **Dashboards** | Live metrics and alerts |
451
- | **Webhooks** | External events to rooms |
452
- | **Presence** | Online/offline status |
853
+ ---
453
854
 
454
- ## Best Practices
455
-
456
- **DO:**
457
- - Use `setState()` to sync state to your own frontend
458
- - Use `$room.emit()` to notify other components
459
- - Register handlers with `$room.on()` in `joinRoom`
460
- - Clean up with `$room.leave()` when leaving
461
- - Use HTTP API for external integrations
855
+ ## Files Reference
462
856
 
463
- **DON'T:**
464
- - Rely on `$room.emit()` to update your own frontend
465
- - Forget to handle events from other users
466
- - Store non-serializable data in room state
467
- - Skip error handling in event handlers
857
+ | File | Purpose |
858
+ |------|---------|
859
+ | `app/server/live/rooms/ChatRoom.ts` | Example typed room with password support |
860
+ | `app/server/live/rooms/DirectoryRoom.ts` | Shared room directory for discovery |
861
+ | `app/server/live/LiveRoomChat.ts` | Chat component using typed rooms |
862
+ | `app/client/src/live/RoomChatDemo.tsx` | Frontend React component |
863
+ | `core/server/live/websocket-plugin.ts` | Room auto-discovery and LiveServer setup |
468
864
 
469
- ## Files Reference
865
+ ### @fluxstack/live Package Files
470
866
 
471
867
  | File | Purpose |
472
868
  |------|---------|
473
- | `core/server/live/LiveRoomManager.ts` | Room membership and broadcasting |
474
- | `core/server/live/RoomEventBus.ts` | Server-side event pub/sub |
475
- | `core/types/types.ts` | `$room` and `$rooms` implementation |
476
- | `app/server/routes/room.routes.ts` | HTTP API for rooms |
869
+ | `packages/core/src/rooms/LiveRoom.ts` | LiveRoom base class |
870
+ | `packages/core/src/rooms/RoomRegistry.ts` | Room type class mapping |
871
+ | `packages/core/src/rooms/LiveRoomManager.ts` | Room membership, broadcasting, lifecycle |
872
+ | `packages/core/src/component/managers/ComponentRoomProxy.ts` | `$room` proxy (typed + untyped) |
873
+ | `packages/core/src/server/LiveServer.ts` | Server-side join enforcement |
477
874
 
478
875
  ## Related
479
876
 
480
877
  - [Live Components](./live-components.md) - Base component system
878
+ - [Live Auth](./live-auth.md) - Authentication for Live Components
481
879
  - [Routes with Eden Treaty](./routes-eden.md) - HTTP API patterns
482
880
  - [Type Safety](../patterns/type-safety.md) - TypeScript patterns