Skip to main content

p2panda_core/
cursor.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! State vector to track and compare logs.
4use std::hash::Hash as StdHash;
5
6use crate::logs::{LogHeights, LogId, LogRanges, SeqNum, compare};
7use crate::traits::Author;
8
9/// Cursor to track log heights (state vector).
10///
11/// It offers methods to "advance" a log and compute the difference to another state vector. A
12/// cursor can be used to manage a state vector over a topic ("log heights" of logs scoped by a
13/// topic).
14#[derive(Clone, Debug, Ord, PartialOrd, PartialEq, Eq, StdHash)]
15pub struct Cursor<A, L> {
16    name: String,
17    state: LogHeights<A, L>,
18}
19
20impl<A, L> Cursor<A, L>
21where
22    A: Author,
23    L: LogId,
24{
25    pub fn new(name: impl AsRef<str>, state: LogHeights<A, L>) -> Self {
26        Self {
27            name: name.as_ref().to_string(),
28            state,
29        }
30    }
31
32    pub fn name(&self) -> &str {
33        &self.name
34    }
35
36    /// Returns state vector.
37    pub fn state(&self) -> &LogHeights<A, L> {
38        &self.state
39    }
40
41    /// Returns state vector for a specific log.
42    pub fn log_height(&self, author: &A, log_id: &L) -> Option<&SeqNum> {
43        self.state.get(author).and_then(|logs| logs.get(log_id))
44    }
45
46    /// Calculates the difference between two state vectors.
47    pub fn compare(&self, other: &LogHeights<A, L>) -> LogRanges<A, L> {
48        compare(other, &self.state)
49    }
50
51    /// Advances the state of a specific log.
52    pub fn advance(&mut self, author: A, log_id: L, log_height: SeqNum) {
53        // Ignore if given log-height is lower-or-equal than current state.
54        if let Some(current_log_height) = self.log_height(&author, &log_id)
55            && current_log_height >= &log_height
56        {
57            return;
58        }
59
60        self.state
61            .entry(author)
62            .or_default()
63            .insert(log_id, log_height);
64    }
65}
66
67#[cfg(test)]
68mod tests {
69    use crate::logs::LogHeights;
70    use crate::{SigningKey, VerifyingKey};
71
72    use super::Cursor;
73
74    #[test]
75    fn advance_log_height() {
76        let author_1 = SigningKey::generate().verifying_key();
77        let author_2 = SigningKey::generate().verifying_key();
78
79        let mut cursor = Cursor::<VerifyingKey, u64>::new("test", LogHeights::default());
80        assert_eq!(cursor.name(), "test");
81
82        assert!(cursor.log_height(&author_1, &0).is_none());
83        assert!(cursor.log_height(&author_2, &0).is_none());
84
85        cursor.advance(author_1, 0, 23);
86        assert_eq!(cursor.log_height(&author_1, &0), Some(&23));
87        assert!(cursor.log_height(&author_2, &0).is_none());
88
89        cursor.advance(author_2, 0, 10);
90        cursor.advance(author_2, 1, 2);
91        assert_eq!(cursor.log_height(&author_1, &0), Some(&23));
92        assert_eq!(cursor.log_height(&author_2, &0), Some(&10));
93        assert_eq!(cursor.log_height(&author_2, &1), Some(&2));
94    }
95
96    #[test]
97    fn strict_monotonic_incremental() {
98        let author = SigningKey::generate().verifying_key();
99        let mut cursor = Cursor::<VerifyingKey, u64>::new("test", LogHeights::default());
100
101        // Ignore attempts to move the cursor "backwards".
102        cursor.advance(author, 0, 10);
103        cursor.advance(author, 0, 5);
104        assert_eq!(cursor.log_height(&author, &0), Some(&10));
105    }
106
107    #[test]
108    fn compare() {
109        let author = SigningKey::generate().verifying_key();
110        let log_id_1 = 1;
111        let log_id_2 = 2;
112
113        let mut cursor_1 = Cursor::<VerifyingKey, u64>::new("one", LogHeights::default());
114        let mut cursor_2 = Cursor::<VerifyingKey, u64>::new("two", LogHeights::default());
115
116        cursor_1.advance(author, log_id_1, 121);
117        cursor_1.advance(author, log_id_2, 13);
118        cursor_2.advance(author, log_id_1, 287);
119
120        let ranges = cursor_1.compare(cursor_2.state());
121        assert_eq!(
122            ranges.get(&author).unwrap().get(&log_id_1).unwrap(),
123            &(Some(121), Some(287))
124        );
125        assert!(ranges.get(&author).unwrap().get(&log_id_2).is_none());
126
127        let ranges = cursor_2.compare(cursor_1.state());
128        assert!(ranges.get(&author).unwrap().get(&log_id_1).is_none());
129        assert_eq!(
130            ranges.get(&author).unwrap().get(&log_id_2).unwrap(),
131            &(None, Some(13))
132        );
133    }
134}