CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-yjs

Conflict-free Replicated Data Type (CRDT) framework for real-time collaborative applications

Overview
Eval results
Files

undo-redo-system.mddocs/

Undo/Redo System

Multi-level undo/redo functionality with scope management and origin tracking. Yjs provides a comprehensive undo/redo system that works seamlessly with collaborative editing scenarios.

Capabilities

UndoManager Class

Core class for managing undo/redo operations across one or more shared types.

/**
 * Manages undo/redo operations for shared types
 */
class UndoManager {
  constructor(
    typeScope: Doc | AbstractType<any> | Array<AbstractType<any>>,
    options?: UndoManagerOptions
  );
  
  /** Document this undo manager operates on */
  readonly doc: Doc;
  
  /** Types that are tracked for undo/redo */
  readonly scope: Array<AbstractType<any> | Doc>;
  
  /** Stack of undo operations */
  readonly undoStack: Array<StackItem>;
  
  /** Stack of redo operations */
  readonly redoStack: Array<StackItem>;
  
  /** Whether currently performing undo operation */
  readonly undoing: boolean;
  
  /** Whether currently performing redo operation */
  readonly redoing: boolean;
  
  /** Set of transaction origins that are tracked */
  readonly trackedOrigins: Set<any>;
  
  /** Time window for merging consecutive operations (ms) */
  readonly captureTimeout: number;
}

interface UndoManagerOptions {
  /** Time window for merging operations in milliseconds (default: 500) */
  captureTimeout?: number;
  
  /** Function to determine if transaction should be captured */
  captureTransaction?: (transaction: Transaction) => boolean;
  
  /** Function to filter which items can be deleted during undo */
  deleteFilter?: (item: Item) => boolean;
  
  /** Set of origins to track (default: all origins) */
  trackedOrigins?: Set<any>;
  
  /** Whether to ignore remote changes (default: true) */
  ignoreRemoteMapChanges?: boolean;
}

Usage Examples:

import * as Y from "yjs";

const doc = new Y.Doc();
const ytext = doc.getText("document");

// Create undo manager for single type
const undoManager = new Y.UndoManager(ytext);

// Create undo manager for multiple types
const yarray = doc.getArray("items");
const ymap = doc.getMap("metadata");
const multiTypeUndoManager = new Y.UndoManager([ytext, yarray, ymap]);

// Create undo manager with options
const configuredUndoManager = new Y.UndoManager(ytext, {
  captureTimeout: 1000, // 1 second merge window
  trackedOrigins: new Set(["user-input", "paste-operation"]),
  captureTransaction: (transaction) => {
    // Only capture transactions from specific origins
    return transaction.origin === "user-input";
  }
});

Undo/Redo Operations

Core methods for performing undo and redo operations.

/**
 * Undo the last captured operation
 * @returns StackItem that was undone, or null if nothing to undo
 */
undo(): StackItem | null;

/**
 * Redo the last undone operation
 * @returns StackItem that was redone, or null if nothing to redo
 */
redo(): StackItem | null;

/**
 * Check if undo is possible
 * @returns True if there are operations to undo
 */
canUndo(): boolean;

/**
 * Check if redo is possible
 * @returns True if there are operations to redo
 */
canRedo(): boolean;

Usage Examples:

import * as Y from "yjs";

const doc = new Y.Doc();
const ytext = doc.getText("document");
const undoManager = new Y.UndoManager(ytext);

// Make some changes
ytext.insert(0, "Hello");
ytext.insert(5, " World");

console.log("Text:", ytext.toString()); // "Hello World"
console.log("Can undo:", undoManager.canUndo()); // true

// Undo last operation
const undone = undoManager.undo();
console.log("Text after undo:", ytext.toString()); // "Hello"

// Undo another operation
undoManager.undo();
console.log("Text after second undo:", ytext.toString()); // ""

console.log("Can undo:", undoManager.canUndo()); // false
console.log("Can redo:", undoManager.canRedo()); // true

// Redo operations
undoManager.redo();
console.log("Text after redo:", ytext.toString()); // "Hello"

undoManager.redo();
console.log("Text after second redo:", ytext.toString()); // "Hello World"

Stack Management

Methods for managing the undo/redo stacks.

/**
 * Clear undo/redo stacks
 * @param clearUndoStack - Whether to clear undo stack (default: true)
 * @param clearRedoStack - Whether to clear redo stack (default: true)
 */
clear(clearUndoStack?: boolean, clearRedoStack?: boolean): void;

/**
 * Stop capturing consecutive operations into current stack item
 */
stopCapturing(): void;

Usage Examples:

import * as Y from "yjs";

const doc = new Y.Doc();
const ytext = doc.getText("document");
const undoManager = new Y.UndoManager(ytext);

// Make changes
ytext.insert(0, "Hello");
ytext.insert(5, " World");

console.log("Undo stack size:", undoManager.undoStack.length);

// Clear only redo stack
undoManager.clear(false, true);

// Clear all stacks
undoManager.clear();
console.log("Undo stack size after clear:", undoManager.undoStack.length); // 0

// Stop capturing to force new stack item
ytext.insert(0, "A");
ytext.insert(1, "B"); // These might be merged

undoManager.stopCapturing();
ytext.insert(2, "C"); // This will be in separate stack item

Scope Management

Methods for managing which types are tracked by the undo manager.

/**
 * Add types to the undo manager scope
 * @param ytypes - Types or document to add to scope
 */
addToScope(ytypes: Array<AbstractType<any> | Doc> | AbstractType<any> | Doc): void;

/**
 * Add origin to set of tracked origins
 * @param origin - Origin to start tracking
 */
addTrackedOrigin(origin: any): void;

/**
 * Remove origin from set of tracked origins
 * @param origin - Origin to stop tracking
 */
removeTrackedOrigin(origin: any): void;

Usage Examples:

import * as Y from "yjs";

const doc = new Y.Doc();
const ytext = doc.getText("document");
const yarray = doc.getArray("items");
const undoManager = new Y.UndoManager(ytext);

// Add another type to scope
undoManager.addToScope(yarray);

// Now changes to both ytext and yarray are tracked
ytext.insert(0, "Hello");
yarray.push(["item1"]);

undoManager.undo(); // Undoes both operations

// Manage tracked origins
undoManager.addTrackedOrigin("user-input");
undoManager.addTrackedOrigin("paste-operation");

// Only track specific origins
doc.transact(() => {
  ytext.insert(0, "Tracked ");
}, "user-input");

doc.transact(() => {
  ytext.insert(0, "Not tracked ");
}, "auto-save");

// Only the "user-input" transaction is available for undo

StackItem Class

Individual items in the undo/redo stacks representing atomic operations.

/**
 * Individual undo/redo operation
 */
class StackItem {
  constructor(deletions: DeleteSet, insertions: DeleteSet);
  
  /** Items that were inserted in this operation */
  readonly insertions: DeleteSet;
  
  /** Items that were deleted in this operation */
  readonly deletions: DeleteSet;
  
  /** Metadata associated with this stack item */
  readonly meta: Map<any, any>;
}

Usage Examples:

import * as Y from "yjs";

const doc = new Y.Doc();
const ytext = doc.getText("document");
const undoManager = new Y.UndoManager(ytext);

// Make changes
ytext.insert(0, "Hello World");

// Examine stack items
const stackItem = undoManager.undoStack[0];
console.log("Insertions:", stackItem.insertions);
console.log("Deletions:", stackItem.deletions);
console.log("Metadata:", stackItem.meta);

// Add metadata to stack items
undoManager.on('stack-item-added', (event) => {
  event.stackItem.meta.set('timestamp', Date.now());
  event.stackItem.meta.set('userId', 'current-user');
});

Advanced Undo Manager Patterns

Selective Undo/Redo:

import * as Y from "yjs";

class SelectiveUndoManager {
  private undoManager: Y.UndoManager;
  private operationHistory: Array<{
    id: string;
    stackItem: Y.StackItem;
    description: string;
    timestamp: number;
  }> = [];

  constructor(types: Y.AbstractType<any> | Array<Y.AbstractType<any>>) {
    this.undoManager = new Y.UndoManager(types);
    
    // Track all operations
    this.undoManager.on('stack-item-added', (event) => {
      this.operationHistory.push({
        id: `op-${Date.now()}-${Math.random()}`,
        stackItem: event.stackItem,
        description: this.getOperationDescription(event.stackItem),
        timestamp: Date.now()
      });
    });
  }

  getOperationHistory() {
    return [...this.operationHistory];
  }

  undoSpecificOperation(operationId: string): boolean {
    const operation = this.operationHistory.find(op => op.id === operationId);
    if (!operation) return false;

    // Find operation in stack and undo up to that point
    const stackIndex = this.undoManager.undoStack.indexOf(operation.stackItem);
    if (stackIndex === -1) return false;

    // Undo operations in reverse order up to target
    for (let i = 0; i <= stackIndex; i++) {
      if (!this.undoManager.canUndo()) break;
      this.undoManager.undo();
    }

    return true;
  }

  private getOperationDescription(stackItem: Y.StackItem): string {
    // Analyze stack item to generate description
    return `Operation at ${new Date().toLocaleTimeString()}`;
  }
}

Collaborative Undo:

import * as Y from "yjs";

class CollaborativeUndoManager {
  private undoManager: Y.UndoManager;
  private clientId: number;

  constructor(doc: Y.Doc, types: Y.AbstractType<any> | Array<Y.AbstractType<any>>) {
    this.clientId = doc.clientID;
    
    // Only track local changes
    this.undoManager = new Y.UndoManager(types, {
      captureTransaction: (transaction) => {
        return transaction.local && transaction.origin !== 'undo' && transaction.origin !== 'redo';
      }
    });
  }

  undoLocal(): Y.StackItem | null {
    // Only undo operations made by this client
    return this.undoManager.undo();
  }

  redoLocal(): Y.StackItem | null {
    // Only redo operations made by this client
    return this.undoManager.redo();
  }

  getLocalOperationCount(): number {
    return this.undoManager.undoStack.length;
  }
}

Undo with Confirmation:

import * as Y from "yjs";

class ConfirmingUndoManager {
  private undoManager: Y.UndoManager;
  private confirmationRequired: boolean = false;

  constructor(types: Y.AbstractType<any> | Array<Y.AbstractType<any>>) {
    this.undoManager = new Y.UndoManager(types);
  }

  setConfirmationRequired(required: boolean) {
    this.confirmationRequired = required;
  }

  async undoWithConfirmation(): Promise<Y.StackItem | null> {
    if (!this.undoManager.canUndo()) return null;

    if (this.confirmationRequired) {
      const confirmed = await this.showConfirmationDialog("Undo last operation?");
      if (!confirmed) return null;
    }

    return this.undoManager.undo();
  }

  async redoWithConfirmation(): Promise<Y.StackItem | null> {
    if (!this.undoManager.canRedo()) return null;

    if (this.confirmationRequired) {
      const confirmed = await this.showConfirmationDialog("Redo last operation?");
      if (!confirmed) return null;
    }

    return this.undoManager.redo();
  }

  private showConfirmationDialog(message: string): Promise<boolean> {
    // Implementation would show actual confirmation dialog
    return Promise.resolve(confirm(message));
  }
}

Undo Manager Events:

import * as Y from "yjs";

const doc = new Y.Doc();
const ytext = doc.getText("document");
const undoManager = new Y.UndoManager(ytext);

// Listen for undo manager events
undoManager.on('stack-item-added', (event) => {
  console.log("New operation added to stack:", event.stackItem);
});

undoManager.on('stack-item-popped', (event) => {
  console.log("Operation removed from stack:", event.stackItem);
});

undoManager.on('stack-cleared', (event) => {
  console.log("Stacks cleared:", event);
});

// Custom event handling for UI updates
class UndoRedoUI {
  private undoButton: HTMLButtonElement;
  private redoButton: HTMLButtonElement;

  constructor(undoManager: Y.UndoManager, undoBtn: HTMLButtonElement, redoBtn: HTMLButtonElement) {
    this.undoButton = undoBtn;
    this.redoButton = redoBtn;

    this.updateButtons(undoManager);

    undoManager.on('stack-item-added', () => this.updateButtons(undoManager));
    undoManager.on('stack-item-popped', () => this.updateButtons(undoManager));
    undoManager.on('stack-cleared', () => this.updateButtons(undoManager));
  }

  private updateButtons(undoManager: Y.UndoManager) {
    this.undoButton.disabled = !undoManager.canUndo();
    this.redoButton.disabled = !undoManager.canRedo();
    
    this.undoButton.title = `Undo (${undoManager.undoStack.length} operations)`;
    this.redoButton.title = `Redo (${undoManager.redoStack.length} operations)`;
  }
}

Lifecycle Management

/**
 * Destroy the undo manager and clean up resources
 */
destroy(): void;

Usage Examples:

import * as Y from "yjs";

const doc = new Y.Doc();
const ytext = doc.getText("document");
const undoManager = new Y.UndoManager(ytext);

// Use undo manager...

// Clean up when done
undoManager.destroy();

// Undo manager is no longer functional after destroy
console.log("Can undo after destroy:", undoManager.canUndo()); // false

Install with Tessl CLI

npx tessl i tessl/npm-yjs

docs

document-management.md

event-system.md

index.md

position-tracking.md

shared-data-types.md

snapshot-system.md

synchronization.md

transaction-system.md

undo-redo-system.md

xml-types.md

tile.json