15 minlesson

Managing Memento History

Managing Memento History

Undo/Redo with Memento Stack

The most common use of mementos is implementing undo/redo functionality. Here's a complete implementation:

typescript
1class UndoRedoManager<T> {
2 private history: T[] = [];
3 private currentIndex: number = -1;
4
5 // Save current state
6 push(memento: T): void {
7 // Remove any "future" history when adding new state
8 this.history = this.history.slice(0, this.currentIndex + 1);
9
10 this.history.push(memento);
11 this.currentIndex++;
12 }
13
14 // Undo to previous state
15 undo(): T | null {
16 if (!this.canUndo()) {
17 return null;
18 }
19
20 this.currentIndex--;
21 return this.history[this.currentIndex];
22 }
23
24 // Redo to next state
25 redo(): T | null {
26 if (!this.canRedo()) {
27 return null;
28 }
29
30 this.currentIndex++;
31 return this.history[this.currentIndex];
32 }
33
34 // Get current state without changing position
35 current(): T | null {
36 if (this.currentIndex < 0) {
37 return null;
38 }
39 return this.history[this.currentIndex];
40 }
41
42 canUndo(): boolean {
43 return this.currentIndex > 0;
44 }
45
46 canRedo(): boolean {
47 return this.currentIndex < this.history.length - 1;
48 }
49
50 clear(): void {
51 this.history = [];
52 this.currentIndex = -1;
53 }
54
55 getHistorySize(): number {
56 return this.history.length;
57 }
58}

Using the Undo/Redo Manager

typescript
1class OrderDraft {
2 private items: string[] = [];
3 private customer: string = '';
4
5 addItem(item: string): void {
6 this.items.push(item);
7 }
8
9 setCustomer(customer: string): void {
10 this.customer = customer;
11 }
12
13 save(): OrderMemento {
14 return new OrderMemento(
15 [...this.items],
16 this.customer,
17 new Date()
18 );
19 }
20
21 restore(memento: OrderMemento): void {
22 const state = memento.getState();
23 this.items = [...state.items];
24 this.customer = state.customer;
25 }
26
27 getPreview(): string {
28 return `${this.customer || '(no customer)'}: ${this.items.length} items`;
29 }
30}
31
32// Usage
33const draft = new OrderDraft();
34const manager = new UndoRedoManager<OrderMemento>();
35
36// Initial save
37draft.setCustomer('ACME Corp');
38manager.push(draft.save());
39
40// Make changes and save
41draft.addItem('Widget A');
42manager.push(draft.save());
43
44draft.addItem('Widget B');
45manager.push(draft.save());
46
47draft.addItem('Widget C');
48manager.push(draft.save());
49
50console.log('Current:', draft.getPreview());
51// Current: ACME Corp: 3 items
52
53// Undo twice
54let memento = manager.undo();
55if (memento) draft.restore(memento);
56console.log('After undo:', draft.getPreview());
57// After undo: ACME Corp: 2 items
58
59memento = manager.undo();
60if (memento) draft.restore(memento);
61console.log('After undo:', draft.getPreview());
62// After undo: ACME Corp: 1 items
63
64// Redo once
65memento = 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:

typescript
1class BoundedUndoManager<T> {
2 private history: T[] = [];
3 private currentIndex: number = -1;
4 private maxSize: number;
5
6 constructor(maxSize: number = 50) {
7 this.maxSize = maxSize;
8 }
9
10 push(memento: T): void {
11 // Remove future history
12 this.history = this.history.slice(0, this.currentIndex + 1);
13
14 this.history.push(memento);
15 this.currentIndex++;
16
17 // Keep only last N items
18 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 }
24
25 // ... rest of methods same as UndoRedoManager
26}

Strategy 2: Time-Based Expiration

Remove old mementos based on age:

typescript
1class TimeBoundedHistory<T extends { getTimestamp(): Date }> {
2 private history: T[] = [];
3 private maxAge: number; // milliseconds
4
5 constructor(maxAgeMinutes: number = 60) {
6 this.maxAge = maxAgeMinutes * 60 * 1000;
7 }
8
9 push(memento: T): void {
10 this.cleanup(); // Remove expired first
11 this.history.push(memento);
12 }
13
14 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 }
21
22 getAll(): T[] {
23 this.cleanup();
24 return [...this.history];
25 }
26}

Strategy 3: Compression

Compress older mementos to save memory:

typescript
1class CompressingHistory {
2 private recent: OrderMemento[] = [];
3 private compressed: CompressedMemento[] = [];
4 private recentLimit: number = 10;
5
6 push(memento: OrderMemento): void {
7 this.recent.push(memento);
8
9 // Compress oldest recent when limit exceeded
10 if (this.recent.length > this.recentLimit) {
11 const toCompress = this.recent.shift()!;
12 this.compressed.push(new CompressedMemento(toCompress));
13 }
14 }
15
16 getRecent(): OrderMemento[] {
17 return [...this.recent];
18 }
19
20 getAll(): OrderMemento[] {
21 return [
22 ...this.compressed.map(c => c.decompress()),
23 ...this.recent
24 ];
25 }
26}

Serialization for Persistence

Strategy 1: Local Storage

typescript
1class PersistentHistory {
2 private storageKey: string;
3
4 constructor(key: string = 'order-history') {
5 this.storageKey = key;
6 }
7
8 save(mementos: OrderMemento[]): void {
9 const serialized = mementos.map(m => ({
10 state: m.getState(),
11 timestamp: m.getTimestamp().toISOString()
12 }));
13
14 localStorage.setItem(this.storageKey, JSON.stringify(serialized));
15 }
16
17 load(): OrderMemento[] {
18 const json = localStorage.getItem(this.storageKey);
19 if (!json) return [];
20
21 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 }
30
31 clear(): void {
32 localStorage.removeItem(this.storageKey);
33 }
34}

Strategy 2: IndexedDB for Large Histories

typescript
1class IndexedDBHistory {
2 private dbName: string = 'order-history';
3 private storeName: string = 'mementos';
4
5 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);
9
10 await store.put({
11 id,
12 state: memento.getState(),
13 timestamp: memento.getTimestamp()
14 });
15 }
16
17 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);
21
22 const data = await store.get(id);
23 if (!data) return null;
24
25 return new OrderMemento(
26 data.state.items,
27 data.state.customer,
28 data.timestamp
29 );
30 }
31
32 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);
36
37 const all = await store.getAll();
38 return all.map(data =>
39 new OrderMemento(
40 data.state.items,
41 data.state.customer,
42 data.timestamp
43 )
44 );
45 }
46
47 private async openDB(): Promise<IDBDatabase> {
48 return new Promise((resolve, reject) => {
49 const request = indexedDB.open(this.dbName, 1);
50
51 request.onerror = () => reject(request.error);
52 request.onsuccess = () => resolve(request.result);
53
54 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
typescript
1// Complex state - better with Memento
2class RouteOptimizer {
3 private stops: Stop[] = [];
4 private vehicle: Vehicle;
5 private constraints: Constraint[];
6 private optimizationScore: number;
7
8 // Memento captures entire complex state
9 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.optimizationScore
15 });
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
typescript
1// Discrete operations - better with Command
2interface Command {
3 execute(): void;
4 undo(): void;
5}
6
7class AddStopCommand implements Command {
8 constructor(
9 private route: Route,
10 private stop: Stop,
11 private index: number
12 ) {}
13
14 execute(): void {
15 this.route.insertStop(this.index, this.stop);
16 }
17
18 undo(): void {
19 this.route.removeStopAt(this.index);
20 }
21}

Combining Memento and Command

For the best of both worlds:

typescript
1class HybridHistory {
2 private checkpoints: OrderMemento[] = [];
3 private commands: Command[] = [];
4 private commandsSinceCheckpoint: number = 0;
5
6 executeCommand(command: Command): void {
7 command.execute();
8 this.commands.push(command);
9 this.commandsSinceCheckpoint++;
10
11 // Create checkpoint every 10 commands
12 if (this.commandsSinceCheckpoint >= 10) {
13 this.createCheckpoint();
14 }
15 }
16
17 private createCheckpoint(): void {
18 // Save current state as memento
19 const memento = this.originator.save();
20 this.checkpoints.push(memento);
21 this.commandsSinceCheckpoint = 0;
22
23 // Clear old commands (they're now in checkpoint)
24 this.commands = [];
25 }
26
27 undo(): void {
28 if (this.commands.length > 0) {
29 // Undo using command
30 const command = this.commands.pop()!;
31 command.undo();
32 this.commandsSinceCheckpoint--;
33 } else if (this.checkpoints.length > 0) {
34 // Restore from checkpoint
35 const checkpoint = this.checkpoints.pop()!;
36 this.originator.restore(checkpoint);
37 }
38 }
39}

Best Practices

  1. Set Memory Limits: Always bound history size
  2. Compress Old Data: Use compression for archived mementos
  3. Use Timestamps: Tag mementos with creation time
  4. Implement Expiration: Remove old mementos periodically
  5. Consider Persistence: Save important states to storage
  6. Provide Metadata: Add descriptions for user-visible history
  7. 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.