create-fluxstack 1.9.1 → 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 (49) hide show
  1. package/LIVE_COMPONENTS_REVIEW.md +781 -0
  2. package/app/client/src/App.tsx +39 -43
  3. package/app/client/src/lib/eden-api.ts +2 -7
  4. package/app/client/src/live/FileUploadExample.tsx +359 -0
  5. package/app/client/src/live/MinimalLiveClock.tsx +47 -0
  6. package/app/client/src/live/QuickUploadTest.tsx +193 -0
  7. package/app/client/src/main.tsx +10 -10
  8. package/app/client/src/vite-env.d.ts +1 -1
  9. package/app/client/tsconfig.app.json +45 -44
  10. package/app/client/tsconfig.node.json +25 -25
  11. package/app/server/index.ts +30 -103
  12. package/app/server/live/LiveFileUploadComponent.ts +77 -0
  13. package/app/server/live/register-components.ts +19 -19
  14. package/core/build/bundler.ts +4 -1
  15. package/core/build/index.ts +124 -4
  16. package/core/build/live-components-generator.ts +68 -1
  17. package/core/cli/index.ts +163 -35
  18. package/core/client/LiveComponentsProvider.tsx +3 -9
  19. package/core/client/hooks/AdaptiveChunkSizer.ts +215 -0
  20. package/core/client/hooks/useChunkedUpload.ts +112 -61
  21. package/core/client/hooks/useHybridLiveComponent.ts +80 -26
  22. package/core/client/hooks/useTypedLiveComponent.ts +133 -0
  23. package/core/client/hooks/useWebSocket.ts +4 -16
  24. package/core/client/index.ts +20 -2
  25. package/core/framework/server.ts +181 -8
  26. package/core/live/ComponentRegistry.ts +5 -1
  27. package/core/plugins/built-in/index.ts +8 -5
  28. package/core/plugins/built-in/live-components/commands/create-live-component.ts +55 -63
  29. package/core/plugins/built-in/vite/index.ts +75 -187
  30. package/core/plugins/built-in/vite/vite-dev.ts +88 -0
  31. package/core/plugins/registry.ts +54 -2
  32. package/core/plugins/types.ts +86 -2
  33. package/core/server/index.ts +1 -2
  34. package/core/server/live/ComponentRegistry.ts +14 -5
  35. package/core/server/live/FileUploadManager.ts +22 -25
  36. package/core/server/live/auto-generated-components.ts +29 -26
  37. package/core/server/live/websocket-plugin.ts +19 -5
  38. package/core/server/plugins/static-files-plugin.ts +49 -240
  39. package/core/server/plugins/swagger.ts +33 -33
  40. package/core/types/build.ts +1 -0
  41. package/core/types/plugin.ts +9 -1
  42. package/core/types/types.ts +137 -0
  43. package/core/utils/logger/startup-banner.ts +20 -4
  44. package/core/utils/version.ts +1 -1
  45. package/eslint.config.js +23 -23
  46. package/package.json +3 -3
  47. package/plugins/crypto-auth/server/middlewares.ts +19 -19
  48. package/tsconfig.json +52 -52
  49. package/workspace.json +5 -5
@@ -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 {
@@ -19,9 +19,15 @@ export type {
19
19
  // Hooks
20
20
  export { useWebSocket } from './hooks/useWebSocket'
21
21
  export { useHybridLiveComponent } from './hooks/useHybridLiveComponent'
22
+ export { useTypedLiveComponent, createTypedLiveComponentHook } from './hooks/useTypedLiveComponent'
22
23
  export { useChunkedUpload } from './hooks/useChunkedUpload'
24
+ export { AdaptiveChunkSizer } from './hooks/AdaptiveChunkSizer'
23
25
  export { StateValidator } from './hooks/state-validator'
24
26
 
27
+ // Hook types
28
+ export type { AdaptiveChunkConfig, ChunkMetrics } from './hooks/AdaptiveChunkSizer'
29
+ export type { ChunkedUploadOptions, ChunkedUploadState } from './hooks/useChunkedUpload'
30
+
25
31
  // Re-export types from core/types/types.ts for convenience
26
32
  export type {
27
33
  // Live Components types
@@ -56,8 +62,20 @@ export type {
56
62
  ComponentActions,
57
63
  ComponentProps,
58
64
  ActionParameters,
59
- ActionReturnType
65
+ ActionReturnType,
66
+
67
+ // Type inference system (similar to Eden Treaty)
68
+ ExtractActions,
69
+ ActionNames,
70
+ ActionPayload,
71
+ ActionReturn,
72
+ InferComponentState,
73
+ TypedCall,
74
+ TypedCallAndWait,
75
+ TypedSetValue,
76
+ UseTypedLiveComponentReturn
60
77
  } from '../types/types'
61
78
 
62
79
  // Hook return types
63
- export type { UseHybridLiveComponentReturn } from './hooks/useHybridLiveComponent'
80
+ export type { UseHybridLiveComponentReturn } from './hooks/useHybridLiveComponent'
81
+ export type { ComponentRegistry } from './hooks/useTypedLiveComponent'