create-fluxstack 1.10.1 → 1.12.1
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 +161 -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 +127 -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,703 @@
|
|
|
1
|
+
# Live Components
|
|
2
|
+
|
|
3
|
+
**Version:** 1.12.0 | **Updated:** 2025-02-09
|
|
4
|
+
|
|
5
|
+
## Quick Facts
|
|
6
|
+
|
|
7
|
+
- Server-side state management with WebSocket sync
|
|
8
|
+
- **Reactive state Proxy** - `this.state.count++` auto-syncs
|
|
9
|
+
- Automatic state persistence and re-hydration
|
|
10
|
+
- Room-based event system for multi-user sync
|
|
11
|
+
- Type-safe client-server communication (FluxStackWebSocket)
|
|
12
|
+
- Built-in connection management and recovery
|
|
13
|
+
- **Client component links** - Ctrl+Click navigation
|
|
14
|
+
|
|
15
|
+
## LiveComponent Class Structure (v1.12.0)
|
|
16
|
+
|
|
17
|
+
Server-side component extends `LiveComponent` with **static defaultState**:
|
|
18
|
+
|
|
19
|
+
```typescript
|
|
20
|
+
// app/server/live/LiveCounter.ts
|
|
21
|
+
import { LiveComponent } from '@core/types/types'
|
|
22
|
+
|
|
23
|
+
// Componente Cliente (Ctrl+Click para navegar)
|
|
24
|
+
import type { CounterDemo as _Client } from '@client/src/live/CounterDemo'
|
|
25
|
+
|
|
26
|
+
export class LiveCounter extends LiveComponent<typeof LiveCounter.defaultState> {
|
|
27
|
+
static componentName = 'LiveCounter'
|
|
28
|
+
static defaultState = {
|
|
29
|
+
count: 0
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// ✅ Reactive state - auto-syncs with frontend
|
|
33
|
+
async increment() {
|
|
34
|
+
this.state.count++
|
|
35
|
+
return { success: true, count: this.state.count }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async decrement() {
|
|
39
|
+
this.state.count--
|
|
40
|
+
return { success: true, count: this.state.count }
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async reset() {
|
|
44
|
+
this.state.count = 0
|
|
45
|
+
return { success: true }
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Key Changes in v1.12.0
|
|
51
|
+
|
|
52
|
+
1. **Static defaultState inside class** - No external export needed
|
|
53
|
+
2. **Reactive Proxy** - `this.state.count++` triggers sync automatically
|
|
54
|
+
3. **No constructor needed** - Base class handles defaultState merge
|
|
55
|
+
4. **Client link** - `import type { Demo as _Client }` enables Ctrl+Click
|
|
56
|
+
5. **Type-safe WebSocket** - `FluxStackWebSocket` interface
|
|
57
|
+
|
|
58
|
+
### With Room Events (Advanced)
|
|
59
|
+
|
|
60
|
+
```typescript
|
|
61
|
+
import { LiveComponent, type FluxStackWebSocket } from '@core/types/types'
|
|
62
|
+
|
|
63
|
+
export class LiveCounter extends LiveComponent<typeof LiveCounter.defaultState> {
|
|
64
|
+
static componentName = 'LiveCounter'
|
|
65
|
+
static defaultState = {
|
|
66
|
+
count: 0,
|
|
67
|
+
lastUpdatedBy: null as string | null,
|
|
68
|
+
connectedUsers: 0
|
|
69
|
+
}
|
|
70
|
+
protected roomType = 'counter'
|
|
71
|
+
|
|
72
|
+
// Constructor only needed for room event subscriptions
|
|
73
|
+
constructor(
|
|
74
|
+
initialState: Partial<typeof LiveCounter.defaultState>,
|
|
75
|
+
ws: FluxStackWebSocket,
|
|
76
|
+
options?: { room?: string; userId?: string }
|
|
77
|
+
) {
|
|
78
|
+
super(initialState, ws, options)
|
|
79
|
+
|
|
80
|
+
this.onRoomEvent<{ count: number }>('COUNT_CHANGED', (data) => {
|
|
81
|
+
this.setState({ count: data.count })
|
|
82
|
+
})
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async increment() {
|
|
86
|
+
this.state.count++
|
|
87
|
+
this.emitRoomEventWithState('COUNT_CHANGED',
|
|
88
|
+
{ count: this.state.count },
|
|
89
|
+
{ count: this.state.count }
|
|
90
|
+
)
|
|
91
|
+
return { success: true, count: this.state.count }
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
destroy() {
|
|
95
|
+
super.destroy()
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Lifecycle Methods
|
|
101
|
+
|
|
102
|
+
```typescript
|
|
103
|
+
export class MyComponent extends LiveComponent<typeof MyComponent.defaultState> {
|
|
104
|
+
static componentName = 'MyComponent'
|
|
105
|
+
static defaultState = {
|
|
106
|
+
// Define state here
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Constructor ONLY needed if:
|
|
110
|
+
// - Subscribing to room events
|
|
111
|
+
// - Custom initialization logic
|
|
112
|
+
// Otherwise, omit it entirely!
|
|
113
|
+
|
|
114
|
+
destroy() {
|
|
115
|
+
// Cleanup subscriptions, timers, etc.
|
|
116
|
+
super.destroy()
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## State Management
|
|
122
|
+
|
|
123
|
+
### Reactive State (v1.12.0) ✨
|
|
124
|
+
|
|
125
|
+
State mutations auto-sync with frontend via Proxy:
|
|
126
|
+
|
|
127
|
+
```typescript
|
|
128
|
+
// ✅ Direct mutation - auto-syncs!
|
|
129
|
+
this.state.count++
|
|
130
|
+
this.state.message = 'Hello'
|
|
131
|
+
|
|
132
|
+
// No need for setState() on single property updates
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### setState (Batch Updates)
|
|
136
|
+
|
|
137
|
+
Use `setState` for multiple properties at once (single emit):
|
|
138
|
+
|
|
139
|
+
```typescript
|
|
140
|
+
// ✅ Batch update - one sync event
|
|
141
|
+
this.setState({
|
|
142
|
+
count: newCount,
|
|
143
|
+
lastUpdatedBy: userId,
|
|
144
|
+
updatedAt: new Date().toISOString()
|
|
145
|
+
})
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### getSerializableState
|
|
149
|
+
|
|
150
|
+
Get current state for serialization:
|
|
151
|
+
|
|
152
|
+
```typescript
|
|
153
|
+
const currentState = this.getSerializableState()
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### State Persistence
|
|
157
|
+
|
|
158
|
+
State is automatically signed and persisted on client. On reconnection, state is re-hydrated:
|
|
159
|
+
|
|
160
|
+
```typescript
|
|
161
|
+
// Automatic - no code needed
|
|
162
|
+
// Client stores signed state in localStorage
|
|
163
|
+
// On reconnect, sends signed state to server
|
|
164
|
+
// Server validates signature and restores component
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
## Room Events System
|
|
168
|
+
|
|
169
|
+
### Subscribe to Room Events
|
|
170
|
+
|
|
171
|
+
```typescript
|
|
172
|
+
constructor(initialState, ws, options) {
|
|
173
|
+
super(initialState, ws, options)
|
|
174
|
+
|
|
175
|
+
// Listen for room events
|
|
176
|
+
this.onRoomEvent<{ count: number }>('COUNT_CHANGED', (data) => {
|
|
177
|
+
this.setState({ count: data.count })
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
this.onRoomEvent<{ message: string }>('MESSAGE_SENT', (data) => {
|
|
181
|
+
// Handle message
|
|
182
|
+
})
|
|
183
|
+
}
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
### Emit Room Events
|
|
187
|
+
|
|
188
|
+
```typescript
|
|
189
|
+
// Emit event to all room members
|
|
190
|
+
this.emitRoomEvent('MESSAGE_SENT', {
|
|
191
|
+
message: 'Hello',
|
|
192
|
+
userId: this.userId
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
// Emit event AND update local state
|
|
196
|
+
this.emitRoomEventWithState('COUNT_CHANGED',
|
|
197
|
+
{ count: newCount }, // Event data
|
|
198
|
+
{ count: newCount } // State update
|
|
199
|
+
)
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
### Room Subscription
|
|
203
|
+
|
|
204
|
+
Components automatically join rooms specified in options:
|
|
205
|
+
|
|
206
|
+
```typescript
|
|
207
|
+
// Client-side
|
|
208
|
+
const counter = Live.use(LiveCounter, {
|
|
209
|
+
room: 'global-counter' // All instances in this room sync
|
|
210
|
+
})
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
## Actions
|
|
214
|
+
|
|
215
|
+
Actions are methods called from the client:
|
|
216
|
+
|
|
217
|
+
```typescript
|
|
218
|
+
// Server-side
|
|
219
|
+
export class LiveForm extends LiveComponent<FormState> {
|
|
220
|
+
async submit() {
|
|
221
|
+
const { name, email } = this.state
|
|
222
|
+
|
|
223
|
+
if (!name || !email) {
|
|
224
|
+
throw new Error('Name and email required')
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Process submission
|
|
228
|
+
this.setState({ submitted: true })
|
|
229
|
+
|
|
230
|
+
return { success: true, data: { name, email } }
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
async validate() {
|
|
234
|
+
const errors: Record<string, string> = {}
|
|
235
|
+
|
|
236
|
+
if (!this.state.name) errors.name = 'Name required'
|
|
237
|
+
if (!this.state.email) errors.email = 'Email required'
|
|
238
|
+
|
|
239
|
+
return { valid: Object.keys(errors).length === 0, errors }
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
## Client-Side Integration
|
|
245
|
+
|
|
246
|
+
### Provider Setup
|
|
247
|
+
|
|
248
|
+
Wrap app with LiveComponentsProvider:
|
|
249
|
+
|
|
250
|
+
```typescript
|
|
251
|
+
// app/client/src/App.tsx
|
|
252
|
+
import { LiveComponentsProvider } from '@/core/client'
|
|
253
|
+
|
|
254
|
+
function App() {
|
|
255
|
+
return (
|
|
256
|
+
<LiveComponentsProvider
|
|
257
|
+
url="ws://localhost:3000"
|
|
258
|
+
autoConnect={true}
|
|
259
|
+
reconnectInterval={1000}
|
|
260
|
+
debug={true}
|
|
261
|
+
>
|
|
262
|
+
<AppContent />
|
|
263
|
+
</LiveComponentsProvider>
|
|
264
|
+
)
|
|
265
|
+
}
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
### Using Components
|
|
269
|
+
|
|
270
|
+
```typescript
|
|
271
|
+
import { Live } from '@/core/client'
|
|
272
|
+
import { LiveCounter } from '@server/live/LiveCounter'
|
|
273
|
+
|
|
274
|
+
export function CounterDemo() {
|
|
275
|
+
// Mount component with options
|
|
276
|
+
const counter = Live.use(LiveCounter, {
|
|
277
|
+
room: 'global-counter',
|
|
278
|
+
initialState: LiveCounter.defaultState // ✅ Use static defaultState
|
|
279
|
+
})
|
|
280
|
+
|
|
281
|
+
// Access state
|
|
282
|
+
const count = counter.$state.count
|
|
283
|
+
|
|
284
|
+
// Check connection status
|
|
285
|
+
const isConnected = counter.$connected
|
|
286
|
+
|
|
287
|
+
// Check loading state
|
|
288
|
+
const isLoading = counter.$loading
|
|
289
|
+
|
|
290
|
+
// Call actions
|
|
291
|
+
const handleIncrement = async () => {
|
|
292
|
+
await counter.increment()
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
return (
|
|
296
|
+
<div>
|
|
297
|
+
<p>Count: {count}</p>
|
|
298
|
+
<p>Status: {isConnected ? 'Connected' : 'Disconnected'}</p>
|
|
299
|
+
<button onClick={handleIncrement} disabled={isLoading}>
|
|
300
|
+
Increment
|
|
301
|
+
</button>
|
|
302
|
+
</div>
|
|
303
|
+
)
|
|
304
|
+
}
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
### Field Binding
|
|
308
|
+
|
|
309
|
+
For form components, use `$field` helper:
|
|
310
|
+
|
|
311
|
+
```typescript
|
|
312
|
+
const form = Live.use(LiveForm)
|
|
313
|
+
|
|
314
|
+
// Sync on blur
|
|
315
|
+
<input {...form.$field('name', { syncOn: 'blur' })} />
|
|
316
|
+
|
|
317
|
+
// Sync on change with debounce
|
|
318
|
+
<input {...form.$field('email', { syncOn: 'change', debounce: 500 })} />
|
|
319
|
+
|
|
320
|
+
// Manual sync
|
|
321
|
+
await form.$sync()
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
### Client API
|
|
325
|
+
|
|
326
|
+
```typescript
|
|
327
|
+
// State access
|
|
328
|
+
counter.$state.count
|
|
329
|
+
|
|
330
|
+
// Connection status
|
|
331
|
+
counter.$connected
|
|
332
|
+
|
|
333
|
+
// Loading state
|
|
334
|
+
counter.$loading
|
|
335
|
+
|
|
336
|
+
// Call action
|
|
337
|
+
await counter.increment()
|
|
338
|
+
|
|
339
|
+
// Field binding (forms)
|
|
340
|
+
form.$field('fieldName', options)
|
|
341
|
+
|
|
342
|
+
// Manual sync
|
|
343
|
+
await form.$sync()
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
## Component Registry
|
|
347
|
+
|
|
348
|
+
Components are auto-discovered from `app/server/live/`:
|
|
349
|
+
|
|
350
|
+
```typescript
|
|
351
|
+
// app/server/live/register-components.ts
|
|
352
|
+
import { componentRegistry } from '@core/server/live'
|
|
353
|
+
|
|
354
|
+
// Auto-discover all components in directory
|
|
355
|
+
await componentRegistry.autoDiscoverComponents('./app/server/live')
|
|
356
|
+
|
|
357
|
+
// Or manually register
|
|
358
|
+
componentRegistry.registerComponent({
|
|
359
|
+
name: 'MyComponent',
|
|
360
|
+
component: MyComponent,
|
|
361
|
+
initialState: defaultState
|
|
362
|
+
}, '1.0.0')
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
## WebSocket Connection Handling
|
|
366
|
+
|
|
367
|
+
### Automatic Reconnection
|
|
368
|
+
|
|
369
|
+
Client automatically reconnects on disconnect:
|
|
370
|
+
|
|
371
|
+
```typescript
|
|
372
|
+
<LiveComponentsProvider
|
|
373
|
+
reconnectInterval={1000} // Retry every 1 second
|
|
374
|
+
autoConnect={true}
|
|
375
|
+
>
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
### State Re-hydration
|
|
379
|
+
|
|
380
|
+
On reconnect, components restore previous state:
|
|
381
|
+
|
|
382
|
+
1. Client stores signed state in localStorage
|
|
383
|
+
2. On reconnect, sends signed state to server
|
|
384
|
+
3. Server validates signature
|
|
385
|
+
4. Component re-hydrated with previous state
|
|
386
|
+
|
|
387
|
+
No manual code needed - automatic.
|
|
388
|
+
|
|
389
|
+
## Multi-User Synchronization
|
|
390
|
+
|
|
391
|
+
### Room-Based Sync
|
|
392
|
+
|
|
393
|
+
All components in same room receive events:
|
|
394
|
+
|
|
395
|
+
```typescript
|
|
396
|
+
// User A increments
|
|
397
|
+
await counter.increment()
|
|
398
|
+
// Emits COUNT_CHANGED to room
|
|
399
|
+
|
|
400
|
+
// User B's component receives event
|
|
401
|
+
this.onRoomEvent('COUNT_CHANGED', (data) => {
|
|
402
|
+
this.setState({ count: data.count })
|
|
403
|
+
})
|
|
404
|
+
// User B sees updated count
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
### User Tracking
|
|
408
|
+
|
|
409
|
+
Track connected users in room:
|
|
410
|
+
|
|
411
|
+
```typescript
|
|
412
|
+
constructor(initialState, ws, options) {
|
|
413
|
+
super(initialState, ws, options)
|
|
414
|
+
|
|
415
|
+
// Notify room of new user
|
|
416
|
+
const newCount = this.state.connectedUsers + 1
|
|
417
|
+
this.emitRoomEventWithState('USER_COUNT_CHANGED',
|
|
418
|
+
{ connectedUsers: newCount },
|
|
419
|
+
{ connectedUsers: newCount }
|
|
420
|
+
)
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
destroy() {
|
|
424
|
+
// Notify room of user leaving
|
|
425
|
+
const newCount = Math.max(0, this.state.connectedUsers - 1)
|
|
426
|
+
this.emitRoomEvent('USER_COUNT_CHANGED', { connectedUsers: newCount })
|
|
427
|
+
super.destroy()
|
|
428
|
+
}
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
## Error Handling
|
|
432
|
+
|
|
433
|
+
```typescript
|
|
434
|
+
// Server-side - throw errors
|
|
435
|
+
async submit() {
|
|
436
|
+
if (!this.state.email) {
|
|
437
|
+
throw new Error('Email required')
|
|
438
|
+
}
|
|
439
|
+
// Process...
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// Client-side - catch errors
|
|
443
|
+
try {
|
|
444
|
+
await form.submit()
|
|
445
|
+
} catch (error) {
|
|
446
|
+
alert(error.message)
|
|
447
|
+
}
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
## Performance Monitoring
|
|
451
|
+
|
|
452
|
+
Built-in performance tracking:
|
|
453
|
+
|
|
454
|
+
```typescript
|
|
455
|
+
// Automatic metrics collection
|
|
456
|
+
// - Render times
|
|
457
|
+
// - Action execution times
|
|
458
|
+
// - Error counts
|
|
459
|
+
// - Memory usage
|
|
460
|
+
|
|
461
|
+
// Access via registry
|
|
462
|
+
const health = componentRegistry.getComponentHealth(componentId)
|
|
463
|
+
// { status: 'healthy', metrics: {...} }
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
## Component Organization
|
|
467
|
+
|
|
468
|
+
```
|
|
469
|
+
app/server/live/
|
|
470
|
+
├── LiveCounter.ts # Counter component
|
|
471
|
+
├── LiveForm.ts # Form component
|
|
472
|
+
├── LiveChat.ts # Chat component
|
|
473
|
+
├── LiveLocalCounter.ts # Local counter (no room)
|
|
474
|
+
└── register-components.ts # Registration
|
|
475
|
+
|
|
476
|
+
app/client/src/live/
|
|
477
|
+
├── CounterDemo.tsx # Counter UI
|
|
478
|
+
├── FormDemo.tsx # Form UI
|
|
479
|
+
├── ChatDemo.tsx # Chat UI
|
|
480
|
+
└── ...
|
|
481
|
+
```
|
|
482
|
+
|
|
483
|
+
Each server file contains:
|
|
484
|
+
- `static componentName` - Component identifier
|
|
485
|
+
- `static defaultState` - Initial state object
|
|
486
|
+
- Component class extending `LiveComponent`
|
|
487
|
+
- Client link via `import type { Demo as _Client }`
|
|
488
|
+
|
|
489
|
+
## Testing Components
|
|
490
|
+
|
|
491
|
+
```typescript
|
|
492
|
+
// tests/unit/live/LiveCounter.test.ts
|
|
493
|
+
import { describe, it, expect } from 'vitest'
|
|
494
|
+
import { LiveCounter, defaultState } from '@app/server/live/LiveCounter'
|
|
495
|
+
|
|
496
|
+
describe('LiveCounter', () => {
|
|
497
|
+
it('should increment count', async () => {
|
|
498
|
+
const mockWs = { send: vi.fn() }
|
|
499
|
+
const counter = new LiveCounter(defaultState, mockWs)
|
|
500
|
+
|
|
501
|
+
const result = await counter.increment()
|
|
502
|
+
|
|
503
|
+
expect(result.success).toBe(true)
|
|
504
|
+
expect(result.count).toBe(1)
|
|
505
|
+
expect(counter.state.count).toBe(1)
|
|
506
|
+
})
|
|
507
|
+
})
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
## Advanced: Dependencies
|
|
511
|
+
|
|
512
|
+
Register services for dependency injection:
|
|
513
|
+
|
|
514
|
+
```typescript
|
|
515
|
+
// Register service
|
|
516
|
+
componentRegistry.registerService('database', () => db)
|
|
517
|
+
|
|
518
|
+
// Register dependencies
|
|
519
|
+
componentRegistry.registerDependencies('MyComponent', [
|
|
520
|
+
{ name: 'database', version: '1.0.0', required: true, factory: () => db }
|
|
521
|
+
])
|
|
522
|
+
|
|
523
|
+
// Component receives service
|
|
524
|
+
export class MyComponent extends LiveComponent<State> {
|
|
525
|
+
private database: any
|
|
526
|
+
|
|
527
|
+
setDatabase(db: any) {
|
|
528
|
+
this.database = db
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
```
|
|
532
|
+
|
|
533
|
+
## Critical Rules
|
|
534
|
+
|
|
535
|
+
**ALWAYS:**
|
|
536
|
+
- Define `static componentName` matching class name
|
|
537
|
+
- Define `static defaultState` inside the class
|
|
538
|
+
- Use `typeof ClassName.defaultState` for type parameter
|
|
539
|
+
- Call `super.destroy()` in destroy method if overriding
|
|
540
|
+
- Use `emitRoomEventWithState` for state changes in rooms
|
|
541
|
+
- Handle errors in actions (throw Error)
|
|
542
|
+
- Add client link: `import type { Demo as _Client } from '@client/...'`
|
|
543
|
+
|
|
544
|
+
**NEVER:**
|
|
545
|
+
- Export separate `defaultState` constant (use static)
|
|
546
|
+
- Create constructor just to call super() (not needed)
|
|
547
|
+
- Forget `static componentName` (breaks minification)
|
|
548
|
+
- Emit room events without subscribing first
|
|
549
|
+
- Store non-serializable data in state
|
|
550
|
+
|
|
551
|
+
**STATE UPDATES (v1.12.0):**
|
|
552
|
+
```typescript
|
|
553
|
+
// ✅ Single property - use direct mutation
|
|
554
|
+
this.state.count++
|
|
555
|
+
|
|
556
|
+
// ✅ Multiple properties - use setState (one sync)
|
|
557
|
+
this.setState({ a: 1, b: 2, c: 3 })
|
|
558
|
+
|
|
559
|
+
// ❌ Don't use setState for single property (unnecessary)
|
|
560
|
+
this.setState({ count: this.state.count + 1 })
|
|
561
|
+
```
|
|
562
|
+
|
|
563
|
+
---
|
|
564
|
+
|
|
565
|
+
## Live Upload (Chunked Upload via WebSocket)
|
|
566
|
+
|
|
567
|
+
This project includes a Live Component-based upload system that streams file chunks
|
|
568
|
+
over the Live Components WebSocket. The client uses a chunked upload hook; the server
|
|
569
|
+
tracks progress and assembles the file in `uploads/`.
|
|
570
|
+
|
|
571
|
+
### Server: LiveUpload Component
|
|
572
|
+
|
|
573
|
+
Create server-side upload actions inside a Live Component. This example is the base
|
|
574
|
+
implementation used by the demos:
|
|
575
|
+
|
|
576
|
+
```typescript
|
|
577
|
+
// app/server/live/LiveUpload.ts
|
|
578
|
+
import { LiveComponent } from '@core/types/types'
|
|
579
|
+
import { liveUploadDefaultState, type LiveUploadState } from '@app/shared'
|
|
580
|
+
|
|
581
|
+
export const defaultState: LiveUploadState = liveUploadDefaultState
|
|
582
|
+
|
|
583
|
+
export class LiveUpload extends LiveComponent<LiveUploadState> {
|
|
584
|
+
static defaultState = defaultState
|
|
585
|
+
|
|
586
|
+
constructor(initialState: Partial<typeof defaultState>, ws: any, options?: { room?: string; userId?: string }) {
|
|
587
|
+
super({ ...defaultState, ...initialState }, ws, options)
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
async startUpload(payload: { fileName: string; fileSize: number; fileType: string }) {
|
|
591
|
+
// Basic validation (example)
|
|
592
|
+
const normalized = payload.fileName.toLowerCase()
|
|
593
|
+
if (normalized.includes('..') || normalized.includes('/') || normalized.includes('\\')) {
|
|
594
|
+
throw new Error('Invalid file name')
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
const ext = normalized.includes('.') ? normalized.split('.').pop() || '' : ''
|
|
598
|
+
const blocked = ['exe', 'bat', 'cmd', 'sh', 'ps1', 'msi', 'jar']
|
|
599
|
+
if (ext && blocked.includes(ext)) {
|
|
600
|
+
throw new Error(`File extension not allowed: .${ext}`)
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
this.setState({
|
|
604
|
+
status: 'uploading',
|
|
605
|
+
progress: 0,
|
|
606
|
+
fileName: payload.fileName,
|
|
607
|
+
fileSize: payload.fileSize,
|
|
608
|
+
fileType: payload.fileType,
|
|
609
|
+
fileUrl: '',
|
|
610
|
+
bytesUploaded: 0,
|
|
611
|
+
totalBytes: payload.fileSize,
|
|
612
|
+
error: null
|
|
613
|
+
})
|
|
614
|
+
|
|
615
|
+
return { success: true }
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
async updateProgress(payload: { progress: number; bytesUploaded: number; totalBytes: number }) {
|
|
619
|
+
const progress = Math.max(0, Math.min(100, payload.progress))
|
|
620
|
+
this.setState({
|
|
621
|
+
progress,
|
|
622
|
+
bytesUploaded: payload.bytesUploaded,
|
|
623
|
+
totalBytes: payload.totalBytes
|
|
624
|
+
})
|
|
625
|
+
|
|
626
|
+
return { success: true, progress }
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
async completeUpload(payload: { fileUrl: string }) {
|
|
630
|
+
this.setState({
|
|
631
|
+
status: 'complete',
|
|
632
|
+
progress: 100,
|
|
633
|
+
fileUrl: payload.fileUrl,
|
|
634
|
+
error: null
|
|
635
|
+
})
|
|
636
|
+
|
|
637
|
+
return { success: true }
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
async failUpload(payload: { error: string }) {
|
|
641
|
+
this.setState({
|
|
642
|
+
status: 'error',
|
|
643
|
+
error: payload.error || 'Upload failed'
|
|
644
|
+
})
|
|
645
|
+
|
|
646
|
+
return { success: true }
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
async reset() {
|
|
650
|
+
this.setState({ ...defaultState })
|
|
651
|
+
return { success: true }
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
```
|
|
655
|
+
|
|
656
|
+
### Client: useLiveUpload + Widget
|
|
657
|
+
|
|
658
|
+
Use the client hook and UI widget to wire the upload to the Live Component:
|
|
659
|
+
|
|
660
|
+
```typescript
|
|
661
|
+
// app/client/src/live/UploadDemo.tsx
|
|
662
|
+
import { useLiveUpload } from './useLiveUpload'
|
|
663
|
+
import { LiveUploadWidget } from '../components/LiveUploadWidget'
|
|
664
|
+
|
|
665
|
+
export function UploadDemo() {
|
|
666
|
+
const { live } = useLiveUpload()
|
|
667
|
+
|
|
668
|
+
return (
|
|
669
|
+
<LiveUploadWidget live={live} />
|
|
670
|
+
)
|
|
671
|
+
}
|
|
672
|
+
```
|
|
673
|
+
|
|
674
|
+
### Chunked Upload Flow
|
|
675
|
+
|
|
676
|
+
1. Client calls `startUpload()` (Live Component action).
|
|
677
|
+
2. Client streams file chunks over WebSocket with `useChunkedUpload`.
|
|
678
|
+
3. Server assembles file in `uploads/` and returns `/uploads/...`.
|
|
679
|
+
4. Client maps to `/api/uploads/...` for access.
|
|
680
|
+
|
|
681
|
+
### Error Handling
|
|
682
|
+
|
|
683
|
+
- If an action throws, the error surfaces in `live.$error` on the client.
|
|
684
|
+
- The widget shows `localError || state.error || $error`.
|
|
685
|
+
|
|
686
|
+
### Files Involved
|
|
687
|
+
|
|
688
|
+
**Server**
|
|
689
|
+
- `app/server/live/LiveUpload.ts`
|
|
690
|
+
- `core/server/live/FileUploadManager.ts` (chunk handling + file assembly)
|
|
691
|
+
- `core/server/live/websocket-plugin.ts` (upload message routing)
|
|
692
|
+
|
|
693
|
+
**Client**
|
|
694
|
+
- `core/client/hooks/useChunkedUpload.ts` (streaming chunks)
|
|
695
|
+
- `core/client/hooks/useLiveUpload.ts` (Live Component wrapper)
|
|
696
|
+
- `app/client/src/components/LiveUploadWidget.tsx` (UI)
|
|
697
|
+
|
|
698
|
+
## Related
|
|
699
|
+
|
|
700
|
+
- [Project Structure](../patterns/project-structure.md)
|
|
701
|
+
- [Type Safety Patterns](../patterns/type-safety.md)
|
|
702
|
+
- [WebSocket Plugin](../core/plugin-system.md)
|
|
703
|
+
- [Live Upload](./live-upload.md)
|