When Undo Lies: Fixing a Shared History Stack in a Multi-Table Editor
TLDR
Sharing a single undo history array across multiple spreadsheet tables causes cross-context state pollution. Editing table A consumes available history slots for table B, resulting in inaccurate undo depths. Replacing global arrays with aMap keyed by ${sheetId}::${tableId} gives every table an independent 50-state stack.
| History Architecture | Storage Scope | Table Switch Behavior | Undo Isolation |
|---|---|---|---|
| Global Shared Array | #tableContainer innerHTML | Overwrites unrelated tables | Fails (Edits contaminate other tables) |
| Per-Slot Context Map | Target table.outerHTML | In-place replaceWith swap | 100% Independent Per Table |
Global history arrays pollute undo buffers and trigger cross-context data corruption
Our initial history manager stored full container snapshots in a global 50-item array. In multi-table documents, editing table A ten times consumed 10 slots from the global history pool.
When users switched to table B and pressed undo, the manager restored a snapshot captured during table A's editing sequence, corrupting table B's state.
Keying undo stacks in a Map and swapping targeted DOM elements isolates editing context
To isolate history stacks per table, we replaced the shared array with a Map keyed by ${sheetId}::${tableId}:
GLOBAL SHARED HISTORY (Vulnerable to cross-pollution):
[Edits on Table A] ──┐
[Edits on Table B] ──┼─> [ Single Stack: [A1, A2, B1, A3, B2] ]
[Edits on Table C] ──┘
(Undo on Table B restores A3! State corrupted!)
KEYED HISTORY MAP (Isolated contexts): Map: ┌─────────────────┬──────────────────────────────────────┐ │ Context Key │ History Stack │ ├─────────────────┼──────────────────────────────────────┤ │ Sheet1::TableA │ [ A1, A2, A3 ] │ │ Sheet1::TableB │ [ B1, B2 ] │ │ Sheet2::TableC │ [ C1 ] │ └─────────────────┴──────────────────────────────────────┘ (Undo on Table B restores B1. Safe and isolated!)
Here is the implementation of our per-slot stack isolation:
// Per-slot history manager storing targeted table HTML snapshots
const slotHistoryMap = new Map();
let isRestoringState = false;
function getContextKey() { const sheetId = window.activeSheetId || 'default'; const tableId = window.currentTable ? window.currentTable.getAttribute('data-tifany-id') : 'default'; return ${sheetId}::${tableId}; }
export function saveSlotState() { if (isRestoringState || !window.currentTable) return; const key = getContextKey(); if (!slotHistoryMap.has(key)) { slotHistoryMap.set(key, { history: [], index: -1 }); }
const slot = slotHistoryMap.get(key); const tableHtml = window.currentTable.outerHTML;
if (slot.index >= 0 && slot.history[slot.index] === tableHtml) return;
slot.history = slot.history.slice(0, slot.index + 1); slot.history.push(tableHtml); if (slot.history.length > 50) slot.history.shift(); else slot.index++; }
During restoration, the editor replaces only the target table element using replaceWith(), keeping adjacent tables untouched.
Rule of thumb: Key history stacks to active context IDs (sheetId::tableId) and use in-place DOM replacements to isolate undo actions to a single component.