"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const tslib_1 = require("tslib");
const log_1 = tslib_1.__importDefault(require("../log"));
const nodes_1 = require("./nodes");
const proto_1 = require("./proto");
/**
 * Multi peer connection that tries all nodes one after another. It can send the command to more
 * than one node in parallel and return the first success if 'parallel' i > 1.
 *
 * It uses the Nodes class to manage which nodes will be contacted.
 */
class RosterWSConnection {
    /**
     * @param r         The roster to use or the rID of the Nodes
     * @param service   The name of the service to reach
     * @param parallel  How many nodes to contact in parallel. Can be changed afterwards
     */
    constructor(r, service, parallel = RosterWSConnection.defaultParallel) {
        this.service = service;
        this.msgNbr = 0;
        this.setParallel(parallel);
        if (r instanceof proto_1.Roster) {
            this.rID = r.id.toString("hex");
            if (!RosterWSConnection.nodes.has(this.rID)) {
                RosterWSConnection.nodes.set(this.rID, new nodes_1.Nodes(r));
            }
        }
        else {
            this.rID = r;
            if (!RosterWSConnection.nodes.has(this.rID)) {
                throw new Error("unknown roster-ID");
            }
        }
        this.nodes = RosterWSConnection.nodes.get(this.rID);
        this.connNbr = RosterWSConnection.totalConnNbr;
        RosterWSConnection.totalConnNbr++;
    }
    /**
     * Set a new parameter for how many nodes should be contacted in parallel.
     * @param p
     */
    setParallel(p) {
        if (p < 1) {
            throw new Error("Parallel needs to be bigger or equal to 1");
        }
        this.parallel = p;
    }
    /**
     * Sends a message to conodes in parallel. As soon as one of the conodes returns
     * success, the message is returned. If a conode returns an error (or times out),
     * a next conode from this.addresses is contacted. If all conodes return an error,
     * the promise is rejected.
     *
     * @param message the message to send
     * @param reply the type of the message to return
     */
    send(message, reply) {
        return tslib_1.__awaiter(this, void 0, void 0, function* () {
            const errors = [];
            const msgNbr = this.msgNbr;
            this.msgNbr++;
            const list = this.nodes.newList(this.service, this.parallel);
            const pool = list.active;
            log_1.default.lvl3(`${this.connNbr}/${msgNbr}`, "sending", message.constructor.name, "with list:", pool.map((conn) => conn.getURL()));
            // Get the first reply - need to take care not to return a reject too soon, else
            // all other promises will be ignored.
            // The promises that never 'resolve' or 'reject' will later be collected by GC:
            // https://stackoverflow.com/questions/36734900/what-happens-if-we-dont-resolve-or-reject-the-promise
            return Promise.race(pool.map((conn) => {
                return new Promise((resolve, reject) => tslib_1.__awaiter(this, void 0, void 0, function* () {
                    do {
                        const idStr = `${this.connNbr}/${msgNbr.toString()}: ${conn.getURL()}`;
                        try {
                            log_1.default.lvl3(idStr, "sending");
                            const sub = yield conn.send(message, reply);
                            log_1.default.lvl3(idStr, "received OK");
                            if (list.done(conn) === 0) {
                                log_1.default.lvl3(idStr, "first to receive");
                                resolve(sub);
                            }
                            return;
                        }
                        catch (e) {
                            log_1.default.lvl3(idStr, "has error", e);
                            errors.push(e);
                            conn = list.replace(conn);
                            if (errors.length >= list.length / 2) {
                                // More than half of the nodes threw an error - quit.
                                reject(errors);
                            }
                        }
                    } while (conn !== undefined);
                }));
            }));
        });
    }
    /**
     * To be conform with an IConnection
     */
    getURL() {
        return this.nodes.newList(this.service, 1).active[0].getURL();
    }
    /**
     * To be conform with an IConnection - sets the timeout on all connections.
     */
    setTimeout(value) {
        this.nodes.setTimeout(value);
    }
    /** @inheritdoc */
    sendStream(message, reply) {
        const list = this.nodes.newList(this.service, this.parallel);
        return list.active[0].sendStream(message, reply);
    }
    /**
     * Return a new RosterWSConnection for the given service.
     * @param service
     */
    copy(service) {
        return new RosterWSConnection(this.rID, service, this.parallel);
    }
    /**
     * Invalidate a given address.
     * @param address
     */
    invalidate(address) {
        this.nodes.gotError(address);
    }
    /**
     * Update roster for this connection.
     * @param r
     */
    setRoster(r) {
        const newID = r.id.toString("hex");
        if (newID !== this.rID) {
            this.rID = newID;
            if (!RosterWSConnection.nodes.has(this.rID)) {
                RosterWSConnection.nodes.set(this.rID, new nodes_1.Nodes(r, this.nodes));
            }
            this.nodes = RosterWSConnection.nodes.get(this.rID);
        }
    }
}
exports.RosterWSConnection = RosterWSConnection;
// Can be set to override the default parallel value
RosterWSConnection.defaultParallel = 3;
// debugging variable
RosterWSConnection.totalConnNbr = 0;
RosterWSConnection.nodes = new Map();
