Home Reference Source Test

src/main/generic/consensus/base/account/tree/AccountsTreeNode.js

class AccountsTreeNode {
    /**
     * @param {string} prefix
     * @param {Account} account
     * @returns {AccountsTreeNode}
     */
    static terminalNode(prefix, account) {
        return new AccountsTreeNode(AccountsTreeNode.TERMINAL, prefix, account);
    }

    /**
     * @param {string} prefix
     * @param {Array.<string>} childrenSuffixes
     * @param {Array.<Hash>} childrenHashes
     * @returns {AccountsTreeNode}
     */
    static branchNode(prefix, childrenSuffixes = [], childrenHashes = []) {
        if (childrenSuffixes.length !== childrenHashes.length) {
            throw new Error('Invalid list of children for branch node');
        }
        return new AccountsTreeNode(AccountsTreeNode.BRANCH, prefix, childrenSuffixes, childrenHashes);
    }

    /**
     * @param type
     * @param {string} prefix
     * @param {Account|Array.<string>} arg
     * @param {Array.<Hash>} [arg2]
     */
    constructor(type, prefix = '', arg, arg2 = []) {
        this._type = type;
        /** @type {string} */
        this._prefix = prefix;
        if (this.isBranch()) {
            /** @type {Array.<string>} */
            this._childrenSuffixes = arg;
            /** @type {Array.<Hash>} */
            this._childrenHashes = arg2;
        } else if (this.isTerminal()) {
            /** @type {Account} */
            this._account = arg;
        } else {
            throw `Invalid AccountsTreeNode type: ${type}`;
        }
    }

    /**
     * @param type
     * @returns {boolean}
     */
    static isTerminalType(type) {
        return type === AccountsTreeNode.TERMINAL;
    }

    /**
     * @param type
     * @returns {boolean}
     */
    static isBranchType(type) {
        return type === AccountsTreeNode.BRANCH;
    }

    /**
     * @param {SerialBuffer} buf
     * @returns {AccountsTreeNode}
     */
    static unserialize(buf) {
        const type = buf.readUint8();
        const prefix = buf.readVarLengthString();

        if (AccountsTreeNode.isTerminalType(type)) {
            // Terminal node
            const account = Account.unserialize(buf);
            return AccountsTreeNode.terminalNode(prefix, account);
        } else if (AccountsTreeNode.isBranchType(type)) {
            // Branch node
            const childrenSuffixes = [], childrenHashes = [];
            const childCount = buf.readUint8();
            for (let i = 0; i < childCount; ++i) {
                const childSuffix = buf.readVarLengthString();
                const childHash = Hash.unserialize(buf);
                const childIndex = parseInt(childSuffix[0], 16);
                childrenSuffixes[childIndex] = childSuffix;
                childrenHashes[childIndex] = childHash;
            }
            return AccountsTreeNode.branchNode(prefix, childrenSuffixes, childrenHashes);
        } else {
            throw `Invalid AccountsTreeNode type: ${type}`;
        }
    }

    serialize(buf) {
        buf = buf || new SerialBuffer(this.serializedSize);
        buf.writeUint8(this._type);
        buf.writeVarLengthString(this._prefix);
        if (this.isTerminal()) {
            // Terminal node
            this._account.serialize(buf);
        } else {
            // Branch node
            const childCount = this._childrenSuffixes.reduce((count, child) => count + !!child, 0);
            buf.writeUint8(childCount);
            for (let i = 0; i < this._childrenSuffixes.length; ++i) {
                if (this._childrenHashes[i]) {
                    buf.writeVarLengthString(this._childrenSuffixes[i]);
                    this._childrenHashes[i].serialize(buf);
                }
            }
        }
        return buf;
    }

    /** @type {number} */
    get serializedSize() {
        let payloadSize;
        if (this.isTerminal()) {
            payloadSize = this._account.serializedSize;
        } else {
            // The children array contains undefined values for non existing children.
            // Only count existing ones.
            const childrenSize = this._childrenHashes.reduce((sum, child, i) => {
                return sum + (child ? child.serializedSize + SerialBuffer.varLengthStringSize(this._childrenSuffixes[i]) : 0);
            }, 0);
            payloadSize = /*childCount*/ 1 + childrenSize;
        }

        return /*type*/ 1
            + SerialBuffer.varLengthStringSize(this._prefix)
            + payloadSize;
    }

    /**
     * @param {string} prefix
     * @returns {?Hash}
     */
    getChildHash(prefix) {
        return this._childrenHashes && this._childrenHashes[this._getChildIndex(prefix)];
    }

    /**
     * @param {string} prefix
     * @returns {?string}
     */
    getChild(prefix) {
        const suffix = this._childrenSuffixes && this._childrenSuffixes[this._getChildIndex(prefix)];
        if (suffix) {
            return this.prefix + suffix;
        }
        return suffix;
    }

    /**
     * @param {string} prefix
     * @param {Hash} childHash
     * @returns {AccountsTreeNode}
     */
    withChild(prefix, childHash) {
        const childrenSuffixes = this._childrenSuffixes.slice() || [];
        const childrenHashes = this._childrenHashes.slice() || [];
        childrenSuffixes[this._getChildIndex(prefix)] = prefix.substr(this.prefix.length);
        childrenHashes[this._getChildIndex(prefix)] = childHash;
        return AccountsTreeNode.branchNode(this._prefix, childrenSuffixes, childrenHashes);
    }

    /**
     * @param {string} prefix
     * @returns {AccountsTreeNode}
     */
    withoutChild(prefix) {
        const childrenSuffixes = this._childrenSuffixes.slice() || [];
        const childrenHashes = this._childrenHashes.slice() || [];
        delete childrenSuffixes[this._getChildIndex(prefix)];
        delete childrenHashes[this._getChildIndex(prefix)];
        return AccountsTreeNode.branchNode(this._prefix, childrenSuffixes, childrenHashes);
    }

    /**
     * @returns {boolean}
     */
    hasChildren() {
        return this._childrenSuffixes && this._childrenSuffixes.some(child => !!child);
    }

    /**
     * @returns {boolean}
     */
    hasSingleChild() {
        return this._childrenSuffixes && this._childrenSuffixes.reduce((count, child) => count + !!child, 0) === 1;
    }

    /**
     * @returns {?string}
     */
    getFirstChild() {
        if (!this._childrenSuffixes) {
            return undefined;
        }
        const suffix = this._childrenSuffixes.find(child => !!child);
        return suffix ? this.prefix + suffix : undefined;
    }

    /**
     * @returns {?string}
     */
    getLastChild() {
        if (!this._childrenSuffixes) {
            return undefined;
        }
        for (let i = this._childrenSuffixes.length - 1; i >= 0; --i) {
            if (this._childrenSuffixes[i]) {
                return this.prefix + this._childrenSuffixes[i];
            }
        }
        return undefined;
    }

    /**
     * @returns {?Array.<string>}
     */
    getChildren() {
        if (!this._childrenSuffixes) {
            return undefined;
        }
        return this._childrenSuffixes.filter(child => !!child).map(child => this.prefix + child);
    }

    /** @type {Account} */
    get account() {
        return this._account;
    }

    /** @type {string} */
    get prefix() {
        return this._prefix;
    }

    /** @type {string} */
    set prefix(value) {
        this._prefix = value;
        this._hash = undefined;
    }

    /**
     * @param {Account} account
     * @returns {AccountsTreeNode}
     */
    withAccount(account) {
        return AccountsTreeNode.terminalNode(this._prefix, account);
    }

    /**
     * @returns {Hash}
     */
    hash() {
        if (!this._hash) {
            this._hash = Hash.light(this.serialize());
        }
        return this._hash;
    }

    /**
     * Tests if this node is a child of some other node.
     * @param {AccountsTreeNode} parent
     * @returns {boolean}
     */
    isChildOf(parent) {
        return parent.getChildren() && parent.getChildren().includes(this._prefix);
    }

    /**
     * @returns {boolean}
     */
    isTerminal() {
        return AccountsTreeNode.isTerminalType(this._type);
    }

    /**
     * @returns {boolean}
     */
    isBranch() {
        return AccountsTreeNode.isBranchType(this._type);
    }

    /**
     * @param {string} prefix
     * @returns {number}
     * @private
     */
    _getChildIndex(prefix) {
        Assert.that(prefix.substr(0, this.prefix.length) === this.prefix, `Prefix ${prefix} is not a child of the current node ${this.prefix}`);
        return parseInt(prefix[this.prefix.length], 16);
    }

    /**
     * @param {AccountsTreeNode} o
     * @returns {boolean}
     */
    equals(o) {
        if (!(o instanceof AccountsTreeNode)) return false;
        if (!Object.is(this.prefix, o.prefix)) return false;
        if (this.isTerminal()) {
            return o.isTerminal() && o._account.equals(this._account);
        } else {
            if (!o.isBranch()) return false;
            if (this._childrenSuffixes.length !== o._childrenSuffixes.length) return false;
            if (o._childrenSuffixes.length !== o._childrenHashes.length) return false;
            for (let i = 0; i < this._childrenSuffixes.length; ++i) {
                // hashes of child nodes
                const ourChild = this._childrenHashes[i];
                const otherChild = o._childrenHashes[i];
                if (ourChild) {
                    if (!otherChild || !ourChild.equals(otherChild)) return false;
                } else {
                    if (otherChild) return false;
                }
                if (this._childrenSuffixes[i] !== o._childrenSuffixes[i]) return false;
            }
        }
        return true;
    }
}
AccountsTreeNode.BRANCH = 0x00;
AccountsTreeNode.TERMINAL = 0xff;
Class.register(AccountsTreeNode);