/*!
 * Copyright 2017 - 2020 by ChartIQ, Inc.
 * All rights reserved.
 */
// This routerTransport module is shared between router clients and the router service.  It supports
// the addition of new transports without any change to the router code. Each transport is
// point-to-point between a router client and the router service (i.e. hub and spoke).  Each router
// client can use a different transport (i.e. the router service connects to them all).
import Logger from "../../../clients/logger";
import { System } from "../../../common/system";
import { Globals } from "../../../common/Globals";
const RouterTransportImplementation = {}; // a convenience namespace for router-transport implementations
/**
 * <h2>Router Transport</h2>
 * **Service-Level Module**.  Manages and contains the point-to-point transports (i.e., Layer 2) supported by Finsemble.
 * Each transport communicates between a Finsemble services or component (i.e. a router client on one end) and the Finsemble router service (another router client on the other end).
 *
 * Integration into routerService.js is automatic on startup.
 *
 * Developer Notes on Adding New Transport:
 * 1) Create new transport constructor.
 * 2) Call RouterTransport.addTransport() to make the transport constructor (see the bottom of this file)
 *
 * Each transport constructor must be implemented with the following interface:
 *
 *	ExampleTransportConstructor(params, parentMessageHandler, source, destination) where
 *
 * 			params is a passed in object including data that may (or may not) be needed for implementing the transport
 *					params.transportSettings: transport settings from finsemble.router.transportSettings if defined, otherwise an empty object
 *
 * 			parentMessageHandler(incomingTransportInfo, routerMessage) where
 * 					incomingTransportInfo is a transport-specific object containing essential information to route back to the same client.
 * 						The same object will be returned on a send() so the transport can use to send the message to that client.
 * 						It's up to the developer to decide what to put in the incomingTransportInfo object. The RouterService never
 * 						directly uses the object, except to do a property-based comparison for equality (so equality must be based on the top-level properties within the object.)
 * 					routerMessage is an object containing a single router message. The transport generally does not need to know the contents --
 * 						it only sends and receives these messages. However, the router's header (routerMessage.header) is available to the transport if needed.
 *
 * 			source is either the source's client name or "RouterService" (when the RouterService is the source)
 *
 * 			destination is either the destination's client name or "RouterService" (when the RouterService is the designation)
 *
 * 			callback(this) returns the constructor.  Normally a constructor is not asynchronous, but support in case the constructed transport requires async initialization.
 *
 * The transport constructor must implement two functions.
 * 		1) send(transport, routerMessage) -- transport object contains destination transport info; routerMessage is the message to send
 * 		2) identifier() -- returns transport's name
 *
 * These functions along with the parentMessageHandler callback all that's needed to interface with the higher-level router (either a client or router service):
 *
 * The three transports implemented at the bottom of this file can serve as examples.
 *
 * @namespace RouterTransport
 */
const RouterTransport = {
    activeTransports: {},
    liveTransport: null,
    setTransports() {
        // add the transports to the available/active list
        RouterTransport.addTransport("SharedWorkerForCoreServices", RouterTransportImplementation.SharedWorkerForCoreServicesTransport);
        RouterTransport.addTransport("SharedWorker", RouterTransportImplementation.SharedWorkerTransport);
        RouterTransport.addTransport("IPCBus", RouterTransportImplementation.IPCTransport);
        RouterTransport.addTransport("FinsembleTransport", RouterTransportImplementation.FinsembleTransport);
    },
    /**
     * Adds a new type of router transport to pass message between RouterClient and RouterService.
     *
     * @param {string} transportName identifies the new transport
     * @param {object} transportConstructor returns an instance of the new transport
     */
    addTransport(transportName, transportConstructor) {
        this.activeTransports[transportName] = transportConstructor;
    },
    /**
     * Gets array of active transports.  What is active depends both on config and what is supported by the environment. Typically, if OF IAB is defined then the IAB transport is added to active list.  Likewise, if SharedWorker defined, then SharedWork transport added to the active list.  Special transports that don't have backwards compatibility (e.g. FinsembleTransport) are only added if specified in the config.
     *
     * @param {string} params transport parameters
     *
     * @returns array of active transport names
     */
    getActiveTransports(params) {
        const transportNames = [];
        // convenience function to add transport to active list only if it's not already in the list
        const addToActive = (transportName) => {
            if (transportNames.indexOf(transportName) === -1) {
                // if not already in the list, then add it
                transportNames.push(transportName);
            }
        };
        // if IPCBus available, then add IAB to active list
        if (System.InterApplicationBus)
            addToActive("IPCBus");
        // Always have FinsembleTransport active
        addToActive("FinsembleTransport");
        // if shared worker available, then add shared-worker transport to active list
        if (SharedWorker) {
            addToActive("SharedWorker");
            addToActive("SharedWorkerForCoreServices");
        }
        // add whatever the sameDomainTransport is to the active list
        addToActive(params.sameDomainTransport);
        // add whatever the crossDomainTransport is to the active list
        addToActive(params.crossDomainTransport);
        Logger.system.log("getActiveTransports", transportNames);
        return transportNames;
    },
    /**
     * Get a specific transport by name. The transport must be in list of the active transports (i.e. previously added).
     *
     * @param {object} params parameters for transport
     * @param {any} transportName
     * @param {any} incomingMessageHandler
     * @param {any} source
     * @param {any} destination
     * @param {function} failureHandler
     * @returns the transport object
     */
    getTransport(params, transportName, incomingMessageHandler, source, destination, failureHandler) {
        return new Promise((resolve, reject) => {
            const TransportConstructor = RouterTransport.activeTransports[transportName];
            if (TransportConstructor) {
                RouterTransport.liveTransport = new TransportConstructor(params, incomingMessageHandler, source, destination, (newTransport) => {
                    resolve(newTransport);
                }, failureHandler);
            }
            else {
                const warning = `routerTransport.getTransport() Unknown router transport name: ${transportName}`;
                Logger.warn(warning);
                reject(warning);
            }
        });
    },
    close() {
        if (RouterTransport.liveTransport) {
            RouterTransport.liveTransport.close();
            RouterTransport.liveTransport = null;
        }
    },
};
// ////////////////////////////////////////////////////////////
// Below all transports are defined then added to active list
// ////////////////////////////////////////////////////////////
/*
 * Implements the SharedWorker Transport for use by Finsemble Core Services.
 *
 * Required Functions (used by transport clients):
 * 		send(routerMessage) -- transports the event
 * 		identifier() -- returns transport name/identifier
 *
 * @param {object} params various params to support transports
 * @param {any} parentMessageHandler callback for incoming event
 * @param {any} source either the client name or "RouterService"
 * @param {any} destination either the client name or "RouterService" (unused in SharedWorker)
 */
RouterTransportImplementation.SharedWorkerForCoreServicesTransport = function (params, parentMessageHandler, source, destination, callback) {
    var _a;
    let routerThread;
    // receives incoming shared-worker messages then passes on to parent with correct "wrapper"
    const sharedWorkerMessageHandler = (swMessage) => {
        const port = swMessage.data[0];
        const routerMessage = swMessage.data[1];
        const incomingTransportInfo = { transportID: this.identifier(), port };
        Logger.system.verbose("SharedWorkerForCoreServicesTransport Incoming Transport", incomingTransportInfo, "Message", routerMessage);
        parentMessageHandler(incomingTransportInfo, routerMessage);
    };
    /**
     * Required function for parent (i.e. routeClient or routeService)
     */
    this.send = function (transport, routerMessage) {
        // handle optional transport parm
        if (arguments.length === 1) {
            // clients use just one parm -- routerMessage
            routerMessage = arguments[0];
            transport = null;
        }
        else {
            // router services uses both parameters
            transport = arguments[0];
            routerMessage = arguments[1];
        }
        Logger.system.verbose("SharedWorkerForCoreServicesTransport Outgoing Transport", routerMessage);
        try {
            routerThread.port.postMessage([transport, routerMessage]);
        }
        catch (e) {
            Logger.system.error(`SharedWorkerForCoreServicesTransport: post message failed: ${JSON.stringify(e)}`, "Probable cause is sending illegal data type (e.g. function).");
        }
    };
    // required function for parent (i.e. routeClient or routeService)
    this.identifier = () => "SharedWorkerForCoreServices";
    const workerPath = params.transportSettings.SharedWorker && params.transportSettings.SharedWorker.workerPath
        ? params.transportSettings.SharedWorker.workerPath
        : `${params.routerDomainRoot}/routerSharedWorkerForCoreServices.js`;
    Logger.console.log(`SharedWorkerForCoreServices Transport Initializing for ${source} using ${workerPath}`);
    routerThread = new SharedWorker(workerPath, {
        name: "Finsemble",
        credentials: "include",
    });
    routerThread.port.onmessage = sharedWorkerMessageHandler;
    routerThread.onerror = (e) => {
        Logger.console.error(`SharedWorkerForCoreServicesTransport Transport Error ${JSON.stringify(e)}`);
    };
    routerThread.port.start();
    if (source === "RouterService") {
        // send first message though shared worker to identify router service
        routerThread.port.postMessage({ data: "connect", source: "RouterService" });
    }
    this.close = () => routerThread.port.close();
    (_a = Globals.fin) === null || _a === void 0 ? void 0 : _a.log("preload: shared worked core services established callback");
    callback === null || callback === void 0 ? void 0 : callback(this);
};
/*
 * Implements the SharedWorker Transport.
 *
 * Required Functions (used by transport clients):
 * 		send(routerMessage) -- transports the event
 * 		identifier() -- returns transport name/identifier
 *
 * @param {object} params various params to support transports
 * @param {any} parentMessageHandler callback for incoming event
 * @param {any} source either the client name or "RouterService"
 * @param {any} destination either the client name or "RouterService" (unused in SharedWorker)
 */
RouterTransportImplementation.SharedWorkerTransport = function (params, parentMessageHandler, source, destination, callback) {
    var _a;
    let routerThread;
    // receives incoming shared-worker messages then passes on to parent with correct "wrapper"
    const sharedWorkerMessageHandler = (swMessage) => {
        const port = swMessage.data[0];
        const routerMessage = swMessage.data[1];
        const incomingTransportInfo = { transportID: this.identifier(), port };
        Logger.system.verbose("SharedWorkerTransport Incoming Transport", incomingTransportInfo, "Message", routerMessage);
        parentMessageHandler(incomingTransportInfo, routerMessage);
    };
    /**
     * Required function for parent (i.e. routeClient or routeService)
     */
    this.send = function (transport, routerMessage) {
        // handle optional transport parm
        if (arguments.length === 1) {
            // clients use just one parm -- routerMessage
            routerMessage = arguments[0];
            transport = null;
        }
        else {
            // router services uses both parameters
            transport = arguments[0];
            routerMessage = arguments[1];
        }
        Logger.system.verbose("SharedWorkerTransport Outgoing Transport", routerMessage);
        try {
            routerThread.port.postMessage([transport, routerMessage]);
        }
        catch (e) {
            Logger.system.error(`SharedWorkerTransport: post message failed: ${JSON.stringify(e)}`, "Probable cause is sending illegal data type (e.g. function).");
        }
    };
    // required function for parent (i.e. routeClient or routeService)
    this.identifier = () => "SharedWorker";
    const workerPath = params.transportSettings.SharedWorker && params.transportSettings.SharedWorker.workerPath
        ? params.transportSettings.SharedWorker.workerPath
        : `${params.routerDomainRoot}/routerSharedWorker.js`;
    Logger.console.log(`SharedWorker Transport Initializing for ${source} using ${workerPath}`);
    routerThread = new SharedWorker(workerPath, {
        name: "Finsemble",
        credentials: "include",
    });
    routerThread.port.onmessage = sharedWorkerMessageHandler;
    routerThread.onerror = (e) => {
        Logger.console.error(`SharedWorkerTransport Transport Error ${JSON.stringify(e)}`);
    };
    routerThread.port.start();
    if (source === "RouterService") {
        // send first message though shared worker to identify router service
        routerThread.port.postMessage({ data: "connect", source: "RouterService" });
    }
    this.close = () => routerThread.port.close();
    (_a = Globals.fin) === null || _a === void 0 ? void 0 : _a.log("preload: shared worked established callback");
    callback === null || callback === void 0 ? void 0 : callback(this);
};
/*
 * Implements the IPC Bus Transport.
 *
 * Required Functions (used by transport clients):
 * 		send(transport, routerMessage) -- transport object contains destination transport info; routerMessage is the message to send
 * 		identifier() -- returns transport's name
 *
 * @param {object} params unused in IPC transport
 * @param {any} parentMessageHandler callback for incoming event
 * @param {any} source either the client name or "RouterService"
 * @param {any} destination either the client name or "RouterService"
 */
RouterTransportImplementation.IPCTransport = function (params, parentMessageHandler, source, destination, callback) {
    var _a;
    // receives incoming bus messages then passes on to parent with correct "wrapper"
    const messageHandler = (routerMessage) => {
        const incomingTransportInfo = {
            transportID: this.identifier(),
            name: routerMessage.header.origin,
        };
        Logger.system.verbose("IPCTransport Incoming Transport", incomingTransportInfo, "Message", routerMessage);
        parentMessageHandler(incomingTransportInfo, routerMessage);
    };
    const subscribeFailure = (reason) => Logger.console.error(`IPCBus Subscribe Failure: ${reason}`);
    // required function for the parent (i.e. routeClient or routeService)
    this.send = function send(transport, routerMessage) {
        let destTopic;
        // handle optional transport parm
        if (arguments.length === 1) {
            // client use just one parameter - routerMessage
            destTopic = destination;
            routerMessage = arguments[0];
        }
        else {
            // router service uses both parameters
            destTopic = transport.name;
            routerMessage = arguments[1];
        }
        Logger.system.verbose("IPCTransport Outgoing Transport", destTopic, "Message", routerMessage);
        System.InterApplicationBus.publish(destTopic, routerMessage, () => { }, () => { });
    };
    // required function for the parent (i.e. routeClient or routeService)
    this.identifier = () => "IPCBus";
    Logger.console.log(`IPCBus Transport Initializing for ${source}`);
    /**
     * Listen for IAB messages for:
     * "*" - From any sender
     * source - The "topic" is the name of *this* window. e.g. Any messages for topic "Toolbar" will be received.
     */
    const subscriptionID = System.InterApplicationBus.subscribe("*", source, messageHandler, () => { }, subscribeFailure);
    this.close = () => System.InterApplicationBus.unsubscribe(subscriptionID);
    (_a = Globals.fin) === null || _a === void 0 ? void 0 : _a.log("preload: IPC established callback");
    callback === null || callback === void 0 ? void 0 : callback(this);
};
/*
 * Implements the FinsembleTransport (alternative to IAB without iFrame problems with supporting server commonly running on local server).
 *
 * Required Functions (used by transport clients):
 * 		send(event) -- transports the event
 * 		identifier() -- returns transport name/identifier
 *
 * @param {object} params various params to support transports
 * @param {any} parentMessageHandler callback for incoming event
 * @param {any} parentFailureHandler optional callback for transport failure
 * @param {any} source either the client name or "RouterService"
 * @param {any} destination either the client name or "RouterService" (unused in FinsembleTransport)
 * @param {function} callback for when transport is ready
 * @param {function} parentFailureHandler optional callback if transport fails
 */
RouterTransportImplementation.FinsembleTransport = function (params, parentMessageHandler, source, destination, callback, parentFailureHandler) {
    var _a, _b, _c, _d, _e;
    // The router should always pass transportSettings in! Until we're type safe though we'll check dynamically.
    // First case is the real way it should be handled
    // Second case is if the serverAddress is passed in without nesting it in {FinsembleTransport : {}}
    // Third case is the default if no transportSettings are passed in
    const serverAddress = (_e = (_c = (_b = (_a = params === null || params === void 0 ? void 0 : params.transportSettings) === null || _a === void 0 ? void 0 : _a["FinsembleTransport"]) === null || _b === void 0 ? void 0 : _b.serverAddress) !== null && _c !== void 0 ? _c : (_d = params === null || params === void 0 ? void 0 : params.transportSettings) === null || _d === void 0 ? void 0 : _d.serverAddress) !== null && _e !== void 0 ? _e : "ws://127.0.0.1:3376";
    const SOCKET_SERVER_ADDRESS = `${serverAddress}/router`; //  /router namespace is no longer necessary, when removed, also remove from MockRouterService.ts
    let socketClient;
    const OpenIntervalTimeout = 10000;
    let setupSocketInterval;
    let handshakeComplete = true;
    const keepAliveHandshakeComplete = () => {
        Logger.system.verbose("FinsembleTransport keepalive completed");
        handshakeComplete = true;
    };
    const keepAliveStart = () => {
        const KeepaliveDest = "IAC_KEEPALIVE_SERVER";
        const KeepAliveInterval = 5000;
        const intervalTimer = setInterval(() => {
            // The previous handshake has to be complete before the time to send the next handshake request; otherwise failure.
            if (!handshakeComplete) {
                Logger.console.error("FinsembleTransport keepalive handshake failed -- transport is essentially dead", KeepAliveInterval);
                clearInterval(intervalTimer);
                if (parentFailureHandler)
                    parentFailureHandler();
            }
            else {
                // reset handshakeComplete since starting new handshake
                handshakeComplete = false;
                socketClient.send(JSON.stringify({ dest: KeepaliveDest, source }));
                Logger.system.verbose("FinsembleTransport keepalive sent");
            }
        }, KeepAliveInterval);
    };
    // Receives incoming messages then passes on to parent (what's passed to parent should be same routerMessage received in send()
    const finsembleMessageHandler = (routerMessage) => {
        if (routerMessage === "IAC_KeepaliveHandshake") {
            Logger.system.verbose("FinsembleTransport Incoming Message", "Message", routerMessage);
            keepAliveHandshakeComplete();
        }
        else {
            const incomingTransportInfo = {
                transportID: this.identifier(),
                client: routerMessage.clientMessage.header.origin,
            };
            Logger.system.verbose("FinsembleTransport Incoming Transport", incomingTransportInfo, "Message", routerMessage);
            parentMessageHandler(incomingTransportInfo, routerMessage.clientMessage);
        }
    };
    /**
     * Send start message to the web socket server to initialize client connection
     */
    const sendImplicitConnectMessageToIACServer = () => {
        const dest = "IAC_SERVER";
        socketClient.send(JSON.stringify({ dest, source }));
    };
    const setupSocket = () => {
        Logger.console.log(`FinsembleTransport Transport Initializing for ${source} using ${SOCKET_SERVER_ADDRESS} trying to connect to server`);
        if (SOCKET_SERVER_ADDRESS.startsWith("ws:") || SOCKET_SERVER_ADDRESS.startsWith("wss:")) {
            socketClient = new Globals.WebSocket(SOCKET_SERVER_ADDRESS);
        }
        else {
            Logger.system.error(`Illegal WebSocket address "${SOCKET_SERVER_ADDRESS}" (Missing ws:// or wss:// ?)`);
            socketClient = new Globals.WebSocket(SOCKET_SERVER_ADDRESS);
        }
        socketClient.addEventListener("open", () => {
            var _a;
            clearInterval(setupSocketInterval);
            Logger.console.log("FinsembleTransport connected to server");
            // Send initial start message addressed to the IAC Server so the transport client can be bound to the socket.
            // Note this is optional for most clients (i.e. any router client that sends a message before one is received),
            // but it's required for the RouterService client.
            sendImplicitConnectMessageToIACServer();
            // The Router Service doesn't need to maintain keepalive since it runs within Finsemble (plus it would not work anyway because of keepalive routing)
            if (source !== "RouterService") {
                // Start keepalive timer that will identify if transport goes down (which happens if Finsemble goes down or restarts)
                keepAliveStart();
                Logger.console.log("FinsembleTransport keep-alive handshake started");
            }
            // Set up listener to handle each incoming message
            socketClient.addEventListener("message", (event) => {
                const data = JSON.parse(event.data);
                finsembleMessageHandler(data.message);
            });
            (_a = Globals.fin) === null || _a === void 0 ? void 0 : _a.log("preload: websocket established callback");
            callback === null || callback === void 0 ? void 0 : callback(this);
        });
        /**
         * For debugging, print an error if we cannot connect to the websocket server. Unfortunately, browsers
         * do not provide the reason why we cannot connect.
         */
        socketClient.addEventListener("error", (ev) => {
            Logger.system.error("FinsembleTransport websocket error", ev);
            console.log(`Error connecting to websocket server ${SOCKET_SERVER_ADDRESS}`);
        });
        socketClient.addEventListener("close", (ev) => {
            Logger.system.log("FinsembleTransport websocket close", ev);
            console.log(`Websocket is closed with code ${ev.code}`);
            // Socket is closed with code 1006 when the machine goes to the sleep mode.
            if (ev.code === 1006) {
                // sometimes the close event could happen before the open where the main clearInterval is called
                clearInterval(setupSocketInterval);
                // reconnection
                setupSocketInterval = setInterval(setupSocket, OpenIntervalTimeout);
            }
        });
    };
    // required function for the parent (i.e. routeClient or routeService)
    this.send = function send(transport, routerMessage) {
        let dest;
        let message;
        // decide how to route the message based on whether client or routerService is sending
        if (arguments.length === 1) {
            // clients use just one parameter, so send client message to RouterService
            dest = "ROUTER_SERVICE";
            routerMessage = arguments[0];
            message = { clientMessage: routerMessage }; // no client property needed to route on server since always going to router service
        }
        else {
            // router service uses both parameters, so send router-service message to a client
            dest = "ROUTER_CLIENT";
            routerMessage = arguments[1];
            message = { client: transport.client, clientMessage: routerMessage }; // client property used to router on server
        }
        Logger.system.verbose("FinsembleTransport Outgoing Transport", dest, "NewMessage", message);
        socketClient.send(JSON.stringify({ dest, message }));
    };
    // required function for the parent (i.e. routeClient or routeService)
    this.identifier = () => "FinsembleTransport";
    this.close = () => {
        socketClient.close();
    };
    setupSocketInterval = setInterval(setupSocket, OpenIntervalTimeout);
    setupSocket();
};
export default RouterTransport;
//# sourceMappingURL=routerTransport.js.map