dx_forge/crdt/
operations.rs

1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3use uuid::Uuid;
4
5#[derive(Debug, Clone, Serialize, Deserialize)]
6pub struct Operation {
7    pub id: Uuid,
8    pub timestamp: DateTime<Utc>,
9    pub actor_id: String,
10    pub file_path: String,
11    pub op_type: OperationType,
12    pub parent_ops: Vec<Uuid>, // For causality tracking
13}
14
15#[derive(Debug, Clone, Serialize, Deserialize)]
16pub enum OperationType {
17    Insert {
18        position: Position,
19        content: String,
20        length: usize,
21    },
22    Delete {
23        position: Position,
24        length: usize,
25    },
26    Replace {
27        position: Position,
28        old_content: String,
29        new_content: String,
30    },
31    FileCreate {
32        content: String,
33    },
34    FileDelete,
35    FileRename {
36        old_path: String,
37        new_path: String,
38    },
39}
40
41#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
42pub struct Position {
43    /// CRDT-based position that survives transformations
44    pub lamport_timestamp: u64,
45    pub actor_id: String,
46    pub offset: usize,
47
48    /// Human-readable position (may change)
49    pub line: usize,
50    pub column: usize,
51}
52
53impl Position {
54    pub fn new(line: usize, column: usize, offset: usize, actor_id: String, lamport: u64) -> Self {
55        Self {
56            lamport_timestamp: lamport,
57            actor_id,
58            offset,
59            line,
60            column,
61        }
62    }
63
64    /// Create a stable identifier that survives code transformations
65    pub fn stable_id(&self) -> String {
66        format!(
67            "{}:{}:{}",
68            self.actor_id, self.lamport_timestamp, self.offset
69        )
70    }
71}
72
73impl Operation {
74    pub fn new(file_path: String, op_type: OperationType, actor_id: String) -> Self {
75        Self {
76            id: Uuid::new_v4(),
77            timestamp: Utc::now(),
78            actor_id,
79            file_path,
80            op_type,
81            parent_ops: Vec::new(),
82        }
83    }
84
85    pub fn with_parents(mut self, parents: Vec<Uuid>) -> Self {
86        self.parent_ops = parents;
87        self
88    }
89
90    pub fn lamport(&self) -> Option<u64> {
91        match &self.op_type {
92            OperationType::Insert { position, .. }
93            | OperationType::Delete { position, .. }
94            | OperationType::Replace { position, .. } => Some(position.lamport_timestamp),
95            _ => None,
96        }
97    }
98
99    /// Check if operations can be batched together
100    pub fn can_batch_with(&self, other: &Operation) -> bool {
101        // Operations must be on the same file
102        if self.file_path != other.file_path {
103            return false;
104        }
105
106        // Must have same actor
107        if self.actor_id != other.actor_id {
108            return false;
109        }
110
111        // Check if they are consecutive operations
112        match (&self.op_type, &other.op_type) {
113            (OperationType::Insert { .. }, OperationType::Insert { .. }) => true,
114            (OperationType::Delete { .. }, OperationType::Delete { .. }) => true,
115            _ => false,
116        }
117    }
118}
119
120/// Batch of operations for efficient processing
121#[derive(Debug, Clone, Serialize, Deserialize)]
122pub struct OperationBatch {
123    pub operations: Vec<Operation>,
124    pub batch_id: Uuid,
125    pub created_at: DateTime<Utc>,
126}
127
128impl OperationBatch {
129    /// Create a new batch
130    pub fn new(operations: Vec<Operation>) -> Self {
131        Self {
132            operations,
133            batch_id: Uuid::new_v4(),
134            created_at: Utc::now(),
135        }
136    }
137
138    /// Merge consecutive operations in the batch
139    pub fn optimize(&mut self) {
140        // Simple optimization: remove redundant operations
141        self.operations.dedup_by(|a, b| {
142            a.file_path == b.file_path && 
143            a.actor_id == b.actor_id &&
144            a.timestamp == b.timestamp
145        });
146    }
147
148    /// Get total size of batch
149    pub fn len(&self) -> usize {
150        self.operations.len()
151    }
152
153    /// Check if batch is empty
154    pub fn is_empty(&self) -> bool {
155        self.operations.is_empty()
156    }
157}