package core import ( "fmt" "path/filepath" "git.dws.rip/DWS/onyx/internal/models" "git.dws.rip/DWS/onyx/internal/storage" ) // Transaction represents a transactional operation with oplog support type Transaction struct { repo *OnyxRepository oplogWriter *storage.OplogWriter stateCapture *storage.StateCapture } // NewTransaction creates a new transaction for the given repository func NewTransaction(repo *OnyxRepository) (*Transaction, error) { oplogPath := filepath.Join(repo.GetOnyxPath(), "oplog") oplogWriter, err := storage.OpenOplog(oplogPath) if err != nil { return nil, fmt.Errorf("failed to open oplog: %w", err) } stateCapture := storage.NewStateCapture(repo.GetGitRepo()) return &Transaction{ repo: repo, oplogWriter: oplogWriter, stateCapture: stateCapture, }, nil } // ExecuteWithTransaction executes a function within a transaction context // It captures the state before and after the operation and logs it to the oplog func (t *Transaction) ExecuteWithTransaction(operation, description string, fn func() error) error { // 1. Capture state_before stateBefore, err := t.stateCapture.CaptureState() if err != nil { return fmt.Errorf("failed to capture state before: %w", err) } // 2. Execute the function err = fn() if err != nil { // On error, we don't log to oplog since the operation failed return fmt.Errorf("operation failed: %w", err) } // 3. Capture state_after stateAfter, err := t.stateCapture.CaptureState() if err != nil { // Even if we can't capture the after state, we should try to log what we can // This is a warning situation rather than a failure fmt.Printf("Warning: failed to capture state after: %v\n", err) stateAfter = stateBefore // Use the before state as a fallback } // 4. Create oplog entry entry := models.NewOplogEntry(0, operation, description, stateBefore, stateAfter) // 5. Write to oplog err = t.oplogWriter.AppendEntry(entry) if err != nil { return fmt.Errorf("failed to write to oplog: %w", err) } return nil } // Close closes the transaction and releases resources func (t *Transaction) Close() error { return t.oplogWriter.Close() } // ExecuteWithTransactionAndMetadata executes a function with custom metadata func (t *Transaction) ExecuteWithTransactionAndMetadata( operation, description string, metadata map[string]string, fn func() error, ) error { // Capture state_before stateBefore, err := t.stateCapture.CaptureState() if err != nil { return fmt.Errorf("failed to capture state before: %w", err) } // Execute the function err = fn() if err != nil { return fmt.Errorf("operation failed: %w", err) } // Capture state_after stateAfter, err := t.stateCapture.CaptureState() if err != nil { fmt.Printf("Warning: failed to capture state after: %v\n", err) stateAfter = stateBefore } // Create oplog entry with metadata entry := models.NewOplogEntry(0, operation, description, stateBefore, stateAfter) entry.Metadata = metadata // Write to oplog err = t.oplogWriter.AppendEntry(entry) if err != nil { return fmt.Errorf("failed to write to oplog: %w", err) } return nil } // Rollback attempts to rollback to a previous state func (t *Transaction) Rollback(entryID uint64) error { // Read the oplog entry oplogPath := filepath.Join(t.repo.GetOnyxPath(), "oplog") reader := storage.NewOplogReader(oplogPath) entry, err := reader.ReadEntry(entryID) if err != nil { return fmt.Errorf("failed to read entry %d: %w", entryID, err) } // Restore the state_before from that entry if entry.StateBefore == nil { return fmt.Errorf("entry %d has no state_before to restore", entryID) } err = t.stateCapture.RestoreState(entry.StateBefore) if err != nil { return fmt.Errorf("failed to restore state: %w", err) } // Log the rollback operation stateAfter, _ := t.stateCapture.CaptureState() rollbackEntry := models.NewOplogEntry( 0, "rollback", fmt.Sprintf("Rolled back to entry %d", entryID), stateAfter, // The current state becomes the "before" entry.StateBefore, // The restored state becomes the "after" ) rollbackEntry.Metadata = map[string]string{ "rollback_to_entry_id": fmt.Sprintf("%d", entryID), } err = t.oplogWriter.AppendEntry(rollbackEntry) if err != nil { // Don't fail the rollback if we can't log it fmt.Printf("Warning: failed to log rollback: %v\n", err) } return nil } // Helper function to execute a transaction on a repository func ExecuteWithTransaction(repo *OnyxRepository, operation, description string, fn func() error) error { txn, err := NewTransaction(repo) if err != nil { return err } defer txn.Close() return txn.ExecuteWithTransaction(operation, description, fn) }