UNPKG

@chainsafe/ssz

Version:

Simple Serialize

281 lines 12.2 kB
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