@metamask/network-controller
Version:
Provides an interface to the currently selected network via a MetaMask-compatible provider object
300 lines • 15.7 kB
JavaScript
var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
if (kind === "m") throw new TypeError("Private method is not writable");
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
};
var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
};
var _RpcServiceChain_onAvailableEventEmitter, _RpcServiceChain_onBreakEventEmitter, _RpcServiceChain_onDegradedEventEmitter, _RpcServiceChain_primaryService, _RpcServiceChain_services, _RpcServiceChain_status;
import { CircuitState, CockatielEventEmitter } from "@metamask/controller-utils";
import { RpcService } from "./rpc-service.mjs";
import { projectLogger, createModuleLogger } from "../logger.mjs";
const log = createModuleLogger(projectLogger, 'RpcServiceChain');
/**
* Statuses that the RPC service chain can be in.
*/
const STATUSES = {
Available: 'available',
Degraded: 'degraded',
Unknown: 'unknown',
Unavailable: 'unavailable',
};
/**
* This class constructs and manages requests to a chain of RpcService objects
* which represent RPC endpoints with which to access a particular network. The
* first service in the chain is intended to be the primary way of hitting the
* network and the remaining services are used as failovers.
*/
export class RpcServiceChain {
/**
* Constructs a new RpcServiceChain object.
*
* @param rpcServiceConfigurations - The options for the RPC services
* that you want to construct. Each object in this array is the same as
* {@link RpcServiceOptions}.
*/
constructor(rpcServiceConfigurations) {
/**
* The event emitter for the `onAvailable` event.
*/
_RpcServiceChain_onAvailableEventEmitter.set(this, void 0);
/**
* The event emitter for the `onBreak` event.
*/
_RpcServiceChain_onBreakEventEmitter.set(this, void 0);
/**
* The event emitter for the `onDegraded` event.
*/
_RpcServiceChain_onDegradedEventEmitter.set(this, void 0);
/**
* The first RPC service that requests will be sent to.
*/
_RpcServiceChain_primaryService.set(this, void 0);
/**
* The RPC services in the chain.
*/
_RpcServiceChain_services.set(this, void 0);
/**
* The status of the RPC service chain.
*/
_RpcServiceChain_status.set(this, void 0);
__classPrivateFieldSet(this, _RpcServiceChain_services, rpcServiceConfigurations.map((rpcServiceConfiguration) => new RpcService(rpcServiceConfiguration)), "f");
__classPrivateFieldSet(this, _RpcServiceChain_primaryService, __classPrivateFieldGet(this, _RpcServiceChain_services, "f")[0], "f");
__classPrivateFieldSet(this, _RpcServiceChain_status, STATUSES.Unknown, "f");
__classPrivateFieldSet(this, _RpcServiceChain_onBreakEventEmitter, new CockatielEventEmitter(), "f");
__classPrivateFieldSet(this, _RpcServiceChain_onDegradedEventEmitter, new CockatielEventEmitter(), "f");
for (const service of __classPrivateFieldGet(this, _RpcServiceChain_services, "f")) {
service.onDegraded((data) => {
if (__classPrivateFieldGet(this, _RpcServiceChain_status, "f") !== STATUSES.Degraded) {
log('Updating status to "degraded"', data);
__classPrivateFieldSet(this, _RpcServiceChain_status, STATUSES.Degraded, "f");
const { endpointUrl, ...rest } = data;
__classPrivateFieldGet(this, _RpcServiceChain_onDegradedEventEmitter, "f").emit(rest);
}
});
}
__classPrivateFieldSet(this, _RpcServiceChain_onAvailableEventEmitter, new CockatielEventEmitter(), "f");
for (const service of __classPrivateFieldGet(this, _RpcServiceChain_services, "f")) {
service.onAvailable((data) => {
if (__classPrivateFieldGet(this, _RpcServiceChain_status, "f") !== STATUSES.Available) {
log('Updating status to "available"', data);
__classPrivateFieldSet(this, _RpcServiceChain_status, STATUSES.Available, "f");
const { endpointUrl, ...rest } = data;
__classPrivateFieldGet(this, _RpcServiceChain_onAvailableEventEmitter, "f").emit(rest);
}
});
}
}
/**
* Calls the provided callback when any of the RPC services is retried.
*
* This is mainly useful for tests.
*
* @param listener - The callback to be called.
* @returns An object with a `dispose` method which can be used to unregister
* the event listener.
*/
onServiceRetry(listener) {
const disposables = __classPrivateFieldGet(this, _RpcServiceChain_services, "f").map((service) => service.onRetry((data) => {
listener({
...data,
primaryEndpointUrl: __classPrivateFieldGet(this, _RpcServiceChain_primaryService, "f").endpointUrl.toString(),
});
}));
return {
dispose() {
disposables.forEach((disposable) => disposable.dispose());
},
};
}
/**
* Calls the provided callback only when the maximum number of failed
* consecutive attempts to receive a 2xx response has been reached for all
* RPC services in the chain, and all services' underlying circuits have
* broken.
*
* The callback will not be called if a service's circuit breaks but its
* failover does not. Use `onServiceBreak` if you'd like a lower level of
* granularity.
*
* @param listener - The callback to be called.
* @returns An object with a `dispose` method which can be used to unregister
* the callback.
*/
onBreak(listener) {
return __classPrivateFieldGet(this, _RpcServiceChain_onBreakEventEmitter, "f").addListener(listener);
}
/**
* Calls the provided callback each time when, for *any* of the RPC services
* in this chain, the maximum number of failed consecutive attempts to receive
* a 2xx response has been reached and the underlying circuit has broken. A
* more granular version of `onBreak`.
*
* @param listener - The callback to be called.
* @returns An object with a `dispose` method which can be used to unregister
* the callback.
*/
onServiceBreak(listener) {
const disposables = __classPrivateFieldGet(this, _RpcServiceChain_services, "f").map((service) => service.onBreak((data) => {
listener({
...data,
primaryEndpointUrl: __classPrivateFieldGet(this, _RpcServiceChain_primaryService, "f").endpointUrl.toString(),
});
}));
return {
dispose() {
disposables.forEach((disposable) => disposable.dispose());
},
};
}
/**
* Calls the provided callback if no requests have been initiated yet or
* all requests to RPC services in this chain have responded successfully in a
* timely fashion, and then one of the two conditions apply:
*
* 1. When a retriable error is encountered making a request to an RPC
* service, and the request is retried until a set maximum is reached.
* 2. When a RPC service responds successfully, but the request takes longer
* than a set number of seconds to complete.
*
* Note that the callback will be called even if there are local connectivity
* issues which prevent requests from being initiated. This is intentional.
*
* Also note this callback will only be called if the RPC service chain as a
* whole is in a "degraded" state, and will then only be called once (e.g., it
* will not be called if a failover service falls into a degraded state, then
* the primary comes back online, but it is slow). Use `onServiceDegraded` if
* you'd like a lower level of granularity.
*
* @param listener - The callback to be called.
* @returns An object with a `dispose` method which can be used to unregister
* the callback.
*/
onDegraded(listener) {
return __classPrivateFieldGet(this, _RpcServiceChain_onDegradedEventEmitter, "f").addListener(listener);
}
/**
* Calls the provided callback each time one of the two conditions apply:
*
* 1. When a retriable error is encountered making a request to an RPC
* service, and the request is retried until a set maximum is reached.
* 2. When a RPC service responds successfully, but the request takes longer
* than a set number of seconds to complete.
*
* Note that the callback will be called even if there are local connectivity
* issues which prevent requests from being initiated. This is intentional.
*
* This is a more granular version of `onDegraded`. The callback will be
* called for each slow request to an RPC service. It may also be called again
* if a failover service falls into a degraded state, then the primary comes
* back online, but it is slow.
*
* @param listener - The callback to be called.
* @returns An object with a `dispose` method which can be used to unregister
* the callback.
*/
onServiceDegraded(listener) {
const disposables = __classPrivateFieldGet(this, _RpcServiceChain_services, "f").map((service) => service.onDegraded((data) => {
listener({
...data,
primaryEndpointUrl: __classPrivateFieldGet(this, _RpcServiceChain_primaryService, "f").endpointUrl.toString(),
});
}));
return {
dispose() {
disposables.forEach((disposable) => disposable.dispose());
},
};
}
/**
* Calls the provided callback in one of the following two conditions:
*
* 1. The first time that a 2xx request is made to any of the RPC services in
* this chain.
* 2. When requests to any the failover RPC services in this chain were
* failing such that they were degraded or their underyling circuits broke,
* but the first request to the primary succeeds again.
*
* Note this callback will only be called if the RPC service chain as a whole
* is in an "available" state.
*
* @param listener - The callback to be called.
* @returns An object with a `dispose` method which can be used to unregister
* the callback.
*/
onAvailable(listener) {
return __classPrivateFieldGet(this, _RpcServiceChain_onAvailableEventEmitter, "f").addListener(listener);
}
async request(jsonRpcRequest, fetchOptions = {}) {
// Start with the primary (first) service and switch to failovers as the
// need arises. This is a bit confusing, so keep reading for more on how
// this works.
let availableServiceIndex;
let response;
for (const [i, service] of __classPrivateFieldGet(this, _RpcServiceChain_services, "f").entries()) {
log(`Trying service #${i + 1}...`);
const previousCircuitState = service.getCircuitState();
try {
// Try making the request through the service.
response = await service.request(jsonRpcRequest, fetchOptions);
log('Service successfully received request.');
availableServiceIndex = i;
break;
}
catch (error) {
// Oops, that didn't work.
// Capture this error so that we can handle it later.
const { lastError } = service;
const isCircuitOpen = service.getCircuitState() === CircuitState.Open;
log('Service failed! error =', error, 'lastError = ', lastError);
if (isCircuitOpen) {
if (i < __classPrivateFieldGet(this, _RpcServiceChain_services, "f").length - 1) {
log("This service's circuit is open. Proceeding to next service...");
continue;
}
if (previousCircuitState !== CircuitState.Open &&
__classPrivateFieldGet(this, _RpcServiceChain_status, "f") !== STATUSES.Unavailable &&
lastError !== undefined) {
// If the service's circuit just broke and it's the last one in the
// chain, then trigger the onBreak event. (But if for some reason we
// have already done this, then don't do it.)
log('This service\'s circuit just opened and it is the last service. Updating status to "unavailable" and triggering onBreak.');
__classPrivateFieldSet(this, _RpcServiceChain_status, STATUSES.Unavailable, "f");
__classPrivateFieldGet(this, _RpcServiceChain_onBreakEventEmitter, "f").emit({
error: lastError,
});
}
}
// The service failed, and we throw whatever the error is. The calling
// code can try again if it so desires.
log(`${isCircuitOpen ? '' : "This service's circuit is closed. "}Re-throwing error.`);
throw error;
}
}
if (response) {
// If one of the services is available, reset all of the circuits of the
// following services. If we didn't do this and the service became
// unavailable in the future, and any of the failovers' circuits were
// open (due to previous failures), we would receive a "circuit broken"
// error when we attempted to divert traffic to the failovers again.
//
if (availableServiceIndex !== undefined) {
for (const [i, service] of [...__classPrivateFieldGet(this, _RpcServiceChain_services, "f").entries()].slice(availableServiceIndex + 1)) {
log(`Resetting policy for service #${i + 1}.`);
service.resetPolicy();
}
}
return response;
}
// The only way we can end up here is if there are no services to loop over.
// That is not possible due to the types on the constructor, but TypeScript
// doesn't know this, so we have to appease it.
throw new Error('Nothing to return');
}
}
_RpcServiceChain_onAvailableEventEmitter = new WeakMap(), _RpcServiceChain_onBreakEventEmitter = new WeakMap(), _RpcServiceChain_onDegradedEventEmitter = new WeakMap(), _RpcServiceChain_primaryService = new WeakMap(), _RpcServiceChain_services = new WeakMap(), _RpcServiceChain_status = new WeakMap();
//# sourceMappingURL=rpc-service-chain.mjs.map