create-fluxstack 1.5.5 → 1.7.3
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/README.md +172 -215
- package/app/client/src/App.tsx +45 -19
- package/app/client/src/components/FluxStackConfig.tsx +1 -1
- package/app/client/src/components/HybridLiveCounter.tsx +1 -1
- package/app/client/src/components/LiveClock.tsx +1 -1
- package/app/client/src/components/MainLayout.tsx +0 -2
- package/app/client/src/components/SidebarNavigation.tsx +1 -1
- package/app/client/src/components/SystemMonitor.tsx +16 -10
- package/app/client/src/components/UserProfile.tsx +1 -1
- package/app/server/live/FluxStackConfig.ts +2 -1
- package/app/server/live/LiveClockComponent.ts +8 -7
- package/app/server/live/SidebarNavigation.ts +2 -1
- package/app/server/live/SystemMonitor.ts +1 -0
- package/app/server/live/UserProfileComponent.ts +36 -30
- package/config/server.config.ts +1 -0
- package/core/build/index.ts +14 -0
- package/core/cli/command-registry.ts +10 -10
- package/core/cli/commands/plugin-deps.ts +13 -5
- package/core/cli/plugin-discovery.ts +1 -1
- package/core/client/LiveComponentsProvider.tsx +414 -0
- package/core/client/hooks/useHybridLiveComponent.ts +194 -530
- package/core/client/index.ts +16 -0
- package/core/framework/server.ts +144 -63
- package/core/index.ts +4 -1
- package/core/plugins/built-in/monitoring/index.ts +1 -1
- package/core/plugins/built-in/static/index.ts +1 -1
- package/core/plugins/built-in/swagger/index.ts +1 -1
- package/core/plugins/built-in/vite/index.ts +1 -1
- package/core/plugins/config.ts +1 -1
- package/core/plugins/discovery.ts +1 -1
- package/core/plugins/executor.ts +1 -1
- package/core/plugins/index.ts +1 -0
- package/core/server/live/ComponentRegistry.ts +3 -1
- package/core/server/live/WebSocketConnectionManager.ts +14 -4
- package/core/server/live/websocket-plugin.ts +453 -434
- package/core/server/middleware/elysia-helpers.ts +3 -5
- package/core/server/plugins/database.ts +1 -1
- package/core/server/plugins/static-files-plugin.ts +1 -1
- package/core/templates/create-project.ts +1 -0
- package/core/types/index.ts +1 -1
- package/core/types/plugin.ts +1 -1
- package/core/types/types.ts +6 -2
- package/core/utils/logger/colors.ts +4 -4
- package/core/utils/logger/index.ts +37 -4
- package/core/utils/logger/winston-logger.ts +1 -1
- package/core/utils/sync-version.ts +67 -0
- package/core/utils/version.ts +6 -5
- package/package.json +3 -2
- package/plugins/crypto-auth/server/middlewares/cryptoAuthAdmin.ts +1 -1
- package/plugins/crypto-auth/server/middlewares/cryptoAuthOptional.ts +1 -1
- package/plugins/crypto-auth/server/middlewares/cryptoAuthPermissions.ts +1 -1
- package/plugins/crypto-auth/server/middlewares/cryptoAuthRequired.ts +1 -1
- package/vite.config.ts +8 -0
- package/.dockerignore +0 -50
- package/CRYPTO-AUTH-MIDDLEWARE-GUIDE.md +0 -475
- package/CRYPTO-AUTH-MIDDLEWARES.md +0 -473
- package/CRYPTO-AUTH-USAGE.md +0 -491
- package/EXEMPLO-ROTA-PROTEGIDA.md +0 -347
- package/QUICK-START-CRYPTO-AUTH.md +0 -221
- package/app/client/src/components/Teste.tsx +0 -104
- package/app/server/live/TesteComponent.ts +0 -87
- package/test-crypto-auth.ts +0 -101
|
@@ -1,15 +1,17 @@
|
|
|
1
|
-
// 🔥 Hybrid Live Component Hook
|
|
2
|
-
//
|
|
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 {
|
|
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:
|
|
155
|
-
validation: StateValidator.createValidation(
|
|
156
|
-
conflicts: [],
|
|
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
|
-
//
|
|
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
|
|
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)
|
|
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
|
-
//
|
|
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
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
log('
|
|
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
|
-
|
|
426
|
-
|
|
427
|
-
|
|
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
|
|
465
|
-
|
|
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
|
|
493
|
-
error: message.error,
|
|
494
|
-
currentComponentId: componentId,
|
|
495
|
-
rehydrating
|
|
496
|
-
})
|
|
497
|
-
|
|
278
|
+
log('🔄 Component re-hydration required')
|
|
498
279
|
if (!rehydrating) {
|
|
499
|
-
attemptRehydration()
|
|
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
|
|
527
|
-
errorMessage,
|
|
528
|
-
currentComponentId: componentId,
|
|
529
|
-
rehydrating
|
|
530
|
-
})
|
|
531
|
-
|
|
288
|
+
log('🔄 Component re-hydration required from ERROR')
|
|
532
289
|
if (!rehydrating) {
|
|
533
|
-
attemptRehydration()
|
|
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
|
-
|
|
555
|
-
|
|
556
|
-
|
|
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
|
|
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,
|
|
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
|
|
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,
|
|
659
|
-
|
|
464
|
+
}, [componentId, connected, contextSendMessage, log])
|
|
660
465
|
|
|
661
|
-
// Server-only actions
|
|
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
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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,
|
|
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(
|
|
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,
|
|
542
|
+
}, [connected, autoMount, mount, componentId, rehydrating, attemptRehydration])
|
|
804
543
|
|
|
805
|
-
// Monitor connection status changes
|
|
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
|
|
835
|
-
|
|
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,
|
|
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
|
-
//
|
|
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
|
|
603
|
+
}, [stateData, call])
|
|
919
604
|
|
|
920
|
-
// Calculate
|
|
605
|
+
// Calculate status
|
|
921
606
|
const getStatus = () => {
|
|
922
607
|
if (!connected) return 'connecting'
|
|
923
608
|
if (rehydrating) return 'reconnecting'
|
|
924
|
-
if (mountLoading) return 'loading'
|
|
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
|
-
|
|
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
|
+
}
|