create-fluxstack 1.0.13 → 1.0.15

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 (214) hide show
  1. package/.env.example +29 -29
  2. package/app/client/README.md +69 -69
  3. package/app/client/index.html +14 -13
  4. package/app/client/src/App.tsx +157 -524
  5. package/app/client/src/components/ErrorBoundary.tsx +107 -0
  6. package/app/client/src/components/ErrorDisplay.css +365 -0
  7. package/app/client/src/components/ErrorDisplay.tsx +258 -0
  8. package/app/client/src/components/FluxStackConfig.tsx +1321 -0
  9. package/app/client/src/components/HybridLiveCounter.tsx +140 -0
  10. package/app/client/src/components/LiveClock.tsx +286 -0
  11. package/app/client/src/components/MainLayout.tsx +390 -0
  12. package/app/client/src/components/SidebarNavigation.tsx +391 -0
  13. package/app/client/src/components/StateDemo.tsx +178 -0
  14. package/app/client/src/components/SystemMonitor.tsx +1038 -0
  15. package/app/client/src/components/Teste.tsx +104 -0
  16. package/app/client/src/components/UserProfile.tsx +809 -0
  17. package/app/client/src/hooks/useAuth.ts +39 -0
  18. package/app/client/src/hooks/useNotifications.ts +56 -0
  19. package/app/client/src/lib/eden-api.ts +189 -53
  20. package/app/client/src/lib/errors.ts +340 -0
  21. package/app/client/src/lib/hooks/useErrorHandler.ts +258 -0
  22. package/app/client/src/lib/index.ts +45 -0
  23. package/app/client/src/main.tsx +3 -2
  24. package/app/client/src/pages/ApiDocs.tsx +182 -0
  25. package/app/client/src/pages/Demo.tsx +174 -0
  26. package/app/client/src/pages/HybridLive.tsx +263 -0
  27. package/app/client/src/pages/Overview.tsx +155 -0
  28. package/app/client/src/store/README.md +43 -0
  29. package/app/client/src/store/index.ts +16 -0
  30. package/app/client/src/store/slices/uiSlice.ts +151 -0
  31. package/app/client/src/store/slices/userSlice.ts +161 -0
  32. package/app/client/src/test/README.md +257 -0
  33. package/app/client/src/test/setup.ts +70 -0
  34. package/app/client/src/test/types.ts +12 -0
  35. package/app/client/src/vite-env.d.ts +1 -1
  36. package/app/client/tsconfig.app.json +44 -43
  37. package/app/client/tsconfig.json +7 -7
  38. package/app/client/tsconfig.node.json +25 -25
  39. package/app/client/zustand-setup.md +65 -0
  40. package/app/server/controllers/users.controller.ts +68 -68
  41. package/app/server/index.ts +9 -1
  42. package/app/server/live/CounterComponent.ts +191 -0
  43. package/app/server/live/FluxStackConfig.ts +529 -0
  44. package/app/server/live/LiveClockComponent.ts +214 -0
  45. package/app/server/live/SidebarNavigation.ts +156 -0
  46. package/app/server/live/SystemMonitor.ts +594 -0
  47. package/app/server/live/SystemMonitorIntegration.ts +151 -0
  48. package/app/server/live/TesteComponent.ts +87 -0
  49. package/app/server/live/UserProfileComponent.ts +135 -0
  50. package/app/server/live/register-components.ts +28 -0
  51. package/app/server/middleware/auth.ts +136 -0
  52. package/app/server/middleware/errorHandling.ts +250 -0
  53. package/app/server/middleware/index.ts +10 -0
  54. package/app/server/middleware/rateLimit.ts +193 -0
  55. package/app/server/middleware/requestLogging.ts +215 -0
  56. package/app/server/middleware/validation.ts +270 -0
  57. package/app/server/routes/index.ts +14 -2
  58. package/app/server/routes/upload.ts +92 -0
  59. package/app/server/routes/users.routes.ts +2 -9
  60. package/app/server/services/NotificationService.ts +302 -0
  61. package/app/server/services/UserService.ts +222 -0
  62. package/app/server/services/index.ts +46 -0
  63. package/core/cli/commands/plugin-deps.ts +263 -0
  64. package/core/cli/generators/README.md +339 -0
  65. package/core/cli/generators/component.ts +770 -0
  66. package/core/cli/generators/controller.ts +299 -0
  67. package/core/cli/generators/index.ts +144 -0
  68. package/core/cli/generators/interactive.ts +228 -0
  69. package/core/cli/generators/prompts.ts +83 -0
  70. package/core/cli/generators/route.ts +513 -0
  71. package/core/cli/generators/service.ts +465 -0
  72. package/core/cli/generators/template-engine.ts +154 -0
  73. package/core/cli/generators/types.ts +71 -0
  74. package/core/cli/generators/utils.ts +192 -0
  75. package/core/cli/index.ts +69 -0
  76. package/core/cli/plugin-discovery.ts +16 -85
  77. package/core/client/fluxstack.ts +17 -0
  78. package/core/client/hooks/index.ts +7 -0
  79. package/core/client/hooks/state-validator.ts +130 -0
  80. package/core/client/hooks/useAuth.ts +49 -0
  81. package/core/client/hooks/useChunkedUpload.ts +258 -0
  82. package/core/client/hooks/useHybridLiveComponent.ts +967 -0
  83. package/core/client/hooks/useWebSocket.ts +373 -0
  84. package/core/client/index.ts +47 -0
  85. package/core/client/state/createStore.ts +193 -0
  86. package/core/client/state/index.ts +15 -0
  87. package/core/config/env-dynamic.ts +1 -1
  88. package/core/config/env.ts +2 -1
  89. package/core/config/runtime-config.ts +3 -3
  90. package/core/config/schema.ts +84 -49
  91. package/core/framework/server.ts +30 -0
  92. package/core/index.ts +25 -0
  93. package/core/live/ComponentRegistry.ts +399 -0
  94. package/core/live/types.ts +164 -0
  95. package/core/plugins/built-in/live-components/commands/create-live-component.ts +1201 -0
  96. package/core/plugins/built-in/live-components/index.ts +27 -0
  97. package/core/plugins/built-in/logger/index.ts +1 -1
  98. package/core/plugins/built-in/monitoring/index.ts +1 -1
  99. package/core/plugins/built-in/static/index.ts +1 -1
  100. package/core/plugins/built-in/swagger/index.ts +1 -1
  101. package/core/plugins/built-in/vite/index.ts +1 -1
  102. package/core/plugins/dependency-manager.ts +384 -0
  103. package/core/plugins/index.ts +5 -1
  104. package/core/plugins/manager.ts +7 -3
  105. package/core/plugins/registry.ts +88 -10
  106. package/core/plugins/types.ts +11 -11
  107. package/core/server/framework.ts +43 -0
  108. package/core/server/index.ts +11 -1
  109. package/core/server/live/ComponentRegistry.ts +1017 -0
  110. package/core/server/live/FileUploadManager.ts +272 -0
  111. package/core/server/live/LiveComponentPerformanceMonitor.ts +930 -0
  112. package/core/server/live/SingleConnectionManager.ts +0 -0
  113. package/core/server/live/StateSignature.ts +644 -0
  114. package/core/server/live/WebSocketConnectionManager.ts +688 -0
  115. package/core/server/live/websocket-plugin.ts +435 -0
  116. package/core/server/middleware/errorHandling.ts +141 -0
  117. package/core/server/middleware/index.ts +16 -0
  118. package/core/server/plugins/static-files-plugin.ts +232 -0
  119. package/core/server/services/BaseService.ts +95 -0
  120. package/core/server/services/ServiceContainer.ts +144 -0
  121. package/core/server/services/index.ts +9 -0
  122. package/core/templates/create-project.ts +196 -33
  123. package/core/testing/index.ts +10 -0
  124. package/core/testing/setup.ts +74 -0
  125. package/core/types/build.ts +38 -14
  126. package/core/types/types.ts +319 -0
  127. package/core/utils/env-runtime.ts +7 -0
  128. package/core/utils/errors/handlers.ts +264 -39
  129. package/core/utils/errors/index.ts +528 -18
  130. package/core/utils/errors/middleware.ts +114 -0
  131. package/core/utils/logger/formatters.ts +222 -0
  132. package/core/utils/logger/index.ts +167 -48
  133. package/core/utils/logger/middleware.ts +253 -0
  134. package/core/utils/logger/performance.ts +384 -0
  135. package/core/utils/logger/transports.ts +365 -0
  136. package/create-fluxstack.ts +296 -296
  137. package/fluxstack.config.ts +17 -1
  138. package/package-template.json +66 -66
  139. package/package.json +31 -6
  140. package/public/README.md +16 -0
  141. package/vite.config.ts +29 -14
  142. package/.claude/settings.local.json +0 -74
  143. package/.github/workflows/ci-build-tests.yml +0 -480
  144. package/.github/workflows/dependency-management.yml +0 -324
  145. package/.github/workflows/release-validation.yml +0 -355
  146. package/.kiro/specs/fluxstack-architecture-optimization/design.md +0 -700
  147. package/.kiro/specs/fluxstack-architecture-optimization/requirements.md +0 -127
  148. package/.kiro/specs/fluxstack-architecture-optimization/tasks.md +0 -330
  149. package/CLAUDE.md +0 -200
  150. package/Dockerfile +0 -58
  151. package/Dockerfile.backend +0 -52
  152. package/Dockerfile.frontend +0 -54
  153. package/README-Docker.md +0 -85
  154. package/ai-context/00-QUICK-START.md +0 -86
  155. package/ai-context/README.md +0 -88
  156. package/ai-context/development/eden-treaty-guide.md +0 -362
  157. package/ai-context/development/patterns.md +0 -382
  158. package/ai-context/development/plugins-guide.md +0 -572
  159. package/ai-context/examples/crud-complete.md +0 -626
  160. package/ai-context/project/architecture.md +0 -399
  161. package/ai-context/project/overview.md +0 -213
  162. package/ai-context/recent-changes/eden-treaty-refactor.md +0 -281
  163. package/ai-context/recent-changes/type-inference-fix.md +0 -223
  164. package/ai-context/reference/environment-vars.md +0 -384
  165. package/ai-context/reference/troubleshooting.md +0 -407
  166. package/app/client/src/components/TestPage.tsx +0 -453
  167. package/bun.lock +0 -1063
  168. package/bunfig.toml +0 -16
  169. package/core/__tests__/integration.test.ts +0 -227
  170. package/core/build/index.ts +0 -186
  171. package/core/config/__tests__/config-loader.test.ts +0 -554
  172. package/core/config/__tests__/config-merger.test.ts +0 -657
  173. package/core/config/__tests__/env-converter.test.ts +0 -372
  174. package/core/config/__tests__/env-processor.test.ts +0 -431
  175. package/core/config/__tests__/env.test.ts +0 -452
  176. package/core/config/__tests__/integration.test.ts +0 -418
  177. package/core/config/__tests__/loader.test.ts +0 -331
  178. package/core/config/__tests__/schema.test.ts +0 -129
  179. package/core/config/__tests__/validator.test.ts +0 -318
  180. package/core/framework/__tests__/server.test.ts +0 -233
  181. package/core/plugins/__tests__/built-in.test.ts.disabled +0 -366
  182. package/core/plugins/__tests__/manager.test.ts +0 -398
  183. package/core/plugins/__tests__/monitoring.test.ts +0 -401
  184. package/core/plugins/__tests__/registry.test.ts +0 -335
  185. package/core/utils/__tests__/errors.test.ts +0 -139
  186. package/core/utils/__tests__/helpers.test.ts +0 -297
  187. package/core/utils/__tests__/logger.test.ts +0 -141
  188. package/create-test-app.ts +0 -156
  189. package/docker-compose.microservices.yml +0 -75
  190. package/docker-compose.simple.yml +0 -57
  191. package/docker-compose.yml +0 -71
  192. package/eslint.config.js +0 -23
  193. package/flux-cli.ts +0 -214
  194. package/nginx-lb.conf +0 -37
  195. package/publish.sh +0 -63
  196. package/run-clean.ts +0 -26
  197. package/run-env-tests.ts +0 -313
  198. package/tailwind.config.js +0 -34
  199. package/tests/__mocks__/api.ts +0 -56
  200. package/tests/fixtures/users.ts +0 -69
  201. package/tests/integration/api/users.routes.test.ts +0 -221
  202. package/tests/setup.ts +0 -29
  203. package/tests/unit/app/client/App-simple.test.tsx +0 -56
  204. package/tests/unit/app/client/App.test.tsx.skip +0 -237
  205. package/tests/unit/app/client/eden-api.test.ts +0 -186
  206. package/tests/unit/app/client/simple.test.tsx +0 -23
  207. package/tests/unit/app/controllers/users.controller.test.ts +0 -150
  208. package/tests/unit/core/create-project.test.ts.skip +0 -95
  209. package/tests/unit/core/framework.test.ts +0 -144
  210. package/tests/unit/core/plugins/logger.test.ts.skip +0 -268
  211. package/tests/unit/core/plugins/vite.test.ts.disabled +0 -188
  212. package/tests/utils/test-helpers.ts +0 -61
  213. package/vitest.config.ts +0 -50
  214. package/workspace.json +0 -6
@@ -0,0 +1,967 @@
1
+ // 🔥 Hybrid Live Component Hook - Server-Driven with Zustand
2
+ // Direct WebSocket integration (no dependency on useLiveComponent)
3
+
4
+ import { useState, useEffect, useCallback, useRef } from 'react'
5
+ import { create } from 'zustand'
6
+ import { subscribeWithSelector } from 'zustand/middleware'
7
+ import { useWebSocket, type WebSocketMessage, type WebSocketResponse } from './useWebSocket'
8
+ import { StateValidator } from './state-validator'
9
+ import type {
10
+ HybridState,
11
+ StateConflict,
12
+ HybridComponentOptions
13
+ } from '../../types/types'
14
+
15
+ // Client-side state persistence for reconnection
16
+ interface PersistedComponentState {
17
+ componentName: string
18
+ signedState: any
19
+ room?: string
20
+ userId?: string
21
+ lastUpdate: number
22
+ }
23
+
24
+ const STORAGE_KEY_PREFIX = 'fluxstack_component_'
25
+ const STATE_MAX_AGE = 24 * 60 * 60 * 1000 // 24 hours
26
+
27
+ // Global re-hydration throttling by component name
28
+ const globalRehydrationAttempts = new Map<string, Promise<boolean>>()
29
+
30
+ // Utility functions for state persistence
31
+ const persistComponentState = (componentName: string, signedState: any, room?: string, userId?: string) => {
32
+ try {
33
+ const persistedState: PersistedComponentState = {
34
+ componentName,
35
+ signedState,
36
+ room,
37
+ userId,
38
+ lastUpdate: Date.now()
39
+ }
40
+
41
+ const key = `${STORAGE_KEY_PREFIX}${componentName}`
42
+ localStorage.setItem(key, JSON.stringify(persistedState))
43
+
44
+ // State persisted silently to avoid log spam
45
+ } catch (error) {
46
+ console.warn('⚠️ Failed to persist component state:', error)
47
+ }
48
+ }
49
+
50
+ const getPersistedState = (componentName: string): PersistedComponentState | null => {
51
+ try {
52
+ const key = `${STORAGE_KEY_PREFIX}${componentName}`
53
+ console.log('🔍 Getting persisted state', { componentName, key })
54
+ const stored = localStorage.getItem(key)
55
+
56
+ if (!stored) {
57
+ console.log('❌ No localStorage data found', { key })
58
+ return null
59
+ }
60
+
61
+ console.log('✅ Found localStorage data', { stored })
62
+ const persistedState: PersistedComponentState = JSON.parse(stored)
63
+
64
+ // Check if state is not too old
65
+ const age = Date.now() - persistedState.lastUpdate
66
+ if (age > STATE_MAX_AGE) {
67
+ localStorage.removeItem(key)
68
+ console.log('🗑️ Expired persisted state removed:', { componentName, age, maxAge: STATE_MAX_AGE })
69
+ return null
70
+ }
71
+
72
+ console.log('✅ Valid persisted state found', { componentName, age })
73
+ return persistedState
74
+ } catch (error) {
75
+ console.warn('⚠️ Failed to retrieve persisted state:', error)
76
+ return null
77
+ }
78
+ }
79
+
80
+ const clearPersistedState = (componentName: string) => {
81
+ try {
82
+ const key = `${STORAGE_KEY_PREFIX}${componentName}`
83
+ localStorage.removeItem(key)
84
+ console.log('🗑️ Persisted state cleared:', componentName)
85
+ } catch (error) {
86
+ console.warn('⚠️ Failed to clear persisted state:', error)
87
+ }
88
+ }
89
+
90
+ interface HybridStore<T> {
91
+ hybridState: HybridState<T>
92
+ updateState: (newState: T, source?: 'server' | 'mount') => void
93
+ reset: (initialState: T) => void
94
+ }
95
+
96
+ export interface UseHybridLiveComponentReturn<T> {
97
+ // Server-driven state (read-only from frontend perspective)
98
+ state: T
99
+
100
+ // Status information
101
+ loading: boolean
102
+ error: string | null
103
+ connected: boolean
104
+ componentId: string | null
105
+
106
+ // Connection status with all possible states
107
+ status: 'synced' | 'disconnected' | 'connecting' | 'reconnecting' | 'loading' | 'mounting' | 'error'
108
+
109
+ // Actions (all go to server)
110
+ call: (action: string, payload?: any) => Promise<void>
111
+ callAndWait: (action: string, payload?: any, timeout?: number) => Promise<any>
112
+ mount: () => Promise<void>
113
+ unmount: () => Promise<void>
114
+
115
+ // WebSocket utilities
116
+ sendMessage: (message: any) => Promise<WebSocketResponse>
117
+ sendMessageAndWait: (message: any, timeout?: number) => Promise<WebSocketResponse>
118
+
119
+ // Helper for temporary input state
120
+ useControlledField: <K extends keyof T>(field: K, action?: string) => {
121
+ value: T[K]
122
+ setValue: (value: T[K]) => void
123
+ commit: (value?: T[K]) => Promise<void>
124
+ isDirty: boolean
125
+ }
126
+ }
127
+
128
+ /**
129
+ * Create Zustand store for component instance
130
+ */
131
+ function createHybridStore<T>(initialState: T) {
132
+ return create<HybridStore<T>>()(
133
+ subscribeWithSelector((set, get) => ({
134
+ hybridState: {
135
+ data: initialState,
136
+ validation: StateValidator.createValidation(initialState, 'mount'),
137
+ conflicts: [],
138
+ status: 'disconnected' as const
139
+ },
140
+
141
+ updateState: (newState: T, source: 'server' | 'mount' = 'server') => {
142
+ console.log('🔄 [Zustand] Server state update', { newState, source })
143
+ 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
+ return {
153
+ hybridState: {
154
+ data: updatedData,
155
+ validation: StateValidator.createValidation(updatedData, source),
156
+ conflicts: [], // No conflicts - server is source of truth
157
+ status: 'synced'
158
+ }
159
+ }
160
+ })
161
+ },
162
+
163
+ reset: (initialState: T) => {
164
+ set({
165
+ hybridState: {
166
+ data: initialState,
167
+ validation: StateValidator.createValidation(initialState, 'mount'),
168
+ conflicts: [],
169
+ status: 'disconnected'
170
+ }
171
+ })
172
+ }
173
+ }))
174
+ )
175
+ }
176
+
177
+ export function useHybridLiveComponent<T = any>(
178
+ componentName: string,
179
+ initialState: T,
180
+ options: HybridComponentOptions = {}
181
+ ): UseHybridLiveComponentReturn<T> {
182
+ const {
183
+ fallbackToLocal = true,
184
+ room,
185
+ userId,
186
+ autoMount = true,
187
+ debug = false
188
+ } = options
189
+
190
+ // Create unique instance ID to avoid conflicts between multiple instances
191
+ const instanceId = useRef(`${componentName}-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`)
192
+ const logPrefix = `${instanceId.current}${room ? `[${room}]` : ''}`
193
+
194
+ // Create Zustand store instance (one per component instance)
195
+ const storeRef = useRef<ReturnType<typeof createHybridStore<T>> | null>(null)
196
+ if (!storeRef.current) {
197
+ storeRef.current = createHybridStore(initialState)
198
+ }
199
+ const store = storeRef.current
200
+
201
+ // Get state from Zustand store with optimized selectors
202
+ const hybridState = store((state) => state.hybridState)
203
+ const stateData = store((state) => state.hybridState.data)
204
+ 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
+
229
+ // Component state
230
+ const [componentId, setComponentId] = useState<string | null>(null)
231
+ const [lastServerState, setLastServerState] = useState<T | null>(null)
232
+ const [mountLoading, setMountLoading] = useState(false) // Only for mount/unmount operations
233
+ const [error, setError] = useState<string | null>(null)
234
+ const [rehydrating, setRehydrating] = useState(false)
235
+ const [currentSignedState, setCurrentSignedState] = useState<any>(null)
236
+ const mountedRef = useRef(false)
237
+ const mountingRef = useRef(false)
238
+ const lastKnownComponentIdRef = useRef<string | null>(null)
239
+
240
+ const log = useCallback((message: string, data?: any) => {
241
+ if (debug) {
242
+ console.log(`[${logPrefix}] ${message}`, data)
243
+ }
244
+ }, [debug, logPrefix])
245
+
246
+ // Prevent multiple simultaneous re-hydration attempts
247
+ const rehydrationAttemptRef = useRef<Promise<boolean> | null>(null)
248
+
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)
364
+ useEffect(() => {
365
+ if (!componentId) {
366
+ return
367
+ }
368
+
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
+ }
389
+
390
+ log('✅ Processing message immediately', { type: message.type, componentId: message.componentId })
391
+
392
+ switch (message.type) {
393
+ case 'STATE_UPDATE':
394
+ log('Processing STATE_UPDATE', message.payload)
395
+ if (message.payload?.state) {
396
+ const newState = message.payload.state
397
+ log('Updating Zustand with server state', newState)
398
+ updateState(newState, 'server')
399
+ setLastServerState(newState)
400
+
401
+ // Debug signed state persistence
402
+ if (message.payload?.signedState) {
403
+ log('Found signedState in STATE_UPDATE - persisting', {
404
+ componentName,
405
+ signedState: message.payload.signedState
406
+ })
407
+ setCurrentSignedState(message.payload.signedState)
408
+ 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
+ }
413
+
414
+ log('State updated from server successfully', newState)
415
+ } else {
416
+ log('STATE_UPDATE has no state payload', message.payload)
417
+ }
418
+ break
419
+
420
+ case 'STATE_REHYDRATED':
421
+ log('Processing STATE_REHYDRATED', message.payload)
422
+ if (message.payload?.state && message.payload?.newComponentId) {
423
+ const newState = message.payload.state
424
+ 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
434
+ setComponentId(newComponentId)
435
+ lastKnownComponentIdRef.current = newComponentId
436
+ updateState(newState, 'server')
437
+ setLastServerState(newState)
438
+
439
+ // Update signed state
440
+ if (message.payload?.signedState) {
441
+ setCurrentSignedState(message.payload.signedState)
442
+ persistComponentState(componentName, message.payload.signedState, room, userId)
443
+ }
444
+
445
+ setRehydrating(false)
446
+ setError(null)
447
+
448
+ log('Re-hydration completed successfully')
449
+ }
450
+ break
451
+
452
+ 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
+ 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
470
+ setComponentId(message.result.newComponentId)
471
+ lastKnownComponentIdRef.current = message.result.newComponentId
472
+ setRehydrating(false)
473
+ setError(null)
474
+
475
+ log('🎯 ComponentId updated, re-hydration completed')
476
+ } else if (!message.success) {
477
+ log('❌ Re-hydration failed', message.error)
478
+ setRehydrating(false)
479
+ setError(message.error || 'Re-hydration failed')
480
+ }
481
+
482
+ // This is also handled by sendMessageAndWait, but we process it here too for immediate UI updates
483
+ break
484
+
485
+ 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
+ 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
+
498
+ 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')
512
+ }
513
+ }
514
+ break
515
+
516
+ case 'BROADCAST':
517
+ log('Received broadcast', message.payload)
518
+ break
519
+
520
+ case 'ERROR':
521
+ log('Received error', message.payload)
522
+ const errorMessage = message.payload?.error || 'Unknown error'
523
+
524
+ // Check for re-hydration required error
525
+ 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
+
532
+ 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')
546
+ }
547
+ } else {
548
+ setError(errorMessage)
549
+ }
550
+ break
551
+ }
552
+ })
553
+
554
+ // Cleanup callback on unmount
555
+ return unsubscribe
556
+ }, [componentId, updateState, log, onMessage, attemptRehydration])
557
+
558
+ // Mount component
559
+ const mount = useCallback(async () => {
560
+ if (!connected || mountedRef.current || mountingRef.current) {
561
+ return
562
+ }
563
+
564
+ mountingRef.current = true
565
+ setMountLoading(true)
566
+ setError(null)
567
+ log('Mounting component - server will control all state')
568
+
569
+ try {
570
+ const message: WebSocketMessage = {
571
+ type: 'COMPONENT_MOUNT',
572
+ componentId: instanceId.current,
573
+ payload: {
574
+ component: componentName,
575
+ props: initialState,
576
+ room,
577
+ userId
578
+ }
579
+ }
580
+
581
+ const response = await sendMessageAndWait(message, 10000)
582
+
583
+ log('Mount response received', { response, fullResponse: JSON.stringify(response) })
584
+
585
+ if (response?.success && response?.result?.componentId) {
586
+ const newComponentId = response.result.componentId
587
+ setComponentId(newComponentId)
588
+ mountedRef.current = true
589
+
590
+ // Immediately persist signed state from mount response
591
+ if (response.result.signedState) {
592
+ log('Found signedState in mount response - persisting immediately', {
593
+ componentName,
594
+ signedState: response.result.signedState
595
+ })
596
+ setCurrentSignedState(response.result.signedState)
597
+ 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
+ }
602
+
603
+ // Update state if provided
604
+ if (response.result.initialState) {
605
+ log('Updating state from mount response', response.result.initialState)
606
+ updateState(response.result.initialState, 'server')
607
+ setLastServerState(response.result.initialState)
608
+ }
609
+
610
+ log('Component mounted successfully', { componentId: newComponentId })
611
+ } 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
+ throw new Error(response?.error || 'No component ID returned from server')
620
+ }
621
+ } catch (err) {
622
+ const errorMessage = err instanceof Error ? err.message : 'Mount failed'
623
+ setError(errorMessage)
624
+ log('Mount failed', err)
625
+
626
+ if (fallbackToLocal) {
627
+ log('Using local state as fallback until reconnection')
628
+ } else {
629
+ throw err
630
+ }
631
+ } finally {
632
+ setMountLoading(false)
633
+ mountingRef.current = false
634
+ }
635
+ }, [connected, componentName, initialState, room, userId, sendMessage, log, fallbackToLocal])
636
+
637
+ // Unmount component
638
+ const unmount = useCallback(async () => {
639
+ if (!componentId || !connected) {
640
+ return
641
+ }
642
+
643
+ log('Unmounting component', { componentId })
644
+
645
+ try {
646
+ await sendMessage({
647
+ type: 'COMPONENT_UNMOUNT',
648
+ componentId
649
+ })
650
+
651
+ setComponentId(null)
652
+ mountedRef.current = false
653
+ mountingRef.current = false
654
+ log('Component unmounted successfully')
655
+ } catch (err) {
656
+ log('Unmount failed', err)
657
+ }
658
+ }, [componentId, connected, sendMessage, log])
659
+
660
+
661
+ // Server-only actions (no client-side state mutations)
662
+ const call = useCallback(async (action: string, payload?: any): Promise<void> => {
663
+ if (!componentId || !connected) {
664
+ throw new Error('Component not mounted or WebSocket not connected')
665
+ }
666
+
667
+ log('Calling server action', { action, payload })
668
+
669
+ try {
670
+ // Don't set loading for actions to avoid UI flicker
671
+
672
+ const message: WebSocketMessage = {
673
+ type: 'CALL_ACTION',
674
+ componentId,
675
+ action,
676
+ payload,
677
+ expectResponse: true // Always expect response to catch errors like re-hydration required
678
+ }
679
+
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')
725
+ }
726
+ } else {
727
+ // Re-throw other WebSocket errors
728
+ throw wsError
729
+ }
730
+ }
731
+
732
+ log('Action sent to server - waiting for server state update', { action, payload })
733
+ } catch (err) {
734
+ const errorMessage = err instanceof Error ? err.message : 'Action failed'
735
+ setError(errorMessage)
736
+ log('Action failed', { action, error: err })
737
+ throw err
738
+ } finally {
739
+ // No loading state for actions to prevent UI flicker
740
+ }
741
+ }, [componentId, connected, sendMessage, log])
742
+
743
+ // Call action and wait for specific return value
744
+ const callAndWait = useCallback(async (action: string, payload?: any, timeout?: number): Promise<any> => {
745
+ if (!componentId || !connected) {
746
+ throw new Error('Component not mounted or WebSocket not connected')
747
+ }
748
+
749
+ log('Calling server action and waiting for response', { action, payload })
750
+
751
+ try {
752
+ // Don't set loading for callAndWait to avoid UI flicker
753
+
754
+ const message: WebSocketMessage = {
755
+ type: 'CALL_ACTION',
756
+ componentId,
757
+ action,
758
+ payload
759
+ }
760
+
761
+ // Send action and wait for response
762
+ const result = await sendMessageAndWait(message, timeout)
763
+
764
+ log('Action completed with result', { action, payload, result })
765
+ return result
766
+ } catch (err) {
767
+ const errorMessage = err instanceof Error ? err.message : 'Action failed'
768
+ setError(errorMessage)
769
+ log('Action failed', { action, error: err })
770
+ throw err
771
+ } finally {
772
+ // No loading state for actions to prevent UI flicker
773
+ }
774
+ }, [componentId, connected, sendMessageAndWait, log])
775
+
776
+ // Auto-mount with re-hydration attempt
777
+ useEffect(() => {
778
+ 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
+ attemptRehydration().then(rehydrated => {
790
+ if (!rehydrated && !mountedRef.current && !componentId && !mountingRef.current) {
791
+ log('Re-hydration failed or not available, proceeding with normal mount')
792
+ mount()
793
+ } else if (rehydrated) {
794
+ log('Re-hydration successful, skipping normal mount')
795
+ }
796
+ }).catch(error => {
797
+ log('Re-hydration attempt failed with error, proceeding with normal mount', error)
798
+ if (!mountedRef.current && !componentId && !mountingRef.current) {
799
+ mount()
800
+ }
801
+ })
802
+ }
803
+ }, [connected, autoMount, mount, componentId, log, rehydrating, attemptRehydration])
804
+
805
+ // Monitor connection status changes and force reconnection
806
+ const prevConnectedRef = useRef(connected)
807
+ useEffect(() => {
808
+ const wasConnected = prevConnectedRef.current
809
+ 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
819
+ if (wasConnected && !isConnected && mountedRef.current) {
820
+ log('🔄 Connection lost - marking component for remount on reconnection')
821
+ mountedRef.current = false
822
+ setComponentId(null)
823
+ }
824
+
825
+ // If we reconnected and don't have a component mounted, try re-hydration first
826
+ 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
+ setTimeout(() => {
831
+ if (!mountedRef.current && !mountingRef.current && !rehydrating) {
832
+ 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
+ })
852
+ } else {
853
+ log('🚀 No persisted state found - executing fresh mount after reconnection')
854
+ mount()
855
+ }
856
+ }
857
+ }, 100)
858
+ }
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
+
871
+ prevConnectedRef.current = connected
872
+ }, [connected, mount, componentId, log, attemptRehydration, componentName, rehydrating])
873
+
874
+ // Unmount on cleanup
875
+ useEffect(() => {
876
+ return () => {
877
+ if (mountedRef.current) {
878
+ unmount()
879
+ }
880
+ }
881
+ }, [unmount])
882
+
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)
891
+ const useControlledField = useCallback(<K extends keyof T>(
892
+ field: K,
893
+ action: string = 'updateField'
894
+ ) => {
895
+ const [tempValue, setTempValue] = useState<T[K]>(stateData[field])
896
+
897
+ // Always sync temp value with server state (server is source of truth)
898
+ useEffect(() => {
899
+ setTempValue(stateData[field])
900
+ }, [stateData[field]])
901
+
902
+ const commitValue = useCallback(async (value?: T[K]) => {
903
+ 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
+ await call(action, { field, value: valueToCommit })
908
+
909
+ // No local state mutation - wait for server response
910
+ }, [tempValue, field, action])
911
+
912
+ return {
913
+ value: tempValue,
914
+ setValue: setTempValue,
915
+ commit: commitValue,
916
+ isDirty: JSON.stringify(tempValue) !== JSON.stringify(stateData[field])
917
+ }
918
+ }, [stateData, call, log])
919
+
920
+ // Calculate detailed status
921
+ const getStatus = () => {
922
+ if (!connected) return 'connecting'
923
+ if (rehydrating) return 'reconnecting'
924
+ if (mountLoading) return 'loading' // Only show loading for mount operations
925
+ if (error) return 'error'
926
+ if (!componentId) return 'mounting'
927
+ if (hybridState.status === 'disconnected') return 'disconnected'
928
+ return 'synced'
929
+ }
930
+
931
+ const status = getStatus()
932
+
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
+ }
942
+
943
+ return {
944
+ // Server-driven state
945
+ state: stateData,
946
+
947
+ // Status
948
+ loading: mountLoading, // Only loading for mount operations
949
+ error,
950
+ connected,
951
+ componentId,
952
+ status,
953
+
954
+ // Actions (all server-driven)
955
+ call,
956
+ callAndWait,
957
+ mount,
958
+ unmount,
959
+
960
+ // WebSocket utilities
961
+ sendMessage,
962
+ sendMessageAndWait,
963
+
964
+ // Helper for forms
965
+ useControlledField
966
+ }
967
+ }