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.
Files changed (96) hide show
  1. package/LLMD/patterns/anti-patterns.md +100 -0
  2. package/LLMD/reference/routing.md +39 -39
  3. package/LLMD/resources/live-auth.md +20 -2
  4. package/LLMD/resources/live-components.md +300 -21
  5. package/LLMD/resources/live-logging.md +95 -33
  6. package/LLMD/resources/live-upload.md +59 -8
  7. package/app/client/.live-stubs/LiveAdminPanel.js +5 -0
  8. package/app/client/.live-stubs/LiveChat.js +7 -0
  9. package/app/client/.live-stubs/LiveCounter.js +9 -0
  10. package/app/client/.live-stubs/LiveForm.js +11 -0
  11. package/app/client/.live-stubs/LiveLocalCounter.js +8 -0
  12. package/app/client/.live-stubs/LiveRoomChat.js +10 -0
  13. package/app/client/.live-stubs/LiveTodoList.js +9 -0
  14. package/app/client/.live-stubs/LiveUpload.js +15 -0
  15. package/app/client/index.html +2 -2
  16. package/app/client/public/favicon.svg +46 -0
  17. package/app/client/src/App.tsx +13 -1
  18. package/app/client/src/assets/fluxstack-static.svg +46 -0
  19. package/app/client/src/assets/fluxstack.svg +183 -0
  20. package/app/client/src/components/AppLayout.tsx +146 -9
  21. package/app/client/src/components/BackButton.tsx +13 -13
  22. package/app/client/src/components/DemoPage.tsx +4 -4
  23. package/app/client/src/live/AuthDemo.tsx +23 -21
  24. package/app/client/src/live/ChatDemo.tsx +2 -2
  25. package/app/client/src/live/CounterDemo.tsx +12 -12
  26. package/app/client/src/live/FormDemo.tsx +2 -2
  27. package/app/client/src/live/LiveDebuggerPanel.tsx +779 -0
  28. package/app/client/src/live/RoomChatDemo.tsx +24 -16
  29. package/app/client/src/live/TodoListDemo.tsx +158 -0
  30. package/app/client/src/main.tsx +13 -13
  31. package/app/client/src/pages/ApiTestPage.tsx +6 -6
  32. package/app/client/src/pages/HomePage.tsx +80 -52
  33. package/app/server/auth/DevAuthProvider.ts +2 -2
  34. package/app/server/auth/JWTAuthProvider.example.ts +2 -2
  35. package/app/server/index.ts +2 -2
  36. package/app/server/live/LiveAdminPanel.ts +2 -1
  37. package/app/server/live/LiveChat.ts +78 -77
  38. package/app/server/live/LiveCounter.ts +1 -1
  39. package/app/server/live/LiveForm.ts +1 -0
  40. package/app/server/live/LiveLocalCounter.ts +38 -37
  41. package/app/server/live/LiveProtectedChat.ts +2 -1
  42. package/app/server/live/LiveRoomChat.ts +1 -0
  43. package/app/server/live/LiveTodoList.ts +110 -0
  44. package/app/server/live/LiveUpload.ts +1 -0
  45. package/app/server/live/register-components.ts +19 -19
  46. package/app/server/routes/room.routes.ts +1 -2
  47. package/config/system/runtime.config.ts +4 -0
  48. package/core/build/live-components-generator.ts +1 -1
  49. package/core/build/optimizer.ts +235 -235
  50. package/core/build/vite-plugins.ts +28 -0
  51. package/core/client/components/LiveDebugger.tsx +1324 -0
  52. package/core/client/hooks/useLiveUpload.ts +3 -4
  53. package/core/client/index.ts +41 -21
  54. package/core/framework/server.ts +1 -1
  55. package/core/plugins/built-in/index.ts +134 -134
  56. package/core/plugins/built-in/live-components/commands/create-live-component.ts +4 -0
  57. package/core/plugins/built-in/vite/index.ts +75 -21
  58. package/core/server/index.ts +14 -15
  59. package/core/server/live/auto-generated-components.ts +6 -3
  60. package/core/server/live/index.ts +95 -21
  61. package/core/server/live/websocket-plugin.ts +27 -862
  62. package/core/server/plugins/static-files-plugin.ts +179 -69
  63. package/core/types/build.ts +219 -219
  64. package/core/types/plugin.ts +107 -107
  65. package/core/types/types.ts +77 -890
  66. package/core/utils/logger/startup-banner.ts +82 -82
  67. package/core/utils/version.ts +6 -6
  68. package/create-fluxstack.ts +1 -1
  69. package/package.json +5 -1
  70. package/plugins/crypto-auth/index.ts +1 -1
  71. package/plugins/crypto-auth/server/CryptoAuthLiveProvider.ts +2 -2
  72. package/vite.config.ts +40 -12
  73. package/app/client/src/assets/react.svg +0 -1
  74. package/core/client/LiveComponentsProvider.tsx +0 -531
  75. package/core/client/components/Live.tsx +0 -105
  76. package/core/client/hooks/AdaptiveChunkSizer.ts +0 -215
  77. package/core/client/hooks/state-validator.ts +0 -130
  78. package/core/client/hooks/useChunkedUpload.ts +0 -359
  79. package/core/client/hooks/useLiveChunkedUpload.ts +0 -87
  80. package/core/client/hooks/useLiveComponent.ts +0 -843
  81. package/core/client/hooks/useRoom.ts +0 -409
  82. package/core/client/hooks/useRoomProxy.ts +0 -382
  83. package/core/server/live/ComponentRegistry.ts +0 -1099
  84. package/core/server/live/FileUploadManager.ts +0 -282
  85. package/core/server/live/LiveComponentPerformanceMonitor.ts +0 -931
  86. package/core/server/live/LiveLogger.ts +0 -111
  87. package/core/server/live/LiveRoomManager.ts +0 -262
  88. package/core/server/live/RoomEventBus.ts +0 -234
  89. package/core/server/live/RoomStateManager.ts +0 -172
  90. package/core/server/live/SingleConnectionManager.ts +0 -0
  91. package/core/server/live/StateSignature.ts +0 -645
  92. package/core/server/live/WebSocketConnectionManager.ts +0 -709
  93. package/core/server/live/auth/LiveAuthContext.ts +0 -71
  94. package/core/server/live/auth/LiveAuthManager.ts +0 -304
  95. package/core/server/live/auth/index.ts +0 -19
  96. package/core/server/live/auth/types.ts +0 -179
@@ -1,12 +1,17 @@
1
1
  # Live Components
2
2
 
3
- **Version:** 1.13.0 | **Updated:** 2025-02-09
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
- - Automatic state persistence and re-hydration
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 logging = ['lifecycle', 'messages'] as const // Per-component logging (optional)
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 Methods
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
- // Define state here
292
+ visitors: 0,
293
+ alerts: [] as string[],
294
+ lastRefresh: ''
117
295
  }
118
296
 
119
- // Constructor ONLY needed if:
120
- // - Subscribing to room events
121
- // - Custom initialization logic
122
- // Otherwise, omit it entirely!
297
+ protected async onMount() {
298
+ this.state.visitors++
299
+ this.state.lastRefresh = new Date().toISOString()
300
+ }
123
301
 
124
- destroy() {
125
- // Cleanup subscriptions, timers, etc.
126
- super.destroy()
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
- // Client can call this without defining a custom action
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 called from the client:
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
- - Call `super.destroy()` in destroy method if overriding
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.0 | **Updated:** 2025-02-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
- - Opt-in via `static logging` property on LiveComponent subclasses
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
- ## Usage
14
+ ## Two Logging Channels
14
15
 
15
- ### Enable Logging on a Component
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/LiveCounter.ts
19
- export class LiveCounter extends LiveComponent<typeof LiveCounter.defaultState> {
20
- static componentName = 'LiveCounter'
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', 'messages', 'state', 'rooms'] as const
43
+ static logging = ['lifecycle', 'rooms'] as const
27
44
 
28
45
  // ✅ Silent (default — omit property or set false)
29
- // static logging = false
46
+ // No static logging needed
30
47
  }
31
48
  ```
32
49
 
33
- ### Global Logs (Non-Component)
50
+ ### Global (LIVE_LOGGING env var)
34
51
 
35
- Logs not tied to a specific component (room cleanup, key rotation, etc.) are controlled by the `LIVE_LOGGING` env var:
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 check the registry before emitting
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 a Specific Component
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 set)
127
- # All components without static logging → silent
128
- # Components with static logging still log (remove for production)
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/ComponentRegistry.ts` | Reads `static logging` on mount/unmount |
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 in production
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