create-fluxstack 1.14.0 → 1.15.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/LLMD/resources/live-components.md +207 -12
- package/app/client/.live-stubs/LiveAdminPanel.js +5 -0
- package/app/client/.live-stubs/LiveChat.js +7 -0
- package/app/client/.live-stubs/LiveCounter.js +9 -0
- package/app/client/.live-stubs/LiveForm.js +11 -0
- package/app/client/.live-stubs/LiveLocalCounter.js +8 -0
- package/app/client/.live-stubs/LiveRoomChat.js +10 -0
- package/app/client/.live-stubs/LiveTodoList.js +9 -0
- package/app/client/.live-stubs/LiveUpload.js +15 -0
- package/app/client/src/App.tsx +11 -0
- package/app/client/src/components/AppLayout.tsx +16 -8
- package/app/client/src/live/LiveDebuggerPanel.tsx +1 -1
- package/app/client/src/live/TodoListDemo.tsx +158 -0
- package/app/server/auth/DevAuthProvider.ts +2 -2
- package/app/server/auth/JWTAuthProvider.example.ts +2 -2
- package/app/server/index.ts +2 -2
- package/app/server/live/LiveAdminPanel.ts +1 -1
- package/app/server/live/LiveProtectedChat.ts +1 -1
- package/app/server/live/LiveTodoList.ts +110 -0
- package/app/server/routes/room.routes.ts +1 -2
- package/core/build/live-components-generator.ts +1 -1
- package/core/build/vite-plugins.ts +28 -0
- package/core/client/components/LiveDebugger.tsx +1 -1
- package/core/client/hooks/useLiveUpload.ts +3 -4
- package/core/client/index.ts +37 -31
- package/core/framework/server.ts +1 -1
- package/core/server/index.ts +1 -2
- package/core/server/live/auto-generated-components.ts +6 -3
- package/core/server/live/index.ts +95 -21
- package/core/server/live/websocket-plugin.ts +27 -1087
- package/core/types/types.ts +76 -1025
- package/core/utils/version.ts +1 -1
- package/create-fluxstack.ts +1 -1
- package/package.json +5 -1
- package/plugins/crypto-auth/index.ts +1 -1
- package/plugins/crypto-auth/server/CryptoAuthLiveProvider.ts +2 -2
- package/vite.config.ts +40 -12
- package/core/client/LiveComponentsProvider.tsx +0 -531
- package/core/client/components/Live.tsx +0 -111
- package/core/client/hooks/AdaptiveChunkSizer.ts +0 -215
- package/core/client/hooks/state-validator.ts +0 -130
- package/core/client/hooks/useChunkedUpload.ts +0 -359
- package/core/client/hooks/useLiveChunkedUpload.ts +0 -87
- package/core/client/hooks/useLiveComponent.ts +0 -853
- package/core/client/hooks/useLiveDebugger.ts +0 -392
- package/core/client/hooks/useRoom.ts +0 -409
- package/core/client/hooks/useRoomProxy.ts +0 -382
- package/core/server/live/ComponentRegistry.ts +0 -1128
- package/core/server/live/FileUploadManager.ts +0 -446
- package/core/server/live/LiveComponentPerformanceMonitor.ts +0 -931
- package/core/server/live/LiveDebugger.ts +0 -462
- package/core/server/live/LiveLogger.ts +0 -144
- package/core/server/live/LiveRoomManager.ts +0 -278
- package/core/server/live/RoomEventBus.ts +0 -234
- package/core/server/live/RoomStateManager.ts +0 -172
- package/core/server/live/SingleConnectionManager.ts +0 -0
- package/core/server/live/StateSignature.ts +0 -705
- package/core/server/live/WebSocketConnectionManager.ts +0 -710
- package/core/server/live/auth/LiveAuthContext.ts +0 -71
- package/core/server/live/auth/LiveAuthManager.ts +0 -304
- package/core/server/live/auth/index.ts +0 -19
- package/core/server/live/auth/types.ts +0 -179
|
@@ -1,111 +0,0 @@
|
|
|
1
|
-
// 🔥 FluxStack Live - Hook para componentes real-time
|
|
2
|
-
//
|
|
3
|
-
// Uso:
|
|
4
|
-
// import { Live } from '@/core/client'
|
|
5
|
-
// import { LiveForm } from '@server/live/LiveForm'
|
|
6
|
-
//
|
|
7
|
-
// // Sem estado inicial - usa defaultState do componente
|
|
8
|
-
// const form = Live.use(LiveForm)
|
|
9
|
-
//
|
|
10
|
-
// // Com estado inicial parcial (override)
|
|
11
|
-
// const form = Live.use(LiveForm, { name: 'João' })
|
|
12
|
-
//
|
|
13
|
-
// return (
|
|
14
|
-
// <input {...form.$field('name', { syncOn: 'blur' })} />
|
|
15
|
-
// <button onClick={() => form.submit()}>Enviar</button>
|
|
16
|
-
// )
|
|
17
|
-
//
|
|
18
|
-
// 🔥 Broadcasts Tipados (Discriminated Union):
|
|
19
|
-
// // No servidor, defina a interface de broadcasts:
|
|
20
|
-
// export interface LiveFormBroadcasts {
|
|
21
|
-
// FORM_SUBMITTED: { formId: string; data: any }
|
|
22
|
-
// FIELD_CHANGED: { field: string; value: any }
|
|
23
|
-
// }
|
|
24
|
-
//
|
|
25
|
-
// // No cliente, use com tipagem automática (discriminated union):
|
|
26
|
-
// import { LiveForm, type LiveFormBroadcasts } from '@server/live/LiveForm'
|
|
27
|
-
//
|
|
28
|
-
// const form = Live.use(LiveForm)
|
|
29
|
-
// form.$onBroadcast<LiveFormBroadcasts>((event) => {
|
|
30
|
-
// switch (event.type) {
|
|
31
|
-
// case 'FORM_SUBMITTED':
|
|
32
|
-
// console.log(event.data.formId) // ✅ Tipado como string!
|
|
33
|
-
// break
|
|
34
|
-
// case 'FIELD_CHANGED':
|
|
35
|
-
// console.log(event.data.field) // ✅ Tipado como string!
|
|
36
|
-
// break
|
|
37
|
-
// }
|
|
38
|
-
// })
|
|
39
|
-
|
|
40
|
-
import { useLiveComponent } from '../hooks/useLiveComponent'
|
|
41
|
-
import type { UseLiveComponentOptions, LiveProxy, LiveProxyWithBroadcasts } from '../hooks/useLiveComponent'
|
|
42
|
-
|
|
43
|
-
// ===== Tipos para Inferência do Servidor =====
|
|
44
|
-
|
|
45
|
-
// Extrai o defaultState estático da classe
|
|
46
|
-
type ExtractDefaultState<T> = T extends { defaultState: infer S }
|
|
47
|
-
? S extends Record<string, any> ? S : Record<string, any>
|
|
48
|
-
: Record<string, any>
|
|
49
|
-
|
|
50
|
-
// Extrai o State da classe do servidor (via instance.state)
|
|
51
|
-
type ExtractState<T> = T extends { new(...args: any[]): { state: infer S } }
|
|
52
|
-
? S extends Record<string, any> ? S : Record<string, any>
|
|
53
|
-
: ExtractDefaultState<T>
|
|
54
|
-
|
|
55
|
-
// Extrai os nomes de publicActions como union type
|
|
56
|
-
type ExtractPublicActionNames<T> = T extends { publicActions: readonly (infer A)[] }
|
|
57
|
-
? A extends string ? A : never
|
|
58
|
-
: never
|
|
59
|
-
|
|
60
|
-
// Extrai as Actions respeitando publicActions (MANDATORY)
|
|
61
|
-
// - Se publicActions está definido: somente métodos listados são expostos
|
|
62
|
-
// - Se publicActions NÃO está definido: nenhuma action disponível (secure by default)
|
|
63
|
-
type ExtractActions<T> = T extends { new(...args: any[]): infer Instance }
|
|
64
|
-
? T extends { publicActions: readonly string[] }
|
|
65
|
-
? {
|
|
66
|
-
[K in keyof Instance as K extends ExtractPublicActionNames<T>
|
|
67
|
-
? Instance[K] extends (...args: any[]) => Promise<any> ? K : never
|
|
68
|
-
: never
|
|
69
|
-
]: Instance[K]
|
|
70
|
-
}
|
|
71
|
-
: Record<string, never>
|
|
72
|
-
: Record<string, never>
|
|
73
|
-
|
|
74
|
-
// ===== Opções do Live.use() =====
|
|
75
|
-
|
|
76
|
-
interface LiveUseOptions<TState> extends UseLiveComponentOptions {
|
|
77
|
-
/** Estado inicial para o componente */
|
|
78
|
-
initialState?: Partial<TState>
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// ===== Hook Principal =====
|
|
82
|
-
|
|
83
|
-
function useLive<
|
|
84
|
-
T extends { new(...args: any[]): any; defaultState?: Record<string, any>; componentName: string; publicActions?: readonly string[] },
|
|
85
|
-
TBroadcasts extends Record<string, any> = Record<string, any>
|
|
86
|
-
>(
|
|
87
|
-
ComponentClass: T,
|
|
88
|
-
options?: LiveUseOptions<ExtractState<T>>
|
|
89
|
-
): LiveProxyWithBroadcasts<ExtractState<T>, ExtractActions<T>, TBroadcasts> {
|
|
90
|
-
// Use static componentName (required for production builds with minification)
|
|
91
|
-
const componentName = ComponentClass.componentName
|
|
92
|
-
|
|
93
|
-
// Usa defaultState da classe se não passar initialState
|
|
94
|
-
const defaultState = (ComponentClass as any).defaultState || {}
|
|
95
|
-
const { initialState, ...restOptions } = options || {}
|
|
96
|
-
const mergedState = { ...defaultState, ...initialState } as ExtractState<T>
|
|
97
|
-
|
|
98
|
-
return useLiveComponent<ExtractState<T>, ExtractActions<T>, TBroadcasts>(
|
|
99
|
-
componentName,
|
|
100
|
-
mergedState,
|
|
101
|
-
restOptions
|
|
102
|
-
)
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
// ===== Export =====
|
|
106
|
-
|
|
107
|
-
export const Live = {
|
|
108
|
-
use: useLive
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
export default Live
|
|
@@ -1,215 +0,0 @@
|
|
|
1
|
-
// 🚀 Adaptive Chunk Sizing - Dynamic chunk size adjustment based on connection speed
|
|
2
|
-
// Automatically optimizes upload speed by adjusting chunk sizes
|
|
3
|
-
|
|
4
|
-
export interface AdaptiveChunkConfig {
|
|
5
|
-
minChunkSize: number // Minimum chunk size (default: 16KB)
|
|
6
|
-
maxChunkSize: number // Maximum chunk size (default: 1MB)
|
|
7
|
-
initialChunkSize: number // Starting chunk size (default: 64KB)
|
|
8
|
-
targetLatency: number // Target latency per chunk in ms (default: 200ms)
|
|
9
|
-
adjustmentFactor: number // How aggressively to adjust (default: 1.5)
|
|
10
|
-
measurementWindow: number // Number of chunks to measure (default: 3)
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export interface ChunkMetrics {
|
|
14
|
-
chunkIndex: number
|
|
15
|
-
chunkSize: number
|
|
16
|
-
startTime: number
|
|
17
|
-
endTime: number
|
|
18
|
-
latency: number
|
|
19
|
-
throughput: number // bytes per second
|
|
20
|
-
success: boolean
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export class AdaptiveChunkSizer {
|
|
24
|
-
private config: Required<AdaptiveChunkConfig>
|
|
25
|
-
private currentChunkSize: number
|
|
26
|
-
private metrics: ChunkMetrics[] = []
|
|
27
|
-
private consecutiveErrors = 0
|
|
28
|
-
private consecutiveSuccesses = 0
|
|
29
|
-
|
|
30
|
-
constructor(config: Partial<AdaptiveChunkConfig> = {}) {
|
|
31
|
-
this.config = {
|
|
32
|
-
minChunkSize: config.minChunkSize ?? 16 * 1024, // 16KB
|
|
33
|
-
maxChunkSize: config.maxChunkSize ?? 1024 * 1024, // 1MB
|
|
34
|
-
initialChunkSize: config.initialChunkSize ?? 64 * 1024, // 64KB
|
|
35
|
-
targetLatency: config.targetLatency ?? 200, // 200ms
|
|
36
|
-
adjustmentFactor: config.adjustmentFactor ?? 1.5,
|
|
37
|
-
measurementWindow: config.measurementWindow ?? 3
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
this.currentChunkSize = this.config.initialChunkSize
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Get the current optimal chunk size
|
|
45
|
-
*/
|
|
46
|
-
getChunkSize(): number {
|
|
47
|
-
return this.currentChunkSize
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Record the start of a chunk upload
|
|
52
|
-
*/
|
|
53
|
-
recordChunkStart(chunkIndex: number): number {
|
|
54
|
-
return Date.now()
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* Record the completion of a chunk upload and adjust chunk size
|
|
59
|
-
*/
|
|
60
|
-
recordChunkComplete(
|
|
61
|
-
chunkIndex: number,
|
|
62
|
-
chunkSize: number,
|
|
63
|
-
startTime: number,
|
|
64
|
-
success: boolean
|
|
65
|
-
): void {
|
|
66
|
-
const endTime = Date.now()
|
|
67
|
-
const latency = endTime - startTime
|
|
68
|
-
const throughput = success ? (chunkSize / latency) * 1000 : 0 // bytes per second
|
|
69
|
-
|
|
70
|
-
const metric: ChunkMetrics = {
|
|
71
|
-
chunkIndex,
|
|
72
|
-
chunkSize,
|
|
73
|
-
startTime,
|
|
74
|
-
endTime,
|
|
75
|
-
latency,
|
|
76
|
-
throughput,
|
|
77
|
-
success
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
this.metrics.push(metric)
|
|
81
|
-
|
|
82
|
-
// Keep only recent measurements
|
|
83
|
-
if (this.metrics.length > this.config.measurementWindow * 2) {
|
|
84
|
-
this.metrics = this.metrics.slice(-this.config.measurementWindow * 2)
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
if (success) {
|
|
88
|
-
this.consecutiveSuccesses++
|
|
89
|
-
this.consecutiveErrors = 0
|
|
90
|
-
this.adjustChunkSizeUp(latency)
|
|
91
|
-
} else {
|
|
92
|
-
this.consecutiveErrors++
|
|
93
|
-
this.consecutiveSuccesses = 0
|
|
94
|
-
this.adjustChunkSizeDown()
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
console.log(`📊 Adaptive Chunk Stats:`, {
|
|
98
|
-
chunkIndex,
|
|
99
|
-
currentSize: this.formatBytes(this.currentChunkSize),
|
|
100
|
-
latency: `${latency}ms`,
|
|
101
|
-
throughput: `${this.formatBytes(throughput)}/s`,
|
|
102
|
-
avgThroughput: `${this.formatBytes(this.getAverageThroughput())}/s`,
|
|
103
|
-
success
|
|
104
|
-
})
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/**
|
|
108
|
-
* Increase chunk size if connection is fast
|
|
109
|
-
*/
|
|
110
|
-
private adjustChunkSizeUp(latency: number): void {
|
|
111
|
-
// Only increase if we have enough successful measurements
|
|
112
|
-
if (this.consecutiveSuccesses < 2) return
|
|
113
|
-
|
|
114
|
-
// Only increase if latency is below target
|
|
115
|
-
if (latency > this.config.targetLatency) return
|
|
116
|
-
|
|
117
|
-
// Calculate new chunk size based on how much faster we are than target
|
|
118
|
-
const latencyRatio = this.config.targetLatency / latency
|
|
119
|
-
let newSize = Math.floor(this.currentChunkSize * Math.min(latencyRatio, this.config.adjustmentFactor))
|
|
120
|
-
|
|
121
|
-
// Cap at max chunk size
|
|
122
|
-
newSize = Math.min(newSize, this.config.maxChunkSize)
|
|
123
|
-
|
|
124
|
-
if (newSize > this.currentChunkSize) {
|
|
125
|
-
console.log(`⬆️ Increasing chunk size: ${this.formatBytes(this.currentChunkSize)} → ${this.formatBytes(newSize)}`)
|
|
126
|
-
this.currentChunkSize = newSize
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
/**
|
|
131
|
-
* Decrease chunk size if connection is slow or unstable
|
|
132
|
-
*/
|
|
133
|
-
private adjustChunkSizeDown(): void {
|
|
134
|
-
// Decrease more aggressively on errors
|
|
135
|
-
const decreaseFactor = this.consecutiveErrors > 1 ? 2 : this.config.adjustmentFactor
|
|
136
|
-
|
|
137
|
-
let newSize = Math.floor(this.currentChunkSize / decreaseFactor)
|
|
138
|
-
|
|
139
|
-
// Cap at min chunk size
|
|
140
|
-
newSize = Math.max(newSize, this.config.minChunkSize)
|
|
141
|
-
|
|
142
|
-
if (newSize < this.currentChunkSize) {
|
|
143
|
-
console.log(`⬇️ Decreasing chunk size: ${this.formatBytes(this.currentChunkSize)} → ${this.formatBytes(newSize)}`)
|
|
144
|
-
this.currentChunkSize = newSize
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
/**
|
|
149
|
-
* Get average throughput from recent measurements
|
|
150
|
-
*/
|
|
151
|
-
getAverageThroughput(): number {
|
|
152
|
-
if (this.metrics.length === 0) return 0
|
|
153
|
-
|
|
154
|
-
const recentMetrics = this.metrics
|
|
155
|
-
.slice(-this.config.measurementWindow)
|
|
156
|
-
.filter(m => m.success)
|
|
157
|
-
|
|
158
|
-
if (recentMetrics.length === 0) return 0
|
|
159
|
-
|
|
160
|
-
const totalThroughput = recentMetrics.reduce((sum, m) => sum + m.throughput, 0)
|
|
161
|
-
return totalThroughput / recentMetrics.length
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
/**
|
|
165
|
-
* Get average latency from recent measurements
|
|
166
|
-
*/
|
|
167
|
-
getAverageLatency(): number {
|
|
168
|
-
if (this.metrics.length === 0) return 0
|
|
169
|
-
|
|
170
|
-
const recentMetrics = this.metrics
|
|
171
|
-
.slice(-this.config.measurementWindow)
|
|
172
|
-
.filter(m => m.success)
|
|
173
|
-
|
|
174
|
-
if (recentMetrics.length === 0) return 0
|
|
175
|
-
|
|
176
|
-
const totalLatency = recentMetrics.reduce((sum, m) => sum + m.latency, 0)
|
|
177
|
-
return totalLatency / recentMetrics.length
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
/**
|
|
181
|
-
* Get current performance statistics
|
|
182
|
-
*/
|
|
183
|
-
getStats() {
|
|
184
|
-
return {
|
|
185
|
-
currentChunkSize: this.currentChunkSize,
|
|
186
|
-
averageThroughput: this.getAverageThroughput(),
|
|
187
|
-
averageLatency: this.getAverageLatency(),
|
|
188
|
-
consecutiveSuccesses: this.consecutiveSuccesses,
|
|
189
|
-
consecutiveErrors: this.consecutiveErrors,
|
|
190
|
-
totalMeasurements: this.metrics.length,
|
|
191
|
-
config: this.config
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
/**
|
|
196
|
-
* Reset the adaptive chunking state
|
|
197
|
-
*/
|
|
198
|
-
reset(): void {
|
|
199
|
-
this.currentChunkSize = this.config.initialChunkSize
|
|
200
|
-
this.metrics = []
|
|
201
|
-
this.consecutiveErrors = 0
|
|
202
|
-
this.consecutiveSuccesses = 0
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
/**
|
|
206
|
-
* Format bytes for display
|
|
207
|
-
*/
|
|
208
|
-
private formatBytes(bytes: number): string {
|
|
209
|
-
if (bytes === 0) return '0 B'
|
|
210
|
-
const k = 1024
|
|
211
|
-
const sizes = ['B', 'KB', 'MB', 'GB']
|
|
212
|
-
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
|
213
|
-
return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i]
|
|
214
|
-
}
|
|
215
|
-
}
|
|
@@ -1,130 +0,0 @@
|
|
|
1
|
-
// 🔥 State Validation Utilities
|
|
2
|
-
|
|
3
|
-
import type { StateValidation, StateConflict, HybridState } from '@core/types/types'
|
|
4
|
-
|
|
5
|
-
export class StateValidator {
|
|
6
|
-
/**
|
|
7
|
-
* Generate checksum for state object
|
|
8
|
-
*/
|
|
9
|
-
static generateChecksum(state: any): string {
|
|
10
|
-
const json = JSON.stringify(state, Object.keys(state).sort())
|
|
11
|
-
let hash = 0
|
|
12
|
-
for (let i = 0; i < json.length; i++) {
|
|
13
|
-
const char = json.charCodeAt(i)
|
|
14
|
-
hash = ((hash << 5) - hash) + char
|
|
15
|
-
hash = hash & hash // Convert to 32-bit integer
|
|
16
|
-
}
|
|
17
|
-
return Math.abs(hash).toString(16)
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Create validation metadata
|
|
22
|
-
*/
|
|
23
|
-
static createValidation(
|
|
24
|
-
state: any,
|
|
25
|
-
source: 'client' | 'server' | 'mount' = 'client'
|
|
26
|
-
): StateValidation {
|
|
27
|
-
return {
|
|
28
|
-
checksum: this.generateChecksum(state),
|
|
29
|
-
version: Date.now(),
|
|
30
|
-
timestamp: Date.now(),
|
|
31
|
-
source
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Compare two states and detect conflicts
|
|
37
|
-
*/
|
|
38
|
-
static detectConflicts<T>(
|
|
39
|
-
clientState: T,
|
|
40
|
-
serverState: T,
|
|
41
|
-
excludeFields: string[] = ['lastUpdated', 'version']
|
|
42
|
-
): StateConflict[] {
|
|
43
|
-
const conflicts: StateConflict[] = []
|
|
44
|
-
|
|
45
|
-
const clientKeys = Object.keys(clientState as any)
|
|
46
|
-
const serverKeys = Object.keys(serverState as any)
|
|
47
|
-
const allKeys = Array.from(new Set([...clientKeys, ...serverKeys]))
|
|
48
|
-
|
|
49
|
-
for (const key of allKeys) {
|
|
50
|
-
if (excludeFields.includes(key)) continue
|
|
51
|
-
|
|
52
|
-
const clientValue = (clientState as any)?.[key]
|
|
53
|
-
const serverValue = (serverState as any)?.[key]
|
|
54
|
-
|
|
55
|
-
if (JSON.stringify(clientValue) !== JSON.stringify(serverValue)) {
|
|
56
|
-
conflicts.push({
|
|
57
|
-
property: key as string,
|
|
58
|
-
clientValue,
|
|
59
|
-
serverValue,
|
|
60
|
-
timestamp: Date.now(),
|
|
61
|
-
resolved: false
|
|
62
|
-
})
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
return conflicts
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* Merge states with conflict resolution
|
|
71
|
-
*/
|
|
72
|
-
static mergeStates<T>(
|
|
73
|
-
clientState: T,
|
|
74
|
-
serverState: T,
|
|
75
|
-
conflicts: StateConflict[],
|
|
76
|
-
strategy: 'client' | 'server' | 'smart' = 'smart'
|
|
77
|
-
): T {
|
|
78
|
-
const merged = { ...clientState }
|
|
79
|
-
|
|
80
|
-
for (const conflict of conflicts) {
|
|
81
|
-
switch (strategy) {
|
|
82
|
-
case 'client':
|
|
83
|
-
// Keep client value
|
|
84
|
-
break
|
|
85
|
-
|
|
86
|
-
case 'server':
|
|
87
|
-
(merged as any)[conflict.property] = conflict.serverValue
|
|
88
|
-
break
|
|
89
|
-
|
|
90
|
-
case 'smart':
|
|
91
|
-
// Smart resolution based on field type and context
|
|
92
|
-
if (conflict.property === 'lastUpdated') {
|
|
93
|
-
// Server timestamp wins
|
|
94
|
-
(merged as any)[conflict.property] = conflict.serverValue
|
|
95
|
-
} else if (typeof conflict.serverValue === 'number' && typeof conflict.clientValue === 'number') {
|
|
96
|
-
// For numbers, use the higher value (e.g., counters)
|
|
97
|
-
(merged as any)[conflict.property] = Math.max(conflict.serverValue, conflict.clientValue)
|
|
98
|
-
} else {
|
|
99
|
-
// Default to server for other types
|
|
100
|
-
(merged as any)[conflict.property] = conflict.serverValue
|
|
101
|
-
}
|
|
102
|
-
break
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
return merged
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
/**
|
|
110
|
-
* Validate state integrity
|
|
111
|
-
*/
|
|
112
|
-
static validateState<T>(hybridState: HybridState<T>): boolean {
|
|
113
|
-
const currentChecksum = this.generateChecksum(hybridState.data)
|
|
114
|
-
return currentChecksum === hybridState.validation.checksum
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
/**
|
|
118
|
-
* Update validation after state change
|
|
119
|
-
*/
|
|
120
|
-
static updateValidation<T>(
|
|
121
|
-
hybridState: HybridState<T>,
|
|
122
|
-
source: 'client' | 'server' | 'mount' = 'client'
|
|
123
|
-
): HybridState<T> {
|
|
124
|
-
return {
|
|
125
|
-
...hybridState,
|
|
126
|
-
validation: this.createValidation(hybridState.data, source),
|
|
127
|
-
status: 'synced'
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
}
|