@bigmi/core
Version:
TypeScript library for Bitcoin apps.
195 lines • 7.99 kB
JavaScript
import { RpcErrorCode } from '../errors/rpc.js';
import { AllTransportsFailedError, TransportMethodNotSupportedError, } from '../errors/transport.js';
import { InsufficientUTXOBalanceError } from '../errors/utxo.js';
import { createTransport } from '../factories/createTransport.js';
import { wait } from '../utils/wait.js';
export 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 = createTransport({
key,
name,
async request({ method, params }) {
// Filter transports to only those that support the requested method
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 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 we've reached the end of the fallbacks, throw all collected errors.
if (i === supportedTransports.length - 1) {
throw new AllTransportsFailedError({
method,
params,
errors: collectedErrors,
totalAttempts: i + 1,
});
}
// Otherwise, try the next fallback.
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;
});
}
export function shouldThrow(error) {
if (error instanceof InsufficientUTXOBalanceError) {
return true;
}
if ('code' in error && typeof error.code === 'number') {
if (error.code === RpcErrorCode.INTERNAL_ERROR ||
error.code === RpcErrorCode.USER_REJECTION ||
error.code === 5000 // CAIP UserRejectedRequestError
) {
return true;
}
}
return false;
}
/** @internal */
export 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 () => {
// 1. Take a sample from each Transport.
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 };
}));
// 2. Store the sample. If we have more than `sampleCount` samples, remove
// the oldest sample.
samples.push(sample);
if (samples.length > sampleCount) {
samples.shift();
}
// 3. Calculate the max latency from samples.
const maxLatency = Math.max(...samples.map((sample) => Math.max(...sample.map(({ latency }) => latency))));
// 4. Calculate the score for each Transport.
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]);
// 5. Sort the Transports by score.
onTransports(scores.map(([, i]) => transports[i]));
// 6. Wait, and then rank again.
await wait(interval);
rankTransports_();
};
rankTransports_();
}
//# sourceMappingURL=fallback.js.map