Managing Memento History
Undo/Redo with Memento Stack
The most common use of mementos is implementing undo/redo functionality. Here's a complete implementation:
typescript1class UndoRedoManager<T> {2 private history: T[] = [];3 private currentIndex: number = -1;45 // Save current state6 push(memento: T): void {7 // Remove any "future" history when adding new state8 this.history = this.history.slice(0, this.currentIndex + 1);910 this.history.push(memento);11 this.currentIndex++;12 }1314 // Undo to previous state15 undo(): T | null {16 if (!this.canUndo()) {17 return null;18 }1920 this.currentIndex--;21 return this.history[this.currentIndex];22 }2324 // Redo to next state25 redo(): T | null {26 if (!this.canRedo()) {27 return null;28 }2930 this.currentIndex++;31 return this.history[this.currentIndex];32 }3334 // Get current state without changing position35 current(): T | null {36 if (this.currentIndex < 0) {37 return null;38 }39 return this.history[this.currentIndex];40 }4142 canUndo(): boolean {43 return this.currentIndex > 0;44 }4546 canRedo(): boolean {47 return this.currentIndex < this.history.length - 1;48 }4950 clear(): void {51 this.history = [];52 this.currentIndex = -1;53 }5455 getHistorySize(): number {56 return this.history.length;57 }58}
Using the Undo/Redo Manager
typescript1class OrderDraft {2 private items: string[] = [];3 private customer: string = '';45 addItem(item: string): void {6 this.items.push(item);7 }89 setCustomer(customer: string): void {10 this.customer = customer;11 }1213 save(): OrderMemento {14 return new OrderMemento(15 [...this.items],16 this.customer,17 new Date()18 );19 }2021 restore(memento: OrderMemento): void {22 const state = memento.getState();23 this.items = [...state.items];24 this.customer = state.customer;25 }2627 getPreview(): string {28 return `${this.customer || '(no customer)'}: ${this.items.length} items`;29 }30}3132// Usage33const draft = new OrderDraft();34const manager = new UndoRedoManager<OrderMemento>();3536// Initial save37draft.setCustomer('ACME Corp');38manager.push(draft.save());3940// Make changes and save41draft.addItem('Widget A');42manager.push(draft.save());4344draft.addItem('Widget B');45manager.push(draft.save());4647draft.addItem('Widget C');48manager.push(draft.save());4950console.log('Current:', draft.getPreview());51// Current: ACME Corp: 3 items5253// Undo twice54let memento = manager.undo();55if (memento) draft.restore(memento);56console.log('After undo:', draft.getPreview());57// After undo: ACME Corp: 2 items5859memento = manager.undo();60if (memento) draft.restore(memento);61console.log('After undo:', draft.getPreview());62// After undo: ACME Corp: 1 items6364// Redo once65memento = manager.redo();66if (memento) draft.restore(memento);67console.log('After redo:', draft.getPreview());68// After redo: ACME Corp: 2 items
Memory Management Strategies
Strategy 1: Bounded History
Keep only the last N states to prevent unbounded memory growth:
typescript1class BoundedUndoManager<T> {2 private history: T[] = [];3 private currentIndex: number = -1;4 private maxSize: number;56 constructor(maxSize: number = 50) {7 this.maxSize = maxSize;8 }910 push(memento: T): void {11 // Remove future history12 this.history = this.history.slice(0, this.currentIndex + 1);1314 this.history.push(memento);15 this.currentIndex++;1617 // Keep only last N items18 if (this.history.length > this.maxSize) {19 const overflow = this.history.length - this.maxSize;20 this.history = this.history.slice(overflow);21 this.currentIndex -= overflow;22 }23 }2425 // ... rest of methods same as UndoRedoManager26}
Strategy 2: Time-Based Expiration
Remove old mementos based on age:
typescript1class TimeBoundedHistory<T extends { getTimestamp(): Date }> {2 private history: T[] = [];3 private maxAge: number; // milliseconds45 constructor(maxAgeMinutes: number = 60) {6 this.maxAge = maxAgeMinutes * 60 * 1000;7 }89 push(memento: T): void {10 this.cleanup(); // Remove expired first11 this.history.push(memento);12 }1314 private cleanup(): void {15 const now = Date.now();16 this.history = this.history.filter(memento => {17 const age = now - memento.getTimestamp().getTime();18 return age < this.maxAge;19 });20 }2122 getAll(): T[] {23 this.cleanup();24 return [...this.history];25 }26}
Strategy 3: Compression
Compress older mementos to save memory:
typescript1class CompressingHistory {2 private recent: OrderMemento[] = [];3 private compressed: CompressedMemento[] = [];4 private recentLimit: number = 10;56 push(memento: OrderMemento): void {7 this.recent.push(memento);89 // Compress oldest recent when limit exceeded10 if (this.recent.length > this.recentLimit) {11 const toCompress = this.recent.shift()!;12 this.compressed.push(new CompressedMemento(toCompress));13 }14 }1516 getRecent(): OrderMemento[] {17 return [...this.recent];18 }1920 getAll(): OrderMemento[] {21 return [22 ...this.compressed.map(c => c.decompress()),23 ...this.recent24 ];25 }26}
Serialization for Persistence
Strategy 1: Local Storage
typescript1class PersistentHistory {2 private storageKey: string;34 constructor(key: string = 'order-history') {5 this.storageKey = key;6 }78 save(mementos: OrderMemento[]): void {9 const serialized = mementos.map(m => ({10 state: m.getState(),11 timestamp: m.getTimestamp().toISOString()12 }));1314 localStorage.setItem(this.storageKey, JSON.stringify(serialized));15 }1617 load(): OrderMemento[] {18 const json = localStorage.getItem(this.storageKey);19 if (!json) return [];2021 const data = JSON.parse(json);22 return data.map((item: any) =>23 new OrderMemento(24 item.state.items,25 item.state.customer,26 new Date(item.timestamp)27 )28 );29 }3031 clear(): void {32 localStorage.removeItem(this.storageKey);33 }34}
Strategy 2: IndexedDB for Large Histories
typescript1class IndexedDBHistory {2 private dbName: string = 'order-history';3 private storeName: string = 'mementos';45 async save(id: string, memento: OrderMemento): Promise<void> {6 const db = await this.openDB();7 const transaction = db.transaction(this.storeName, 'readwrite');8 const store = transaction.objectStore(this.storeName);910 await store.put({11 id,12 state: memento.getState(),13 timestamp: memento.getTimestamp()14 });15 }1617 async load(id: string): Promise<OrderMemento | null> {18 const db = await this.openDB();19 const transaction = db.transaction(this.storeName, 'readonly');20 const store = transaction.objectStore(this.storeName);2122 const data = await store.get(id);23 if (!data) return null;2425 return new OrderMemento(26 data.state.items,27 data.state.customer,28 data.timestamp29 );30 }3132 async loadAll(): Promise<OrderMemento[]> {33 const db = await this.openDB();34 const transaction = db.transaction(this.storeName, 'readonly');35 const store = transaction.objectStore(this.storeName);3637 const all = await store.getAll();38 return all.map(data =>39 new OrderMemento(40 data.state.items,41 data.state.customer,42 data.timestamp43 )44 );45 }4647 private async openDB(): Promise<IDBDatabase> {48 return new Promise((resolve, reject) => {49 const request = indexedDB.open(this.dbName, 1);5051 request.onerror = () => reject(request.error);52 request.onsuccess = () => resolve(request.result);5354 request.onupgradeneeded = (event) => {55 const db = (event.target as IDBOpenDBRequest).result;56 if (!db.objectStoreNames.contains(this.storeName)) {57 db.createObjectStore(this.storeName, { keyPath: 'id' });58 }59 };60 });61 }62}
Memento vs Command for Undo
When to Use Memento
Memento is better when:
- State is complex with many interdependent fields
- Restoring requires setting entire object state
- Operations aren't easily reversible
- Need to compare states from different times
typescript1// Complex state - better with Memento2class RouteOptimizer {3 private stops: Stop[] = [];4 private vehicle: Vehicle;5 private constraints: Constraint[];6 private optimizationScore: number;78 // Memento captures entire complex state9 save(): RouteMemento {10 return new RouteMemento({11 stops: this.stops.map(s => s.clone()),12 vehicle: this.vehicle.clone(),13 constraints: [...this.constraints],14 optimizationScore: this.optimizationScore15 });16 }17}
When to Use Command
Command is better when:
- Operations are discrete and independent
- Each operation can be easily reversed
- Need to log/replay specific actions
- Operations have side effects to undo
typescript1// Discrete operations - better with Command2interface Command {3 execute(): void;4 undo(): void;5}67class AddStopCommand implements Command {8 constructor(9 private route: Route,10 private stop: Stop,11 private index: number12 ) {}1314 execute(): void {15 this.route.insertStop(this.index, this.stop);16 }1718 undo(): void {19 this.route.removeStopAt(this.index);20 }21}
Combining Memento and Command
For the best of both worlds:
typescript1class HybridHistory {2 private checkpoints: OrderMemento[] = [];3 private commands: Command[] = [];4 private commandsSinceCheckpoint: number = 0;56 executeCommand(command: Command): void {7 command.execute();8 this.commands.push(command);9 this.commandsSinceCheckpoint++;1011 // Create checkpoint every 10 commands12 if (this.commandsSinceCheckpoint >= 10) {13 this.createCheckpoint();14 }15 }1617 private createCheckpoint(): void {18 // Save current state as memento19 const memento = this.originator.save();20 this.checkpoints.push(memento);21 this.commandsSinceCheckpoint = 0;2223 // Clear old commands (they're now in checkpoint)24 this.commands = [];25 }2627 undo(): void {28 if (this.commands.length > 0) {29 // Undo using command30 const command = this.commands.pop()!;31 command.undo();32 this.commandsSinceCheckpoint--;33 } else if (this.checkpoints.length > 0) {34 // Restore from checkpoint35 const checkpoint = this.checkpoints.pop()!;36 this.originator.restore(checkpoint);37 }38 }39}
Best Practices
- Set Memory Limits: Always bound history size
- Compress Old Data: Use compression for archived mementos
- Use Timestamps: Tag mementos with creation time
- Implement Expiration: Remove old mementos periodically
- Consider Persistence: Save important states to storage
- Provide Metadata: Add descriptions for user-visible history
- Choose Right Pattern: Use Memento for state, Command for operations
Summary
- Undo/Redo requires managing a stack of mementos
- Memory management is critical for long-running applications
- Bounded history prevents unbounded memory growth
- Serialization enables persistence across sessions
- Memento for complex state, Command for discrete operations
- Hybrid approaches combine benefits of both patterns
The key is balancing memory usage with the ability to restore previous states effectively.