create-fluxstack 1.10.1 → 1.12.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 (257) hide show
  1. package/.dockerignore +1 -2
  2. package/Dockerfile +8 -8
  3. package/LLMD/INDEX.md +64 -0
  4. package/LLMD/MAINTENANCE.md +197 -0
  5. package/LLMD/MIGRATION.md +156 -0
  6. package/LLMD/config/.gitkeep +1 -0
  7. package/LLMD/config/declarative-system.md +268 -0
  8. package/LLMD/config/environment-vars.md +327 -0
  9. package/LLMD/config/runtime-reload.md +401 -0
  10. package/LLMD/core/.gitkeep +1 -0
  11. package/LLMD/core/build-system.md +599 -0
  12. package/LLMD/core/framework-lifecycle.md +229 -0
  13. package/LLMD/core/plugin-system.md +451 -0
  14. package/LLMD/patterns/.gitkeep +1 -0
  15. package/LLMD/patterns/anti-patterns.md +297 -0
  16. package/LLMD/patterns/project-structure.md +264 -0
  17. package/LLMD/patterns/type-safety.md +440 -0
  18. package/LLMD/reference/.gitkeep +1 -0
  19. package/LLMD/reference/cli-commands.md +250 -0
  20. package/LLMD/reference/plugin-hooks.md +357 -0
  21. package/LLMD/reference/routing.md +39 -0
  22. package/LLMD/reference/troubleshooting.md +364 -0
  23. package/LLMD/resources/.gitkeep +1 -0
  24. package/LLMD/resources/controllers.md +465 -0
  25. package/LLMD/resources/live-components.md +703 -0
  26. package/LLMD/resources/live-rooms.md +482 -0
  27. package/LLMD/resources/live-upload.md +130 -0
  28. package/LLMD/resources/plugins-external.md +617 -0
  29. package/LLMD/resources/routes-eden.md +254 -0
  30. package/README.md +37 -17
  31. package/app/client/index.html +0 -1
  32. package/app/client/src/App.tsx +107 -150
  33. package/app/client/src/components/AppLayout.tsx +68 -0
  34. package/app/client/src/components/BackButton.tsx +13 -0
  35. package/app/client/src/components/DemoPage.tsx +20 -0
  36. package/app/client/src/components/LiveUploadWidget.tsx +204 -0
  37. package/app/client/src/lib/eden-api.ts +85 -60
  38. package/app/client/src/live/ChatDemo.tsx +107 -0
  39. package/app/client/src/live/CounterDemo.tsx +206 -0
  40. package/app/client/src/live/FormDemo.tsx +119 -0
  41. package/app/client/src/live/RoomChatDemo.tsx +242 -0
  42. package/app/client/src/live/UploadDemo.tsx +21 -0
  43. package/app/client/src/main.tsx +4 -1
  44. package/app/client/src/pages/ApiTestPage.tsx +108 -0
  45. package/app/client/src/pages/HomePage.tsx +76 -0
  46. package/app/server/app.ts +1 -4
  47. package/app/server/controllers/users.controller.ts +36 -44
  48. package/app/server/index.ts +25 -35
  49. package/app/server/live/LiveChat.ts +77 -0
  50. package/app/server/live/LiveCounter.ts +67 -0
  51. package/app/server/live/LiveForm.ts +63 -0
  52. package/app/server/live/LiveLocalCounter.ts +32 -0
  53. package/app/server/live/LiveRoomChat.ts +285 -0
  54. package/app/server/live/LiveUpload.ts +81 -0
  55. package/app/server/routes/index.ts +3 -1
  56. package/app/server/routes/room.routes.ts +117 -0
  57. package/app/server/routes/users.routes.ts +35 -27
  58. package/app/shared/types/index.ts +14 -2
  59. package/config/app.config.ts +2 -62
  60. package/config/client.config.ts +2 -95
  61. package/config/database.config.ts +2 -99
  62. package/config/fluxstack.config.ts +25 -45
  63. package/config/index.ts +57 -38
  64. package/config/monitoring.config.ts +2 -114
  65. package/config/plugins.config.ts +2 -80
  66. package/config/server.config.ts +2 -68
  67. package/config/services.config.ts +2 -130
  68. package/config/system/app.config.ts +29 -0
  69. package/config/system/build.config.ts +49 -0
  70. package/config/system/client.config.ts +68 -0
  71. package/config/system/database.config.ts +17 -0
  72. package/config/system/fluxstack.config.ts +114 -0
  73. package/config/{logger.config.ts → system/logger.config.ts} +3 -1
  74. package/config/system/monitoring.config.ts +114 -0
  75. package/config/system/plugins.config.ts +84 -0
  76. package/config/{runtime.config.ts → system/runtime.config.ts} +1 -1
  77. package/config/system/server.config.ts +68 -0
  78. package/config/system/services.config.ts +46 -0
  79. package/config/{system.config.ts → system/system.config.ts} +1 -1
  80. package/core/build/flux-plugins-generator.ts +325 -325
  81. package/core/build/index.ts +39 -27
  82. package/core/build/live-components-generator.ts +3 -3
  83. package/core/build/optimizer.ts +235 -235
  84. package/core/cli/command-registry.ts +6 -4
  85. package/core/cli/commands/build.ts +79 -0
  86. package/core/cli/commands/create.ts +54 -0
  87. package/core/cli/commands/dev.ts +101 -0
  88. package/core/cli/commands/help.ts +34 -0
  89. package/core/cli/commands/index.ts +34 -0
  90. package/core/cli/commands/make-plugin.ts +90 -0
  91. package/core/cli/commands/plugin-add.ts +197 -0
  92. package/core/cli/commands/plugin-deps.ts +2 -2
  93. package/core/cli/commands/plugin-list.ts +208 -0
  94. package/core/cli/commands/plugin-remove.ts +170 -0
  95. package/core/cli/generators/component.ts +769 -769
  96. package/core/cli/generators/controller.ts +1 -1
  97. package/core/cli/generators/index.ts +146 -146
  98. package/core/cli/generators/interactive.ts +227 -227
  99. package/core/cli/generators/plugin.ts +2 -2
  100. package/core/cli/generators/prompts.ts +82 -82
  101. package/core/cli/generators/route.ts +6 -6
  102. package/core/cli/generators/service.ts +2 -2
  103. package/core/cli/generators/template-engine.ts +4 -3
  104. package/core/cli/generators/types.ts +2 -2
  105. package/core/cli/generators/utils.ts +191 -191
  106. package/core/cli/index.ts +115 -686
  107. package/core/cli/plugin-discovery.ts +2 -2
  108. package/core/client/LiveComponentsProvider.tsx +60 -8
  109. package/core/client/api/eden.ts +183 -0
  110. package/core/client/api/index.ts +11 -0
  111. package/core/client/components/Live.tsx +104 -0
  112. package/core/client/fluxstack.ts +1 -9
  113. package/core/client/hooks/AdaptiveChunkSizer.ts +215 -215
  114. package/core/client/hooks/state-validator.ts +1 -1
  115. package/core/client/hooks/useAuth.ts +48 -48
  116. package/core/client/hooks/useChunkedUpload.ts +85 -35
  117. package/core/client/hooks/useLiveChunkedUpload.ts +87 -0
  118. package/core/client/hooks/useLiveComponent.ts +800 -0
  119. package/core/client/hooks/useLiveUpload.ts +71 -0
  120. package/core/client/hooks/useRoom.ts +409 -0
  121. package/core/client/hooks/useRoomProxy.ts +382 -0
  122. package/core/client/index.ts +17 -68
  123. package/core/client/standalone-entry.ts +8 -0
  124. package/core/client/standalone.ts +74 -53
  125. package/core/client/state/createStore.ts +192 -192
  126. package/core/client/state/index.ts +14 -14
  127. package/core/config/index.ts +70 -291
  128. package/core/config/schema.ts +42 -723
  129. package/core/framework/client.ts +131 -131
  130. package/core/framework/index.ts +7 -7
  131. package/core/framework/server.ts +47 -40
  132. package/core/framework/types.ts +2 -2
  133. package/core/index.ts +23 -4
  134. package/core/live/ComponentRegistry.ts +3 -3
  135. package/core/live/types.ts +77 -0
  136. package/core/plugins/built-in/index.ts +134 -134
  137. package/core/plugins/built-in/live-components/commands/create-live-component.ts +242 -1066
  138. package/core/plugins/built-in/live-components/index.ts +1 -1
  139. package/core/plugins/built-in/monitoring/index.ts +111 -47
  140. package/core/plugins/built-in/static/index.ts +1 -1
  141. package/core/plugins/built-in/swagger/index.ts +68 -265
  142. package/core/plugins/built-in/vite/index.ts +85 -185
  143. package/core/plugins/built-in/vite/vite-dev.ts +10 -16
  144. package/core/plugins/config.ts +9 -7
  145. package/core/plugins/dependency-manager.ts +31 -1
  146. package/core/plugins/discovery.ts +19 -7
  147. package/core/plugins/executor.ts +2 -2
  148. package/core/plugins/index.ts +203 -203
  149. package/core/plugins/manager.ts +27 -39
  150. package/core/plugins/module-resolver.ts +19 -8
  151. package/core/plugins/registry.ts +255 -19
  152. package/core/plugins/types.ts +20 -53
  153. package/core/server/framework.ts +66 -43
  154. package/core/server/index.ts +15 -15
  155. package/core/server/live/ComponentRegistry.ts +78 -71
  156. package/core/server/live/FileUploadManager.ts +23 -10
  157. package/core/server/live/LiveComponentPerformanceMonitor.ts +1 -1
  158. package/core/server/live/LiveRoomManager.ts +261 -0
  159. package/core/server/live/RoomEventBus.ts +234 -0
  160. package/core/server/live/RoomStateManager.ts +172 -0
  161. package/core/server/live/StateSignature.ts +643 -643
  162. package/core/server/live/WebSocketConnectionManager.ts +30 -19
  163. package/core/server/live/auto-generated-components.ts +21 -9
  164. package/core/server/live/index.ts +14 -0
  165. package/core/server/live/websocket-plugin.ts +214 -67
  166. package/core/server/middleware/elysia-helpers.ts +7 -2
  167. package/core/server/middleware/errorHandling.ts +1 -1
  168. package/core/server/middleware/index.ts +31 -31
  169. package/core/server/plugins/database.ts +180 -180
  170. package/core/server/plugins/static-files-plugin.ts +69 -69
  171. package/core/server/plugins/swagger.ts +1 -1
  172. package/core/server/rooms/RoomBroadcaster.ts +357 -0
  173. package/core/server/rooms/RoomSystem.ts +463 -0
  174. package/core/server/rooms/index.ts +13 -0
  175. package/core/server/services/BaseService.ts +1 -1
  176. package/core/server/services/ServiceContainer.ts +1 -1
  177. package/core/server/services/index.ts +8 -8
  178. package/core/templates/create-project.ts +12 -12
  179. package/core/testing/index.ts +9 -9
  180. package/core/testing/setup.ts +73 -73
  181. package/core/types/api.ts +168 -168
  182. package/core/types/build.ts +219 -219
  183. package/core/types/config.ts +56 -26
  184. package/core/types/index.ts +4 -4
  185. package/core/types/plugin.ts +107 -107
  186. package/core/types/types.ts +353 -14
  187. package/core/utils/build-logger.ts +324 -324
  188. package/core/utils/config-schema.ts +480 -480
  189. package/core/utils/env.ts +2 -8
  190. package/core/utils/errors/codes.ts +114 -114
  191. package/core/utils/errors/handlers.ts +36 -1
  192. package/core/utils/errors/index.ts +49 -5
  193. package/core/utils/errors/middleware.ts +113 -113
  194. package/core/utils/helpers.ts +6 -16
  195. package/core/utils/index.ts +17 -17
  196. package/core/utils/logger/colors.ts +114 -114
  197. package/core/utils/logger/config.ts +13 -9
  198. package/core/utils/logger/formatter.ts +82 -82
  199. package/core/utils/logger/group-logger.ts +101 -101
  200. package/core/utils/logger/index.ts +6 -1
  201. package/core/utils/logger/stack-trace.ts +3 -1
  202. package/core/utils/logger/startup-banner.ts +82 -82
  203. package/core/utils/logger/winston-logger.ts +152 -152
  204. package/core/utils/monitoring/index.ts +211 -211
  205. package/core/utils/sync-version.ts +66 -66
  206. package/core/utils/version.ts +1 -1
  207. package/create-fluxstack.ts +8 -7
  208. package/package.json +12 -13
  209. package/plugins/crypto-auth/cli/make-protected-route.command.ts +1 -1
  210. package/plugins/crypto-auth/client/CryptoAuthClient.ts +302 -302
  211. package/plugins/crypto-auth/client/components/index.ts +11 -11
  212. package/plugins/crypto-auth/client/index.ts +11 -11
  213. package/plugins/crypto-auth/config/index.ts +1 -1
  214. package/plugins/crypto-auth/index.ts +4 -4
  215. package/plugins/crypto-auth/package.json +65 -65
  216. package/plugins/crypto-auth/server/AuthMiddleware.ts +1 -1
  217. package/plugins/crypto-auth/server/CryptoAuthService.ts +185 -185
  218. package/plugins/crypto-auth/server/index.ts +21 -21
  219. package/plugins/crypto-auth/server/middlewares/cryptoAuthAdmin.ts +3 -3
  220. package/plugins/crypto-auth/server/middlewares/cryptoAuthOptional.ts +1 -1
  221. package/plugins/crypto-auth/server/middlewares/cryptoAuthPermissions.ts +2 -2
  222. package/plugins/crypto-auth/server/middlewares/cryptoAuthRequired.ts +2 -2
  223. package/plugins/crypto-auth/server/middlewares/helpers.ts +1 -1
  224. package/plugins/crypto-auth/server/middlewares/index.ts +22 -22
  225. package/tsconfig.api-strict.json +16 -0
  226. package/tsconfig.json +48 -52
  227. package/{app/client/tsconfig.node.json → tsconfig.node.json} +25 -25
  228. package/types/global.d.ts +29 -29
  229. package/types/vitest.d.ts +8 -8
  230. package/vite.config.ts +38 -62
  231. package/vitest.config.live.ts +10 -9
  232. package/vitest.config.ts +29 -17
  233. package/app/client/README.md +0 -69
  234. package/app/client/SIMPLIFICATION.md +0 -140
  235. package/app/client/frontend-only.ts +0 -12
  236. package/app/client/src/live/FileUploadExample.tsx +0 -359
  237. package/app/client/src/live/MinimalLiveClock.tsx +0 -47
  238. package/app/client/src/live/QuickUploadTest.tsx +0 -193
  239. package/app/client/tsconfig.app.json +0 -45
  240. package/app/client/tsconfig.json +0 -7
  241. package/app/client/zustand-setup.md +0 -65
  242. package/app/server/backend-only.ts +0 -18
  243. package/app/server/live/LiveClockComponent.ts +0 -215
  244. package/app/server/live/LiveFileUploadComponent.ts +0 -77
  245. package/app/server/routes/env-test.ts +0 -110
  246. package/core/client/hooks/index.ts +0 -7
  247. package/core/client/hooks/useHybridLiveComponent.ts +0 -685
  248. package/core/client/hooks/useTypedLiveComponent.ts +0 -133
  249. package/core/client/hooks/useWebSocket.ts +0 -361
  250. package/core/config/env.ts +0 -546
  251. package/core/config/loader.ts +0 -522
  252. package/core/config/runtime-config.ts +0 -327
  253. package/core/config/validator.ts +0 -540
  254. package/core/server/backend-entry.ts +0 -51
  255. package/core/server/standalone.ts +0 -106
  256. package/core/utils/regenerate-files.ts +0 -69
  257. package/fluxstack.config.ts +0 -354
@@ -1,685 +0,0 @@
1
- // 🔥 Hybrid Live Component Hook v2 - Uses Single WebSocket Connection
2
- // Refactored to use LiveComponentsProvider context instead of creating its own connection
3
-
4
- import { useState, useEffect, useCallback, useRef } from 'react'
5
- import { create } from 'zustand'
6
- import { subscribeWithSelector } from 'zustand/middleware'
7
- import { useLiveComponents } from '../LiveComponentsProvider'
8
- import { StateValidator } from './state-validator'
9
- import type {
10
- HybridState,
11
- StateConflict,
12
- HybridComponentOptions,
13
- WebSocketMessage,
14
- WebSocketResponse
15
- } from '@/core/types/types'
16
-
17
- // Client-side state persistence for reconnection
18
- interface PersistedComponentState {
19
- componentName: string
20
- signedState: any
21
- room?: string
22
- userId?: string
23
- lastUpdate: number
24
- }
25
-
26
- const STORAGE_KEY_PREFIX = 'fluxstack_component_'
27
- const STATE_MAX_AGE = 24 * 60 * 60 * 1000 // 24 hours
28
-
29
- // Global re-hydration throttling by component name
30
- const globalRehydrationAttempts = new Map<string, Promise<boolean>>()
31
-
32
- // Utility functions for state persistence
33
- const persistComponentState = (componentName: string, signedState: any, room?: string, userId?: string) => {
34
- try {
35
- const persistedState: PersistedComponentState = {
36
- componentName,
37
- signedState,
38
- room,
39
- userId,
40
- lastUpdate: Date.now()
41
- }
42
-
43
- const key = `${STORAGE_KEY_PREFIX}${componentName}`
44
- localStorage.setItem(key, JSON.stringify(persistedState))
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
- const stored = localStorage.getItem(key)
54
-
55
- if (!stored) {
56
- return null
57
- }
58
-
59
- const persistedState: PersistedComponentState = JSON.parse(stored)
60
-
61
- // Check if state is not too old
62
- const age = Date.now() - persistedState.lastUpdate
63
- if (age > STATE_MAX_AGE) {
64
- localStorage.removeItem(key)
65
- return null
66
- }
67
-
68
- return persistedState
69
- } catch (error) {
70
- console.warn('⚠️ Failed to retrieve persisted state:', error)
71
- return null
72
- }
73
- }
74
-
75
- const clearPersistedState = (componentName: string) => {
76
- try {
77
- const key = `${STORAGE_KEY_PREFIX}${componentName}`
78
- localStorage.removeItem(key)
79
- } catch (error) {
80
- console.warn('⚠️ Failed to clear persisted state:', error)
81
- }
82
- }
83
-
84
- interface HybridStore<T> {
85
- hybridState: HybridState<T>
86
- updateState: (newState: T, source?: 'server' | 'mount') => void
87
- reset: (initialState: T) => void
88
- }
89
-
90
- export interface UseHybridLiveComponentReturn<T> {
91
- // Server-driven state (read-only from frontend perspective)
92
- state: T
93
-
94
- // Status information
95
- loading: boolean
96
- error: string | null
97
- connected: boolean
98
- componentId: string | null
99
-
100
- // Connection status with all possible states
101
- status: 'synced' | 'disconnected' | 'connecting' | 'reconnecting' | 'loading' | 'mounting' | 'error'
102
-
103
- // Actions (all go to server)
104
- call: (action: string, payload?: any) => Promise<void>
105
- callAndWait: (action: string, payload?: any, timeout?: number) => Promise<any>
106
- mount: () => Promise<void>
107
- unmount: () => Promise<void>
108
-
109
- // Helper for temporary input state
110
- useControlledField: <K extends keyof T>(field: K, action?: string) => {
111
- value: T[K]
112
- setValue: (value: T[K]) => void
113
- commit: (value?: T[K]) => Promise<void>
114
- isDirty: boolean
115
- }
116
- }
117
-
118
- /**
119
- * Create Zustand store for component instance
120
- */
121
- function createHybridStore<T>(initialState: T) {
122
- return create<HybridStore<T>>()(
123
- subscribeWithSelector((set, get) => ({
124
- hybridState: {
125
- data: initialState,
126
- validation: StateValidator.createValidation(initialState, 'mount'),
127
- conflicts: [],
128
- status: 'disconnected' as const
129
- },
130
-
131
- updateState: (newState: T, source: 'server' | 'mount' = 'server') => {
132
- set((state) => {
133
- return {
134
- hybridState: {
135
- data: newState,
136
- validation: StateValidator.createValidation(newState, source),
137
- conflicts: [],
138
- status: 'synced'
139
- }
140
- }
141
- })
142
- },
143
-
144
- reset: (initialState: T) => {
145
- set({
146
- hybridState: {
147
- data: initialState,
148
- validation: StateValidator.createValidation(initialState, 'mount'),
149
- conflicts: [],
150
- status: 'disconnected'
151
- }
152
- })
153
- }
154
- }))
155
- )
156
- }
157
-
158
- export function useHybridLiveComponent<T = any>(
159
- componentName: string,
160
- initialState: T,
161
- options: HybridComponentOptions = {}
162
- ): UseHybridLiveComponentReturn<T> {
163
- const {
164
- fallbackToLocal = true,
165
- room,
166
- userId,
167
- autoMount = true,
168
- debug = false,
169
- onConnect,
170
- onMount,
171
- onDisconnect,
172
- onRehydrate,
173
- onError,
174
- onStateChange
175
- } = options
176
-
177
- // Use Live Components context (singleton WebSocket connection)
178
- const {
179
- connected,
180
- sendMessage: contextSendMessage,
181
- sendMessageAndWait: contextSendMessageAndWait,
182
- registerComponent,
183
- unregisterComponent
184
- } = useLiveComponents()
185
-
186
- // Create unique instance ID
187
- const instanceId = useRef(`${componentName}-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`)
188
- const logPrefix = `${instanceId.current}${room ? `[${room}]` : ''}`
189
-
190
- // Create Zustand store instance (one per component instance)
191
- const storeRef = useRef<ReturnType<typeof createHybridStore<T>> | null>(null)
192
- if (!storeRef.current) {
193
- storeRef.current = createHybridStore(initialState)
194
- }
195
- const store = storeRef.current
196
-
197
- // Get state from Zustand store
198
- const hybridState = store((state) => state.hybridState)
199
- const stateData = store((state) => state.hybridState.data)
200
- const updateState = store((state) => state.updateState)
201
-
202
- // Component state
203
- const [componentId, setComponentId] = useState<string | null>(null)
204
- const [lastServerState, setLastServerState] = useState<T | null>(null)
205
- const [mountLoading, setMountLoading] = useState(false)
206
- const [error, setError] = useState<string | null>(null)
207
- const [rehydrating, setRehydrating] = useState(false)
208
- const [currentSignedState, setCurrentSignedState] = useState<any>(null)
209
- const mountedRef = useRef(false)
210
- const mountingRef = useRef(false)
211
- const lastKnownComponentIdRef = useRef<string | null>(null)
212
-
213
- const log = useCallback((message: string, data?: any) => {
214
- if (debug) {
215
- console.log(`[${logPrefix}] ${message}`, data)
216
- }
217
- }, [debug, logPrefix])
218
-
219
- // Prevent multiple simultaneous re-hydration attempts
220
- const rehydrationAttemptRef = useRef<Promise<boolean> | null>(null)
221
-
222
- // Register this component with WebSocket context
223
- useEffect(() => {
224
- if (!componentId) return
225
-
226
- log('📝 Registering component with WebSocket context', componentId)
227
-
228
- const unregister = registerComponent(componentId, (message: WebSocketResponse) => {
229
- log('📨 Received message for component', { type: message.type })
230
-
231
- switch (message.type) {
232
- case 'STATE_UPDATE':
233
- if (message.payload?.state) {
234
- const newState = message.payload.state
235
- const oldState = stateData
236
- updateState(newState, 'server')
237
- setLastServerState(newState)
238
-
239
- // Call onStateChange callback
240
- onStateChange?.(newState, oldState)
241
-
242
- if (message.payload?.signedState) {
243
- setCurrentSignedState(message.payload.signedState)
244
- persistComponentState(componentName, message.payload.signedState, room, userId)
245
- }
246
- }
247
- break
248
-
249
- case 'STATE_REHYDRATED':
250
- if (message.payload?.state && message.payload?.newComponentId) {
251
- const newState = message.payload.state
252
- const newComponentId = message.payload.newComponentId
253
-
254
- log('Component re-hydrated successfully', { newComponentId })
255
-
256
- setComponentId(newComponentId)
257
- lastKnownComponentIdRef.current = newComponentId
258
- updateState(newState, 'server')
259
- setLastServerState(newState)
260
-
261
- if (message.payload?.signedState) {
262
- setCurrentSignedState(message.payload.signedState)
263
- persistComponentState(componentName, message.payload.signedState, room, userId)
264
- }
265
-
266
- setRehydrating(false)
267
- setError(null)
268
-
269
- // Call onRehydrate callback
270
- onRehydrate?.()
271
- }
272
- break
273
-
274
- case 'COMPONENT_REHYDRATED':
275
- if (message.success && message.result?.newComponentId) {
276
- log('✅ Re-hydration succeeded', message.result.newComponentId)
277
-
278
- setComponentId(message.result.newComponentId)
279
- lastKnownComponentIdRef.current = message.result.newComponentId
280
- setRehydrating(false)
281
- setError(null)
282
-
283
- // Call onRehydrate callback
284
- onRehydrate?.()
285
- } else if (!message.success) {
286
- log('❌ Re-hydration failed', message.error)
287
- setRehydrating(false)
288
- const errorMessage = message.error || 'Re-hydration failed'
289
- setError(errorMessage)
290
-
291
- // Call onError callback
292
- onError?.(errorMessage)
293
- }
294
- break
295
-
296
- case 'MESSAGE_RESPONSE':
297
- if (!message.success && message.error?.includes?.('COMPONENT_REHYDRATION_REQUIRED')) {
298
- log('🔄 Component re-hydration required')
299
- if (!rehydrating) {
300
- attemptRehydration()
301
- }
302
- }
303
- break
304
-
305
- case 'ERROR':
306
- const errorMsg = message.payload?.error || 'Unknown error'
307
- if (errorMsg.includes('COMPONENT_REHYDRATION_REQUIRED')) {
308
- log('🔄 Component re-hydration required from ERROR')
309
- if (!rehydrating) {
310
- attemptRehydration()
311
- }
312
- } else {
313
- setError(errorMsg)
314
- // Call onError callback
315
- onError?.(errorMsg)
316
- }
317
- break
318
-
319
- case 'COMPONENT_PONG':
320
- log('🏓 Received pong from server')
321
- // Component is alive - update lastActivity if needed
322
- break
323
- }
324
- })
325
-
326
- return () => {
327
- log('🗑️ Unregistering component from WebSocket context')
328
- unregister()
329
- }
330
- }, [componentId, registerComponent, unregisterComponent, log, updateState, componentName, room, userId, rehydrating, stateData, onStateChange, onRehydrate, onError])
331
-
332
- // Automatic re-hydration on reconnection
333
- const attemptRehydration = useCallback(async () => {
334
- if (!connected || rehydrating || mountingRef.current) {
335
- return false
336
- }
337
-
338
- // Prevent multiple simultaneous attempts (local)
339
- if (rehydrationAttemptRef.current) {
340
- return await rehydrationAttemptRef.current
341
- }
342
-
343
- // Prevent multiple simultaneous attempts (global)
344
- if (globalRehydrationAttempts.has(componentName)) {
345
- return await globalRehydrationAttempts.get(componentName)!
346
- }
347
-
348
- const persistedState = getPersistedState(componentName)
349
- if (!persistedState) {
350
- return false
351
- }
352
-
353
- // Check if state is too old (> 1 hour = likely expired signing key)
354
- const stateAge = Date.now() - persistedState.lastUpdate
355
- const ONE_HOUR = 60 * 60 * 1000
356
- if (stateAge > ONE_HOUR) {
357
- log('⏰ Persisted state too old, clearing and skipping rehydration', {
358
- age: stateAge,
359
- ageMinutes: Math.floor(stateAge / 60000)
360
- })
361
- clearPersistedState(componentName)
362
- return false
363
- }
364
-
365
- const rehydrationPromise = (async () => {
366
- setRehydrating(true)
367
- setError(null)
368
-
369
- try {
370
- const tempComponentId = lastKnownComponentIdRef.current || instanceId.current
371
-
372
- const response = await contextSendMessageAndWait({
373
- type: 'COMPONENT_REHYDRATE',
374
- componentId: tempComponentId,
375
- payload: {
376
- componentName,
377
- signedState: persistedState.signedState,
378
- room: persistedState.room,
379
- userId: persistedState.userId
380
- }
381
- }, 2000) // Reduced from 10s to 2s - fast fail for invalid states
382
-
383
- if (response?.success && response?.result?.newComponentId) {
384
- setComponentId(response.result.newComponentId)
385
- lastKnownComponentIdRef.current = response.result.newComponentId
386
- mountedRef.current = true
387
-
388
- // Call onRehydrate callback after React has processed the state update
389
- // This ensures the component is registered to receive messages before the callback runs
390
- setTimeout(() => {
391
- onRehydrate?.()
392
- }, 0)
393
-
394
- return true
395
- } else {
396
- clearPersistedState(componentName)
397
- const errorMsg = response?.error || 'Re-hydration failed'
398
- setError(errorMsg)
399
- onError?.(errorMsg)
400
- return false
401
- }
402
-
403
- } catch (error: any) {
404
- clearPersistedState(componentName)
405
- setError(error.message)
406
- return false
407
- } finally {
408
- setRehydrating(false)
409
- rehydrationAttemptRef.current = null
410
- globalRehydrationAttempts.delete(componentName)
411
- }
412
- })()
413
-
414
- rehydrationAttemptRef.current = rehydrationPromise
415
- globalRehydrationAttempts.set(componentName, rehydrationPromise)
416
-
417
- return await rehydrationPromise
418
- }, [connected, rehydrating, componentName, contextSendMessageAndWait, log, onRehydrate, onError])
419
-
420
- // Mount component
421
- const mount = useCallback(async () => {
422
- if (!connected || mountedRef.current || mountingRef.current) {
423
- return
424
- }
425
-
426
- mountingRef.current = true
427
- setMountLoading(true)
428
- setError(null)
429
-
430
- try {
431
- const message: WebSocketMessage = {
432
- type: 'COMPONENT_MOUNT',
433
- componentId: instanceId.current,
434
- payload: {
435
- component: componentName,
436
- props: initialState,
437
- room,
438
- userId
439
- }
440
- }
441
-
442
- const response = await contextSendMessageAndWait(message, 5000) // 5s for mount is enough
443
-
444
- if (response?.success && response?.result?.componentId) {
445
- const newComponentId = response.result.componentId
446
- setComponentId(newComponentId)
447
- lastKnownComponentIdRef.current = newComponentId
448
- mountedRef.current = true
449
-
450
- if (response.result.signedState) {
451
- setCurrentSignedState(response.result.signedState)
452
- persistComponentState(componentName, response.result.signedState, room, userId)
453
- }
454
-
455
- if (response.result.initialState) {
456
- updateState(response.result.initialState, 'server')
457
- setLastServerState(response.result.initialState)
458
- }
459
-
460
- log('✅ Component mounted successfully', { componentId: newComponentId })
461
-
462
- // Call onMount callback after React has processed the state update
463
- // This ensures the component is registered to receive messages before the callback runs
464
- setTimeout(() => {
465
- onMount?.()
466
- }, 0)
467
- } else {
468
- throw new Error(response?.error || 'No component ID returned from server')
469
- }
470
- } catch (err) {
471
- const errorMessage = err instanceof Error ? err.message : 'Mount failed'
472
- setError(errorMessage)
473
- log('❌ Mount failed', err)
474
-
475
- // Call onError callback
476
- onError?.(errorMessage)
477
-
478
- if (!fallbackToLocal) {
479
- throw err
480
- }
481
- } finally {
482
- setMountLoading(false)
483
- mountingRef.current = false
484
- }
485
- }, [connected, componentName, initialState, room, userId, contextSendMessageAndWait, log, fallbackToLocal, updateState, onMount, onError])
486
-
487
- // Unmount component
488
- const unmount = useCallback(async () => {
489
- if (!componentId || !connected) {
490
- return
491
- }
492
-
493
- try {
494
- await contextSendMessage({
495
- type: 'COMPONENT_UNMOUNT',
496
- componentId
497
- })
498
-
499
- setComponentId(null)
500
- mountedRef.current = false
501
- mountingRef.current = false
502
- log('✅ Component unmounted successfully')
503
- } catch (err) {
504
- log('❌ Unmount failed', err)
505
- }
506
- }, [componentId, connected, contextSendMessage, log])
507
-
508
- // Server-only actions
509
- const call = useCallback(async (action: string, payload?: any): Promise<void> => {
510
- // Use ref as fallback for componentId (handles timing issues after rehydration)
511
- const currentComponentId = componentId || lastKnownComponentIdRef.current
512
- if (!currentComponentId || !connected) {
513
- throw new Error('Component not mounted or WebSocket not connected')
514
- }
515
-
516
- try {
517
- const message: WebSocketMessage = {
518
- type: 'CALL_ACTION',
519
- componentId: currentComponentId,
520
- action,
521
- payload
522
- }
523
-
524
- const response = await contextSendMessageAndWait(message, 5000)
525
-
526
- if (!response.success && response.error?.includes?.('COMPONENT_REHYDRATION_REQUIRED')) {
527
- const rehydrated = await attemptRehydration()
528
- if (rehydrated) {
529
- // Use updated ref for retry
530
- const retryComponentId = lastKnownComponentIdRef.current || currentComponentId
531
- const retryMessage: WebSocketMessage = {
532
- type: 'CALL_ACTION',
533
- componentId: retryComponentId,
534
- action,
535
- payload
536
- }
537
- await contextSendMessageAndWait(retryMessage, 5000)
538
- } else {
539
- throw new Error('Component lost connection and could not be recovered')
540
- }
541
- } else if (!response.success) {
542
- throw new Error(response.error || 'Action failed')
543
- }
544
- } catch (err) {
545
- const errorMessage = err instanceof Error ? err.message : 'Action failed'
546
- setError(errorMessage)
547
- throw err
548
- }
549
- }, [componentId, connected, contextSendMessageAndWait, attemptRehydration])
550
-
551
- // Call action and wait for specific return value
552
- const callAndWait = useCallback(async (action: string, payload?: any, timeout?: number): Promise<any> => {
553
- // Use ref as fallback for componentId (handles timing issues after rehydration)
554
- const currentComponentId = componentId || lastKnownComponentIdRef.current
555
- if (!currentComponentId || !connected) {
556
- throw new Error('Component not mounted or WebSocket not connected')
557
- }
558
-
559
- try {
560
- const message: WebSocketMessage = {
561
- type: 'CALL_ACTION',
562
- componentId: currentComponentId,
563
- action,
564
- payload
565
- }
566
-
567
- const result = await contextSendMessageAndWait(message, timeout)
568
- return result
569
- } catch (err) {
570
- const errorMessage = err instanceof Error ? err.message : 'Action failed'
571
- setError(errorMessage)
572
- throw err
573
- }
574
- }, [componentId, connected, contextSendMessageAndWait])
575
-
576
- // Auto-mount with re-hydration attempt
577
- useEffect(() => {
578
- if (connected && autoMount && !mountedRef.current && !componentId && !mountingRef.current && !rehydrating) {
579
- attemptRehydration().then(rehydrated => {
580
- if (!rehydrated && !mountedRef.current && !componentId && !mountingRef.current) {
581
- mount()
582
- }
583
- }).catch(() => {
584
- if (!mountedRef.current && !componentId && !mountingRef.current) {
585
- mount()
586
- }
587
- })
588
- }
589
- }, [connected, autoMount, mount, componentId, rehydrating, attemptRehydration])
590
-
591
- // Monitor connection status changes
592
- const prevConnectedRef = useRef(connected)
593
- useEffect(() => {
594
- const wasConnected = prevConnectedRef.current
595
- const isConnected = connected
596
-
597
- if (wasConnected && !isConnected && mountedRef.current) {
598
- mountedRef.current = false
599
- setComponentId(null)
600
- // Call onDisconnect callback
601
- onDisconnect?.()
602
- }
603
-
604
- if (!wasConnected && isConnected) {
605
- // Call onConnect callback when WebSocket connects
606
- onConnect?.()
607
-
608
- if (!mountedRef.current && !mountingRef.current && !rehydrating) {
609
- setTimeout(() => {
610
- if (!mountedRef.current && !mountingRef.current && !rehydrating) {
611
- const persistedState = getPersistedState(componentName)
612
-
613
- if (persistedState?.signedState) {
614
- attemptRehydration()
615
- } else {
616
- mount()
617
- }
618
- }
619
- }, 100)
620
- }
621
- }
622
-
623
- prevConnectedRef.current = connected
624
- }, [connected, mount, componentId, attemptRehydration, componentName, rehydrating, onDisconnect, onConnect])
625
-
626
- // Unmount on cleanup
627
- useEffect(() => {
628
- return () => {
629
- if (mountedRef.current) {
630
- unmount()
631
- }
632
- }
633
- }, [unmount])
634
-
635
- // Helper for controlled inputs
636
- const useControlledField = useCallback(<K extends keyof T>(
637
- field: K,
638
- action: string = 'updateField'
639
- ) => {
640
- const [tempValue, setTempValue] = useState<T[K]>(stateData[field])
641
-
642
- useEffect(() => {
643
- setTempValue(stateData[field])
644
- }, [stateData[field]])
645
-
646
- const commitValue = useCallback(async (value?: T[K]) => {
647
- const valueToCommit = value !== undefined ? value : tempValue
648
- await call(action, { field, value: valueToCommit })
649
- }, [tempValue, field, action])
650
-
651
- return {
652
- value: tempValue,
653
- setValue: setTempValue,
654
- commit: commitValue,
655
- isDirty: JSON.stringify(tempValue) !== JSON.stringify(stateData[field])
656
- }
657
- }, [stateData, call])
658
-
659
- // Calculate status
660
- const getStatus = () => {
661
- if (!connected) return 'connecting'
662
- if (rehydrating) return 'reconnecting'
663
- if (mountLoading) return 'loading'
664
- if (error) return 'error'
665
- if (!componentId) return 'mounting'
666
- if (hybridState.status === 'disconnected') return 'disconnected'
667
- return 'synced'
668
- }
669
-
670
- const status = getStatus()
671
-
672
- return {
673
- state: stateData,
674
- loading: mountLoading,
675
- error,
676
- connected,
677
- componentId,
678
- status,
679
- call,
680
- callAndWait,
681
- mount,
682
- unmount,
683
- useControlledField
684
- }
685
- }