Skip to main content

lean_ctx_doc/
fold.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! Lowest-seq membership of doc nodes (C0 2.2, shared with Hive's
4//! `hive-docs`): a section's page is the `to` of its live `part_of` edge with
5//! the lowest seq; a note's subject the `to` of its live `about` edge with the
6//! lowest seq; a slug belongs to the live page with the lowest `node.add` seq
7//! that holds it now, and every other live page holding it is in conflict.
8
9use std::collections::{BTreeMap, BTreeSet};
10
11/// The doc-relevant part of one place event, in log order.
12#[derive(Debug, Clone, PartialEq, Eq)]
13pub enum DocEvent {
14    /// `node.add` of a `page` (with its slug), `section` or `note`.
15    NodeAdd {
16        seq: u64,
17        node: String,
18        node_type: String,
19        slug: Option<String>,
20    },
21    /// `field.set slug` of a page (other fields do not change membership).
22    SlugSet {
23        seq: u64,
24        node: String,
25        slug: String,
26    },
27    /// `edge.add` of kind `part_of` or `about`.
28    EdgeAdd {
29        seq: u64,
30        edge: String,
31        kind: String,
32        from: String,
33        to: String,
34    },
35    /// `retract` of a node or an edge.
36    Retract { seq: u64, target: String },
37    /// `restore` of a node or an edge.
38    Restore { seq: u64, target: String },
39}
40
41/// Who belongs where after a fold.
42#[derive(Debug, Clone, Default, PartialEq, Eq)]
43pub struct Membership {
44    /// Section → its page; `None` while unattached.
45    pub section_page: BTreeMap<String, Option<String>>,
46    /// Note → the page or section it is about; `None` while it has none.
47    pub note_about: BTreeMap<String, Option<String>>,
48    /// Slug → the page that holds it.
49    pub slug_owner: BTreeMap<String, String>,
50    /// Live pages that hold a slug another page owns, in id order.
51    pub slug_conflict: Vec<String>,
52}
53
54struct Edge<'a> {
55    seq: u64,
56    kind: &'a str,
57    from: &'a str,
58    to: &'a str,
59}
60
61/// Folds `events` (any order; seq decides) into the doc membership.
62pub fn fold(events: &[DocEvent]) -> Membership {
63    let mut ordered: Vec<&DocEvent> = events.iter().collect();
64    ordered.sort_by_key(|e| seq(e));
65
66    let mut types: BTreeMap<&str, (&str, u64)> = BTreeMap::new();
67    let mut slugs: BTreeMap<&str, &str> = BTreeMap::new();
68    let mut edges: BTreeMap<&str, Edge<'_>> = BTreeMap::new();
69    let mut retracted: BTreeSet<&str> = BTreeSet::new();
70    for e in ordered {
71        match e {
72            DocEvent::NodeAdd {
73                seq,
74                node,
75                node_type,
76                slug,
77            } => {
78                types.entry(node).or_insert((node_type, *seq));
79                if let Some(s) = slug {
80                    slugs.insert(node, s);
81                }
82            }
83            DocEvent::SlugSet { node, slug, .. } => {
84                slugs.insert(node, slug);
85            }
86            DocEvent::EdgeAdd {
87                seq,
88                edge,
89                kind,
90                from,
91                to,
92            } => {
93                edges.entry(edge).or_insert(Edge {
94                    seq: *seq,
95                    kind,
96                    from,
97                    to,
98                });
99            }
100            DocEvent::Retract { target, .. } => {
101                retracted.insert(target);
102            }
103            DocEvent::Restore { target, .. } => {
104                retracted.remove(target.as_str());
105            }
106        }
107    }
108
109    let mut m = Membership::default();
110    let live = |id: &str| !retracted.contains(id);
111    // Edges name their ends even when the nodes' adds are elsewhere in the log.
112    for e in edges.values() {
113        let map = match e.kind {
114            "part_of" => &mut m.section_page,
115            "about" => &mut m.note_about,
116            _ => continue,
117        };
118        map.entry(e.from.to_owned()).or_insert(None);
119    }
120    let mut best: BTreeMap<(&str, &str), (u64, &str)> = BTreeMap::new();
121    for (id, e) in &edges {
122        if !live(id) {
123            continue;
124        }
125        let slot = best.entry((e.kind, e.from)).or_insert((e.seq, e.to));
126        if e.seq < slot.0 {
127            *slot = (e.seq, e.to);
128        }
129    }
130    for ((kind, from), (_, to)) in best {
131        let map = if kind == "part_of" {
132            &mut m.section_page
133        } else {
134            &mut m.note_about
135        };
136        map.insert(from.to_owned(), Some(to.to_owned()));
137    }
138
139    let mut holders: BTreeMap<&str, Vec<(u64, &str)>> = BTreeMap::new();
140    for (node, slug) in &slugs {
141        // No let-chain: rust-version is 1.87.
142        if let Some(("page", first)) = types.get(node) {
143            if live(node) {
144                holders.entry(slug).or_default().push((*first, node));
145            }
146        }
147    }
148    for (slug, mut pages) in holders {
149        pages.sort_unstable();
150        m.slug_owner.insert(slug.to_owned(), pages[0].1.to_owned());
151        m.slug_conflict
152            .extend(pages[1..].iter().map(|(_, p)| (*p).to_owned()));
153    }
154    m.slug_conflict.sort();
155    m
156}
157
158fn seq(e: &DocEvent) -> u64 {
159    match e {
160        DocEvent::NodeAdd { seq, .. }
161        | DocEvent::SlugSet { seq, .. }
162        | DocEvent::EdgeAdd { seq, .. }
163        | DocEvent::Retract { seq, .. }
164        | DocEvent::Restore { seq, .. } => *seq,
165    }
166}
167
168#[cfg(test)]
169mod tests {
170    use super::*;
171
172    const SEC: &str = "n_aaaaaaaaaaaaaaaaaaaaaadsea";
173    const PA: &str = "n_aaaaaaaaaaaaaaaaaaaaaadpga";
174    const PB: &str = "n_aaaaaaaaaaaaaaaaaaaaaadpgb";
175    const NOTE: &str = "n_aaaaaaaaaaaaaaaaaaaaaadnta";
176
177    fn edge(seq: u64, edge: &str, kind: &str, from: &str, to: &str) -> DocEvent {
178        DocEvent::EdgeAdd {
179            seq,
180            edge: edge.into(),
181            kind: kind.into(),
182            from: from.into(),
183            to: to.into(),
184        }
185    }
186    fn page(seq: u64, node: &str, slug: &str) -> DocEvent {
187        DocEvent::NodeAdd {
188            seq,
189            node: node.into(),
190            node_type: "page".into(),
191            slug: Some(slug.into()),
192        }
193    }
194    fn retract(seq: u64, target: &str) -> DocEvent {
195        DocEvent::Retract {
196            seq,
197            target: target.into(),
198        }
199    }
200
201    // C0 2.2 vectors (hive-v2 vectors/doc_fold).
202    #[test]
203    fn part_of_and_about_vectors() {
204        let two = [
205            edge(5, "e_a", "part_of", SEC, PA),
206            edge(7, "e_b", "part_of", SEC, PB),
207        ];
208        assert_eq!(
209            fold(&two).section_page[SEC].as_deref(),
210            Some(PA),
211            "part_of_lowest_seq"
212        );
213        let mut after = two.to_vec();
214        after.push(retract(9, "e_a"));
215        assert_eq!(
216            fold(&after).section_page[SEC].as_deref(),
217            Some(PB),
218            "part_of_after_retract"
219        );
220        let gone = [edge(5, "e_a", "part_of", SEC, PA), retract(6, "e_a")];
221        assert_eq!(fold(&gone).section_page[SEC], None, "unattached");
222        let about = [
223            edge(4, "e_c", "about", NOTE, SEC),
224            edge(8, "e_d", "about", NOTE, PA),
225        ];
226        assert_eq!(
227            fold(&about).note_about[NOTE].as_deref(),
228            Some(SEC),
229            "about_lowest_seq"
230        );
231    }
232
233    #[test]
234    fn slug_vectors() {
235        let both = [page(3, PA, "a"), page(4, PB, "a")];
236        let m = fold(&both);
237        assert_eq!(m.slug_owner["a"], PA);
238        assert_eq!(m.slug_conflict, [PB]);
239
240        let mut freed = both.to_vec();
241        freed.push(retract(6, PA));
242        let m = fold(&freed);
243        assert_eq!(m.slug_owner["a"], PB);
244        assert!(m.slug_conflict.is_empty());
245
246        let mut renamed = both.to_vec();
247        renamed.push(DocEvent::SlugSet {
248            seq: 6,
249            node: PA.into(),
250            slug: "b".into(),
251        });
252        let m = fold(&renamed);
253        assert_eq!(
254            (m.slug_owner["a"].as_str(), m.slug_owner["b"].as_str()),
255            (PB, PA)
256        );
257        assert!(m.slug_conflict.is_empty());
258    }
259}