stateset_sync/
conflict.rs1use serde::{Deserialize, Serialize};
2
3use crate::event::SyncEvent;
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
9#[non_exhaustive]
10pub enum ConflictStrategy {
11 RemoteWins,
13 LocalWins,
15 LastWriterWins,
17}
18
19impl Default for ConflictStrategy {
20 fn default() -> Self {
21 Self::RemoteWins
22 }
23}
24
25#[derive(Debug, Clone, PartialEq, Eq)]
27#[non_exhaustive]
28pub enum Resolution {
29 KeepLocal,
31 KeepRemote,
33 Merge(SyncEvent),
35}
36
37#[derive(Debug, Clone)]
57pub struct ConflictResolver {
58 strategy: ConflictStrategy,
59}
60
61impl ConflictResolver {
62 #[must_use]
64 pub const fn new(strategy: ConflictStrategy) -> Self {
65 Self { strategy }
66 }
67
68 #[must_use]
70 pub const fn strategy(&self) -> ConflictStrategy {
71 self.strategy
72 }
73
74 #[must_use]
76 pub fn resolve(&self, local: &SyncEvent, remote: &SyncEvent) -> Resolution {
77 match self.strategy {
78 ConflictStrategy::RemoteWins => Resolution::KeepRemote,
79 ConflictStrategy::LocalWins => Resolution::KeepLocal,
80 ConflictStrategy::LastWriterWins => {
81 if local.timestamp >= remote.timestamp {
82 Resolution::KeepLocal
83 } else {
84 Resolution::KeepRemote
85 }
86 }
87 }
88 }
89
90 #[must_use]
94 pub fn resolve_batch(&self, pairs: &[(&SyncEvent, &SyncEvent)]) -> Vec<Resolution> {
95 pairs.iter().map(|(l, r)| self.resolve(l, r)).collect()
96 }
97}
98
99impl Default for ConflictResolver {
100 fn default() -> Self {
101 Self::new(ConflictStrategy::default())
102 }
103}
104
105#[cfg(test)]
106mod tests {
107 use super::*;
108 use chrono::{Duration, Utc};
109 use serde_json::json;
110 use uuid::Uuid;
111
112 fn make_event_at(name: &str, ts_offset_secs: i64) -> SyncEvent {
113 let ts = Utc::now() + Duration::seconds(ts_offset_secs);
114 SyncEvent::with_id(Uuid::new_v4(), 0, name, "order", "ORD-1", json!({"action": name}), ts)
115 }
116
117 #[test]
118 fn remote_wins_strategy() {
119 let resolver = ConflictResolver::new(ConflictStrategy::RemoteWins);
120 let local = make_event_at("local", 0);
121 let remote = make_event_at("remote", 0);
122 assert_eq!(resolver.resolve(&local, &remote), Resolution::KeepRemote);
123 }
124
125 #[test]
126 fn local_wins_strategy() {
127 let resolver = ConflictResolver::new(ConflictStrategy::LocalWins);
128 let local = make_event_at("local", 0);
129 let remote = make_event_at("remote", 0);
130 assert_eq!(resolver.resolve(&local, &remote), Resolution::KeepLocal);
131 }
132
133 #[test]
134 fn last_writer_wins_local_newer() {
135 let resolver = ConflictResolver::new(ConflictStrategy::LastWriterWins);
136 let local = make_event_at("local", 10); let remote = make_event_at("remote", -10); assert_eq!(resolver.resolve(&local, &remote), Resolution::KeepLocal);
139 }
140
141 #[test]
142 fn last_writer_wins_remote_newer() {
143 let resolver = ConflictResolver::new(ConflictStrategy::LastWriterWins);
144 let local = make_event_at("local", -10);
145 let remote = make_event_at("remote", 10);
146 assert_eq!(resolver.resolve(&local, &remote), Resolution::KeepRemote);
147 }
148
149 #[test]
150 fn last_writer_wins_equal_timestamps_keeps_local() {
151 let resolver = ConflictResolver::new(ConflictStrategy::LastWriterWins);
152 let ts = Utc::now();
153 let local = SyncEvent::with_id(Uuid::new_v4(), 1, "local", "o", "1", json!({}), ts);
154 let remote = SyncEvent::with_id(Uuid::new_v4(), 2, "remote", "o", "1", json!({}), ts);
155 assert_eq!(resolver.resolve(&local, &remote), Resolution::KeepLocal);
157 }
158
159 #[test]
160 fn default_strategy_is_remote_wins() {
161 let resolver = ConflictResolver::default();
162 assert_eq!(resolver.strategy(), ConflictStrategy::RemoteWins);
163 }
164
165 #[test]
166 fn resolve_batch() {
167 let resolver = ConflictResolver::new(ConflictStrategy::RemoteWins);
168 let l1 = make_event_at("l1", 0);
169 let r1 = make_event_at("r1", 0);
170 let l2 = make_event_at("l2", 0);
171 let r2 = make_event_at("r2", 0);
172
173 let pairs = vec![(&l1, &r1), (&l2, &r2)];
174 let resolutions = resolver.resolve_batch(&pairs);
175 assert_eq!(resolutions.len(), 2);
176 assert!(resolutions.iter().all(|r| *r == Resolution::KeepRemote));
177 }
178
179 #[test]
180 fn conflict_strategy_serde_roundtrip() {
181 let strategy = ConflictStrategy::LastWriterWins;
182 let json = serde_json::to_string(&strategy).unwrap();
183 let deserialized: ConflictStrategy = serde_json::from_str(&json).unwrap();
184 assert_eq!(deserialized, strategy);
185 }
186
187 #[test]
188 fn conflict_strategy_debug() {
189 let strategy = ConflictStrategy::LocalWins;
190 let debug = format!("{strategy:?}");
191 assert!(debug.contains("LocalWins"));
192 }
193
194 #[test]
195 fn resolver_clone() {
196 let resolver = ConflictResolver::new(ConflictStrategy::LocalWins);
197 let cloned = resolver;
198 assert_eq!(cloned.strategy(), ConflictStrategy::LocalWins);
199 }
200
201 #[test]
202 fn resolution_debug() {
203 let resolution = Resolution::KeepLocal;
204 let debug = format!("{resolution:?}");
205 assert!(debug.contains("KeepLocal"));
206 }
207
208 #[test]
209 fn resolution_clone_eq() {
210 let r1 = Resolution::KeepRemote;
211 let r2 = r1.clone();
212 assert_eq!(r1, r2);
213 }
214
215 #[test]
216 fn resolution_merge_variant() {
217 let event = make_event_at("merged", 0);
218 let resolution = Resolution::Merge(event);
219 if let Resolution::Merge(merged) = resolution {
220 assert_eq!(merged.event_type, "merged");
221 } else {
222 panic!("Expected Merge variant");
223 }
224 }
225}