"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.getOurNodeIndexFromSyncingList = exports.Comparison = void 0;
exports.compareQuery = compareQuery;
exports.sequentialQuery = sequentialQuery;
exports.robustQuery = robustQuery;
exports.attempt = attempt;
exports.generateUUID = generateUUID;
exports.getOurNodeIndex = getOurNodeIndex;
exports.getRandomAvailableArchiver = getRandomAvailableArchiver;
exports.getActiveNodesFromArchiver = getActiveNodesFromArchiver;
exports.isNodeInRotationBounds = isNodeInRotationBounds;
const crypto = __importStar(require("crypto"));
const util_1 = __importDefault(require("util"));
const logger_1 = require("../logger");
const utils = __importStar(require("../utils"));
const utils_1 = require("../utils");
const FastRandomIterator_1 = __importDefault(require("../utils/FastRandomIterator"));
const nestedCounters_1 = require("../utils/nestedCounters");
const profiler_1 = require("../utils/profiler");
const Context_1 = require("./Context");
const Context = __importStar(require("./Context"));
const Self = __importStar(require("./Self"));
const NodeList = __importStar(require("./NodeList"));
const Self_1 = require("./Self");
const Archivers_1 = require("./Archivers");
var Comparison;
(function (Comparison) {
    Comparison[Comparison["BETTER"] = 0] = "BETTER";
    Comparison[Comparison["EQUAL"] = 1] = "EQUAL";
    Comparison[Comparison["WORSE"] = 2] = "WORSE";
    Comparison[Comparison["ABORT"] = 3] = "ABORT";
})(Comparison || (exports.Comparison = Comparison = {}));
async function compareQuery(nodes, queryFn, compareFn, matches) {
    let abort;
    let startOver;
    let errors;
    let matched;
    do {
        abort = false;
        startOver = false;
        errors = [];
        matched = 0;
        for (const node of nodes) {
            try {
                const response = await queryFn(node);
                switch (compareFn(response)) {
                    case Comparison.BETTER:
                        // We start over
                        startOver = true;
                        break;
                    case Comparison.EQUAL:
                        matched++;
                        if (matched >= matches)
                            return errors;
                        break;
                    case Comparison.WORSE:
                        // Try the next one
                        break;
                    case Comparison.ABORT:
                        // End everything and return
                        abort = true;
                        break;
                    default:
                }
                if (abort)
                    break;
                if (startOver)
                    break;
            }
            catch (error) {
                errors.push({
                    node,
                    error: JSON.stringify(error, Object.getOwnPropertyNames(error)),
                });
            }
        }
    } while (startOver);
    return errors;
}
/**
 * TODO PERF replace shuffle with fastRandomIterator (currently sequentialQuery is unused)
 * @param nodes
 * @param queryFn
 * @param verifyFn
 */
async function sequentialQuery(nodes, queryFn, verifyFn = () => true) {
    nodes = [...nodes];
    utils.shuffleArray(nodes);
    let result;
    const errors = [];
    for (const node of nodes) {
        try {
            const response = await queryFn(node);
            if (verifyFn(response) === false) {
                errors.push({
                    node,
                    error: new Error('Response failed verifyFn'),
                    response,
                });
                continue;
            }
            result = response;
        }
        catch (error) {
            errors.push({
                node,
                error,
            });
        }
    }
    return {
        result,
        errors,
    };
}
class Tally {
    constructor(winCount, equalFn, extraDebugging = false) {
        this.winCount = winCount;
        this.equalFn = equalFn;
        this.items = [];
        this.extraDebugging = extraDebugging;
    }
    add(response, node) {
        if (response === null) {
            if (this.extraDebugging)
                nestedCounters_1.nestedCountersInstance.countEvent('robustQuery', `response is null`);
            return null;
        }
        // We search to see if we've already seen this item before
        for (const item of this.items) {
            // If the value of the new item is not equal to the current item, we continue searching
            if (!this.equalFn(response, item.value))
                continue;
            // If the new item is equal to the current item in the list,
            // we increment the current item's counter and add the current node to the list
            item.count++;
            item.nodes.push(node);
            // Here we check our win condition if the current item's counter was incremented
            // If we meet the win requirement, we return an array with the value of the item,
            // and the list of nodes who voted for that item
            if (item.count >= this.winCount) {
                return item;
            }
            // Otherwise, if the win condition hasn't been met,
            // We return null to indicate no winner yet
            return null;
        }
        // If we made it through the entire items list without finding a match,
        // We create a new item and set the count to 1
        const newItem = { value: response, count: 1, nodes: [node] };
        this.items.push(newItem);
        // Finally, we check to see if the winCount is 1,
        // and return the item we just created if that is the case
        if (this.winCount === 1)
            return newItem; //return [newItem, [node]]
    }
    getHighestCount() {
        if (!this.items.length)
            return 0;
        let highestCount = 0;
        for (const item of this.items) {
            if (item.count > highestCount) {
                highestCount = item.count;
            }
        }
        return highestCount;
    }
    getHighestCountItem() {
        if (!this.items.length)
            return null;
        let highestCount = 0;
        let highestIndex = 0;
        let i = 0;
        for (const item of this.items) {
            if (item.count > highestCount) {
                highestCount = item.count;
                highestIndex = i;
            }
            i += 1;
        }
        return this.items[highestIndex];
    }
}
/**
 * [TODO] robustQuery should handle being given an enourmous node list (Dont copy and shuffle it)
 *
 * TODO replace console.log with a specific log funtion.
 *
 * Note -
 * robustQuery should NOT be given a node list that includes yourself (Use NodeList.activeOthersByIdOrder).
 * OR
 * the queryFunction can return null if the node being queried is Self.id
 *
 * @param nodes
 * @param queryFn
 * @param equalityFn
 * @param redundancy
 * @param shuffleNodes
 * @param strictRedundancy
 * @param extraDebugging
 */
async function robustQuery(nodes = [], queryFn, equalityFn = util_1.default.isDeepStrictEqual, redundancy = 3, shuffleNodes = true, strictRedundancy = false, extraDebugging = false, note = 'general', maxRetry = 20) {
    if (nodes.length === 0)
        throw new Error('No nodes given.');
    if (typeof queryFn !== 'function') {
        throw new Error(`Provided queryFn ${queryFn} is not a valid function.`);
    }
    // let originalRedundancy = redundancy
    if (redundancy < 1) {
        redundancy = 3;
    }
    if (redundancy > nodes.length) {
        if (strictRedundancy) {
            if (extraDebugging)
                nestedCounters_1.nestedCountersInstance.countEvent('robustQuery', `${note} not enough nodes to meet strictRedundancy`);
            if (logger_1.logFlags.console || Context_1.config.debug.robustQueryDebug || extraDebugging)
                console.log('robustQuery: isRobustResult=false. not enough nodes to meet strictRedundancy');
            return { topResult: null, winningNodes: [], isRobustResult: false };
        }
        redundancy = nodes.length;
    }
    const responses = new Tally(redundancy, equalityFn);
    let errors = 0;
    // old shuffle.  replaced by FastRandomIterator has much better performance as the pools size grows.
    // this will be helpfull for large networks with many active nodes.
    // nodes = [...nodes]
    // if (shuffleNodes === true) {
    //   utils.shuffleArray(nodes)
    // }
    let randomNodeIterator = null;
    if (shuffleNodes === true) {
        randomNodeIterator = new FastRandomIterator_1.default(nodes.length, redundancy);
    }
    else {
        nodes = [...nodes];
    }
    const nodeCount = nodes.length;
    const queryNodes = async (nodes) => {
        // Wrap the query so that we know which node it's coming from
        const wrappedQuery = async (node) => {
            const response = await queryFn(node);
            return { response, node };
        };
        // We create a promise for each of the first `redundancy` nodes in the shuffled array
        const queries = [];
        for (let i = 0; i < nodes.length; i++) {
            const node = nodes[i];
            queries.push(wrappedQuery(node));
        }
        if (logger_1.logFlags.profiling_verbose)
            profiler_1.profilerInstance.scopedProfileSectionStart(`robustQuery ${note} queryNodes`);
        const [results, errs] = await utils.robustPromiseAll(queries);
        if (logger_1.logFlags.profiling_verbose)
            profiler_1.profilerInstance.scopedProfileSectionEnd(`robustQuery ${note} queryNodes`);
        if (logger_1.logFlags.console || Context_1.config.debug.robustQueryDebug || extraDebugging) {
            console.log('robustQuery results', note, results);
            console.log('robustQuery errs', note, errs);
        }
        let finalResult;
        for (const result of results) {
            const { response, node } = result;
            if (response === null) {
                if (extraDebugging)
                    nestedCounters_1.nestedCountersInstance.countEvent('robustQuery', `${note} response is null`);
                continue;
            } // ignore null response; can be null if we tried to query ourself
            finalResult = responses.add(response, node);
            if (finalResult) {
                if (extraDebugging)
                    nestedCounters_1.nestedCountersInstance.countEvent('robustQuery', `${note} got final result`);
                break;
            }
        }
        if (extraDebugging) {
            console.log('robustQuery tally items', note, responses.items);
            console.log('robustQuery final result', note, finalResult);
        }
        for (const err of errs) {
            if (logger_1.logFlags.console || Context_1.config.debug.robustQueryDebug || extraDebugging)
                console.log('robustQuery: err:', err);
            errors += 1;
            if (extraDebugging)
                nestedCounters_1.nestedCountersInstance.countEvent('robustQuery', `${note} error: ${err.message}`);
        }
        if (!finalResult) {
            if (extraDebugging)
                nestedCounters_1.nestedCountersInstance.countEvent('robustQuery', `${note} no final result`);
            return null;
        }
        return finalResult;
    };
    let finalResult = null;
    let tries = 0;
    while (!finalResult) {
        tries += 1;
        const toQuery = redundancy - responses.getHighestCount();
        if (nodes.length < toQuery) {
            /* prettier-ignore */ if (logger_1.logFlags.console || Context_1.config.debug.robustQueryDebug || extraDebugging)
                console.log(`robustQuery: ${note} stopping since we ran out of nodes to query.`);
            if (extraDebugging)
                nestedCounters_1.nestedCountersInstance.countEvent('robustQuery', `${note} stopping since we ran out of nodes to query.`);
            break;
        }
        let nodesToQuery;
        if (shuffleNodes) {
            let index = randomNodeIterator.getNextIndex();
            nodesToQuery = [];
            while (index >= 0 && nodesToQuery.length < toQuery) {
                nodesToQuery.push(nodes[index]);
                index = randomNodeIterator.getNextIndex();
            }
        }
        else {
            nodesToQuery = nodes.splice(0, toQuery);
        }
        finalResult = await queryNodes(nodesToQuery);
        if (tries >= maxRetry) {
            /* prettier-ignore */ if (logger_1.logFlags.console || Context_1.config.debug.robustQueryDebug || extraDebugging)
                console.log('robustQuery: stopping after 20 tries.');
            if (extraDebugging)
                nestedCounters_1.nestedCountersInstance.countEvent('robustQuery', `${note} stopped after 20 tries`);
            break;
        }
    }
    nestedCounters_1.nestedCountersInstance.countEvent('robustQuery', `${note} tries: ${tries}`);
    if (finalResult) {
        const isRobustResult = finalResult.count >= redundancy;
        if (Context_1.config.debug.robustQueryDebug || extraDebugging)
            console.log(`robustQuery: ${note} stopping since we got a finalResult:${(0, utils_1.stringifyReduce)(finalResult)}`);
        if (extraDebugging)
            nestedCounters_1.nestedCountersInstance.countEvent('robustQuery', `${note} stopping since we got finalResult:${(0, utils_1.stringifyReduce)(finalResult)}`);
        return {
            topResult: finalResult.value,
            winningNodes: finalResult.nodes,
            isRobustResult,
        };
    }
    else {
        // Note:  We return the item that had the most nodes reporting it. However, the caller should know
        //        The calling code can now check isRobustResult to see if a topResult is valid
        if (logger_1.logFlags.console || Context_1.config.debug.robustQueryDebug || extraDebugging)
            console.log(`robustQuery: Could not get ${redundancy} ${redundancy > 1 ? 'redundant responses' : 'response'} from ${nodeCount} ${nodeCount !== 1 ? 'nodes' : 'node'}. Encountered ${errors} query errors.`);
        const highestCountItem = responses.getHighestCountItem();
        if (highestCountItem === null) {
            if (Context_1.config.debug.robustQueryDebug || extraDebugging) {
                console.log(`isRobustResult=false. highestCountItem=null robust tally dump: ${(0, utils_1.stringifyReduce)(responses)}`);
            }
            //if there was no highestCountItem then we had no responses at all
            /* prettier-ignore */ if (logger_1.logFlags.console || Context_1.config.debug.robustQueryDebug || extraDebugging)
                console.log('robustQuery: isRobustResult=false. no responses at all');
            if (extraDebugging)
                nestedCounters_1.nestedCountersInstance.countEvent('robustQuery', `isRobustResult=false. no responses at all`);
            return { topResult: null, winningNodes: [], isRobustResult: false };
        }
        //this isRobustResult should always be false if we get to this code.
        const isRobustResult = highestCountItem.count >= redundancy;
        if (logger_1.logFlags.console || Context_1.config.debug.robustQueryDebug)
            console.log('robustQuery: isRobustResult=false. returning highest count response');
        if (Context_1.config.debug.robustQueryDebug || extraDebugging) {
            console.log(`isRobustResult=false. robust tally dump: ${(0, utils_1.stringifyReduce)(responses)}`);
        }
        if (extraDebugging)
            nestedCounters_1.nestedCountersInstance.countEvent('robustQuery', `${note} isRobustResult=false. returning highest count response. ${(0, utils_1.stringifyReduce)(responses)}`);
        return {
            topResult: highestCountItem.value,
            winningNodes: highestCountItem.nodes,
            isRobustResult,
        };
    }
    // NOTE: this function does not throw errors for situations where we don't have enough responses.
    // instead we return a structured result with enough information about how the query worked.
    // throwing errors was causing problems in past testing.
    // it is OK to throw errors for stuff that is an unexected code mistake in cases where the code would
    //   fail right away.
}
/**
 * Attempts to execute a given asynchronous function up to a certain number of retries upon failure.
 *
 * @template T The type of the resolved value of the input function.
 * @param {() => Promise<T>} fn - The asynchronous function to execute. This function should return a Promise that resolves to a value of type `T`.
 * @param {AttemptOptions} options - Optional. Options passed to change the behavior of this function. See the `AttemptOptions` interface in this same file for details.
 * @returns {Promise<T>} A Promise that resolves to the return value of the input function, if successful.
 * @throws Will throw an error if the function fails all attempts. The error will be the last error thrown by the input function.
 */
async function attempt(fn, options) {
    // fallback to option defaults if needed
    const maxRetries = options?.maxRetries || 3;
    const delay = options?.delay || 2000;
    const logPrefix = options?.logPrefix || 'attempt';
    const logger = options?.logger;
    // initialize our lastError variable
    let lastError = new Error('out of retries');
    // loop until we're successful
    for (let i = 0; i < maxRetries; i++) {
        try {
            // run the function and return the result. if the funciton fails,
            // we'll catch it below
            return await fn();
        }
        catch (e) {
            // log the error
            if (logger && logger_1.logFlags.error)
                logger.error(`${logPrefix}: attempt failure #${i + 1}: ${e.message}`);
            // save the error in case we need to throw it later
            lastError = e;
            // sleep before trying again
            await (0, utils_1.sleep)(delay);
            continue;
        }
    }
    // log that we've run out of attempts
    if (logger && logger_1.logFlags.error)
        logger.error(`${logPrefix}: giving up`);
    // think fast!
    throw lastError;
}
function generateUUID() {
    const buffer = crypto.randomBytes(16);
    buffer[6] = (buffer[6] & 0x0f) | 0x40; // Version 4
    buffer[8] = (buffer[8] & 0x3f) | 0x80; // Variant
    const uuid = buffer.toString('hex');
    return `${uuid.substring(0, 8)}-${uuid.substring(8, 4)}-${uuid.substring(12, 4)}-${uuid.substring(16, 4)}-${uuid.substring(20)}`;
}
function getOurNodeIndex() {
    let nodeInfo = Context_1.stateManager.currentCycleShardData.nodeShardDataMap.get(Self.id);
    // no such node in the list
    if (!nodeInfo)
        return null;
    return nodeInfo.ourNodeIndex;
}
const getOurNodeIndexFromSyncingList = () => {
    let nodeIndex = NodeList.syncingByIdOrder.findIndex((node) => node.id === Self.id);
    console.log('getOurNodeIndexFromSyncingList', nodeIndex);
    if (nodeIndex === -1)
        return null;
    return nodeIndex;
};
exports.getOurNodeIndexFromSyncingList = getOurNodeIndexFromSyncingList;
function getRandomAvailableArchiver() {
    if ((0, Archivers_1.getNumArchivers)() === 0) {
        // original version - choose among *configured* archivers
        // note that over time these could go down and new ones could join the network
        // but may be useful during startup (not verified)
        const availableArchivers = Context.config.p2p.existingArchivers;
        return (0, utils_1.getRandom)(availableArchivers, 1)[0];
    }
    return (0, Archivers_1.getRandomArchiver)(); // from Archivers.ts
}
async function getActiveNodesFromArchiver(archiver) {
    const nodeInfo = (0, Self_1.getPublicNodeInfo)();
    return (0, Archivers_1.postToArchiver)(archiver, 'nodelist', Context.crypto.sign({
        nodeInfo,
    }), 10000).mapErr((e) => {
        // transform the error if we get one
        nestedCounters_1.nestedCountersInstance.countRareEvent('archiver_nodelist', 'Could not get seed list from seed node server 2 ');
        const nodeListUrl = `http://${archiver.ip}:${archiver.port}/nodelist`;
        return Error(`Could not get seed list from seed node server 2 ${nodeListUrl}: ` + e.message);
    });
}
function isNodeRecentlyRotatedIn(idx, numActiveNodes) {
    return (numActiveNodes >= 10 + Context_1.config.p2p.rotationEdgeToAvoid &&
        Context_1.config.p2p.rotationEdgeToAvoid &&
        idx <= Context_1.config.p2p.rotationEdgeToAvoid);
}
function isNodeNearRotatingOut(idx, numActiveNodes) {
    return (numActiveNodes >= 10 + Context_1.config.p2p.rotationEdgeToAvoid &&
        Context_1.config.p2p.rotationEdgeToAvoid &&
        idx >= numActiveNodes - Context_1.config.p2p.rotationEdgeToAvoid);
}
/**
 * Returns true if a node was recently rotate in or
 * will be rotated out soon
 * @param nodeId
 * @returns
 */
function isNodeInRotationBounds(nodeId) {
    const { idx, total } = NodeList.getAgeIndexForNodeId(nodeId);
    // skip freshly rotated in nodes
    if (isNodeRecentlyRotatedIn(idx, total)) {
        nestedCounters_1.nestedCountersInstance.countEvent('skip-newly-rotated-node', nodeId);
        return true;
    }
    // skip about to be rotated out nodes
    if (isNodeNearRotatingOut(idx, total)) {
        nestedCounters_1.nestedCountersInstance.countEvent('skip-about-to-rotate-out-node', nodeId);
        return true;
    }
    return false;
}
//# sourceMappingURL=Utils.js.map