heroforge_core/fs/
transaction.rs1use crate::fs::errors::{FsError, FsResult};
8use crate::fs::operations::{FsOperation, OperationSummary};
9use sha3::{Digest, Sha3_256};
10use std::sync::{Arc, Mutex};
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum TransactionMode {
15 ReadOnly,
17
18 ReadWrite,
20
21 Exclusive,
23}
24
25impl TransactionMode {
26 pub fn allows_writes(&self) -> bool {
28 matches!(
29 self,
30 TransactionMode::ReadWrite | TransactionMode::Exclusive
31 )
32 }
33
34 pub fn is_exclusive(&self) -> bool {
36 *self == TransactionMode::Exclusive
37 }
38}
39
40#[derive(Debug, Clone, Copy, PartialEq, Eq)]
42pub enum TransactionState {
43 Active,
45
46 Committed,
48
49 RolledBack,
51
52 Error,
54}
55
56#[derive(Clone)]
72pub struct Transaction {
73 id: String,
75
76 mode: TransactionMode,
78
79 state: Arc<Mutex<TransactionState>>,
81
82 operations: Arc<Mutex<Vec<FsOperation>>>,
84
85 summary: Arc<Mutex<OperationSummary>>,
87
88 parent_commit: Option<String>,
90
91 branch: Option<String>,
93
94 created_at: i64,
96
97 max_operations: usize,
99}
100
101impl Transaction {
102 pub fn new(mode: TransactionMode) -> Self {
104 let id = uuid::Uuid::new_v4().to_string();
105 let now = std::time::SystemTime::now()
106 .duration_since(std::time::UNIX_EPOCH)
107 .unwrap_or_default()
108 .as_secs() as i64;
109
110 Self {
111 id,
112 mode,
113 state: Arc::new(Mutex::new(TransactionState::Active)),
114 operations: Arc::new(Mutex::new(Vec::new())),
115 summary: Arc::new(Mutex::new(OperationSummary::new())),
116 parent_commit: None,
117 branch: None,
118 created_at: now,
119 max_operations: 0,
120 }
121 }
122
123 pub fn id(&self) -> &str {
125 &self.id
126 }
127
128 pub fn state(&self) -> TransactionState {
130 *self.state.lock().unwrap()
131 }
132
133 pub fn is_active(&self) -> bool {
135 self.state() == TransactionState::Active
136 }
137
138 pub fn allows_writes(&self) -> bool {
140 self.mode.allows_writes()
141 }
142
143 pub fn mode(&self) -> TransactionMode {
145 self.mode
146 }
147
148 pub fn operation_count(&self) -> usize {
150 self.operations.lock().unwrap().len()
151 }
152
153 pub fn operations(&self) -> Vec<FsOperation> {
155 self.operations.lock().unwrap().clone()
156 }
157
158 pub fn summary(&self) -> OperationSummary {
160 self.summary.lock().unwrap().clone()
161 }
162
163 pub fn add_operation(&self, op: FsOperation) -> FsResult<()> {
165 if !self.is_active() {
167 return Err(FsError::TransactionError(
168 "Cannot add operation to inactive transaction".to_string(),
169 ));
170 }
171
172 if !self.allows_writes() {
174 return Err(FsError::TransactionError(
175 "Cannot add write operation to read-only transaction".to_string(),
176 ));
177 }
178
179 if self.max_operations > 0 && self.operation_count() >= self.max_operations {
181 return Err(FsError::TransactionError(format!(
182 "Transaction operation limit ({}) exceeded",
183 self.max_operations
184 )));
185 }
186
187 let mut ops = self.operations.lock().unwrap();
188 ops.push(op);
189
190 Ok(())
191 }
192
193 pub fn set_parent(&mut self, commit_hash: String) {
195 self.parent_commit = Some(commit_hash);
196 }
197
198 pub fn set_branch(&mut self, branch: String) {
200 self.branch = Some(branch);
201 }
202
203 pub fn parent_commit(&self) -> Option<&str> {
205 self.parent_commit.as_deref()
206 }
207
208 pub fn branch(&self) -> Option<&str> {
210 self.branch.as_deref()
211 }
212
213 pub fn commit(&self, message: &str, author: &str) -> FsResult<String> {
226 if !self.is_active() {
228 return Err(FsError::TransactionError(format!(
229 "Cannot commit {} transaction",
230 match self.state() {
231 TransactionState::Committed => "already-committed",
232 TransactionState::RolledBack => "rolled-back",
233 TransactionState::Error => "error",
234 TransactionState::Active => "unknown",
235 }
236 )));
237 }
238
239 let ops = self.operations.lock().unwrap();
240 if ops.is_empty() {
241 return Err(FsError::TransactionError(
242 "Cannot commit empty transaction".to_string(),
243 ));
244 }
245
246 let mut hasher = Sha3_256::new();
248 hasher.update(format!("{}{}{}", message, author, self.id).as_bytes());
249 let hash = hasher.finalize();
250 let commit_hash = format!("commit_{:x}", hash);
251
252 *self.state.lock().unwrap() = TransactionState::Committed;
254
255 Ok(commit_hash)
256 }
257
258 pub fn rollback(&self) -> FsResult<()> {
262 if !self.is_active() {
263 return Err(FsError::TransactionError(
264 "Cannot rollback inactive transaction".to_string(),
265 ));
266 }
267
268 self.operations.lock().unwrap().clear();
269 *self.state.lock().unwrap() = TransactionState::RolledBack;
270
271 Ok(())
272 }
273
274 pub fn set_error(&self) {
276 *self.state.lock().unwrap() = TransactionState::Error;
277 }
278
279 pub fn savepoint(&self) -> FsResult<SavePoint> {
283 if !self.is_active() {
284 return Err(FsError::TransactionError(
285 "Cannot create savepoint in inactive transaction".to_string(),
286 ));
287 }
288
289 let ops = self.operations.lock().unwrap();
290 Ok(SavePoint {
291 transaction_id: self.id.clone(),
292 operation_count: ops.len(),
293 })
294 }
295
296 pub fn rollback_to_savepoint(&self, savepoint: &SavePoint) -> FsResult<()> {
298 if self.id != savepoint.transaction_id {
299 return Err(FsError::TransactionError(
300 "Savepoint is from a different transaction".to_string(),
301 ));
302 }
303
304 let mut ops = self.operations.lock().unwrap();
305 if savepoint.operation_count < ops.len() {
306 ops.truncate(savepoint.operation_count);
307 Ok(())
308 } else {
309 Err(FsError::TransactionError(
310 "Invalid savepoint state".to_string(),
311 ))
312 }
313 }
314}
315
316impl std::fmt::Debug for Transaction {
317 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
318 f.debug_struct("Transaction")
319 .field("id", &self.id)
320 .field("mode", &self.mode)
321 .field("state", &self.state())
322 .field("operations", &self.operation_count())
323 .field("parent_commit", &self.parent_commit)
324 .field("branch", &self.branch)
325 .field("created_at", &self.created_at)
326 .finish()
327 }
328}
329
330#[derive(Debug, Clone)]
332pub struct SavePoint {
333 transaction_id: String,
335
336 operation_count: usize,
338}
339
340impl SavePoint {
341 pub fn transaction_id(&self) -> &str {
343 &self.transaction_id
344 }
345
346 pub fn operation_count(&self) -> usize {
348 self.operation_count
349 }
350}
351
352pub struct TransactionHandle {
354 transaction: Arc<Transaction>,
355}
356
357impl TransactionHandle {
358 pub fn new(mode: TransactionMode) -> Self {
360 Self {
361 transaction: Arc::new(Transaction::new(mode)),
362 }
363 }
364
365 pub fn transaction(&self) -> &Transaction {
367 &self.transaction
368 }
369
370 pub fn commit(self, message: &str, author: &str) -> FsResult<String> {
372 self.transaction.commit(message, author)
373 }
374
375 pub fn rollback(self) -> FsResult<()> {
377 self.transaction.rollback()
378 }
379}
380
381#[cfg(test)]
382mod tests {
383 use super::*;
384
385 #[test]
386 fn test_transaction_creation() {
387 let tx = Transaction::new(TransactionMode::ReadWrite);
388 assert!(tx.is_active());
389 assert!(tx.allows_writes());
390 assert_eq!(tx.operation_count(), 0);
391 }
392
393 #[test]
394 fn test_transaction_state() {
395 let tx = Transaction::new(TransactionMode::ReadOnly);
396 assert_eq!(tx.state(), TransactionState::Active);
397 assert!(!tx.allows_writes());
398 }
399
400 #[test]
401 fn test_transaction_mode() {
402 let rw = TransactionMode::ReadWrite;
403 let ro = TransactionMode::ReadOnly;
404
405 assert!(rw.allows_writes());
406 assert!(!ro.allows_writes());
407 assert!(!rw.is_exclusive());
408 assert!(TransactionMode::Exclusive.is_exclusive());
409 }
410
411 #[test]
412 fn test_operation_count() {
413 let tx = Transaction::new(TransactionMode::ReadWrite);
414 assert_eq!(tx.operation_count(), 0);
415 }
416}