Skip to main content

riffdb_types/
consumer.rs

1//! Stable bounded identities used by durable event consumers.
2
3use std::error::Error;
4use std::fmt;
5use std::num::{NonZeroU8, NonZeroU64};
6
7use crate::{
8    DatabaseId, EventConsumerIdentityHash, QueryParameterHash, ReactiveModuleHash,
9    ReactiveOperationName, hash_event_consumer_identity,
10};
11
12/// Maximum visible-ASCII bytes in one durable consumer name.
13pub const MAX_EVENT_CONSUMER_NAME_BYTES: usize = 64;
14/// Exact opaque bytes in one attempt-specific lease token.
15pub const EVENT_LEASE_TOKEN_BYTES: usize = 32;
16/// Terminal failed-attempt count for one event delivery.
17pub const MAX_EVENT_DELIVERY_ATTEMPTS: u8 = 10;
18
19/// Hashes the one canonical complete durable consumer identity tuple.
20#[must_use]
21pub fn event_consumer_identity_hash(
22    database_id: DatabaseId,
23    module_hash: ReactiveModuleHash,
24    operation_name: &ReactiveOperationName,
25    parameter_hash: QueryParameterHash,
26    consumer_name: &EventConsumerName,
27) -> EventConsumerIdentityHash {
28    let mut bytes = Vec::with_capacity(
29        16 + 32 + 4 + operation_name.as_str().len() + 32 + 4 + consumer_name.as_bytes().len(),
30    );
31    bytes.extend_from_slice(database_id.as_bytes());
32    bytes.extend_from_slice(module_hash.as_bytes());
33    append_bytes(&mut bytes, operation_name.as_str().as_bytes());
34    bytes.extend_from_slice(parameter_hash.as_bytes());
35    append_bytes(&mut bytes, consumer_name.as_bytes());
36    hash_event_consumer_identity(&bytes)
37}
38
39fn append_bytes(output: &mut Vec<u8>, bytes: &[u8]) {
40    output.extend_from_slice(&(bytes.len() as u32).to_be_bytes());
41    output.extend_from_slice(bytes);
42}
43
44/// One exact bounded application-selected durable consumer name.
45#[derive(Clone, Eq, Hash, Ord, PartialEq, PartialOrd)]
46pub struct EventConsumerName(String);
47
48impl EventConsumerName {
49    /// Checks `[A-Za-z][A-Za-z0-9_-]{0,63}` and retains the exact bytes.
50    pub fn new(value: impl Into<String>) -> Result<Self, EventConsumerNameError> {
51        let value = value.into();
52        let bytes = value.as_bytes();
53        if bytes.is_empty()
54            || bytes.len() > MAX_EVENT_CONSUMER_NAME_BYTES
55            || !bytes[0].is_ascii_alphabetic()
56            || !bytes[1..]
57                .iter()
58                .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'-'))
59        {
60            return Err(EventConsumerNameError);
61        }
62        Ok(Self(value))
63    }
64
65    /// Borrows the exact consumer name.
66    #[must_use]
67    pub fn as_str(&self) -> &str {
68        &self.0
69    }
70
71    /// Returns the exact visible-ASCII bytes.
72    #[must_use]
73    pub fn as_bytes(&self) -> &[u8] {
74        self.0.as_bytes()
75    }
76}
77
78impl fmt::Debug for EventConsumerName {
79    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
80        formatter.write_str("EventConsumerName([REDACTED])")
81    }
82}
83
84/// A consumer name was empty, malformed, or exceeded its fixed bound.
85#[derive(Clone, Copy, Debug, Eq, PartialEq)]
86pub struct EventConsumerNameError;
87
88impl fmt::Display for EventConsumerNameError {
89    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
90        formatter.write_str("invalid event consumer name")
91    }
92}
93
94impl Error for EventConsumerNameError {}
95
96/// Monotonic compare-and-transition revision local to one consumer record.
97#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
98pub struct EventConsumerRevision(NonZeroU64);
99
100impl EventConsumerRevision {
101    /// Constructs a nonzero revision.
102    #[must_use]
103    pub const fn new(value: u64) -> Option<Self> {
104        match NonZeroU64::new(value) {
105            Some(value) => Some(Self(value)),
106            None => None,
107        }
108    }
109
110    /// Returns the first revision.
111    #[must_use]
112    pub const fn first() -> Self {
113        Self(NonZeroU64::MIN)
114    }
115
116    /// Returns the integer value.
117    #[must_use]
118    pub const fn get(self) -> u64 {
119        self.0.get()
120    }
121
122    /// Advances without wrapping.
123    #[must_use]
124    pub const fn checked_next(self) -> Option<Self> {
125        match self.0.get().checked_add(1) {
126            Some(value) => Self::new(value),
127            None => None,
128        }
129    }
130}
131
132/// One attempt number in the closed range `1..=10`.
133#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
134pub struct EventDeliveryAttempt(NonZeroU8);
135
136impl EventDeliveryAttempt {
137    /// Checks the fixed attempt range.
138    #[must_use]
139    pub const fn new(value: u8) -> Option<Self> {
140        match NonZeroU8::new(value) {
141            Some(value) if value.get() <= MAX_EVENT_DELIVERY_ATTEMPTS => Some(Self(value)),
142            _ => None,
143        }
144    }
145
146    /// Returns the first delivery attempt.
147    #[must_use]
148    pub const fn first() -> Self {
149        Self(NonZeroU8::MIN)
150    }
151
152    /// Returns the attempt number.
153    #[must_use]
154    pub const fn get(self) -> u8 {
155        self.0.get()
156    }
157
158    /// Advances while another attempt remains.
159    #[must_use]
160    pub const fn checked_next(self) -> Option<Self> {
161        Self::new(self.0.get().saturating_add(1))
162    }
163}
164
165/// One opaque attempt-specific lease token. Possession is not authority.
166#[derive(Clone, Copy, Eq, Hash, Ord, PartialEq, PartialOrd)]
167pub struct EventLeaseToken([u8; EVENT_LEASE_TOKEN_BYTES]);
168
169impl EventLeaseToken {
170    /// Retains exactly 32 generated bytes.
171    #[must_use]
172    pub const fn from_bytes(bytes: [u8; EVENT_LEASE_TOKEN_BYTES]) -> Self {
173        Self(bytes)
174    }
175
176    /// Borrows the exact opaque bytes.
177    #[must_use]
178    pub const fn as_bytes(&self) -> &[u8; EVENT_LEASE_TOKEN_BYTES] {
179        &self.0
180    }
181}
182
183impl fmt::Debug for EventLeaseToken {
184    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
185        formatter.write_str("EventLeaseToken([REDACTED])")
186    }
187}
188
189#[cfg(test)]
190mod tests {
191    use super::*;
192
193    #[test]
194    fn consumer_name_grammar_is_exact() {
195        for accepted in ["a", "Worker_1", "worker-2", &"z".repeat(64)] {
196            assert!(EventConsumerName::new(accepted).is_ok(), "{accepted}");
197        }
198        for rejected in ["", "1worker", "has.dot", "has space", &"z".repeat(65)] {
199            assert!(EventConsumerName::new(rejected).is_err(), "{rejected}");
200        }
201    }
202
203    #[test]
204    fn revisions_and_attempts_never_wrap() {
205        assert_eq!(EventConsumerRevision::first().get(), 1);
206        assert!(
207            EventConsumerRevision::new(u64::MAX)
208                .expect("nonzero")
209                .checked_next()
210                .is_none()
211        );
212        assert_eq!(EventDeliveryAttempt::first().get(), 1);
213        assert_eq!(
214            EventDeliveryAttempt::new(10)
215                .expect("bounded")
216                .checked_next(),
217            None
218        );
219        assert!(EventDeliveryAttempt::new(11).is_none());
220    }
221}