Skip to main content

liminal/
causal.rs

1pub mod orderer;
2
3pub use orderer::{CausalOrderer, OrderedEnvelope};
4
5use uuid::Uuid;
6
7/// Unique identifier for a message in a causal chain.
8#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
9pub struct MessageId(Uuid);
10
11impl MessageId {
12    /// Generates a new message identifier.
13    #[must_use]
14    pub fn new() -> Self {
15        Self(Uuid::new_v4())
16    }
17
18    /// Wraps an existing UUID as a message identifier.
19    #[must_use]
20    pub const fn from_uuid(uuid: Uuid) -> Self {
21        Self(uuid)
22    }
23
24    /// Returns the underlying UUID.
25    #[must_use]
26    pub const fn as_uuid(self) -> Uuid {
27        self.0
28    }
29}
30
31impl Default for MessageId {
32    fn default() -> Self {
33        Self::new()
34    }
35}
36
37/// Causal metadata used to relate a message to an optional parent message.
38#[derive(Clone, Debug, PartialEq, Eq)]
39pub struct CausalContext {
40    /// Parent message that must causally precede this message, if any.
41    pub parent: Option<MessageId>,
42    /// Full causal parent chain ordered as parent, grandparent, and so on.
43    pub parent_chain: Vec<MessageId>,
44}
45
46impl CausalContext {
47    /// Creates an independent context with no parent message.
48    #[must_use]
49    pub const fn root() -> Self {
50        Self {
51            parent: None,
52            parent_chain: Vec::new(),
53        }
54    }
55
56    /// Creates a context for a message that directly follows `parent`, with a
57    /// depth-1 chain (`parent_chain == [parent]`).
58    ///
59    /// WARNING: this records ONLY the immediate parent, so
60    /// [`happened_before`](Self::happened_before) will not see `parent`'s own
61    /// ancestors. Use this only when `parent` is a chain root (no ancestors of
62    /// its own). When the parent itself has a causal context, build the child
63    /// with [`child_of_context`](Self::child_of_context) so transitive
64    /// happens-before is preserved — otherwise causal ordering is silently
65    /// truncated to one hop.
66    #[must_use]
67    pub fn child_of(parent: MessageId) -> Self {
68        Self::from_parent_chain(vec![parent])
69    }
70
71    /// Creates a child context by prepending `parent` to the parent's own causal chain.
72    #[must_use]
73    pub fn child_of_context(parent: MessageId, parent_context: &Self) -> Self {
74        let mut parent_chain = Vec::with_capacity(parent_context.parent_chain.len() + 1);
75        parent_chain.push(parent);
76        parent_chain.extend_from_slice(&parent_context.parent_chain);
77        Self::from_parent_chain(parent_chain)
78    }
79
80    /// Creates a context from an explicit parent chain ordered parent to oldest ancestor.
81    #[must_use]
82    pub fn from_parent_chain(parent_chain: Vec<MessageId>) -> Self {
83        let parent = parent_chain.first().copied();
84        Self {
85            parent,
86            parent_chain,
87        }
88    }
89
90    /// Returns the full parent chain ordered parent to oldest ancestor.
91    #[must_use]
92    pub fn parent_chain(&self) -> &[MessageId] {
93        &self.parent_chain
94    }
95
96    /// Returns whether `ancestor_id` causally happened before this context's message.
97    #[must_use]
98    pub fn happened_before(&self, ancestor_id: MessageId) -> bool {
99        self.parent_chain.contains(&ancestor_id)
100    }
101}
102
103impl Default for CausalContext {
104    fn default() -> Self {
105        Self::root()
106    }
107}
108
109#[cfg(test)]
110mod tests {
111    use super::{CausalContext, MessageId};
112
113    #[test]
114    fn root_context_has_no_parent() {
115        assert_eq!(CausalContext::root().parent, None);
116        assert!(CausalContext::root().parent_chain().is_empty());
117    }
118
119    #[test]
120    fn child_context_carries_parent_reference() {
121        let parent = MessageId::new();
122        let context = CausalContext::child_of(parent);
123
124        assert_eq!(context.parent, Some(parent));
125        assert_eq!(context.parent_chain(), &[parent]);
126    }
127
128    #[test]
129    fn generated_message_ids_are_unique() {
130        let first = MessageId::new();
131        let second = MessageId::new();
132
133        assert_ne!(first, second);
134    }
135
136    #[test]
137    fn happened_before_follows_full_parent_chain() {
138        let a_id = MessageId::new();
139        let a_context = CausalContext::root();
140
141        let b_id = MessageId::new();
142        let b_context = CausalContext::child_of(a_id);
143
144        let c_id = MessageId::new();
145        let c_context = CausalContext::child_of_context(b_id, &b_context);
146
147        let d_context = CausalContext::root();
148
149        assert!(c_context.happened_before(a_id));
150        assert!(c_context.happened_before(b_id));
151        assert_eq!(c_context.parent_chain(), &[b_id, a_id]);
152        assert!(!a_context.happened_before(c_id));
153        assert!(!d_context.happened_before(a_id));
154    }
155}