"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const constants_1 = require("../../constants");
const types_1 = require("@deepstream/types");
class DefaultSubscriptionRegistry {
    /**
     * A generic mechanism to handle subscriptions from sockets to topics.
     * A bit like an event-hub, only that it registers SocketWrappers rather
     * than functions
     */
    constructor(pluginConfig, services, config, topic, clusterTopic) {
        this.pluginConfig = pluginConfig;
        this.services = services;
        this.config = config;
        this.topic = topic;
        this.sockets = new Map();
        this.subscriptions = new Map();
        this.subscriptionListener = null;
        this.logger = this.services.logger.getNameSpace('SUBSCRIPTION_REGISTRY');
        this.invalidSockets = new Set();
        switch (topic) {
            case constants_1.TOPIC.RECORD:
            case constants_1.STATE_REGISTRY_TOPIC.RECORD_LISTEN_PATTERNS:
                this.actions = constants_1.RECORD_ACTION;
                break;
            case constants_1.TOPIC.EVENT:
            case constants_1.STATE_REGISTRY_TOPIC.EVENT_LISTEN_PATTERNS:
                this.actions = constants_1.EVENT_ACTION;
                break;
            case constants_1.TOPIC.RPC:
                this.actions = constants_1.RPC_ACTION;
                break;
            case constants_1.TOPIC.PRESENCE:
                this.actions = constants_1.PRESENCE_ACTION;
                break;
            case constants_1.TOPIC.MONITORING:
                this.actions = constants_1.MONITORING_ACTION;
                break;
        }
        this.constants = {
            MULTIPLE_SUBSCRIPTIONS: this.actions.MULTIPLE_SUBSCRIPTIONS,
            NOT_SUBSCRIBED: this.actions.NOT_SUBSCRIBED,
            SUBSCRIBE: this.actions.SUBSCRIBE,
            UNSUBSCRIBE: this.actions.UNSUBSCRIBE,
        };
        this.onSocketClose = this.onSocketClose.bind(this);
        this.clusterSubscriptions = this.services.clusterStates.getStateRegistry(clusterTopic);
        if (this.pluginConfig.subscriptionsSanityTimer > 0) {
            setInterval(this.illegalCleanup.bind(this), this.pluginConfig.subscriptionsSanityTimer);
            setInterval(() => this.invalidSockets.clear(), this.pluginConfig.subscriptionsSanityTimer * 100);
        }
    }
    async whenReady() {
        await this.clusterSubscriptions.whenReady();
    }
    async close() {
        await this.clusterSubscriptions.whenReady();
    }
    /**
     * Return all the servers that have this subscription.
     */
    getAllServers(subscriptionName) {
        return this.clusterSubscriptions.getAllServers(subscriptionName);
    }
    /**
     * Return all the servers that have this subscription excluding the current
     * server name
     */
    getAllRemoteServers(subscriptionName) {
        const serverNames = this.clusterSubscriptions.getAllServers(subscriptionName);
        const localServerIndex = serverNames.indexOf(this.config.serverName);
        if (localServerIndex > -1) {
            serverNames.splice(serverNames.indexOf(this.config.serverName), 1);
        }
        return serverNames;
    }
    /**
     * Returns a list of all the topic this registry
     * currently has subscribers for
     */
    getNames() {
        return this.clusterSubscriptions.getAll();
    }
    /**
     * Returns true if the subscription exists somewhere
     * in the cluster
     */
    hasName(subscriptionName) {
        return this.clusterSubscriptions.has(subscriptionName);
    }
    /**
    * This method allows you to customise the SubscriptionRegistry so that it can send
    * custom events and ack messages back.
    * For example, when using the ACTIONS.LISTEN, you would override SUBSCRIBE with
    * ACTIONS.SUBSCRIBE and UNSUBSCRIBE with UNSUBSCRIBE
    */
    setAction(name, value) {
        this.constants[name.toUpperCase()] = value;
    }
    /**
     * Enqueues a message string to be broadcast to all subscribers. Broadcasts will potentially
     * be reordered in relation to *other* subscription names, but never in relation to the same
     * subscription name. Each broadcast is given 'broadcastTimeout' ms to coalesce into one big
     * broadcast.
     */
    sendToSubscribers(name, message, noDelay, senderSocket, suppressRemote = false) {
        // If the senderSocket is null it means it was received via the message bus
        if (senderSocket !== null && suppressRemote === false) {
            this.services.clusterNode.send(message);
        }
        const subscription = this.subscriptions.get(name);
        if (!subscription) {
            return;
        }
        const subscribers = subscription.sockets;
        this.services.monitoring.onBroadcast(message, subscribers.size);
        const serializedMessages = {};
        for (const socket of subscribers) {
            if (socket === senderSocket) {
                continue;
            }
            if (!serializedMessages[socket.socketType]) {
                if (message.parsedData) {
                    delete message.data;
                }
                this.logger.debug('SEND_TO_SUBSCRIBERS', `encoding ${name} with a different protocol ${socket.socketType} with data ${message.parsedData}`);
                serializedMessages[socket.socketType] = socket.getMessage(message);
            }
            this.logger.debug('SEND_TO_SUBSCRIBERS', `sending ${socket.socketType} payload of ${serializedMessages[socket.socketType]}`);
            socket.sendBuiltMessage(serializedMessages[socket.socketType], true);
        }
    }
    /**
     * Adds a SocketWrapper as a subscriber to a topic
     */
    subscribeBulk(message, socket, silent) {
        const length = message.names.length;
        for (let i = 0; i < length; i++) {
            this.subscribe(message.names[i], message, socket, true);
        }
        if (!silent) {
            socket.sendAckMessage({
                topic: message.topic,
                action: message.action,
                correlationId: message.correlationId
            });
        }
    }
    /**
     * Adds a SocketWrapper as a subscriber to a topic
     */
    unsubscribeBulk(message, socket, silent) {
        message.names.forEach((name) => {
            this.unsubscribe(name, message, socket, true);
        });
        if (!silent) {
            socket.sendAckMessage({
                topic: message.topic,
                action: message.action,
                correlationId: message.correlationId
            });
        }
    }
    /**
     * Adds a SocketWrapper as a subscriber to a topic
     */
    subscribe(name, message, socket, silent) {
        const subscription = this.subscriptions.get(name) || {
            name,
            sockets: new Set()
        };
        if (subscription.sockets.size === 0) {
            this.subscriptions.set(name, subscription);
        }
        else if (subscription.sockets.has(socket)) {
            if (this.logger.shouldLog(types_1.LOG_LEVEL.WARN)) {
                const msg = `repeat subscription to "${name}" by ${socket.userId}`;
                this.logger.warn(constants_1.EVENT_ACTION[this.constants.MULTIPLE_SUBSCRIPTIONS], msg, { message, socketWrapper: socket });
            }
            socket.sendMessage({
                topic: this.topic,
                action: this.constants.MULTIPLE_SUBSCRIPTIONS,
                originalAction: message.action,
                name
            });
            return;
        }
        subscription.sockets.add(socket);
        this.addSocket(subscription, socket);
        if (!silent) {
            if (this.logger.shouldLog(types_1.LOG_LEVEL.DEBUG)) {
                const logMsg = `for ${constants_1.TOPIC[this.topic] || constants_1.STATE_REGISTRY_TOPIC[this.topic]}:${name} by ${socket.userId}`;
                this.logger.debug(this.actions[this.constants.SUBSCRIBE], logMsg);
            }
            socket.sendAckMessage(message);
        }
    }
    /**
     * Removes a SocketWrapper from the list of subscriptions for a topic
     */
    unsubscribe(name, message, socket, silent) {
        const subscription = this.subscriptions.get(name);
        if (!subscription || !subscription.sockets.delete(socket)) {
            if (!silent) {
                if (this.logger.shouldLog(types_1.LOG_LEVEL.WARN)) {
                    const msg = `${socket.userId} is not subscribed to ${name}`;
                    this.logger.warn(this.actions[this.constants.NOT_SUBSCRIBED], msg, { socketWrapper: socket, message });
                }
                socket.sendMessage({
                    topic: this.topic,
                    action: this.constants.NOT_SUBSCRIBED,
                    originalAction: message.action,
                    name
                });
            }
            return;
        }
        this.removeSocket(subscription, socket);
        if (!silent) {
            if (this.logger.shouldLog(types_1.LOG_LEVEL.DEBUG)) {
                const logMsg = `for ${this.topic}:${name} by ${socket.userId}`;
                this.logger.debug(this.actions[this.constants.UNSUBSCRIBE], logMsg);
            }
            socket.sendAckMessage(message);
        }
    }
    /**
     * Returns an array of SocketWrappers that are subscribed
     * to <name> or null if there are no subscribers
     */
    getLocalSubscribers(name) {
        const subscription = this.subscriptions.get(name);
        return subscription ? subscription.sockets : new Set();
    }
    /**
     * Returns true if there are SocketWrappers that
     * are subscribed to <name> or false if there
     * aren't any subscribers
     */
    hasLocalSubscribers(name) {
        return this.subscriptions.has(name);
    }
    /**
     * Allows to set a subscriptionListener after the class had been instantiated
     */
    setSubscriptionListener(listener) {
        this.subscriptionListener = listener;
        this.clusterSubscriptions.onAdd(listener.onFirstSubscriptionMade.bind(listener));
        this.clusterSubscriptions.onRemove(listener.onLastSubscriptionRemoved.bind(listener));
    }
    addSocket(subscription, socket) {
        const subscriptions = this.sockets.get(socket) || new Set();
        if (subscriptions.size === 0) {
            this.sockets.set(socket, subscriptions);
            socket.onClose(this.onSocketClose);
        }
        subscriptions.add(subscription);
        this.clusterSubscriptions.add(subscription.name);
        if (this.subscriptionListener) {
            this.subscriptionListener.onSubscriptionMade(subscription.name, socket);
        }
    }
    removeSocket(subscription, socket) {
        if (subscription.sockets.size === 0) {
            this.subscriptions.delete(subscription.name);
        }
        if (this.subscriptionListener) {
            this.subscriptionListener.onSubscriptionRemoved(subscription.name, socket);
        }
        this.clusterSubscriptions.remove(subscription.name);
        const subscriptions = this.sockets.get(socket);
        if (subscriptions) {
            subscriptions.delete(subscription);
            if (subscriptions.size === 0) {
                this.sockets.delete(socket);
                socket.removeOnClose(this.onSocketClose);
            }
        }
        else {
            this.logger.error(types_1.EVENT.ERROR, 'Attempting to delete a subscription that doesn\'t exist');
        }
    }
    /**
    * Called whenever a socket closes to remove all of its subscriptions
    */
    onSocketClose(socket) {
        const subscriptions = this.sockets.get(socket);
        if (!subscriptions) {
            this.logger.error(constants_1.EVENT_ACTION[this.constants.NOT_SUBSCRIBED], 'A socket has an illegal registered close callback', { socketWrapper: socket });
            return;
        }
        for (const subscription of subscriptions) {
            subscription.sockets.delete(socket);
            this.removeSocket(subscription, socket);
        }
        this.sockets.delete(socket);
    }
    illegalCleanup() {
        this.sockets.forEach((subscriptions, socket) => {
            if (socket.isClosed) {
                if (!this.invalidSockets.has(socket)) {
                    this.logger.error(types_1.EVENT.CLOSED_SOCKET, `Socket ${socket.uuid} is closed but still in registry. Currently there are ${this.invalidSockets.size} sockets. If you see this please raise a github issue!`);
                    this.invalidSockets.add(socket);
                }
            }
        });
    }
}
exports.DefaultSubscriptionRegistry = DefaultSubscriptionRegistry;
//# sourceMappingURL=default-subscription-registry.js.map