1pub mod crdt;
7pub mod peer;
8
9use chrono::{DateTime, Utc};
10use schemars::JsonSchema;
11use serde::{Deserialize, Serialize};
12use uuid::Uuid;
13
14#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
16#[serde(rename_all = "snake_case")]
17pub enum ConflictResolution {
18 #[default]
20 LastWriterWins,
21 HighestNodeId,
23 ForkAndMerge,
25}
26
27#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
29#[serde(rename_all = "snake_case")]
30pub enum SyncState {
31 Synced,
33 LocalAhead { pending_changes: u64 },
35 RemoteAhead { pending_changes: u64 },
37 Diverged {
39 local_changes: u64,
40 remote_changes: u64,
41 },
42 Conflicted { workflow_id: String },
44}
45
46#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
48pub struct SyncResult {
49 pub workflows_synced: usize,
50 pub conflicts: Vec<SyncConflict>,
51 pub timestamp: DateTime<Utc>,
52}
53
54#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
56pub struct SyncConflict {
57 pub workflow_id: String,
58 pub local_version: u64,
59 pub remote_version: u64,
60 pub local_peer: String,
61 pub remote_peer: String,
62 pub detected_at: DateTime<Utc>,
63}
64
65pub trait TeamSync: Send + Sync {
67 fn push(&self, workflow_id: &str, yaml: &str) -> anyhow::Result<SyncState>;
69
70 fn pull(&self, workflow_id: &str) -> anyhow::Result<(String, SyncState)>;
72
73 fn state(&self, workflow_id: &str) -> anyhow::Result<SyncState>;
75
76 fn resolve_conflict(
78 &self,
79 workflow_id: &str,
80 resolution: &ConflictResolution,
81 ) -> anyhow::Result<String>;
82
83 fn local_peer_id(&self) -> Uuid;
85}
86
87#[cfg(test)]
88mod tests {
89 use super::*;
90
91 #[test]
92 fn test_conflict_resolution_default() {
93 let cr = ConflictResolution::default();
94 assert_eq!(cr, ConflictResolution::LastWriterWins);
95 }
96
97 #[test]
98 fn test_sync_state_serialization() {
99 let state = SyncState::Diverged {
100 local_changes: 3,
101 remote_changes: 5,
102 };
103 let json = serde_json::to_string(&state).unwrap();
104 let deserialized: SyncState = serde_json::from_str(&json).unwrap();
105 assert_eq!(state, deserialized);
106 }
107
108 #[test]
109 fn test_sync_state_variants() {
110 let synced = SyncState::Synced;
111 let json = serde_json::to_string(&synced).unwrap();
112 assert!(json.contains("synced"));
113
114 let conflicted = SyncState::Conflicted {
115 workflow_id: "wf-1".into(),
116 };
117 let json = serde_json::to_string(&conflicted).unwrap();
118 assert!(json.contains("wf-1"));
119 }
120
121 #[test]
122 fn test_conflict_resolution_serde_roundtrip() {
123 for variant in [
124 ConflictResolution::LastWriterWins,
125 ConflictResolution::HighestNodeId,
126 ConflictResolution::ForkAndMerge,
127 ] {
128 let json = serde_json::to_string(&variant).unwrap();
129 let back: ConflictResolution = serde_json::from_str(&json).unwrap();
130 assert_eq!(variant, back);
131 }
132 }
133}