create-fluxstack 1.5.4 → 1.7.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 (61) hide show
  1. package/README.md +172 -215
  2. package/app/client/src/App.tsx +45 -19
  3. package/app/client/src/components/FluxStackConfig.tsx +1 -1
  4. package/app/client/src/components/HybridLiveCounter.tsx +1 -1
  5. package/app/client/src/components/LiveClock.tsx +1 -1
  6. package/app/client/src/components/MainLayout.tsx +0 -2
  7. package/app/client/src/components/SidebarNavigation.tsx +1 -1
  8. package/app/client/src/components/SystemMonitor.tsx +16 -10
  9. package/app/client/src/components/UserProfile.tsx +1 -1
  10. package/app/server/live/FluxStackConfig.ts +2 -1
  11. package/app/server/live/LiveClockComponent.ts +8 -7
  12. package/app/server/live/SidebarNavigation.ts +2 -1
  13. package/app/server/live/SystemMonitor.ts +1 -0
  14. package/app/server/live/UserProfileComponent.ts +36 -30
  15. package/config/server.config.ts +1 -0
  16. package/core/cli/command-registry.ts +10 -10
  17. package/core/cli/commands/plugin-deps.ts +13 -5
  18. package/core/cli/plugin-discovery.ts +1 -1
  19. package/core/client/LiveComponentsProvider.tsx +414 -0
  20. package/core/client/hooks/useHybridLiveComponent.ts +194 -530
  21. package/core/client/index.ts +16 -0
  22. package/core/framework/server.ts +144 -63
  23. package/core/index.ts +4 -1
  24. package/core/plugins/built-in/monitoring/index.ts +1 -1
  25. package/core/plugins/built-in/static/index.ts +1 -1
  26. package/core/plugins/built-in/swagger/index.ts +1 -1
  27. package/core/plugins/built-in/vite/index.ts +1 -1
  28. package/core/plugins/config.ts +1 -1
  29. package/core/plugins/discovery.ts +1 -1
  30. package/core/plugins/executor.ts +1 -1
  31. package/core/plugins/index.ts +1 -0
  32. package/core/server/live/ComponentRegistry.ts +3 -1
  33. package/core/server/live/WebSocketConnectionManager.ts +14 -4
  34. package/core/server/live/websocket-plugin.ts +453 -434
  35. package/core/server/middleware/elysia-helpers.ts +3 -5
  36. package/core/server/plugins/database.ts +1 -1
  37. package/core/server/plugins/static-files-plugin.ts +1 -1
  38. package/core/types/index.ts +1 -1
  39. package/core/types/plugin.ts +1 -1
  40. package/core/types/types.ts +6 -2
  41. package/core/utils/logger/colors.ts +4 -4
  42. package/core/utils/logger/index.ts +37 -4
  43. package/core/utils/logger/winston-logger.ts +1 -1
  44. package/core/utils/sync-version.ts +61 -0
  45. package/core/utils/version.ts +6 -5
  46. package/create-fluxstack.ts +1 -1
  47. package/package.json +5 -3
  48. package/plugins/crypto-auth/server/middlewares/cryptoAuthAdmin.ts +1 -1
  49. package/plugins/crypto-auth/server/middlewares/cryptoAuthOptional.ts +1 -1
  50. package/plugins/crypto-auth/server/middlewares/cryptoAuthPermissions.ts +1 -1
  51. package/plugins/crypto-auth/server/middlewares/cryptoAuthRequired.ts +1 -1
  52. package/vite.config.ts +8 -0
  53. package/.dockerignore +0 -50
  54. package/CRYPTO-AUTH-MIDDLEWARE-GUIDE.md +0 -475
  55. package/CRYPTO-AUTH-MIDDLEWARES.md +0 -473
  56. package/CRYPTO-AUTH-USAGE.md +0 -491
  57. package/EXEMPLO-ROTA-PROTEGIDA.md +0 -347
  58. package/QUICK-START-CRYPTO-AUTH.md +0 -221
  59. package/app/client/src/components/Teste.tsx +0 -104
  60. package/app/server/live/TesteComponent.ts +0 -87
  61. package/test-crypto-auth.ts +0 -101
@@ -1,15 +1,17 @@
1
- // 🔥 Hybrid Live Component Hook - Server-Driven with Zustand
2
- // Direct WebSocket integration (no dependency on useLiveComponent)
1
+ // 🔥 Hybrid Live Component Hook v2 - Uses Single WebSocket Connection
2
+ // Refactored to use LiveComponentsProvider context instead of creating its own connection
3
3
 
4
4
  import { useState, useEffect, useCallback, useRef } from 'react'
5
5
  import { create } from 'zustand'
6
6
  import { subscribeWithSelector } from 'zustand/middleware'
7
- import { useWebSocket, type WebSocketMessage, type WebSocketResponse } from './useWebSocket'
7
+ import { useLiveComponents } from '../LiveComponentsProvider'
8
8
  import { StateValidator } from './state-validator'
9
- import type {
10
- HybridState,
11
- StateConflict,
12
- HybridComponentOptions
9
+ import type {
10
+ HybridState,
11
+ StateConflict,
12
+ HybridComponentOptions,
13
+ WebSocketMessage,
14
+ WebSocketResponse
13
15
  } from '../../types/types'
14
16
 
15
17
  // Client-side state persistence for reconnection
@@ -37,11 +39,9 @@ const persistComponentState = (componentName: string, signedState: any, room?: s
37
39
  userId,
38
40
  lastUpdate: Date.now()
39
41
  }
40
-
42
+
41
43
  const key = `${STORAGE_KEY_PREFIX}${componentName}`
42
44
  localStorage.setItem(key, JSON.stringify(persistedState))
43
-
44
- // State persisted silently to avoid log spam
45
45
  } catch (error) {
46
46
  console.warn('⚠️ Failed to persist component state:', error)
47
47
  }
@@ -50,26 +50,21 @@ const persistComponentState = (componentName: string, signedState: any, room?: s
50
50
  const getPersistedState = (componentName: string): PersistedComponentState | null => {
51
51
  try {
52
52
  const key = `${STORAGE_KEY_PREFIX}${componentName}`
53
- console.log('🔍 Getting persisted state', { componentName, key })
54
53
  const stored = localStorage.getItem(key)
55
-
54
+
56
55
  if (!stored) {
57
- console.log('❌ No localStorage data found', { key })
58
56
  return null
59
57
  }
60
-
61
- console.log('✅ Found localStorage data', { stored })
58
+
62
59
  const persistedState: PersistedComponentState = JSON.parse(stored)
63
-
60
+
64
61
  // Check if state is not too old
65
62
  const age = Date.now() - persistedState.lastUpdate
66
63
  if (age > STATE_MAX_AGE) {
67
64
  localStorage.removeItem(key)
68
- console.log('🗑️ Expired persisted state removed:', { componentName, age, maxAge: STATE_MAX_AGE })
69
65
  return null
70
66
  }
71
-
72
- console.log('✅ Valid persisted state found', { componentName, age })
67
+
73
68
  return persistedState
74
69
  } catch (error) {
75
70
  console.warn('⚠️ Failed to retrieve persisted state:', error)
@@ -81,7 +76,6 @@ const clearPersistedState = (componentName: string) => {
81
76
  try {
82
77
  const key = `${STORAGE_KEY_PREFIX}${componentName}`
83
78
  localStorage.removeItem(key)
84
- console.log('🗑️ Persisted state cleared:', componentName)
85
79
  } catch (error) {
86
80
  console.warn('⚠️ Failed to clear persisted state:', error)
87
81
  }
@@ -96,26 +90,22 @@ interface HybridStore<T> {
96
90
  export interface UseHybridLiveComponentReturn<T> {
97
91
  // Server-driven state (read-only from frontend perspective)
98
92
  state: T
99
-
93
+
100
94
  // Status information
101
95
  loading: boolean
102
96
  error: string | null
103
97
  connected: boolean
104
98
  componentId: string | null
105
-
99
+
106
100
  // Connection status with all possible states
107
101
  status: 'synced' | 'disconnected' | 'connecting' | 'reconnecting' | 'loading' | 'mounting' | 'error'
108
-
102
+
109
103
  // Actions (all go to server)
110
104
  call: (action: string, payload?: any) => Promise<void>
111
105
  callAndWait: (action: string, payload?: any, timeout?: number) => Promise<any>
112
106
  mount: () => Promise<void>
113
107
  unmount: () => Promise<void>
114
-
115
- // WebSocket utilities
116
- sendMessage: (message: any) => Promise<WebSocketResponse>
117
- sendMessageAndWait: (message: any, timeout?: number) => Promise<WebSocketResponse>
118
-
108
+
119
109
  // Helper for temporary input state
120
110
  useControlledField: <K extends keyof T>(field: K, action?: string) => {
121
111
  value: T[K]
@@ -139,21 +129,12 @@ function createHybridStore<T>(initialState: T) {
139
129
  },
140
130
 
141
131
  updateState: (newState: T, source: 'server' | 'mount' = 'server') => {
142
- console.log('🔄 [Zustand] Server state update', { newState, source })
143
132
  set((state) => {
144
- // Backend is ONLY source of state mutations
145
- const updatedData = newState
146
-
147
- console.log('🔄 [Zustand] State replaced from server', {
148
- from: state.hybridState.data,
149
- to: updatedData
150
- })
151
-
152
133
  return {
153
134
  hybridState: {
154
- data: updatedData,
155
- validation: StateValidator.createValidation(updatedData, source),
156
- conflicts: [], // No conflicts - server is source of truth
135
+ data: newState,
136
+ validation: StateValidator.createValidation(newState, source),
137
+ conflicts: [],
157
138
  status: 'synced'
158
139
  }
159
140
  }
@@ -187,7 +168,16 @@ export function useHybridLiveComponent<T = any>(
187
168
  debug = false
188
169
  } = options
189
170
 
190
- // Create unique instance ID to avoid conflicts between multiple instances
171
+ // Use Live Components context (singleton WebSocket connection)
172
+ const {
173
+ connected,
174
+ sendMessage: contextSendMessage,
175
+ sendMessageAndWait: contextSendMessageAndWait,
176
+ registerComponent,
177
+ unregisterComponent
178
+ } = useLiveComponents()
179
+
180
+ // Create unique instance ID
191
181
  const instanceId = useRef(`${componentName}-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`)
192
182
  const logPrefix = `${instanceId.current}${room ? `[${room}]` : ''}`
193
183
 
@@ -198,38 +188,15 @@ export function useHybridLiveComponent<T = any>(
198
188
  }
199
189
  const store = storeRef.current
200
190
 
201
- // Get state from Zustand store with optimized selectors
191
+ // Get state from Zustand store
202
192
  const hybridState = store((state) => state.hybridState)
203
193
  const stateData = store((state) => state.hybridState.data)
204
194
  const updateState = store((state) => state.updateState)
205
-
206
- // Log state changes (throttled to avoid spam)
207
- const lastLoggedStateRef = useRef<string>('')
208
- useEffect(() => {
209
- if (debug) {
210
- const stateString = JSON.stringify(stateData)
211
- if (stateString !== lastLoggedStateRef.current) {
212
- console.log('🔍 [Zustand] State data changed:', stateData)
213
- lastLoggedStateRef.current = stateString
214
- }
215
- }
216
- }, [stateData, debug])
217
-
218
- // Direct WebSocket integration
219
- const {
220
- connected,
221
- sendMessage,
222
- sendMessageAndWait,
223
- onMessage,
224
- error: wsError
225
- } = useWebSocket({
226
- debug
227
- })
228
195
 
229
196
  // Component state
230
197
  const [componentId, setComponentId] = useState<string | null>(null)
231
198
  const [lastServerState, setLastServerState] = useState<T | null>(null)
232
- const [mountLoading, setMountLoading] = useState(false) // Only for mount/unmount operations
199
+ const [mountLoading, setMountLoading] = useState(false)
233
200
  const [error, setError] = useState<string | null>(null)
234
201
  const [rehydrating, setRehydrating] = useState(false)
235
202
  const [currentSignedState, setCurrentSignedState] = useState<any>(null)
@@ -246,314 +213,177 @@ export function useHybridLiveComponent<T = any>(
246
213
  // Prevent multiple simultaneous re-hydration attempts
247
214
  const rehydrationAttemptRef = useRef<Promise<boolean> | null>(null)
248
215
 
249
- // Automatic re-hydration on reconnection
250
- const attemptRehydration = useCallback(async () => {
251
- log('🔄 attemptRehydration called', { connected, rehydrating, mounting: mountingRef.current })
252
-
253
- if (!connected || rehydrating || mountingRef.current) {
254
- log('❌ Re-hydration blocked', { connected, rehydrating, mounting: mountingRef.current })
255
- return false
256
- }
257
-
258
- // Prevent multiple simultaneous attempts (local)
259
- if (rehydrationAttemptRef.current) {
260
- log('⏳ Re-hydration already in progress locally, waiting...')
261
- return await rehydrationAttemptRef.current
262
- }
263
-
264
- // Prevent multiple simultaneous attempts (global by component name)
265
- if (globalRehydrationAttempts.has(componentName)) {
266
- log('⏳ Re-hydration already in progress globally for', componentName)
267
- return await globalRehydrationAttempts.get(componentName)!
268
- }
269
-
270
- // Check for persisted state
271
- log('🔍 Checking for persisted state', { componentName })
272
- const persistedState = getPersistedState(componentName)
273
- if (!persistedState) {
274
- log('❌ No persisted state found for re-hydration', { componentName })
275
- return false
276
- }
277
-
278
- log('✅ Found persisted state', { persistedState })
279
-
280
- // Create and store the re-hydration promise
281
- const rehydrationPromise = (async () => {
282
- setRehydrating(true)
283
- setError(null)
284
- log('Attempting automatic re-hydration', {
285
- componentName,
286
- persistedState: {
287
- lastUpdate: persistedState.lastUpdate,
288
- age: Date.now() - persistedState.lastUpdate
289
- }
290
- })
291
-
292
- try {
293
- // Send re-hydration request with signed state
294
- const tempComponentId = lastKnownComponentIdRef.current || instanceId.current
295
-
296
- log('📤 Sending COMPONENT_REHYDRATE request', {
297
- tempComponentId,
298
- componentName,
299
- currentRehydrating: rehydrating,
300
- persistedState: {
301
- room: persistedState.room,
302
- userId: persistedState.userId,
303
- signedStateVersion: persistedState.signedState?.version
304
- }
305
- })
306
-
307
- const response = await sendMessageAndWait({
308
- type: 'COMPONENT_REHYDRATE',
309
- componentId: tempComponentId,
310
- payload: {
311
- componentName,
312
- signedState: persistedState.signedState,
313
- room: persistedState.room,
314
- userId: persistedState.userId
315
- },
316
- expectResponse: true
317
- }, 10000)
318
-
319
- log('💫 Re-hydration response received:', {
320
- success: response?.success,
321
- newComponentId: response?.result?.newComponentId,
322
- error: response?.error
323
- })
324
-
325
- if (response?.success && response?.result?.newComponentId) {
326
- log('✅ Re-hydration successful - updating componentId in attemptRehydration', {
327
- oldComponentId: componentId,
328
- newComponentId: response.result.newComponentId
329
- })
330
-
331
- // Update componentId immediately to prevent further re-hydration attempts
332
- setComponentId(response.result.newComponentId)
333
- lastKnownComponentIdRef.current = response.result.newComponentId
334
-
335
- return true
336
- } else {
337
- log('❌ Re-hydration failed', response?.error || 'Unknown error')
338
- // Clear invalid persisted state
339
- clearPersistedState(componentName)
340
- setError(response?.error || 'Re-hydration failed')
341
- return false
342
- }
343
-
344
- } catch (error: any) {
345
- log('Re-hydration error', error.message)
346
- clearPersistedState(componentName)
347
- setError(error.message)
348
- return false
349
- } finally {
350
- setRehydrating(false)
351
- rehydrationAttemptRef.current = null // Clear the local reference
352
- globalRehydrationAttempts.delete(componentName) // Clear the global reference
353
- }
354
- })()
355
-
356
- // Store both locally and globally
357
- rehydrationAttemptRef.current = rehydrationPromise
358
- globalRehydrationAttempts.set(componentName, rehydrationPromise)
359
-
360
- return await rehydrationPromise
361
- }, [connected, rehydrating, componentName, sendMessageAndWait, log])
362
-
363
- // Handle incoming WebSocket messages (real-time processing)
216
+ // Register this component with WebSocket context
364
217
  useEffect(() => {
365
- if (!componentId) {
366
- return
367
- }
218
+ if (!componentId) return
368
219
 
369
- // Process each message immediately as it arrives
370
- const unsubscribe = onMessage((message: any) => {
371
- // Debug: Log all received messages first
372
- log('🔍 Received WebSocket message', {
373
- type: message.type,
374
- messageComponentId: message.componentId,
375
- currentComponentId: componentId,
376
- requestId: message.requestId,
377
- success: message.success
378
- })
379
-
380
- // Don't filter STATE_REHYDRATED and COMPONENT_REHYDRATED - they may have different componentIds
381
- if (message.type !== 'STATE_REHYDRATED' && message.type !== 'COMPONENT_REHYDRATED' && message.componentId !== componentId) {
382
- log('🚫 Filtering out message - componentId mismatch', {
383
- type: message.type,
384
- messageComponentId: message.componentId,
385
- currentComponentId: componentId
386
- })
387
- return
388
- }
220
+ log('📝 Registering component with WebSocket context', componentId)
221
+
222
+ const unregister = registerComponent(componentId, (message: WebSocketResponse) => {
223
+ log('📨 Received message for component', { type: message.type })
389
224
 
390
- log('✅ Processing message immediately', { type: message.type, componentId: message.componentId })
391
-
392
225
  switch (message.type) {
393
226
  case 'STATE_UPDATE':
394
- log('Processing STATE_UPDATE', message.payload)
395
227
  if (message.payload?.state) {
396
228
  const newState = message.payload.state
397
- log('Updating Zustand with server state', newState)
398
229
  updateState(newState, 'server')
399
230
  setLastServerState(newState)
400
-
401
- // Debug signed state persistence
231
+
402
232
  if (message.payload?.signedState) {
403
- log('Found signedState in STATE_UPDATE - persisting', {
404
- componentName,
405
- signedState: message.payload.signedState
406
- })
407
233
  setCurrentSignedState(message.payload.signedState)
408
234
  persistComponentState(componentName, message.payload.signedState, room, userId)
409
- log('State persisted successfully')
410
- } else {
411
- log('⚠️ No signedState in STATE_UPDATE payload', message.payload)
412
235
  }
413
-
414
- log('State updated from server successfully', newState)
415
- } else {
416
- log('STATE_UPDATE has no state payload', message.payload)
417
236
  }
418
237
  break
419
238
 
420
239
  case 'STATE_REHYDRATED':
421
- log('Processing STATE_REHYDRATED', message.payload)
422
240
  if (message.payload?.state && message.payload?.newComponentId) {
423
241
  const newState = message.payload.state
424
242
  const newComponentId = message.payload.newComponentId
425
- const oldComponentId = message.payload.oldComponentId
426
-
427
- log('Component re-hydrated successfully', {
428
- oldComponentId,
429
- newComponentId,
430
- state: newState
431
- })
432
-
433
- // Update component ID and state
243
+
244
+ log('Component re-hydrated successfully', { newComponentId })
245
+
434
246
  setComponentId(newComponentId)
435
247
  lastKnownComponentIdRef.current = newComponentId
436
248
  updateState(newState, 'server')
437
249
  setLastServerState(newState)
438
-
439
- // Update signed state
250
+
440
251
  if (message.payload?.signedState) {
441
252
  setCurrentSignedState(message.payload.signedState)
442
253
  persistComponentState(componentName, message.payload.signedState, room, userId)
443
254
  }
444
-
255
+
445
256
  setRehydrating(false)
446
257
  setError(null)
447
-
448
- log('Re-hydration completed successfully')
449
258
  }
450
259
  break
451
260
 
452
261
  case 'COMPONENT_REHYDRATED':
453
- log('🎉 Processing COMPONENT_REHYDRATED response', message)
454
- log('🎉 Response details:', {
455
- success: message.success,
456
- newComponentId: message.result?.newComponentId,
457
- requestId: message.requestId,
458
- currentRehydrating: rehydrating,
459
- currentComponentId: componentId
460
- })
461
-
462
- // Check if this is a successful re-hydration
463
262
  if (message.success && message.result?.newComponentId) {
464
- log('✅ Re-hydration succeeded, updating componentId', {
465
- from: componentId,
466
- to: message.result.newComponentId
467
- })
468
-
469
- // Update componentId immediately to stop the loop
263
+ log('✅ Re-hydration succeeded', message.result.newComponentId)
264
+
470
265
  setComponentId(message.result.newComponentId)
471
266
  lastKnownComponentIdRef.current = message.result.newComponentId
472
267
  setRehydrating(false)
473
268
  setError(null)
474
-
475
- log('🎯 ComponentId updated, re-hydration completed')
476
269
  } else if (!message.success) {
477
270
  log('❌ Re-hydration failed', message.error)
478
271
  setRehydrating(false)
479
272
  setError(message.error || 'Re-hydration failed')
480
273
  }
481
-
482
- // This is also handled by sendMessageAndWait, but we process it here too for immediate UI updates
483
274
  break
484
275
 
485
276
  case 'MESSAGE_RESPONSE':
486
- if (message.originalType !== 'CALL_ACTION') {
487
- log('Received response for', message.originalType)
488
- }
489
-
490
- // Check for re-hydration required error
491
277
  if (!message.success && message.error?.includes?.('COMPONENT_REHYDRATION_REQUIRED')) {
492
- log('🔄 Component re-hydration required from MESSAGE_RESPONSE - attempting automatic re-hydration', {
493
- error: message.error,
494
- currentComponentId: componentId,
495
- rehydrating
496
- })
497
-
278
+ log('🔄 Component re-hydration required')
498
279
  if (!rehydrating) {
499
- attemptRehydration().then(rehydrated => {
500
- if (rehydrated) {
501
- log('✅ Re-hydration successful after action error')
502
- } else {
503
- log('❌ Re-hydration failed after action error')
504
- setError('Component lost connection and could not be recovered')
505
- }
506
- }).catch(error => {
507
- log('💥 Re-hydration error after action error', error)
508
- setError('Component recovery failed')
509
- })
510
- } else {
511
- log('⚠️ Already re-hydrating, skipping duplicate attempt')
280
+ attemptRehydration()
512
281
  }
513
282
  }
514
283
  break
515
284
 
516
- case 'BROADCAST':
517
- log('Received broadcast', message.payload)
518
- break
519
-
520
285
  case 'ERROR':
521
- log('Received error', message.payload)
522
286
  const errorMessage = message.payload?.error || 'Unknown error'
523
-
524
- // Check for re-hydration required error
525
287
  if (errorMessage.includes('COMPONENT_REHYDRATION_REQUIRED')) {
526
- log('🔄 Component re-hydration required from ERROR - attempting automatic re-hydration', {
527
- errorMessage,
528
- currentComponentId: componentId,
529
- rehydrating
530
- })
531
-
288
+ log('🔄 Component re-hydration required from ERROR')
532
289
  if (!rehydrating) {
533
- attemptRehydration().then(rehydrated => {
534
- if (rehydrated) {
535
- log('✅ Re-hydration successful after error')
536
- } else {
537
- log('❌ Re-hydration failed after error')
538
- setError('Component lost connection and could not be recovered')
539
- }
540
- }).catch(error => {
541
- log('💥 Re-hydration error after error', error)
542
- setError('Component recovery failed')
543
- })
544
- } else {
545
- log('⚠️ Already re-hydrating, skipping duplicate attempt from ERROR')
290
+ attemptRehydration()
546
291
  }
547
292
  } else {
548
293
  setError(errorMessage)
549
294
  }
550
295
  break
296
+
297
+ case 'COMPONENT_PONG':
298
+ log('🏓 Received pong from server')
299
+ // Component is alive - update lastActivity if needed
300
+ break
551
301
  }
552
302
  })
553
303
 
554
- // Cleanup callback on unmount
555
- return unsubscribe
556
- }, [componentId, updateState, log, onMessage, attemptRehydration])
304
+ return () => {
305
+ log('🗑️ Unregistering component from WebSocket context')
306
+ unregister()
307
+ }
308
+ }, [componentId, registerComponent, unregisterComponent, log, updateState, componentName, room, userId, rehydrating])
309
+
310
+ // Automatic re-hydration on reconnection
311
+ const attemptRehydration = useCallback(async () => {
312
+ if (!connected || rehydrating || mountingRef.current) {
313
+ return false
314
+ }
315
+
316
+ // Prevent multiple simultaneous attempts (local)
317
+ if (rehydrationAttemptRef.current) {
318
+ return await rehydrationAttemptRef.current
319
+ }
320
+
321
+ // Prevent multiple simultaneous attempts (global)
322
+ if (globalRehydrationAttempts.has(componentName)) {
323
+ return await globalRehydrationAttempts.get(componentName)!
324
+ }
325
+
326
+ const persistedState = getPersistedState(componentName)
327
+ if (!persistedState) {
328
+ return false
329
+ }
330
+
331
+ // Check if state is too old (> 1 hour = likely expired signing key)
332
+ const stateAge = Date.now() - persistedState.lastUpdate
333
+ const ONE_HOUR = 60 * 60 * 1000
334
+ if (stateAge > ONE_HOUR) {
335
+ log('⏰ Persisted state too old, clearing and skipping rehydration', {
336
+ age: stateAge,
337
+ ageMinutes: Math.floor(stateAge / 60000)
338
+ })
339
+ clearPersistedState(componentName)
340
+ return false
341
+ }
342
+
343
+ const rehydrationPromise = (async () => {
344
+ setRehydrating(true)
345
+ setError(null)
346
+
347
+ try {
348
+ const tempComponentId = lastKnownComponentIdRef.current || instanceId.current
349
+
350
+ const response = await contextSendMessageAndWait({
351
+ type: 'COMPONENT_REHYDRATE',
352
+ componentId: tempComponentId,
353
+ payload: {
354
+ componentName,
355
+ signedState: persistedState.signedState,
356
+ room: persistedState.room,
357
+ userId: persistedState.userId
358
+ }
359
+ }, 2000) // Reduced from 10s to 2s - fast fail for invalid states
360
+
361
+ if (response?.success && response?.result?.newComponentId) {
362
+ setComponentId(response.result.newComponentId)
363
+ lastKnownComponentIdRef.current = response.result.newComponentId
364
+ return true
365
+ } else {
366
+ clearPersistedState(componentName)
367
+ setError(response?.error || 'Re-hydration failed')
368
+ return false
369
+ }
370
+
371
+ } catch (error: any) {
372
+ clearPersistedState(componentName)
373
+ setError(error.message)
374
+ return false
375
+ } finally {
376
+ setRehydrating(false)
377
+ rehydrationAttemptRef.current = null
378
+ globalRehydrationAttempts.delete(componentName)
379
+ }
380
+ })()
381
+
382
+ rehydrationAttemptRef.current = rehydrationPromise
383
+ globalRehydrationAttempts.set(componentName, rehydrationPromise)
384
+
385
+ return await rehydrationPromise
386
+ }, [connected, rehydrating, componentName, contextSendMessageAndWait, log])
557
387
 
558
388
  // Mount component
559
389
  const mount = useCallback(async () => {
@@ -564,7 +394,6 @@ export function useHybridLiveComponent<T = any>(
564
394
  mountingRef.current = true
565
395
  setMountLoading(true)
566
396
  setError(null)
567
- log('Mounting component - server will control all state')
568
397
 
569
398
  try {
570
399
  const message: WebSocketMessage = {
@@ -578,61 +407,40 @@ export function useHybridLiveComponent<T = any>(
578
407
  }
579
408
  }
580
409
 
581
- const response = await sendMessageAndWait(message, 10000)
582
-
583
- log('Mount response received', { response, fullResponse: JSON.stringify(response) })
584
-
410
+ const response = await contextSendMessageAndWait(message, 5000) // 5s for mount is enough
411
+
585
412
  if (response?.success && response?.result?.componentId) {
586
413
  const newComponentId = response.result.componentId
587
414
  setComponentId(newComponentId)
588
415
  mountedRef.current = true
589
-
590
- // Immediately persist signed state from mount response
416
+
591
417
  if (response.result.signedState) {
592
- log('Found signedState in mount response - persisting immediately', {
593
- componentName,
594
- signedState: response.result.signedState
595
- })
596
418
  setCurrentSignedState(response.result.signedState)
597
419
  persistComponentState(componentName, response.result.signedState, room, userId)
598
- log('Mount state persisted successfully')
599
- } else {
600
- log('⚠️ No signedState in mount response', response.result)
601
420
  }
602
-
603
- // Update state if provided
421
+
604
422
  if (response.result.initialState) {
605
- log('Updating state from mount response', response.result.initialState)
606
423
  updateState(response.result.initialState, 'server')
607
424
  setLastServerState(response.result.initialState)
608
425
  }
609
-
610
- log('Component mounted successfully', { componentId: newComponentId })
426
+
427
+ log('Component mounted successfully', { componentId: newComponentId })
611
428
  } else {
612
- log('Failed to parse response', {
613
- hasResponse: !!response,
614
- hasSuccess: response?.success,
615
- hasResult: !!response?.result,
616
- hasComponentId: response?.result?.componentId,
617
- error: response?.error
618
- })
619
429
  throw new Error(response?.error || 'No component ID returned from server')
620
430
  }
621
431
  } catch (err) {
622
432
  const errorMessage = err instanceof Error ? err.message : 'Mount failed'
623
433
  setError(errorMessage)
624
- log('Mount failed', err)
625
-
626
- if (fallbackToLocal) {
627
- log('Using local state as fallback until reconnection')
628
- } else {
434
+ log('Mount failed', err)
435
+
436
+ if (!fallbackToLocal) {
629
437
  throw err
630
438
  }
631
439
  } finally {
632
440
  setMountLoading(false)
633
441
  mountingRef.current = false
634
442
  }
635
- }, [connected, componentName, initialState, room, userId, sendMessage, log, fallbackToLocal])
443
+ }, [connected, componentName, initialState, room, userId, contextSendMessageAndWait, log, fallbackToLocal, updateState])
636
444
 
637
445
  // Unmount component
638
446
  const unmount = useCallback(async () => {
@@ -640,105 +448,60 @@ export function useHybridLiveComponent<T = any>(
640
448
  return
641
449
  }
642
450
 
643
- log('Unmounting component', { componentId })
644
-
645
451
  try {
646
- await sendMessage({
452
+ await contextSendMessage({
647
453
  type: 'COMPONENT_UNMOUNT',
648
454
  componentId
649
455
  })
650
-
456
+
651
457
  setComponentId(null)
652
458
  mountedRef.current = false
653
459
  mountingRef.current = false
654
- log('Component unmounted successfully')
460
+ log('Component unmounted successfully')
655
461
  } catch (err) {
656
- log('Unmount failed', err)
462
+ log('Unmount failed', err)
657
463
  }
658
- }, [componentId, connected, sendMessage, log])
659
-
464
+ }, [componentId, connected, contextSendMessage, log])
660
465
 
661
- // Server-only actions (no client-side state mutations)
466
+ // Server-only actions
662
467
  const call = useCallback(async (action: string, payload?: any): Promise<void> => {
663
468
  if (!componentId || !connected) {
664
469
  throw new Error('Component not mounted or WebSocket not connected')
665
470
  }
666
471
 
667
- log('Calling server action', { action, payload })
668
-
669
472
  try {
670
- // Don't set loading for actions to avoid UI flicker
671
-
672
473
  const message: WebSocketMessage = {
673
474
  type: 'CALL_ACTION',
674
475
  componentId,
675
476
  action,
676
- payload,
677
- expectResponse: true // Always expect response to catch errors like re-hydration required
477
+ payload
678
478
  }
679
479
 
680
- // Send action - server will update state and send back changes
681
- try {
682
- const response = await sendMessageAndWait(message, 5000)
683
-
684
- // Check for re-hydration required error
685
- if (!response.success && response.error?.includes?.('COMPONENT_REHYDRATION_REQUIRED')) {
686
- log('Component re-hydration required - attempting automatic re-hydration')
687
- const rehydrated = await attemptRehydration()
688
- if (rehydrated) {
689
- log('Re-hydration successful - retrying action with new component ID')
690
- // Use the updated componentId after re-hydration
691
- const currentComponentId = componentId // This should be updated by re-hydration
692
- const retryMessage: WebSocketMessage = {
693
- type: 'CALL_ACTION',
694
- componentId: currentComponentId,
695
- action,
696
- payload,
697
- expectResponse: true
698
- }
699
- await sendMessageAndWait(retryMessage, 5000)
700
- } else {
701
- throw new Error('Component lost connection and could not be recovered')
702
- }
703
- } else if (!response.success) {
704
- throw new Error(response.error || 'Action failed')
705
- }
706
- } catch (wsError: any) {
707
- // Check if the WebSocket error is about re-hydration
708
- if (wsError.message?.includes?.('COMPONENT_REHYDRATION_REQUIRED')) {
709
- log('Component re-hydration required (from WebSocket error) - attempting automatic re-hydration')
710
- const rehydrated = await attemptRehydration()
711
- if (rehydrated) {
712
- log('Re-hydration successful - retrying action with new component ID')
713
- // Use the updated componentId after re-hydration
714
- const currentComponentId = componentId
715
- const retryMessage: WebSocketMessage = {
716
- type: 'CALL_ACTION',
717
- componentId: currentComponentId,
718
- action,
719
- payload,
720
- expectResponse: true
721
- }
722
- await sendMessageAndWait(retryMessage, 5000)
723
- } else {
724
- throw new Error('Component lost connection and could not be recovered')
480
+ const response = await contextSendMessageAndWait(message, 5000)
481
+
482
+ if (!response.success && response.error?.includes?.('COMPONENT_REHYDRATION_REQUIRED')) {
483
+ const rehydrated = await attemptRehydration()
484
+ if (rehydrated) {
485
+ // Retry action
486
+ const retryMessage: WebSocketMessage = {
487
+ type: 'CALL_ACTION',
488
+ componentId,
489
+ action,
490
+ payload
725
491
  }
492
+ await contextSendMessageAndWait(retryMessage, 5000)
726
493
  } else {
727
- // Re-throw other WebSocket errors
728
- throw wsError
494
+ throw new Error('Component lost connection and could not be recovered')
729
495
  }
496
+ } else if (!response.success) {
497
+ throw new Error(response.error || 'Action failed')
730
498
  }
731
-
732
- log('Action sent to server - waiting for server state update', { action, payload })
733
499
  } catch (err) {
734
500
  const errorMessage = err instanceof Error ? err.message : 'Action failed'
735
501
  setError(errorMessage)
736
- log('Action failed', { action, error: err })
737
502
  throw err
738
- } finally {
739
- // No loading state for actions to prevent UI flicker
740
503
  }
741
- }, [componentId, connected, sendMessage, log])
504
+ }, [componentId, connected, contextSendMessageAndWait, attemptRehydration])
742
505
 
743
506
  // Call action and wait for specific return value
744
507
  const callAndWait = useCallback(async (action: string, payload?: any, timeout?: number): Promise<any> => {
@@ -746,11 +509,7 @@ export function useHybridLiveComponent<T = any>(
746
509
  throw new Error('Component not mounted or WebSocket not connected')
747
510
  }
748
511
 
749
- log('Calling server action and waiting for response', { action, payload })
750
-
751
512
  try {
752
- // Don't set loading for callAndWait to avoid UI flicker
753
-
754
513
  const message: WebSocketMessage = {
755
514
  type: 'CALL_ACTION',
756
515
  componentId,
@@ -758,118 +517,57 @@ export function useHybridLiveComponent<T = any>(
758
517
  payload
759
518
  }
760
519
 
761
- // Send action and wait for response
762
- const result = await sendMessageAndWait(message, timeout)
763
-
764
- log('Action completed with result', { action, payload, result })
520
+ const result = await contextSendMessageAndWait(message, timeout)
765
521
  return result
766
522
  } catch (err) {
767
523
  const errorMessage = err instanceof Error ? err.message : 'Action failed'
768
524
  setError(errorMessage)
769
- log('Action failed', { action, error: err })
770
525
  throw err
771
- } finally {
772
- // No loading state for actions to prevent UI flicker
773
526
  }
774
- }, [componentId, connected, sendMessageAndWait, log])
527
+ }, [componentId, connected, contextSendMessageAndWait])
775
528
 
776
529
  // Auto-mount with re-hydration attempt
777
530
  useEffect(() => {
778
531
  if (connected && autoMount && !mountedRef.current && !componentId && !mountingRef.current && !rehydrating) {
779
- log('Auto-mounting with re-hydration attempt', {
780
- connected,
781
- autoMount,
782
- mounted: mountedRef.current,
783
- componentId,
784
- mounting: mountingRef.current,
785
- rehydrating
786
- })
787
-
788
- // First try re-hydration, then fall back to normal mount
789
532
  attemptRehydration().then(rehydrated => {
790
533
  if (!rehydrated && !mountedRef.current && !componentId && !mountingRef.current) {
791
- log('Re-hydration failed or not available, proceeding with normal mount')
792
534
  mount()
793
- } else if (rehydrated) {
794
- log('Re-hydration successful, skipping normal mount')
795
535
  }
796
- }).catch(error => {
797
- log('Re-hydration attempt failed with error, proceeding with normal mount', error)
536
+ }).catch(() => {
798
537
  if (!mountedRef.current && !componentId && !mountingRef.current) {
799
538
  mount()
800
539
  }
801
540
  })
802
541
  }
803
- }, [connected, autoMount, mount, componentId, log, rehydrating, attemptRehydration])
542
+ }, [connected, autoMount, mount, componentId, rehydrating, attemptRehydration])
804
543
 
805
- // Monitor connection status changes and force reconnection
544
+ // Monitor connection status changes
806
545
  const prevConnectedRef = useRef(connected)
807
546
  useEffect(() => {
808
547
  const wasConnected = prevConnectedRef.current
809
548
  const isConnected = connected
810
-
811
- log('🔍 Connection status change detected:', {
812
- wasConnected,
813
- isConnected,
814
- componentMounted: mountedRef.current,
815
- componentId
816
- })
817
-
818
- // If we lost connection and had a component mounted, prepare for reconnection
549
+
819
550
  if (wasConnected && !isConnected && mountedRef.current) {
820
- log('🔄 Connection lost - marking component for remount on reconnection')
821
551
  mountedRef.current = false
822
552
  setComponentId(null)
823
553
  }
824
-
825
- // If we reconnected and don't have a component mounted, try re-hydration first
554
+
826
555
  if (!wasConnected && isConnected && !mountedRef.current && !mountingRef.current && !rehydrating) {
827
- log('🔗 Connection restored - checking for persisted state to re-hydrate')
828
-
829
- // Small delay to ensure WebSocket is fully established
830
556
  setTimeout(() => {
831
557
  if (!mountedRef.current && !mountingRef.current && !rehydrating) {
832
558
  const persistedState = getPersistedState(componentName)
833
-
834
- if (persistedState && persistedState.signedState) {
835
- log('🔄 Found persisted state - attempting re-hydration on reconnection', {
836
- hasSignedState: !!persistedState.signedState,
837
- lastUpdate: persistedState.lastUpdate,
838
- age: Date.now() - persistedState.lastUpdate
839
- })
840
-
841
- attemptRehydration().then(success => {
842
- if (success) {
843
- log('✅ Re-hydration successful on reconnection')
844
- } else {
845
- log('❌ Re-hydration failed on reconnection - falling back to mount')
846
- mount()
847
- }
848
- }).catch(error => {
849
- log('💥 Re-hydration error on reconnection - falling back to mount', error)
850
- mount()
851
- })
559
+
560
+ if (persistedState?.signedState) {
561
+ attemptRehydration()
852
562
  } else {
853
- log('🚀 No persisted state found - executing fresh mount after reconnection')
854
563
  mount()
855
564
  }
856
565
  }
857
566
  }, 100)
858
567
  }
859
-
860
- // If connected but no component after some time, force mount (fallback)
861
- if (isConnected && !mountedRef.current && !mountingRef.current && !rehydrating) {
862
- log('🔄 Connected but no component - scheduling fallback mount attempt')
863
- setTimeout(() => {
864
- if (connected && !mountedRef.current && !mountingRef.current && !rehydrating) {
865
- log('🚀 Forcing fallback mount for orphaned connection')
866
- mount()
867
- }
868
- }, 500) // Increased timeout to allow for re-hydration attempts
869
- }
870
-
568
+
871
569
  prevConnectedRef.current = connected
872
- }, [connected, mount, componentId, log, attemptRehydration, componentName, rehydrating])
570
+ }, [connected, mount, componentId, attemptRehydration, componentName, rehydrating])
873
571
 
874
572
  // Unmount on cleanup
875
573
  useEffect(() => {
@@ -880,88 +578,54 @@ export function useHybridLiveComponent<T = any>(
880
578
  }
881
579
  }, [unmount])
882
580
 
883
- // Update error from WebSocket
884
- useEffect(() => {
885
- if (wsError) {
886
- setError(wsError)
887
- }
888
- }, [wsError])
889
-
890
- // Helper for controlled inputs (temporary local state + server commit)
581
+ // Helper for controlled inputs
891
582
  const useControlledField = useCallback(<K extends keyof T>(
892
583
  field: K,
893
584
  action: string = 'updateField'
894
585
  ) => {
895
586
  const [tempValue, setTempValue] = useState<T[K]>(stateData[field])
896
-
897
- // Always sync temp value with server state (server is source of truth)
587
+
898
588
  useEffect(() => {
899
589
  setTempValue(stateData[field])
900
590
  }, [stateData[field]])
901
-
591
+
902
592
  const commitValue = useCallback(async (value?: T[K]) => {
903
593
  const valueToCommit = value !== undefined ? value : tempValue
904
- log('Committing field to server', { field, value: valueToCommit })
905
-
906
- // Call server action - server will update state and send back changes
907
594
  await call(action, { field, value: valueToCommit })
908
-
909
- // No local state mutation - wait for server response
910
595
  }, [tempValue, field, action])
911
-
596
+
912
597
  return {
913
598
  value: tempValue,
914
599
  setValue: setTempValue,
915
600
  commit: commitValue,
916
601
  isDirty: JSON.stringify(tempValue) !== JSON.stringify(stateData[field])
917
602
  }
918
- }, [stateData, call, log])
603
+ }, [stateData, call])
919
604
 
920
- // Calculate detailed status
605
+ // Calculate status
921
606
  const getStatus = () => {
922
607
  if (!connected) return 'connecting'
923
608
  if (rehydrating) return 'reconnecting'
924
- if (mountLoading) return 'loading' // Only show loading for mount operations
609
+ if (mountLoading) return 'loading'
925
610
  if (error) return 'error'
926
611
  if (!componentId) return 'mounting'
927
612
  if (hybridState.status === 'disconnected') return 'disconnected'
928
613
  return 'synced'
929
614
  }
930
-
931
- const status = getStatus()
932
615
 
933
- // Debug log for state return (throttled)
934
- const lastReturnedStateRef = useRef<string>('')
935
- if (debug) {
936
- const currentStateString = JSON.stringify(stateData)
937
- if (currentStateString !== lastReturnedStateRef.current) {
938
- console.log('🎯 [Hook] Returning state to component:', stateData)
939
- lastReturnedStateRef.current = currentStateString
940
- }
941
- }
616
+ const status = getStatus()
942
617
 
943
618
  return {
944
- // Server-driven state
945
619
  state: stateData,
946
-
947
- // Status
948
- loading: mountLoading, // Only loading for mount operations
620
+ loading: mountLoading,
949
621
  error,
950
622
  connected,
951
623
  componentId,
952
624
  status,
953
-
954
- // Actions (all server-driven)
955
625
  call,
956
626
  callAndWait,
957
627
  mount,
958
628
  unmount,
959
-
960
- // WebSocket utilities
961
- sendMessage,
962
- sendMessageAndWait,
963
-
964
- // Helper for forms
965
629
  useControlledField
966
630
  }
967
- }
631
+ }