Transaction System

A transaction system ensures atomic operations and provides rollback capabilities.

Overview

Transactions group multiple operations into atomic units. Either all operations succeed, or none are applied. This ensures document consistency and enables undo/redo functionality.

Transaction Model

Atomic Operations

Group operations into transactions for atomicity:

class Transaction {
  #operations = [];
  #snapshot = null;
  #editor = null;
  
  constructor(editor) {
    this.#editor = editor;
    this.#snapshot = this.#editor.getDocument();
  }
  
  add(operation) {
    this.#operations.push(operation);
  }
  
  async commit() {
    // Validate all operations
    for (const op of this.#operations) {
      if (!this.#editor.canApply(op)) {
        return false;
      }
    }
    
    // Apply all operations atomically
    try {
      for (const op of this.#operations) {
        this.#editor.applyOperation(op);
      }
      
      // Commit successful
      return true;
    } catch (error) {
      // Rollback on error
      this.rollback();
      return false;
    }
  }
  
  rollback() {
    // Restore snapshot
    this.#editor.setDocument(this.#snapshot);
    this.#operations = [];
  }
  
  getOperations() {
    return [...this.#operations];
  }
}

// Usage
const transaction = editor.beginTransaction();
transaction.add({ type: 'insert', path: [0], content: 'Hello' });
transaction.add({ type: 'insert', path: [0], content: ' World' });

if (await transaction.commit()) {
  console.log('Transaction committed');
} else {
  console.log('Transaction failed, rolled back');
}

Transaction Lifecycle

Complete transaction lifecycle:

class TransactionManager {
  #activeTransactions = [];
  
  beginTransaction(editor) {
    const transaction = new Transaction(editor);
    this.#activeTransactions.push(transaction);
    return transaction;
  }
  
  async commitTransaction(transaction) {
    const index = this.#activeTransactions.indexOf(transaction);
    if (index === -1) {
      throw new Error('Transaction not found');
    }
    
    const success = await transaction.commit();
    
    if (success) {
      this.#activeTransactions.splice(index, 1);
    }
    
    return success;
  }
  
  rollbackTransaction(transaction) {
    const index = this.#activeTransactions.indexOf(transaction);
    if (index === -1) {
      throw new Error('Transaction not found');
    }
    
    transaction.rollback();
    this.#activeTransactions.splice(index, 1);
  }
  
  rollbackAll() {
    this.#activeTransactions.forEach(transaction => {
      transaction.rollback();
    });
    this.#activeTransactions = [];
  }
}

Rollback Mechanism

Operation Inversion

Implement rollback using operation inversion:

class RollbackManager {
  #history = [];
  #inverseHistory = [];
  
  apply(operation) {
    // Apply operation
    this.#applyOperation(operation);
    
    // Generate inverse operation
    const inverse = this.#inverse(operation);
    
    // Store both
    this.#history.push(operation);
    this.#inverseHistory.push(inverse);
    
    return inverse;
  }
  
  #inverse(operation) {
    switch (operation.type) {
      case 'insert':
        return {
          type: 'delete',
          path: operation.path,
          length: typeof operation.content === 'string' 
            ? operation.content.length 
            : 1
        };
      
      case 'delete':
        return {
          type: 'insert',
          path: operation.path,
          content: operation.content // Restore deleted content
        };
      
      case 'update':
        return {
          type: 'update',
          path: operation.path,
          attrs: operation.previousAttrs // Restore previous attributes
        };
    }
  }
  
  undo() {
    if (this.#inverseHistory.length === 0) {
      return false;
    }
    
    const inverse = this.#inverseHistory.pop();
    const original = this.#history.pop();
    
    // Apply inverse
    this.#applyOperation(inverse);
    
    return true;
  }
  
  redo() {
    if (this.#history.length === 0) {
      return false;
    }
    
    // Re-apply last operation
    const operation = this.#history[this.#history.length - 1];
    this.#applyOperation(operation);
    
    return true;
  }
}

Snapshot Restore

Alternative approach using snapshots:

class SnapshotManager {
  #snapshots = [];
  #currentIndex = -1;
  
  createSnapshot(document) {
    // Deep clone document
    const snapshot = this.#deepClone(document);
    
    // Remove old snapshots after current index
    this.#snapshots = this.#snapshots.slice(0, this.#currentIndex + 1);
    
    // Add new snapshot
    this.#snapshots.push(snapshot);
    this.#currentIndex = this.#snapshots.length - 1;
    
    return this.#currentIndex;
  }
  
  restoreSnapshot(index) {
    if (index < 0 || index >= this.#snapshots.length) {
      return false;
    }
    
    const snapshot = this.#snapshots[index];
    this.#currentIndex = index;
    
    // Restore document
    return this.#deepClone(snapshot);
  }
  
  undo() {
    if (this.#currentIndex > 0) {
      this.#currentIndex--;
      return this.#snapshots[this.#currentIndex];
    }
    return null;
  }
  
  redo() {
    if (this.#currentIndex < this.#snapshots.length - 1) {
      this.#currentIndex++;
      return this.#snapshots[this.#currentIndex];
    }
    return null;
  }
  
  #deepClone(obj) {
    return JSON.parse(JSON.stringify(obj));
  }
}

Nested Transactions

Support nested transactions:

class NestedTransaction extends Transaction {
  #parent = null;
  #children = [];
  
  constructor(editor, parent = null) {
    super(editor);
    this.#parent = parent;
    if (parent) {
      parent.#children.push(this);
    }
  }
  
  async commit() {
    // Commit all child transactions first
    for (const child of this.#children) {
      const success = await child.commit();
      if (!success) {
        return false;
      }
    }
    
    // Then commit this transaction
    return await super.commit();
  }
  
  rollback() {
    // Rollback all child transactions
    this.#children.forEach(child => child.rollback());
    
    // Then rollback this transaction
    super.rollback();
  }
}

// Usage
const outer = editor.beginTransaction();
outer.add({ type: 'insert', path: [0], content: 'Outer' });

const inner = editor.beginTransaction(outer);
inner.add({ type: 'insert', path: [1], content: 'Inner' });

// Committing outer will also commit inner
await outer.commit();

Transaction Isolation

Ensure transaction isolation:

class IsolatedTransaction extends Transaction {
  #isolatedDocument = null;
  
  constructor(editor) {
    super(editor);
    // Create isolated copy of document
    this.#isolatedDocument = this.#deepClone(editor.getDocument());
  }
  
  add(operation) {
    // Apply to isolated document
    this.#isolatedDocument = this.#applyToDocument(
      this.#isolatedDocument,
      operation
    );
    this.#operations.push(operation);
  }
  
  async commit() {
    // Validate on isolated document
    const valid = this.#validate(this.#isolatedDocument);
    if (!valid) {
      return false;
    }
    
    // Apply all operations to real document
    for (const op of this.#operations) {
      this.#editor.applyOperation(op);
    }
    
    return true;
  }
  
  // Read operations work on isolated document
  read(path) {
    return this.#getNodeAtPath(this.#isolatedDocument, path);
  }
}