UNPKG

@metamask/network-controller

Version:

Provides an interface to the currently selected network via a MetaMask-compatible provider object

300 lines 15.7 kB
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