Copy-on-Write in the Browser: How TAFNE's CellStore Uses UUIDs and Reference Counting
TLDR
Storing spreadsheet cell values directly inside HTML<td> elements limits data tracking and formula dependency graphs. Moving cell data to a flat, UUID-keyed store (CellStore) with copy-on-write semantics ensures multiple visual handles can reference shared content safely without accidental mutations.
| Architecture Model | Storage Location | Mutation Strategy | Shared Identity Tracking |
|---|---|---|---|
| DOM-Direct Storage | <td> Element innerText | In-place overwrite | None (Elements are isolated) |
| Copy-on-Write CellStore | Global Flat Object (CellStore) | deref() Copy-on-Write | UUID Reference Counting |
Coupling data to DOM elements breaks reference sharing and limits state tracking
Most web table editors store cell values directly inside <td> DOM nodes. While straightforward for basic text entry, this creates massive roadblocks when row duplications, formula calculations, or granular undo systems need to share state across cells.
When cloning rows, DOM nodes remain separate elements without shared identity metrics. To enable formula dependencies where multiple cells reference identical underlying values, we needed a structured data layer separate from presentation nodes.
Copy-on-write dereferencing isolates shared cell mutations without duplicating memory upfront
We decoupled cell content from DOM elements by storing values in a centralized CellStore managed by unique identifiers (crypto.randomUUID()):
INITIAL STATE: Cells share the same data reference
┌──────────────────┐ ┌──────────────────┐
│ Cell A (DOM) │ │ Cell B (DOM) │
│ [UUID: 001] │ │ [UUID: 001] │
└────────┬─────────┘ └────────┬─────────┘
│ │
└───────────────┬──────────────────┘
│
▼
┌──────────────────────────┐
│ CellStore [UUID: 001] │
│ value: "Hello" │
│ refCount: 2 │
└──────────────────────────┘
AFTER MUTATING CELL B: deref() forks the write ┌──────────────────┐ ┌──────────────────┐ │ Cell A (DOM) │ │ Cell B (DOM) │ │ [UUID: 001] │ │ [UUID: 999] │ └────────┬─────────┘ └────────┬─────────┘ │ │ ▼ ▼ ┌──────────────────────────┐ ┌──────────────────────────┐ │ CellStore [UUID: 001] │ │ CellStore [UUID: 999] │ │ value: "Hello" │ │ value: "World" │ │ refCount: 1 │ │ refCount: 1 │ └──────────────────────────┘ └──────────────────────────┘
Here is how we implemented the centralized manager:
class CellStoreManager {
constructor() {
this.store = window.CellStore || {};
}
create(value, type = 'string') { const id = crypto.randomUUID(); this.store[id] = { value, type, refCount: 1 }; return id; }
deref(id) { const cell = this.store[id]; if (!cell) return null; // Copy-on-write: Duplicate data if shared across multiple references if (cell.refCount > 1) { cell.refCount--; return this.create(cell.value, cell.type); } return id; } }
Before mutating a cell, the editor calls deref(id). If refCount exceeds 1, the manager decrements the original count and creates a private copy with a new UUID. If the cell is unshared, it mutates in place safely.
Rule of thumb: Call deref() before editing shared cells to decrement reference counts and fork mutations into new memory blocks dynamically.