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
@@ -0,0 +1,800 @@
1
+ // 🔥 FluxStack Live Component Hook - Proxy-based State Access
2
+ // Acesse estado do servidor como se fossem variáveis locais (estilo Livewire)
3
+ //
4
+ // Uso:
5
+ // const clock = useLiveComponent('LiveClock', { currentTime: '', format: '24h' })
6
+ //
7
+ // // Lê estado como variável normal
8
+ // console.log(clock.currentTime) // "14:30:25"
9
+ //
10
+ // // Escreve estado - sincroniza automaticamente com servidor
11
+ // clock.format = '12h'
12
+ //
13
+ // // Chama actions diretamente
14
+ // await clock.setTimeFormat({ format: '24h' })
15
+ //
16
+ // // Metadata via $ prefix
17
+ // clock.$connected // boolean
18
+ // clock.$loading // boolean
19
+ // clock.$error // string | null
20
+
21
+ import { useRef, useMemo, useState, useEffect, useCallback } from 'react'
22
+ import { create } from 'zustand'
23
+ import { subscribeWithSelector } from 'zustand/middleware'
24
+ import { useLiveComponents } from '../LiveComponentsProvider'
25
+ import { StateValidator } from './state-validator'
26
+ import { RoomManager } from './useRoomProxy'
27
+ import type { RoomProxy, RoomServerMessage } from './useRoomProxy'
28
+ import type {
29
+ HybridState,
30
+ HybridComponentOptions,
31
+ WebSocketMessage,
32
+ WebSocketResponse
33
+ } from '@core/types/types'
34
+
35
+ // ===== Tipos =====
36
+
37
+ // Opções para $field()
38
+ export interface FieldOptions {
39
+ /** Quando sincronizar: 'change' (debounced), 'blur' (ao sair), 'manual' (só $sync) */
40
+ syncOn?: 'change' | 'blur' | 'manual'
41
+ /** Debounce em ms (só para syncOn: 'change'). Default: 150 */
42
+ debounce?: number
43
+ /** Transformar valor antes de sincronizar */
44
+ transform?: (value: any) => any
45
+ }
46
+
47
+ // Retorno do $field()
48
+ export interface FieldBinding {
49
+ value: any
50
+ onChange: (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) => void
51
+ onBlur: () => void
52
+ name: string
53
+ }
54
+
55
+ export interface LiveComponentProxy<
56
+ TState extends Record<string, any>,
57
+ TRoomState = any,
58
+ TRoomEvents extends Record<string, any> = Record<string, any>
59
+ > {
60
+ // Propriedades de estado são acessadas diretamente: proxy.propertyName
61
+
62
+ // Metadata ($ prefix)
63
+ readonly $state: TState
64
+ readonly $connected: boolean
65
+ readonly $loading: boolean
66
+ readonly $error: string | null
67
+ readonly $status: 'synced' | 'disconnected' | 'connecting' | 'reconnecting' | 'loading' | 'mounting' | 'error'
68
+ readonly $componentId: string | null
69
+ readonly $dirty: boolean
70
+
71
+ // Methods
72
+ $call: (action: string, payload?: any) => Promise<void>
73
+ $callAndWait: <R = any>(action: string, payload?: any, timeout?: number) => Promise<R>
74
+ $mount: () => Promise<void>
75
+ $unmount: () => Promise<void>
76
+ $refresh: () => Promise<void>
77
+ $set: <K extends keyof TState>(key: K, value: TState[K]) => Promise<void>
78
+
79
+ /** Bind de campo com controle de sincronização */
80
+ $field: <K extends keyof TState>(key: K, options?: FieldOptions) => FieldBinding
81
+
82
+ /** Sincroniza todos os campos pendentes (para syncOn: 'manual') */
83
+ $sync: () => Promise<void>
84
+
85
+ /** Registra handler para broadcasts recebidos de outros usuários (sem tipagem) */
86
+ $onBroadcast: (handler: (type: string, data: any) => void) => void
87
+
88
+ /** Atualiza estado local diretamente (para processar broadcasts) */
89
+ $updateLocal: (updates: Partial<TState>) => void
90
+
91
+ /**
92
+ * Sistema de salas - acessa sala padrão ou específica
93
+ * @example
94
+ * // Sala padrão (definida em options.room)
95
+ * component.$room.emit('typing', { user: 'João' })
96
+ * component.$room.on('message:new', handler)
97
+ *
98
+ * // Sala específica
99
+ * component.$room('sala-vip').join()
100
+ * component.$room('sala-vip').emit('typing', { user: 'João' })
101
+ * component.$room('sala-vip').leave()
102
+ */
103
+ readonly $room: RoomProxy<TRoomState, TRoomEvents>
104
+
105
+ /** Lista de IDs das salas que está participando */
106
+ readonly $rooms: string[]
107
+ }
108
+
109
+ // Helper type para criar union de broadcasts
110
+ type BroadcastEvent<T extends Record<string, any>> = {
111
+ [K in keyof T]: { type: K; data: T[K] }
112
+ }[keyof T]
113
+
114
+ // Proxy com broadcasts tipados
115
+ export interface LiveComponentProxyWithBroadcasts<
116
+ TState extends Record<string, any>,
117
+ TBroadcasts extends Record<string, any> = Record<string, any>,
118
+ TRoomState = any,
119
+ TRoomEvents extends Record<string, any> = Record<string, any>
120
+ > extends Omit<LiveComponentProxy<TState, TRoomState, TRoomEvents>, '$onBroadcast'> {
121
+ /**
122
+ * Registra handler para broadcasts tipados
123
+ * @example
124
+ * // Uso com tipagem:
125
+ * chat.$onBroadcast<LiveChatBroadcasts>((event) => {
126
+ * if (event.type === 'NEW_MESSAGE') {
127
+ * console.log(event.data.message) // ✅ Tipado como ChatMessage
128
+ * }
129
+ * })
130
+ */
131
+ $onBroadcast: <T extends TBroadcasts = TBroadcasts>(
132
+ handler: (event: BroadcastEvent<T>) => void
133
+ ) => void
134
+ }
135
+
136
+ // Actions são qualquer método que não existe no state
137
+ export type LiveProxy<
138
+ TState extends Record<string, any>,
139
+ TActions = {},
140
+ TRoomState = any,
141
+ TRoomEvents extends Record<string, any> = Record<string, any>
142
+ > = TState & LiveComponentProxy<TState, TRoomState, TRoomEvents> & TActions
143
+
144
+ // Proxy com broadcasts tipados
145
+ export type LiveProxyWithBroadcasts<
146
+ TState extends Record<string, any>,
147
+ TActions = {},
148
+ TBroadcasts extends Record<string, any> = Record<string, any>,
149
+ TRoomState = any,
150
+ TRoomEvents extends Record<string, any> = Record<string, any>
151
+ > = TState & LiveComponentProxyWithBroadcasts<TState, TBroadcasts, TRoomState, TRoomEvents> & TActions
152
+
153
+ export interface UseLiveComponentOptions extends HybridComponentOptions {
154
+ /** Debounce para sets (ms). Default: 150 */
155
+ debounce?: number
156
+ /** Atualização otimista (UI atualiza antes do servidor confirmar). Default: true */
157
+ optimistic?: boolean
158
+ /** Modo de sync: 'immediate' | 'debounced' | 'manual'. Default: 'debounced' */
159
+ syncMode?: 'immediate' | 'debounced' | 'manual'
160
+ /** Persistir estado em localStorage (rehydration). Default: true */
161
+ persistState?: boolean
162
+ }
163
+
164
+ // ===== Propriedades Reservadas =====
165
+
166
+ const RESERVED_PROPS = new Set([
167
+ '$state', '$connected', '$loading', '$error', '$status', '$componentId', '$dirty',
168
+ '$call', '$callAndWait', '$mount', '$unmount', '$refresh', '$set', '$onBroadcast', '$updateLocal',
169
+ '$room', '$rooms', '$field', '$sync',
170
+ 'then', 'toJSON', 'valueOf', 'toString',
171
+ Symbol.toStringTag, Symbol.iterator,
172
+ ])
173
+
174
+ // ===== Persistência de Estado =====
175
+
176
+ const STORAGE_KEY_PREFIX = 'fluxstack_component_'
177
+ const STATE_MAX_AGE = 24 * 60 * 60 * 1000
178
+
179
+ interface PersistedState {
180
+ componentName: string
181
+ signedState: any
182
+ room?: string
183
+ userId?: string
184
+ lastUpdate: number
185
+ }
186
+
187
+ const persistState = (enabled: boolean, name: string, signedState: any, room?: string, userId?: string) => {
188
+ if (!enabled) return
189
+ try {
190
+ localStorage.setItem(`${STORAGE_KEY_PREFIX}${name}`, JSON.stringify({
191
+ componentName: name, signedState, room, userId, lastUpdate: Date.now()
192
+ }))
193
+ } catch {}
194
+ }
195
+
196
+ const getPersistedState = (enabled: boolean, name: string): PersistedState | null => {
197
+ if (!enabled) return null
198
+ try {
199
+ const stored = localStorage.getItem(`${STORAGE_KEY_PREFIX}${name}`)
200
+ if (!stored) return null
201
+ const state: PersistedState = JSON.parse(stored)
202
+ if (Date.now() - state.lastUpdate > STATE_MAX_AGE) {
203
+ localStorage.removeItem(`${STORAGE_KEY_PREFIX}${name}`)
204
+ return null
205
+ }
206
+ return state
207
+ } catch { return null }
208
+ }
209
+
210
+ const clearPersistedState = (enabled: boolean, name: string) => {
211
+ if (!enabled) return
212
+ try { localStorage.removeItem(`${STORAGE_KEY_PREFIX}${name}`) } catch {}
213
+ }
214
+
215
+ // ===== Zustand Store =====
216
+
217
+ interface Store<T> {
218
+ state: T
219
+ status: 'synced' | 'disconnected'
220
+ updateState: (newState: T) => void
221
+ }
222
+
223
+ function createStore<T>(initialState: T) {
224
+ return create<Store<T>>()(
225
+ subscribeWithSelector((set) => ({
226
+ state: initialState,
227
+ status: 'disconnected',
228
+ updateState: (newState: T) => set({ state: newState, status: 'synced' })
229
+ }))
230
+ )
231
+ }
232
+
233
+ // ===== Hook Principal =====
234
+
235
+ export function useLiveComponent<
236
+ TState extends Record<string, any>,
237
+ TActions = {},
238
+ TBroadcasts extends Record<string, any> = Record<string, any>
239
+ >(
240
+ componentName: string,
241
+ initialState: TState,
242
+ options: UseLiveComponentOptions = {}
243
+ ): LiveProxyWithBroadcasts<TState, TActions, TBroadcasts> {
244
+ const {
245
+ debounce = 150,
246
+ optimistic = true,
247
+ syncMode = 'debounced',
248
+ persistState: persistEnabled = true,
249
+ fallbackToLocal = true,
250
+ room,
251
+ userId,
252
+ autoMount = true,
253
+ debug = false,
254
+ onConnect,
255
+ onMount,
256
+ onDisconnect,
257
+ onRehydrate,
258
+ onError,
259
+ onStateChange
260
+ } = options
261
+
262
+ // WebSocket context
263
+ const {
264
+ connected,
265
+ sendMessage,
266
+ sendMessageAndWait,
267
+ registerComponent,
268
+ unregisterComponent
269
+ } = useLiveComponents()
270
+
271
+ // Refs
272
+ const instanceId = useRef(`${componentName}-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`)
273
+ const storeRef = useRef<ReturnType<typeof createStore<TState>> | null>(null)
274
+ if (!storeRef.current) storeRef.current = createStore(initialState)
275
+ const store = storeRef.current
276
+
277
+ const pendingChanges = useRef<Map<keyof TState, { value: any; synced: boolean }>>(new Map())
278
+ const debounceTimers = useRef<Map<keyof TState, NodeJS.Timeout>>(new Map())
279
+ const localFieldValues = useRef<Map<keyof TState, any>>(new Map()) // Valores locais para campos com syncOn: blur/manual
280
+ const fieldOptions = useRef<Map<keyof TState, FieldOptions>>(new Map()) // Opções por campo
281
+ const [localVersion, setLocalVersion] = useState(0) // Força re-render quando valores locais mudam
282
+ const mountedRef = useRef(false)
283
+ const mountingRef = useRef(false)
284
+ const rehydratingRef = useRef(false) // Previne múltiplas tentativas de rehydrate
285
+ const lastComponentIdRef = useRef<string | null>(null)
286
+ const broadcastHandlerRef = useRef<((event: { type: string; data: any }) => void) | null>(null)
287
+ const roomMessageHandlers = useRef<Set<(msg: RoomServerMessage) => void>>(new Set())
288
+ const roomManagerRef = useRef<RoomManager | null>(null)
289
+
290
+ // State
291
+ const stateData = store((s) => s.state)
292
+ const updateState = store((s) => s.updateState)
293
+ const [componentId, setComponentId] = useState<string | null>(null)
294
+ const [loading, setLoading] = useState(false)
295
+ const [error, setError] = useState<string | null>(null)
296
+ const [rehydrating, setRehydrating] = useState(false)
297
+ const [mountFailed, setMountFailed] = useState(false) // Previne loop infinito de mount
298
+
299
+ const log = useCallback((msg: string, data?: any) => {
300
+ if (debug) console.log(`[${componentName}] ${msg}`, data || '')
301
+ }, [debug, componentName])
302
+
303
+ // ===== Set Property =====
304
+ const setProperty = useCallback(async <K extends keyof TState>(key: K, value: TState[K]) => {
305
+ // Clear existing timer
306
+ const timer = debounceTimers.current.get(key)
307
+ if (timer) clearTimeout(timer)
308
+
309
+ // Track pending
310
+ pendingChanges.current.set(key, { value, synced: false })
311
+
312
+ const doSync = async () => {
313
+ try {
314
+ const id = componentId || lastComponentIdRef.current
315
+ if (!id || !connected) return
316
+
317
+ await sendMessageAndWait({
318
+ type: 'CALL_ACTION',
319
+ componentId: id,
320
+ action: 'setValue',
321
+ payload: { key, value }
322
+ }, 5000)
323
+
324
+ pendingChanges.current.get(key)!.synced = true
325
+ } catch (err: any) {
326
+ pendingChanges.current.delete(key)
327
+ setError(err.message)
328
+ }
329
+ }
330
+
331
+ if (syncMode === 'immediate') {
332
+ await doSync()
333
+ } else if (syncMode === 'debounced') {
334
+ debounceTimers.current.set(key, setTimeout(doSync, debounce))
335
+ }
336
+ }, [componentId, connected, sendMessageAndWait, debounce, syncMode])
337
+
338
+ // ===== Mount =====
339
+ const mount = useCallback(async () => {
340
+ // Usa refs para prevenir chamadas duplicadas (React StrictMode)
341
+ if (!connected || mountedRef.current || mountingRef.current || rehydratingRef.current || mountFailed) return
342
+
343
+ mountingRef.current = true
344
+ setLoading(true)
345
+ setError(null)
346
+
347
+ try {
348
+ const response = await sendMessageAndWait({
349
+ type: 'COMPONENT_MOUNT',
350
+ componentId: instanceId.current,
351
+ payload: { component: componentName, props: initialState, room, userId }
352
+ }, 5000)
353
+
354
+ if (response?.success && response?.result?.componentId) {
355
+ const newId = response.result.componentId
356
+ setComponentId(newId)
357
+ lastComponentIdRef.current = newId
358
+ mountedRef.current = true
359
+
360
+ if (response.result.signedState) {
361
+ persistState(persistEnabled, componentName, response.result.signedState, room, userId)
362
+ }
363
+ if (response.result.initialState) {
364
+ updateState(response.result.initialState)
365
+ }
366
+
367
+ log('Mounted', newId)
368
+ setTimeout(() => onMount?.(), 0)
369
+ } else {
370
+ throw new Error(response?.error || 'Mount failed')
371
+ }
372
+ } catch (err: any) {
373
+ setError(err.message)
374
+ setMountFailed(true) // Previne loop infinito
375
+ onError?.(err.message)
376
+ if (!fallbackToLocal) throw err
377
+ } finally {
378
+ setLoading(false)
379
+ mountingRef.current = false
380
+ }
381
+ }, [connected, componentName, initialState, room, userId, sendMessageAndWait, updateState, log, onMount, onError, fallbackToLocal, mountFailed])
382
+
383
+ // ===== Unmount =====
384
+ const unmount = useCallback(async () => {
385
+ if (!componentId || !connected) return
386
+ try {
387
+ await sendMessage({ type: 'COMPONENT_UNMOUNT', componentId })
388
+ setComponentId(null)
389
+ mountedRef.current = false
390
+ } catch {}
391
+ }, [componentId, connected, sendMessage])
392
+
393
+ // ===== Rehydrate =====
394
+ const rehydrate = useCallback(async () => {
395
+ // Usa ref para prevenir chamadas duplicadas (React StrictMode)
396
+ if (!connected || rehydratingRef.current || mountingRef.current || mountedRef.current) return false
397
+
398
+ const persisted = getPersistedState(persistEnabled, componentName)
399
+ if (!persisted) return false
400
+
401
+ // Skip if too old (> 1 hour)
402
+ if (Date.now() - persisted.lastUpdate > 60 * 60 * 1000) {
403
+ clearPersistedState(persistEnabled, componentName)
404
+ return false
405
+ }
406
+
407
+ rehydratingRef.current = true
408
+ setRehydrating(true)
409
+ try {
410
+ const response = await sendMessageAndWait({
411
+ type: 'COMPONENT_REHYDRATE',
412
+ componentId: lastComponentIdRef.current || instanceId.current,
413
+ payload: {
414
+ componentName,
415
+ signedState: persisted.signedState,
416
+ room: persisted.room,
417
+ userId: persisted.userId
418
+ }
419
+ }, 2000)
420
+
421
+ if (response?.success && response?.result?.newComponentId) {
422
+ setComponentId(response.result.newComponentId)
423
+ lastComponentIdRef.current = response.result.newComponentId
424
+ mountedRef.current = true
425
+ setTimeout(() => onRehydrate?.(), 0)
426
+ return true
427
+ }
428
+ clearPersistedState(persistEnabled, componentName)
429
+ return false
430
+ } catch {
431
+ clearPersistedState(persistEnabled, componentName)
432
+ return false
433
+ } finally {
434
+ rehydratingRef.current = false
435
+ setRehydrating(false)
436
+ }
437
+ }, [connected, componentName, sendMessageAndWait, onRehydrate])
438
+
439
+ // ===== Call Action =====
440
+ const call = useCallback(async (action: string, payload?: any) => {
441
+ const id = componentId || lastComponentIdRef.current
442
+ if (!id || !connected) throw new Error('Not connected')
443
+
444
+ const response = await sendMessageAndWait({
445
+ type: 'CALL_ACTION',
446
+ componentId: id,
447
+ action,
448
+ payload
449
+ }, 5000)
450
+
451
+ if (!response.success) throw new Error(response.error || 'Action failed')
452
+ }, [componentId, connected, sendMessageAndWait])
453
+
454
+ const callAndWait = useCallback(async <R = any>(action: string, payload?: any, timeout = 10000): Promise<R> => {
455
+ const id = componentId || lastComponentIdRef.current
456
+ if (!id || !connected) throw new Error('Not connected')
457
+
458
+ const response = await sendMessageAndWait({
459
+ type: 'CALL_ACTION',
460
+ componentId: id,
461
+ action,
462
+ payload
463
+ }, timeout)
464
+
465
+ return response as R
466
+ }, [componentId, connected, sendMessageAndWait])
467
+
468
+ // ===== Refresh =====
469
+ const refresh = useCallback(async () => {
470
+ for (const [key, change] of pendingChanges.current) {
471
+ if (!change.synced) {
472
+ await setProperty(key, change.value)
473
+ }
474
+ }
475
+ }, [setProperty])
476
+
477
+ // ===== Sync (para campos com syncOn: manual) =====
478
+ const sync = useCallback(async () => {
479
+ const promises: Promise<void>[] = []
480
+
481
+ for (const [key, value] of localFieldValues.current) {
482
+ const currentServerValue = stateData[key]
483
+ if (value !== currentServerValue) {
484
+ promises.push(setProperty(key, value))
485
+ }
486
+ }
487
+
488
+ await Promise.all(promises)
489
+ }, [stateData, setProperty])
490
+
491
+ // ===== Field Binding =====
492
+ const createFieldBinding = useCallback(<K extends keyof TState>(
493
+ key: K,
494
+ options: FieldOptions = {}
495
+ ): FieldBinding => {
496
+ const {
497
+ syncOn = 'change',
498
+ debounce: fieldDebounce = debounce,
499
+ transform
500
+ } = options
501
+
502
+ // Salvar opções do campo
503
+ fieldOptions.current.set(key, options)
504
+
505
+ // Valor atual: local (se existir) ou do servidor
506
+ const currentValue = localFieldValues.current.has(key)
507
+ ? localFieldValues.current.get(key)
508
+ : stateData[key]
509
+
510
+ return {
511
+ name: String(key),
512
+ value: currentValue ?? '',
513
+
514
+ onChange: (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) => {
515
+ let value: any = e.target.value
516
+
517
+ // Checkbox support
518
+ if (e.target.type === 'checkbox') {
519
+ value = (e.target as HTMLInputElement).checked
520
+ }
521
+
522
+ // Transform
523
+ if (transform) {
524
+ value = transform(value)
525
+ }
526
+
527
+ // Sempre salvar localmente primeiro (para UI responsiva)
528
+ localFieldValues.current.set(key, value)
529
+
530
+ // Forçar re-render
531
+ setLocalVersion(v => v + 1)
532
+ pendingChanges.current.set(key, { value, synced: false })
533
+
534
+ if (syncOn === 'change') {
535
+ // Debounced sync
536
+ const timer = debounceTimers.current.get(key)
537
+ if (timer) clearTimeout(timer)
538
+
539
+ debounceTimers.current.set(key, setTimeout(async () => {
540
+ await setProperty(key, value)
541
+ localFieldValues.current.delete(key) // Limpar valor local após sync
542
+ }, fieldDebounce))
543
+ }
544
+ // blur e manual: não faz nada aqui, espera onBlur ou $sync()
545
+ },
546
+
547
+ onBlur: () => {
548
+ if (syncOn === 'blur') {
549
+ const value = localFieldValues.current.get(key)
550
+ if (value !== undefined && value !== stateData[key]) {
551
+ setProperty(key, value).then(() => {
552
+ localFieldValues.current.delete(key)
553
+ })
554
+ }
555
+ }
556
+ }
557
+ }
558
+ }, [stateData, debounce, setProperty, localVersion])
559
+
560
+ // ===== Register with WebSocket =====
561
+ useEffect(() => {
562
+ if (!componentId) return
563
+
564
+ const unregister = registerComponent(componentId, (message: WebSocketResponse) => {
565
+ switch (message.type) {
566
+ case 'STATE_UPDATE':
567
+ if (message.payload?.state) {
568
+ const oldState = stateData
569
+ updateState(message.payload.state)
570
+ onStateChange?.(message.payload.state, oldState)
571
+ if (message.payload?.signedState) {
572
+ persistState(persistEnabled, componentName, message.payload.signedState, room, userId)
573
+ }
574
+ }
575
+ break
576
+ case 'STATE_REHYDRATED':
577
+ if (message.payload?.state && message.payload?.newComponentId) {
578
+ setComponentId(message.payload.newComponentId)
579
+ lastComponentIdRef.current = message.payload.newComponentId
580
+ updateState(message.payload.state)
581
+ setRehydrating(false)
582
+ onRehydrate?.()
583
+ }
584
+ break
585
+ case 'BROADCAST':
586
+ // Handle broadcast messages from other users in the same room
587
+ if (message.payload?.type) {
588
+ // Emit broadcast event for component to handle (as { type, data } object)
589
+ broadcastHandlerRef.current?.({ type: message.payload.type, data: message.payload.data })
590
+ }
591
+ break
592
+ case 'ERROR':
593
+ setError(message.payload?.error || 'Unknown error')
594
+ onError?.(message.payload?.error)
595
+ break
596
+
597
+ // Room system messages
598
+ case 'ROOM_EVENT':
599
+ case 'ROOM_STATE':
600
+ case 'ROOM_SYSTEM':
601
+ case 'ROOM_JOINED':
602
+ case 'ROOM_LEFT':
603
+ // Forward to room handlers
604
+ for (const handler of roomMessageHandlers.current) {
605
+ handler(message as unknown as RoomServerMessage)
606
+ }
607
+ break
608
+ }
609
+ })
610
+
611
+ return () => unregister()
612
+ }, [componentId, registerComponent, updateState, stateData, componentName, room, userId, onStateChange, onRehydrate, onError])
613
+
614
+ // ===== Auto Mount =====
615
+ useEffect(() => {
616
+ if (connected && autoMount && !mountedRef.current && !componentId && !mountingRef.current && !rehydrating && !mountFailed) {
617
+ rehydrate().then(ok => {
618
+ if (!ok && !mountedRef.current && !mountFailed) mount()
619
+ })
620
+ }
621
+ }, [connected, autoMount, mount, componentId, rehydrating, rehydrate, mountFailed])
622
+
623
+ // ===== Connection Changes =====
624
+ const prevConnected = useRef(connected)
625
+ useEffect(() => {
626
+ if (prevConnected.current && !connected && mountedRef.current) {
627
+ mountedRef.current = false
628
+ setComponentId(null)
629
+ onDisconnect?.()
630
+ }
631
+ if (!prevConnected.current && connected) {
632
+ onConnect?.()
633
+ if (!mountedRef.current && !mountingRef.current) {
634
+ setTimeout(() => {
635
+ const persisted = getPersistedState(persistEnabled, componentName)
636
+ if (persisted?.signedState) rehydrate()
637
+ else mount()
638
+ }, 100)
639
+ }
640
+ }
641
+ prevConnected.current = connected
642
+ }, [connected, mount, rehydrate, componentName, onConnect, onDisconnect])
643
+
644
+ // ===== Room Manager =====
645
+ const roomManager = useMemo(() => {
646
+ if (roomManagerRef.current) {
647
+ roomManagerRef.current.setComponentId(componentId)
648
+ return roomManagerRef.current
649
+ }
650
+
651
+ const manager = new RoomManager({
652
+ componentId,
653
+ defaultRoom: room,
654
+ sendMessage,
655
+ sendMessageAndWait,
656
+ onMessage: (handler) => {
657
+ roomMessageHandlers.current.add(handler)
658
+ return () => {
659
+ roomMessageHandlers.current.delete(handler)
660
+ }
661
+ }
662
+ })
663
+
664
+ roomManagerRef.current = manager
665
+ return manager
666
+ }, [componentId, room, sendMessage, sendMessageAndWait])
667
+
668
+ // Atualizar componentId no RoomManager quando mudar
669
+ useEffect(() => {
670
+ roomManagerRef.current?.setComponentId(componentId)
671
+ }, [componentId])
672
+
673
+ // ===== Cleanup =====
674
+ useEffect(() => {
675
+ return () => {
676
+ debounceTimers.current.forEach(t => clearTimeout(t))
677
+ roomManagerRef.current?.destroy()
678
+ if (mountedRef.current) unmount()
679
+ }
680
+ }, [unmount])
681
+
682
+ // ===== Status =====
683
+ const getStatus = () => {
684
+ if (!connected) return 'connecting'
685
+ if (rehydrating) return 'reconnecting'
686
+ if (loading) return 'loading'
687
+ if (error) return 'error'
688
+ if (!componentId) return 'mounting'
689
+ return 'synced'
690
+ }
691
+
692
+ // ===== Proxy =====
693
+ const proxy = useMemo(() => {
694
+ return new Proxy({} as LiveProxyWithBroadcasts<TState, TActions, TBroadcasts>, {
695
+ get(_, prop: string | symbol) {
696
+ if (typeof prop === 'symbol') {
697
+ if (prop === Symbol.toStringTag) return 'LiveComponent'
698
+ return undefined
699
+ }
700
+
701
+ // Metadata ($ prefix)
702
+ switch (prop) {
703
+ // $state returns FRESH state from store (not stale closure)
704
+ case '$state': return storeRef.current?.getState().state ?? stateData
705
+ case '$connected': return connected
706
+ case '$loading': return loading
707
+ case '$error': return error
708
+ case '$status': return getStatus()
709
+ case '$componentId': return componentId
710
+ case '$dirty': return pendingChanges.current.size > 0
711
+ case '$call': return call
712
+ case '$callAndWait': return callAndWait
713
+ case '$mount': return mount
714
+ case '$unmount': return unmount
715
+ case '$refresh': return refresh
716
+ case '$set': return setProperty
717
+ case '$field': return createFieldBinding
718
+ case '$sync': return sync
719
+ case '$onBroadcast': return (handler: (event: { type: string; data: any }) => void) => {
720
+ broadcastHandlerRef.current = handler
721
+ }
722
+ case '$updateLocal': return (updates: Partial<TState>) => {
723
+ const currentState = storeRef.current?.getState().state
724
+ if (currentState) {
725
+ updateState({ ...currentState, ...updates } as TState)
726
+ }
727
+ }
728
+ case '$room': return roomManager.createProxy()
729
+ case '$rooms': return roomManager.getJoinedRooms()
730
+ }
731
+
732
+ // Se é propriedade do state → retorna valor
733
+ if (prop in stateData) {
734
+ // Valor local tem prioridade (para UI responsiva com $field)
735
+ if (localFieldValues.current.has(prop as keyof TState)) {
736
+ return localFieldValues.current.get(prop as keyof TState)
737
+ }
738
+ // Optimistic update
739
+ if (optimistic) {
740
+ const pending = pendingChanges.current.get(prop as keyof TState)
741
+ if (pending && !pending.synced) return pending.value
742
+ }
743
+ return stateData[prop as keyof TState]
744
+ }
745
+
746
+ // Se NÃO é propriedade do state → é uma action!
747
+ // Retorna uma função que chama a action no servidor
748
+ return async (payload?: any) => {
749
+ const id = componentId || lastComponentIdRef.current
750
+ if (!id || !connected) throw new Error('Not connected')
751
+
752
+ const response = await sendMessageAndWait({
753
+ type: 'CALL_ACTION',
754
+ componentId: id,
755
+ action: prop,
756
+ payload
757
+ }, 10000)
758
+
759
+ if (!response.success) throw new Error(response.error || 'Action failed')
760
+ return response.result
761
+ }
762
+ },
763
+
764
+ set(_, prop: string | symbol, value) {
765
+ if (typeof prop === 'symbol' || RESERVED_PROPS.has(prop as string)) return false
766
+ setProperty(prop as keyof TState, value)
767
+ return true
768
+ },
769
+
770
+ has(_, prop) {
771
+ if (typeof prop === 'symbol') return false
772
+ return RESERVED_PROPS.has(prop) || prop in stateData
773
+ },
774
+
775
+ ownKeys() {
776
+ return [...Object.keys(stateData), '$state', '$connected', '$loading', '$error', '$status', '$componentId', '$dirty', '$call', '$callAndWait', '$mount', '$unmount', '$refresh', '$set', '$field', '$sync', '$onBroadcast', '$updateLocal', '$room', '$rooms']
777
+ }
778
+ })
779
+ }, [stateData, connected, loading, error, componentId, call, callAndWait, mount, unmount, refresh, setProperty, optimistic, sendMessageAndWait, createFieldBinding, sync, localVersion, roomManager])
780
+
781
+ return proxy
782
+ }
783
+
784
+ // ===== Factory =====
785
+
786
+ export function createLiveComponent<
787
+ TState extends Record<string, any>,
788
+ TActions = {},
789
+ TBroadcasts extends Record<string, any> = Record<string, any>
790
+ >(
791
+ componentName: string,
792
+ defaultOptions: Omit<UseLiveComponentOptions, keyof HybridComponentOptions> = {}
793
+ ) {
794
+ return function useComponent(
795
+ initialState: TState,
796
+ options: UseLiveComponentOptions = {}
797
+ ): LiveProxyWithBroadcasts<TState, TActions, TBroadcasts> {
798
+ return useLiveComponent<TState, TActions, TBroadcasts>(componentName, initialState, { ...defaultOptions, ...options })
799
+ }
800
+ }