@chainsafe/ssz
Version:
Simple Serialize
281 lines • 12.2 kB
JavaScript
import { getHashComputations, getNodeAtDepth, getNodesAtDepth, setNodesAtDepth, } from "@chainsafe/persistent-merkle-tree";
import { TreeViewDU } from "./abstract.js";
export class ArrayCompositeTreeViewDU extends TreeViewDU {
type;
_rootNode;
nodes;
caches;
viewsChanged = new Map();
_length;
// TODO: Consider these properties are not accessible in the cache object persisted in the parent's cache.
// nodes, caches, _length, and nodesPopulated are mutated. Consider having them in a _cache object such that
// mutations affect the cache already found in the parent object
dirtyLength = false;
nodesPopulated;
constructor(type, _rootNode, cache) {
super();
this.type = type;
this._rootNode = _rootNode;
if (cache) {
this.nodes = cache.nodes;
this.caches = cache.caches;
this._length = cache.length;
this.nodesPopulated = cache.nodesPopulated;
}
else {
this.nodes = [];
this.caches = [];
this._length = this.type.tree_getLength(_rootNode);
// If there are exactly 0 nodes, nodesPopulated = true because 0 / 0 are in the nodes array
this.nodesPopulated = this._length === 0;
}
}
/**
* Number of elements in the array. Equal to un-commited length of the array
*/
get length() {
return this._length;
}
get node() {
return this._rootNode;
}
get cache() {
return {
nodes: this.nodes,
caches: this.caches,
length: this._length,
nodesPopulated: this.nodesPopulated,
};
}
/**
* Get element at `index`. Returns a view of the Composite element type.
*
* NOTE: Assumes that any view created here will change and will call .commit() on it.
* .get() should be used only for cases when something may mutate. To get all items without
* triggering a .commit() in all them use .getAllReadOnly().
*/
get(index) {
const viewChanged = this.viewsChanged.get(index);
if (viewChanged) {
return viewChanged;
}
let node = this.nodes[index];
if (node === undefined) {
node = getNodeAtDepth(this._rootNode, this.type.depth, index);
this.nodes[index] = node;
}
// Keep a reference to the new view to call .commit on it latter, only if mutable
const view = this.type.elementType.getViewDU(node, this.caches[index]);
if (this.type.elementType.isViewMutable) {
this.viewsChanged.set(index, view);
}
// No need to persist the child's view cache since a second get returns this view instance.
// The cache is only persisted on commit where the viewsChanged map is dropped.
return view;
}
/**
* Get element at `index`. Returns a view of the Composite element type.
* DOES NOT PROPAGATE CHANGES: use only for reads and to skip parent references.
*/
getReadonly(index) {
const viewChanged = this.viewsChanged.get(index);
if (viewChanged) {
return viewChanged;
}
let node = this.nodes[index];
if (node === undefined) {
node = getNodeAtDepth(this._rootNode, this.type.depth, index);
this.nodes[index] = node;
}
return this.type.elementType.getViewDU(node, this.caches[index]);
}
// Did not implemented
// `getReadonlyValue(index: number): ValueOf<ElementType>`
// because it can break in unexpected ways if there are pending changes in this.viewsChanged.
// This function could first check if `this.viewsChanged` has a view for `index` and commit it,
// but that would be pretty slow, and the same result can be achieved with
// `this.getReadonly(index).toValue()`
/**
* Set Composite element type `view` at `index`
*/
set(index, view) {
if (index >= this._length) {
throw Error(`Error setting index over length ${index} > ${this._length}`);
}
// When setting a view:
// - Not necessary to commit node
// - Not necessary to persist cache
// Just keeping a reference to the view in this.viewsChanged ensures consistency
this.viewsChanged.set(index, view);
}
/**
* Returns all elements at every index, if an index is modified it will return the modified view.
* No need to commit() before calling this function.
* @param views optional output parameter, if is provided it must be an array of the same length as this array
*/
getAllReadonly(views) {
if (views && views.length !== this._length) {
throw Error(`Expected ${this._length} views, got ${views.length}`);
}
this.populateAllOldNodes();
views = views ?? new Array(this._length);
for (let i = 0; i < this._length; i++) {
// this will get pending change first, if not it will get from the `this.nodes` array
views[i] = this.getReadonly(i);
}
return views;
}
/**
* Apply `fn` to each ViewDU in the array.
* Similar to getAllReadOnly(), no need to commit() before calling this function.
* if an item is modified it will return the modified view.
*/
forEach(fn) {
this.populateAllOldNodes();
for (let i = 0; i < this._length; i++) {
fn(this.getReadonly(i), i);
}
}
/**
* WARNING: Returns all commited changes, if there are any pending changes commit them beforehand
* @param values optional output parameter, if is provided it must be an array of the same length as this array
*/
getAllReadonlyValues(values) {
if (values && values.length !== this._length) {
throw Error(`Expected ${this._length} values, got ${values.length}`);
}
this.populateAllNodes();
values = values ?? new Array(this._length);
for (let i = 0; i < this._length; i++) {
values[i] = this.type.elementType.tree_toValue(this.nodes[i]);
}
return values;
}
/**
* Apply `fn` to each value in the array
*/
forEachValue(fn) {
this.populateAllNodes();
for (let i = 0; i < this._length; i++) {
fn(this.type.elementType.tree_toValue(this.nodes[i]), i);
}
}
/**
* Get by range of indexes. Returns an array of views of the Composite element type.
* This is similar to getAllReadonly() where we dont have to commit() before calling this function.
*/
getReadonlyByRange(startIndex, count) {
if (startIndex < 0) {
throw Error(`Error getting by range, startIndex < 0: ${startIndex}`);
}
if (count <= 0) {
throw Error(`Error getting by range, count <= 0: ${count}`);
}
const originalLength = this.dirtyLength ? this.type.tree_getLength(this._rootNode) : this._length;
if (startIndex >= originalLength) {
throw Error(`Error getting by range, startIndex >= length: ${startIndex} >= ${originalLength}`);
}
count = Math.min(count, originalLength - startIndex);
let dataAvailable = true;
for (let i = startIndex; i < startIndex + count; i++) {
if (this.nodes[i] == null) {
dataAvailable = false;
break;
}
}
// if one of nodes is not available, get all nodes at depth
if (!dataAvailable) {
const nodes = getNodesAtDepth(this._rootNode, this.type.depth, startIndex, count);
for (const [i, node] of nodes.entries()) {
this.nodes[startIndex + i] = node;
}
}
const result = new Array(count);
for (let i = 0; i < count; i++) {
result[i] = this.getReadonly(startIndex + i);
}
return result;
}
/**
* When we need to compute HashComputations (hcByLevel != null):
* - if old _rootNode is hashed, then only need to put pending changes to hcByLevel
* - if old _rootNode is not hashed, need to traverse and put to hcByLevel
*/
commit(hcOffset = 0, hcByLevel = null) {
const isOldRootHashed = this._rootNode.h0 !== null;
if (this.viewsChanged.size === 0) {
if (!isOldRootHashed && hcByLevel !== null) {
getHashComputations(this._rootNode, hcOffset, hcByLevel);
}
return;
}
// each view may mutate hcByLevel at offset + depth
const offsetView = hcOffset + this.type.depth;
// Depth includes the extra level for the length node
const byLevelView = hcByLevel != null && isOldRootHashed ? hcByLevel : null;
const nodesChanged = [];
for (const [index, view] of this.viewsChanged) {
const node = this.type.elementType.commitViewDU(view, offsetView, byLevelView);
// there's a chance the view is not changed, no need to rebind nodes in that case
if (this.nodes[index] !== node) {
// Set new node in nodes array to ensure data represented in the tree and fast nodes access is equal
this.nodes[index] = node;
nodesChanged.push({ index, node });
}
// Cache the view's caches to preserve it's data after 'this.viewsChanged.clear()'
const cache = this.type.elementType.cacheOfViewDU(view);
if (cache)
this.caches[index] = cache;
}
// TODO: Optimize to loop only once, Numerical sort ascending
const nodesChangedSorted = nodesChanged.sort((a, b) => a.index - b.index);
const indexes = nodesChangedSorted.map((entry) => entry.index);
const nodes = nodesChangedSorted.map((entry) => entry.node);
const chunksNode = this.type.tree_getChunksNode(this._rootNode);
const offsetThis = hcOffset + this.type.tree_chunksNodeOffset();
const byLevelThis = hcByLevel != null && isOldRootHashed ? hcByLevel : null;
const newChunksNode = setNodesAtDepth(chunksNode, this.type.chunkDepth, indexes, nodes, offsetThis, byLevelThis);
this._rootNode = this.type.tree_setChunksNode(this._rootNode, newChunksNode, this.dirtyLength ? this._length : null, hcOffset, hcByLevel);
if (!isOldRootHashed && hcByLevel !== null) {
getHashComputations(this._rootNode, hcOffset, hcByLevel);
}
this.viewsChanged.clear();
this.dirtyLength = false;
}
clearCache() {
this.nodes = [];
this.caches = [];
this.nodesPopulated = false;
// It's not necessary to clear this.viewsChanged since they have no effect on the cache.
// However preserving _SOME_ caches results in a very unpredictable experience.
this.viewsChanged.clear();
// Reset cached length only if it has been mutated
if (this.dirtyLength) {
this._length = this.type.tree_getLength(this._rootNode);
this.dirtyLength = false;
}
}
populateAllNodes() {
// If there's uncommited changes it may break.
// this.length can be increased but this._rootNode doesn't have that item
if (this.viewsChanged.size > 0) {
throw Error("Must commit changes before reading all nodes");
}
if (!this.nodesPopulated) {
this.nodes = getNodesAtDepth(this._rootNode, this.type.depth, 0, this.length);
this.nodesPopulated = true;
}
}
/**
* Similar to `populateAllNodes` but this does not require a commit() before reading all nodes.
* If there are pendingChanges, they will NOT be included in the `nodes` array.
*/
populateAllOldNodes() {
if (!this.nodesPopulated) {
const originalLength = this.dirtyLength ? this.type.tree_getLength(this._rootNode) : this._length;
this.nodes = getNodesAtDepth(this._rootNode, this.type.depth, 0, originalLength);
this.nodesPopulated = true;
}
}
}
//# sourceMappingURL=arrayComposite.js.map