create-fluxstack 1.10.1 → 1.12.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/.dockerignore +1 -2
- package/Dockerfile +8 -8
- package/LLMD/INDEX.md +64 -0
- package/LLMD/MAINTENANCE.md +197 -0
- package/LLMD/MIGRATION.md +156 -0
- package/LLMD/config/.gitkeep +1 -0
- package/LLMD/config/declarative-system.md +268 -0
- package/LLMD/config/environment-vars.md +327 -0
- package/LLMD/config/runtime-reload.md +401 -0
- package/LLMD/core/.gitkeep +1 -0
- package/LLMD/core/build-system.md +599 -0
- package/LLMD/core/framework-lifecycle.md +229 -0
- package/LLMD/core/plugin-system.md +451 -0
- package/LLMD/patterns/.gitkeep +1 -0
- package/LLMD/patterns/anti-patterns.md +297 -0
- package/LLMD/patterns/project-structure.md +264 -0
- package/LLMD/patterns/type-safety.md +440 -0
- package/LLMD/reference/.gitkeep +1 -0
- package/LLMD/reference/cli-commands.md +250 -0
- package/LLMD/reference/plugin-hooks.md +357 -0
- package/LLMD/reference/routing.md +39 -0
- package/LLMD/reference/troubleshooting.md +364 -0
- package/LLMD/resources/.gitkeep +1 -0
- package/LLMD/resources/controllers.md +465 -0
- package/LLMD/resources/live-components.md +703 -0
- package/LLMD/resources/live-rooms.md +482 -0
- package/LLMD/resources/live-upload.md +130 -0
- package/LLMD/resources/plugins-external.md +617 -0
- package/LLMD/resources/routes-eden.md +254 -0
- package/README.md +37 -17
- package/app/client/index.html +0 -1
- package/app/client/src/App.tsx +107 -150
- package/app/client/src/components/AppLayout.tsx +68 -0
- package/app/client/src/components/BackButton.tsx +13 -0
- package/app/client/src/components/DemoPage.tsx +20 -0
- package/app/client/src/components/LiveUploadWidget.tsx +204 -0
- package/app/client/src/lib/eden-api.ts +85 -60
- package/app/client/src/live/ChatDemo.tsx +107 -0
- package/app/client/src/live/CounterDemo.tsx +206 -0
- package/app/client/src/live/FormDemo.tsx +119 -0
- package/app/client/src/live/RoomChatDemo.tsx +242 -0
- package/app/client/src/live/UploadDemo.tsx +21 -0
- package/app/client/src/main.tsx +4 -1
- package/app/client/src/pages/ApiTestPage.tsx +108 -0
- package/app/client/src/pages/HomePage.tsx +76 -0
- package/app/server/app.ts +1 -4
- package/app/server/controllers/users.controller.ts +36 -44
- package/app/server/index.ts +25 -35
- package/app/server/live/LiveChat.ts +77 -0
- package/app/server/live/LiveCounter.ts +67 -0
- package/app/server/live/LiveForm.ts +63 -0
- package/app/server/live/LiveLocalCounter.ts +32 -0
- package/app/server/live/LiveRoomChat.ts +285 -0
- package/app/server/live/LiveUpload.ts +81 -0
- package/app/server/routes/index.ts +3 -1
- package/app/server/routes/room.routes.ts +117 -0
- package/app/server/routes/users.routes.ts +35 -27
- package/app/shared/types/index.ts +14 -2
- package/config/app.config.ts +2 -62
- package/config/client.config.ts +2 -95
- package/config/database.config.ts +2 -99
- package/config/fluxstack.config.ts +25 -45
- package/config/index.ts +57 -38
- package/config/monitoring.config.ts +2 -114
- package/config/plugins.config.ts +2 -80
- package/config/server.config.ts +2 -68
- package/config/services.config.ts +2 -130
- package/config/system/app.config.ts +29 -0
- package/config/system/build.config.ts +49 -0
- package/config/system/client.config.ts +68 -0
- package/config/system/database.config.ts +17 -0
- package/config/system/fluxstack.config.ts +114 -0
- package/config/{logger.config.ts → system/logger.config.ts} +3 -1
- package/config/system/monitoring.config.ts +114 -0
- package/config/system/plugins.config.ts +84 -0
- package/config/{runtime.config.ts → system/runtime.config.ts} +1 -1
- package/config/system/server.config.ts +68 -0
- package/config/system/services.config.ts +46 -0
- package/config/{system.config.ts → system/system.config.ts} +1 -1
- package/core/build/flux-plugins-generator.ts +325 -325
- package/core/build/index.ts +39 -27
- package/core/build/live-components-generator.ts +3 -3
- package/core/build/optimizer.ts +235 -235
- package/core/cli/command-registry.ts +6 -4
- package/core/cli/commands/build.ts +79 -0
- package/core/cli/commands/create.ts +54 -0
- package/core/cli/commands/dev.ts +101 -0
- package/core/cli/commands/help.ts +34 -0
- package/core/cli/commands/index.ts +34 -0
- package/core/cli/commands/make-plugin.ts +90 -0
- package/core/cli/commands/plugin-add.ts +197 -0
- package/core/cli/commands/plugin-deps.ts +2 -2
- package/core/cli/commands/plugin-list.ts +208 -0
- package/core/cli/commands/plugin-remove.ts +170 -0
- package/core/cli/generators/component.ts +769 -769
- package/core/cli/generators/controller.ts +1 -1
- package/core/cli/generators/index.ts +146 -146
- package/core/cli/generators/interactive.ts +227 -227
- package/core/cli/generators/plugin.ts +2 -2
- package/core/cli/generators/prompts.ts +82 -82
- package/core/cli/generators/route.ts +6 -6
- package/core/cli/generators/service.ts +2 -2
- package/core/cli/generators/template-engine.ts +4 -3
- package/core/cli/generators/types.ts +2 -2
- package/core/cli/generators/utils.ts +191 -191
- package/core/cli/index.ts +115 -686
- package/core/cli/plugin-discovery.ts +2 -2
- package/core/client/LiveComponentsProvider.tsx +60 -8
- package/core/client/api/eden.ts +183 -0
- package/core/client/api/index.ts +11 -0
- package/core/client/components/Live.tsx +104 -0
- package/core/client/fluxstack.ts +1 -9
- package/core/client/hooks/AdaptiveChunkSizer.ts +215 -215
- package/core/client/hooks/state-validator.ts +1 -1
- package/core/client/hooks/useAuth.ts +48 -48
- package/core/client/hooks/useChunkedUpload.ts +85 -35
- package/core/client/hooks/useLiveChunkedUpload.ts +87 -0
- package/core/client/hooks/useLiveComponent.ts +800 -0
- package/core/client/hooks/useLiveUpload.ts +71 -0
- package/core/client/hooks/useRoom.ts +409 -0
- package/core/client/hooks/useRoomProxy.ts +382 -0
- package/core/client/index.ts +17 -68
- package/core/client/standalone-entry.ts +8 -0
- package/core/client/standalone.ts +74 -53
- package/core/client/state/createStore.ts +192 -192
- package/core/client/state/index.ts +14 -14
- package/core/config/index.ts +70 -291
- package/core/config/schema.ts +42 -723
- package/core/framework/client.ts +131 -131
- package/core/framework/index.ts +7 -7
- package/core/framework/server.ts +47 -40
- package/core/framework/types.ts +2 -2
- package/core/index.ts +23 -4
- package/core/live/ComponentRegistry.ts +3 -3
- package/core/live/types.ts +77 -0
- package/core/plugins/built-in/index.ts +134 -134
- package/core/plugins/built-in/live-components/commands/create-live-component.ts +242 -1066
- package/core/plugins/built-in/live-components/index.ts +1 -1
- package/core/plugins/built-in/monitoring/index.ts +111 -47
- package/core/plugins/built-in/static/index.ts +1 -1
- package/core/plugins/built-in/swagger/index.ts +68 -265
- package/core/plugins/built-in/vite/index.ts +85 -185
- package/core/plugins/built-in/vite/vite-dev.ts +10 -16
- package/core/plugins/config.ts +9 -7
- package/core/plugins/dependency-manager.ts +31 -1
- package/core/plugins/discovery.ts +19 -7
- package/core/plugins/executor.ts +2 -2
- package/core/plugins/index.ts +203 -203
- package/core/plugins/manager.ts +27 -39
- package/core/plugins/module-resolver.ts +19 -8
- package/core/plugins/registry.ts +255 -19
- package/core/plugins/types.ts +20 -53
- package/core/server/framework.ts +66 -43
- package/core/server/index.ts +15 -15
- package/core/server/live/ComponentRegistry.ts +78 -71
- package/core/server/live/FileUploadManager.ts +23 -10
- package/core/server/live/LiveComponentPerformanceMonitor.ts +1 -1
- package/core/server/live/LiveRoomManager.ts +261 -0
- package/core/server/live/RoomEventBus.ts +234 -0
- package/core/server/live/RoomStateManager.ts +172 -0
- package/core/server/live/StateSignature.ts +643 -643
- package/core/server/live/WebSocketConnectionManager.ts +30 -19
- package/core/server/live/auto-generated-components.ts +21 -9
- package/core/server/live/index.ts +14 -0
- package/core/server/live/websocket-plugin.ts +214 -67
- package/core/server/middleware/elysia-helpers.ts +7 -2
- package/core/server/middleware/errorHandling.ts +1 -1
- package/core/server/middleware/index.ts +31 -31
- package/core/server/plugins/database.ts +180 -180
- package/core/server/plugins/static-files-plugin.ts +69 -69
- package/core/server/plugins/swagger.ts +1 -1
- package/core/server/rooms/RoomBroadcaster.ts +357 -0
- package/core/server/rooms/RoomSystem.ts +463 -0
- package/core/server/rooms/index.ts +13 -0
- package/core/server/services/BaseService.ts +1 -1
- package/core/server/services/ServiceContainer.ts +1 -1
- package/core/server/services/index.ts +8 -8
- package/core/templates/create-project.ts +12 -12
- package/core/testing/index.ts +9 -9
- package/core/testing/setup.ts +73 -73
- package/core/types/api.ts +168 -168
- package/core/types/build.ts +219 -219
- package/core/types/config.ts +56 -26
- package/core/types/index.ts +4 -4
- package/core/types/plugin.ts +107 -107
- package/core/types/types.ts +353 -14
- package/core/utils/build-logger.ts +324 -324
- package/core/utils/config-schema.ts +480 -480
- package/core/utils/env.ts +2 -8
- package/core/utils/errors/codes.ts +114 -114
- package/core/utils/errors/handlers.ts +36 -1
- package/core/utils/errors/index.ts +49 -5
- package/core/utils/errors/middleware.ts +113 -113
- package/core/utils/helpers.ts +6 -16
- package/core/utils/index.ts +17 -17
- package/core/utils/logger/colors.ts +114 -114
- package/core/utils/logger/config.ts +13 -9
- package/core/utils/logger/formatter.ts +82 -82
- package/core/utils/logger/group-logger.ts +101 -101
- package/core/utils/logger/index.ts +6 -1
- package/core/utils/logger/stack-trace.ts +3 -1
- package/core/utils/logger/startup-banner.ts +82 -82
- package/core/utils/logger/winston-logger.ts +152 -152
- package/core/utils/monitoring/index.ts +211 -211
- package/core/utils/sync-version.ts +66 -66
- package/core/utils/version.ts +1 -1
- package/create-fluxstack.ts +8 -7
- package/package.json +12 -13
- package/plugins/crypto-auth/cli/make-protected-route.command.ts +1 -1
- 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/config/index.ts +1 -1
- package/plugins/crypto-auth/index.ts +4 -4
- package/plugins/crypto-auth/package.json +65 -65
- package/plugins/crypto-auth/server/AuthMiddleware.ts +1 -1
- package/plugins/crypto-auth/server/CryptoAuthService.ts +185 -185
- package/plugins/crypto-auth/server/index.ts +21 -21
- package/plugins/crypto-auth/server/middlewares/cryptoAuthAdmin.ts +3 -3
- package/plugins/crypto-auth/server/middlewares/cryptoAuthOptional.ts +1 -1
- package/plugins/crypto-auth/server/middlewares/cryptoAuthPermissions.ts +2 -2
- package/plugins/crypto-auth/server/middlewares/cryptoAuthRequired.ts +2 -2
- package/plugins/crypto-auth/server/middlewares/helpers.ts +1 -1
- package/plugins/crypto-auth/server/middlewares/index.ts +22 -22
- package/tsconfig.api-strict.json +16 -0
- package/tsconfig.json +48 -52
- package/{app/client/tsconfig.node.json → tsconfig.node.json} +25 -25
- package/types/global.d.ts +29 -29
- package/types/vitest.d.ts +8 -8
- package/vite.config.ts +38 -62
- package/vitest.config.live.ts +10 -9
- package/vitest.config.ts +29 -17
- package/app/client/README.md +0 -69
- package/app/client/SIMPLIFICATION.md +0 -140
- package/app/client/frontend-only.ts +0 -12
- package/app/client/src/live/FileUploadExample.tsx +0 -359
- package/app/client/src/live/MinimalLiveClock.tsx +0 -47
- package/app/client/src/live/QuickUploadTest.tsx +0 -193
- package/app/client/tsconfig.app.json +0 -45
- package/app/client/tsconfig.json +0 -7
- package/app/client/zustand-setup.md +0 -65
- package/app/server/backend-only.ts +0 -18
- package/app/server/live/LiveClockComponent.ts +0 -215
- package/app/server/live/LiveFileUploadComponent.ts +0 -77
- package/app/server/routes/env-test.ts +0 -110
- package/core/client/hooks/index.ts +0 -7
- package/core/client/hooks/useHybridLiveComponent.ts +0 -685
- package/core/client/hooks/useTypedLiveComponent.ts +0 -133
- package/core/client/hooks/useWebSocket.ts +0 -361
- package/core/config/env.ts +0 -546
- package/core/config/loader.ts +0 -522
- package/core/config/runtime-config.ts +0 -327
- package/core/config/validator.ts +0 -540
- package/core/server/backend-entry.ts +0 -51
- package/core/server/standalone.ts +0 -106
- package/core/utils/regenerate-files.ts +0 -69
- package/fluxstack.config.ts +0 -354
|
@@ -0,0 +1,482 @@
|
|
|
1
|
+
# Live Room System
|
|
2
|
+
|
|
3
|
+
**Version:** 1.11.0 | **Updated:** 2025-02-09
|
|
4
|
+
|
|
5
|
+
## Quick Facts
|
|
6
|
+
|
|
7
|
+
- Server-side room management for real-time communication
|
|
8
|
+
- Multiple rooms per component supported
|
|
9
|
+
- Events propagate to all room members automatically
|
|
10
|
+
- HTTP API integration for external systems (webhooks, bots)
|
|
11
|
+
- Type-safe event handling with TypeScript
|
|
12
|
+
|
|
13
|
+
## Overview
|
|
14
|
+
|
|
15
|
+
The Room System enables real-time communication between Live Components. It's **server-side first**, meaning:
|
|
16
|
+
|
|
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
|
|
20
|
+
|
|
21
|
+
## Core API
|
|
22
|
+
|
|
23
|
+
### Server-Side ($room)
|
|
24
|
+
|
|
25
|
+
```typescript
|
|
26
|
+
// app/server/live/MyComponent.ts
|
|
27
|
+
import { LiveComponent } from '@core/types/types'
|
|
28
|
+
|
|
29
|
+
export class MyComponent extends LiveComponent<typeof defaultState> {
|
|
30
|
+
|
|
31
|
+
// Join a room
|
|
32
|
+
this.$room('room-id').join()
|
|
33
|
+
|
|
34
|
+
// Leave a room
|
|
35
|
+
this.$room('room-id').leave()
|
|
36
|
+
|
|
37
|
+
// Emit event to all members (except self)
|
|
38
|
+
this.$room('room-id').emit('event-name', { data: 'value' })
|
|
39
|
+
|
|
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
|
+
})
|
|
45
|
+
|
|
46
|
+
// Get room state
|
|
47
|
+
const state = this.$room('room-id').state
|
|
48
|
+
|
|
49
|
+
// Set room state (broadcasts to all)
|
|
50
|
+
this.$room('room-id').setState({ key: 'value' })
|
|
51
|
+
|
|
52
|
+
// List all joined rooms
|
|
53
|
+
const rooms = this.$rooms // ['room-1', 'room-2']
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Default Room
|
|
58
|
+
|
|
59
|
+
If component has a default room (via `options.room`), you can use shorthand:
|
|
60
|
+
|
|
61
|
+
```typescript
|
|
62
|
+
// Using default room
|
|
63
|
+
this.$room.emit('event', data)
|
|
64
|
+
this.$room.on('event', handler)
|
|
65
|
+
|
|
66
|
+
// Equivalent to:
|
|
67
|
+
this.$room('default-room-id').emit('event', data)
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Complete Example: Chat Component
|
|
71
|
+
|
|
72
|
+
### Server Component
|
|
73
|
+
|
|
74
|
+
```typescript
|
|
75
|
+
// app/server/live/LiveRoomChat.ts
|
|
76
|
+
import { LiveComponent } from '@core/types/types'
|
|
77
|
+
|
|
78
|
+
export interface ChatMessage {
|
|
79
|
+
id: string
|
|
80
|
+
user: string
|
|
81
|
+
text: string
|
|
82
|
+
timestamp: number
|
|
83
|
+
}
|
|
84
|
+
|
|
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[]>
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export class LiveRoomChat extends LiveComponent<typeof defaultState> {
|
|
94
|
+
static defaultState = defaultState
|
|
95
|
+
|
|
96
|
+
constructor(initialState: Partial<typeof defaultState>, ws: any, options?: { room?: string; userId?: string }) {
|
|
97
|
+
super({ ...defaultState, ...initialState }, ws, options)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Join a chat room
|
|
101
|
+
async joinRoom(payload: { roomId: string; roomName?: string }) {
|
|
102
|
+
const { roomId, roomName } = payload
|
|
103
|
+
|
|
104
|
+
// 1. Join the room on server
|
|
105
|
+
this.$room(roomId).join()
|
|
106
|
+
|
|
107
|
+
// 2. Listen for messages from OTHER users
|
|
108
|
+
this.$room(roomId).on('message:new', (msg: ChatMessage) => {
|
|
109
|
+
this.addMessageToState(roomId, msg)
|
|
110
|
+
})
|
|
111
|
+
|
|
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)
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
// 4. Update local state (syncs to frontend)
|
|
118
|
+
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]: [] }
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
return { success: true, roomId }
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Send message
|
|
129
|
+
async sendMessage(payload: { text: string }) {
|
|
130
|
+
const roomId = this.state.activeRoom
|
|
131
|
+
if (!roomId) throw new Error('No active room')
|
|
132
|
+
|
|
133
|
+
const message: ChatMessage = {
|
|
134
|
+
id: `msg-${Date.now()}`,
|
|
135
|
+
user: this.state.username,
|
|
136
|
+
text: payload.text,
|
|
137
|
+
timestamp: Date.now()
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// 1. Add to MY state (syncs to MY frontend)
|
|
141
|
+
this.addMessageToState(roomId, message)
|
|
142
|
+
|
|
143
|
+
// 2. Emit to OTHERS (they receive via $room.on)
|
|
144
|
+
this.$room(roomId).emit('message:new', message)
|
|
145
|
+
|
|
146
|
+
return { success: true, message }
|
|
147
|
+
}
|
|
148
|
+
|
|
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
|
|
156
|
+
}
|
|
157
|
+
})
|
|
158
|
+
}
|
|
159
|
+
|
|
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)
|
|
166
|
+
|
|
167
|
+
this.setState({
|
|
168
|
+
typingUsers: { ...this.state.typingUsers, [roomId]: updated }
|
|
169
|
+
})
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Start typing indicator
|
|
173
|
+
async startTyping() {
|
|
174
|
+
const roomId = this.state.activeRoom
|
|
175
|
+
if (!roomId) return { success: false }
|
|
176
|
+
|
|
177
|
+
this.$room(roomId).emit('user:typing', {
|
|
178
|
+
user: this.state.username,
|
|
179
|
+
typing: true
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
return { success: true }
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Leave room
|
|
186
|
+
async leaveRoom(payload: { roomId: string }) {
|
|
187
|
+
const { roomId } = payload
|
|
188
|
+
|
|
189
|
+
this.$room(roomId).leave()
|
|
190
|
+
|
|
191
|
+
const { [roomId]: _, ...restMessages } = this.state.messages
|
|
192
|
+
const { [roomId]: __, ...restTyping } = this.state.typingUsers
|
|
193
|
+
|
|
194
|
+
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
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
return { success: true }
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
### Frontend Component
|
|
207
|
+
|
|
208
|
+
```typescript
|
|
209
|
+
// app/client/src/live/RoomChatDemo.tsx
|
|
210
|
+
import { Live } from '@/core/client'
|
|
211
|
+
import { LiveRoomChat, defaultState } from '@server/live/LiveRoomChat'
|
|
212
|
+
|
|
213
|
+
export function RoomChatDemo() {
|
|
214
|
+
const [text, setText] = useState('')
|
|
215
|
+
|
|
216
|
+
// Connect to Live Component
|
|
217
|
+
const chat = Live.use(LiveRoomChat, {
|
|
218
|
+
initialState: { ...defaultState, username: 'User123' }
|
|
219
|
+
})
|
|
220
|
+
|
|
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] || []) : []
|
|
225
|
+
|
|
226
|
+
// Join room
|
|
227
|
+
const handleJoinRoom = async (roomId: string) => {
|
|
228
|
+
await chat.joinRoom({ roomId, roomName: roomId })
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Send message
|
|
232
|
+
const handleSend = async () => {
|
|
233
|
+
if (!text.trim()) return
|
|
234
|
+
await chat.sendMessage({ text })
|
|
235
|
+
setText('')
|
|
236
|
+
}
|
|
237
|
+
|
|
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
|
+
)
|
|
275
|
+
}
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
## HTTP API Integration
|
|
279
|
+
|
|
280
|
+
Send messages from external systems via REST API:
|
|
281
|
+
|
|
282
|
+
### Routes
|
|
283
|
+
|
|
284
|
+
```typescript
|
|
285
|
+
// app/server/routes/room.routes.ts
|
|
286
|
+
import { Elysia, t } from 'elysia'
|
|
287
|
+
import { liveRoomManager } from '@core/server/live/LiveRoomManager'
|
|
288
|
+
|
|
289
|
+
export const roomRoutes = new Elysia({ prefix: '/rooms' })
|
|
290
|
+
|
|
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
|
+
}
|
|
299
|
+
|
|
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
|
+
})
|
|
314
|
+
|
|
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
|
+
})
|
|
330
|
+
|
|
331
|
+
// Get stats
|
|
332
|
+
.get('/stats', () => liveRoomManager.getStats())
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
### Usage Examples
|
|
336
|
+
|
|
337
|
+
```bash
|
|
338
|
+
# Send message via curl
|
|
339
|
+
curl -X POST http://localhost:3000/api/rooms/geral/messages \
|
|
340
|
+
-H "Content-Type: application/json" \
|
|
341
|
+
-d '{"user": "Webhook Bot", "text": "New deployment completed!"}'
|
|
342
|
+
|
|
343
|
+
# Emit custom event
|
|
344
|
+
curl -X POST http://localhost:3000/api/rooms/tech/emit \
|
|
345
|
+
-H "Content-Type: application/json" \
|
|
346
|
+
-d '{"event": "notification", "data": {"type": "alert", "message": "Server restarted"}}'
|
|
347
|
+
|
|
348
|
+
# Get room stats
|
|
349
|
+
curl http://localhost:3000/api/rooms/stats
|
|
350
|
+
```
|
|
351
|
+
|
|
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
|
+
```
|
|
377
|
+
|
|
378
|
+
## Room Manager API
|
|
379
|
+
|
|
380
|
+
Direct access to room manager for advanced use cases:
|
|
381
|
+
|
|
382
|
+
```typescript
|
|
383
|
+
import { liveRoomManager } from '@core/server/live/LiveRoomManager'
|
|
384
|
+
|
|
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
|
|
407
|
+
liveRoomManager.cleanupComponent(componentId)
|
|
408
|
+
|
|
409
|
+
// Get statistics
|
|
410
|
+
const stats = liveRoomManager.getStats()
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
## Room Event Bus
|
|
414
|
+
|
|
415
|
+
For server-side event handling:
|
|
416
|
+
|
|
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
|
+
)
|
|
430
|
+
|
|
431
|
+
// Emit events
|
|
432
|
+
roomEvents.emit('room', 'geral', 'message', { text: 'Hello' }, excludeId)
|
|
433
|
+
|
|
434
|
+
// Cleanup
|
|
435
|
+
roomEvents.unsubscribeAll(componentId)
|
|
436
|
+
roomEvents.clearRoom('room', 'geral')
|
|
437
|
+
|
|
438
|
+
// Stats
|
|
439
|
+
const stats = roomEvents.getStats()
|
|
440
|
+
```
|
|
441
|
+
|
|
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 |
|
|
453
|
+
|
|
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
|
|
462
|
+
|
|
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
|
|
468
|
+
|
|
469
|
+
## Files Reference
|
|
470
|
+
|
|
471
|
+
| File | Purpose |
|
|
472
|
+
|------|---------|
|
|
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 |
|
|
477
|
+
|
|
478
|
+
## Related
|
|
479
|
+
|
|
480
|
+
- [Live Components](./live-components.md) - Base component system
|
|
481
|
+
- [Routes with Eden Treaty](./routes-eden.md) - HTTP API patterns
|
|
482
|
+
- [Type Safety](../patterns/type-safety.md) - TypeScript patterns
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# Live Upload (Chunked Upload via WebSocket)
|
|
2
|
+
|
|
3
|
+
**Version:** 1.11.0 | **Updated:** 2026-02-08
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
FluxStack supports chunked file upload over the Live Components WebSocket. The
|
|
8
|
+
server tracks progress and assembles the file in `uploads/`. The client streams
|
|
9
|
+
chunks without loading the entire file into memory.
|
|
10
|
+
|
|
11
|
+
## Server: LiveUpload Component
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
// app/server/live/LiveUpload.ts
|
|
15
|
+
import { LiveComponent } from '@core/types/types'
|
|
16
|
+
import { liveUploadDefaultState, type LiveUploadState } from '@app/shared'
|
|
17
|
+
|
|
18
|
+
export const defaultState: LiveUploadState = liveUploadDefaultState
|
|
19
|
+
|
|
20
|
+
export class LiveUpload extends LiveComponent<LiveUploadState> {
|
|
21
|
+
static defaultState = defaultState
|
|
22
|
+
|
|
23
|
+
constructor(initialState: Partial<typeof defaultState>, ws: any, options?: { room?: string; userId?: string }) {
|
|
24
|
+
super({ ...defaultState, ...initialState }, ws, options)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async startUpload(payload: { fileName: string; fileSize: number; fileType: string }) {
|
|
28
|
+
const normalized = payload.fileName.toLowerCase()
|
|
29
|
+
if (normalized.includes('..') || normalized.includes('/') || normalized.includes('\\')) {
|
|
30
|
+
throw new Error('Invalid file name')
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const ext = normalized.includes('.') ? normalized.split('.').pop() || '' : ''
|
|
34
|
+
const blocked = ['exe', 'bat', 'cmd', 'sh', 'ps1', 'msi', 'jar']
|
|
35
|
+
if (ext && blocked.includes(ext)) {
|
|
36
|
+
throw new Error(`File extension not allowed: .${ext}`)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
this.setState({
|
|
40
|
+
status: 'uploading',
|
|
41
|
+
progress: 0,
|
|
42
|
+
fileName: payload.fileName,
|
|
43
|
+
fileSize: payload.fileSize,
|
|
44
|
+
fileType: payload.fileType,
|
|
45
|
+
fileUrl: '',
|
|
46
|
+
bytesUploaded: 0,
|
|
47
|
+
totalBytes: payload.fileSize,
|
|
48
|
+
error: null
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
return { success: true }
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async updateProgress(payload: { progress: number; bytesUploaded: number; totalBytes: number }) {
|
|
55
|
+
const progress = Math.max(0, Math.min(100, payload.progress))
|
|
56
|
+
this.setState({
|
|
57
|
+
progress,
|
|
58
|
+
bytesUploaded: payload.bytesUploaded,
|
|
59
|
+
totalBytes: payload.totalBytes
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
return { success: true, progress }
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async completeUpload(payload: { fileUrl: string }) {
|
|
66
|
+
this.setState({
|
|
67
|
+
status: 'complete',
|
|
68
|
+
progress: 100,
|
|
69
|
+
fileUrl: payload.fileUrl,
|
|
70
|
+
error: null
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
return { success: true }
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async failUpload(payload: { error: string }) {
|
|
77
|
+
this.setState({
|
|
78
|
+
status: 'error',
|
|
79
|
+
error: payload.error || 'Upload failed'
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
return { success: true }
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async reset() {
|
|
86
|
+
this.setState({ ...defaultState })
|
|
87
|
+
return { success: true }
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Client: useLiveUpload + Widget
|
|
93
|
+
|
|
94
|
+
```typescript
|
|
95
|
+
// app/client/src/live/UploadDemo.tsx
|
|
96
|
+
import { useLiveUpload } from './useLiveUpload'
|
|
97
|
+
import { LiveUploadWidget } from '../components/LiveUploadWidget'
|
|
98
|
+
|
|
99
|
+
export function UploadDemo() {
|
|
100
|
+
const { live } = useLiveUpload()
|
|
101
|
+
|
|
102
|
+
return (
|
|
103
|
+
<LiveUploadWidget live={live} />
|
|
104
|
+
)
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## Chunked Upload Flow
|
|
109
|
+
|
|
110
|
+
1. Client calls `startUpload()` (Live Component action).
|
|
111
|
+
2. Client streams file chunks over WebSocket with `useChunkedUpload`.
|
|
112
|
+
3. Server assembles file in `uploads/` and returns `/uploads/...`.
|
|
113
|
+
4. Client maps to `/api/uploads/...` for access.
|
|
114
|
+
|
|
115
|
+
## Error Handling
|
|
116
|
+
|
|
117
|
+
- If an action throws, the error surfaces in `live.$error` on the client.
|
|
118
|
+
- The widget shows `localError || state.error || $error`.
|
|
119
|
+
|
|
120
|
+
## Files Involved
|
|
121
|
+
|
|
122
|
+
**Server**
|
|
123
|
+
- `app/server/live/LiveUpload.ts`
|
|
124
|
+
- `core/server/live/FileUploadManager.ts`
|
|
125
|
+
- `core/server/live/websocket-plugin.ts`
|
|
126
|
+
|
|
127
|
+
**Client**
|
|
128
|
+
- `core/client/hooks/useChunkedUpload.ts`
|
|
129
|
+
- `core/client/hooks/useLiveUpload.ts`
|
|
130
|
+
- `app/client/src/components/LiveUploadWidget.tsx`
|