"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
    var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
    if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
    else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
    return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
    if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
var __param = (this && this.__param) || function (paramIndex, decorator) {
    return function (target, key) { decorator(target, key, paramIndex); }
};
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.NodeManager = void 0;
const LogStoreNodeManager_json_1 = require("@logsn/contracts/artifacts/src/NodeManager.sol/LogStoreNodeManager.json");
const utils_1 = require("@streamr/utils");
const rxjs_1 = require("rxjs");
const streamr_client_1 = __importDefault(require("streamr-client"));
const tsyringe_1 = require("tsyringe");
const lodash_1 = require("lodash");
const Config_1 = require("../Config");
const Ethereum_1 = require("../Ethereum");
const Config_2 = require("../streamr/Config");
const ContractFactory_1 = require("../streamr/ContractFactory");
const LoggerFactory_1 = require("../streamr/LoggerFactory");
const StreamrClient_1 = require("../streamr/StreamrClient");
const contract_1 = require("../streamr/utils/contract");
const AsyncStaleThenRevalidateCache_1 = require("../utils/AsyncStaleThenRevalidateCache");
const BroadbandObservable_1 = require("../utils/rxjs/BroadbandObservable");
const takeUntilWithFilter_1 = require("../utils/rxjs/takeUntilWithFilter");
let NodeManager = class NodeManager {
    // ==================================
    constructor(contractFactory, loggerFactory, logStoreClientConfig, streamrClientConfig, streamrClient) {
        this.streamrClient = streamrClient;
        this.abort$ = new rxjs_1.Subject();
        this.nodeUrlCache = new AsyncStaleThenRevalidateCache_1.AsyncStaleThenRevalidateCache({
            maxAge: 300000, // 5 min
        });
        // ==== Node URL List Management ====
        // replay subject emits the last value to new subscribers. But if there's no value, it won't emit until it does.
        this.lastUrlList$ = new rxjs_1.ReplaySubject(1);
        // throttledUpdateList will help us to update the lastUrlList$ with the node information output at most once every X minutes.
        this.throttledUpdateList = (0, lodash_1.throttle)(() => { }, 60000);
        this.getNodeFromAddress = async (address) => {
            return (0, contract_1.queryAllReadonlyContracts)((contract) => {
                return contract.nodes(address);
            }, this.logStoreManagerContractsReadonly);
        };
        this.parseMetadata = (metadata) => {
            try {
                return JSON.parse(metadata);
            }
            catch (e) {
                this.logger.error(`Error parsing metadata: ${e}`);
                throw e;
            }
        };
        this.contractFactory = contractFactory;
        this.logStoreClientConfig = logStoreClientConfig;
        this.streamrClientConfig = streamrClientConfig;
        this.logger = loggerFactory.createLogger(module);
        this.logStoreManagerContractsReadonly = (0, Ethereum_1.getStreamRegistryChainProviders)(this.streamrClientConfig).map((provider) => {
            return this.contractFactory.createReadContract((0, utils_1.toEthereumAddress)(this.logStoreClientConfig.contracts.logStoreNodeManagerChainAddress), LogStoreNodeManager_json_1.abi, provider, 'logStoreNodeManager');
        });
        // This function is throttled to run at most once every minute.
        // Also, we're able to get the lastUrlList$ value instantly, as soon as the first heartbeat message arrives.
        this.throttledUpdateList = (0, lodash_1.throttle)(() => this.updateNodeUrlList(), 60000, { leading: true });
        this.throttledUpdateList();
    }
    /**
     * Returns an observable that emits a list nodes,
     * sorted in order of their latency (from lowest to highest).
     * Latency is calculated from the time the message was published to the time it was received.
     */
    get nodeListByLatency$() {
        const heartbeatStreamAddress = this.logStoreClientConfig.contracts.logStoreNodeManagerChainAddress +
            '/heartbeat';
        // from promise to observable
        const heartbeatStream$ = (0, rxjs_1.defer)(() => this.streamrClient.getStream(heartbeatStreamAddress));
        const heartbeatMessage$ = heartbeatStream$.pipe(
        // merge map is used because the result is also an observable that we want to subscribe to.
        (0, rxjs_1.mergeMap)((stream) => (0, BroadbandObservable_1.createBroadbandObservable)(this.streamrClient, stream)));
        const heartbeatInfo$ = heartbeatMessage$.pipe(
        // merge map is used because it's a Promise. We need to wait for the promise to resolve.
        (0, rxjs_1.mergeMap)(async ([_content, metadata]) => {
            // calculates the lag time
            // we know the message timestamp is the time the message was published, and we can compare it to the current time
            const latency = Date.now() - metadata.timestamp;
            return {
                nodeAddress: metadata.publisherId,
                publishDate: new Date(metadata.timestamp),
                latency,
            };
        }));
        return heartbeatInfo$.pipe(
        // creates a map of EthereumAddress to latency
        // map is used so there's no repeated EthereumAddress
        (0, rxjs_1.scan)((acc, info) => {
            acc.set(info.nodeAddress, info);
            return acc;
        }, new Map()), 
        // audit is like throttle but emits the last value instead of the first.
        // avoids emitting a new value for every heartbeat message, like a batch.
        (0, rxjs_1.auditTime)(10), // in milliseconds
        // orders the output list by latency, ascending
        (0, rxjs_1.map)((nodeMap) => Array.from(nodeMap.entries()).sort((a, b) => a[1].latency - b[1].latency)), 
        // extract only heartbeat information
        (0, rxjs_1.map)((nodeList) => nodeList.map((infoMap) => infoMap[1])), 
        // Share so that if the user also subscribes directly,
        // it wouldn't create another subscription, but reuse the same.
        (0, rxjs_1.share)());
    }
    /**
     * updateNodeUrl is a function that, when called, updates the lastUrlList$
     *
     * Steps:
     *
     * - Transforms the list of addresses in a list of URLs.
     * - After subscription, a counter is initiated after 3 seconds after the first URL is found.
     * - Otherwise it runs and times out after 15 seconds.
     * @private
     */
    updateNodeUrlList() {
        this.nodeListByLatency$
            .pipe(
        // transform node addresses into URLs
        (0, rxjs_1.mergeMap)((heartbeatInfoList) => Promise.all(heartbeatInfoList.map((info) => this.getNodeUrl(info.nodeAddress)))), 
        // remove nulls -- nodes that choose not to expose a gateway
        (0, rxjs_1.map)((urlList) => urlList.filter((url) => url !== null)), 
        // We should take until 3 seconds, but this timer should start after the first value is emitted.
        (0, takeUntilWithFilter_1.takeUntilWithFilter)((list) => list.length > 0, (0, rxjs_1.timer)(3000)), 
        // Emits a value just if there's one url or more. This function cannot return an empty list.
        (0, rxjs_1.filter)((urlList) => urlList.length > 0), 
        // Should emit at least one-value list within 15 seconds.
        // If didn't receive any data until then, it's an error.
        (0, rxjs_1.timeout)(15000), (0, rxjs_1.takeUntil)(this.abort$))
            // we subscribe indefinetely, but we don't worry about memory leak because it completes after 3 or 15 seconds.
            .subscribe({
            next: (list) => {
                this.lastUrlList$.next(list);
            },
            error: (e) => {
                this.logger.error('Error getting node URL list', e);
            },
        });
    }
    async getNodeUrlsByLatency() {
        this.throttledUpdateList();
        // should be instant, as the lastUrlList$ is a replay subject
        // however if the lastUrlList$ is empty, it will wait for the first value to be emitted
        // and it should throw a timeout error if something goes wrong.
        return (0, rxjs_1.firstValueFrom)(this.lastUrlList$.pipe((0, rxjs_1.timeout)(15000)));
    }
    async getNodeAddressFromUrl(url) {
        const nodeAddresses = await this.getActiveNodes();
        for (const nodeAddress of nodeAddresses) {
            const node = await this.getNodeFromAddress(nodeAddress);
            if (node.metadata.includes(url)) {
                return nodeAddress;
            }
        }
        return null;
    }
    async getNodeUrl(nodeAddress, useCache = true) {
        if (useCache) {
            const cachedUrl = this.nodeUrlCache.get(nodeAddress, async () => this.getNodeUrl(nodeAddress, false));
            if (cachedUrl) {
                return cachedUrl;
            }
        }
        const node = await this.getNodeFromAddress(nodeAddress);
        if (node.metadata.includes('http')) {
            try {
                const metadata = this.parseMetadata(node.metadata);
                return metadata.http ?? null;
            }
            catch (e) {
                this.logger.error(`Error parsing metadata: ${e}`);
            }
        }
        return null;
    }
    async getActiveNodes() {
        return (0, contract_1.queryAllReadonlyContracts)((contract) => {
            return contract.nodeAddresses();
        }, this.logStoreManagerContractsReadonly);
    }
    destroy() {
        this.abort$.next(1);
        this.lastUrlList$.complete();
        this.throttledUpdateList.cancel();
    }
    [Symbol.dispose]() {
        this.destroy();
    }
};
exports.NodeManager = NodeManager;
exports.NodeManager = NodeManager = __decorate([
    (0, tsyringe_1.scoped)(tsyringe_1.Lifecycle.ContainerScoped),
    __param(0, (0, tsyringe_1.inject)(ContractFactory_1.ContractFactoryInjectionToken)),
    __param(1, (0, tsyringe_1.inject)(LoggerFactory_1.LoggerFactoryInjectionToken)),
    __param(2, (0, tsyringe_1.inject)(Config_1.LogStoreClientConfigInjectionToken)),
    __param(3, (0, tsyringe_1.inject)(Config_2.StreamrClientConfigInjectionToken)),
    __param(4, (0, tsyringe_1.inject)(StreamrClient_1.StreamrClientInjectionToken)),
    __metadata("design:paramtypes", [Object, Object, Object, Object, streamr_client_1.default])
], NodeManager);
//# sourceMappingURL=NodeManager.js.map