create-fluxstack 1.14.0 → 1.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LLMD/INDEX.md +4 -3
- package/LLMD/resources/live-binary-delta.md +507 -0
- package/LLMD/resources/live-components.md +208 -12
- package/LLMD/resources/live-rooms.md +731 -333
- package/app/client/.live-stubs/LiveAdminPanel.js +5 -0
- package/app/client/.live-stubs/LiveCounter.js +9 -0
- package/app/client/.live-stubs/LiveForm.js +11 -0
- package/app/client/.live-stubs/LiveLocalCounter.js +8 -0
- package/app/client/.live-stubs/LivePingPong.js +10 -0
- package/app/client/.live-stubs/LiveRoomChat.js +11 -0
- package/app/client/.live-stubs/LiveSharedCounter.js +10 -0
- package/app/client/.live-stubs/LiveUpload.js +15 -0
- package/app/client/src/App.tsx +19 -7
- package/app/client/src/components/AppLayout.tsx +18 -10
- 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/auth/DevAuthProvider.ts +2 -2
- package/app/server/auth/JWTAuthProvider.example.ts +2 -2
- package/app/server/index.ts +2 -2
- package/app/server/live/LiveAdminPanel.ts +1 -1
- package/app/server/live/LivePingPong.ts +61 -0
- package/app/server/live/LiveProtectedChat.ts +1 -1
- 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/app/server/routes/room.routes.ts +1 -2
- package/core/build/live-components-generator.ts +11 -2
- package/core/build/vite-plugins.ts +28 -0
- package/core/client/hooks/useLiveUpload.ts +3 -4
- package/core/client/index.ts +25 -35
- package/core/framework/server.ts +1 -1
- package/core/server/index.ts +1 -2
- package/core/server/live/auto-generated-components.ts +5 -8
- package/core/server/live/index.ts +90 -21
- package/core/server/live/websocket-plugin.ts +54 -1079
- package/core/types/types.ts +76 -1025
- package/core/utils/version.ts +1 -1
- package/create-fluxstack.ts +1 -1
- package/package.json +100 -95
- package/plugins/crypto-auth/index.ts +1 -1
- package/plugins/crypto-auth/server/CryptoAuthLiveProvider.ts +2 -2
- package/tsconfig.json +4 -1
- package/vite.config.ts +40 -12
- package/app/client/src/live/ChatDemo.tsx +0 -107
- package/app/client/src/live/LiveDebuggerPanel.tsx +0 -779
- package/app/server/live/LiveChat.ts +0 -78
- package/core/client/LiveComponentsProvider.tsx +0 -531
- package/core/client/components/Live.tsx +0 -111
- package/core/client/components/LiveDebugger.tsx +0 -1324
- package/core/client/hooks/AdaptiveChunkSizer.ts +0 -215
- package/core/client/hooks/state-validator.ts +0 -130
- package/core/client/hooks/useChunkedUpload.ts +0 -359
- package/core/client/hooks/useLiveChunkedUpload.ts +0 -87
- package/core/client/hooks/useLiveComponent.ts +0 -853
- package/core/client/hooks/useLiveDebugger.ts +0 -392
- package/core/client/hooks/useRoom.ts +0 -409
- package/core/client/hooks/useRoomProxy.ts +0 -382
- package/core/server/live/ComponentRegistry.ts +0 -1128
- package/core/server/live/FileUploadManager.ts +0 -446
- package/core/server/live/LiveComponentPerformanceMonitor.ts +0 -931
- package/core/server/live/LiveDebugger.ts +0 -462
- package/core/server/live/LiveLogger.ts +0 -144
- package/core/server/live/LiveRoomManager.ts +0 -278
- package/core/server/live/RoomEventBus.ts +0 -234
- package/core/server/live/RoomStateManager.ts +0 -172
- package/core/server/live/SingleConnectionManager.ts +0 -0
- package/core/server/live/StateSignature.ts +0 -705
- package/core/server/live/WebSocketConnectionManager.ts +0 -710
- package/core/server/live/auth/LiveAuthContext.ts +0 -71
- package/core/server/live/auth/LiveAuthManager.ts +0 -304
- package/core/server/live/auth/index.ts +0 -19
- package/core/server/live/auth/types.ts +0 -179
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
# Live Components
|
|
2
2
|
|
|
3
|
-
**Version:** 1.
|
|
3
|
+
**Version:** 1.14.0 | **Updated:** 2025-02-27
|
|
4
4
|
|
|
5
5
|
## Quick Facts
|
|
6
6
|
|
|
7
7
|
- Server-side state management with WebSocket sync
|
|
8
8
|
- **Direct state access** - `this.count++` auto-syncs (v1.13.0)
|
|
9
|
+
- **Lifecycle hooks** - `onMount()` / `onDestroy()` for proper initialization and cleanup (v1.14.0)
|
|
10
|
+
- **HMR persistence** - `static persistent` + `this.$persistent` survives hot reloads (v1.14.0)
|
|
11
|
+
- **Singleton components** - `static singleton = true` for shared server-side instances (v1.14.0)
|
|
9
12
|
- **Mandatory `publicActions`** - Only whitelisted methods are callable from client (secure by default)
|
|
13
|
+
- **Helpful error messages** - Forgotten `publicActions` entries show exactly what to fix (v1.14.0)
|
|
10
14
|
- Automatic state persistence and re-hydration (with anti-replay nonces)
|
|
11
15
|
- Room-based event system for multi-user sync
|
|
12
16
|
- Type-safe client-server communication (FluxStackWebSocket)
|
|
@@ -53,6 +57,13 @@ export class LiveCounter extends LiveComponent<typeof LiveCounter.defaultState>
|
|
|
53
57
|
}
|
|
54
58
|
```
|
|
55
59
|
|
|
60
|
+
### Key Changes in v1.14.0
|
|
61
|
+
|
|
62
|
+
1. **Lifecycle hooks** - `onMount()` (async) and `onDestroy()` (sync) replace constructor/destroy workarounds
|
|
63
|
+
2. **HMR persistence** - `static persistent` + `this.$persistent` for data that survives hot module reloads
|
|
64
|
+
3. **Singleton components** - `static singleton = true` for shared state across all connected clients
|
|
65
|
+
4. **Better publicActions errors** - Clear message when a method exists but is missing from `publicActions`
|
|
66
|
+
|
|
56
67
|
### Key Changes in v1.13.0
|
|
57
68
|
|
|
58
69
|
1. **Direct state access** - `this.count++` instead of `this.state.count++`
|
|
@@ -111,27 +122,207 @@ export class LiveCounter extends LiveComponent<typeof LiveCounter.defaultState>
|
|
|
111
122
|
}
|
|
112
123
|
```
|
|
113
124
|
|
|
114
|
-
## Lifecycle
|
|
125
|
+
## Lifecycle Hooks (v1.14.0)
|
|
126
|
+
|
|
127
|
+
Full lifecycle hook system — no more constructor workarounds:
|
|
115
128
|
|
|
116
129
|
```typescript
|
|
117
130
|
export class MyComponent extends LiveComponent<typeof MyComponent.defaultState> {
|
|
118
131
|
static componentName = 'MyComponent'
|
|
132
|
+
static publicActions = ['doWork'] as const
|
|
133
|
+
static defaultState = { users: [] as string[], ready: false, currentRoom: '' }
|
|
134
|
+
|
|
135
|
+
private _pollTimer?: NodeJS.Timeout
|
|
136
|
+
|
|
137
|
+
// 1️⃣ Called when WebSocket connection is established (before onMount)
|
|
138
|
+
protected onConnect() {
|
|
139
|
+
console.log('WebSocket connected for this component')
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// 2️⃣ Called AFTER component is fully mounted (rooms, auth, injections ready)
|
|
143
|
+
// Can be async!
|
|
144
|
+
protected async onMount() {
|
|
145
|
+
this.$room.join()
|
|
146
|
+
this.$room.on('user:joined', (user) => {
|
|
147
|
+
this.state.users = [...this.state.users, user]
|
|
148
|
+
})
|
|
149
|
+
const data = await fetchInitialData(this.$auth.user?.id)
|
|
150
|
+
this.state.ready = true
|
|
151
|
+
this._pollTimer = setInterval(() => this.poll(), 5000)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Called after state is restored from localStorage (rehydration)
|
|
155
|
+
protected onRehydrate(previousState: typeof MyComponent.defaultState) {
|
|
156
|
+
if (!previousState.ready) {
|
|
157
|
+
this.state.ready = false // Re-validate stale state
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Called after any state mutation (proxy or setState)
|
|
162
|
+
protected onStateChange(changes: Partial<typeof MyComponent.defaultState>) {
|
|
163
|
+
if ('users' in changes) {
|
|
164
|
+
console.log(`User count: ${this.state.users.length}`)
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Called when joining a room
|
|
169
|
+
protected onRoomJoin(roomId: string) {
|
|
170
|
+
this.state.currentRoom = roomId
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Called when leaving a room
|
|
174
|
+
protected onRoomLeave(roomId: string) {
|
|
175
|
+
if (this.state.currentRoom === roomId) this.state.currentRoom = ''
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Called before each action — return false to cancel
|
|
179
|
+
protected onAction(action: string, payload: any) {
|
|
180
|
+
console.log(`[${this.id}] ${action}`, payload)
|
|
181
|
+
// return false // ← would cancel the action
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Called when WebSocket drops (NOT on intentional unmount)
|
|
185
|
+
protected onDisconnect() {
|
|
186
|
+
console.log('Connection lost — saving recovery data')
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Called BEFORE internal cleanup (sync only)
|
|
190
|
+
protected onDestroy() {
|
|
191
|
+
clearInterval(this._pollTimer)
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async doWork() { /* ... */ }
|
|
195
|
+
private poll() { /* ... */ }
|
|
196
|
+
}
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
### Lifecycle Order
|
|
200
|
+
|
|
201
|
+
```
|
|
202
|
+
WebSocket connects
|
|
203
|
+
└→ onConnect()
|
|
204
|
+
└→ onMount() ← async, rooms/auth ready
|
|
205
|
+
└→ [component active]
|
|
206
|
+
├→ onAction(action, payload) ← before each action (return false to cancel)
|
|
207
|
+
├→ onStateChange(changes) ← after each state mutation
|
|
208
|
+
├→ onRoomJoin(roomId) ← when joining a room
|
|
209
|
+
└→ onRoomLeave(roomId) ← when leaving a room
|
|
210
|
+
|
|
211
|
+
Connection drops:
|
|
212
|
+
└→ onDisconnect() ← only on unexpected disconnect
|
|
213
|
+
└→ onDestroy() ← sync, before internal cleanup
|
|
214
|
+
|
|
215
|
+
Rehydration (reconnect with saved state):
|
|
216
|
+
└→ onConnect()
|
|
217
|
+
└→ onRehydrate(previousState)
|
|
218
|
+
└→ onMount()
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
### Rules
|
|
222
|
+
|
|
223
|
+
| Hook | Async? | When |
|
|
224
|
+
|------|--------|------|
|
|
225
|
+
| `onConnect()` | No | WebSocket established, before mount |
|
|
226
|
+
| `onMount()` | **Yes** | After all setup (rooms, auth, DI) |
|
|
227
|
+
| `onRehydrate(prevState)` | No | After state restored from localStorage |
|
|
228
|
+
| `onStateChange(changes)` | No | After every state mutation |
|
|
229
|
+
| `onRoomJoin(roomId)` | No | After `$room.join()` |
|
|
230
|
+
| `onRoomLeave(roomId)` | No | After `$room.leave()` |
|
|
231
|
+
| `onAction(action, payload)` | **Yes** | Before action execution (return `false` to cancel) |
|
|
232
|
+
| `onDisconnect()` | No | Connection lost (NOT intentional unmount) |
|
|
233
|
+
| `onDestroy()` | No | Before internal cleanup |
|
|
234
|
+
|
|
235
|
+
- All hooks are optional — override only what you need
|
|
236
|
+
- All hook errors are caught and logged — they never break the system
|
|
237
|
+
- Constructor is still needed ONLY for `this.onRoomEvent()` subscriptions
|
|
238
|
+
- All hooks are in BLOCKED_ACTIONS — clients cannot call them remotely
|
|
239
|
+
|
|
240
|
+
## HMR Persistence (v1.14.0)
|
|
241
|
+
|
|
242
|
+
Data in `static persistent` survives Hot Module Replacement reloads via `globalThis`:
|
|
243
|
+
|
|
244
|
+
```typescript
|
|
245
|
+
export class LiveMigration extends LiveComponent<typeof LiveMigration.defaultState> {
|
|
246
|
+
static componentName = 'LiveMigration'
|
|
247
|
+
static publicActions = ['runMigration'] as const
|
|
248
|
+
static defaultState = { status: 'idle', lastResult: '' }
|
|
249
|
+
|
|
250
|
+
// Define shape and defaults for persistent data
|
|
251
|
+
static persistent = {
|
|
252
|
+
cache: {} as Record<string, any>,
|
|
253
|
+
runCount: 0
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
protected onMount() {
|
|
257
|
+
this.$persistent.runCount++
|
|
258
|
+
console.log(`Mount #${this.$persistent.runCount}`) // Survives HMR!
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
async runMigration(payload: { key: string }) {
|
|
262
|
+
// Check HMR-safe cache
|
|
263
|
+
if (this.$persistent.cache[payload.key]) {
|
|
264
|
+
return { cached: true, result: this.$persistent.cache[payload.key] }
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const result = await expensiveComputation(payload.key)
|
|
268
|
+
this.$persistent.cache[payload.key] = result
|
|
269
|
+
this.state.lastResult = result
|
|
270
|
+
return { cached: false, result }
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
**Key facts:**
|
|
276
|
+
- `this.$persistent` reads from `globalThis.__fluxstack_persistent_{ComponentName}`
|
|
277
|
+
- Each component class has its own namespace
|
|
278
|
+
- Defaults come from `static persistent` — initialized once, then persisted
|
|
279
|
+
- Not sent to client — server-only
|
|
280
|
+
- `$persistent` is in BLOCKED_ACTIONS (can't be called from client)
|
|
281
|
+
|
|
282
|
+
## Singleton Components (v1.14.0)
|
|
283
|
+
|
|
284
|
+
When `static singleton = true`, only ONE server-side instance exists. All clients share the same state:
|
|
285
|
+
|
|
286
|
+
```typescript
|
|
287
|
+
export class LiveDashboard extends LiveComponent<typeof LiveDashboard.defaultState> {
|
|
288
|
+
static componentName = 'LiveDashboard'
|
|
289
|
+
static singleton = true // All clients share this instance
|
|
290
|
+
static publicActions = ['refresh', 'addAlert'] as const
|
|
119
291
|
static defaultState = {
|
|
120
|
-
|
|
292
|
+
visitors: 0,
|
|
293
|
+
alerts: [] as string[],
|
|
294
|
+
lastRefresh: ''
|
|
121
295
|
}
|
|
122
296
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
297
|
+
protected async onMount() {
|
|
298
|
+
this.state.visitors++
|
|
299
|
+
this.state.lastRefresh = new Date().toISOString()
|
|
300
|
+
}
|
|
127
301
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
302
|
+
async refresh() {
|
|
303
|
+
const data = await fetchDashboardData()
|
|
304
|
+
this.setState(data) // Broadcasts to ALL connected clients
|
|
305
|
+
return { success: true }
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
async addAlert(payload: { message: string }) {
|
|
309
|
+
this.state.alerts = [...this.state.alerts, payload.message]
|
|
310
|
+
// All clients see the new alert instantly
|
|
311
|
+
return { success: true }
|
|
131
312
|
}
|
|
132
313
|
}
|
|
133
314
|
```
|
|
134
315
|
|
|
316
|
+
**How it works:**
|
|
317
|
+
- First client to mount creates the singleton instance
|
|
318
|
+
- Subsequent clients join the existing instance and receive current state
|
|
319
|
+
- `emit` / `setState` / `this.state.x = y` broadcast to ALL connected WebSockets
|
|
320
|
+
- When a client disconnects, it's removed from the singleton's connections
|
|
321
|
+
- When the LAST client disconnects, the singleton is destroyed
|
|
322
|
+
- Stats visible at `/api/live/stats` (shows singleton connection counts)
|
|
323
|
+
|
|
324
|
+
**Use cases:** Shared dashboards, global migration state, admin panels, live counters
|
|
325
|
+
|
|
135
326
|
## State Management
|
|
136
327
|
|
|
137
328
|
### Reactive State Proxy (How It Works)
|
|
@@ -665,19 +856,23 @@ export class MyComponent extends LiveComponent<State> {
|
|
|
665
856
|
- Define `static defaultState` inside the class
|
|
666
857
|
- Use `typeof ClassName.defaultState` for type parameter
|
|
667
858
|
- Use `declare` for each state property (TypeScript type hint)
|
|
668
|
-
-
|
|
859
|
+
- Use `onMount()` for async initialization (rooms, auth, data fetching)
|
|
860
|
+
- Use `onDestroy()` for cleanup (timers, connections) — sync only
|
|
669
861
|
- Use `emitRoomEventWithState` for state changes in rooms
|
|
670
862
|
- Handle errors in actions (throw Error)
|
|
671
863
|
- Add client link: `import type { Demo as _Client } from '@client/...'`
|
|
864
|
+
- Use `$persistent` for data that should survive HMR reloads
|
|
865
|
+
- Use `static singleton = true` for shared cross-client state
|
|
672
866
|
|
|
673
867
|
**NEVER:**
|
|
674
868
|
- Omit `static publicActions` (component will deny ALL remote actions)
|
|
675
869
|
- Export separate `defaultState` constant (use static)
|
|
676
870
|
- Create constructor just to call super() (not needed)
|
|
677
871
|
- Forget `static componentName` (breaks minification)
|
|
872
|
+
- Override `destroy()` directly — use `onDestroy()` instead (v1.14.0)
|
|
678
873
|
- Emit room events without subscribing first
|
|
679
874
|
- Store non-serializable data in state
|
|
680
|
-
- Use reserved names for state properties (id, state, ws, room, userId, $room, $rooms, $private, broadcastToRoom, roomType)
|
|
875
|
+
- Use reserved names for state properties (id, state, ws, room, userId, $room, $rooms, $private, $persistent, broadcastToRoom, roomType)
|
|
681
876
|
- Include `setValue` in `publicActions` unless you trust clients to modify any state key
|
|
682
877
|
- Store sensitive data (tokens, API keys, secrets) in `state` — use `$private` instead
|
|
683
878
|
|
|
@@ -840,6 +1035,7 @@ export function UploadDemo() {
|
|
|
840
1035
|
- [Live Logging](./live-logging.md) - Per-component logging control
|
|
841
1036
|
- [Live Rooms](./live-rooms.md) - Multi-room real-time communication
|
|
842
1037
|
- [Live Upload](./live-upload.md) - Chunked file upload
|
|
1038
|
+
- [Live Binary Delta](./live-binary-delta.md) - High-frequency binary state sync
|
|
843
1039
|
- [Project Structure](../patterns/project-structure.md)
|
|
844
1040
|
- [Type Safety Patterns](../patterns/type-safety.md)
|
|
845
1041
|
- [WebSocket Plugin](../core/plugin-system.md)
|