@bigmi/core
Version: 
TypeScript library for Bitcoin apps.
188 lines • 7.54 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.fallback = fallback;
exports.shouldThrow = shouldThrow;
exports.rankTransports = rankTransports;
const rpc_js_1 = require("../errors/rpc.js");
const transport_js_1 = require("../errors/transport.js");
const utxo_js_1 = require("../errors/utxo.js");
const createTransport_js_1 = require("../factories/createTransport.js");
const wait_js_1 = require("../utils/wait.js");
function fallback(transports_, config = {}) {
    const { key = 'fallback', name = 'Fallback', rank = false, shouldThrow: shouldThrow_ = shouldThrow, retryCount, retryDelay, } = config;
    return (({ chain, pollingInterval = 4000, timeout, ...rest }) => {
        let transports = transports_;
        let onResponse = () => { };
        const transport = (0, createTransport_js_1.createTransport)({
            key,
            name,
            async request({ method, params }) {
                const supportedTransports = transports.reduce((supportedTransports, transport) => {
                    const instance = transport({
                        ...rest,
                        chain,
                        retryCount: 0,
                        timeout,
                    });
                    const { include, exclude } = instance.config.methods || {};
                    if (include) {
                        if (include.includes(method)) {
                            supportedTransports.push(instance);
                        }
                        return supportedTransports;
                    }
                    if (exclude) {
                        if (!exclude.includes(method)) {
                            supportedTransports.push(instance);
                        }
                        return supportedTransports;
                    }
                    supportedTransports.push(instance);
                    return supportedTransports;
                }, []);
                if (!supportedTransports.length) {
                    throw new transport_js_1.TransportMethodNotSupportedError({ method });
                }
                const collectedErrors = [];
                const fetch = async (i = 0) => {
                    const transport = supportedTransports[i];
                    try {
                        const response = await transport.request({
                            method,
                            params,
                        });
                        onResponse({
                            method,
                            params: params,
                            response,
                            transport,
                            status: 'success',
                        });
                        return response;
                    }
                    catch (err) {
                        onResponse({
                            error: err,
                            method,
                            params: params,
                            transport,
                            status: 'error',
                        });
                        if (shouldThrow_(err)) {
                            throw err;
                        }
                        collectedErrors.push({
                            transport: transport.config.name,
                            error: err,
                            attempt: i + 1,
                        });
                        if (i === supportedTransports.length - 1) {
                            throw new transport_js_1.AllTransportsFailedError({
                                method,
                                params,
                                errors: collectedErrors,
                                totalAttempts: i + 1,
                            });
                        }
                        return fetch(i + 1);
                    }
                };
                return fetch();
            },
            retryCount,
            retryDelay,
            type: 'fallback',
        }, {
            onResponse: (fn) => {
                onResponse = fn;
            },
            transports: transports.map((fn) => fn({ chain, retryCount: 0 })),
        });
        if (rank) {
            const rankOptions = (typeof rank === 'object' ? rank : {});
            rankTransports({
                chain,
                interval: rankOptions.interval ?? pollingInterval,
                onTransports: (transports_) => {
                    transports = transports_;
                },
                ping: rankOptions.ping,
                sampleCount: rankOptions.sampleCount,
                timeout: rankOptions.timeout,
                transports,
                weights: rankOptions.weights,
            });
        }
        return transport;
    });
}
function shouldThrow(error) {
    if (error instanceof utxo_js_1.InsufficientUTXOBalanceError) {
        return true;
    }
    if ('code' in error && typeof error.code === 'number') {
        if (error.code === rpc_js_1.RpcErrorCode.INTERNAL_ERROR ||
            error.code === rpc_js_1.RpcErrorCode.USER_REJECTION ||
            error.code === 5000) {
            return true;
        }
    }
    return false;
}
function rankTransports({ chain, interval = 4000, onTransports, ping, sampleCount = 10, timeout = 1000, transports, weights = {}, }) {
    const { stability: stabilityWeight = 0.7, latency: latencyWeight = 0.3 } = weights;
    const samples = [];
    const rankTransports_ = async () => {
        const sample = await Promise.all(transports.map(async (transport) => {
            const transport_ = transport({ chain, retryCount: 0, timeout });
            const start = Date.now();
            let end;
            let success;
            try {
                await (ping
                    ? ping({ transport: transport_ })
                    : transport_.request({
                        method: 'net_listening',
                        params: undefined,
                    }));
                success = 1;
            }
            catch {
                success = 0;
            }
            finally {
                end = Date.now();
            }
            const latency = end - start;
            return { latency, success };
        }));
        samples.push(sample);
        if (samples.length > sampleCount) {
            samples.shift();
        }
        const maxLatency = Math.max(...samples.map((sample) => Math.max(...sample.map(({ latency }) => latency))));
        const scores = transports
            .map((_, i) => {
            const latencies = samples.map((sample) => sample[i].latency);
            const meanLatency = latencies.reduce((acc, latency) => acc + latency, 0) /
                latencies.length;
            const latencyScore = 1 - meanLatency / maxLatency;
            const successes = samples.map((sample) => sample[i].success);
            const stabilityScore = successes.reduce((acc, success) => acc + success, 0) /
                successes.length;
            if (stabilityScore === 0) {
                return [0, i];
            }
            return [
                latencyWeight * latencyScore + stabilityWeight * stabilityScore,
                i,
            ];
        })
            .sort((a, b) => b[0] - a[0]);
        onTransports(scores.map(([, i]) => transports[i]));
        await (0, wait_js_1.wait)(interval);
        rankTransports_();
    };
    rankTransports_();
}
//# sourceMappingURL=fallback.js.map