"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.NetworkNodeFacade = exports.NetworkNodeFactory = void 0;
const trackerless_network_1 = require("@streamr/trackerless-network");
const utils_1 = require("@streamr/utils");
const eventemitter3_1 = __importDefault(require("eventemitter3"));
const tsyringe_1 = require("tsyringe");
const Authentication_1 = require("./Authentication");
const Config_1 = require("./Config");
const DestroySignal_1 = require("./DestroySignal");
const OperatorRegistry_1 = require("./contracts/OperatorRegistry");
const promises_1 = require("./utils/promises");
const utils_2 = require("./utils/utils");
/**
 * The factory is used so that integration tests can replace the real network node with a fake instance
 */
/* eslint-disable class-methods-use-this */
let NetworkNodeFactory = class NetworkNodeFactory {
    createNetworkNode(opts) {
        return (0, trackerless_network_1.createNetworkNode)(opts);
    }
};
exports.NetworkNodeFactory = NetworkNodeFactory;
exports.NetworkNodeFactory = NetworkNodeFactory = __decorate([
    (0, tsyringe_1.scoped)(tsyringe_1.Lifecycle.ContainerScoped)
], NetworkNodeFactory);
/**
 * Wrap a network node.
 * Lazily creates & starts node on first call to getNode().
 */
let NetworkNodeFacade = class NetworkNodeFacade {
    cachedNode;
    startNodeCalled = false;
    startNodeComplete = false;
    networkNodeFactory;
    operatorRegistry;
    config;
    authentication;
    eventEmitter;
    destroySignal;
    constructor(networkNodeFactory, operatorRegistry, config, authentication, destroySignal) {
        this.networkNodeFactory = networkNodeFactory;
        this.operatorRegistry = operatorRegistry;
        this.config = config;
        this.authentication = authentication;
        this.eventEmitter = new eventemitter3_1.default();
        this.destroySignal = destroySignal;
        destroySignal.onDestroy.listen(this.destroy);
    }
    assertNotDestroyed() {
        this.destroySignal.assertNotDestroyed();
    }
    async getNetworkOptions() {
        const entryPoints = await this.getEntryPoints();
        const localPeerDescriptor = this.config.network.controlLayer.peerDescriptor ?
            (0, utils_2.peerDescriptorTranslator)(this.config.network.controlLayer.peerDescriptor) : undefined;
        return {
            layer0: {
                ...this.config.network.controlLayer,
                entryPoints: entryPoints.map(utils_2.peerDescriptorTranslator),
                peerDescriptor: localPeerDescriptor,
                websocketPortRange: (this.config.network.controlLayer.websocketPortRange !== null)
                    ? this.config.network.controlLayer.websocketPortRange
                    : undefined
            },
            networkNode: this.config.network.node,
            metricsContext: new utils_1.MetricsContext()
        };
    }
    async initNode() {
        this.assertNotDestroyed();
        if (this.cachedNode) {
            return this.cachedNode;
        }
        const node = this.networkNodeFactory.createNetworkNode(await this.getNetworkOptions());
        if (!this.destroySignal.isDestroyed()) {
            this.cachedNode = node;
        }
        return node;
    }
    /**
     * Stop network node, or wait for it to stop if already stopping.
     * Subsequent calls to getNode/start will fail.
     */
    destroy = (0, promises_1.pOnce)(async () => {
        const node = this.cachedNode;
        this.cachedNode = undefined;
        // stop node only if started or in progress
        if (node && this.startNodeCalled) {
            if (!this.startNodeComplete) {
                // wait for start to finish before stopping node
                const startNodeTask = this.startNodeTask();
                this.startNodeTask.reset(); // allow subsequent calls to fail
                await startNodeTask;
            }
            await node.stop();
        }
        this.startNodeTask.reset(); // allow subsequent calls to fail
    });
    /**
     * Start network node, or wait for it to start if already started.
     */
    startNodeTask = (0, promises_1.pOnce)(async (doJoin = true) => {
        this.startNodeCalled = true;
        try {
            const node = await this.initNode();
            if (!this.destroySignal.isDestroyed()) {
                await node.start(doJoin);
            }
            if (this.destroySignal.isDestroyed()) {
                await node.stop();
            }
            else {
                this.eventEmitter.emit('start');
            }
            this.assertNotDestroyed();
            return node;
        }
        finally {
            this.startNodeComplete = true;
        }
    });
    startNode = this.startNodeTask;
    getNode = this.startNodeTask;
    async getNodeId() {
        const node = await this.getNode();
        return node.getNodeId();
    }
    /**
     * Calls publish on node after starting it.
     * Basically a wrapper around: (await getNode()).publish(…)
     * but will be sync in case that node is already started.
     * Zalgo intentional. See below.
     */
    async publishToNode(streamMessage) {
        // NOTE: function is intentionally not async for performance reasons.
        // Will call cachedNode.publish immediately if cachedNode is set.
        // Otherwise will wait for node to start.
        this.destroySignal.assertNotDestroyed();
        if (this.isStarting()) {
            // use .then instead of async/await so
            // this.cachedNode.publish call can be sync
            return this.startNodeTask().then((node) => node.broadcast(streamMessage));
        }
        return this.cachedNode.broadcast(streamMessage);
    }
    async inspect(node, streamPartId) {
        if (this.isStarting()) {
            await this.startNodeTask(false);
        }
        const peerDescriptor = (0, utils_2.peerDescriptorTranslator)(node);
        return this.cachedNode.inspect(peerDescriptor, streamPartId);
    }
    async setProxies(streamPartId, nodes, direction, connectionCount) {
        if (this.isStarting()) {
            await this.startNodeTask(false);
        }
        const peerDescriptors = nodes.map(utils_2.peerDescriptorTranslator);
        await this.cachedNode.setProxies(streamPartId, peerDescriptors, direction, await this.authentication.getAddress(), connectionCount);
    }
    async setStreamPartEntryPoints(streamPartId, nodeDescriptors) {
        if (this.isStarting()) {
            await this.startNodeTask(false);
        }
        const peerDescriptors = nodeDescriptors.map(utils_2.peerDescriptorTranslator);
        this.cachedNode.setStreamPartEntryPoints(streamPartId, peerDescriptors);
    }
    isStarting() {
        return !this.cachedNode || !this.startNodeComplete;
    }
    once(eventName, listener) {
        this.eventEmitter.once(eventName, listener);
    }
    async getEntryPoints() {
        const discoveryConfig = this.config.network.controlLayer.entryPointDiscovery;
        const discoveredEntryPoints = (discoveryConfig?.enabled)
            ? await this.operatorRegistry.findRandomNetworkEntrypoints(discoveryConfig.maxEntryPoints, discoveryConfig.maxQueryResults, discoveryConfig.maxHeartbeatAgeHours)
            : [];
        return [...this.config.network.controlLayer.entryPoints, ...discoveredEntryPoints];
    }
};
exports.NetworkNodeFacade = NetworkNodeFacade;
exports.NetworkNodeFacade = NetworkNodeFacade = __decorate([
    (0, tsyringe_1.scoped)(tsyringe_1.Lifecycle.ContainerScoped),
    __param(2, (0, tsyringe_1.inject)(Config_1.ConfigInjectionToken)),
    __param(3, (0, tsyringe_1.inject)(Authentication_1.AuthenticationInjectionToken)),
    __metadata("design:paramtypes", [NetworkNodeFactory,
        OperatorRegistry_1.OperatorRegistry, Object, Object, DestroySignal_1.DestroySignal])
], NetworkNodeFacade);
//# sourceMappingURL=NetworkNodeFacade.js.map