Skip to main content

pubky_common/
events.rs

1//! Event types shared across Pubky crates.
2//!
3//! This module provides unified types for event streaming functionality,
4//! used by both the homeserver and SDK.
5
6use std::fmt::Display;
7use std::str::FromStr;
8
9use crate::crypto::Hash;
10
11/// Cursor for pagination in event queries.
12///
13/// The cursor represents the ID of an event and is used for pagination.
14/// It can be parsed from a string representation of an integer.
15///
16/// Note: Uses `u64` internally, but Postgres BIGINT is signed (`i64`).
17/// sea_query/sqlx binds `u64` values, which works correctly as long as
18/// IDs stay within `i64::MAX` (~9.2 quintillion). Since event IDs are
19/// auto-incrementing from 1, this is not a practical concern.
20#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
21pub struct EventCursor(u64);
22
23impl EventCursor {
24    /// Create a new cursor from an event ID.
25    #[must_use]
26    pub fn new(id: u64) -> Self {
27        Self(id)
28    }
29
30    /// Get the underlying ID value.
31    #[must_use]
32    pub fn id(&self) -> u64 {
33        self.0
34    }
35}
36
37impl Display for EventCursor {
38    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
39        write!(f, "{}", self.0)
40    }
41}
42
43impl FromStr for EventCursor {
44    type Err = std::num::ParseIntError;
45
46    fn from_str(s: &str) -> Result<Self, Self::Err> {
47        Ok(EventCursor(s.parse()?))
48    }
49}
50
51impl From<u64> for EventCursor {
52    fn from(id: u64) -> Self {
53        EventCursor(id)
54    }
55}
56
57impl TryFrom<&str> for EventCursor {
58    type Error = std::num::ParseIntError;
59
60    fn try_from(s: &str) -> Result<Self, Self::Error> {
61        s.parse()
62    }
63}
64
65impl TryFrom<String> for EventCursor {
66    type Error = std::num::ParseIntError;
67
68    fn try_from(s: String) -> Result<Self, Self::Error> {
69        s.parse()
70    }
71}
72
73/// Type of event in the event stream.
74#[derive(Debug, Clone, PartialEq, Eq)]
75pub enum EventType {
76    /// PUT event - resource created or updated, with its content hash.
77    Put {
78        /// Blake3 hash of the content.
79        content_hash: Hash,
80    },
81    /// DELETE event - resource deleted.
82    Delete,
83}
84
85impl EventType {
86    /// Get the string representation of the event type.
87    pub fn as_str(&self) -> &'static str {
88        match self {
89            EventType::Put { .. } => "PUT",
90            EventType::Delete => "DEL",
91        }
92    }
93
94    /// Get the content hash if this is a PUT event.
95    pub fn content_hash(&self) -> Option<&Hash> {
96        match self {
97            EventType::Put { content_hash } => Some(content_hash),
98            EventType::Delete => None,
99        }
100    }
101}
102
103impl Display for EventType {
104    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
105        write!(f, "{}", self.as_str())
106    }
107}
108
109#[cfg(test)]
110mod tests {
111    use super::*;
112
113    #[test]
114    fn cursor_display_and_from_str() {
115        let cursor = EventCursor::new(12345);
116        assert_eq!(cursor.to_string(), "12345");
117
118        let parsed: EventCursor = "67890".parse().unwrap();
119        assert_eq!(parsed.id(), 67890);
120
121        let from_u64: EventCursor = 111u64.into();
122        assert_eq!(from_u64.id(), 111);
123
124        let try_from_str = EventCursor::try_from("222").unwrap();
125        assert_eq!(try_from_str.id(), 222);
126
127        let try_from_string = EventCursor::try_from("333".to_string()).unwrap();
128        assert_eq!(try_from_string.id(), 333);
129    }
130
131    #[test]
132    fn cursor_ordering() {
133        let c1 = EventCursor::new(1);
134        let c2 = EventCursor::new(2);
135        let c3 = EventCursor::new(2);
136
137        assert!(c1 < c2);
138        assert!(c2 > c1);
139        assert_eq!(c2, c3);
140    }
141
142    #[test]
143    fn event_type_display() {
144        let put = EventType::Put {
145            content_hash: Hash::from_bytes([0; 32]),
146        };
147        let del = EventType::Delete;
148
149        assert_eq!(put.to_string(), "PUT");
150        assert_eq!(del.to_string(), "DEL");
151        assert_eq!(put.as_str(), "PUT");
152        assert_eq!(del.as_str(), "DEL");
153    }
154
155    #[test]
156    fn event_type_content_hash() {
157        let hash = Hash::from_bytes([1; 32]);
158        let put = EventType::Put { content_hash: hash };
159        let del = EventType::Delete;
160
161        assert_eq!(put.content_hash(), Some(&hash));
162        assert_eq!(del.content_hash(), None);
163    }
164
165    #[test]
166    fn cursor_parse_error() {
167        assert!("abc".parse::<EventCursor>().is_err());
168        assert!("".parse::<EventCursor>().is_err());
169        assert!("-1".parse::<EventCursor>().is_err());
170        assert!("12.34".parse::<EventCursor>().is_err());
171    }
172}