create-fluxstack 1.9.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 (259) hide show
  1. package/.dockerignore +1 -2
  2. package/Dockerfile +8 -8
  3. package/LIVE_COMPONENTS_REVIEW.md +781 -0
  4. package/LLMD/INDEX.md +64 -0
  5. package/LLMD/MAINTENANCE.md +197 -0
  6. package/LLMD/MIGRATION.md +156 -0
  7. package/LLMD/config/.gitkeep +1 -0
  8. package/LLMD/config/declarative-system.md +268 -0
  9. package/LLMD/config/environment-vars.md +327 -0
  10. package/LLMD/config/runtime-reload.md +401 -0
  11. package/LLMD/core/.gitkeep +1 -0
  12. package/LLMD/core/build-system.md +599 -0
  13. package/LLMD/core/framework-lifecycle.md +229 -0
  14. package/LLMD/core/plugin-system.md +451 -0
  15. package/LLMD/patterns/.gitkeep +1 -0
  16. package/LLMD/patterns/anti-patterns.md +297 -0
  17. package/LLMD/patterns/project-structure.md +264 -0
  18. package/LLMD/patterns/type-safety.md +440 -0
  19. package/LLMD/reference/.gitkeep +1 -0
  20. package/LLMD/reference/cli-commands.md +250 -0
  21. package/LLMD/reference/plugin-hooks.md +357 -0
  22. package/LLMD/reference/routing.md +39 -0
  23. package/LLMD/reference/troubleshooting.md +364 -0
  24. package/LLMD/resources/.gitkeep +1 -0
  25. package/LLMD/resources/controllers.md +465 -0
  26. package/LLMD/resources/live-components.md +703 -0
  27. package/LLMD/resources/live-rooms.md +482 -0
  28. package/LLMD/resources/live-upload.md +130 -0
  29. package/LLMD/resources/plugins-external.md +617 -0
  30. package/LLMD/resources/routes-eden.md +254 -0
  31. package/README.md +37 -17
  32. package/app/client/index.html +0 -1
  33. package/app/client/src/App.tsx +109 -156
  34. package/app/client/src/components/AppLayout.tsx +68 -0
  35. package/app/client/src/components/BackButton.tsx +13 -0
  36. package/app/client/src/components/DemoPage.tsx +20 -0
  37. package/app/client/src/components/LiveUploadWidget.tsx +204 -0
  38. package/app/client/src/lib/eden-api.ts +85 -65
  39. package/app/client/src/live/ChatDemo.tsx +107 -0
  40. package/app/client/src/live/CounterDemo.tsx +206 -0
  41. package/app/client/src/live/FormDemo.tsx +119 -0
  42. package/app/client/src/live/RoomChatDemo.tsx +242 -0
  43. package/app/client/src/live/UploadDemo.tsx +21 -0
  44. package/app/client/src/main.tsx +13 -10
  45. package/app/client/src/pages/ApiTestPage.tsx +108 -0
  46. package/app/client/src/pages/HomePage.tsx +76 -0
  47. package/app/client/src/vite-env.d.ts +1 -1
  48. package/app/server/app.ts +1 -4
  49. package/app/server/controllers/users.controller.ts +36 -44
  50. package/app/server/index.ts +24 -107
  51. package/app/server/live/LiveChat.ts +77 -0
  52. package/app/server/live/LiveCounter.ts +67 -0
  53. package/app/server/live/LiveForm.ts +63 -0
  54. package/app/server/live/LiveLocalCounter.ts +32 -0
  55. package/app/server/live/LiveRoomChat.ts +285 -0
  56. package/app/server/live/LiveUpload.ts +81 -0
  57. package/app/server/live/register-components.ts +19 -19
  58. package/app/server/routes/index.ts +3 -1
  59. package/app/server/routes/room.routes.ts +117 -0
  60. package/app/server/routes/users.routes.ts +35 -27
  61. package/app/shared/types/index.ts +14 -2
  62. package/config/app.config.ts +2 -62
  63. package/config/client.config.ts +2 -95
  64. package/config/database.config.ts +2 -99
  65. package/config/fluxstack.config.ts +25 -45
  66. package/config/index.ts +57 -38
  67. package/config/monitoring.config.ts +2 -114
  68. package/config/plugins.config.ts +2 -80
  69. package/config/server.config.ts +2 -68
  70. package/config/services.config.ts +2 -130
  71. package/config/system/app.config.ts +29 -0
  72. package/config/system/build.config.ts +49 -0
  73. package/config/system/client.config.ts +68 -0
  74. package/config/system/database.config.ts +17 -0
  75. package/config/system/fluxstack.config.ts +114 -0
  76. package/config/{logger.config.ts → system/logger.config.ts} +3 -1
  77. package/config/system/monitoring.config.ts +114 -0
  78. package/config/system/plugins.config.ts +84 -0
  79. package/config/{runtime.config.ts → system/runtime.config.ts} +1 -1
  80. package/config/system/server.config.ts +68 -0
  81. package/config/system/services.config.ts +46 -0
  82. package/config/{system.config.ts → system/system.config.ts} +1 -1
  83. package/core/build/bundler.ts +4 -1
  84. package/core/build/flux-plugins-generator.ts +325 -325
  85. package/core/build/index.ts +159 -27
  86. package/core/build/live-components-generator.ts +70 -3
  87. package/core/build/optimizer.ts +235 -235
  88. package/core/cli/command-registry.ts +6 -4
  89. package/core/cli/commands/build.ts +79 -0
  90. package/core/cli/commands/create.ts +54 -0
  91. package/core/cli/commands/dev.ts +101 -0
  92. package/core/cli/commands/help.ts +34 -0
  93. package/core/cli/commands/index.ts +34 -0
  94. package/core/cli/commands/make-plugin.ts +90 -0
  95. package/core/cli/commands/plugin-add.ts +197 -0
  96. package/core/cli/commands/plugin-deps.ts +2 -2
  97. package/core/cli/commands/plugin-list.ts +208 -0
  98. package/core/cli/commands/plugin-remove.ts +170 -0
  99. package/core/cli/generators/component.ts +769 -769
  100. package/core/cli/generators/controller.ts +1 -1
  101. package/core/cli/generators/index.ts +146 -146
  102. package/core/cli/generators/interactive.ts +227 -227
  103. package/core/cli/generators/plugin.ts +2 -2
  104. package/core/cli/generators/prompts.ts +82 -82
  105. package/core/cli/generators/route.ts +6 -6
  106. package/core/cli/generators/service.ts +2 -2
  107. package/core/cli/generators/template-engine.ts +4 -3
  108. package/core/cli/generators/types.ts +2 -2
  109. package/core/cli/generators/utils.ts +191 -191
  110. package/core/cli/index.ts +115 -558
  111. package/core/cli/plugin-discovery.ts +2 -2
  112. package/core/client/LiveComponentsProvider.tsx +63 -17
  113. package/core/client/api/eden.ts +183 -0
  114. package/core/client/api/index.ts +11 -0
  115. package/core/client/components/Live.tsx +104 -0
  116. package/core/client/fluxstack.ts +1 -9
  117. package/core/client/hooks/AdaptiveChunkSizer.ts +215 -0
  118. package/core/client/hooks/state-validator.ts +1 -1
  119. package/core/client/hooks/useAuth.ts +48 -48
  120. package/core/client/hooks/useChunkedUpload.ts +170 -69
  121. package/core/client/hooks/useLiveChunkedUpload.ts +87 -0
  122. package/core/client/hooks/useLiveComponent.ts +800 -0
  123. package/core/client/hooks/useLiveUpload.ts +71 -0
  124. package/core/client/hooks/useRoom.ts +409 -0
  125. package/core/client/hooks/useRoomProxy.ts +382 -0
  126. package/core/client/index.ts +18 -51
  127. package/core/client/standalone-entry.ts +8 -0
  128. package/core/client/standalone.ts +74 -53
  129. package/core/client/state/createStore.ts +192 -192
  130. package/core/client/state/index.ts +14 -14
  131. package/core/config/index.ts +70 -291
  132. package/core/config/schema.ts +42 -723
  133. package/core/framework/client.ts +131 -131
  134. package/core/framework/index.ts +7 -7
  135. package/core/framework/server.ts +227 -47
  136. package/core/framework/types.ts +2 -2
  137. package/core/index.ts +23 -4
  138. package/core/live/ComponentRegistry.ts +7 -3
  139. package/core/live/types.ts +77 -0
  140. package/core/plugins/built-in/index.ts +134 -131
  141. package/core/plugins/built-in/live-components/commands/create-live-component.ts +242 -1074
  142. package/core/plugins/built-in/live-components/index.ts +1 -1
  143. package/core/plugins/built-in/monitoring/index.ts +111 -47
  144. package/core/plugins/built-in/static/index.ts +1 -1
  145. package/core/plugins/built-in/swagger/index.ts +68 -265
  146. package/core/plugins/built-in/vite/index.ts +94 -306
  147. package/core/plugins/built-in/vite/vite-dev.ts +82 -0
  148. package/core/plugins/config.ts +9 -7
  149. package/core/plugins/dependency-manager.ts +31 -1
  150. package/core/plugins/discovery.ts +19 -7
  151. package/core/plugins/executor.ts +2 -2
  152. package/core/plugins/index.ts +203 -203
  153. package/core/plugins/manager.ts +27 -39
  154. package/core/plugins/module-resolver.ts +19 -8
  155. package/core/plugins/registry.ts +309 -21
  156. package/core/plugins/types.ts +106 -55
  157. package/core/server/framework.ts +66 -43
  158. package/core/server/index.ts +15 -16
  159. package/core/server/live/ComponentRegistry.ts +91 -75
  160. package/core/server/live/FileUploadManager.ts +41 -31
  161. package/core/server/live/LiveComponentPerformanceMonitor.ts +1 -1
  162. package/core/server/live/LiveRoomManager.ts +261 -0
  163. package/core/server/live/RoomEventBus.ts +234 -0
  164. package/core/server/live/RoomStateManager.ts +172 -0
  165. package/core/server/live/StateSignature.ts +643 -643
  166. package/core/server/live/WebSocketConnectionManager.ts +30 -19
  167. package/core/server/live/auto-generated-components.ts +41 -26
  168. package/core/server/live/index.ts +14 -0
  169. package/core/server/live/websocket-plugin.ts +233 -72
  170. package/core/server/middleware/elysia-helpers.ts +7 -2
  171. package/core/server/middleware/errorHandling.ts +1 -1
  172. package/core/server/middleware/index.ts +31 -31
  173. package/core/server/plugins/database.ts +180 -180
  174. package/core/server/plugins/static-files-plugin.ts +69 -260
  175. package/core/server/plugins/swagger.ts +33 -33
  176. package/core/server/rooms/RoomBroadcaster.ts +357 -0
  177. package/core/server/rooms/RoomSystem.ts +463 -0
  178. package/core/server/rooms/index.ts +13 -0
  179. package/core/server/services/BaseService.ts +1 -1
  180. package/core/server/services/ServiceContainer.ts +1 -1
  181. package/core/server/services/index.ts +8 -8
  182. package/core/templates/create-project.ts +12 -12
  183. package/core/testing/index.ts +9 -9
  184. package/core/testing/setup.ts +73 -73
  185. package/core/types/api.ts +168 -168
  186. package/core/types/build.ts +219 -218
  187. package/core/types/config.ts +56 -26
  188. package/core/types/index.ts +4 -4
  189. package/core/types/plugin.ts +107 -99
  190. package/core/types/types.ts +490 -14
  191. package/core/utils/build-logger.ts +324 -324
  192. package/core/utils/config-schema.ts +480 -480
  193. package/core/utils/env.ts +2 -8
  194. package/core/utils/errors/codes.ts +114 -114
  195. package/core/utils/errors/handlers.ts +36 -1
  196. package/core/utils/errors/index.ts +49 -5
  197. package/core/utils/errors/middleware.ts +113 -113
  198. package/core/utils/helpers.ts +6 -16
  199. package/core/utils/index.ts +17 -17
  200. package/core/utils/logger/colors.ts +114 -114
  201. package/core/utils/logger/config.ts +13 -9
  202. package/core/utils/logger/formatter.ts +82 -82
  203. package/core/utils/logger/group-logger.ts +101 -101
  204. package/core/utils/logger/index.ts +6 -1
  205. package/core/utils/logger/stack-trace.ts +3 -1
  206. package/core/utils/logger/startup-banner.ts +82 -66
  207. package/core/utils/logger/winston-logger.ts +152 -152
  208. package/core/utils/monitoring/index.ts +211 -211
  209. package/core/utils/sync-version.ts +66 -66
  210. package/core/utils/version.ts +1 -1
  211. package/create-fluxstack.ts +8 -7
  212. package/eslint.config.js +23 -23
  213. package/package.json +14 -15
  214. package/plugins/crypto-auth/cli/make-protected-route.command.ts +1 -1
  215. package/plugins/crypto-auth/client/CryptoAuthClient.ts +302 -302
  216. package/plugins/crypto-auth/client/components/index.ts +11 -11
  217. package/plugins/crypto-auth/client/index.ts +11 -11
  218. package/plugins/crypto-auth/config/index.ts +1 -1
  219. package/plugins/crypto-auth/index.ts +4 -4
  220. package/plugins/crypto-auth/package.json +65 -65
  221. package/plugins/crypto-auth/server/AuthMiddleware.ts +1 -1
  222. package/plugins/crypto-auth/server/CryptoAuthService.ts +185 -185
  223. package/plugins/crypto-auth/server/index.ts +21 -21
  224. package/plugins/crypto-auth/server/middlewares/cryptoAuthAdmin.ts +3 -3
  225. package/plugins/crypto-auth/server/middlewares/cryptoAuthOptional.ts +1 -1
  226. package/plugins/crypto-auth/server/middlewares/cryptoAuthPermissions.ts +2 -2
  227. package/plugins/crypto-auth/server/middlewares/cryptoAuthRequired.ts +2 -2
  228. package/plugins/crypto-auth/server/middlewares/helpers.ts +1 -1
  229. package/plugins/crypto-auth/server/middlewares/index.ts +22 -22
  230. package/plugins/crypto-auth/server/middlewares.ts +19 -19
  231. package/tsconfig.api-strict.json +16 -0
  232. package/tsconfig.json +10 -14
  233. package/{app/client/tsconfig.node.json → tsconfig.node.json} +1 -1
  234. package/types/global.d.ts +29 -29
  235. package/types/vitest.d.ts +8 -8
  236. package/vite.config.ts +38 -62
  237. package/vitest.config.live.ts +10 -9
  238. package/vitest.config.ts +29 -17
  239. package/workspace.json +5 -5
  240. package/app/client/README.md +0 -69
  241. package/app/client/SIMPLIFICATION.md +0 -140
  242. package/app/client/frontend-only.ts +0 -12
  243. package/app/client/tsconfig.app.json +0 -44
  244. package/app/client/tsconfig.json +0 -7
  245. package/app/client/zustand-setup.md +0 -65
  246. package/app/server/backend-only.ts +0 -18
  247. package/app/server/live/LiveClockComponent.ts +0 -215
  248. package/app/server/routes/env-test.ts +0 -110
  249. package/core/client/hooks/index.ts +0 -7
  250. package/core/client/hooks/useHybridLiveComponent.ts +0 -631
  251. package/core/client/hooks/useWebSocket.ts +0 -373
  252. package/core/config/env.ts +0 -546
  253. package/core/config/loader.ts +0 -522
  254. package/core/config/runtime-config.ts +0 -327
  255. package/core/config/validator.ts +0 -540
  256. package/core/server/backend-entry.ts +0 -51
  257. package/core/server/standalone.ts +0 -106
  258. package/core/utils/regenerate-files.ts +0 -69
  259. 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
+ }