"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
require('source-map-support').install();
const events_1 = require("events");
const pkg = require("../package.json");
const utils_1 = require("./utils/utils");
const constants_1 = require("./constants");
const message_processor_1 = require("./utils/message-processor");
const message_distributor_1 = require("./utils/message-distributor");
const event_handler_1 = require("./handlers/event/event-handler");
const rpc_handler_1 = require("./handlers/rpc/rpc-handler");
const presence_handler_1 = require("./handlers/presence/presence-handler");
const monitoring_1 = require("./handlers/monitoring/monitoring");
const default_options_1 = require("./default-options");
const configInitializer = require("./config/config-initialiser");
const jsYamlLoader = require("./config/js-yaml-loader");
const dependency_initialiser_1 = require("./utils/dependency-initialiser");
const types_1 = require("@deepstream/types");
const record_handler_1 = require("./handlers/record/record-handler");
const json_path_1 = require("./utils/json-path");
const combine_authentication_1 = require("./services/authentication/combine/combine-authentication");
/**
 * Sets the name of the process
 */
process.title = 'deepstream server';
class Deepstream extends events_1.EventEmitter {
    /**
     * Deepstream is a realtime data server that supports data-sync,
     * publish-subscribe, request-response, listening, permissions
     * and a host of other features!
     */
    constructor(config = null) {
        super();
        this.connectionListeners = new Set();
        this.overrideSettings = [];
        this.startWhenLoaded = false;
        this.stateMachine = {
            init: constants_1.STATES.STOPPED,
            transitions: [
                { name: 'loading-config', from: constants_1.STATES.STOPPED, to: constants_1.STATES.CONFIG_LOADED, handler: this.configLoaded },
                { name: 'start', from: constants_1.STATES.CONFIG_LOADED, to: constants_1.STATES.LOGGER_INIT, handler: this.loggerInit },
                { name: 'logger-started', from: constants_1.STATES.LOGGER_INIT, to: constants_1.STATES.SERVICE_INIT, handler: this.serviceInit },
                { name: 'services-started', from: constants_1.STATES.SERVICE_INIT, to: constants_1.STATES.HANDLER_INIT, handler: this.handlerInit },
                { name: 'handlers-started', from: constants_1.STATES.HANDLER_INIT, to: constants_1.STATES.PLUGIN_INIT, handler: this.pluginsInit },
                { name: 'plugins-started', from: constants_1.STATES.PLUGIN_INIT, to: constants_1.STATES.CONNECTION_ENDPOINT_INIT, handler: this.connectionEndpointInit },
                { name: 'connection-endpoints-started', from: constants_1.STATES.CONNECTION_ENDPOINT_INIT, to: constants_1.STATES.RUNNING, handler: this.run },
                { name: 'stop', from: constants_1.STATES.LOGGER_INIT, to: constants_1.STATES.LOGGER_SHUTDOWN, handler: this.loggerShutdown },
                { name: 'stop', from: constants_1.STATES.SERVICE_INIT, to: constants_1.STATES.SERVICE_SHUTDOWN, handler: this.serviceShutdown },
                { name: 'stop', from: constants_1.STATES.CONNECTION_ENDPOINT_INIT, to: constants_1.STATES.CONNECTION_ENDPOINT_SHUTDOWN, handler: this.connectionEndpointShutdown },
                { name: 'stop', from: constants_1.STATES.PLUGIN_INIT, to: constants_1.STATES.PLUGIN_SHUTDOWN, handler: this.pluginsShutdown },
                { name: 'stop', from: constants_1.STATES.RUNNING, to: constants_1.STATES.CONNECTION_ENDPOINT_SHUTDOWN, handler: this.connectionEndpointShutdown },
                { name: 'connection-endpoints-closed', from: constants_1.STATES.CONNECTION_ENDPOINT_SHUTDOWN, to: constants_1.STATES.PLUGIN_SHUTDOWN, handler: this.pluginsShutdown },
                { name: 'plugins-closed', from: constants_1.STATES.PLUGIN_SHUTDOWN, to: constants_1.STATES.HANDLER_SHUTDOWN, handler: this.handlerShutdown },
                { name: 'handlers-closed', from: constants_1.STATES.HANDLER_SHUTDOWN, to: constants_1.STATES.SERVICE_SHUTDOWN, handler: this.serviceShutdown },
                { name: 'services-closed', from: constants_1.STATES.SERVICE_SHUTDOWN, to: constants_1.STATES.LOGGER_SHUTDOWN, handler: this.loggerShutdown },
                { name: 'logger-closed', from: constants_1.STATES.LOGGER_SHUTDOWN, to: constants_1.STATES.STOPPED, handler: this.stopped },
            ]
        };
        this.currentState = this.stateMachine.init;
        this.loadConfig(config);
        this.messageProcessor = null;
        this.messageDistributor = null;
    }
    /**
     * Set a deepstream option. For a list of all available options
     * please see default-options.
     */
    set(key, value) {
        if (this.currentState === constants_1.STATES.STOPPED) {
            this.overrideSettings.push({ key, value });
            return;
        }
        if (key === 'storageExclusion') {
            throw new Error('storageExclusion has been replace with record.storageExclusionPrefixes instead, which is an array of prefixes');
        }
        if (key === 'auth') {
            throw new Error('auth has been replaced with authentication');
        }
        if (key === 'authentication') {
            this.services.authentication = new combine_authentication_1.CombineAuthentication(value instanceof Array ? value : [value]);
            return;
        }
        if (this.services[key] !== undefined) {
            this.services[key] = value;
        }
        else if (json_path_1.getValue(this.config, key) !== undefined) {
            json_path_1.setValue(this.config, key, value);
        }
        else {
            throw new Error(`Unknown option or service "${key}"`);
        }
        return this;
    }
    /**
     * Returns true if the deepstream server is running, otherwise false
     */
    isRunning() {
        return this.currentState === constants_1.STATES.RUNNING;
    }
    /**
     * Starts up deepstream. The startup process has three steps:
     *
     * - First of all initialize the logger and wait for it (ready event)
     * - Then initialize all other dependencies (cache connector, message connector, storage connector)
     * - Instantiate the messaging pipeline and record-, rpc- and event-handler
     * - Start WS server
     */
    start() {
        if (this.currentState !== constants_1.STATES.CONFIG_LOADED) {
            this.startWhenLoaded = true;
            return;
        }
        this.transition('start');
    }
    /**
     * Stops the server and closes all connections. Will emit a 'stopped' event once done
     */
    stop() {
        if (this.currentState === constants_1.STATES.STOPPED) {
            throw new Error('The server is already stopped.');
        }
        if ([constants_1.STATES.CONNECTION_ENDPOINT_SHUTDOWN, constants_1.STATES.SERVICE_SHUTDOWN, constants_1.STATES.PLUGIN_SHUTDOWN, constants_1.STATES.LOGGER_SHUTDOWN].indexOf(this.currentState) !== -1) {
            this.services.logger.info(types_1.EVENT.INFO, `Server is currently shutting down, currently in state ${constants_1.STATES[this.currentState]}`);
            return;
        }
        this.transition('stop');
    }
    getServices() {
        return this.services;
    }
    getConfig() {
        return this.config;
    }
    /* ======================================================================= *
     * ========================== State Transitions ========================== *
     * ======================================================================= */
    /**
     * Try to perform a state change
     */
    transition(transitionName) {
        let transition;
        for (let i = 0; i < this.stateMachine.transitions.length; i++) {
            transition = this.stateMachine.transitions[i];
            if (transitionName === transition.name && this.currentState === transition.from) {
                // found transition
                this.onTransition(transition);
                this.currentState = transition.to;
                transition.handler.call(this);
                this.emit(types_1.EVENT.DEEPSTREAM_STATE_CHANGED, this.currentState);
                return;
            }
        }
        const details = JSON.stringify({ transition: transitionName, state: this.currentState });
        throw new Error(`Invalid state transition: ${details}`);
    }
    /**
     * Log state transitions for debugging.
     */
    onTransition(transition) {
        const logger = this.services.logger;
        if (logger && constants_1.STATES[transition.to] !== constants_1.STATES.CONFIG_LOADED) {
            logger.debug(types_1.EVENT.INFO, `State transition (${transition.name}): ${constants_1.STATES[transition.from]} -> ${constants_1.STATES[transition.to]}`);
        }
    }
    configLoaded() {
        if (this.startWhenLoaded) {
            this.overrideSettings.forEach((setting) => this.set(setting.key, setting.value));
            this.start();
        }
    }
    /**
     * First stage in the Deepstream initialization sequence. Initialises the logger.
     */
    async loggerInit() {
        const logger = this.services.logger;
        const loggerInitialiser = new dependency_initialiser_1.DependencyInitialiser(this.config, this.services, logger, 'logger');
        await loggerInitialiser.whenReady();
        const infoLogger = (message) => this.services.logger.info(types_1.EVENT.INFO, message);
        infoLogger(`server name: ${this.config.serverName}`);
        infoLogger(`deepstream version: ${pkg.version}`);
        // otherwise (no configFile) deepstream was invoked by API
        if (this.configFile != null) {
            infoLogger(`configuration file loaded from ${this.configFile}`);
        }
        // @ts-ignore
        if (global.deepstreamLibDir) {
            // @ts-ignore
            infoLogger(`library directory set to: ${global.deepstreamLibDir}`);
        }
        this.transition('logger-started');
    }
    /**
     * Invoked once the logger is initialised. Initialises all deepstream services.
    */
    async serviceInit() {
        const readyPromises = Object.keys(this.services).reduce((promises, serviceName) => {
            if (['connectionEndpoints', 'plugins', 'notifyFatalException', 'logger'].includes(serviceName)) {
                return promises;
            }
            const service = this.services[serviceName];
            const initialiser = new dependency_initialiser_1.DependencyInitialiser(this.config, this.services, service, serviceName);
            promises.push(initialiser.whenReady());
            return promises;
        }, []);
        await Promise.all(readyPromises);
        this.messageProcessor = new message_processor_1.default(this.config, this.services);
        this.messageDistributor = new message_distributor_1.default(this.config, this.services);
        this.services.messageDistributor = this.messageDistributor;
        this.transition('services-started');
    }
    /**
     * Invoked once all plugins are initialised. Instantiates the messaging pipeline and
     * the various handlers.
     */
    async handlerInit() {
        if (this.config.enabledFeatures.event) {
            this.eventHandler = new event_handler_1.default(this.config, this.services);
            this.messageDistributor.registerForTopic(constants_1.TOPIC.EVENT, this.eventHandler.handle.bind(this.eventHandler));
        }
        if (this.config.enabledFeatures.rpc) {
            this.rpcHandler = new rpc_handler_1.default(this.config, this.services);
            this.messageDistributor.registerForTopic(constants_1.TOPIC.RPC, this.rpcHandler.handle.bind(this.rpcHandler));
        }
        if (this.config.enabledFeatures.record) {
            this.recordHandler = new record_handler_1.default(this.config, this.services);
            this.messageDistributor.registerForTopic(constants_1.TOPIC.RECORD, this.recordHandler.handle.bind(this.recordHandler));
        }
        if (this.config.enabledFeatures.presence) {
            this.presenceHandler = new presence_handler_1.default(this.config, this.services);
            this.messageDistributor.registerForTopic(constants_1.TOPIC.PRESENCE, this.presenceHandler.handle.bind(this.presenceHandler));
            this.connectionListeners.add(this.presenceHandler);
        }
        if (this.config.enabledFeatures.monitoring) {
            this.monitoringHandler = new monitoring_1.default(this.config, this.services);
            this.messageDistributor.registerForTopic(constants_1.TOPIC.MONITORING, this.monitoringHandler.handle.bind(this.monitoringHandler));
        }
        this.messageProcessor.onAuthenticatedMessage =
            this.messageDistributor.distribute.bind(this.messageDistributor);
        if (this.services.permission.setRecordHandler) {
            this.services.permission.setRecordHandler(this.recordHandler);
        }
        this.transition('handlers-started');
    }
    async pluginsInit() {
        const readyPromises = Object.keys(this.services.plugins).reduce((promises, pluginName) => {
            const plugin = this.services.plugins[pluginName];
            if (isConnectionListener(plugin)) {
                this.connectionListeners.add(plugin);
            }
            const initialiser = new dependency_initialiser_1.DependencyInitialiser(this.config, this.services, plugin, pluginName);
            promises.push(initialiser.whenReady());
            return promises;
        }, []);
        await Promise.all(readyPromises);
        this.transition('plugins-started');
    }
    /**
     * Invoked once all dependencies and services are initialised.
     * The startup sequence will be complete once the connection endpoint is started and listening.
     */
    async connectionEndpointInit() {
        const endpoints = this.services.connectionEndpoints;
        const readyPromises = [];
        for (let i = 0; i < endpoints.length; i++) {
            const connectionEndpoint = endpoints[i];
            const dependencyInitialiser = new dependency_initialiser_1.DependencyInitialiser(this.config, this.services, connectionEndpoint, 'connectionEndpoint');
            connectionEndpoint.onMessages = this.messageProcessor.process.bind(this.messageProcessor);
            if (connectionEndpoint.setConnectionListener) {
                connectionEndpoint.setConnectionListener({
                    onClientConnected: this.onClientConnected.bind(this),
                    onClientDisconnected: this.onClientDisconnected.bind(this)
                });
            }
            readyPromises.push(dependencyInitialiser.whenReady());
        }
        await Promise.all(readyPromises);
        this.transition('connection-endpoints-started');
    }
    /**
     * Initialization complete - Deepstream is up and running.
     */
    run() {
        this.services.logger.info(types_1.EVENT.INFO, 'Deepstream started');
        this.emit('started');
    }
    /**
   * Close any (perhaps partially initialised) plugins.
   */
    async pluginsShutdown() {
        const shutdownPromises = Object.keys(this.services.plugins).reduce((promises, pluginName) => {
            const plugin = this.services.plugins[pluginName];
            if (plugin.close) {
                promises.push(plugin.close());
            }
            return promises;
        }, []);
        await Promise.all(shutdownPromises);
        this.transition('plugins-closed');
    }
    /**
     * Begin deepstream shutdown.
     * Closes the (perhaps partially initialised) connectionEndpoints.
     */
    async connectionEndpointShutdown() {
        const closeCallbacks = this.services.connectionEndpoints.map((endpoint) => endpoint.close());
        await Promise.all(closeCallbacks);
        this.transition('connection-endpoints-closed');
    }
    async handlerShutdown() {
        await this.eventHandler.close();
        await this.rpcHandler.close();
        await this.recordHandler.close();
        await this.presenceHandler.close();
        await this.monitoringHandler.close();
        this.transition('handlers-closed');
    }
    /**
     * Shutdown the services.
     */
    async serviceShutdown() {
        const shutdownPromises = Object.keys(this.services).reduce((promises, serviceName) => {
            const service = this.services[serviceName];
            if (service.close) {
                promises.push(service.close());
            }
            return promises;
        }, []);
        await Promise.all(shutdownPromises);
        this.transition('services-closed');
    }
    /**
     * Close the (perhaps partially initialised) logger.
     */
    async loggerShutdown() {
        const logger = this.services.logger;
        await logger.close();
        this.transition('logger-closed');
    }
    /**
     * Final stop state.
     * Deepstream can now be started again.
     */
    stopped() {
        this.emit('stopped');
    }
    /**
     * Synchronously loads a configuration file
     * Initialization of plugins and logger will be triggered by the
     * configInitialiser, but it should not block. Instead the ready events of
     * those plugins are handled through the DependencyInitialiser in this instance.
     */
    async loadConfig(config) {
        let result;
        if (config === null || typeof config === 'string') {
            result = await jsYamlLoader.loadConfig(this, config);
            this.configFile = result.file;
        }
        else {
            configInitializer.mergeConnectionOptions(config);
            const rawConfig = utils_1.merge(default_options_1.get(), config);
            result = configInitializer.initialize(this, rawConfig);
        }
        this.config = result.config;
        this.services = result.services;
        this.transition('loading-config');
    }
    onClientConnected(socketWrapper) {
        this.connectionListeners.forEach((connectionListener) => connectionListener.onClientConnected(socketWrapper));
    }
    onClientDisconnected(socketWrapper) {
        this.connectionListeners.forEach((connectionListener) => connectionListener.onClientDisconnected(socketWrapper));
    }
}
exports.Deepstream = Deepstream;
function isConnectionListener(object) {
    return 'onClientConnected' in object && 'onClientDisconnected' in object;
}
exports.default = Deepstream;
//# sourceMappingURL=deepstream.io.js.map