Skip to main content

p2panda_core/
cursor.rs

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