create-fluxstack 1.13.0 → 1.15.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/patterns/anti-patterns.md +100 -0
- package/LLMD/reference/routing.md +39 -39
- package/LLMD/resources/live-auth.md +20 -2
- package/LLMD/resources/live-components.md +300 -21
- package/LLMD/resources/live-logging.md +95 -33
- package/LLMD/resources/live-upload.md +59 -8
- package/app/client/.live-stubs/LiveAdminPanel.js +5 -0
- package/app/client/.live-stubs/LiveChat.js +7 -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/LiveRoomChat.js +10 -0
- package/app/client/.live-stubs/LiveTodoList.js +9 -0
- package/app/client/.live-stubs/LiveUpload.js +15 -0
- package/app/client/index.html +2 -2
- package/app/client/public/favicon.svg +46 -0
- package/app/client/src/App.tsx +13 -1
- package/app/client/src/assets/fluxstack-static.svg +46 -0
- package/app/client/src/assets/fluxstack.svg +183 -0
- package/app/client/src/components/AppLayout.tsx +146 -9
- package/app/client/src/components/BackButton.tsx +13 -13
- package/app/client/src/components/DemoPage.tsx +4 -4
- package/app/client/src/live/AuthDemo.tsx +23 -21
- package/app/client/src/live/ChatDemo.tsx +2 -2
- package/app/client/src/live/CounterDemo.tsx +12 -12
- package/app/client/src/live/FormDemo.tsx +2 -2
- package/app/client/src/live/LiveDebuggerPanel.tsx +779 -0
- package/app/client/src/live/RoomChatDemo.tsx +24 -16
- package/app/client/src/live/TodoListDemo.tsx +158 -0
- package/app/client/src/main.tsx +13 -13
- package/app/client/src/pages/ApiTestPage.tsx +6 -6
- package/app/client/src/pages/HomePage.tsx +80 -52
- 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 +2 -1
- package/app/server/live/LiveChat.ts +78 -77
- package/app/server/live/LiveCounter.ts +1 -1
- package/app/server/live/LiveForm.ts +1 -0
- package/app/server/live/LiveLocalCounter.ts +38 -37
- package/app/server/live/LiveProtectedChat.ts +2 -1
- package/app/server/live/LiveRoomChat.ts +1 -0
- package/app/server/live/LiveTodoList.ts +110 -0
- package/app/server/live/LiveUpload.ts +1 -0
- package/app/server/live/register-components.ts +19 -19
- package/app/server/routes/room.routes.ts +1 -2
- package/config/system/runtime.config.ts +4 -0
- package/core/build/live-components-generator.ts +1 -1
- package/core/build/optimizer.ts +235 -235
- package/core/build/vite-plugins.ts +28 -0
- package/core/client/components/LiveDebugger.tsx +1324 -0
- package/core/client/hooks/useLiveUpload.ts +3 -4
- package/core/client/index.ts +41 -21
- package/core/framework/server.ts +1 -1
- package/core/plugins/built-in/index.ts +134 -134
- package/core/plugins/built-in/live-components/commands/create-live-component.ts +4 -0
- package/core/plugins/built-in/vite/index.ts +75 -21
- package/core/server/index.ts +14 -15
- package/core/server/live/auto-generated-components.ts +6 -3
- package/core/server/live/index.ts +95 -21
- package/core/server/live/websocket-plugin.ts +27 -862
- package/core/server/plugins/static-files-plugin.ts +179 -69
- package/core/types/build.ts +219 -219
- package/core/types/plugin.ts +107 -107
- package/core/types/types.ts +77 -890
- package/core/utils/logger/startup-banner.ts +82 -82
- package/core/utils/version.ts +6 -6
- package/create-fluxstack.ts +1 -1
- package/package.json +5 -1
- package/plugins/crypto-auth/index.ts +1 -1
- package/plugins/crypto-auth/server/CryptoAuthLiveProvider.ts +2 -2
- package/vite.config.ts +40 -12
- package/app/client/src/assets/react.svg +0 -1
- package/core/client/LiveComponentsProvider.tsx +0 -531
- package/core/client/components/Live.tsx +0 -105
- 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 -843
- package/core/client/hooks/useRoom.ts +0 -409
- package/core/client/hooks/useRoomProxy.ts +0 -382
- package/core/server/live/ComponentRegistry.ts +0 -1099
- package/core/server/live/FileUploadManager.ts +0 -282
- package/core/server/live/LiveComponentPerformanceMonitor.ts +0 -931
- package/core/server/live/LiveLogger.ts +0 -111
- package/core/server/live/LiveRoomManager.ts +0 -262
- 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 -645
- package/core/server/live/WebSocketConnectionManager.ts +0 -709
- 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,17 @@
|
|
|
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
|
-
-
|
|
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)
|
|
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)
|
|
14
|
+
- Automatic state persistence and re-hydration (with anti-replay nonces)
|
|
10
15
|
- Room-based event system for multi-user sync
|
|
11
16
|
- Type-safe client-server communication (FluxStackWebSocket)
|
|
12
17
|
- Built-in connection management and recovery
|
|
@@ -25,7 +30,8 @@ import type { CounterDemo as _Client } from '@client/src/live/CounterDemo'
|
|
|
25
30
|
|
|
26
31
|
export class LiveCounter extends LiveComponent<typeof LiveCounter.defaultState> {
|
|
27
32
|
static componentName = 'LiveCounter'
|
|
28
|
-
static
|
|
33
|
+
static publicActions = ['increment', 'decrement', 'reset'] as const // 🔒 REQUIRED
|
|
34
|
+
// static logging = ['lifecycle', 'messages'] as const // Console logging (optional, prefer DEBUG_LIVE)
|
|
29
35
|
static defaultState = {
|
|
30
36
|
count: 0
|
|
31
37
|
}
|
|
@@ -51,11 +57,19 @@ export class LiveCounter extends LiveComponent<typeof LiveCounter.defaultState>
|
|
|
51
57
|
}
|
|
52
58
|
```
|
|
53
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
|
+
|
|
54
67
|
### Key Changes in v1.13.0
|
|
55
68
|
|
|
56
69
|
1. **Direct state access** - `this.count++` instead of `this.state.count++`
|
|
57
70
|
2. **declare keyword** - TypeScript hint for dynamic properties
|
|
58
71
|
3. **Cleaner code** - No need to prefix with `this.state.`
|
|
72
|
+
4. **Mandatory `publicActions`** - Components without it deny ALL remote actions (secure by default)
|
|
59
73
|
|
|
60
74
|
### Key Changes in v1.12.0
|
|
61
75
|
|
|
@@ -72,6 +86,7 @@ import { LiveComponent, type FluxStackWebSocket } from '@core/types/types'
|
|
|
72
86
|
|
|
73
87
|
export class LiveCounter extends LiveComponent<typeof LiveCounter.defaultState> {
|
|
74
88
|
static componentName = 'LiveCounter'
|
|
89
|
+
static publicActions = ['increment'] as const // 🔒 REQUIRED
|
|
75
90
|
static defaultState = {
|
|
76
91
|
count: 0,
|
|
77
92
|
lastUpdatedBy: null as string | null,
|
|
@@ -107,27 +122,207 @@ export class LiveCounter extends LiveComponent<typeof LiveCounter.defaultState>
|
|
|
107
122
|
}
|
|
108
123
|
```
|
|
109
124
|
|
|
110
|
-
## Lifecycle
|
|
125
|
+
## Lifecycle Hooks (v1.14.0)
|
|
126
|
+
|
|
127
|
+
Full lifecycle hook system — no more constructor workarounds:
|
|
111
128
|
|
|
112
129
|
```typescript
|
|
113
130
|
export class MyComponent extends LiveComponent<typeof MyComponent.defaultState> {
|
|
114
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
|
|
115
291
|
static defaultState = {
|
|
116
|
-
|
|
292
|
+
visitors: 0,
|
|
293
|
+
alerts: [] as string[],
|
|
294
|
+
lastRefresh: ''
|
|
117
295
|
}
|
|
118
296
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
297
|
+
protected async onMount() {
|
|
298
|
+
this.state.visitors++
|
|
299
|
+
this.state.lastRefresh = new Date().toISOString()
|
|
300
|
+
}
|
|
123
301
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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 }
|
|
127
312
|
}
|
|
128
313
|
}
|
|
129
314
|
```
|
|
130
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
|
+
|
|
131
326
|
## State Management
|
|
132
327
|
|
|
133
328
|
### Reactive State Proxy (How It Works)
|
|
@@ -186,16 +381,86 @@ this.setState(prev => ({
|
|
|
186
381
|
|
|
187
382
|
### setValue (Generic Action)
|
|
188
383
|
|
|
189
|
-
Built-in action to set any state key from the client
|
|
384
|
+
Built-in action to set any state key from the client. **Must be explicitly included in `publicActions` to be callable:**
|
|
190
385
|
|
|
191
386
|
```typescript
|
|
192
|
-
//
|
|
387
|
+
// Server: opt-in to setValue
|
|
388
|
+
static publicActions = ['increment', 'setValue'] as const // Must include 'setValue'
|
|
389
|
+
|
|
390
|
+
// Client can then call:
|
|
193
391
|
await component.setValue({ key: 'count', value: 42 })
|
|
194
392
|
```
|
|
195
393
|
|
|
394
|
+
> **Security note:** `setValue` is powerful - it allows the client to set any state key. Only add it to `publicActions` if you trust the client to modify any state field.
|
|
395
|
+
|
|
396
|
+
### $private — Server-Only State
|
|
397
|
+
|
|
398
|
+
`$private` is a key-value store that lives **exclusively on the server**. It is NEVER synchronized with the client — no `STATE_UPDATE`, no `STATE_DELTA`, not included in `getSerializableState()`.
|
|
399
|
+
|
|
400
|
+
Use it for sensitive data like tokens, API keys, internal IDs, or any server-side bookkeeping:
|
|
401
|
+
|
|
402
|
+
```typescript
|
|
403
|
+
export class Chat extends LiveComponent<typeof Chat.defaultState> {
|
|
404
|
+
static componentName = 'Chat'
|
|
405
|
+
static publicActions = ['connect', 'sendMessage'] as const
|
|
406
|
+
static defaultState = { messages: [] as string[] }
|
|
407
|
+
|
|
408
|
+
async connect(payload: { token: string }) {
|
|
409
|
+
// 🔒 Stays on server — never sent to client
|
|
410
|
+
this.$private.token = payload.token
|
|
411
|
+
this.$private.apiKey = await getApiKey()
|
|
412
|
+
|
|
413
|
+
// ✅ Only UI data goes to state (synced with client)
|
|
414
|
+
this.state.messages = await fetchMessages(this.$private.token)
|
|
415
|
+
return { success: true }
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
async sendMessage(payload: { text: string }) {
|
|
419
|
+
// Use $private data for server-side logic
|
|
420
|
+
await postToAPI(this.$private.apiKey, payload.text)
|
|
421
|
+
this.state.messages = [...this.state.messages, payload.text]
|
|
422
|
+
return { success: true }
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
#### Typed $private (optional)
|
|
428
|
+
|
|
429
|
+
Pass a second generic to get full autocomplete and type checking:
|
|
430
|
+
|
|
431
|
+
```typescript
|
|
432
|
+
interface ChatPrivate {
|
|
433
|
+
token: string
|
|
434
|
+
apiKey: string
|
|
435
|
+
retryCount: number
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
export class Chat extends LiveComponent<typeof Chat.defaultState, ChatPrivate> {
|
|
439
|
+
static componentName = 'Chat'
|
|
440
|
+
static publicActions = ['connect'] as const
|
|
441
|
+
static defaultState = { messages: [] as string[] }
|
|
442
|
+
|
|
443
|
+
async connect(payload: { token: string }) {
|
|
444
|
+
this.$private.token = payload.token // ✅ autocomplete
|
|
445
|
+
this.$private.retryCount = 0 // ✅ must be number
|
|
446
|
+
this.$private.tokkken = 'x' // ❌ TypeScript error (typo)
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
The second generic defaults to `Record<string, any>`, so existing components work without changes.
|
|
452
|
+
|
|
453
|
+
**Key facts:**
|
|
454
|
+
- Starts as an empty `{}` — no static default needed
|
|
455
|
+
- Mutations do NOT trigger any WebSocket messages
|
|
456
|
+
- Cleared automatically on `destroy()`
|
|
457
|
+
- Lost on rehydration (re-populate in your action handlers)
|
|
458
|
+
- Blocked from remote access (`$private` and `_privateState` are in BLOCKED_ACTIONS)
|
|
459
|
+
- Optional `TPrivate` generic for full type safety
|
|
460
|
+
|
|
196
461
|
### getSerializableState
|
|
197
462
|
|
|
198
|
-
Get current state for serialization:
|
|
463
|
+
Get current state for serialization (does NOT include `$private`):
|
|
199
464
|
|
|
200
465
|
```typescript
|
|
201
466
|
const currentState = this.getSerializableState()
|
|
@@ -260,11 +525,13 @@ const counter = Live.use(LiveCounter, {
|
|
|
260
525
|
|
|
261
526
|
## Actions
|
|
262
527
|
|
|
263
|
-
Actions are methods
|
|
528
|
+
Actions are methods callable from the client. **Only methods listed in `publicActions` can be called remotely.** Components without `publicActions` deny ALL remote actions.
|
|
264
529
|
|
|
265
530
|
```typescript
|
|
266
531
|
// Server-side
|
|
267
532
|
export class LiveForm extends LiveComponent<FormState> {
|
|
533
|
+
static publicActions = ['submit', 'validate'] as const // 🔒 REQUIRED
|
|
534
|
+
|
|
268
535
|
async submit() {
|
|
269
536
|
const { name, email } = this.state
|
|
270
537
|
|
|
@@ -429,10 +696,11 @@ On reconnect, components restore previous state:
|
|
|
429
696
|
|
|
430
697
|
1. Client stores signed state in localStorage
|
|
431
698
|
2. On reconnect, sends signed state to server
|
|
432
|
-
3. Server validates signature
|
|
699
|
+
3. Server validates signature (HMAC-SHA256) and **anti-replay nonce**
|
|
433
700
|
4. Component re-hydrated with previous state
|
|
701
|
+
5. State expires after 24 hours (configurable)
|
|
434
702
|
|
|
435
|
-
No manual code needed - automatic.
|
|
703
|
+
No manual code needed - automatic. Each signed state includes a cryptographic nonce that is consumed on validation, preventing replay attacks.
|
|
436
704
|
|
|
437
705
|
## Multi-User Synchronization
|
|
438
706
|
|
|
@@ -530,8 +798,9 @@ app/client/src/live/
|
|
|
530
798
|
|
|
531
799
|
Each server file contains:
|
|
532
800
|
- `static componentName` - Component identifier
|
|
801
|
+
- `static publicActions` - **REQUIRED** whitelist of client-callable methods
|
|
533
802
|
- `static defaultState` - Initial state object
|
|
534
|
-
- `static logging` - Per-component log control (optional, see [Live Logging](./live-logging.md))
|
|
803
|
+
- `static logging` - Per-component console log control (optional, prefer `DEBUG_LIVE=true` for debug panel — see [Live Logging](./live-logging.md))
|
|
535
804
|
- Component class extending `LiveComponent`
|
|
536
805
|
- Client link via `import type { Demo as _Client }`
|
|
537
806
|
|
|
@@ -583,21 +852,29 @@ export class MyComponent extends LiveComponent<State> {
|
|
|
583
852
|
|
|
584
853
|
**ALWAYS:**
|
|
585
854
|
- Define `static componentName` matching class name
|
|
855
|
+
- Define `static publicActions` listing ALL client-callable methods (MANDATORY)
|
|
586
856
|
- Define `static defaultState` inside the class
|
|
587
857
|
- Use `typeof ClassName.defaultState` for type parameter
|
|
588
858
|
- Use `declare` for each state property (TypeScript type hint)
|
|
589
|
-
-
|
|
859
|
+
- Use `onMount()` for async initialization (rooms, auth, data fetching)
|
|
860
|
+
- Use `onDestroy()` for cleanup (timers, connections) — sync only
|
|
590
861
|
- Use `emitRoomEventWithState` for state changes in rooms
|
|
591
862
|
- Handle errors in actions (throw Error)
|
|
592
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
|
|
593
866
|
|
|
594
867
|
**NEVER:**
|
|
868
|
+
- Omit `static publicActions` (component will deny ALL remote actions)
|
|
595
869
|
- Export separate `defaultState` constant (use static)
|
|
596
870
|
- Create constructor just to call super() (not needed)
|
|
597
871
|
- Forget `static componentName` (breaks minification)
|
|
872
|
+
- Override `destroy()` directly — use `onDestroy()` instead (v1.14.0)
|
|
598
873
|
- Emit room events without subscribing first
|
|
599
874
|
- Store non-serializable data in state
|
|
600
|
-
- Use reserved names for state properties (id, state, ws, room, userId, $room, $rooms, broadcastToRoom, roomType)
|
|
875
|
+
- Use reserved names for state properties (id, state, ws, room, userId, $room, $rooms, $private, $persistent, broadcastToRoom, roomType)
|
|
876
|
+
- Include `setValue` in `publicActions` unless you trust clients to modify any state key
|
|
877
|
+
- Store sensitive data (tokens, API keys, secrets) in `state` — use `$private` instead
|
|
601
878
|
|
|
602
879
|
**STATE UPDATES (v1.13.0) — all auto-sync via Proxy:**
|
|
603
880
|
```typescript
|
|
@@ -636,6 +913,8 @@ import { liveUploadDefaultState, type LiveUploadState } from '@app/shared'
|
|
|
636
913
|
export const defaultState: LiveUploadState = liveUploadDefaultState
|
|
637
914
|
|
|
638
915
|
export class LiveUpload extends LiveComponent<LiveUploadState> {
|
|
916
|
+
static componentName = 'LiveUpload'
|
|
917
|
+
static publicActions = ['startUpload', 'updateProgress', 'completeUpload', 'failUpload', 'reset'] as const
|
|
639
918
|
static defaultState = defaultState
|
|
640
919
|
|
|
641
920
|
constructor(initialState: Partial<typeof defaultState>, ws: any, options?: { room?: string; userId?: string }) {
|
|
@@ -1,56 +1,100 @@
|
|
|
1
1
|
# Live Logging
|
|
2
2
|
|
|
3
|
-
**Version:** 1.12.
|
|
3
|
+
**Version:** 1.12.1 | **Updated:** 2025-02-22
|
|
4
4
|
|
|
5
5
|
## Quick Facts
|
|
6
6
|
|
|
7
7
|
- Per-component logging control — silent by default
|
|
8
|
-
-
|
|
8
|
+
- Two output channels: **console** (`LIVE_LOGGING`) and **debug panel** (`DEBUG_LIVE`)
|
|
9
|
+
- Both off by default — opt-in only
|
|
9
10
|
- 6 categories: `lifecycle`, `messages`, `state`, `performance`, `rooms`, `websocket`
|
|
10
|
-
- Global (non-component) logs controlled by `LIVE_LOGGING` env var
|
|
11
11
|
- `console.error` always visible regardless of config
|
|
12
|
+
- All `liveLog`/`liveWarn` calls are forwarded to the Live Debugger as `LOG` events when `DEBUG_LIVE=true`
|
|
12
13
|
|
|
13
|
-
##
|
|
14
|
+
## Two Logging Channels
|
|
14
15
|
|
|
15
|
-
|
|
16
|
+
| Channel | Env Var | Default | Purpose |
|
|
17
|
+
|---------|---------|---------|---------|
|
|
18
|
+
| **Console** | `LIVE_LOGGING` | `false` | Server terminal output |
|
|
19
|
+
| **Debug Panel** | `DEBUG_LIVE` | `false` | Live Debugger WebSocket stream |
|
|
20
|
+
|
|
21
|
+
The debug panel receives **all** `liveLog`/`liveWarn` calls as `LOG` events (with `category`, `level`, `message`, and `details`) regardless of the `LIVE_LOGGING` console setting. This keeps the console clean while making everything visible in the debug panel.
|
|
22
|
+
|
|
23
|
+
### Recommended Workflow
|
|
24
|
+
|
|
25
|
+
- **Normal development**: both off — clean console, no debug overhead
|
|
26
|
+
- **Debugging live components**: `DEBUG_LIVE=true` — open the debug panel at `/api/live/debug/ws`
|
|
27
|
+
- **Quick console debugging**: `LIVE_LOGGING=lifecycle,state` — targeted categories to console
|
|
28
|
+
- **Per-component debugging**: `static logging = true` on the specific component class
|
|
29
|
+
|
|
30
|
+
## Console Logging
|
|
31
|
+
|
|
32
|
+
### Per-Component (static logging)
|
|
16
33
|
|
|
17
34
|
```typescript
|
|
18
|
-
// app/server/live/
|
|
19
|
-
export class
|
|
20
|
-
static componentName = '
|
|
35
|
+
// app/server/live/LiveChat.ts
|
|
36
|
+
export class LiveChat extends LiveComponent<typeof LiveChat.defaultState> {
|
|
37
|
+
static componentName = 'LiveChat'
|
|
21
38
|
|
|
22
|
-
// ✅ All categories
|
|
39
|
+
// ✅ All categories to console
|
|
23
40
|
static logging = true
|
|
24
41
|
|
|
25
42
|
// ✅ Specific categories only
|
|
26
|
-
static logging = ['lifecycle', '
|
|
43
|
+
static logging = ['lifecycle', 'rooms'] as const
|
|
27
44
|
|
|
28
45
|
// ✅ Silent (default — omit property or set false)
|
|
29
|
-
// static logging
|
|
46
|
+
// No static logging needed
|
|
30
47
|
}
|
|
31
48
|
```
|
|
32
49
|
|
|
33
|
-
### Global
|
|
50
|
+
### Global (LIVE_LOGGING env var)
|
|
34
51
|
|
|
35
|
-
Logs not tied to a specific component (
|
|
52
|
+
Logs not tied to a specific component (connection cleanup, key rotation, etc.):
|
|
36
53
|
|
|
37
54
|
```bash
|
|
38
55
|
# .env
|
|
39
|
-
LIVE_LOGGING=true # All global logs
|
|
56
|
+
LIVE_LOGGING=true # All global logs to console
|
|
40
57
|
LIVE_LOGGING=lifecycle,rooms # Specific categories only
|
|
41
58
|
# (unset or 'false') # Silent (default)
|
|
42
59
|
```
|
|
43
60
|
|
|
61
|
+
## Debug Panel (DEBUG_LIVE)
|
|
62
|
+
|
|
63
|
+
When `DEBUG_LIVE=true`, all `liveLog`/`liveWarn` calls emit `LOG` events to the Live Debugger, regardless of `LIVE_LOGGING` or `static logging` settings.
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
# .env
|
|
67
|
+
DEBUG_LIVE=true # Enable debug panel events
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Each `LOG` event contains:
|
|
71
|
+
|
|
72
|
+
```typescript
|
|
73
|
+
{
|
|
74
|
+
type: 'LOG',
|
|
75
|
+
componentId: string | null,
|
|
76
|
+
componentName: null,
|
|
77
|
+
data: {
|
|
78
|
+
category: 'lifecycle' | 'messages' | 'state' | 'performance' | 'rooms' | 'websocket',
|
|
79
|
+
level: 'info' | 'warn',
|
|
80
|
+
message: string,
|
|
81
|
+
details?: unknown // Extra args passed to liveLog/liveWarn
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
The debug panel also receives all other debug events (`COMPONENT_MOUNT`, `STATE_CHANGE`, `ACTION_CALL`, etc.) — see [Live Components](./live-components.md) for the full event list.
|
|
87
|
+
|
|
44
88
|
## Categories
|
|
45
89
|
|
|
46
90
|
| Category | What It Logs |
|
|
47
91
|
|----------|-------------|
|
|
48
92
|
| `lifecycle` | Mount, unmount, rehydration, recovery, migration |
|
|
49
|
-
| `messages` | Received/sent WebSocket messages, file uploads |
|
|
93
|
+
| `messages` | Received/sent WebSocket messages, file uploads, queue operations |
|
|
50
94
|
| `state` | Signing, backup, compression, encryption, validation |
|
|
51
95
|
| `performance` | Monitoring init, alerts, optimization suggestions |
|
|
52
96
|
| `rooms` | Room create/join/leave, emit, broadcast |
|
|
53
|
-
| `websocket` | Connection open/close, auth |
|
|
97
|
+
| `websocket` | Connection open/close/cleanup, pool management, auth |
|
|
54
98
|
|
|
55
99
|
## Type Definition
|
|
56
100
|
|
|
@@ -69,12 +113,12 @@ static logging = ['lifecycle', 'messages'] as const
|
|
|
69
113
|
|
|
70
114
|
## API (Framework Internal)
|
|
71
115
|
|
|
72
|
-
These functions are used by the framework — app developers only need `static logging
|
|
116
|
+
These functions are used by the framework — app developers only need `static logging` or env vars:
|
|
73
117
|
|
|
74
118
|
```typescript
|
|
75
119
|
import { liveLog, liveWarn, registerComponentLogging, unregisterComponentLogging } from '@core/server/live'
|
|
76
120
|
|
|
77
|
-
// Log gated by component config
|
|
121
|
+
// Log gated by component config (console) + always forwarded to debug panel
|
|
78
122
|
liveLog('lifecycle', componentId, '🚀 Mounted component')
|
|
79
123
|
liveLog('rooms', componentId, `📡 Joined room '${roomId}'`)
|
|
80
124
|
|
|
@@ -89,70 +133,88 @@ unregisterComponentLogging(componentId)
|
|
|
89
133
|
## How It Works
|
|
90
134
|
|
|
91
135
|
1. **Mount**: `ComponentRegistry` reads `static logging` from the class and calls `registerComponentLogging(componentId, config)`
|
|
92
|
-
2. **Runtime**: All `liveLog()`/`liveWarn()` calls
|
|
136
|
+
2. **Runtime**: All `liveLog()`/`liveWarn()` calls:
|
|
137
|
+
- Forward to the Live Debugger as `LOG` events (when `DEBUG_LIVE=true`)
|
|
138
|
+
- Check the registry before emitting to console (when `LIVE_LOGGING` or `static logging` is active)
|
|
93
139
|
3. **Unmount**: `unregisterComponentLogging(componentId)` removes the entry
|
|
94
140
|
4. **Global logs**: Fall back to `LIVE_LOGGING` env var when `componentId` is `null`
|
|
95
141
|
|
|
96
142
|
## Examples
|
|
97
143
|
|
|
98
|
-
### Debug
|
|
144
|
+
### Debug via Panel (Recommended)
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
# .env
|
|
148
|
+
DEBUG_LIVE=true
|
|
149
|
+
# No LIVE_LOGGING needed — console stays clean
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Open the debug panel WebSocket at `/api/live/debug/ws` to see all events in real-time.
|
|
153
|
+
|
|
154
|
+
### Debug a Specific Component (Console)
|
|
99
155
|
|
|
100
156
|
```typescript
|
|
101
|
-
// Only this component will show logs
|
|
157
|
+
// Only this component will show console logs
|
|
102
158
|
export class LiveChat extends LiveComponent<typeof LiveChat.defaultState> {
|
|
103
159
|
static componentName = 'LiveChat'
|
|
104
|
-
static logging = true // See everything for this component
|
|
160
|
+
static logging = true // See everything for this component in console
|
|
105
161
|
}
|
|
106
162
|
|
|
107
|
-
// All other components remain silent
|
|
163
|
+
// All other components remain silent in console
|
|
108
164
|
export class LiveCounter extends LiveComponent<typeof LiveCounter.defaultState> {
|
|
109
165
|
static componentName = 'LiveCounter'
|
|
110
|
-
// No static logging → silent
|
|
166
|
+
// No static logging → silent in console
|
|
111
167
|
}
|
|
112
168
|
```
|
|
113
169
|
|
|
114
|
-
### Monitor Only Room Activity
|
|
170
|
+
### Monitor Only Room Activity (Console)
|
|
115
171
|
|
|
116
172
|
```typescript
|
|
117
173
|
export class LiveChat extends LiveComponent<typeof LiveChat.defaultState> {
|
|
118
174
|
static componentName = 'LiveChat'
|
|
119
|
-
static logging = ['rooms'] as const // Only room events
|
|
175
|
+
static logging = ['rooms'] as const // Only room events in console
|
|
120
176
|
}
|
|
121
177
|
```
|
|
122
178
|
|
|
123
179
|
### Production: Silent Everywhere
|
|
124
180
|
|
|
125
181
|
```bash
|
|
126
|
-
# .env (no LIVE_LOGGING
|
|
127
|
-
#
|
|
128
|
-
#
|
|
182
|
+
# .env (no LIVE_LOGGING, no DEBUG_LIVE)
|
|
183
|
+
# Console: silent
|
|
184
|
+
# Debug panel: disabled
|
|
129
185
|
```
|
|
130
186
|
|
|
131
187
|
## Files Reference
|
|
132
188
|
|
|
133
189
|
| File | Purpose |
|
|
134
190
|
|------|---------|
|
|
135
|
-
| `core/server/live/LiveLogger.ts` | Logger implementation, registry, shouldLog logic |
|
|
136
|
-
| `core/server/live/
|
|
191
|
+
| `core/server/live/LiveLogger.ts` | Logger implementation, registry, shouldLog logic, debugger forwarding |
|
|
192
|
+
| `core/server/live/LiveDebugger.ts` | Debug event bus, `LOG` event type, debug client management |
|
|
193
|
+
| `core/server/live/ComponentRegistry.ts` | Reads `static logging` on mount/unmount, uses `liveLog` |
|
|
137
194
|
| `core/server/live/websocket-plugin.ts` | Uses `liveLog` for WebSocket events |
|
|
195
|
+
| `core/server/live/WebSocketConnectionManager.ts` | Uses `liveLog`/`liveWarn` for connection pool management |
|
|
196
|
+
| `core/server/live/FileUploadManager.ts` | Uses `liveLog`/`liveWarn` for upload operations |
|
|
138
197
|
| `core/server/live/StateSignature.ts` | Uses `liveLog`/`liveWarn` for state operations |
|
|
139
198
|
| `core/server/live/LiveRoomManager.ts` | Uses `liveLog` for room lifecycle |
|
|
140
199
|
| `core/server/live/LiveComponentPerformanceMonitor.ts` | Uses `liveLog`/`liveWarn` for perf |
|
|
200
|
+
| `config/system/runtime.config.ts` | `DEBUG_LIVE` env var config |
|
|
141
201
|
| `core/types/types.ts` | `LiveComponent` base class with `static logging` property |
|
|
142
202
|
|
|
143
203
|
## Critical Rules
|
|
144
204
|
|
|
145
205
|
**ALWAYS:**
|
|
146
206
|
- Use `as const` on logging arrays for type safety
|
|
147
|
-
- Keep components silent by default
|
|
207
|
+
- Keep components silent by default (no `static logging`)
|
|
208
|
+
- Use `DEBUG_LIVE=true` for debugging instead of `static logging` on components
|
|
148
209
|
- Use specific categories instead of `true` when possible
|
|
149
210
|
|
|
150
211
|
**NEVER:**
|
|
151
212
|
- Use `console.log` directly in Live Component code — use `liveLog()`
|
|
152
213
|
- Forget that `console.error` is always visible (not gated)
|
|
214
|
+
- Enable `LIVE_LOGGING` or `DEBUG_LIVE` in production
|
|
153
215
|
|
|
154
216
|
## Related
|
|
155
217
|
|
|
156
218
|
- [Live Components](./live-components.md) - Base component system
|
|
157
219
|
- [Live Rooms](./live-rooms.md) - Room system (logged under `rooms` category)
|
|
158
|
-
- [Environment Variables](../config/environment-vars.md) - `LIVE_LOGGING` reference
|
|
220
|
+
- [Environment Variables](../config/environment-vars.md) - `LIVE_LOGGING` and `DEBUG_LIVE` reference
|