tuwunel_matrix/room/state/resolution/
topological_sort.rs1use std::{
26 cmp::{Ordering, Reverse},
27 collections::{BinaryHeap, HashMap, HashSet},
28};
29
30use futures::{Stream, TryFutureExt, TryStreamExt, stream::try_unfold};
31use ruma::{
32 MilliSecondsSinceUnixEpoch, OwnedEventId, events::room::power_levels::UserPowerLevel,
33};
34use tuwunel_core::{
35 Error, Result, is_not_equal_to, smallvec::SmallVec, utils::stream::IterStream, validated,
36};
37
38pub type ReferencedIds = SmallVec<[OwnedEventId; 3]>;
42type PduInfo = (UserPowerLevel, MilliSecondsSinceUnixEpoch);
43
44#[derive(PartialEq, Eq)]
45struct TieBreaker {
46 event_id: OwnedEventId,
47 power_level: UserPowerLevel,
48 origin_server_ts: MilliSecondsSinceUnixEpoch,
49}
50
51impl Ord for TieBreaker {
53 fn cmp(&self, other: &Self) -> Ordering {
54 other
55 .power_level
56 .cmp(&self.power_level)
57 .then(self.origin_server_ts.cmp(&other.origin_server_ts))
58 .then(self.event_id.cmp(&other.event_id))
59 }
60}
61
62impl PartialOrd for TieBreaker {
63 fn partial_cmp(&self, other: &Self) -> Option<Ordering> { Some(self.cmp(other)) }
64}
65
66#[tracing::instrument(
88 level = "debug",
89 skip_all,
90 fields(
91 graph = graph.len(),
92 )
93)]
94#[expect(clippy::implicit_hasher)]
95pub async fn topological_sort<Query, Fut>(
96 graph: HashMap<OwnedEventId, ReferencedIds>,
97 query: &Query,
98) -> Result<Vec<OwnedEventId>>
99where
100 Query: Fn(OwnedEventId) -> Fut + Sync,
101 Fut: Future<Output = Result<PduInfo>> + Send,
102{
103 let query = async |event_id: OwnedEventId| {
104 let (power_level, origin_server_ts) = query(event_id.clone()).await?;
105 Ok::<_, Error>(TieBreaker { event_id, power_level, origin_server_ts })
106 };
107
108 let max_edges = graph
109 .values()
110 .map(ReferencedIds::len)
111 .fold(graph.len(), |a, c| validated!(a + c));
112
113 let incoming = graph
114 .iter()
115 .flat_map(|(event_id, out)| {
116 out.iter()
117 .map(move |reference| (event_id, reference))
118 })
119 .fold(HashMap::with_capacity(max_edges), |mut incoming, (event_id, reference)| {
120 let references: &mut ReferencedIds = incoming.entry(reference.clone()).or_default();
121
122 if !references.contains(event_id) {
123 references.push(event_id.clone());
124 }
125
126 incoming
127 });
128
129 let horizon = graph
131 .iter()
132 .filter(|(_, references)| {
133 !references
134 .iter()
135 .any(|reference| graph.contains_key(reference))
136 })
137 .try_stream()
138 .and_then(async |(event_id, _)| Ok(Reverse(query(event_id.clone()).await?)))
139 .try_collect::<BinaryHeap<Reverse<TieBreaker>>>()
140 .await?;
141
142 kahn_sort(horizon, graph, &incoming, &query)
143 .try_collect()
144 .await
145}
146
147#[tracing::instrument(
151 level = "debug",
152 skip_all,
153 fields(
154 heap = %heap.len(),
155 graph = %graph.len(),
156 )
157)]
158fn kahn_sort<Query, Fut>(
159 heap: BinaryHeap<Reverse<TieBreaker>>,
160 graph: HashMap<OwnedEventId, ReferencedIds>,
161 incoming: &HashMap<OwnedEventId, ReferencedIds>,
162 query: &Query,
163) -> impl Stream<Item = Result<OwnedEventId>> + Send
164where
165 Query: Fn(OwnedEventId) -> Fut + Sync,
166 Fut: Future<Output = Result<TieBreaker>> + Send,
167{
168 try_unfold((heap, graph), async move |(mut heap, graph)| {
169 let Some(Reverse(item)) = heap.pop() else {
170 return Ok(None);
171 };
172
173 let references = incoming.get(&item.event_id).cloned();
174 let state = (item.event_id, (heap, graph));
175 references
176 .into_iter()
177 .flatten()
178 .try_stream()
179 .try_fold(state, async |(event_id, (mut heap, mut graph)), parent_id| {
180 graph
181 .get_mut(&parent_id)
182 .expect("contains all parent_ids")
183 .retain(is_not_equal_to!(&event_id));
184
185 if !graph[&parent_id]
187 .iter()
188 .any(|reference| graph.contains_key(reference))
189 {
190 heap.push(Reverse(query(parent_id.clone()).await?));
191 }
192
193 Ok::<_, Error>((event_id, (heap, graph)))
194 })
195 .map_ok(Some)
196 .await
197 })
198}
199
200#[expect(clippy::implicit_hasher)]
210pub fn is_topologically_sorted<'a, Order>(
211 order: Order,
212 graph: &HashMap<OwnedEventId, ReferencedIds>,
213) -> bool
214where
215 Order: IntoIterator<Item = &'a OwnedEventId>,
216{
217 order
218 .into_iter()
219 .try_fold(HashSet::with_capacity(graph.len()), |mut seen, event_id| {
220 let satisfied = graph
221 .get(event_id)
222 .into_iter()
223 .flatten()
224 .filter(|reference| graph.contains_key(*reference))
225 .all(|reference| seen.contains(reference));
226
227 seen.insert(event_id);
228 satisfied.then_some(seen)
229 })
230 .is_some()
231}
232
233pub fn is_topologically_sorted_in_place<'a, T, Id, Refs, Ref>(
246 items: &'a [T],
247 id: Id,
248 references: Refs,
249) -> bool
250where
251 Id: Fn(&'a T) -> &'a str,
252 Refs: Fn(&'a T) -> Ref,
253 Ref: Iterator<Item = &'a str>,
254{
255 if items.len() < 2 {
256 return true;
257 }
258
259 items.iter().enumerate().all(|(i, item)| {
260 references(item).all(|reference| {
261 items[i..]
262 .iter()
263 .all(|other| id(other) != reference)
264 })
265 })
266}
267
268#[cfg(test)]
269mod tests {
270 use super::is_topologically_sorted_in_place;
271
272 fn sorted(items: &[(&str, &[&str])]) -> bool {
273 is_topologically_sorted_in_place(
274 items,
275 |item: &(&str, &[&str])| item.0,
276 |item: &(&str, &[&str])| item.1.iter().copied(),
277 )
278 }
279
280 #[test]
281 fn empty_or_single_is_sorted() {
282 assert!(sorted(&[]));
283 assert!(sorted(&[("a", &[])]));
284 }
285
286 #[test]
287 fn parents_before_children() {
288 assert!(sorted(&[("a", &[]), ("b", &["a"]), ("c", &["a", "b"])]));
289 }
290
291 #[test]
292 fn child_before_parent_is_unsorted() {
293 assert!(!sorted(&[("b", &["a"]), ("a", &[])]));
294 }
295
296 #[test]
297 fn absent_reference_is_a_non_edge() {
298 assert!(sorted(&[("b", &["x"]), ("a", &["x"])]));
299 }
300
301 #[test]
302 fn self_reference_is_unsorted() {
303 assert!(!sorted(&[("a", &["a"]), ("b", &[])]));
304 }
305
306 #[test]
307 fn later_duplicate_of_a_parent_is_unsorted() {
308 assert!(!sorted(&[("a", &[]), ("b", &["a"]), ("a", &[])]));
309 }
310}