@affectively/aeon-pages 1.3.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/CHANGELOG.md +112 -0
- package/README.md +625 -0
- package/examples/basic/aeon.config.ts +39 -0
- package/examples/basic/components/Cursor.tsx +86 -0
- package/examples/basic/components/OfflineIndicator.tsx +103 -0
- package/examples/basic/components/PresenceBar.tsx +77 -0
- package/examples/basic/package.json +20 -0
- package/examples/basic/pages/index.tsx +80 -0
- package/package.json +101 -0
- package/packages/analytics/README.md +309 -0
- package/packages/analytics/build.ts +35 -0
- package/packages/analytics/package.json +50 -0
- package/packages/analytics/src/click-tracker.ts +368 -0
- package/packages/analytics/src/context-bridge.ts +319 -0
- package/packages/analytics/src/data-layer.ts +302 -0
- package/packages/analytics/src/gtm-loader.ts +239 -0
- package/packages/analytics/src/index.ts +230 -0
- package/packages/analytics/src/merkle-tree.ts +489 -0
- package/packages/analytics/src/provider.tsx +300 -0
- package/packages/analytics/src/types.ts +320 -0
- package/packages/analytics/src/use-analytics.ts +296 -0
- package/packages/analytics/tsconfig.json +19 -0
- package/packages/benchmarks/src/benchmark.test.ts +691 -0
- package/packages/cli/dist/index.js +61899 -0
- package/packages/cli/package.json +43 -0
- package/packages/cli/src/commands/build.test.ts +682 -0
- package/packages/cli/src/commands/build.ts +890 -0
- package/packages/cli/src/commands/dev.ts +473 -0
- package/packages/cli/src/commands/init.ts +409 -0
- package/packages/cli/src/commands/start.ts +297 -0
- package/packages/cli/src/index.ts +105 -0
- package/packages/directives/src/use-aeon.ts +272 -0
- package/packages/mcp-server/package.json +51 -0
- package/packages/mcp-server/src/index.ts +178 -0
- package/packages/mcp-server/src/resources.ts +346 -0
- package/packages/mcp-server/src/tools/index.ts +36 -0
- package/packages/mcp-server/src/tools/navigation.ts +545 -0
- package/packages/mcp-server/tsconfig.json +21 -0
- package/packages/react/package.json +40 -0
- package/packages/react/src/Link.tsx +388 -0
- package/packages/react/src/components/InstallPrompt.tsx +286 -0
- package/packages/react/src/components/OfflineDiagnostics.tsx +677 -0
- package/packages/react/src/components/PushNotifications.tsx +453 -0
- package/packages/react/src/hooks/useAeonNavigation.ts +219 -0
- package/packages/react/src/hooks/useConflicts.ts +277 -0
- package/packages/react/src/hooks/useNetworkState.ts +209 -0
- package/packages/react/src/hooks/usePilotNavigation.ts +254 -0
- package/packages/react/src/hooks/useServiceWorker.ts +278 -0
- package/packages/react/src/hooks.ts +195 -0
- package/packages/react/src/index.ts +151 -0
- package/packages/react/src/provider.tsx +467 -0
- package/packages/react/tsconfig.json +19 -0
- package/packages/runtime/README.md +399 -0
- package/packages/runtime/build.ts +48 -0
- package/packages/runtime/package.json +71 -0
- package/packages/runtime/schema.sql +40 -0
- package/packages/runtime/src/api-routes.ts +465 -0
- package/packages/runtime/src/benchmark.ts +171 -0
- package/packages/runtime/src/cache.ts +479 -0
- package/packages/runtime/src/durable-object.ts +1341 -0
- package/packages/runtime/src/index.ts +360 -0
- package/packages/runtime/src/navigation.test.ts +421 -0
- package/packages/runtime/src/navigation.ts +422 -0
- package/packages/runtime/src/nextjs-adapter.ts +272 -0
- package/packages/runtime/src/offline/encrypted-queue.test.ts +607 -0
- package/packages/runtime/src/offline/encrypted-queue.ts +478 -0
- package/packages/runtime/src/offline/encryption.test.ts +412 -0
- package/packages/runtime/src/offline/encryption.ts +397 -0
- package/packages/runtime/src/offline/types.ts +465 -0
- package/packages/runtime/src/predictor.ts +371 -0
- package/packages/runtime/src/registry.ts +351 -0
- package/packages/runtime/src/router/context-extractor.ts +661 -0
- package/packages/runtime/src/router/esi-control-react.tsx +2053 -0
- package/packages/runtime/src/router/esi-control.ts +541 -0
- package/packages/runtime/src/router/esi-cyrano.ts +779 -0
- package/packages/runtime/src/router/esi-format-react.tsx +1744 -0
- package/packages/runtime/src/router/esi-react.tsx +1065 -0
- package/packages/runtime/src/router/esi-translate-observer.ts +476 -0
- package/packages/runtime/src/router/esi-translate-react.tsx +556 -0
- package/packages/runtime/src/router/esi-translate.ts +503 -0
- package/packages/runtime/src/router/esi.ts +666 -0
- package/packages/runtime/src/router/heuristic-adapter.test.ts +295 -0
- package/packages/runtime/src/router/heuristic-adapter.ts +557 -0
- package/packages/runtime/src/router/index.ts +298 -0
- package/packages/runtime/src/router/merkle-capability.ts +473 -0
- package/packages/runtime/src/router/speculation.ts +451 -0
- package/packages/runtime/src/router/types.ts +630 -0
- package/packages/runtime/src/router.test.ts +470 -0
- package/packages/runtime/src/router.ts +302 -0
- package/packages/runtime/src/server.ts +481 -0
- package/packages/runtime/src/service-worker-push.ts +319 -0
- package/packages/runtime/src/service-worker.ts +553 -0
- package/packages/runtime/src/skeleton-hydrate.ts +237 -0
- package/packages/runtime/src/speculation.test.ts +389 -0
- package/packages/runtime/src/speculation.ts +486 -0
- package/packages/runtime/src/storage.test.ts +1297 -0
- package/packages/runtime/src/storage.ts +1048 -0
- package/packages/runtime/src/sync/conflict-resolver.test.ts +528 -0
- package/packages/runtime/src/sync/conflict-resolver.ts +565 -0
- package/packages/runtime/src/sync/coordinator.test.ts +608 -0
- package/packages/runtime/src/sync/coordinator.ts +596 -0
- package/packages/runtime/src/tree-compiler.ts +295 -0
- package/packages/runtime/src/types.ts +728 -0
- package/packages/runtime/src/worker.ts +327 -0
- package/packages/runtime/tsconfig.json +20 -0
- package/packages/runtime/wasm/aeon_pages_runtime.d.ts +504 -0
- package/packages/runtime/wasm/aeon_pages_runtime.js +1657 -0
- package/packages/runtime/wasm/aeon_pages_runtime_bg.wasm +0 -0
- package/packages/runtime/wasm/aeon_pages_runtime_bg.wasm.d.ts +196 -0
- package/packages/runtime/wasm/package.json +21 -0
- package/packages/runtime/wrangler.toml +41 -0
- package/packages/runtime-wasm/Cargo.lock +436 -0
- package/packages/runtime-wasm/Cargo.toml +29 -0
- package/packages/runtime-wasm/pkg/aeon_pages_runtime.d.ts +480 -0
- package/packages/runtime-wasm/pkg/aeon_pages_runtime.js +1568 -0
- package/packages/runtime-wasm/pkg/aeon_pages_runtime_bg.wasm +0 -0
- package/packages/runtime-wasm/pkg/aeon_pages_runtime_bg.wasm.d.ts +192 -0
- package/packages/runtime-wasm/pkg/package.json +21 -0
- package/packages/runtime-wasm/src/hydrate.rs +352 -0
- package/packages/runtime-wasm/src/lib.rs +191 -0
- package/packages/runtime-wasm/src/render.rs +629 -0
- package/packages/runtime-wasm/src/router.rs +298 -0
- package/packages/runtime-wasm/src/skeleton.rs +430 -0
- package/rfcs/RFC-001-ZERO-DEPENDENCY-RENDERING.md +1446 -0
|
@@ -0,0 +1,565 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Aeon Pages Conflict Resolver
|
|
3
|
+
*
|
|
4
|
+
* Handles conflict detection and resolution for offline-first applications.
|
|
5
|
+
* Optimized for edge environments with configurable resolution strategies.
|
|
6
|
+
*
|
|
7
|
+
* Features:
|
|
8
|
+
* - Multiple resolution strategies (local-wins, remote-wins, merge, last-modified)
|
|
9
|
+
* - Automatic resolution for low-severity conflicts
|
|
10
|
+
* - Manual resolution queue for high-severity conflicts
|
|
11
|
+
* - Conflict statistics and history
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type {
|
|
15
|
+
OfflineOperation,
|
|
16
|
+
StoredConflict,
|
|
17
|
+
ConflictDetectionResult,
|
|
18
|
+
ResolutionStrategy,
|
|
19
|
+
SyncCoordinatorEvents,
|
|
20
|
+
} from '../offline/types';
|
|
21
|
+
|
|
22
|
+
// ============================================================================
|
|
23
|
+
// Types
|
|
24
|
+
// ============================================================================
|
|
25
|
+
|
|
26
|
+
export interface ConflictResolverConfig {
|
|
27
|
+
/** Default resolution strategy */
|
|
28
|
+
defaultStrategy: ResolutionStrategy;
|
|
29
|
+
|
|
30
|
+
/** Enable automatic merging for similar updates */
|
|
31
|
+
enableAutoMerge: boolean;
|
|
32
|
+
|
|
33
|
+
/** Enable local-wins fallback */
|
|
34
|
+
enableLocalWins: boolean;
|
|
35
|
+
|
|
36
|
+
/** Maximum conflicts to cache */
|
|
37
|
+
maxConflictCacheSize: number;
|
|
38
|
+
|
|
39
|
+
/** Conflict resolution timeout in ms */
|
|
40
|
+
conflictTimeoutMs: number;
|
|
41
|
+
|
|
42
|
+
/** Similarity threshold (0-100) for auto-merge */
|
|
43
|
+
mergeThreshold: number;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface ConflictStats {
|
|
47
|
+
totalConflicts: number;
|
|
48
|
+
resolvedConflicts: number;
|
|
49
|
+
unresolvedConflicts: number;
|
|
50
|
+
conflictsByType: {
|
|
51
|
+
update_update: number;
|
|
52
|
+
delete_update: number;
|
|
53
|
+
update_delete: number;
|
|
54
|
+
concurrent: number;
|
|
55
|
+
};
|
|
56
|
+
resolutionsByStrategy: {
|
|
57
|
+
'local-wins': number;
|
|
58
|
+
'remote-wins': number;
|
|
59
|
+
merge: number;
|
|
60
|
+
manual: number;
|
|
61
|
+
'last-modified': number;
|
|
62
|
+
};
|
|
63
|
+
averageResolutionTimeMs: number;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ============================================================================
|
|
67
|
+
// Event Emitter (minimal implementation)
|
|
68
|
+
// ============================================================================
|
|
69
|
+
|
|
70
|
+
type EventHandler<T> = (data: T) => void;
|
|
71
|
+
|
|
72
|
+
class EventEmitter<
|
|
73
|
+
Events extends Record<string, unknown> = Record<string, unknown>,
|
|
74
|
+
> {
|
|
75
|
+
private handlers = new Map<string, Set<EventHandler<unknown>>>();
|
|
76
|
+
|
|
77
|
+
on<K extends keyof Events>(event: K, handler: EventHandler<Events[K]>): void {
|
|
78
|
+
const key = event as string;
|
|
79
|
+
if (!this.handlers.has(key)) {
|
|
80
|
+
this.handlers.set(key, new Set());
|
|
81
|
+
}
|
|
82
|
+
this.handlers.get(key)!.add(handler as EventHandler<unknown>);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
off<K extends keyof Events>(
|
|
86
|
+
event: K,
|
|
87
|
+
handler: EventHandler<Events[K]>,
|
|
88
|
+
): void {
|
|
89
|
+
this.handlers
|
|
90
|
+
.get(event as string)
|
|
91
|
+
?.delete(handler as EventHandler<unknown>);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
emit<K extends keyof Events>(event: K, data?: Events[K]): void {
|
|
95
|
+
this.handlers.get(event as string)?.forEach((handler) => handler(data));
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ============================================================================
|
|
100
|
+
// Default Configuration
|
|
101
|
+
// ============================================================================
|
|
102
|
+
|
|
103
|
+
const DEFAULT_CONFIG: ConflictResolverConfig = {
|
|
104
|
+
defaultStrategy: 'last-modified',
|
|
105
|
+
enableAutoMerge: true,
|
|
106
|
+
enableLocalWins: true,
|
|
107
|
+
maxConflictCacheSize: 1000,
|
|
108
|
+
conflictTimeoutMs: 30000,
|
|
109
|
+
mergeThreshold: 70,
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
// ============================================================================
|
|
113
|
+
// Conflict Resolver
|
|
114
|
+
// ============================================================================
|
|
115
|
+
|
|
116
|
+
export class ConflictResolver extends EventEmitter<{
|
|
117
|
+
'conflict:detected': StoredConflict;
|
|
118
|
+
'conflict:resolved': {
|
|
119
|
+
conflict: StoredConflict;
|
|
120
|
+
strategy: ResolutionStrategy;
|
|
121
|
+
};
|
|
122
|
+
'config:updated': ConflictResolverConfig;
|
|
123
|
+
}> {
|
|
124
|
+
private conflicts: Map<string, StoredConflict> = new Map();
|
|
125
|
+
private conflictsByEntity: Map<string, string[]> = new Map();
|
|
126
|
+
private config: ConflictResolverConfig;
|
|
127
|
+
private resolutionTimings: number[] = [];
|
|
128
|
+
|
|
129
|
+
private stats: ConflictStats = {
|
|
130
|
+
totalConflicts: 0,
|
|
131
|
+
resolvedConflicts: 0,
|
|
132
|
+
unresolvedConflicts: 0,
|
|
133
|
+
conflictsByType: {
|
|
134
|
+
update_update: 0,
|
|
135
|
+
delete_update: 0,
|
|
136
|
+
update_delete: 0,
|
|
137
|
+
concurrent: 0,
|
|
138
|
+
},
|
|
139
|
+
resolutionsByStrategy: {
|
|
140
|
+
'local-wins': 0,
|
|
141
|
+
'remote-wins': 0,
|
|
142
|
+
merge: 0,
|
|
143
|
+
manual: 0,
|
|
144
|
+
'last-modified': 0,
|
|
145
|
+
},
|
|
146
|
+
averageResolutionTimeMs: 0,
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
constructor(config: Partial<ConflictResolverConfig> = {}) {
|
|
150
|
+
super();
|
|
151
|
+
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Detect conflicts between local and remote operations
|
|
156
|
+
*/
|
|
157
|
+
detectConflict(
|
|
158
|
+
localOp: OfflineOperation,
|
|
159
|
+
remoteOp: OfflineOperation,
|
|
160
|
+
): StoredConflict | null {
|
|
161
|
+
// Same session, same or overlapping data = potential conflict
|
|
162
|
+
if (localOp.sessionId !== remoteOp.sessionId) {
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Determine conflict type
|
|
167
|
+
const isLocalDelete = localOp.type.includes('delete');
|
|
168
|
+
const isRemoteDelete = remoteOp.type.includes('delete');
|
|
169
|
+
|
|
170
|
+
if (isLocalDelete && isRemoteDelete) {
|
|
171
|
+
// Both deleted - not a real conflict
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
let conflictType: ConflictDetectionResult['type'];
|
|
176
|
+
if (isLocalDelete && !isRemoteDelete) {
|
|
177
|
+
conflictType = 'delete_update';
|
|
178
|
+
} else if (!isLocalDelete && isRemoteDelete) {
|
|
179
|
+
conflictType = 'update_delete';
|
|
180
|
+
} else if (!isLocalDelete && !isRemoteDelete) {
|
|
181
|
+
conflictType = 'update_update';
|
|
182
|
+
} else {
|
|
183
|
+
conflictType = 'concurrent';
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Calculate severity
|
|
187
|
+
const severity = this.calculateSeverity(conflictType, localOp, remoteOp);
|
|
188
|
+
|
|
189
|
+
// Find conflicting fields
|
|
190
|
+
const conflictingFields = this.findConflictingFields(
|
|
191
|
+
localOp.data,
|
|
192
|
+
remoteOp.data,
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
const conflict: StoredConflict = {
|
|
196
|
+
id: `conflict-${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
|
197
|
+
operationId: localOp.id,
|
|
198
|
+
sessionId: localOp.sessionId,
|
|
199
|
+
localData: localOp.data,
|
|
200
|
+
remoteData: remoteOp.data,
|
|
201
|
+
type: conflictType,
|
|
202
|
+
severity,
|
|
203
|
+
detectedAt: Date.now(),
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
this.conflicts.set(conflict.id, conflict);
|
|
207
|
+
|
|
208
|
+
// Track by entity
|
|
209
|
+
const entityKey = `${localOp.sessionId}`;
|
|
210
|
+
if (!this.conflictsByEntity.has(entityKey)) {
|
|
211
|
+
this.conflictsByEntity.set(entityKey, []);
|
|
212
|
+
}
|
|
213
|
+
this.conflictsByEntity.get(entityKey)!.push(conflict.id);
|
|
214
|
+
|
|
215
|
+
// Update stats
|
|
216
|
+
this.stats.totalConflicts++;
|
|
217
|
+
if (conflictType) {
|
|
218
|
+
this.stats.conflictsByType[conflictType]++;
|
|
219
|
+
}
|
|
220
|
+
this.stats.unresolvedConflicts++;
|
|
221
|
+
|
|
222
|
+
this.emit('conflict:detected', conflict);
|
|
223
|
+
|
|
224
|
+
// Try auto-resolution for low severity
|
|
225
|
+
if (this.shouldAutoResolve(conflict)) {
|
|
226
|
+
this.resolveConflict(conflict.id, this.config.defaultStrategy);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return conflict;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Calculate conflict severity
|
|
234
|
+
*/
|
|
235
|
+
private calculateSeverity(
|
|
236
|
+
conflictType: ConflictDetectionResult['type'],
|
|
237
|
+
localOp: OfflineOperation,
|
|
238
|
+
remoteOp: OfflineOperation,
|
|
239
|
+
): 'low' | 'medium' | 'high' {
|
|
240
|
+
// Delete conflicts are high severity
|
|
241
|
+
if (conflictType === 'delete_update' || conflictType === 'update_delete') {
|
|
242
|
+
return 'high';
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Update-update conflicts with significant data differences are high severity
|
|
246
|
+
if (conflictType === 'update_update') {
|
|
247
|
+
const similarity = this.calculateDataSimilarity(
|
|
248
|
+
localOp.data,
|
|
249
|
+
remoteOp.data,
|
|
250
|
+
);
|
|
251
|
+
if (similarity < 30) {
|
|
252
|
+
return 'high';
|
|
253
|
+
} else if (similarity < 60) {
|
|
254
|
+
return 'medium';
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return 'low';
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Calculate data similarity (0-100)
|
|
263
|
+
*/
|
|
264
|
+
private calculateDataSimilarity(data1: unknown, data2: unknown): number {
|
|
265
|
+
if (data1 === data2) return 100;
|
|
266
|
+
if (!data1 || !data2) return 0;
|
|
267
|
+
|
|
268
|
+
try {
|
|
269
|
+
const str1 = JSON.stringify(data1);
|
|
270
|
+
const str2 = JSON.stringify(data2);
|
|
271
|
+
|
|
272
|
+
// Simple character overlap calculation
|
|
273
|
+
const commonChars = Array.from(str1).filter((char) =>
|
|
274
|
+
str2.includes(char),
|
|
275
|
+
).length;
|
|
276
|
+
return Math.round(
|
|
277
|
+
(commonChars / Math.max(str1.length, str2.length)) * 100,
|
|
278
|
+
);
|
|
279
|
+
} catch {
|
|
280
|
+
return 0;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Find conflicting fields between two data objects
|
|
286
|
+
*/
|
|
287
|
+
private findConflictingFields(
|
|
288
|
+
data1: Record<string, unknown>,
|
|
289
|
+
data2: Record<string, unknown>,
|
|
290
|
+
): string[] {
|
|
291
|
+
const conflicts: string[] = [];
|
|
292
|
+
const allKeys = new Set([...Object.keys(data1), ...Object.keys(data2)]);
|
|
293
|
+
|
|
294
|
+
Array.from(allKeys).forEach((key) => {
|
|
295
|
+
const val1 = data1[key];
|
|
296
|
+
const val2 = data2[key];
|
|
297
|
+
|
|
298
|
+
if (JSON.stringify(val1) !== JSON.stringify(val2)) {
|
|
299
|
+
conflicts.push(key);
|
|
300
|
+
}
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
return conflicts;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Determine if conflict should auto-resolve
|
|
308
|
+
*/
|
|
309
|
+
private shouldAutoResolve(conflict: StoredConflict): boolean {
|
|
310
|
+
// Auto-resolve low severity conflicts
|
|
311
|
+
if (conflict.severity === 'low') {
|
|
312
|
+
return true;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Auto-resolve similar updates
|
|
316
|
+
if (conflict.type === 'update_update') {
|
|
317
|
+
const similarity = this.calculateDataSimilarity(
|
|
318
|
+
conflict.localData,
|
|
319
|
+
conflict.remoteData,
|
|
320
|
+
);
|
|
321
|
+
return similarity > this.config.mergeThreshold;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
return false;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Resolve a conflict using specified strategy
|
|
329
|
+
*/
|
|
330
|
+
resolveConflict(
|
|
331
|
+
conflictId: string,
|
|
332
|
+
strategy?: ResolutionStrategy,
|
|
333
|
+
): StoredConflict['resolution'] | null {
|
|
334
|
+
const conflict = this.conflicts.get(conflictId);
|
|
335
|
+
if (!conflict) {
|
|
336
|
+
return null;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const startTime = Date.now();
|
|
340
|
+
const selectedStrategy = strategy || this.config.defaultStrategy;
|
|
341
|
+
let resolvedData: Record<string, unknown>;
|
|
342
|
+
let winner: 'local' | 'remote' | 'merged' | undefined;
|
|
343
|
+
|
|
344
|
+
switch (selectedStrategy) {
|
|
345
|
+
case 'local-wins':
|
|
346
|
+
resolvedData = conflict.localData;
|
|
347
|
+
winner = 'local';
|
|
348
|
+
break;
|
|
349
|
+
|
|
350
|
+
case 'remote-wins':
|
|
351
|
+
resolvedData = conflict.remoteData;
|
|
352
|
+
winner = 'remote';
|
|
353
|
+
break;
|
|
354
|
+
|
|
355
|
+
case 'last-modified':
|
|
356
|
+
// Default to local if we can't determine timestamps
|
|
357
|
+
resolvedData = conflict.localData;
|
|
358
|
+
winner = 'local';
|
|
359
|
+
break;
|
|
360
|
+
|
|
361
|
+
case 'merge':
|
|
362
|
+
if (this.config.enableAutoMerge && conflict.type === 'update_update') {
|
|
363
|
+
resolvedData = this.attemptMerge(
|
|
364
|
+
conflict.localData,
|
|
365
|
+
conflict.remoteData,
|
|
366
|
+
);
|
|
367
|
+
winner = 'merged';
|
|
368
|
+
} else {
|
|
369
|
+
// Fall back to local-wins
|
|
370
|
+
resolvedData = conflict.localData;
|
|
371
|
+
winner = 'local';
|
|
372
|
+
}
|
|
373
|
+
break;
|
|
374
|
+
|
|
375
|
+
case 'manual':
|
|
376
|
+
// Manual resolution - don't auto-resolve
|
|
377
|
+
return null;
|
|
378
|
+
|
|
379
|
+
default:
|
|
380
|
+
resolvedData = conflict.localData;
|
|
381
|
+
winner = 'local';
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
const resolution: StoredConflict['resolution'] = {
|
|
385
|
+
strategy: selectedStrategy,
|
|
386
|
+
resolvedData,
|
|
387
|
+
resolvedAt: Date.now(),
|
|
388
|
+
};
|
|
389
|
+
|
|
390
|
+
conflict.resolution = resolution;
|
|
391
|
+
|
|
392
|
+
// Update stats
|
|
393
|
+
this.stats.resolvedConflicts++;
|
|
394
|
+
this.stats.unresolvedConflicts--;
|
|
395
|
+
this.stats.resolutionsByStrategy[selectedStrategy]++;
|
|
396
|
+
|
|
397
|
+
const resolutionTime = Date.now() - startTime;
|
|
398
|
+
this.resolutionTimings.push(resolutionTime);
|
|
399
|
+
if (this.resolutionTimings.length > 100) {
|
|
400
|
+
this.resolutionTimings.shift();
|
|
401
|
+
}
|
|
402
|
+
this.stats.averageResolutionTimeMs =
|
|
403
|
+
this.resolutionTimings.reduce((a, b) => a + b, 0) /
|
|
404
|
+
this.resolutionTimings.length;
|
|
405
|
+
|
|
406
|
+
this.emit('conflict:resolved', { conflict, strategy: selectedStrategy });
|
|
407
|
+
return resolution;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Attempt to merge conflicting data
|
|
412
|
+
*/
|
|
413
|
+
private attemptMerge(
|
|
414
|
+
data1: Record<string, unknown>,
|
|
415
|
+
data2: Record<string, unknown>,
|
|
416
|
+
): Record<string, unknown> {
|
|
417
|
+
const merged: Record<string, unknown> = { ...data1 };
|
|
418
|
+
|
|
419
|
+
// Merge non-conflicting fields from data2
|
|
420
|
+
for (const key of Object.keys(data2)) {
|
|
421
|
+
if (!(key in merged)) {
|
|
422
|
+
merged[key] = data2[key];
|
|
423
|
+
} else if (
|
|
424
|
+
typeof merged[key] === 'object' &&
|
|
425
|
+
merged[key] !== null &&
|
|
426
|
+
typeof data2[key] === 'object' &&
|
|
427
|
+
data2[key] !== null
|
|
428
|
+
) {
|
|
429
|
+
// Recursive merge for nested objects
|
|
430
|
+
merged[key] = this.attemptMerge(
|
|
431
|
+
merged[key] as Record<string, unknown>,
|
|
432
|
+
data2[key] as Record<string, unknown>,
|
|
433
|
+
);
|
|
434
|
+
}
|
|
435
|
+
// For conflicting primitive values, keep local (data1)
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
return merged;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* Get conflict by ID
|
|
443
|
+
*/
|
|
444
|
+
getConflict(conflictId: string): StoredConflict | undefined {
|
|
445
|
+
return this.conflicts.get(conflictId);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/**
|
|
449
|
+
* Get all unresolved conflicts
|
|
450
|
+
*/
|
|
451
|
+
getUnresolvedConflicts(): StoredConflict[] {
|
|
452
|
+
return Array.from(this.conflicts.values()).filter((c) => !c.resolution);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* Get conflicts for a session
|
|
457
|
+
*/
|
|
458
|
+
getConflictsForSession(sessionId: string): StoredConflict[] {
|
|
459
|
+
const conflictIds = this.conflictsByEntity.get(sessionId) || [];
|
|
460
|
+
return conflictIds
|
|
461
|
+
.map((id) => this.conflicts.get(id))
|
|
462
|
+
.filter((c): c is StoredConflict => c !== undefined);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* Get high severity unresolved conflicts
|
|
467
|
+
*/
|
|
468
|
+
getHighSeverityConflicts(): StoredConflict[] {
|
|
469
|
+
return Array.from(this.conflicts.values()).filter(
|
|
470
|
+
(c) => !c.resolution && c.severity === 'high',
|
|
471
|
+
);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* Get statistics
|
|
476
|
+
*/
|
|
477
|
+
getStats(): ConflictStats {
|
|
478
|
+
return { ...this.stats };
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* Configure resolver
|
|
483
|
+
*/
|
|
484
|
+
configure(config: Partial<ConflictResolverConfig>): void {
|
|
485
|
+
this.config = { ...this.config, ...config };
|
|
486
|
+
this.emit('config:updated', this.config);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* Get current configuration
|
|
491
|
+
*/
|
|
492
|
+
getConfig(): ConflictResolverConfig {
|
|
493
|
+
return { ...this.config };
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* Clear all conflicts
|
|
498
|
+
*/
|
|
499
|
+
clear(): void {
|
|
500
|
+
this.conflicts.clear();
|
|
501
|
+
this.conflictsByEntity.clear();
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* Reset service (for testing)
|
|
506
|
+
*/
|
|
507
|
+
reset(): void {
|
|
508
|
+
this.clear();
|
|
509
|
+
this.resolutionTimings = [];
|
|
510
|
+
this.stats = {
|
|
511
|
+
totalConflicts: 0,
|
|
512
|
+
resolvedConflicts: 0,
|
|
513
|
+
unresolvedConflicts: 0,
|
|
514
|
+
conflictsByType: {
|
|
515
|
+
update_update: 0,
|
|
516
|
+
delete_update: 0,
|
|
517
|
+
update_delete: 0,
|
|
518
|
+
concurrent: 0,
|
|
519
|
+
},
|
|
520
|
+
resolutionsByStrategy: {
|
|
521
|
+
'local-wins': 0,
|
|
522
|
+
'remote-wins': 0,
|
|
523
|
+
merge: 0,
|
|
524
|
+
manual: 0,
|
|
525
|
+
'last-modified': 0,
|
|
526
|
+
},
|
|
527
|
+
averageResolutionTimeMs: 0,
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// ============================================================================
|
|
533
|
+
// Singleton Instance
|
|
534
|
+
// ============================================================================
|
|
535
|
+
|
|
536
|
+
let _instance: ConflictResolver | null = null;
|
|
537
|
+
|
|
538
|
+
/**
|
|
539
|
+
* Get the singleton conflict resolver instance
|
|
540
|
+
*/
|
|
541
|
+
export function getConflictResolver(): ConflictResolver {
|
|
542
|
+
if (!_instance) {
|
|
543
|
+
_instance = new ConflictResolver();
|
|
544
|
+
}
|
|
545
|
+
return _instance;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
/**
|
|
549
|
+
* Create a new conflict resolver with custom configuration
|
|
550
|
+
*/
|
|
551
|
+
export function createConflictResolver(
|
|
552
|
+
config?: Partial<ConflictResolverConfig>,
|
|
553
|
+
): ConflictResolver {
|
|
554
|
+
return new ConflictResolver(config);
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
/**
|
|
558
|
+
* Reset the singleton resolver (for testing)
|
|
559
|
+
*/
|
|
560
|
+
export function resetConflictResolver(): void {
|
|
561
|
+
if (_instance) {
|
|
562
|
+
_instance.reset();
|
|
563
|
+
}
|
|
564
|
+
_instance = null;
|
|
565
|
+
}
|