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.
- package/CHANGELOG.md +80 -0
- package/LLMD/INDEX.md +4 -3
- package/LLMD/resources/live-binary-delta.md +507 -0
- package/LLMD/resources/live-components.md +1 -0
- package/LLMD/resources/live-rooms.md +731 -333
- package/app/client/src/App.tsx +23 -14
- package/app/client/src/components/AppLayout.tsx +4 -4
- package/app/client/src/live/AuthDemo.tsx +4 -4
- 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/live/LivePingPong.ts +61 -0
- 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/core/build/bundler.ts +40 -26
- package/core/build/flux-plugins-generator.ts +325 -325
- package/core/build/index.ts +92 -21
- package/core/cli/command-registry.ts +44 -46
- package/core/cli/commands/build.ts +11 -6
- package/core/cli/commands/create.ts +7 -5
- package/core/cli/commands/dev.ts +6 -5
- package/core/cli/commands/help.ts +3 -2
- package/core/cli/commands/make-plugin.ts +8 -7
- package/core/cli/commands/plugin-add.ts +60 -43
- package/core/cli/commands/plugin-deps.ts +73 -57
- package/core/cli/commands/plugin-list.ts +44 -41
- package/core/cli/commands/plugin-remove.ts +33 -22
- package/core/cli/generators/component.ts +770 -769
- package/core/cli/generators/controller.ts +9 -8
- package/core/cli/generators/index.ts +148 -146
- package/core/cli/generators/interactive.ts +228 -227
- package/core/cli/generators/plugin.ts +11 -10
- package/core/cli/generators/prompts.ts +83 -82
- package/core/cli/generators/route.ts +7 -6
- package/core/cli/generators/service.ts +10 -9
- package/core/cli/generators/template-engine.ts +2 -1
- package/core/cli/generators/types.ts +7 -7
- package/core/cli/generators/utils.ts +191 -191
- package/core/cli/index.ts +9 -8
- package/core/cli/plugin-discovery.ts +2 -2
- package/core/client/hooks/useAuth.ts +48 -48
- package/core/client/index.ts +0 -16
- package/core/client/standalone.ts +18 -17
- package/core/client/state/createStore.ts +192 -192
- package/core/client/state/index.ts +14 -14
- package/core/config/index.ts +1 -0
- package/core/framework/client.ts +131 -131
- package/core/framework/index.ts +7 -7
- package/core/framework/server.ts +72 -112
- package/core/framework/types.ts +2 -2
- package/core/plugins/built-in/live-components/commands/create-live-component.ts +6 -3
- package/core/plugins/built-in/monitoring/index.ts +110 -68
- package/core/plugins/built-in/static/index.ts +2 -2
- package/core/plugins/built-in/swagger/index.ts +9 -9
- package/core/plugins/built-in/vite/index.ts +3 -3
- package/core/plugins/built-in/vite/vite-dev.ts +3 -3
- package/core/plugins/config.ts +50 -47
- package/core/plugins/discovery.ts +10 -4
- package/core/plugins/executor.ts +2 -2
- package/core/plugins/index.ts +206 -203
- package/core/plugins/manager.ts +21 -20
- package/core/plugins/registry.ts +76 -12
- package/core/plugins/types.ts +14 -14
- package/core/server/framework.ts +3 -189
- package/core/server/live/auto-generated-components.ts +11 -35
- package/core/server/live/index.ts +41 -36
- package/core/server/live/websocket-plugin.ts +48 -3
- package/core/server/middleware/elysia-helpers.ts +16 -15
- package/core/server/middleware/errorHandling.ts +14 -14
- package/core/server/middleware/index.ts +31 -31
- package/core/server/plugins/database.ts +181 -180
- package/core/server/plugins/static-files-plugin.ts +4 -3
- package/core/server/plugins/swagger.ts +11 -8
- package/core/server/rooms/RoomBroadcaster.ts +11 -10
- package/core/server/rooms/RoomSystem.ts +14 -11
- package/core/server/services/BaseService.ts +7 -7
- package/core/server/services/ServiceContainer.ts +5 -5
- package/core/server/services/index.ts +8 -8
- package/core/templates/create-project.ts +28 -27
- package/core/testing/index.ts +9 -9
- package/core/testing/setup.ts +73 -73
- package/core/types/api.ts +168 -168
- package/core/types/config.ts +5 -5
- package/core/types/index.ts +1 -1
- package/core/types/plugin.ts +2 -2
- package/core/types/types.ts +3 -3
- package/core/utils/build-logger.ts +324 -324
- package/core/utils/config-schema.ts +480 -480
- package/core/utils/env.ts +10 -8
- package/core/utils/errors/codes.ts +114 -114
- package/core/utils/errors/handlers.ts +30 -20
- package/core/utils/errors/index.ts +54 -46
- package/core/utils/errors/middleware.ts +113 -113
- package/core/utils/helpers.ts +19 -16
- package/core/utils/logger/colors.ts +114 -114
- package/core/utils/logger/config.ts +2 -2
- package/core/utils/logger/formatter.ts +82 -82
- package/core/utils/logger/group-logger.ts +101 -101
- package/core/utils/logger/index.ts +13 -3
- package/core/utils/logger/startup-banner.ts +2 -2
- package/core/utils/logger/winston-logger.ts +152 -152
- package/core/utils/monitoring/index.ts +211 -211
- package/core/utils/sync-version.ts +67 -66
- package/core/utils/version.ts +1 -1
- package/package.json +11 -6
- package/playwright-report/index.html +85 -0
- package/playwright.config.ts +31 -0
- package/plugins/crypto-auth/client/CryptoAuthClient.ts +302 -302
- package/plugins/crypto-auth/client/components/index.ts +11 -11
- package/plugins/crypto-auth/client/index.ts +11 -11
- package/plugins/crypto-auth/package.json +65 -65
- package/plugins/crypto-auth/server/CryptoAuthService.ts +185 -185
- package/plugins/crypto-auth/server/middlewares/cryptoAuthAdmin.ts +6 -5
- package/plugins/crypto-auth/server/middlewares/cryptoAuthPermissions.ts +6 -5
- package/plugins/crypto-auth/server/middlewares/cryptoAuthRequired.ts +3 -3
- package/plugins/crypto-auth/server/middlewares/index.ts +22 -22
- package/plugins/crypto-auth/server/middlewares.ts +19 -19
- package/tsconfig.json +4 -1
- package/vite.config.ts +13 -0
- package/app/client/.live-stubs/LiveAdminPanel.js +0 -5
- package/app/client/.live-stubs/LiveChat.js +0 -7
- package/app/client/.live-stubs/LiveCounter.js +0 -9
- package/app/client/.live-stubs/LiveForm.js +0 -11
- package/app/client/.live-stubs/LiveLocalCounter.js +0 -8
- package/app/client/.live-stubs/LiveRoomChat.js +0 -10
- package/app/client/.live-stubs/LiveTodoList.js +0 -9
- package/app/client/.live-stubs/LiveUpload.js +0 -15
- package/app/client/src/live/ChatDemo.tsx +0 -107
- package/app/client/src/live/LiveDebuggerPanel.tsx +0 -779
- package/app/client/src/live/TodoListDemo.tsx +0 -158
- package/app/server/live/LiveChat.ts +0 -78
- package/app/server/live/LiveTodoList.ts +0 -110
- package/app/server/live/register-components.ts +0 -19
- package/core/build/live-components-generator.ts +0 -312
- package/core/client/components/LiveDebugger.tsx +0 -1324
- package/core/live/ComponentRegistry.ts +0 -403
- package/core/live/types.ts +0 -241
- 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
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
46
|
-
this.$room(roomId)
|
|
47
|
-
const
|
|
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
|
-
//
|
|
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]:
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
|
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
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
+
}
|