create-fluxstack 1.8.3 → 1.10.1

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 (52) hide show
  1. package/LIVE_COMPONENTS_REVIEW.md +781 -0
  2. package/README.md +653 -275
  3. package/app/client/src/App.tsx +39 -43
  4. package/app/client/src/lib/eden-api.ts +2 -7
  5. package/app/client/src/live/FileUploadExample.tsx +359 -0
  6. package/app/client/src/live/MinimalLiveClock.tsx +47 -0
  7. package/app/client/src/live/QuickUploadTest.tsx +193 -0
  8. package/app/client/src/main.tsx +10 -10
  9. package/app/client/src/vite-env.d.ts +1 -1
  10. package/app/client/tsconfig.app.json +45 -44
  11. package/app/client/tsconfig.node.json +25 -25
  12. package/app/server/index.ts +30 -103
  13. package/app/server/live/LiveFileUploadComponent.ts +77 -0
  14. package/app/server/live/register-components.ts +19 -19
  15. package/core/build/bundler.ts +202 -55
  16. package/core/build/index.ts +126 -2
  17. package/core/build/live-components-generator.ts +68 -1
  18. package/core/cli/generators/plugin.ts +6 -6
  19. package/core/cli/index.ts +232 -4
  20. package/core/client/LiveComponentsProvider.tsx +3 -9
  21. package/core/client/hooks/AdaptiveChunkSizer.ts +215 -0
  22. package/core/client/hooks/useChunkedUpload.ts +112 -61
  23. package/core/client/hooks/useHybridLiveComponent.ts +80 -26
  24. package/core/client/hooks/useTypedLiveComponent.ts +133 -0
  25. package/core/client/hooks/useWebSocket.ts +4 -16
  26. package/core/client/index.ts +20 -2
  27. package/core/framework/server.ts +181 -8
  28. package/core/live/ComponentRegistry.ts +5 -1
  29. package/core/plugins/built-in/index.ts +8 -5
  30. package/core/plugins/built-in/live-components/commands/create-live-component.ts +55 -63
  31. package/core/plugins/built-in/vite/index.ts +75 -187
  32. package/core/plugins/built-in/vite/vite-dev.ts +88 -0
  33. package/core/plugins/registry.ts +54 -2
  34. package/core/plugins/types.ts +86 -2
  35. package/core/server/index.ts +1 -2
  36. package/core/server/live/ComponentRegistry.ts +14 -5
  37. package/core/server/live/FileUploadManager.ts +22 -25
  38. package/core/server/live/auto-generated-components.ts +29 -26
  39. package/core/server/live/websocket-plugin.ts +19 -5
  40. package/core/server/plugins/static-files-plugin.ts +49 -240
  41. package/core/server/plugins/swagger.ts +33 -33
  42. package/core/types/build.ts +22 -0
  43. package/core/types/plugin.ts +9 -1
  44. package/core/types/types.ts +137 -0
  45. package/core/utils/logger/startup-banner.ts +20 -4
  46. package/core/utils/version.ts +6 -6
  47. package/create-fluxstack.ts +7 -7
  48. package/eslint.config.js +23 -23
  49. package/package.json +3 -2
  50. package/plugins/crypto-auth/server/middlewares.ts +19 -19
  51. package/tsconfig.json +52 -51
  52. package/workspace.json +5 -5
@@ -1,20 +1,24 @@
1
1
  import { useState, useCallback, useRef } from 'react'
2
- import type {
3
- FileUploadStartMessage,
4
- FileUploadChunkMessage,
2
+ import { AdaptiveChunkSizer, type AdaptiveChunkConfig } from './AdaptiveChunkSizer'
3
+ import type {
4
+ FileUploadStartMessage,
5
+ FileUploadChunkMessage,
5
6
  FileUploadCompleteMessage,
6
7
  FileUploadProgressResponse,
7
8
  FileUploadCompleteResponse
8
9
  } from '@/core/types/types'
9
10
 
10
11
  export interface ChunkedUploadOptions {
11
- chunkSize?: number // Default 64KB
12
+ chunkSize?: number // Default 64KB (used as initial if adaptive is enabled)
12
13
  maxFileSize?: number // Default 50MB
13
14
  allowedTypes?: string[]
14
15
  sendMessageAndWait?: (message: any, timeout?: number) => Promise<any> // WebSocket send function
15
16
  onProgress?: (progress: number, bytesUploaded: number, totalBytes: number) => void
16
17
  onComplete?: (response: FileUploadCompleteResponse) => void
17
18
  onError?: (error: string) => void
19
+ // Adaptive chunking options
20
+ adaptiveChunking?: boolean // Enable adaptive chunk sizing (default: false)
21
+ adaptiveConfig?: Partial<AdaptiveChunkConfig> // Adaptive chunking configuration
18
22
  }
19
23
 
20
24
  export interface ChunkedUploadState {
@@ -27,7 +31,7 @@ export interface ChunkedUploadState {
27
31
  }
28
32
 
29
33
  export function useChunkedUpload(componentId: string, options: ChunkedUploadOptions = {}) {
30
-
34
+
31
35
  const [state, setState] = useState<ChunkedUploadState>({
32
36
  uploading: false,
33
37
  progress: 0,
@@ -44,42 +48,23 @@ export function useChunkedUpload(componentId: string, options: ChunkedUploadOpti
44
48
  sendMessageAndWait,
45
49
  onProgress,
46
50
  onComplete,
47
- onError
51
+ onError,
52
+ adaptiveChunking = false,
53
+ adaptiveConfig
48
54
  } = options
49
55
 
50
56
  const abortControllerRef = useRef<AbortController | null>(null)
57
+ const adaptiveSizerRef = useRef<AdaptiveChunkSizer | null>(null)
51
58
 
52
- // Convert file to base64 chunks
53
- const fileToChunks = useCallback(async (file: File): Promise<string[]> => {
54
- return new Promise((resolve, reject) => {
55
- const reader = new FileReader()
56
-
57
- reader.onload = () => {
58
- const arrayBuffer = reader.result as ArrayBuffer
59
- const uint8Array = new Uint8Array(arrayBuffer)
60
-
61
- // Split binary data into chunks first, then convert each chunk to base64
62
- const chunks: string[] = []
63
- for (let i = 0; i < uint8Array.length; i += chunkSize) {
64
- const chunkEnd = Math.min(i + chunkSize, uint8Array.length)
65
- const chunkBytes = uint8Array.slice(i, chunkEnd)
66
-
67
- // Convert chunk to base64
68
- let binary = ''
69
- for (let j = 0; j < chunkBytes.length; j++) {
70
- binary += String.fromCharCode(chunkBytes[j])
71
- }
72
- const base64Chunk = btoa(binary)
73
- chunks.push(base64Chunk)
74
- }
75
-
76
- resolve(chunks)
77
- }
78
-
79
- reader.onerror = () => reject(new Error('Failed to read file'))
80
- reader.readAsArrayBuffer(file)
59
+ // Initialize adaptive chunk sizer if enabled
60
+ if (adaptiveChunking && !adaptiveSizerRef.current) {
61
+ adaptiveSizerRef.current = new AdaptiveChunkSizer({
62
+ initialChunkSize: chunkSize,
63
+ minChunkSize: 16 * 1024, // 16KB min
64
+ maxChunkSize: 1024 * 1024, // 1MB max
65
+ ...adaptiveConfig
81
66
  })
82
- }, [chunkSize])
67
+ }
83
68
 
84
69
  // Start chunked upload
85
70
  const uploadFile = useCallback(async (file: File) => {
@@ -90,8 +75,8 @@ export function useChunkedUpload(componentId: string, options: ChunkedUploadOpti
90
75
  return
91
76
  }
92
77
 
93
- // Validate file
94
- if (!allowedTypes.includes(file.type)) {
78
+ // Validate file type (skip if allowedTypes is empty = accept all)
79
+ if (allowedTypes.length > 0 && !allowedTypes.includes(file.type)) {
95
80
  const error = `Invalid file type: ${file.type}. Allowed: ${allowedTypes.join(', ')}`
96
81
  setState(prev => ({ ...prev, error }))
97
82
  onError?.(error)
@@ -120,13 +105,22 @@ export function useChunkedUpload(componentId: string, options: ChunkedUploadOpti
120
105
  totalBytes: file.size
121
106
  })
122
107
 
123
- console.log('🚀 Starting chunked upload:', { uploadId, filename: file.name, size: file.size })
108
+ console.log('🚀 Starting chunked upload:', {
109
+ uploadId,
110
+ filename: file.name,
111
+ size: file.size,
112
+ adaptiveChunking
113
+ })
124
114
 
125
- // Convert file to chunks
126
- const chunks = await fileToChunks(file)
127
- const totalChunks = chunks.length
115
+ // Reset adaptive sizer for new upload
116
+ if (adaptiveSizerRef.current) {
117
+ adaptiveSizerRef.current.reset()
118
+ }
119
+
120
+ // Get initial chunk size (adaptive or fixed)
121
+ const initialChunkSize = adaptiveSizerRef.current?.getChunkSize() ?? chunkSize
128
122
 
129
- console.log(`📦 File split into ${totalChunks} chunks of ~${chunkSize} bytes each`)
123
+ console.log(`📦 Initial chunk size: ${initialChunkSize} bytes${adaptiveChunking ? ' (adaptive)' : ''}`)
130
124
 
131
125
  // Start upload
132
126
  const startMessage: FileUploadStartMessage = {
@@ -147,35 +141,92 @@ export function useChunkedUpload(componentId: string, options: ChunkedUploadOpti
147
141
 
148
142
  console.log('✅ Upload started successfully')
149
143
 
150
- // Send chunks sequentially
151
- for (let i = 0; i < chunks.length; i++) {
144
+ // Read file as ArrayBuffer for dynamic chunking
145
+ const fileArrayBuffer = await file.arrayBuffer()
146
+ const fileData = new Uint8Array(fileArrayBuffer)
147
+
148
+ let offset = 0
149
+ let chunkIndex = 0
150
+ const estimatedTotalChunks = Math.ceil(file.size / initialChunkSize)
151
+
152
+ // Send chunks dynamically with adaptive sizing
153
+ while (offset < fileData.length) {
152
154
  if (abortControllerRef.current?.signal.aborted) {
153
155
  throw new Error('Upload cancelled')
154
156
  }
155
157
 
158
+ // Get current chunk size (adaptive or fixed)
159
+ const currentChunkSize = adaptiveSizerRef.current?.getChunkSize() ?? chunkSize
160
+ const chunkEnd = Math.min(offset + currentChunkSize, fileData.length)
161
+ const chunkBytes = fileData.slice(offset, chunkEnd)
162
+
163
+ // Convert chunk to base64
164
+ let binary = ''
165
+ for (let j = 0; j < chunkBytes.length; j++) {
166
+ binary += String.fromCharCode(chunkBytes[j])
167
+ }
168
+ const base64Chunk = btoa(binary)
169
+
170
+ // Record chunk start time for adaptive sizing
171
+ const chunkStartTime = adaptiveSizerRef.current?.recordChunkStart(chunkIndex) ?? 0
172
+
156
173
  const chunkMessage: FileUploadChunkMessage = {
157
174
  type: 'FILE_UPLOAD_CHUNK',
158
175
  componentId,
159
176
  uploadId,
160
- chunkIndex: i,
161
- totalChunks,
162
- data: chunks[i],
163
- requestId: `chunk-${uploadId}-${i}`
177
+ chunkIndex,
178
+ totalChunks: estimatedTotalChunks, // Approximate, will be recalculated
179
+ data: base64Chunk,
180
+ requestId: `chunk-${uploadId}-${chunkIndex}`
164
181
  }
165
182
 
166
- console.log(`📤 Sending chunk ${i + 1}/${totalChunks}`)
183
+ console.log(`📤 Sending chunk ${chunkIndex + 1} (size: ${chunkBytes.length} bytes)`)
167
184
 
168
- // Send chunk and wait for progress response
169
- const progressResponse = await sendMessageAndWait(chunkMessage, 10000) as FileUploadProgressResponse
170
-
171
- if (progressResponse) {
172
- const { progress, bytesUploaded } = progressResponse
173
- setState(prev => ({ ...prev, progress, bytesUploaded }))
174
- onProgress?.(progress, bytesUploaded, file.size)
185
+ try {
186
+ // Send chunk and wait for progress response
187
+ const progressResponse = await sendMessageAndWait(chunkMessage, 10000) as FileUploadProgressResponse
188
+
189
+ if (progressResponse) {
190
+ const { progress, bytesUploaded } = progressResponse
191
+ setState(prev => ({ ...prev, progress, bytesUploaded }))
192
+ onProgress?.(progress, bytesUploaded, file.size)
193
+ }
194
+
195
+ // Record successful chunk upload for adaptive sizing
196
+ if (adaptiveSizerRef.current) {
197
+ adaptiveSizerRef.current.recordChunkComplete(
198
+ chunkIndex,
199
+ chunkBytes.length,
200
+ chunkStartTime,
201
+ true
202
+ )
203
+ }
204
+ } catch (error) {
205
+ // Record failed chunk for adaptive sizing
206
+ if (adaptiveSizerRef.current) {
207
+ adaptiveSizerRef.current.recordChunkComplete(
208
+ chunkIndex,
209
+ chunkBytes.length,
210
+ chunkStartTime,
211
+ false
212
+ )
213
+ }
214
+ throw error
175
215
  }
176
216
 
177
- // Small delay to prevent overwhelming the server
178
- await new Promise(resolve => setTimeout(resolve, 10))
217
+ offset += chunkBytes.length
218
+ chunkIndex++
219
+
220
+ // Small delay to prevent overwhelming the server (only for fixed chunking)
221
+ if (!adaptiveChunking) {
222
+ await new Promise(resolve => setTimeout(resolve, 10))
223
+ }
224
+ }
225
+
226
+ // Log final adaptive stats
227
+ if (adaptiveSizerRef.current) {
228
+ const stats = adaptiveSizerRef.current.getStats()
229
+ console.log('📊 Final Adaptive Chunking Stats:', stats)
179
230
  }
180
231
 
181
232
  // Complete upload
@@ -219,10 +270,10 @@ export function useChunkedUpload(componentId: string, options: ChunkedUploadOpti
219
270
  maxFileSize,
220
271
  chunkSize,
221
272
  sendMessageAndWait,
222
- fileToChunks,
223
273
  onProgress,
224
274
  onComplete,
225
- onError
275
+ onError,
276
+ adaptiveChunking
226
277
  ])
227
278
 
228
279
  // Cancel upload
@@ -165,7 +165,13 @@ export function useHybridLiveComponent<T = any>(
165
165
  room,
166
166
  userId,
167
167
  autoMount = true,
168
- debug = false
168
+ debug = false,
169
+ onConnect,
170
+ onMount,
171
+ onDisconnect,
172
+ onRehydrate,
173
+ onError,
174
+ onStateChange
169
175
  } = options
170
176
 
171
177
  // Use Live Components context (singleton WebSocket connection)
@@ -226,9 +232,13 @@ export function useHybridLiveComponent<T = any>(
226
232
  case 'STATE_UPDATE':
227
233
  if (message.payload?.state) {
228
234
  const newState = message.payload.state
235
+ const oldState = stateData
229
236
  updateState(newState, 'server')
230
237
  setLastServerState(newState)
231
238
 
239
+ // Call onStateChange callback
240
+ onStateChange?.(newState, oldState)
241
+
232
242
  if (message.payload?.signedState) {
233
243
  setCurrentSignedState(message.payload.signedState)
234
244
  persistComponentState(componentName, message.payload.signedState, room, userId)
@@ -255,6 +265,9 @@ export function useHybridLiveComponent<T = any>(
255
265
 
256
266
  setRehydrating(false)
257
267
  setError(null)
268
+
269
+ // Call onRehydrate callback
270
+ onRehydrate?.()
258
271
  }
259
272
  break
260
273
 
@@ -266,10 +279,17 @@ export function useHybridLiveComponent<T = any>(
266
279
  lastKnownComponentIdRef.current = message.result.newComponentId
267
280
  setRehydrating(false)
268
281
  setError(null)
282
+
283
+ // Call onRehydrate callback
284
+ onRehydrate?.()
269
285
  } else if (!message.success) {
270
286
  log('❌ Re-hydration failed', message.error)
271
287
  setRehydrating(false)
272
- setError(message.error || 'Re-hydration failed')
288
+ const errorMessage = message.error || 'Re-hydration failed'
289
+ setError(errorMessage)
290
+
291
+ // Call onError callback
292
+ onError?.(errorMessage)
273
293
  }
274
294
  break
275
295
 
@@ -283,14 +303,16 @@ export function useHybridLiveComponent<T = any>(
283
303
  break
284
304
 
285
305
  case 'ERROR':
286
- const errorMessage = message.payload?.error || 'Unknown error'
287
- if (errorMessage.includes('COMPONENT_REHYDRATION_REQUIRED')) {
306
+ const errorMsg = message.payload?.error || 'Unknown error'
307
+ if (errorMsg.includes('COMPONENT_REHYDRATION_REQUIRED')) {
288
308
  log('🔄 Component re-hydration required from ERROR')
289
309
  if (!rehydrating) {
290
310
  attemptRehydration()
291
311
  }
292
312
  } else {
293
- setError(errorMessage)
313
+ setError(errorMsg)
314
+ // Call onError callback
315
+ onError?.(errorMsg)
294
316
  }
295
317
  break
296
318
 
@@ -305,7 +327,7 @@ export function useHybridLiveComponent<T = any>(
305
327
  log('🗑️ Unregistering component from WebSocket context')
306
328
  unregister()
307
329
  }
308
- }, [componentId, registerComponent, unregisterComponent, log, updateState, componentName, room, userId, rehydrating])
330
+ }, [componentId, registerComponent, unregisterComponent, log, updateState, componentName, room, userId, rehydrating, stateData, onStateChange, onRehydrate, onError])
309
331
 
310
332
  // Automatic re-hydration on reconnection
311
333
  const attemptRehydration = useCallback(async () => {
@@ -361,10 +383,20 @@ export function useHybridLiveComponent<T = any>(
361
383
  if (response?.success && response?.result?.newComponentId) {
362
384
  setComponentId(response.result.newComponentId)
363
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
+
364
394
  return true
365
395
  } else {
366
396
  clearPersistedState(componentName)
367
- setError(response?.error || 'Re-hydration failed')
397
+ const errorMsg = response?.error || 'Re-hydration failed'
398
+ setError(errorMsg)
399
+ onError?.(errorMsg)
368
400
  return false
369
401
  }
370
402
 
@@ -383,7 +415,7 @@ export function useHybridLiveComponent<T = any>(
383
415
  globalRehydrationAttempts.set(componentName, rehydrationPromise)
384
416
 
385
417
  return await rehydrationPromise
386
- }, [connected, rehydrating, componentName, contextSendMessageAndWait, log])
418
+ }, [connected, rehydrating, componentName, contextSendMessageAndWait, log, onRehydrate, onError])
387
419
 
388
420
  // Mount component
389
421
  const mount = useCallback(async () => {
@@ -412,6 +444,7 @@ export function useHybridLiveComponent<T = any>(
412
444
  if (response?.success && response?.result?.componentId) {
413
445
  const newComponentId = response.result.componentId
414
446
  setComponentId(newComponentId)
447
+ lastKnownComponentIdRef.current = newComponentId
415
448
  mountedRef.current = true
416
449
 
417
450
  if (response.result.signedState) {
@@ -425,6 +458,12 @@ export function useHybridLiveComponent<T = any>(
425
458
  }
426
459
 
427
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)
428
467
  } else {
429
468
  throw new Error(response?.error || 'No component ID returned from server')
430
469
  }
@@ -433,6 +472,9 @@ export function useHybridLiveComponent<T = any>(
433
472
  setError(errorMessage)
434
473
  log('❌ Mount failed', err)
435
474
 
475
+ // Call onError callback
476
+ onError?.(errorMessage)
477
+
436
478
  if (!fallbackToLocal) {
437
479
  throw err
438
480
  }
@@ -440,7 +482,7 @@ export function useHybridLiveComponent<T = any>(
440
482
  setMountLoading(false)
441
483
  mountingRef.current = false
442
484
  }
443
- }, [connected, componentName, initialState, room, userId, contextSendMessageAndWait, log, fallbackToLocal, updateState])
485
+ }, [connected, componentName, initialState, room, userId, contextSendMessageAndWait, log, fallbackToLocal, updateState, onMount, onError])
444
486
 
445
487
  // Unmount component
446
488
  const unmount = useCallback(async () => {
@@ -465,14 +507,16 @@ export function useHybridLiveComponent<T = any>(
465
507
 
466
508
  // Server-only actions
467
509
  const call = useCallback(async (action: string, payload?: any): Promise<void> => {
468
- if (!componentId || !connected) {
510
+ // Use ref as fallback for componentId (handles timing issues after rehydration)
511
+ const currentComponentId = componentId || lastKnownComponentIdRef.current
512
+ if (!currentComponentId || !connected) {
469
513
  throw new Error('Component not mounted or WebSocket not connected')
470
514
  }
471
515
 
472
516
  try {
473
517
  const message: WebSocketMessage = {
474
518
  type: 'CALL_ACTION',
475
- componentId,
519
+ componentId: currentComponentId,
476
520
  action,
477
521
  payload
478
522
  }
@@ -482,10 +526,11 @@ export function useHybridLiveComponent<T = any>(
482
526
  if (!response.success && response.error?.includes?.('COMPONENT_REHYDRATION_REQUIRED')) {
483
527
  const rehydrated = await attemptRehydration()
484
528
  if (rehydrated) {
485
- // Retry action
529
+ // Use updated ref for retry
530
+ const retryComponentId = lastKnownComponentIdRef.current || currentComponentId
486
531
  const retryMessage: WebSocketMessage = {
487
532
  type: 'CALL_ACTION',
488
- componentId,
533
+ componentId: retryComponentId,
489
534
  action,
490
535
  payload
491
536
  }
@@ -505,14 +550,16 @@ export function useHybridLiveComponent<T = any>(
505
550
 
506
551
  // Call action and wait for specific return value
507
552
  const callAndWait = useCallback(async (action: string, payload?: any, timeout?: number): Promise<any> => {
508
- if (!componentId || !connected) {
553
+ // Use ref as fallback for componentId (handles timing issues after rehydration)
554
+ const currentComponentId = componentId || lastKnownComponentIdRef.current
555
+ if (!currentComponentId || !connected) {
509
556
  throw new Error('Component not mounted or WebSocket not connected')
510
557
  }
511
558
 
512
559
  try {
513
560
  const message: WebSocketMessage = {
514
561
  type: 'CALL_ACTION',
515
- componentId,
562
+ componentId: currentComponentId,
516
563
  action,
517
564
  payload
518
565
  }
@@ -550,24 +597,31 @@ export function useHybridLiveComponent<T = any>(
550
597
  if (wasConnected && !isConnected && mountedRef.current) {
551
598
  mountedRef.current = false
552
599
  setComponentId(null)
600
+ // Call onDisconnect callback
601
+ onDisconnect?.()
553
602
  }
554
603
 
555
- if (!wasConnected && isConnected && !mountedRef.current && !mountingRef.current && !rehydrating) {
556
- setTimeout(() => {
557
- if (!mountedRef.current && !mountingRef.current && !rehydrating) {
558
- const persistedState = getPersistedState(componentName)
604
+ if (!wasConnected && isConnected) {
605
+ // Call onConnect callback when WebSocket connects
606
+ onConnect?.()
559
607
 
560
- if (persistedState?.signedState) {
561
- attemptRehydration()
562
- } else {
563
- mount()
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
+ }
564
618
  }
565
- }
566
- }, 100)
619
+ }, 100)
620
+ }
567
621
  }
568
622
 
569
623
  prevConnectedRef.current = connected
570
- }, [connected, mount, componentId, attemptRehydration, componentName, rehydrating])
624
+ }, [connected, mount, componentId, attemptRehydration, componentName, rehydrating, onDisconnect, onConnect])
571
625
 
572
626
  // Unmount on cleanup
573
627
  useEffect(() => {
@@ -0,0 +1,133 @@
1
+ // 🔥 Typed Live Component Hook - Full Type Inference for Actions
2
+ // Similar to Eden Treaty - automatic type inference from backend components
3
+
4
+ import { useHybridLiveComponent } from './useHybridLiveComponent'
5
+ import type { UseHybridLiveComponentReturn } from './useHybridLiveComponent'
6
+ import type {
7
+ LiveComponent,
8
+ InferComponentState,
9
+ HybridComponentOptions,
10
+ UseTypedLiveComponentReturn,
11
+ ActionNames,
12
+ ActionPayload,
13
+ ActionReturn
14
+ } from '@/core/types/types'
15
+
16
+ /**
17
+ * Type-safe Live Component hook with automatic action inference
18
+ *
19
+ * @example
20
+ * // Backend component definition
21
+ * class LiveClockComponent extends LiveComponent<LiveClockState> {
22
+ * async setTimeFormat(payload: { format: '12h' | '24h' }) { ... }
23
+ * async toggleSeconds(payload?: { showSeconds?: boolean }) { ... }
24
+ * async getServerInfo() { ... }
25
+ * }
26
+ *
27
+ * // Frontend usage with full type inference
28
+ * const { state, call, callAndWait } = useTypedLiveComponent<LiveClockComponent>(
29
+ * 'LiveClock',
30
+ * initialState
31
+ * )
32
+ *
33
+ * // ✅ Autocomplete for action names
34
+ * await call('setTimeFormat', { format: '12h' })
35
+ *
36
+ * // ✅ Type error if wrong payload
37
+ * await call('setTimeFormat', { format: 'invalid' }) // Error!
38
+ *
39
+ * // ✅ Return type is inferred
40
+ * const result = await callAndWait('getServerInfo')
41
+ * // result is: { success: boolean; info: { serverTime: string; ... } }
42
+ */
43
+ export function useTypedLiveComponent<T extends LiveComponent<any>>(
44
+ componentName: string,
45
+ initialState: InferComponentState<T>,
46
+ options: HybridComponentOptions = {}
47
+ ): UseTypedLiveComponentReturn<T> {
48
+ // Use the original hook
49
+ const result = useHybridLiveComponent<InferComponentState<T>>(
50
+ componentName,
51
+ initialState,
52
+ options
53
+ )
54
+
55
+ // Create convenience setValue helper
56
+ const setValue = async <K extends keyof InferComponentState<T>>(
57
+ key: K,
58
+ value: InferComponentState<T>[K]
59
+ ): Promise<void> => {
60
+ await result.call('setValue', { key, value })
61
+ }
62
+
63
+ // Return with typed call functions and setValue helper
64
+ // The types are enforced at compile time, runtime behavior is the same
65
+ return {
66
+ ...result,
67
+ setValue
68
+ } as unknown as UseTypedLiveComponentReturn<T>
69
+ }
70
+
71
+ /**
72
+ * Helper type to create a component registry map
73
+ * Maps component names to their class types for even better DX
74
+ *
75
+ * @example
76
+ * // Define your component map
77
+ * type MyComponents = {
78
+ * LiveClock: LiveClockComponent
79
+ * LiveCounter: LiveCounterComponent
80
+ * LiveChat: LiveChatComponent
81
+ * }
82
+ *
83
+ * // Create a typed hook for your app
84
+ * function useMyComponent<K extends keyof MyComponents>(
85
+ * name: K,
86
+ * initialState: ComponentState<MyComponents[K]>,
87
+ * options?: HybridComponentOptions
88
+ * ) {
89
+ * return useTypedLiveComponent<MyComponents[K]>(name, initialState, options)
90
+ * }
91
+ *
92
+ * // Usage
93
+ * const clock = useMyComponent('LiveClock', { ... })
94
+ * // TypeScript knows exactly which actions are available!
95
+ */
96
+ export type ComponentRegistry<T extends Record<string, LiveComponent<any>>> = {
97
+ [K in keyof T]: T[K]
98
+ }
99
+
100
+ /**
101
+ * Create a factory for typed live component hooks
102
+ * Useful when you have many components and want simpler imports
103
+ *
104
+ * @example
105
+ * // In your app/client/src/lib/live.ts
106
+ * import { createTypedLiveComponentHook } from '@/core/client/hooks/useTypedLiveComponent'
107
+ * import type { LiveClockComponent } from '@/app/server/live/LiveClockComponent'
108
+ *
109
+ * export const useLiveClock = createTypedLiveComponentHook<LiveClockComponent>('LiveClock')
110
+ *
111
+ * // Usage in component
112
+ * const { state, call } = useLiveClock({ currentTime: '', ... })
113
+ */
114
+ export function createTypedLiveComponentHook<T extends LiveComponent<any>>(
115
+ componentName: string
116
+ ) {
117
+ return function useComponent(
118
+ initialState: InferComponentState<T>,
119
+ options: HybridComponentOptions = {}
120
+ ): UseTypedLiveComponentReturn<T> {
121
+ return useTypedLiveComponent<T>(componentName, initialState, options)
122
+ }
123
+ }
124
+
125
+ // Re-export types for convenience
126
+ export type {
127
+ InferComponentState,
128
+ ActionNames,
129
+ ActionPayload,
130
+ ActionReturn,
131
+ UseTypedLiveComponentReturn,
132
+ HybridComponentOptions
133
+ }
@@ -32,22 +32,10 @@ export function useWebSocket(options: UseWebSocketOptions = {}): UseWebSocketRet
32
32
  // Get WebSocket URL dynamically based on current environment
33
33
  const getWebSocketUrl = () => {
34
34
  if (typeof window === 'undefined') return 'ws://localhost:3000/api/live/ws'
35
-
36
- const hostname = window.location.hostname
37
- const isLocalhost = hostname === 'localhost' || hostname === '127.0.0.1'
38
-
39
- // In production, use current origin with ws/wss protocol
40
- if (!isLocalhost) {
41
- const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'
42
- const url = `${protocol}//${window.location.host}/api/live/ws`
43
- console.log('🔗 [WebSocket] Production URL:', url)
44
- return url
45
- }
46
-
47
- // In development, use backend server (port 3000 for integrated mode)
48
- const url = 'ws://localhost:3000/api/live/ws'
49
- console.log('🔗 [WebSocket] Development URL:', url)
50
- return url
35
+
36
+ // Always use current host - works for both dev and production
37
+ const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'
38
+ return `${protocol}//${window.location.host}/api/live/ws`
51
39
  }
52
40
 
53
41
  const {