@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.
Files changed (124) hide show
  1. package/CHANGELOG.md +112 -0
  2. package/README.md +625 -0
  3. package/examples/basic/aeon.config.ts +39 -0
  4. package/examples/basic/components/Cursor.tsx +86 -0
  5. package/examples/basic/components/OfflineIndicator.tsx +103 -0
  6. package/examples/basic/components/PresenceBar.tsx +77 -0
  7. package/examples/basic/package.json +20 -0
  8. package/examples/basic/pages/index.tsx +80 -0
  9. package/package.json +101 -0
  10. package/packages/analytics/README.md +309 -0
  11. package/packages/analytics/build.ts +35 -0
  12. package/packages/analytics/package.json +50 -0
  13. package/packages/analytics/src/click-tracker.ts +368 -0
  14. package/packages/analytics/src/context-bridge.ts +319 -0
  15. package/packages/analytics/src/data-layer.ts +302 -0
  16. package/packages/analytics/src/gtm-loader.ts +239 -0
  17. package/packages/analytics/src/index.ts +230 -0
  18. package/packages/analytics/src/merkle-tree.ts +489 -0
  19. package/packages/analytics/src/provider.tsx +300 -0
  20. package/packages/analytics/src/types.ts +320 -0
  21. package/packages/analytics/src/use-analytics.ts +296 -0
  22. package/packages/analytics/tsconfig.json +19 -0
  23. package/packages/benchmarks/src/benchmark.test.ts +691 -0
  24. package/packages/cli/dist/index.js +61899 -0
  25. package/packages/cli/package.json +43 -0
  26. package/packages/cli/src/commands/build.test.ts +682 -0
  27. package/packages/cli/src/commands/build.ts +890 -0
  28. package/packages/cli/src/commands/dev.ts +473 -0
  29. package/packages/cli/src/commands/init.ts +409 -0
  30. package/packages/cli/src/commands/start.ts +297 -0
  31. package/packages/cli/src/index.ts +105 -0
  32. package/packages/directives/src/use-aeon.ts +272 -0
  33. package/packages/mcp-server/package.json +51 -0
  34. package/packages/mcp-server/src/index.ts +178 -0
  35. package/packages/mcp-server/src/resources.ts +346 -0
  36. package/packages/mcp-server/src/tools/index.ts +36 -0
  37. package/packages/mcp-server/src/tools/navigation.ts +545 -0
  38. package/packages/mcp-server/tsconfig.json +21 -0
  39. package/packages/react/package.json +40 -0
  40. package/packages/react/src/Link.tsx +388 -0
  41. package/packages/react/src/components/InstallPrompt.tsx +286 -0
  42. package/packages/react/src/components/OfflineDiagnostics.tsx +677 -0
  43. package/packages/react/src/components/PushNotifications.tsx +453 -0
  44. package/packages/react/src/hooks/useAeonNavigation.ts +219 -0
  45. package/packages/react/src/hooks/useConflicts.ts +277 -0
  46. package/packages/react/src/hooks/useNetworkState.ts +209 -0
  47. package/packages/react/src/hooks/usePilotNavigation.ts +254 -0
  48. package/packages/react/src/hooks/useServiceWorker.ts +278 -0
  49. package/packages/react/src/hooks.ts +195 -0
  50. package/packages/react/src/index.ts +151 -0
  51. package/packages/react/src/provider.tsx +467 -0
  52. package/packages/react/tsconfig.json +19 -0
  53. package/packages/runtime/README.md +399 -0
  54. package/packages/runtime/build.ts +48 -0
  55. package/packages/runtime/package.json +71 -0
  56. package/packages/runtime/schema.sql +40 -0
  57. package/packages/runtime/src/api-routes.ts +465 -0
  58. package/packages/runtime/src/benchmark.ts +171 -0
  59. package/packages/runtime/src/cache.ts +479 -0
  60. package/packages/runtime/src/durable-object.ts +1341 -0
  61. package/packages/runtime/src/index.ts +360 -0
  62. package/packages/runtime/src/navigation.test.ts +421 -0
  63. package/packages/runtime/src/navigation.ts +422 -0
  64. package/packages/runtime/src/nextjs-adapter.ts +272 -0
  65. package/packages/runtime/src/offline/encrypted-queue.test.ts +607 -0
  66. package/packages/runtime/src/offline/encrypted-queue.ts +478 -0
  67. package/packages/runtime/src/offline/encryption.test.ts +412 -0
  68. package/packages/runtime/src/offline/encryption.ts +397 -0
  69. package/packages/runtime/src/offline/types.ts +465 -0
  70. package/packages/runtime/src/predictor.ts +371 -0
  71. package/packages/runtime/src/registry.ts +351 -0
  72. package/packages/runtime/src/router/context-extractor.ts +661 -0
  73. package/packages/runtime/src/router/esi-control-react.tsx +2053 -0
  74. package/packages/runtime/src/router/esi-control.ts +541 -0
  75. package/packages/runtime/src/router/esi-cyrano.ts +779 -0
  76. package/packages/runtime/src/router/esi-format-react.tsx +1744 -0
  77. package/packages/runtime/src/router/esi-react.tsx +1065 -0
  78. package/packages/runtime/src/router/esi-translate-observer.ts +476 -0
  79. package/packages/runtime/src/router/esi-translate-react.tsx +556 -0
  80. package/packages/runtime/src/router/esi-translate.ts +503 -0
  81. package/packages/runtime/src/router/esi.ts +666 -0
  82. package/packages/runtime/src/router/heuristic-adapter.test.ts +295 -0
  83. package/packages/runtime/src/router/heuristic-adapter.ts +557 -0
  84. package/packages/runtime/src/router/index.ts +298 -0
  85. package/packages/runtime/src/router/merkle-capability.ts +473 -0
  86. package/packages/runtime/src/router/speculation.ts +451 -0
  87. package/packages/runtime/src/router/types.ts +630 -0
  88. package/packages/runtime/src/router.test.ts +470 -0
  89. package/packages/runtime/src/router.ts +302 -0
  90. package/packages/runtime/src/server.ts +481 -0
  91. package/packages/runtime/src/service-worker-push.ts +319 -0
  92. package/packages/runtime/src/service-worker.ts +553 -0
  93. package/packages/runtime/src/skeleton-hydrate.ts +237 -0
  94. package/packages/runtime/src/speculation.test.ts +389 -0
  95. package/packages/runtime/src/speculation.ts +486 -0
  96. package/packages/runtime/src/storage.test.ts +1297 -0
  97. package/packages/runtime/src/storage.ts +1048 -0
  98. package/packages/runtime/src/sync/conflict-resolver.test.ts +528 -0
  99. package/packages/runtime/src/sync/conflict-resolver.ts +565 -0
  100. package/packages/runtime/src/sync/coordinator.test.ts +608 -0
  101. package/packages/runtime/src/sync/coordinator.ts +596 -0
  102. package/packages/runtime/src/tree-compiler.ts +295 -0
  103. package/packages/runtime/src/types.ts +728 -0
  104. package/packages/runtime/src/worker.ts +327 -0
  105. package/packages/runtime/tsconfig.json +20 -0
  106. package/packages/runtime/wasm/aeon_pages_runtime.d.ts +504 -0
  107. package/packages/runtime/wasm/aeon_pages_runtime.js +1657 -0
  108. package/packages/runtime/wasm/aeon_pages_runtime_bg.wasm +0 -0
  109. package/packages/runtime/wasm/aeon_pages_runtime_bg.wasm.d.ts +196 -0
  110. package/packages/runtime/wasm/package.json +21 -0
  111. package/packages/runtime/wrangler.toml +41 -0
  112. package/packages/runtime-wasm/Cargo.lock +436 -0
  113. package/packages/runtime-wasm/Cargo.toml +29 -0
  114. package/packages/runtime-wasm/pkg/aeon_pages_runtime.d.ts +480 -0
  115. package/packages/runtime-wasm/pkg/aeon_pages_runtime.js +1568 -0
  116. package/packages/runtime-wasm/pkg/aeon_pages_runtime_bg.wasm +0 -0
  117. package/packages/runtime-wasm/pkg/aeon_pages_runtime_bg.wasm.d.ts +192 -0
  118. package/packages/runtime-wasm/pkg/package.json +21 -0
  119. package/packages/runtime-wasm/src/hydrate.rs +352 -0
  120. package/packages/runtime-wasm/src/lib.rs +191 -0
  121. package/packages/runtime-wasm/src/render.rs +629 -0
  122. package/packages/runtime-wasm/src/router.rs +298 -0
  123. package/packages/runtime-wasm/src/skeleton.rs +430 -0
  124. 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
+ }