The Ghost Wire Bug: A Case Study in SVG DOM and Snapshot Corruption
TLDR
Assigning IDs to transparent SVG hitboxes causes element ID collisions when restoring DOM history snapshots. Purging non-essential hitboxes and omitting IDs from helper hitboxes prevents orphaned nodes from intercepting ID lookups during undo passes.| Element Role | ID Assignment | History Snapshot Behavior | Restoration Safety |
|---|---|---|---|
| Visual Path & Hitbox | Both assigned explicit IDs | Hitbox ID collides during restoration | Causes ghost elements |
| Visual Path Only | Only visual path receives ID | Hitboxes purged prior to restore | 100% Clean Restoration |
Assigning IDs to transparent hitboxes creates duplicate node collisions during history restoration
To make thin SVG connector lines easier to click, geometry engines create transparent, wide hitbox elements beneath each path. Initially, hitboxes were assigned IDs using a suffix scheme (${wire.id}-hit).
When deleting wires and restoring state via DOM history snapshots, orphaned hitboxes occasionally persisted in the tree. During subsequent undo operations, snapshot rehydrators matched incoming elements by ID, inadvertently linking visual properties to orphaned invisible hitboxes.
Purging auxiliary hitboxes and omitting IDs prevents orphaned nodes from intercepting lookups
We resolved snapshot collisions by stripping IDs from helper hitboxes and clearing auxiliary nodes before restoring state:
ID COLLISION CORRUPTION (ZOMBIE BUG):
[DOM Snapshot Contains: <path id="wire1">]
│
[Undo Restores Snapshot] ──> document.getElementById('wire1')
│
▼
[Finds Orphaned Hitbox!] ──> <path id="wire1-hitbox" class="transparent">
(Hitbox steals the properties, visual wire disappears!)
CLEAN ID-LESS RESTORATION:
[Hitboxes Have NO ID] ──> <path class="wire-hitbox">
│
[Undo Restores Snapshot] ──> document.getElementById('wire1')
│
▼
[Finds Correct Visual Node!] ──> <path id="wire1" stroke="blue">
(Restoration succeeds, generates fresh hitboxes)
Here is the updated logic handling hitbox generation:
// 1. Omit ID assignment during hitbox generation
export function createWireHitbox(wirePathEl) {
const hitbox = wirePathEl.cloneNode(false);
hitbox.setAttribute('class', 'wire-hitbox');
// ID intentionally omitted to prevent history snapshot collisions
return hitbox;
}
// 2. Purge hitboxes prior to DOM snapshot restoration export function restoreCanvasSnapshot(snapshotHtml) { // Clear all orphaned hitboxes before replacing innerHTML document.querySelectorAll('.wire-hitbox').forEach(el => el.remove()); canvasContainer.innerHTML = snapshotHtml; regenerateHitboxes(); // Re-build hitboxes fresh from restored visual paths }
Omitting IDs from non-essential UI helpers keeps the history restorer focused exclusively on primary data elements.
Rule of thumb: Omit ID attributes from temporary click targets or hitboxes to avoid history snapshot collisions.