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}