@chainsafe/ssz
Version:
Simple Serialize
234 lines • 10.9 kB
JavaScript
import { getHashComputations, getNodeAtDepth, setNodesAtDepth, } from "@chainsafe/persistent-merkle-tree";
import { isBasicType } from "../type/basic.js";
import { isCompositeType } from "../type/composite.js";
import { TreeViewDU } from "./abstract.js";
export class BasicContainerTreeViewDU extends TreeViewDU {
type;
_rootNode;
nodes = [];
caches;
nodesChanged = new Set();
viewsChanged = new Map();
nodesPopulated;
constructor(type, _rootNode, cache) {
super();
this.type = type;
this._rootNode = _rootNode;
if (cache) {
this.nodes = cache.nodes;
this.caches = cache.caches;
this.nodesPopulated = cache.nodesPopulated;
}
else {
this.nodes = [];
this.caches = [];
this.nodesPopulated = false;
}
}
get node() {
return this._rootNode;
}
get cache() {
return {
nodes: this.nodes,
caches: this.caches,
nodesPopulated: this.nodesPopulated,
};
}
/**
* 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.nodesChanged.size === 0 && 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;
// if old root is not hashed, no need to pass hcByLevel to child view bc we need to do full traversal here
const byLevelView = hcByLevel != null && isOldRootHashed ? hcByLevel : null;
const nodesChanged = [];
for (const [index, view] of this.viewsChanged) {
const fieldType = this.type.fieldsEntries[index].fieldType;
const node = fieldType.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 = fieldType.cacheOfViewDU(view);
if (cache)
this.caches[index] = cache;
}
for (const index of this.nodesChanged) {
nodesChanged.push({ index, node: this.nodes[index] });
}
// TODO: Optimize to loop only once, Numerical sort ascending
const nodesChangedSorted = nodesChanged.sort((a, b) => a.index - b.index);
const { indexes, nodes } = this.parseNodesChanged(nodesChangedSorted);
this._rootNode = setNodesAtDepth(this._rootNode, this.type.depth, indexes, nodes, hcOffset, isOldRootHashed ? hcByLevel : null);
// old root is not hashed, need to traverse
if (!isOldRootHashed && hcByLevel !== null) {
getHashComputations(this._rootNode, hcOffset, hcByLevel);
}
this.nodesChanged.clear();
this.viewsChanged.clear();
}
parseNodesChanged(nodes) {
const indexes = nodes.map((entry) => entry.index);
const nodesArray = nodes.map((entry) => entry.node);
return { indexes, nodes: nodesArray };
}
clearCache() {
this.nodes = [];
this.caches = [];
this.nodesPopulated = false;
// Must clear nodesChanged, otherwise a subsequent commit call will break, because it assumes a node is there
this.nodesChanged.clear();
// 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();
}
}
class ContainerTreeViewDU extends BasicContainerTreeViewDU {
type;
_rootNode;
constructor(type, _rootNode, cache) {
super(type, _rootNode, cache);
this.type = type;
this._rootNode = _rootNode;
}
/**
* Same method to `type/container.ts` that call ViewDU.serializeToBytes() of internal fields.
*/
serializeToBytes(output, offset) {
this.commit();
let fixedIndex = offset;
let variableIndex = offset + this.type.fixedEnd;
for (let index = 0; index < this.type.fieldsEntries.length; index++) {
const { fieldType } = this.type.fieldsEntries[index];
let node = this.nodes[index];
if (node === undefined) {
node = getNodeAtDepth(this._rootNode, this.type.depth, index);
this.nodes[index] = node;
}
if (fieldType.fixedSize === null) {
// write offset
output.dataView.setUint32(fixedIndex, variableIndex - offset, true);
fixedIndex += 4;
// write serialized element to variable section
// basic types always have fixedSize
if (isCompositeType(fieldType)) {
const view = fieldType.getViewDU(node, this.caches[index]);
if (view.serializeToBytes !== undefined) {
variableIndex = view.serializeToBytes(output, variableIndex);
}
else {
// some types don't define ViewDU as TreeViewDU, like the UnionType, in that case view.serializeToBytes = undefined
variableIndex = fieldType.tree_serializeToBytes(output, variableIndex, node);
}
}
}
else {
fixedIndex = fieldType.tree_serializeToBytes(output, fixedIndex, node);
}
}
return variableIndex;
}
}
export function getContainerTreeViewDUClass(type) {
class CustomContainerTreeViewDU extends ContainerTreeViewDU {
}
// Dynamically define prototype methods
for (let index = 0; index < type.fieldsEntries.length; index++) {
const { fieldName, fieldType } = type.fieldsEntries[index];
// If the field type is basic, the value to get and set will be the actual 'struct' value (i.e. a JS number).
// The view must use the tree_getFromNode() and tree_setToNode() methods to persist the struct data to the node,
// and use the cached views array to store the new node.
if (isBasicType(fieldType)) {
Object.defineProperty(CustomContainerTreeViewDU.prototype, fieldName, {
configurable: false,
enumerable: true,
// TODO: Review the memory cost of this closures
get: function () {
// First walk through the tree to get the root node for that index
let node = this.nodes[index];
if (node === undefined) {
node = getNodeAtDepth(this._rootNode, this.type.depth, index);
this.nodes[index] = node;
}
return fieldType.tree_getFromNode(node);
},
set: function (value) {
// Create new node if current leafNode is not dirty
let nodeChanged;
if (this.nodesChanged.has(index)) {
// TODO: This assumes that node has already been populated
nodeChanged = this.nodes[index];
}
else {
const nodePrev = (this.nodes[index] ?? getNodeAtDepth(this._rootNode, this.type.depth, index));
nodeChanged = nodePrev.clone();
// Store the changed node in the nodes cache
this.nodes[index] = nodeChanged;
this.nodesChanged.add(index);
}
fieldType.tree_setToNode(nodeChanged, value);
},
});
}
// If the field type is composite, the value to get and set will be another TreeView. The parent TreeView must
// cache the view itself to retain the caches of the child view. To set a value the view must return a node to
// set it to the parent tree in the field gindex.
else if (isCompositeType(fieldType)) {
Object.defineProperty(CustomContainerTreeViewDU.prototype, fieldName, {
configurable: false,
enumerable: true,
// Returns TreeViewDU of fieldName
get: function () {
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 = fieldType.getViewDU(node, this.caches[index]);
if (fieldType.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;
},
// Expects TreeViewDU of fieldName
set: function (view) {
// 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);
},
});
}
// Should never happen
else {
/* istanbul ignore next - unreachable code */
throw Error(`Unknown fieldType ${fieldType.typeName} for fieldName ${String(fieldName)}`);
}
}
// Change class name
Object.defineProperty(CustomContainerTreeViewDU, "name", { value: type.typeName, writable: false });
return CustomContainerTreeViewDU;
}
//# sourceMappingURL=container.js.map