leptos_sync_core/sync/
conflict.rs

1//! Advanced conflict resolution strategies for CRDT synchronization
2
3use crate::crdt::{Mergeable, ReplicaId};
4use chrono::{DateTime, Utc};
5use serde::{Deserialize, Serialize};
6use std::collections::HashMap;
7use thiserror::Error;
8
9#[derive(Error, Debug)]
10pub enum ConflictResolutionError {
11    #[error("Unresolvable conflict: {0}")]
12    Unresolvable(String),
13    #[error("Strategy not applicable: {0}")]
14    StrategyNotApplicable(String),
15    #[error("Invalid conflict data: {0}")]
16    InvalidData(String),
17}
18
19/// Conflict resolution strategy
20#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
21pub enum ConflictStrategy {
22    /// Last-Write-Wins (default)
23    LastWriteWins,
24    /// First-Write-Wins
25    FirstWriteWins,
26    /// Custom merge function
27    CustomMerge,
28    /// Manual resolution required
29    ManualResolution,
30    /// Conflict avoidance (prevent conflicts)
31    ConflictAvoidance,
32}
33
34/// Conflict metadata
35#[derive(Debug, Clone, Serialize, Deserialize)]
36pub struct ConflictMetadata {
37    pub replica_id: ReplicaId,
38    pub timestamp: DateTime<Utc>,
39    pub version: u64,
40    pub conflict_type: String,
41    pub resolution_strategy: ConflictStrategy,
42}
43
44/// Conflict resolution result
45#[derive(Debug, Clone, Serialize, Deserialize)]
46pub struct ConflictResolution<T> {
47    pub resolved_value: T,
48    pub strategy_used: ConflictStrategy,
49    pub metadata: ConflictMetadata,
50    pub conflicts_resolved: usize,
51}
52
53/// Advanced conflict resolver
54pub struct AdvancedConflictResolver {
55    strategies: HashMap<String, Box<dyn ConflictResolutionStrategy + Send + Sync>>,
56    default_strategy: ConflictStrategy,
57    conflict_history: Vec<ConflictMetadata>,
58}
59
60impl AdvancedConflictResolver {
61    pub fn new() -> Self {
62        let mut resolver = Self {
63            strategies: HashMap::new(),
64            default_strategy: ConflictStrategy::LastWriteWins,
65            conflict_history: Vec::new(),
66        };
67        
68        // Register default strategies
69        resolver.register_strategy("lww", Box::new(LastWriteWinsStrategy));
70        resolver.register_strategy("fww", Box::new(FirstWriteWinsStrategy));
71        resolver.register_strategy("custom", Box::new(CustomMergeStrategy));
72        
73        resolver
74    }
75
76    pub fn with_default_strategy(mut self, strategy: ConflictStrategy) -> Self {
77        self.default_strategy = strategy;
78        self
79    }
80
81    pub fn register_strategy(&mut self, name: &str, strategy: Box<dyn ConflictResolutionStrategy + Send + Sync>) {
82        self.strategies.insert(name.to_string(), strategy);
83    }
84
85    pub async fn resolve<T: Mergeable + Clone + Send + Sync>(
86        &mut self,
87        local: &T,
88        remote: &T,
89        metadata: Option<ConflictMetadata>,
90    ) -> Result<ConflictResolution<T>, ConflictResolutionError> {
91        let metadata = metadata.unwrap_or_else(|| ConflictMetadata {
92            replica_id: ReplicaId::default(),
93            timestamp: Utc::now(),
94            version: 1,
95            conflict_type: "default".to_string(),
96            resolution_strategy: self.default_strategy.clone(),
97        });
98
99        // Check if there's actually a conflict
100        if !self.has_conflict(local, remote, &metadata).await? {
101            return Ok(ConflictResolution {
102                resolved_value: local.clone(),
103                strategy_used: ConflictStrategy::LastWriteWins,
104                metadata,
105                conflicts_resolved: 0,
106            });
107        }
108
109        // Record conflict
110        self.conflict_history.push(metadata.clone());
111
112        // Apply resolution strategy
113        let strategy = &metadata.resolution_strategy;
114        let resolved_value = match strategy {
115            ConflictStrategy::LastWriteWins => {
116                self.resolve_last_write_wins(local, remote, &metadata).await?
117            }
118            ConflictStrategy::FirstWriteWins => {
119                self.resolve_first_write_wins(local, remote, &metadata).await?
120            }
121            ConflictStrategy::CustomMerge => {
122                self.resolve_custom_merge(local, remote, &metadata).await?
123            }
124            ConflictStrategy::ManualResolution => {
125                return Err(ConflictResolutionError::Unresolvable(
126                    "Manual resolution required".to_string()
127                ));
128            }
129            ConflictStrategy::ConflictAvoidance => {
130                self.resolve_conflict_avoidance(local, remote, &metadata).await?
131            }
132        };
133
134        Ok(ConflictResolution {
135            resolved_value,
136            strategy_used: strategy.clone(),
137            metadata,
138            conflicts_resolved: 1,
139        })
140    }
141
142    async fn has_conflict<T: Mergeable>(
143        &self,
144        local: &T,
145        remote: &T,
146        metadata: &ConflictMetadata,
147    ) -> Result<bool, ConflictResolutionError> {
148        // Use the CRDT's built-in conflict detection
149        Ok(local.has_conflict(remote))
150    }
151
152    async fn resolve_last_write_wins<T: Mergeable + Clone>(
153        &self,
154        local: &T,
155        remote: &T,
156        metadata: &ConflictMetadata,
157    ) -> Result<T, ConflictResolutionError> {
158        // Simple timestamp-based resolution
159        let mut result = local.clone();
160        result.merge(remote).map_err(|e| {
161            ConflictResolutionError::InvalidData(format!("Merge failed: {}", e))
162        })?;
163        Ok(result)
164    }
165
166    async fn resolve_first_write_wins<T: Mergeable + Clone>(
167        &self,
168        local: &T,
169        remote: &T,
170        metadata: &ConflictMetadata,
171    ) -> Result<T, ConflictResolutionError> {
172        // First-write-wins logic
173        let mut result = local.clone();
174        // In a real implementation, you'd check creation timestamps
175        // For now, just merge and return
176        result.merge(remote).map_err(|e| {
177            ConflictResolutionError::InvalidData(format!("Merge failed: {}", e))
178        })?;
179        Ok(result)
180    }
181
182    async fn resolve_custom_merge<T: Mergeable + Clone>(
183        &self,
184        local: &T,
185        remote: &T,
186        metadata: &ConflictMetadata,
187    ) -> Result<T, ConflictResolutionError> {
188        // Custom merge logic based on conflict type
189        match metadata.conflict_type.as_str() {
190            "text" => self.merge_text_conflicts(local, remote).await,
191            "numeric" => self.merge_numeric_conflicts(local, remote).await,
192            "list" => self.merge_list_conflicts(local, remote).await,
193            _ => self.resolve_last_write_wins(local, remote, metadata).await,
194        }
195    }
196
197    async fn resolve_conflict_avoidance<T: Mergeable + Clone>(
198        &self,
199        local: &T,
200        remote: &T,
201        metadata: &ConflictMetadata,
202    ) -> Result<T, ConflictResolutionError> {
203        // Try to avoid conflicts by using more sophisticated merging
204        let mut result = local.clone();
205        
206        // Attempt to merge without conflicts
207        if let Ok(()) = result.merge(remote) {
208            return Ok(result);
209        }
210
211        // If merge fails, fall back to last-write-wins
212        self.resolve_last_write_wins(local, remote, metadata).await
213    }
214
215    async fn merge_text_conflicts<T: Mergeable + Clone>(
216        &self,
217        local: &T,
218        remote: &T,
219    ) -> Result<T, ConflictResolutionError> {
220        // Text-specific merging logic would go here
221        // For now, fall back to last-write-wins
222        self.resolve_last_write_wins(local, remote, &ConflictMetadata {
223            replica_id: ReplicaId::default(),
224            timestamp: Utc::now(),
225            version: 1,
226            conflict_type: "text".to_string(),
227            resolution_strategy: ConflictStrategy::LastWriteWins,
228        }).await
229    }
230
231    async fn merge_numeric_conflicts<T: Mergeable + Clone>(
232        &self,
233        local: &T,
234        remote: &T,
235    ) -> Result<T, ConflictResolutionError> {
236        // Numeric-specific merging logic would go here
237        // For now, fall back to last-write-wins
238        self.resolve_last_write_wins(local, remote, &ConflictMetadata {
239            replica_id: ReplicaId::default(),
240            timestamp: Utc::now(),
241            version: 1,
242            conflict_type: "numeric".to_string(),
243            resolution_strategy: ConflictStrategy::LastWriteWins,
244        }).await
245    }
246
247    async fn merge_list_conflicts<T: Mergeable + Clone>(
248        &self,
249        local: &T,
250        remote: &T,
251    ) -> Result<T, ConflictResolutionError> {
252        // List-specific merging logic would go here
253        // For now, fall back to last-write-wins
254        self.resolve_last_write_wins(local, remote, &ConflictMetadata {
255            replica_id: ReplicaId::default(),
256            timestamp: Utc::now(),
257            version: 1,
258            conflict_type: "list".to_string(),
259            resolution_strategy: ConflictStrategy::LastWriteWins,
260        }).await
261    }
262
263    pub fn get_conflict_history(&self) -> &[ConflictMetadata] {
264        &self.conflict_history
265    }
266
267    pub fn clear_conflict_history(&mut self) {
268        self.conflict_history.clear();
269    }
270}
271
272impl Default for AdvancedConflictResolver {
273    fn default() -> Self {
274        Self::new()
275    }
276}
277
278/// Trait for custom conflict resolution strategies
279pub trait ConflictResolutionStrategy: Send + Sync {
280    fn name(&self) -> &str;
281    fn can_resolve(&self, conflict_type: &str) -> bool;
282}
283
284/// Last-Write-Wins strategy implementation
285pub struct LastWriteWinsStrategy;
286
287impl ConflictResolutionStrategy for LastWriteWinsStrategy {
288    fn name(&self) -> &str {
289        "last-write-wins"
290    }
291
292    fn can_resolve(&self, _conflict_type: &str) -> bool {
293        true // Can resolve any conflict type
294    }
295}
296
297/// First-Write-Wins strategy implementation
298pub struct FirstWriteWinsStrategy;
299
300impl ConflictResolutionStrategy for FirstWriteWinsStrategy {
301    fn name(&self) -> &str {
302        "first-write-wins"
303    }
304
305    fn can_resolve(&self, _conflict_type: &str) -> bool {
306        true // Can resolve any conflict type
307    }
308}
309
310/// Custom merge strategy implementation
311pub struct CustomMergeStrategy;
312
313impl ConflictResolutionStrategy for CustomMergeStrategy {
314    fn name(&self) -> &str {
315        "custom-merge"
316    }
317
318    fn can_resolve(&self, conflict_type: &str) -> bool {
319        matches!(conflict_type, "text" | "numeric" | "list")
320    }
321}
322
323#[cfg(test)]
324mod tests {
325    use super::*;
326    use crate::crdt::LwwRegister;
327
328    #[tokio::test]
329    async fn test_advanced_conflict_resolver_creation() {
330        let resolver = AdvancedConflictResolver::new();
331        assert_eq!(resolver.default_strategy, ConflictStrategy::LastWriteWins);
332    }
333
334    #[tokio::test]
335    async fn test_conflict_resolution_lww() {
336        let mut resolver = AdvancedConflictResolver::new();
337        let local = LwwRegister::new("local", ReplicaId::default());
338        let remote = LwwRegister::new("remote", ReplicaId::default());
339        
340        let metadata = ConflictMetadata {
341            replica_id: ReplicaId::default(),
342            timestamp: Utc::now(),
343            version: 1,
344            conflict_type: "text".to_string(),
345            resolution_strategy: ConflictStrategy::LastWriteWins,
346        };
347
348        let result = resolver.resolve(&local, &remote, Some(metadata)).await;
349        assert!(result.is_ok());
350    }
351
352    #[tokio::test]
353    async fn test_conflict_strategy_registration() {
354        let mut resolver = AdvancedConflictResolver::new();
355        let custom_strategy = Box::new(CustomMergeStrategy);
356        
357        resolver.register_strategy("custom", custom_strategy);
358        assert!(resolver.strategies.contains_key("custom"));
359    }
360
361    #[tokio::test]
362    async fn test_conflict_resolution_with_different_strategies() {
363        let mut resolver = AdvancedConflictResolver::new();
364        let local = LwwRegister::new("local", ReplicaId::default());
365        let remote = LwwRegister::new("remote", ReplicaId::default());
366        
367        // Test Last-Write-Wins strategy
368        let metadata_lww = ConflictMetadata {
369            replica_id: ReplicaId::default(),
370            timestamp: Utc::now(),
371            version: 1,
372            conflict_type: "text".to_string(),
373            resolution_strategy: ConflictStrategy::LastWriteWins,
374        };
375        let result_lww = resolver.resolve(&local, &remote, Some(metadata_lww)).await;
376        assert!(result_lww.is_ok());
377
378        // Test First-Write-Wins strategy
379        let metadata_fww = ConflictMetadata {
380            replica_id: ReplicaId::default(),
381            timestamp: Utc::now(),
382            version: 1,
383            conflict_type: "text".to_string(),
384            resolution_strategy: ConflictStrategy::FirstWriteWins,
385        };
386        let result_fww = resolver.resolve(&local, &remote, Some(metadata_fww)).await;
387        assert!(result_fww.is_ok());
388
389        // Test Custom Merge strategy
390        let metadata_custom = ConflictMetadata {
391            replica_id: ReplicaId::default(),
392            timestamp: Utc::now(),
393            version: 1,
394            conflict_type: "text".to_string(),
395            resolution_strategy: ConflictStrategy::CustomMerge,
396        };
397        let result_custom = resolver.resolve(&local, &remote, Some(metadata_custom)).await;
398        assert!(result_custom.is_ok());
399    }
400
401    #[tokio::test]
402    async fn test_conflict_history_tracking() {
403        let mut resolver = AdvancedConflictResolver::new();
404        
405        // Create registers with different replica IDs but same timestamp to force a conflict
406        let local_replica = ReplicaId::default();
407        let remote_replica = ReplicaId::default();
408        
409        // Create registers with the same timestamp to force a conflict
410        let now = Utc::now();
411        let local = LwwRegister::new("local", local_replica).with_timestamp(now);
412        let remote = LwwRegister::new("remote", remote_replica).with_timestamp(now);
413        
414        let metadata = ConflictMetadata {
415            replica_id: ReplicaId::default(),
416            timestamp: Utc::now(),
417            version: 1,
418            conflict_type: "text".to_string(),
419            resolution_strategy: ConflictStrategy::LastWriteWins,
420        };
421
422        // Initially no conflicts
423        assert_eq!(resolver.get_conflict_history().len(), 0);
424
425        // Resolve a conflict
426        let _result = resolver.resolve(&local, &remote, Some(metadata)).await;
427
428        // Should have one conflict in history
429        assert_eq!(resolver.get_conflict_history().len(), 1);
430
431        // Clear history
432        resolver.clear_conflict_history();
433        assert_eq!(resolver.get_conflict_history().len(), 0);
434    }
435
436    #[tokio::test]
437    async fn test_conflict_strategy_validation() {
438        let lww_strategy = LastWriteWinsStrategy;
439        let fww_strategy = FirstWriteWinsStrategy;
440        let custom_strategy = CustomMergeStrategy;
441
442        // All strategies should be able to resolve any conflict type
443        assert!(lww_strategy.can_resolve("text"));
444        assert!(lww_strategy.can_resolve("numeric"));
445        assert!(lww_strategy.can_resolve("list"));
446
447        assert!(fww_strategy.can_resolve("text"));
448        assert!(fww_strategy.can_resolve("numeric"));
449        assert!(fww_strategy.can_resolve("list"));
450
451        // Custom strategy should only resolve specific types
452        assert!(custom_strategy.can_resolve("text"));
453        assert!(custom_strategy.can_resolve("numeric"));
454        assert!(custom_strategy.can_resolve("list"));
455        assert!(!custom_strategy.can_resolve("unknown"));
456    }
457
458    #[tokio::test]
459    async fn test_conflict_metadata_serialization() {
460        let metadata = ConflictMetadata {
461            replica_id: ReplicaId::default(),
462            timestamp: Utc::now(),
463            version: 1,
464            conflict_type: "text".to_string(),
465            resolution_strategy: ConflictStrategy::LastWriteWins,
466        };
467
468        // Test serialization
469        let serialized = serde_json::to_string(&metadata);
470        assert!(serialized.is_ok());
471
472        // Test deserialization
473        let deserialized: ConflictMetadata = serde_json::from_str(&serialized.unwrap()).unwrap();
474        assert_eq!(deserialized.conflict_type, "text");
475        assert_eq!(deserialized.resolution_strategy, ConflictStrategy::LastWriteWins);
476    }
477}