Skip to main content

faultline_sync/
model.rs

1use std::collections::BTreeMap;
2
3use chrono::{DateTime, Utc};
4use faultline_core::FeatureRef;
5use serde::{Deserialize, Serialize};
6use serde_json::Value;
7use uuid::Uuid;
8
9#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
10pub struct CausalContext {
11    pub vector_clock: BTreeMap<String, u64>,
12    pub parent_operations: Vec<Uuid>,
13}
14
15impl CausalContext {
16    pub fn empty() -> Self {
17        Self {
18            vector_clock: BTreeMap::new(),
19            parent_operations: Vec::new(),
20        }
21    }
22}
23
24#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
25pub struct OperationEnvelope {
26    pub operation_id: Uuid,
27    pub replica_id: String,
28    pub causal_context: CausalContext,
29    pub operation_kind: String,
30    pub payload: Value,
31    pub feature_lineage_refs: Vec<FeatureRef>,
32    pub created_at: DateTime<Utc>,
33}
34
35impl OperationEnvelope {
36    pub fn new(replica_id: impl Into<String>, operation_kind: impl Into<String>) -> Self {
37        Self {
38            operation_id: Uuid::new_v4(),
39            replica_id: replica_id.into(),
40            causal_context: CausalContext::empty(),
41            operation_kind: operation_kind.into(),
42            payload: Value::Null,
43            feature_lineage_refs: Vec::new(),
44            created_at: Utc::now(),
45        }
46    }
47}
48
49#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
50pub struct OperationLog {
51    pub entries: Vec<OperationEnvelope>,
52}
53
54impl OperationLog {
55    pub fn empty() -> Self {
56        Self {
57            entries: Vec::new(),
58        }
59    }
60}
61
62#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
63pub struct PeerAdvertisement {
64    pub replica_id: String,
65    pub clock_vector: BTreeMap<String, u64>,
66    pub stored_operations: u64,
67}
68
69#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
70pub enum ConflictKind {
71    DuplicateBusinessKey,
72    TopologyViolation,
73    CausalOrderViolation,
74    Unknown,
75}
76
77#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
78pub struct ConflictEvent {
79    pub conflict_id: Uuid,
80    pub kind: ConflictKind,
81    pub left_operation_id: Uuid,
82    pub right_operation_id: Uuid,
83    pub resolution_hint: String,
84    pub detected_at: DateTime<Utc>,
85}
86
87impl ConflictEvent {
88    pub fn new(
89        kind: ConflictKind,
90        left_operation_id: Uuid,
91        right_operation_id: Uuid,
92        resolution_hint: impl Into<String>,
93    ) -> Self {
94        Self {
95            conflict_id: Uuid::new_v4(),
96            kind,
97            left_operation_id,
98            right_operation_id,
99            resolution_hint: resolution_hint.into(),
100            detected_at: Utc::now(),
101        }
102    }
103}
104
105#[cfg(test)]
106mod tests {
107    use super::*;
108
109    #[test]
110    fn operation_envelope_new_sets_default_context_and_payload() {
111        let envelope = OperationEnvelope::new("replica-a", "upsert_feature");
112
113        assert_eq!(envelope.replica_id, "replica-a");
114        assert_eq!(envelope.operation_kind, "upsert_feature");
115        assert!(envelope.causal_context.vector_clock.is_empty());
116        assert!(envelope.feature_lineage_refs.is_empty());
117        assert_eq!(envelope.payload, Value::Null);
118    }
119
120    #[test]
121    fn conflict_event_round_trip_serialization() {
122        let left = Uuid::new_v4();
123        let right = Uuid::new_v4();
124        let conflict = ConflictEvent::new(
125            ConflictKind::CausalOrderViolation,
126            left,
127            right,
128            "replay operations by vector clock order",
129        );
130
131        let json = serde_json::to_string(&conflict).expect("serialize conflict event");
132        let restored: ConflictEvent =
133            serde_json::from_str(&json).expect("deserialize conflict event");
134
135        assert_eq!(restored.kind, ConflictKind::CausalOrderViolation);
136        assert_eq!(restored.left_operation_id, left);
137        assert_eq!(restored.right_operation_id, right);
138    }
139}