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