"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Node = exports.Event = void 0;
const events_1 = require("events");
const protocol_1 = require("@streamr/protocol");
const NodeToNode_1 = require("../protocol/NodeToNode");
const utils_1 = require("@streamr/utils");
const StreamPartManager_1 = require("./StreamPartManager");
const DuplicateMessageDetector_1 = require("./DuplicateMessageDetector");
const utils_2 = require("@streamr/utils");
const constants_1 = require("../constants");
const TrackerManager_1 = require("./TrackerManager");
const Propagation_1 = require("./propagation/Propagation");
const DisconnectionManager_1 = require("./DisconnectionManager");
const ProxyStreamConnectionManager_1 = require("./ProxyStreamConnectionManager");
const logger = new utils_2.Logger(module);
var Event;
(function (Event) {
    Event["NODE_CONNECTED"] = "streamr:node:node-connected";
    Event["NODE_DISCONNECTED"] = "streamr:node:node-disconnected";
    Event["MESSAGE_RECEIVED"] = "streamr:node:message-received";
    Event["UNSEEN_MESSAGE_RECEIVED"] = "streamr:node:unseen-message-received";
    Event["DUPLICATE_MESSAGE_RECEIVED"] = "streamr:node:duplicate-message-received";
    Event["NODE_SUBSCRIBED"] = "streamr:node:subscribed-successfully";
    Event["NODE_UNSUBSCRIBED"] = "streamr:node:node-unsubscribed";
    Event["PROXY_CONNECTION_ACCEPTED"] = "streamr:node:proxy-connection-accepted";
    Event["PROXY_CONNECTION_REJECTED"] = "streamr:node:proxy-connection-rejected";
    Event["ONE_WAY_CONNECTION_CLOSED"] = "stream:node-one-way-connection-closed";
    Event["JOIN_COMPLETED"] = "stream:node-stream-join-operation-completed";
    Event["JOIN_FAILED"] = "stream:node-stream-join-operation-failed";
})(Event = exports.Event || (exports.Event = {}));
class Node extends events_1.EventEmitter {
    constructor(opts) {
        super();
        this.extraMetadata = {};
        this.nodeToNode = opts.protocols.nodeToNode;
        this.peerInfo = opts.peerInfo;
        this.nodeConnectTimeout = opts.nodeConnectTimeout || 15000;
        this.consecutiveDeliveryFailures = {};
        this.started = new Date().toLocaleString();
        this.acceptProxyConnections = opts.acceptProxyConnections;
        this.metricsContext = opts.metricsContext || new utils_1.MetricsContext();
        this.metrics = {
            publishMessagesPerSecond: new utils_1.RateMetric(),
            publishBytesPerSecond: new utils_1.RateMetric(),
        };
        this.metricsContext.addMetrics('node', this.metrics);
        this.streamPartManager = new StreamPartManager_1.StreamPartManager();
        this.disconnectionManager = new DisconnectionManager_1.DisconnectionManager({
            getAllNodes: this.nodeToNode.getAllConnectionNodeIds,
            hasSharedStreamParts: this.streamPartManager.isNodePresent.bind(this.streamPartManager),
            disconnect: this.nodeToNode.disconnectFromNode.bind(this.nodeToNode),
            disconnectionDelayInMs: opts.disconnectionWaitTime,
            cleanUpIntervalInMs: 2 * 60 * 1000
        });
        this.propagation = new Propagation_1.Propagation({
            sendToNeighbor: async (neighborId, streamMessage) => {
                try {
                    await this.nodeToNode.sendData(neighborId, streamMessage);
                    this.consecutiveDeliveryFailures[neighborId] = 0;
                }
                catch (e) {
                    const serializedMsgId = streamMessage.getMessageID().serialize();
                    logger.warn('failed to propagate %s (consecutiveFails=%d) to subscriber %s, reason: %s', serializedMsgId, this.consecutiveDeliveryFailures[neighborId] || 0, neighborId, e);
                    // TODO: this is hack to get around the issue where `StreamStateManager` believes that we are
                    //  connected to a neighbor whilst `WebRtcEndpoint` knows that we are not. In this situation, the
                    //  Node will continuously attempt to propagate messages to the neighbor but will not actually ever
                    //  (re-)attempt a connection unless as a side-effect of something else (e.g. subscribing to another
                    //  stream, and the neighbor in question happens to get assigned to us via the other stream.)
                    //
                    // This hack basically counts consecutive delivery failures, and upon hitting 100 such failures,
                    // decides to forcefully disconnect the neighbor.
                    //
                    // Ideally this hack would not be needed, but alas, it seems like with the current event-system,
                    // we don't end up with an up-to-date state in the logic layer. I believe something like the
                    // ConnectionManager-model could help us solve the issue for good.
                    if (this.consecutiveDeliveryFailures[neighborId] === undefined) {
                        this.consecutiveDeliveryFailures[neighborId] = 0;
                    }
                    this.consecutiveDeliveryFailures[neighborId] += 1;
                    if (this.consecutiveDeliveryFailures[neighborId] >= 100) {
                        logger.warn(`disconnecting from ${neighborId} due to 100 consecutive delivery failures`);
                        this.onNodeDisconnected(neighborId); // force disconnect
                        this.consecutiveDeliveryFailures[neighborId] = 0;
                    }
                    throw e;
                }
            },
            minPropagationTargets: Math.floor(constants_1.DEFAULT_MAX_NEIGHBOR_COUNT / 2)
        });
        this.trackerManager = new TrackerManager_1.TrackerManager(opts.protocols.nodeToTracker, opts, this.streamPartManager, (includeRtt) => ({
            started: this.started,
            location: this.peerInfo.location,
            extra: this.extraMetadata,
            rtts: includeRtt ? this.nodeToNode.getRtts() : null,
            version: "brubeck-1.0"
        }), {
            subscribeToStreamPartIfHaveNotYet: this.subscribeToStreamIfHaveNotYet.bind(this),
            subscribeToStreamPartOnNodes: this.subscribeToStreamPartOnNodes.bind(this),
            unsubscribeFromStreamPartOnNode: this.unsubscribeFromStreamPartOnNode.bind(this),
            emitJoinCompleted: this.emitJoinCompleted.bind(this),
            emitJoinFailed: this.emitJoinFailed.bind(this)
        });
        this.proxyStreamConnectionManager = new ProxyStreamConnectionManager_1.ProxyStreamConnectionManager({
            trackerManager: this.trackerManager,
            streamPartManager: this.streamPartManager,
            propagation: this.propagation,
            node: this,
            nodeToNode: this.nodeToNode,
            acceptProxyConnections: this.acceptProxyConnections,
            nodeConnectTimeout: this.nodeConnectTimeout
        });
        this.nodeToNode.on(NodeToNode_1.Event.NODE_CONNECTED, (nodeId) => this.emit(Event.NODE_CONNECTED, nodeId));
        this.nodeToNode.on(NodeToNode_1.Event.DATA_RECEIVED, (broadcastMessage, nodeId) => this.onDataReceived(broadcastMessage.streamMessage, nodeId));
        this.nodeToNode.on(NodeToNode_1.Event.NODE_DISCONNECTED, (nodeId) => this.onNodeDisconnected(nodeId));
        this.nodeToNode.on(NodeToNode_1.Event.PROXY_CONNECTION_REQUEST_RECEIVED, (message, nodeId) => {
            this.proxyStreamConnectionManager.processProxyConnectionRequest(message, nodeId);
        });
        this.nodeToNode.on(NodeToNode_1.Event.PROXY_CONNECTION_RESPONSE_RECEIVED, (message, nodeId) => {
            this.proxyStreamConnectionManager.processProxyConnectionResponse(message, nodeId);
        });
        this.nodeToNode.on(NodeToNode_1.Event.LEAVE_REQUEST_RECEIVED, (message, nodeId) => {
            this.proxyStreamConnectionManager.processLeaveRequest(message, nodeId);
        });
    }
    start() {
        logger.trace('started');
        this.trackerManager.start();
    }
    subscribeToStreamIfHaveNotYet(streamPartId, sendStatus = true) {
        if (!this.streamPartManager.isSetUp(streamPartId)) {
            logger.trace('add %s to streams', streamPartId);
            this.streamPartManager.setUpStreamPart(streamPartId);
            this.trackerManager.onNewStreamPart(streamPartId); // TODO: perhaps we should react based on event from StreamManager?
            if (sendStatus) {
                this.trackerManager.sendStreamPartStatus(streamPartId);
            }
        }
        else if (this.streamPartManager.isSetUp(streamPartId) && this.streamPartManager.isBehindProxy(streamPartId)) {
            logger.trace(`Could not join stream ${streamPartId} as stream is set to be behind proxy`);
        }
    }
    unsubscribeFromStream(streamPartId, sendStatus = true) {
        logger.trace('remove %s from streams', streamPartId);
        this.streamPartManager.removeStreamPart(streamPartId);
        this.trackerManager.onUnsubscribeFromStreamPart(streamPartId);
        if (sendStatus) {
            this.trackerManager.sendStreamPartStatus(streamPartId);
        }
    }
    subscribeToStreamPartOnNodes(nodeIds, streamPartId, trackerId, reattempt) {
        const subscribePromises = nodeIds.map(async (nodeId) => {
            await (0, utils_2.withTimeout)(this.nodeToNode.connectToNode(nodeId, trackerId, !reattempt), this.nodeConnectTimeout);
            this.disconnectionManager.cancelScheduledDisconnection(nodeId);
            this.subscribeToStreamPartOnNode(nodeId, streamPartId, false);
            return nodeId;
        });
        return Promise.allSettled(subscribePromises);
    }
    async addProxyConnection(streamPartId, contactNodeId, direction, userId) {
        let resolveHandler;
        let rejectHandler;
        await Promise.all([
            new Promise((resolve, reject) => {
                resolveHandler = (node, stream, eventDirection) => {
                    if (node === contactNodeId && stream === streamPartId && direction === eventDirection) {
                        resolve();
                    }
                };
                rejectHandler = (node, stream, eventDirection, reason) => {
                    if (node === contactNodeId && stream === streamPartId && direction === eventDirection) {
                        reject(new Error(`Joining stream as proxy ${direction} failed on contact-node ${contactNodeId} for stream ${streamPartId}`
                            + ` reason: ${reason}`));
                    }
                };
                this.on(Event.PROXY_CONNECTION_ACCEPTED, resolveHandler);
                this.on(Event.PROXY_CONNECTION_REJECTED, rejectHandler);
            }),
            this.proxyStreamConnectionManager.openProxyConnection(streamPartId, contactNodeId, direction, userId)
        ]).finally(() => {
            this.off(Event.PROXY_CONNECTION_ACCEPTED, resolveHandler);
            this.off(Event.PROXY_CONNECTION_REJECTED, rejectHandler);
        });
    }
    async removeProxyConnection(streamPartId, contactNodeId, direction) {
        await this.proxyStreamConnectionManager.closeProxyConnection(streamPartId, contactNodeId, direction);
    }
    // Null source is used when a message is published by the node itself
    onDataReceived(streamMessage, source = null) {
        const streamPartId = streamMessage.getStreamPartID();
        // Check if the stream is set as one-directional and has inbound connection if message is content typed
        if (source
            && this.streamPartManager.isSetUp(streamPartId)
            && this.streamPartManager.isBehindProxy(streamPartId)
            && streamMessage.messageType === protocol_1.StreamMessageType.MESSAGE
            && !this.streamPartManager.hasInboundConnection(streamPartId, source)) {
            logger.warn(`Unexpected message received on outbound proxy stream from node ${source} on stream ${streamPartId}`);
            return;
        }
        this.emit(Event.MESSAGE_RECEIVED, streamMessage, source);
        this.subscribeToStreamIfHaveNotYet(streamPartId);
        // Check duplicate
        let isUnseen;
        try {
            isUnseen = this.streamPartManager.markNumbersAndCheckThatIsNotDuplicate(streamMessage.messageId, streamMessage.prevMsgRef);
        }
        catch (e) {
            if (e instanceof DuplicateMessageDetector_1.InvalidNumberingError) {
                logger.trace('received from %s data %j with invalid numbering', source, streamMessage.messageId);
                return;
            }
            if (e instanceof DuplicateMessageDetector_1.GapMisMatchError) {
                logger.warn('received from %s data %j with gap mismatch detected: %j', source, streamMessage.messageId, e);
                return;
            }
            throw e;
        }
        if (isUnseen) {
            logger.trace('received from %s data %j', source, streamMessage.messageId);
            const propagationTargets = this.getPropagationTargets(streamMessage);
            this.emit(Event.UNSEEN_MESSAGE_RECEIVED, streamMessage, source);
            this.propagation.feedUnseenMessage(streamMessage, propagationTargets, source);
            if (source === null) {
                this.metrics.publishMessagesPerSecond.record(1);
                this.metrics.publishBytesPerSecond.record(streamMessage.getSerializedContent().length);
            }
        }
        else {
            logger.trace('ignoring duplicate data %j (from %s)', streamMessage.messageId, source);
            this.emit(Event.DUPLICATE_MESSAGE_RECEIVED, streamMessage, source);
        }
    }
    stop() {
        this.proxyStreamConnectionManager.stop();
        this.disconnectionManager.stop();
        this.nodeToNode.stop();
        return this.trackerManager.stop();
    }
    getPropagationTargets(streamMessage) {
        const streamPartId = streamMessage.getStreamPartID();
        let propagationTargets = [];
        propagationTargets = propagationTargets.concat([...this.streamPartManager.getOutboundNodesForStreamPart(streamPartId)]);
        if (this.acceptProxyConnections) {
            if (protocol_1.GroupKeyRequest.is(streamMessage) || protocol_1.GroupKeyResponse.is(streamMessage)) {
                const { recipient } = protocol_1.GroupKeyRequest.fromStreamMessage(streamMessage);
                propagationTargets = propagationTargets.concat(this.proxyStreamConnectionManager.getNodeIdsForUserId(streamPartId, recipient));
            }
        }
        else if (this.streamPartManager.isBehindProxy(streamMessage.getStreamPartID())
            && this.proxyStreamConnectionManager.isProxiedStreamPart(streamMessage.getStreamPartID(), protocol_1.ProxyDirection.SUBSCRIBE)) {
            propagationTargets = propagationTargets.concat([...this.streamPartManager.getInboundNodesForStreamPart(streamPartId)]);
        }
        return propagationTargets;
    }
    subscribeToStreamPartOnNode(node, streamPartId, sendStatus = true) {
        this.streamPartManager.addNeighbor(streamPartId, node);
        this.propagation.onNeighborJoined(node, streamPartId);
        if (sendStatus) {
            this.trackerManager.sendStreamPartStatus(streamPartId);
        }
        this.emit(Event.NODE_SUBSCRIBED, node, streamPartId);
        return node;
    }
    unsubscribeFromStreamPartOnNode(node, streamPartId, sendStatus = true) {
        this.streamPartManager.removeNodeFromStreamPart(streamPartId, node);
        logger.trace('node %s unsubscribed from stream %s', node, streamPartId);
        this.emit(Event.NODE_UNSUBSCRIBED, node, streamPartId);
        this.disconnectionManager.scheduleDisconnectionIfNoSharedStreamParts(node);
        if (sendStatus) {
            this.trackerManager.sendStreamPartStatus(streamPartId);
        }
    }
    onNodeDisconnected(node) {
        const [streams, proxiedStreams] = this.streamPartManager.removeNodeFromAllStreamParts(node);
        logger.trace('removed all subscriptions of node %s', node);
        streams.forEach((s) => {
            this.trackerManager.sendStreamPartStatus(s);
        });
        proxiedStreams.forEach((s) => {
            this.proxyStreamConnectionManager.reconnect(node, s);
        });
        this.emit(Event.NODE_DISCONNECTED, node);
    }
    getStreamParts() {
        return this.streamPartManager.getStreamParts();
    }
    getNeighbors() {
        return this.streamPartManager.getAllNodes();
    }
    getNodeId() {
        return this.peerInfo.peerId;
    }
    getMetricsContext() {
        return this.metricsContext;
    }
    async subscribeAndWaitForJoinOperation(streamPartId, timeout = this.nodeConnectTimeout) {
        if (this.streamPartManager.isSetUp(streamPartId)) {
            return this.streamPartManager.getAllNodesForStreamPart(streamPartId).length;
        }
        let resolveHandler;
        let rejectHandler;
        const res = await Promise.all([
            (0, utils_2.withTimeout)(new Promise((resolve, reject) => {
                resolveHandler = (stream, numOfNeighbors) => {
                    if (stream === streamPartId) {
                        resolve(numOfNeighbors);
                    }
                };
                rejectHandler = (stream, error) => {
                    if (stream === streamPartId) {
                        reject(new Error(error));
                    }
                };
                this.on(Event.JOIN_COMPLETED, resolveHandler);
                this.on(Event.JOIN_FAILED, rejectHandler);
            }), timeout),
            this.subscribeToStreamIfHaveNotYet(streamPartId)
        ]).finally(() => {
            this.off(Event.JOIN_COMPLETED, resolveHandler);
            this.off(Event.JOIN_FAILED, rejectHandler);
        });
        return res[0];
    }
    emitJoinCompleted(streamPartId, numOfNeighbors) {
        this.emit(Event.JOIN_COMPLETED, streamPartId, numOfNeighbors);
    }
    emitJoinFailed(streamPartId, error) {
        this.emit(Event.JOIN_FAILED, streamPartId, error);
    }
    isProxiedStreamPart(streamPartId, direction) {
        return this.streamPartManager.isBehindProxy(streamPartId) && this.proxyStreamConnectionManager.isProxiedStreamPart(streamPartId, direction);
    }
}
exports.Node = Node;
//# sourceMappingURL=Node.js.map