Copy-on-Write in the Browser: How TAFNE's CellStore Uses UUIDs and Reference Counting
Most browser table editors store cell data in the DOM. The value lives in the <td> element. You read it, you write it, it stays there. Simple.
TAFNE takes a slightly different approach for its cell data layer: a flat key-value store backed by UUIDs and reference counting, with copy-on-write semantics when cells are shared.
It's worth explaining why, and what the implementation actually looks like.
The Problem With Storing Everything in the DOM
Storing cell values directly in DOM elements works until you need to do something non-trivial with them.
Consider what happens when you duplicate a row. If you clone the DOM node, the copy and the original share the same visual content, but they're independent DOM elements. Any edit to one doesn't affect the other.
That's fine for simple cases. But if you want to track cell identity, for formula references, for undo granularity finer than the full HTML snapshot, for future features like formula dependencies, you need a way to say "this cell and that cell refer to the same underlying value." The DOM gives you no vocabulary for that.
Copy-on-write (COW) is the standard solution. Multiple handles can point to the same data. When one handle tries to modify its data, it first creates a private copy. The other handles still see the original. No mutation propagates unexpectedly.
The CellStore
The store is a global flat object, window.CellStore, managed through a class:
class CellStoreManager {
constructor() {
this.store = window.CellStore;
}
create(value, type = 'string') { const id = crypto.randomUUID(); this.store[id] = { value, type, refCount: 1 }; return id; } }
create() generates a UUID using the browser's native crypto.randomUUID(): cryptographically random, collision-resistant, no external library needed. The entry stores the value, a type hint, and a reference count starting at 1.
The UUID is the stable identifier for a piece of cell data. The DOM element that displays it holds the UUID as a data attribute. The value lives in the store, not in the DOM.
Copy-on-Write: The deref Method
This is the core of the COW semantics:
deref(id) {
const cell = this.store[id];
if (!cell) return null;
if (cell.refCount > 1) {
cell.refCount--;
return this.create(cell.value, cell.type);
}
return id;
}
When a cell is about to be mutated, it calls deref first. If the reference count is greater than 1, meaning multiple handles share this data, the method decrements the original's count and creates a new private copy, returning the new UUID. If the reference count is already 1, the cell is not shared, and the existing ID is returned for in-place mutation.
The caller never needs to think about whether the data is shared. It always calls deref before writing. The store handles the rest.
Reference Management
addRef(id) {
if (this.store[id]) this.store[id].refCount++;
}
release(id) { if (!this.store[id]) return; this.store[id].refCount--; if (this.store[id].refCount <= 0) delete this.store[id]; }
addRef increments the count. release decrements it and deletes the entry when it reaches zero. This is manual reference counting, the same pattern used in C++ smart pointers, Python's garbage collector, and countless other runtimes.
The advantage over garbage collection is predictability. Memory is freed the moment the last reference is released, not at some future collection point.
Snapshot and Restore
The store supports full snapshots for the history system:
snapshot() {
return JSON.parse(JSON.stringify(this.store));
}
restore(snap) { window.CellStore = JSON.parse(JSON.stringify(snap)); this.store = window.CellStore; }
Deep copy both ways. The snapshot is an independent clone of the entire store at a point in time, which can be saved to the history stack and restored on undo without affecting the live store.
Where This Fits in the Architecture
The CellStore is currently one of the more forward-looking pieces of TAFNE's architecture. The table editor's undo system works at the HTML snapshot level. It doesn't use the store for undo. The store's full value would be realized when the Node Editor and formula system are fully wired: formula nodes would hold references into the store, and changes would propagate through the reference graph rather than requiring a full snapshot.
In the current version, the store is loaded and available, but its main role is establishing the infrastructure for that future capability. The UUID scheme, the COW semantics, and the reference counting are all there, waiting for the formula layer to use them.
It's the foundation before the building. Which is a reasonable place to be.
Source: github.com/carnworkstudios/TAFNE