gateway.js

'use strict'

const {NodeStatus} = require('./node')
const {NodeWrr} = require('./node_wrr')

/*eslint-disable no-unused-vars*/
const {GateWayConfig} = require('./type')
const {Node, NodeStat} = require('./node')
/*eslint-enable no-unused-vars*/

/**
 * @typedef {object} TrackedNode
 * @property {NodeStat} stat
 * @property {Node} node
 */

class GatewayError extends Error {
    constructor(message) {
        super(message)
        this.name = 'GatewayError'
    }
}

/**
 * @private
 */
class Gateway {
    /**
     * @param {Array<Node>} nodes
     * @param {GateWayConfig} config
     */
    constructor(nodes, config) {
        let validConfig = Gateway._standardizeConfig(config)

        this._nodes = nodes
        this._healthcheckInterval = validConfig.healthcheckInterval
        this._reorganisationBlock = validConfig.reorganisationBlocks
        this._nodeWrr = new NodeWrr()
    }

    async open() {
        let updateStateNodes = this._updateStatNodes.bind(this)

        await updateStateNodes()
        this._updateStatNodesTimer = setInterval(
            () => {
                updateStateNodes().catch(console.error)
            },
            this._healthcheckInterval
        )
    }

    // eslint-disable-next-line require-await
    async close() {
        if (this._updateStatNodesTimer) {
            clearInterval(this._updateStatNodesTimer)
            this._updateStatNodesTimer = undefined
        }
    }

    /**
     * @returns {number | undefined}
     */
    get blockNumber() {
        return this._nodeWrr.blockNumber
    }

    get safeBlockNumber() {
        return this._safeBlockNumber
    }

    /**
     * @returns {Promise<Node | undefined>}
     */
    pickNode() {
        return this._nodeWrr.pick()
    }

    /**
     * @private
     */
    async _updateStatNodes() {
        let tasks = this._nodes.map(node => node.updateStat())

        await Promise.all(tasks)

        let healthyNodes = this._nodes.filter(node => {
            return node.status === NodeStatus.OK
        })
        let topNodes = Gateway._pickTopNodes(healthyNodes)

        this._nodeWrr.update(topNodes)
        this._updateSafeBlockNumber()
    }

    /**
     * Update attribute "_safeBlockNumber".
     *
     * @returns {undefined}
     */
    _updateSafeBlockNumber() {
        let safeBlockNumber =
            this._nodeWrr.blockNumber - this._reorganisationBlock

        if (!Number.isInteger(safeBlockNumber) || safeBlockNumber < 0) {
            this._safeBlockNumber = undefined

            return
        }

        this._safeBlockNumber = safeBlockNumber
    }

    /**
     * @private
     * @param {object} config
     * @returns {GateWayConfig}
     */
    static _standardizeConfig(config) {
        let defaultConfig = {
            healthcheckInterval: 3000,
            reorganisationBlocks: 6
        }

        return Object.assign(defaultConfig, config)
    }

    /**
     *
     * @param {Array<Node>} nodes
     * @returns {Array<Node>}
     */
    static _pickTopNodes(nodes) {
        let topBlockNumber = Gateway._getTopBlockNumber(nodes)
        let result = []

        for (let node of nodes) {
            if (node.blockNumber === topBlockNumber) {
                result.push(node)
            }
        }

        return result
    }

    /**
     *
     * @param {Array<Node>} nodes
     * @returns {number | undefined}
     */
    static _getTopBlockNumber(nodes) {
        if (nodes.length === 0) {
            return undefined
        }

        let result = nodes[0].blockNumber

        for (let node of nodes) {
            if (node.blockNumber > result) {
                result = node.blockNumber
            }
        }

        return result
    }
}

module.exports = {
    Gateway,
    GatewayError
}