Skip to main content

common/mount/conflict/
last_write_wins.rs

1//! Last-write-wins conflict resolver
2
3use crate::crypto::PublicKey;
4
5use super::types::{Conflict, Resolution};
6use super::ConflictResolver;
7
8/// Last-write-wins conflict resolution (default CRDT behavior)
9///
10/// The operation with the highest OpId wins:
11/// 1. Higher Lamport timestamp wins
12/// 2. If timestamps are equal, higher peer_id wins (lexicographic)
13///
14/// This is deterministic and matches the default PathOpLog behavior.
15#[derive(Debug, Clone, Default)]
16pub struct LastWriteWins;
17
18impl LastWriteWins {
19    /// Create a new LastWriteWins resolver
20    pub fn new() -> Self {
21        Self
22    }
23}
24
25impl ConflictResolver for LastWriteWins {
26    fn resolve(&self, conflict: &Conflict, _local_peer: &PublicKey) -> Resolution {
27        if conflict.incoming.id > conflict.base.id {
28            Resolution::UseIncoming
29        } else {
30            Resolution::UseBase
31        }
32    }
33}
34
35#[cfg(test)]
36mod tests {
37    use super::*;
38    use crate::crypto::SecretKey;
39    use crate::mount::path_ops::{OpId, OpType, PathOperation};
40    use std::path::PathBuf;
41
42    fn make_peer_id(seed: u8) -> PublicKey {
43        let mut seed_bytes = [0u8; 32];
44        seed_bytes[0] = seed;
45        let secret = SecretKey::from(seed_bytes);
46        secret.public()
47    }
48
49    fn make_op(peer_id: PublicKey, timestamp: u64, op_type: OpType, path: &str) -> PathOperation {
50        PathOperation {
51            id: OpId { timestamp, peer_id },
52            op_type,
53            path: PathBuf::from(path),
54            content_link: None,
55            is_dir: false,
56        }
57    }
58
59    #[test]
60    fn test_last_write_wins_incoming() {
61        let peer1 = make_peer_id(1);
62        let peer2 = make_peer_id(2);
63
64        let base = make_op(peer1, 1, OpType::Add, "file.txt");
65        let incoming = make_op(peer2, 2, OpType::Remove, "file.txt");
66
67        let conflict = Conflict::new(PathBuf::from("file.txt"), base, incoming);
68        let resolver = LastWriteWins::new();
69
70        // Incoming has higher timestamp -> UseIncoming
71        assert_eq!(resolver.resolve(&conflict, &peer1), Resolution::UseIncoming);
72    }
73
74    #[test]
75    fn test_last_write_wins_base() {
76        let peer1 = make_peer_id(1);
77        let peer2 = make_peer_id(2);
78
79        let base = make_op(peer1, 2, OpType::Add, "file.txt");
80        let incoming = make_op(peer2, 1, OpType::Remove, "file.txt");
81
82        let conflict = Conflict::new(PathBuf::from("file.txt"), base, incoming);
83        let resolver = LastWriteWins::new();
84
85        // Base has higher timestamp -> UseBase
86        assert_eq!(resolver.resolve(&conflict, &peer1), Resolution::UseBase);
87    }
88
89    #[test]
90    fn test_last_write_wins_tiebreak_by_peer_id() {
91        let peer1 = make_peer_id(1);
92        let peer2 = make_peer_id(2);
93
94        let base = make_op(peer1, 1, OpType::Add, "file.txt");
95        let incoming = make_op(peer2, 1, OpType::Remove, "file.txt");
96
97        let conflict = Conflict::new(PathBuf::from("file.txt"), base, incoming);
98        let resolver = LastWriteWins::new();
99
100        let resolution = resolver.resolve(&conflict, &peer1);
101
102        // Same timestamp, peer2 > peer1 (usually) -> check based on actual ordering
103        if peer2 > peer1 {
104            assert_eq!(resolution, Resolution::UseIncoming);
105        } else {
106            assert_eq!(resolution, Resolution::UseBase);
107        }
108    }
109}