@hiero-ledger/sdk
Version:
474 lines (404 loc) • 13 kB
JavaScript
// SPDX-License-Identifier: Apache-2.0
import LedgerId from "../LedgerId.js";
import * as util from "../util.js";
/**
* @typedef {import("../channel/Channel.js").default} Channel
* @typedef {import("../channel/MirrorChannel.js").default} MirrorChannel
* @typedef {import("../Node.js").default} Node
* @typedef {import("../MirrorNode.js").default} MirrorNode
* @typedef {import("../address_book/NodeAddressBook.js").default} NodeAddressBook
*/
/**
* @template {Channel | MirrorChannel} ChannelT
* @typedef {import("../ManagedNode.js").default<ChannelT>} ManagedNode
*/
/**
* @template {Channel | MirrorChannel} ChannelT
* @template {ManagedNode<ChannelT>} NetworkNodeT
* @template {{ toString: () => string }} KeyT
*/
export default class ManagedNetwork {
/**
* @param {(address: string) => ChannelT} createNetworkChannel
*/
constructor(createNetworkChannel) {
/**
* Map of node account ID (as a string)
* to the node URL.
*
* @internal
* @type {Map<string, NetworkNodeT[]>}
*/
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
this._network = new Map();
/**
* List of node account IDs.
*
* @protected
* @type {NetworkNodeT[]}
*/
this._nodes = [];
/**
* List of node account IDs.
*
* @protected
* @type {NetworkNodeT[]}
*/
this._healthyNodes = [];
/** @type {(address: string, cert?: string) => ChannelT} */
this._createNetworkChannel = createNetworkChannel;
/** @type {LedgerId | null} */
this._ledgerId = null;
this._minBackoff = 8000;
this._maxBackoff = 1000 * 60 * 60;
/** @type {number} */
this._maxNodeAttempts = -1;
this._nodeMinReadmitPeriod = this._minBackoff;
this._nodeMaxReadmitPeriod = this._maxBackoff;
this._earliestReadmitTime = Date.now() + this._nodeMinReadmitPeriod;
}
/**
* @deprecated
* @param {string} networkName
* @returns {this}
*/
setNetworkName(networkName) {
console.warn("Deprecated: Use `setLedgerId` instead");
return this.setLedgerId(networkName);
}
/**
* @deprecated
* @returns {string | null}
*/
get networkName() {
console.warn("Deprecated: Use `ledgerId` instead");
return this.ledgerId != null ? this.ledgerId.toString() : null;
}
/**
* @param {string|LedgerId} ledgerId
* @returns {this}
*/
setLedgerId(ledgerId) {
this._ledgerId =
typeof ledgerId === "string"
? LedgerId.fromString(ledgerId)
: ledgerId;
return this;
}
/**
* @returns {LedgerId | null}
*/
get ledgerId() {
return this._ledgerId != null ? this._ledgerId : null;
}
/**
* @abstract
* @param {[string, KeyT]} entry
* @returns {NetworkNodeT}
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
_createNodeFromNetworkEntry(entry) {
throw new Error("not implemented");
}
/**
* @abstract
* @param {Map<string, KeyT>} network
* @returns {number[]}
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
_getNodesToRemove(network) {
throw new Error("not implemented");
}
_removeDeadNodes() {
if (this._maxNodeAttempts > 0) {
for (let i = this._nodes.length - 1; i >= 0; i--) {
const node = this._nodes[i];
if (node._badGrpcStatusCount < this._maxNodeAttempts) {
continue;
}
this._closeNode(i);
}
}
}
_readmitNodes() {
const now = Date.now();
if (this._earliestReadmitTime <= now) {
let nextEarliestReadmitTime = Number.MAX_SAFE_INTEGER;
let searchForNextEarliestReadmitTime = true;
outer: for (let i = 0; i < this._nodes.length; i++) {
for (let j = 0; j < this._healthyNodes.length; j++) {
if (
searchForNextEarliestReadmitTime &&
this._nodes[i]._readmitTime > now
) {
nextEarliestReadmitTime = Math.min(
this._nodes[i]._readmitTime,
nextEarliestReadmitTime,
);
}
if (this._nodes[i] == this._healthyNodes[j]) {
continue outer;
}
}
searchForNextEarliestReadmitTime = false;
if (this._nodes[i]._readmitTime <= now) {
this._healthyNodes.push(this._nodes[i]);
}
}
this._earliestReadmitTime = Math.min(
Math.max(nextEarliestReadmitTime, this._nodeMinReadmitPeriod),
this._nodeMaxReadmitPeriod,
);
}
}
/**
* @param {number} count
* @returns {NetworkNodeT[]}
*/
_getNumberOfMostHealthyNodes(count) {
this._removeDeadNodes();
this._readmitNodes();
const nodes = [];
// Create a shallow for safe iteration
let healthyNodes = this._healthyNodes.slice();
count = Math.min(count, healthyNodes.length);
for (let i = 0; i < count; i++) {
// Select a random index
const nodeIndex = Math.floor(Math.random() * healthyNodes.length);
const selectedNode = healthyNodes[nodeIndex];
// Check if the node exists
if (!selectedNode) {
break; // Break out of the loop if undefined node is selected
}
// Add the selected node in array for execution
nodes.push(selectedNode);
// Remove all nodes with the same account id as
// the selected node account id from the array
healthyNodes = healthyNodes.filter(
// eslint-disable-next-line ie11/no-loop-func
(node) => node.getKey() !== selectedNode.getKey(),
);
}
return nodes;
}
/**
* @param {number} i
*/
_closeNode(i) {
const node = this._nodes[i];
node.close();
this._removeNodeFromNetwork(node);
this._nodes.splice(i, 1);
}
/**
* @param {NetworkNodeT} node
*/
_removeNodeFromNetwork(node) {
const network = /** @type {NetworkNodeT[]} */ (
this._network.get(node.getKey())
);
for (let j = 0; j < network.length; j++) {
if (network[j] === node) {
network.splice(j, 1);
break;
}
}
if (network.length === 0) {
this._network.delete(node.getKey());
}
}
/**
* @param {Map<string, KeyT>} network
* @returns {this}
*/
_setNetwork(network) {
/** @type {NetworkNodeT[]} */
const newNodes = [];
const newNodeKeys = new Set();
const newNodeAddresses = new Set();
/** @type {NetworkNodeT[]} */
const newHealthyNodes = [];
/** @type {Map<string, NetworkNodeT[]>} */
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const newNetwork = new Map();
// Remove nodes that are not in the new network
for (const i of this._getNodesToRemove(network)) {
this._closeNode(i);
}
// Copy all the unclosed nodes
for (const node of this._nodes) {
newNodes.push(node);
newNodeKeys.add(node.getKey());
newNodeAddresses.add(node.address.toString());
}
// Add new nodes
for (const [key, value] of network) {
if (
newNodeKeys.has(value.toString()) &&
newNodeAddresses.has(key)
) {
continue;
}
newNodes.push(this._createNodeFromNetworkEntry([key, value]));
}
// Shuffle the nodes so we don't immediately pick the first nodes
util.shuffle(newNodes);
// Copy all the nodes into the healhty nodes list initially
// and push the nodes into the network; this maintains the
// shuffled state from `newNodes`
for (const node of newNodes) {
if (!node.isHealthy()) {
continue;
}
newHealthyNodes.push(node);
const newNetworkNodes = newNetwork.has(node.getKey())
? /** @type {NetworkNodeT[]} */ (newNetwork.get(node.getKey()))
: [];
newNetworkNodes.push(node);
newNetwork.set(node.getKey(), newNetworkNodes);
}
this._nodes = newNodes;
this._healthyNodes = newHealthyNodes;
this._network = newNetwork;
return this;
}
/**
* @returns {number}
*/
get maxNodeAttempts() {
return this._maxNodeAttempts;
}
/**
* @param {number} maxNodeAttempts
* @returns {this}
*/
setMaxNodeAttempts(maxNodeAttempts) {
this._maxNodeAttempts = maxNodeAttempts;
return this;
}
/**
* @returns {number}
*/
get minBackoff() {
return this._minBackoff;
}
/**
* @param {number} minBackoff
* @returns {this}
*/
setMinBackoff(minBackoff) {
this._minBackoff = minBackoff;
for (const node of this._nodes) {
node.setMinBackoff(minBackoff);
}
return this;
}
/**
* @returns {number}
*/
get maxBackoff() {
return this._maxBackoff;
}
/**
* @param {number} maxBackoff
* @returns {this}
*/
setMaxBackoff(maxBackoff) {
this._maxBackoff = maxBackoff;
for (const node of this._nodes) {
node.setMaxBackoff(maxBackoff);
}
return this;
}
/**
* @returns {number}
*/
get nodeMinReadmitPeriod() {
return this._nodeMinReadmitPeriod;
}
/**
* @param {number} nodeMinReadmitPeriod
* @returns {this}
*/
setNodeMinReadmitPeriod(nodeMinReadmitPeriod) {
this._nodeMinReadmitPeriod = nodeMinReadmitPeriod;
this._earliestReadmitTime = Date.now() + this._nodeMinReadmitPeriod;
return this;
}
/**
* @returns {number}
*/
get nodeMaxReadmitPeriod() {
return this._nodeMaxReadmitPeriod;
}
/**
* @param {number} nodeMaxReadmitPeriod
* @returns {this}
*/
setNodeMaxReadmitPeriod(nodeMaxReadmitPeriod) {
this._nodeMaxReadmitPeriod = nodeMaxReadmitPeriod;
return this;
}
/**
* @param {KeyT=} key
* @returns {NetworkNodeT}
*/
getNode(key) {
this._readmitNodes();
if (key != null && key != undefined) {
const lockedNodes = this._network.get(key.toString());
if (lockedNodes) {
const randomNodeAddress = Math.floor(
Math.random() * lockedNodes.length,
);
return /** @type {NetworkNodeT[]} */ (lockedNodes)[
randomNodeAddress
];
} else {
const nodes = Array.from(this._network.keys());
const randomNodeAccountId =
nodes[Math.floor(Math.random() * nodes.length)];
const randomNode = this._network.get(randomNodeAccountId);
// We get the `randomNodeAccountId` from the network mapping,
// so it cannot be `undefined`
const randomNodeAddress = Math.floor(
// @ts-ignore
Math.random() * randomNode.length,
);
// @ts-ignore
return randomNode[randomNodeAddress];
}
} else {
if (this._healthyNodes.length == 0) {
throw new Error("failed to find a healthy working node");
}
return this._healthyNodes[
Math.floor(Math.random() * this._healthyNodes.length)
];
}
}
/**
* @param {NetworkNodeT} node
*/
increaseBackoff(node) {
node.increaseBackoff();
for (let i = 0; i < this._healthyNodes.length; i++) {
if (this._healthyNodes[i] == node) {
this._healthyNodes.splice(i, 1);
}
}
}
/**
* @param {NetworkNodeT} node
*/
decreaseBackoff(node) {
node.decreaseBackoff();
}
close() {
for (const node of this._nodes) {
node.close();
}
this._network.clear();
this._nodes = [];
}
}