1use std::collections::{BTreeMap, HashSet};
2
3use crate::event::{Event, EventKind, MemoryData};
4
5#[derive(Clone, Debug, PartialEq, Eq)]
10pub struct Conflict {
11 pub memory_id: String,
12 pub field: String,
13 pub lost_writer: String,
14 pub lost_lamport: u64,
15 pub winning_writer: String,
16 pub winning_lamport: u64,
17}
18
19const FIELDS: [&str; 8] = [
20 "title",
21 "body",
22 "mem_type",
23 "scope",
24 "project",
25 "topic_key",
26 "anchors",
27 "tags",
28];
29
30fn writer(ev: &Event) -> String {
31 format!("{}@{}", ev.engine, ev.machine)
32}
33
34fn content_data(ev: &Event) -> bool {
35 matches!(
36 ev.kind,
37 EventKind::MemoryAdded(_) | EventKind::MemoryUpdated(_) | EventKind::MemoryPatched(_)
38 )
39}
40
41fn touched_values(ev: &Event) -> [(bool, String); 8] {
45 match &ev.kind {
46 EventKind::MemoryAdded(d) | EventKind::MemoryUpdated(d) => {
47 let v = full_field_values(d);
48 std::array::from_fn(|i| (true, v[i].clone()))
49 }
50 EventKind::MemoryPatched(p) => [
51 opt(&p.title),
52 opt(&p.body),
53 opt(&p.mem_type),
54 p.scope
55 .as_ref()
56 .map_or((false, String::new()), |s| (true, format!("{s:?}"))),
57 opt(&p.project),
58 p.topic_key
59 .as_ref()
60 .map_or((false, String::new()), |s| (true, format!("{:?}", Some(s)))),
61 p.anchors
62 .as_ref()
63 .map_or((false, String::new()), |a| (true, format!("{a:?}"))),
64 p.tags
65 .as_ref()
66 .map_or((false, String::new()), |t| (true, format!("{t:?}"))),
67 ],
68 _ => std::array::from_fn(|_| (false, String::new())),
69 }
70}
71
72fn opt(v: &Option<String>) -> (bool, String) {
73 match v {
74 Some(s) => (true, s.clone()),
75 None => (false, String::new()),
76 }
77}
78
79fn full_field_values(d: &MemoryData) -> [String; 8] {
81 [
82 d.title.clone(),
83 d.body.clone(),
84 d.mem_type.clone(),
85 format!("{:?}", d.scope),
86 d.project.clone(),
87 format!("{:?}", d.topic_key),
88 format!("{:?}", d.anchors),
89 format!("{:?}", d.tags),
90 ]
91}
92
93pub fn detect_conflicts(events: &[Event]) -> Vec<Conflict> {
97 let deleted: HashSet<&str> = events
98 .iter()
99 .filter(|e| matches!(e.kind, EventKind::MemoryDeleted))
100 .map(|e| e.memory_id.as_str())
101 .collect();
102
103 let mut by_mem: BTreeMap<&str, Vec<&Event>> = BTreeMap::new();
104 for ev in events {
105 if content_data(ev) && !deleted.contains(ev.memory_id.as_str()) {
106 by_mem.entry(ev.memory_id.as_str()).or_default().push(ev);
107 }
108 }
109
110 let mut out = Vec::new();
111 for (mem_id, mut evs) in by_mem {
112 if evs.len() < 2 {
113 continue;
114 }
115 evs.sort_by(|a, b| {
116 a.lamport
117 .cmp(&b.lamport)
118 .then_with(|| a.event_id.cmp(&b.event_id))
119 });
120
121 let tvals: Vec<[(bool, String); 8]> = evs.iter().map(|e| touched_values(e)).collect();
122
123 for fi in 0..FIELDS.len() {
124 let mut prev: Option<String> = None;
130 let mut setters: Vec<(&Event, String)> = Vec::new();
131 let mut field_winner: Option<&Event> = None;
132 for (i, ev_i) in evs.iter().enumerate() {
133 let (touched, val) = &tvals[i][fi];
134 if *touched {
135 let changed = prev.as_deref() != Some(val.as_str());
136 if changed && prev.is_some() {
137 setters.push((ev_i, val.clone()));
138 }
139 prev = Some(val.clone());
140 field_winner = Some(ev_i);
141 }
142 }
143 let winner_val = prev.unwrap_or_default();
144 let winner_ev = match field_winner {
145 Some(e) => e,
146 None => continue,
147 };
148 let winner_writer = writer(winner_ev);
149
150 let distinct: HashSet<String> = setters.iter().map(|(e, _)| writer(e)).collect();
151 if distinct.len() < 2 {
152 continue;
153 }
154 let lost = setters
155 .iter()
156 .rev()
157 .find(|(e, v)| writer(e) != winner_writer && *v != winner_val);
158 if let Some((lose_ev, _)) = lost {
159 out.push(Conflict {
160 memory_id: mem_id.to_string(),
161 field: FIELDS[fi].to_string(),
162 lost_writer: writer(lose_ev),
163 lost_lamport: lose_ev.lamport,
164 winning_writer: winner_writer.clone(),
165 winning_lamport: winner_ev.lamport,
166 });
167 }
168 }
169 }
170 out
171}