electrum-store
is
getting more and more mature. It provides a simple model of a writable
immutable tree. This is an oxymoron, but is nevertheless true. Let
me explain it here…
The tree is managed by the Store
. At the root of the tree is the root
node.
const store = Store.create ();
const root = store.root;
Every node in the tree is represented by an instance of the State
class. The node can store values, which are accessed with get (id)
.
Just like the tree itself, every node is immutable, yet writable.
Walking the tree
Any node in the tree can be reached directly with select (path)
or find (path)
:
const store = Store.create ();
const node = store.select ('staff.peter.age');
Selecting a node which does not exist in the tree will create it and produce a new instance of the whole tree. This means that if a user holds a reference onto the old tree, she won’t see any change. To see the updated tree, she has to query the store again:
const store = Store.create ();
const root1 = store.root;
const node1 = store.select ('a.b.c');
const root2 = store.root; // this is the root of the new tree
expect (root1.find ('a.b.c')).to.not.exist ();
expect (root2.find ('a.b.c')).to.exist ();
What’s happening under the covers is that any modification of a node (such as adding a new child node or setting a value) will create a copy of the node, recursively up to the root of the tree.
The original data structure is immutable but the API exposes mutation methods which produce new snapshots of the immutable tree, hence making the tree writable.
Beware of the writable immutability
When mutating the immutable tree or its nodes, it is important to keep in mind what’s happening behind the scenes. This code will not produce what would be expected of a mutable data structure:
const store = Store.create ();
const node = store.select ('a.b');
node.set ('x', 10); // node remains unchanged
node.set ('y', 20); // node remains unchanged
In this example, the resulting tree would have a node a.b
with
a single value y
set to 20:
node.set ('x', 10)
→ produces a new node fora.b
, which in turn updates nodea
and the root of the tree. At this point in time,store.find ('a.b').get ('x') === 10
.node.set ('y', 20)
→ starts with the node of the original tree (where nodea.b
is empy), since thenode
instance was not updated, but copied; so this call produces another node fora.b
, which in turn updates nodea
and the root of the tree. At this point in time,store.find ('a.b').get ('y') === 20
.
The tree where the value x
was set to 10
was lost.
To set both x
and y
values on the node a.b
, the code has to
refresh the node being used:
const store = Store.create ();
let node = store.select ('a.b');
node = node.set ('x', 10);
node = node.set ('y', 20);
As you can see, set ('x', 10)
returns a new node instance, which
belongs to the new tree. Applying set ('y', 20)
on the new node
will update the new tree, thus resulting in the expected outcome:
expect (store.find ('a.b').get ('x')).to.equal (10);
expect (store.find ('a.b').get ('y')).to.equal (20);