create-fluxstack 1.15.0 → 1.17.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 (142) hide show
  1. package/CHANGELOG.md +80 -0
  2. package/LLMD/INDEX.md +4 -3
  3. package/LLMD/resources/live-binary-delta.md +507 -0
  4. package/LLMD/resources/live-components.md +1 -0
  5. package/LLMD/resources/live-rooms.md +731 -333
  6. package/app/client/src/App.tsx +23 -14
  7. package/app/client/src/components/AppLayout.tsx +4 -4
  8. package/app/client/src/live/AuthDemo.tsx +4 -4
  9. package/app/client/src/live/PingPongDemo.tsx +199 -0
  10. package/app/client/src/live/RoomChatDemo.tsx +187 -22
  11. package/app/client/src/live/SharedCounterDemo.tsx +142 -0
  12. package/app/server/live/LivePingPong.ts +61 -0
  13. package/app/server/live/LiveRoomChat.ts +106 -38
  14. package/app/server/live/LiveSharedCounter.ts +73 -0
  15. package/app/server/live/rooms/ChatRoom.ts +68 -0
  16. package/app/server/live/rooms/CounterRoom.ts +51 -0
  17. package/app/server/live/rooms/DirectoryRoom.ts +42 -0
  18. package/app/server/live/rooms/PingRoom.ts +40 -0
  19. package/core/build/bundler.ts +40 -26
  20. package/core/build/flux-plugins-generator.ts +325 -325
  21. package/core/build/index.ts +92 -21
  22. package/core/cli/command-registry.ts +44 -46
  23. package/core/cli/commands/build.ts +11 -6
  24. package/core/cli/commands/create.ts +7 -5
  25. package/core/cli/commands/dev.ts +6 -5
  26. package/core/cli/commands/help.ts +3 -2
  27. package/core/cli/commands/make-plugin.ts +8 -7
  28. package/core/cli/commands/plugin-add.ts +60 -43
  29. package/core/cli/commands/plugin-deps.ts +73 -57
  30. package/core/cli/commands/plugin-list.ts +44 -41
  31. package/core/cli/commands/plugin-remove.ts +33 -22
  32. package/core/cli/generators/component.ts +770 -769
  33. package/core/cli/generators/controller.ts +9 -8
  34. package/core/cli/generators/index.ts +148 -146
  35. package/core/cli/generators/interactive.ts +228 -227
  36. package/core/cli/generators/plugin.ts +11 -10
  37. package/core/cli/generators/prompts.ts +83 -82
  38. package/core/cli/generators/route.ts +7 -6
  39. package/core/cli/generators/service.ts +10 -9
  40. package/core/cli/generators/template-engine.ts +2 -1
  41. package/core/cli/generators/types.ts +7 -7
  42. package/core/cli/generators/utils.ts +191 -191
  43. package/core/cli/index.ts +9 -8
  44. package/core/cli/plugin-discovery.ts +2 -2
  45. package/core/client/hooks/useAuth.ts +48 -48
  46. package/core/client/index.ts +0 -16
  47. package/core/client/standalone.ts +18 -17
  48. package/core/client/state/createStore.ts +192 -192
  49. package/core/client/state/index.ts +14 -14
  50. package/core/config/index.ts +1 -0
  51. package/core/framework/client.ts +131 -131
  52. package/core/framework/index.ts +7 -7
  53. package/core/framework/server.ts +72 -112
  54. package/core/framework/types.ts +2 -2
  55. package/core/plugins/built-in/live-components/commands/create-live-component.ts +6 -3
  56. package/core/plugins/built-in/monitoring/index.ts +110 -68
  57. package/core/plugins/built-in/static/index.ts +2 -2
  58. package/core/plugins/built-in/swagger/index.ts +9 -9
  59. package/core/plugins/built-in/vite/index.ts +3 -3
  60. package/core/plugins/built-in/vite/vite-dev.ts +3 -3
  61. package/core/plugins/config.ts +50 -47
  62. package/core/plugins/discovery.ts +10 -4
  63. package/core/plugins/executor.ts +2 -2
  64. package/core/plugins/index.ts +206 -203
  65. package/core/plugins/manager.ts +21 -20
  66. package/core/plugins/registry.ts +76 -12
  67. package/core/plugins/types.ts +14 -14
  68. package/core/server/framework.ts +3 -189
  69. package/core/server/live/auto-generated-components.ts +11 -35
  70. package/core/server/live/index.ts +41 -36
  71. package/core/server/live/websocket-plugin.ts +48 -3
  72. package/core/server/middleware/elysia-helpers.ts +16 -15
  73. package/core/server/middleware/errorHandling.ts +14 -14
  74. package/core/server/middleware/index.ts +31 -31
  75. package/core/server/plugins/database.ts +181 -180
  76. package/core/server/plugins/static-files-plugin.ts +4 -3
  77. package/core/server/plugins/swagger.ts +11 -8
  78. package/core/server/rooms/RoomBroadcaster.ts +11 -10
  79. package/core/server/rooms/RoomSystem.ts +14 -11
  80. package/core/server/services/BaseService.ts +7 -7
  81. package/core/server/services/ServiceContainer.ts +5 -5
  82. package/core/server/services/index.ts +8 -8
  83. package/core/templates/create-project.ts +28 -27
  84. package/core/testing/index.ts +9 -9
  85. package/core/testing/setup.ts +73 -73
  86. package/core/types/api.ts +168 -168
  87. package/core/types/config.ts +5 -5
  88. package/core/types/index.ts +1 -1
  89. package/core/types/plugin.ts +2 -2
  90. package/core/types/types.ts +3 -3
  91. package/core/utils/build-logger.ts +324 -324
  92. package/core/utils/config-schema.ts +480 -480
  93. package/core/utils/env.ts +10 -8
  94. package/core/utils/errors/codes.ts +114 -114
  95. package/core/utils/errors/handlers.ts +30 -20
  96. package/core/utils/errors/index.ts +54 -46
  97. package/core/utils/errors/middleware.ts +113 -113
  98. package/core/utils/helpers.ts +19 -16
  99. package/core/utils/logger/colors.ts +114 -114
  100. package/core/utils/logger/config.ts +2 -2
  101. package/core/utils/logger/formatter.ts +82 -82
  102. package/core/utils/logger/group-logger.ts +101 -101
  103. package/core/utils/logger/index.ts +13 -3
  104. package/core/utils/logger/startup-banner.ts +2 -2
  105. package/core/utils/logger/winston-logger.ts +152 -152
  106. package/core/utils/monitoring/index.ts +211 -211
  107. package/core/utils/sync-version.ts +67 -66
  108. package/core/utils/version.ts +1 -1
  109. package/package.json +11 -6
  110. package/playwright-report/index.html +85 -0
  111. package/playwright.config.ts +31 -0
  112. package/plugins/crypto-auth/client/CryptoAuthClient.ts +302 -302
  113. package/plugins/crypto-auth/client/components/index.ts +11 -11
  114. package/plugins/crypto-auth/client/index.ts +11 -11
  115. package/plugins/crypto-auth/package.json +65 -65
  116. package/plugins/crypto-auth/server/CryptoAuthService.ts +185 -185
  117. package/plugins/crypto-auth/server/middlewares/cryptoAuthAdmin.ts +6 -5
  118. package/plugins/crypto-auth/server/middlewares/cryptoAuthPermissions.ts +6 -5
  119. package/plugins/crypto-auth/server/middlewares/cryptoAuthRequired.ts +3 -3
  120. package/plugins/crypto-auth/server/middlewares/index.ts +22 -22
  121. package/plugins/crypto-auth/server/middlewares.ts +19 -19
  122. package/tsconfig.json +4 -1
  123. package/vite.config.ts +13 -0
  124. package/app/client/.live-stubs/LiveAdminPanel.js +0 -5
  125. package/app/client/.live-stubs/LiveChat.js +0 -7
  126. package/app/client/.live-stubs/LiveCounter.js +0 -9
  127. package/app/client/.live-stubs/LiveForm.js +0 -11
  128. package/app/client/.live-stubs/LiveLocalCounter.js +0 -8
  129. package/app/client/.live-stubs/LiveRoomChat.js +0 -10
  130. package/app/client/.live-stubs/LiveTodoList.js +0 -9
  131. package/app/client/.live-stubs/LiveUpload.js +0 -15
  132. package/app/client/src/live/ChatDemo.tsx +0 -107
  133. package/app/client/src/live/LiveDebuggerPanel.tsx +0 -779
  134. package/app/client/src/live/TodoListDemo.tsx +0 -158
  135. package/app/server/live/LiveChat.ts +0 -78
  136. package/app/server/live/LiveTodoList.ts +0 -110
  137. package/app/server/live/register-components.ts +0 -19
  138. package/core/build/live-components-generator.ts +0 -312
  139. package/core/client/components/LiveDebugger.tsx +0 -1324
  140. package/core/live/ComponentRegistry.ts +0 -403
  141. package/core/live/types.ts +0 -241
  142. package/workspace.json +0 -6
@@ -0,0 +1,142 @@
1
+ // SharedCounterDemo - Contador compartilhado entre todas as abas
2
+ //
3
+ // Abra em varias abas - todos veem o mesmo valor!
4
+ // Usa o sistema de LiveRoom tipado com CounterRoom.
5
+ // Demo de client-side room events via $room().on()
6
+
7
+ import { useMemo, useState, useEffect, useRef } from 'react'
8
+ import { Live } from '@/core/client'
9
+ import { LiveSharedCounter } from '@server/live/LiveSharedCounter'
10
+ import type { CounterRoom } from '@server/live/rooms/CounterRoom'
11
+
12
+ interface FloatingEvent {
13
+ id: number
14
+ text: string
15
+ color: string
16
+ }
17
+
18
+ export function SharedCounterDemo() {
19
+ const username = useMemo(() => {
20
+ const adj = ['Happy', 'Cool', 'Fast', 'Smart', 'Brave'][Math.floor(Math.random() * 5)]
21
+ const noun = ['Panda', 'Tiger', 'Eagle', 'Wolf', 'Bear'][Math.floor(Math.random() * 5)]
22
+ return `${adj}${noun}${Math.floor(Math.random() * 100)}`
23
+ }, [])
24
+
25
+ const counter = Live.use(LiveSharedCounter, {
26
+ initialState: { ...LiveSharedCounter.defaultState, username }
27
+ })
28
+
29
+ // Client-side room events — floating animation
30
+ const [floats, setFloats] = useState<FloatingEvent[]>([])
31
+ const floatIdRef = useRef(0)
32
+
33
+ useEffect(() => {
34
+ const unsub = counter.$room<CounterRoom>('counter:global').on('counter:updated', (data) => {
35
+ const id = ++floatIdRef.current
36
+ const isReset = data.count === 0
37
+ const text = isReset ? '0' : data.count > 0 ? `+${data.count}` : `${data.count}`
38
+ const color = isReset ? 'text-yellow-400' : data.count > 0 ? 'text-emerald-400' : 'text-red-400'
39
+
40
+ setFloats(prev => [...prev, { id, text: `${text} (${data.updatedBy})`, color }])
41
+ setTimeout(() => setFloats(prev => prev.filter(f => f.id !== id)), 2000)
42
+ })
43
+ return unsub
44
+ }, [])
45
+
46
+ const count = counter.$state.count
47
+ const onlineCount = counter.$state.onlineCount
48
+ const lastUpdatedBy = counter.$state.lastUpdatedBy
49
+
50
+ return (
51
+ <div className="flex flex-col items-center gap-8 w-full max-w-md mx-auto">
52
+ {/* Header */}
53
+ <div className="text-center">
54
+ <h2 className="text-2xl font-bold text-white mb-2">Contador Compartilhado</h2>
55
+ <p className="text-sm text-gray-400">Abra em varias abas - todos veem o mesmo valor!</p>
56
+ </div>
57
+
58
+ {/* Connection + Online */}
59
+ <div className="flex items-center gap-4">
60
+ <div className="flex items-center gap-2">
61
+ <div className={`w-2 h-2 rounded-full ${counter.$connected ? 'bg-emerald-400' : 'bg-red-400'}`} />
62
+ <span className="text-sm text-gray-400">{counter.$connected ? 'Conectado' : 'Desconectado'}</span>
63
+ </div>
64
+ <div className="flex items-center gap-2 px-3 py-1 rounded-full bg-white/5 border border-white/10">
65
+ <span className="text-sm text-gray-400">{onlineCount} online</span>
66
+ </div>
67
+ <div className="px-3 py-1 rounded-full bg-purple-500/10 border border-purple-500/20">
68
+ <span className="text-xs text-purple-300">{username}</span>
69
+ </div>
70
+ </div>
71
+
72
+ {/* Counter Display */}
73
+ <div className="relative">
74
+ <div className="absolute inset-0 bg-purple-500/20 rounded-full blur-3xl" />
75
+ <div className="relative bg-gray-800/50 border border-white/10 rounded-3xl px-16 py-10 flex flex-col items-center">
76
+ <span className={`text-7xl font-black tabular-nums transition-colors ${
77
+ count > 0 ? 'text-emerald-400' : count < 0 ? 'text-red-400' : 'text-white'
78
+ }`}>
79
+ {count}
80
+ </span>
81
+ {lastUpdatedBy && (
82
+ <span className="text-xs text-gray-500 mt-3">
83
+ Ultimo: {lastUpdatedBy}
84
+ </span>
85
+ )}
86
+ </div>
87
+
88
+ {/* Floating room events */}
89
+ {floats.map(f => (
90
+ <span
91
+ key={f.id}
92
+ className={`absolute left-1/2 -translate-x-1/2 top-0 ${f.color} text-sm font-bold pointer-events-none animate-float-up`}
93
+ >
94
+ {f.text}
95
+ </span>
96
+ ))}
97
+ </div>
98
+
99
+ {/* Buttons */}
100
+ <div className="flex items-center gap-3">
101
+ <button
102
+ onClick={() => counter.decrement()}
103
+ disabled={counter.$loading}
104
+ className="w-14 h-14 rounded-2xl bg-red-500/20 border border-red-500/30 text-red-300 text-2xl font-bold hover:bg-red-500/30 active:scale-95 disabled:opacity-50 transition-all"
105
+ >
106
+ -
107
+ </button>
108
+ <button
109
+ onClick={() => counter.reset()}
110
+ disabled={counter.$loading}
111
+ className="px-6 h-14 rounded-2xl bg-white/10 border border-white/20 text-gray-300 text-sm font-medium hover:bg-white/20 active:scale-95 disabled:opacity-50 transition-all"
112
+ >
113
+ Reset
114
+ </button>
115
+ <button
116
+ onClick={() => counter.increment()}
117
+ disabled={counter.$loading}
118
+ className="w-14 h-14 rounded-2xl bg-emerald-500/20 border border-emerald-500/30 text-emerald-300 text-2xl font-bold hover:bg-emerald-500/30 active:scale-95 disabled:opacity-50 transition-all"
119
+ >
120
+ +
121
+ </button>
122
+ </div>
123
+
124
+ {/* Info */}
125
+ <div className="text-center text-xs text-gray-600 space-y-1">
126
+ <p>Powered by <code className="text-purple-400">LiveRoom</code> + <code className="text-purple-400">CounterRoom</code></p>
127
+ <p>Estado via component state + eventos via <code className="text-cyan-400">$room().on()</code></p>
128
+ </div>
129
+
130
+ {/* CSS animation */}
131
+ <style>{`
132
+ @keyframes float-up {
133
+ 0% { opacity: 1; transform: translateX(-50%) translateY(0); }
134
+ 100% { opacity: 0; transform: translateX(-50%) translateY(-60px); }
135
+ }
136
+ .animate-float-up {
137
+ animation: float-up 2s ease-out forwards;
138
+ }
139
+ `}</style>
140
+ </div>
141
+ )
142
+ }
@@ -0,0 +1,61 @@
1
+ // LivePingPong - Demo de Binary Codec (msgpack)
2
+ //
3
+ // Demonstra o wire format binario do sistema de rooms.
4
+ // Client envia ping, server responde pong via room event (msgpack).
5
+ // Round-trip time calculado no client.
6
+
7
+ import { LiveComponent, type FluxStackWebSocket } from '@core/types/types'
8
+ import { PingRoom } from './rooms/PingRoom'
9
+
10
+ // Componente Cliente (Ctrl+Click para navegar)
11
+ import type { PingPongDemo as _Client } from '@client/src/live/PingPongDemo'
12
+
13
+ export class LivePingPong extends LiveComponent<typeof LivePingPong.defaultState> {
14
+ static componentName = 'LivePingPong'
15
+ static publicActions = ['ping'] as const
16
+ static defaultState = {
17
+ username: '',
18
+ onlineCount: 0,
19
+ totalPings: 0,
20
+ lastPingBy: null as string | null,
21
+ }
22
+
23
+ private pongUnsub: (() => void) | null = null
24
+
25
+ constructor(
26
+ initialState: Partial<typeof LivePingPong.defaultState> = {},
27
+ ws: FluxStackWebSocket,
28
+ options?: { room?: string; userId?: string }
29
+ ) {
30
+ super(initialState, ws, options)
31
+
32
+ const room = this.$room(PingRoom, 'global')
33
+ room.join()
34
+
35
+ // Sync room state on join
36
+ this.setState({
37
+ onlineCount: room.state.onlineCount,
38
+ totalPings: room.state.totalPings,
39
+ lastPingBy: room.state.lastPingBy,
40
+ })
41
+
42
+ // Listen for pong events (binary msgpack)
43
+ this.pongUnsub = room.on('pong', (data) => {
44
+ this.setState({
45
+ totalPings: this.state.totalPings + 1,
46
+ lastPingBy: data.from,
47
+ })
48
+ })
49
+ }
50
+
51
+ async ping(payload: { seq: number }) {
52
+ const room = this.$room(PingRoom, 'global')
53
+ const total = room.ping(this.state.username || 'Anonymous', payload.seq)
54
+ return { success: true, total }
55
+ }
56
+
57
+ destroy() {
58
+ this.pongUnsub?.()
59
+ super.destroy()
60
+ }
61
+ }
@@ -1,29 +1,28 @@
1
- // LiveRoomChat - Chat multi-salas simplificado
1
+ // LiveRoomChat - Chat multi-salas using typed LiveRoom system
2
2
 
3
3
  import { LiveComponent, type FluxStackWebSocket } from '@core/types/types'
4
+ import { ChatRoom } from './rooms/ChatRoom'
5
+ import { DirectoryRoom } from './rooms/DirectoryRoom'
6
+ import type { ChatMessage } from './rooms/ChatRoom'
7
+ import type { DirectoryEntry } from './rooms/DirectoryRoom'
4
8
 
5
9
  // Componente Cliente (Ctrl+Click para navegar)
6
10
  import type { RoomChatDemo as _Client } from '@client/src/live/RoomChatDemo'
7
11
 
8
- export interface ChatMessage {
9
- id: string
10
- user: string
11
- text: string
12
- timestamp: number
13
- }
14
-
15
12
  export class LiveRoomChat extends LiveComponent<typeof LiveRoomChat.defaultState> {
16
13
  static componentName = 'LiveRoomChat'
17
- static publicActions = ['joinRoom', 'leaveRoom', 'switchRoom', 'sendMessage', 'setUsername'] as const
14
+ static publicActions = ['createRoom', 'joinRoom', 'leaveRoom', 'switchRoom', 'sendMessage', 'setUsername'] as const
18
15
  static defaultState = {
19
16
  username: '',
20
17
  activeRoom: null as string | null,
21
- rooms: [] as { id: string; name: string }[],
22
- messages: {} as Record<string, ChatMessage[]>
18
+ rooms: [] as { id: string; name: string; isPrivate: boolean }[],
19
+ messages: {} as Record<string, ChatMessage[]>,
20
+ customRooms: [] as DirectoryEntry[]
23
21
  }
24
22
 
25
- // Listeners por sala para evitar duplicação
23
+ // Track event unsubscribers per room
26
24
  private roomListeners = new Map<string, (() => void)[]>()
25
+ private directoryUnsubs: (() => void)[] = []
27
26
 
28
27
  constructor(
29
28
  initialState: Partial<typeof LiveRoomChat.defaultState> = {},
@@ -31,20 +30,96 @@ export class LiveRoomChat extends LiveComponent<typeof LiveRoomChat.defaultState
31
30
  options?: { room?: string; userId?: string }
32
31
  ) {
33
32
  super(initialState, ws, options)
33
+
34
+ // Auto-join the directory room so we can see rooms created by others
35
+ const dir = this.$room(DirectoryRoom, 'main')
36
+ dir.join()
37
+
38
+ // Load existing custom rooms from directory state
39
+ this.setState({ customRooms: dir.state.rooms || [] })
40
+
41
+ // Listen for new rooms being added
42
+ const unsubAdd = dir.on('room:added', (entry: DirectoryEntry) => {
43
+ const current = this.state.customRooms.filter(r => r.id !== entry.id)
44
+ this.setState({ customRooms: [...current, entry] })
45
+ })
46
+
47
+ // Listen for rooms being removed
48
+ const unsubRemove = dir.on('room:removed', (data: { id: string }) => {
49
+ this.setState({
50
+ customRooms: this.state.customRooms.filter(r => r.id !== data.id)
51
+ })
52
+ })
53
+
54
+ this.directoryUnsubs = [unsubAdd, unsubRemove]
55
+ }
56
+
57
+ async createRoom(payload: { roomId: string; roomName: string; password?: string }) {
58
+ const { roomId, roomName, password } = payload
59
+
60
+ if (!roomId || !roomName) throw new Error('Room ID and name are required')
61
+ if (roomId.length > 30 || roomName.length > 50) throw new Error('Room ID/name too long')
62
+
63
+ // Create by joining the room first
64
+ const room = this.$room(ChatRoom, roomId)
65
+ const result = room.join()
66
+
67
+ if ('rejected' in result && result.rejected) {
68
+ return { success: false, error: result.reason }
69
+ }
70
+
71
+ // Set password and creator (meta is server-only, never sent to clients)
72
+ if (password) {
73
+ room.setPassword(password)
74
+ }
75
+ room.meta.createdBy = this.state.username || 'Anonymous'
76
+
77
+ // Register in the directory so all users can see it
78
+ const dir = this.$room(DirectoryRoom, 'main')
79
+ dir.addRoom({
80
+ id: roomId,
81
+ name: roomName,
82
+ isPrivate: !!password,
83
+ createdBy: this.state.username || 'Anonymous'
84
+ })
85
+
86
+ // Listen for messages
87
+ const unsub = room.on('chat:message', (msg: ChatMessage) => {
88
+ const msgs = this.state.messages[roomId] || []
89
+ this.setState({
90
+ messages: { ...this.state.messages, [roomId]: [...msgs, msg].slice(-100) }
91
+ })
92
+ })
93
+ this.roomListeners.set(roomId, [unsub])
94
+
95
+ this.setState({
96
+ activeRoom: roomId,
97
+ rooms: [...this.state.rooms.filter(r => r.id !== roomId), { id: roomId, name: roomName, isPrivate: !!password }],
98
+ messages: { ...this.state.messages, [roomId]: [] }
99
+ })
100
+
101
+ return { success: true, roomId }
34
102
  }
35
103
 
36
- async joinRoom(payload: { roomId: string; roomName?: string }) {
37
- const { roomId, roomName } = payload
104
+ async joinRoom(payload: { roomId: string; roomName?: string; password?: string }) {
105
+ const { roomId, roomName, password } = payload
38
106
 
39
- // está na sala? Apenas ativar
107
+ // Already in room? Just activate it
40
108
  if (this.roomListeners.has(roomId)) {
41
109
  this.state.activeRoom = roomId
42
110
  return { success: true, roomId }
43
111
  }
44
112
 
45
- // Entrar e escutar mensagens
46
- this.$room(roomId).join()
47
- const unsub = this.$room(roomId).on('message:new', (msg: ChatMessage) => {
113
+ // Use typed room: $room(ChatRoom, instanceId)
114
+ const room = this.$room(ChatRoom, roomId)
115
+ const result = room.join({ password })
116
+
117
+ if ('rejected' in result && result.rejected) {
118
+ return { success: false, error: 'Senha incorreta' }
119
+ }
120
+
121
+ // Listen for chat messages from other members
122
+ const unsub = room.on('chat:message', (msg: ChatMessage) => {
48
123
  const msgs = this.state.messages[roomId] || []
49
124
  this.setState({
50
125
  messages: { ...this.state.messages, [roomId]: [...msgs, msg].slice(-100) }
@@ -52,11 +127,11 @@ export class LiveRoomChat extends LiveComponent<typeof LiveRoomChat.defaultState
52
127
  })
53
128
  this.roomListeners.set(roomId, [unsub])
54
129
 
55
- // Atualizar estado
130
+ // Update component state — load existing messages from room state
56
131
  this.setState({
57
132
  activeRoom: roomId,
58
- rooms: [...this.state.rooms.filter(r => r.id !== roomId), { id: roomId, name: roomName || roomId }],
59
- messages: { ...this.state.messages, [roomId]: this.state.messages[roomId] || [] }
133
+ rooms: [...this.state.rooms.filter(r => r.id !== roomId), { id: roomId, name: roomName || roomId, isPrivate: room.state.isPrivate }],
134
+ messages: { ...this.state.messages, [roomId]: room.state.messages || [] }
60
135
  })
61
136
 
62
137
  return { success: true, roomId }
@@ -65,12 +140,12 @@ export class LiveRoomChat extends LiveComponent<typeof LiveRoomChat.defaultState
65
140
  async leaveRoom(payload: { roomId: string }) {
66
141
  const { roomId } = payload
67
142
 
68
- // Limpar listeners
143
+ // Cleanup listeners
69
144
  this.roomListeners.get(roomId)?.forEach(fn => fn())
70
145
  this.roomListeners.delete(roomId)
71
- this.$room(roomId).leave()
146
+ this.$room(ChatRoom, roomId).leave()
72
147
 
73
- // Atualizar estado
148
+ // Update state
74
149
  const rooms = this.state.rooms.filter(r => r.id !== roomId)
75
150
  const { [roomId]: _, ...restMessages } = this.state.messages
76
151
 
@@ -84,7 +159,7 @@ export class LiveRoomChat extends LiveComponent<typeof LiveRoomChat.defaultState
84
159
  }
85
160
 
86
161
  async switchRoom(payload: { roomId: string }) {
87
- if (!this.$rooms.includes(payload.roomId)) throw new Error('Not in this room')
162
+ if (!this.roomListeners.has(payload.roomId)) throw new Error('Not in this room')
88
163
  this.state.activeRoom = payload.roomId
89
164
  return { success: true }
90
165
  }
@@ -96,19 +171,10 @@ export class LiveRoomChat extends LiveComponent<typeof LiveRoomChat.defaultState
96
171
  const text = payload.text?.trim()
97
172
  if (!text) throw new Error('Message cannot be empty')
98
173
 
99
- const message: ChatMessage = {
100
- id: `msg-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
101
- user: this.state.username || 'Anônimo',
102
- text,
103
- timestamp: Date.now()
104
- }
105
-
106
- // Adicionar localmente e emitir para outros
107
- const msgs = this.state.messages[roomId] || []
108
- this.setState({
109
- messages: { ...this.state.messages, [roomId]: [...msgs, message].slice(-100) }
110
- })
111
- this.$room(roomId).emit('message:new', message)
174
+ // Use typed room's custom method — the chat:message event handler
175
+ // (set up in joinRoom) updates component state for all members including sender
176
+ const room = this.$room(ChatRoom, roomId)
177
+ const message = room.addMessage(this.state.username || 'Anonymous', text)
112
178
 
113
179
  return { success: true, message }
114
180
  }
@@ -123,6 +189,8 @@ export class LiveRoomChat extends LiveComponent<typeof LiveRoomChat.defaultState
123
189
  destroy() {
124
190
  for (const fns of this.roomListeners.values()) fns.forEach(fn => fn())
125
191
  this.roomListeners.clear()
192
+ this.directoryUnsubs.forEach(fn => fn())
193
+ this.directoryUnsubs = []
126
194
  super.destroy()
127
195
  }
128
196
  }
@@ -0,0 +1,73 @@
1
+ // LiveSharedCounter - Shared counter using typed CounterRoom
2
+ //
3
+ // All connected clients share the same counter value.
4
+ // New joiners see the current value from room state.
5
+
6
+ import { LiveComponent, type FluxStackWebSocket } from '@core/types/types'
7
+ import { CounterRoom } from './rooms/CounterRoom'
8
+
9
+ // Componente Cliente (Ctrl+Click para navegar)
10
+ import type { SharedCounterDemo as _Client } from '@client/src/live/SharedCounterDemo'
11
+
12
+ export class LiveSharedCounter extends LiveComponent<typeof LiveSharedCounter.defaultState> {
13
+ static componentName = 'LiveSharedCounter'
14
+ static publicActions = ['increment', 'decrement', 'reset'] as const
15
+ static defaultState = {
16
+ username: '',
17
+ count: 0,
18
+ lastUpdatedBy: null as string | null,
19
+ onlineCount: 0
20
+ }
21
+
22
+ private counterUnsub: (() => void) | null = null
23
+
24
+ constructor(
25
+ initialState: Partial<typeof LiveSharedCounter.defaultState> = {},
26
+ ws: FluxStackWebSocket,
27
+ options?: { room?: string; userId?: string }
28
+ ) {
29
+ super(initialState, ws, options)
30
+
31
+ // Join the shared counter room
32
+ const room = this.$room(CounterRoom, 'global')
33
+ room.join()
34
+
35
+ // Load current state from room (new joiners see the current value)
36
+ this.setState({
37
+ count: room.state.count,
38
+ lastUpdatedBy: room.state.lastUpdatedBy,
39
+ onlineCount: room.state.onlineCount
40
+ })
41
+
42
+ // Listen for updates from other users
43
+ this.counterUnsub = room.on('counter:updated', (data) => {
44
+ this.setState({
45
+ count: data.count,
46
+ lastUpdatedBy: data.updatedBy
47
+ })
48
+ })
49
+ }
50
+
51
+ async increment() {
52
+ const room = this.$room(CounterRoom, 'global')
53
+ const count = room.increment(this.state.username || 'Anonymous')
54
+ return { success: true, count }
55
+ }
56
+
57
+ async decrement() {
58
+ const room = this.$room(CounterRoom, 'global')
59
+ const count = room.decrement(this.state.username || 'Anonymous')
60
+ return { success: true, count }
61
+ }
62
+
63
+ async reset() {
64
+ const room = this.$room(CounterRoom, 'global')
65
+ const count = room.reset(this.state.username || 'Anonymous')
66
+ return { success: true, count }
67
+ }
68
+
69
+ destroy() {
70
+ this.counterUnsub?.()
71
+ super.destroy()
72
+ }
73
+ }
@@ -0,0 +1,68 @@
1
+ // ChatRoom - Typed room with lifecycle hooks using @fluxstack/live LiveRoom
2
+
3
+ import { LiveRoom } from '@fluxstack/live'
4
+ import type { RoomJoinContext, RoomLeaveContext } from '@fluxstack/live'
5
+
6
+ export interface ChatMessage {
7
+ id: string
8
+ user: string
9
+ text: string
10
+ timestamp: number
11
+ }
12
+
13
+ interface ChatState {
14
+ messages: ChatMessage[]
15
+ onlineCount: number
16
+ isPrivate: boolean
17
+ }
18
+
19
+ interface ChatMeta {
20
+ /** Server-only: password hash. Never sent to clients. */
21
+ password: string | null
22
+ createdBy: string | null
23
+ }
24
+
25
+ interface ChatEvents {
26
+ 'chat:message': ChatMessage
27
+ }
28
+
29
+ export class ChatRoom extends LiveRoom<ChatState, ChatMeta, ChatEvents> {
30
+ static roomName = 'chat'
31
+ static defaultState: ChatState = { messages: [], onlineCount: 0, isPrivate: false }
32
+ static defaultMeta: ChatMeta = { password: null, createdBy: null }
33
+ static $options = { maxMembers: 100 }
34
+
35
+ /** Set a password for this room. Pass null to remove. */
36
+ setPassword(password: string | null) {
37
+ this.meta.password = password
38
+ this.setState({ isPrivate: password !== null })
39
+ }
40
+
41
+ onJoin(ctx: RoomJoinContext) {
42
+ // Validate password if room is protected
43
+ if (this.meta.password) {
44
+ if (ctx.payload?.password !== this.meta.password) {
45
+ return false // Rejected — wrong or missing password
46
+ }
47
+ }
48
+ this.setState({ onlineCount: this.state.onlineCount + 1 })
49
+ }
50
+
51
+ onLeave(_ctx: RoomLeaveContext) {
52
+ this.setState({ onlineCount: Math.max(0, this.state.onlineCount - 1) })
53
+ }
54
+
55
+ addMessage(user: string, text: string) {
56
+ const msg: ChatMessage = {
57
+ id: `msg-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
58
+ user,
59
+ text,
60
+ timestamp: Date.now(),
61
+ }
62
+ this.setState({
63
+ messages: [...this.state.messages.slice(-99), msg],
64
+ })
65
+ this.emit('chat:message', msg)
66
+ return msg
67
+ }
68
+ }
@@ -0,0 +1,51 @@
1
+ // CounterRoom - Shared counter using typed LiveRoom
2
+ //
3
+ // All members see the same count value.
4
+ // New joiners get the current count from room state.
5
+
6
+ import { LiveRoom } from '@fluxstack/live'
7
+ import type { RoomJoinContext, RoomLeaveContext } from '@fluxstack/live'
8
+
9
+ interface CounterState {
10
+ count: number
11
+ lastUpdatedBy: string | null
12
+ onlineCount: number
13
+ }
14
+
15
+ interface CounterEvents {
16
+ 'counter:updated': { count: number; updatedBy: string }
17
+ }
18
+
19
+ export class CounterRoom extends LiveRoom<CounterState, {}, CounterEvents> {
20
+ static roomName = 'counter'
21
+ static defaultState: CounterState = { count: 0, lastUpdatedBy: null, onlineCount: 0 }
22
+ static defaultMeta = {}
23
+
24
+ onJoin(_ctx: RoomJoinContext) {
25
+ this.setState({ onlineCount: this.state.onlineCount + 1 })
26
+ }
27
+
28
+ onLeave(_ctx: RoomLeaveContext) {
29
+ this.setState({ onlineCount: Math.max(0, this.state.onlineCount - 1) })
30
+ }
31
+
32
+ increment(username: string) {
33
+ const count = this.state.count + 1
34
+ this.setState({ count, lastUpdatedBy: username })
35
+ this.emit('counter:updated', { count, updatedBy: username })
36
+ return count
37
+ }
38
+
39
+ decrement(username: string) {
40
+ const count = this.state.count - 1
41
+ this.setState({ count, lastUpdatedBy: username })
42
+ this.emit('counter:updated', { count, updatedBy: username })
43
+ return count
44
+ }
45
+
46
+ reset(username: string) {
47
+ this.setState({ count: 0, lastUpdatedBy: username })
48
+ this.emit('counter:updated', { count: 0, updatedBy: username })
49
+ return 0
50
+ }
51
+ }
@@ -0,0 +1,42 @@
1
+ // DirectoryRoom - Shared room that tracks user-created chat rooms
2
+ //
3
+ // All LiveRoomChat components auto-join this room so they can
4
+ // see rooms created by other users in real-time.
5
+
6
+ import { LiveRoom } from '@fluxstack/live'
7
+
8
+ export interface DirectoryEntry {
9
+ id: string
10
+ name: string
11
+ isPrivate: boolean
12
+ createdBy: string
13
+ }
14
+
15
+ interface DirectoryState {
16
+ rooms: DirectoryEntry[]
17
+ }
18
+
19
+ interface DirectoryEvents {
20
+ 'room:added': DirectoryEntry
21
+ 'room:removed': { id: string }
22
+ }
23
+
24
+ export class DirectoryRoom extends LiveRoom<DirectoryState, {}, DirectoryEvents> {
25
+ static roomName = 'directory'
26
+ static defaultState: DirectoryState = { rooms: [] }
27
+ static defaultMeta = {}
28
+
29
+ addRoom(entry: DirectoryEntry) {
30
+ this.setState({
31
+ rooms: [...this.state.rooms.filter(r => r.id !== entry.id), entry]
32
+ })
33
+ this.emit('room:added', entry)
34
+ }
35
+
36
+ removeRoom(id: string) {
37
+ this.setState({
38
+ rooms: this.state.rooms.filter(r => r.id !== id)
39
+ })
40
+ this.emit('room:removed', { id })
41
+ }
42
+ }