"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const types_1 = require("@deepstream/types");
const constants_1 = require("../../constants");
const events_1 = require("events");
/**
 * This class maintains a list of all nodes that are
 * currently present within the cluster.
 *
 * It provides status messages on a predefined interval
 * and keeps track of incoming status messages.
 */
class DistributedClusterRegistry extends types_1.DeepstreamPlugin {
    /**
     * Creates the class, initialises all intervals and publishes the
     * initial status message that notifies other nodes within this
     * cluster of its presence.
     */
    constructor(pluginOptions, services, config) {
        super();
        this.pluginOptions = pluginOptions;
        this.services = services;
        this.config = config;
        this.description = 'Distributed Cluster Registry';
        this.inCluster = false;
        this.nodes = new Map();
        this.leaderScore = Math.random();
        this.emitter = new events_1.EventEmitter();
        this.role = this.pluginOptions.role || 'deepstream';
    }
    init() {
        this.services.clusterNode.subscribe(constants_1.TOPIC.CLUSTER, this.onMessage.bind(this));
        this.leaveCluster = this.leaveCluster.bind(this);
        this.publishStatus();
        this.publishInterval = setInterval(this.publishStatus.bind(this), this.pluginOptions.keepAliveInterval);
        this.checkInterval = setInterval(this.checkNodes.bind(this), this.pluginOptions.activeCheckInterval);
    }
    async close() {
        return new Promise((resolve) => {
            this.emitter.once('close', resolve);
            this.leaveCluster();
        });
    }
    onServerAdded(callback) {
        this.emitter.on('server-added', callback);
    }
    onServerRemoved(callback) {
        this.emitter.on('server-removed', callback);
    }
    /**
     * Returns the serverNames of all nodes currently present within the cluster
     */
    getAll() {
        return [...this.nodes.keys()];
    }
    /**
     * Returns true if this node is the cluster leader
     */
    isLeader() {
        return this.config.serverName === this.getLeader();
    }
    /**
     * Returns the name of the current leader
     */
    getLeader() {
        let maxScore = 0;
        let leader = this.config.serverName;
        for (const [serverName, node] of this.nodes) {
            if (node.leaderScore > maxScore) {
                maxScore = node.leaderScore;
                leader = serverName;
            }
        }
        return leader;
    }
    /**
     * Distributes incoming messages on the cluster topic
     */
    onMessage(message) {
        if (message.action === constants_1.CLUSTER_ACTION.STATUS) {
            this.updateNode(message);
            return;
        }
        if (message.action === constants_1.CLUSTER_ACTION.REMOVE) {
            this.removeNode(message.serverName);
            return;
        }
        this.services.logger.error(types_1.EVENT.UNKNOWN_ACTION, `TOPIC: ${constants_1.TOPIC[constants_1.TOPIC.CLUSTER]} ${message.action}`);
    }
    /**
     * Called on an interval defined by clusterActiveCheckInterval to check if all nodes
     * within the cluster are still alive.
     *
     * Being alive is defined as having received a status message from that node less than
     * <clusterNodeInactiveTimeout> milliseconds ago.
     */
    checkNodes() {
        const now = Date.now();
        for (const [serverName, node] of this.nodes) {
            if (now - node.lastStatusTime > this.pluginOptions.nodeInactiveTimeout) {
                this.removeNode(serverName);
            }
        }
    }
    /**
     * Updates the status of a node with incoming status data and resets its lastStatusTime.
     *
     * If the remote node doesn't exist yet, it is added and an add event is emitted / logged
     */
    updateNode(message) {
        const node = this.nodes.get(message.serverName);
        this.nodes.set(message.serverName, {
            lastStatusTime: Date.now(),
            leaderScore: message.leaderScore
        });
        if (node) {
            return;
        }
        this.services.logger.info(types_1.EVENT.CLUSTER_JOIN, message.serverName);
        this.services.logger.info(types_1.EVENT.CLUSTER_SIZE, `The cluster size is now ${this.nodes.size}`);
        this.emitter.emit('server-added', message.serverName);
    }
    /**
     * Removes a remote node from this registry if it exists.
     * Logs/emits remove
     */
    removeNode(serverName) {
        const deleted = this.nodes.delete(serverName);
        if (deleted) {
            this.services.logger.info(types_1.EVENT.CLUSTER_LEAVE, serverName);
            this.services.logger.info(types_1.EVENT.CLUSTER_SIZE, `The cluster size is now ${this.nodes.size}`);
            this.emitter.emit('server-removed', serverName);
        }
    }
    /**
     * Publishes this node's status on the message bus
     */
    publishStatus() {
        this.inCluster = true;
        const message = {
            topic: constants_1.TOPIC.CLUSTER,
            action: constants_1.CLUSTER_ACTION.STATUS,
            serverName: this.config.serverName,
            leaderScore: this.leaderScore,
            externalUrl: this.config.externalUrl,
            role: this.role
        };
        this.updateNode(message);
        this.services.clusterNode.send(message);
    }
    /**
     * Prompts this node to leave the cluster, either as a result of a server.close()
     * call or due to the process exiting.
     * This sends out a leave message to all other nodes and destroys this class.
     */
    leaveCluster() {
        if (this.inCluster === false) {
            this.emitter.emit('close');
            return;
        }
        this.services.logger.info(types_1.EVENT.CLUSTER_LEAVE, this.config.serverName);
        this.services.clusterNode.send({
            topic: constants_1.TOPIC.CLUSTER,
            action: constants_1.CLUSTER_ACTION.REMOVE,
            name: this.config.serverName
        });
        // TODO: If a message connector doesn't close this is required to avoid an error
        // being thrown during shutdown
        // this._options.messageConnector.unsubscribe( C.TOPIC.CLUSTER, this._onMessageFn );
        process.removeListener('beforeExit', this.leaveCluster);
        process.removeListener('exit', this.leaveCluster);
        clearInterval(this.publishInterval);
        clearInterval(this.checkInterval);
        this.nodes.clear();
        this.inCluster = false;
        this.emitter.emit('close');
    }
}
exports.DistributedClusterRegistry = DistributedClusterRegistry;
//# sourceMappingURL=distributed-cluster-registry.js.map