/*!
 * Copyright 2017 - 2020 by ChartIQ, Inc.
 * All rights reserved.
 */
/**
 * For communications amongst desktop services, components, or outside the container entirely, Finsemble provides an event infrastructure with high level protocols.
 * The Router is the center of this functionality, sending and receiving messages between windows.
 *
 *
 * See the [Router tutorial](/docs/how-finsemble-works/TheRouter) for an overview.
 * @module RouterClient
 */
import RouterTransport from "../platform/services/router/routerTransport";
import { toast } from "../common/toast";
import { LoggerClient as Logger } from "./logger"; // should never be used, but message sent before router ready will be queue
import { Globals } from "../common/Globals";
import { Performance } from "../common/Performance";
import { singleton } from "../common/Singleton";
import uuidv4 from "uuid-random";
/**
 * Duplicate of Utils.clone(). Do not import Utils into this file because it drags along Monitors.ts which
 * is incompatible with freestanding apps.
 */
function clone(from, to = {}) {
    if (from === null || typeof from !== "object") {
        return from;
    }
    if (from.constructor == Date ||
        from.constructor == RegExp ||
        from.constructor == Function ||
        from.constructor == String ||
        from.constructor == Number ||
        from.constructor == Boolean) {
        return new from.constructor(from);
    }
    to = to || new from.constructor();
    for (const n in from) {
        to[n] = typeof to[n] === "undefined" ? clone(from[n], null) : to[n];
    }
    return to;
}
/**
 * @ignore
 */
export class RouterConnection {
    constructor() {
        this.queue = [];
        this.started = false;
        this.clientName = "RouterClient.clientName not yet set";
        this.transport = null;
        this.isRouterReady = false;
        this.isTransportFailed = false;
        this.parentReadyCallbackQueue = []; // must be queue because may be multiple waiters
        this.parentFailureCallbackQueue = []; // must be queue because may be multiple waiters
        // Maps all listeners ( registered with calls to subscribe() ) for the given topics
        this.mapListeners = {};
        // Maps query channels to the registered responder (from a call to addResponder() )
        this.mapQueryResponders = {};
        this.mapPubSubResponders = {};
        // Contains the current state for all responder channels
        this.mapPubSubResponderState = {};
        this.mapPubSubResponderRegEx = {};
        // For a given pubsub topic, which subscriber ID's are listening
        this.pubsubListOfSubscribers = {};
        // Maps subscriber ID's to their callbacks
        this.mapSubscribersID = {};
        // Maps subscriber ID's to the topic they're subscribed on
        this.mapSubscribersTopic = {};
        this.mapQueryResponses = {};
        // Map of running queries to their timeouts
        this.mapQueryResponseTimeOut = {};
    }
    // ////////////////////
    // Private Functions
    // ////////////////////
    // See IRouterClient.ts
    onReadyCallBack() {
        Logger.system.debug("WINDOW LIFECYCLE:STARTUP:RouterClient Ready");
        this.isRouterReady = true;
        // invoke all the parent callbacks waiting for router to be ready
        while (this.parentReadyCallbackQueue.length > 0) {
            Logger.system.debug("WINDOW LIFECYCLE:STARTUP:RouterClient parentReady invoked");
            const nextParentCallback = this.parentReadyCallbackQueue.shift();
            nextParentCallback === null || nextParentCallback === void 0 ? void 0 : nextParentCallback();
        }
    }
    // *** all outbound messages exit here though the appropriate transport ***
    sendToRouterService(message) {
        if (!this.transport) {
            Logger.system.info("RouterClient: Queuing message since router initialization not complete", message);
            this.queue.push(message);
        }
        else {
            this.transport.send(message);
        }
    }
    // returns true if this routerClient originated the message
    originatedHere(message) {
        return message.header.origin === this.clientName;
    }
    // invoke client callbacks in the input array (that are attached to a specific channel and listener type)
    invokeListeners(message) {
        const { channel, origin } = message.header;
        const listeners = this.mapListeners[channel] || [];
        if (!Array.isArray(listeners) || listeners.length === 0) {
            Logger.system.log(`RouterClient: no listener for incoming transmit on channel ${channel} from ${origin}. Possibly the listener was removed while this message was in transit.`, message);
        }
        else {
            message.originatedHere = () => this.originatedHere(message);
            listeners.forEach((listener) => {
                if (!Logger.channelFilter(channel)) {
                    Logger.system.info("RouterClient: incoming transmit", "CHANNEL", channel, "FROM", origin, "MESSAGE", message);
                }
                listener(null, message); // invoke the callback; the error parameter is always null for this case
            });
        }
    }
    /**
     * When an incoming query request is received from a client (via the RouterService) we look
     * up the registered responder and pass it the incoming query along with
     * a sendQueryResponse() method that will return the data back to the client.
     */
    invokeResponder(queryMessage) {
        const { channel, origin, queryID, error } = queryMessage.header;
        Logger.system.info("RouterClient: incoming query", "CHANNEL", channel, "FROM", origin, "QUERY MESSAGE", queryMessage);
        /**
         * Set up a closure that will allow our responder to send back a QueryResponse
         */
        const sendQueryResponse = (err, responseData) => {
            Logger.system.info("RouterClient: outgoing query response", "CHANNEL", channel, "RESPONSE DATA", responseData, "QUERY ID", queryID);
            let message = {
                header: {
                    origin: this.clientName,
                    queryID: queryID,
                    type: "queryResponse",
                },
                data: responseData,
            };
            // Native apps expect a non-error condition to always result in transmission of null (not missing or undefined).
            // For actual errors, toString() converts from Error or string type
            message.header.error = err ? err.toString() : null;
            this.sendToRouterService(message);
        };
        const responderCallback = this.mapQueryResponders[channel];
        if (responderCallback === undefined) {
            if (queryMessage.header.logWhenNoResponder) {
                Logger.system.warn(`RouterClient: No query responder defined on channel ${channel} for incoming from ${origin}.`, queryMessage);
            }
        }
        else if (!error) {
            // Add the sendQueryResponse() and originatedHere() methods
            const responderObject = {
                ...queryMessage,
                originatedHere: () => this.originatedHere(queryMessage),
                sendQueryResponse: (err, responseData) => {
                    sendQueryResponse(err, responseData);
                },
            };
            responderCallback(null, responderObject); // invoke the callback (no error)
        }
        else {
            // invoke the callback with error since  flag in message (from router service)
            Logger.system.warn("RouterClient: queryResponder error", queryMessage);
            responderCallback(error);
            delete this.mapQueryResponders[channel]; // this is a bad responder (e.g. duplicate) so remove it
        }
    }
    /**
     * Received a query response back from the router service. Look up the callback
     * and invoke it with the received data.
     */
    invokeQueryResponse(message) {
        var _a;
        Logger.system.info("RouterClient: incoming query response", "CHANNEL", message.header.channel, "QUERY ID", message.header.queryID, "message", message);
        const { queryID } = message.header;
        // delete any existing timer waiting on the response
        const theTimer = this.mapQueryResponseTimeOut[queryID];
        delete this.mapQueryResponseTimeOut[queryID];
        if (theTimer)
            clearTimeout(theTimer);
        const clientCallback = this.mapQueryResponses[queryID];
        if (clientCallback) {
            delete this.mapQueryResponses[queryID];
            const error = (_a = message.header.error) !== null && _a !== void 0 ? _a : null;
            clientCallback(error, message);
        }
        else {
            Logger.system.warn("Invariant: RouterClient: no handler found for incoming query response.", message);
        }
    }
    /**
     * Updates pub/sub subscriptions whenever state updates (or on initial subscribe to the topic).
     * This looks up local subscribers and calls them with the pub/sub state.
     */
    invokeNotifyCallback(notifyMessage) {
        const { subscribeID, error } = notifyMessage.header;
        const notifyCallback = this.mapSubscribersID[subscribeID];
        if (!notifyCallback) {
            Logger.system.info("RouterClient: no subscription handler defined for incoming notify for subscriberID. Possibly the subscription was removed while this publish was in transit.", subscribeID, notifyMessage);
        }
        else if (!error) {
            notifyMessage.originatedHere = () => this.originatedHere(notifyMessage);
            Logger.system.info("RouterClient: incoming PubSub notify", "SUBSCRIBER ID", subscribeID, "NOTIFY MESSAGE", notifyMessage);
            notifyCallback(null, notifyMessage);
        }
        else {
            Logger.system.info("RouterClient: incoming PubSub notify error for subscriberID", "SUBSCRIBER ID", subscribeID, "NOTIFY MESSAGE", notifyMessage);
            notifyCallback(error, notifyMessage);
        }
    }
    /**
     * Returns a responder given a topic. The topic could be a regex key, in
     * which case mapPubSubResponderRegEx is searched
     */
    findResponder(topic) {
        const responder = this.mapPubSubResponders[topic];
        // if responder is undefined then look for a matching RegEx topic
        if (!responder) {
            for (const key in this.mapPubSubResponderRegEx) {
                if (this.mapPubSubResponderRegEx[key].test(topic)) {
                    return this.mapPubSubResponders[key];
                }
            }
        }
        else {
            return responder;
        }
    }
    // for incoming Publish: invoke publish callback for pubsub server
    invokePublishEvent(publishMessage) {
        const { topic } = publishMessage.header;
        let responder = this.findResponder(topic);
        // if responder is *still* undefined
        if (!responder) {
            Logger.system.warn("RouterClient: no pubsub responder defined for incoming publish", "TOPIC", topic, "PUBLISH MESSAGE", publishMessage);
        }
        else {
            // callback function for invokePublishPubSubCallback to send Notify
            const sendNotifyToAllSubscribers = (request, err, notifyData) => {
                var _a;
                if (!err) {
                    this.mapPubSubResponderState[topic] = notifyData; // store new state
                    const listOfSubscribers = (_a = this.pubsubListOfSubscribers[topic]) !== null && _a !== void 0 ? _a : [];
                    listOfSubscribers.forEach((subscribeID) => {
                        Logger.system.info("RouterClient: sending pubsub notify", "TOPIC", topic, "NOTIFY DATA", notifyData);
                        const message = {
                            header: {
                                origin: this.clientName,
                                type: "notify",
                                topic: topic,
                                subscribeID,
                            },
                            data: notifyData,
                        };
                        this.sendToRouterService(message);
                    });
                }
                else {
                    Logger.system.warn("RouterClient: incoming publish rejected by pubsub responder", err, notifyData);
                }
            };
            Logger.system.info(`RouterClient: incoming PubSub publish ${responder.publishCallback ? "(publishCallback hook invoked)" : ""}`, "TOPIC", topic, "PUBLISH MESSAGE", publishMessage);
            // If the responder has a hook for publish events
            if (responder.publishCallback) {
                const hookMessage = {
                    ...publishMessage,
                    sendNotifyToAllSubscribers: (err, data) => {
                        sendNotifyToAllSubscribers(publishMessage, err, data);
                    },
                };
                responder.publishCallback(null, hookMessage);
            }
            else {
                // since no publish callback defined, use default functionality
                sendNotifyToAllSubscribers(publishMessage, null, publishMessage.data);
            }
        }
    }
    // for incoming subscribe: invoke notify callback for pubsub responder
    invokeSubscribeEvent(subscribeMessage) {
        var _a;
        const { topic, error } = subscribeMessage.header;
        Logger.system.info("RouterClient: PubSub subscribe", "TOPIC", topic);
        let responder = this.mapPubSubResponders[topic];
        if (!responder) {
            // if responder is undefined then look for a matching RegEx topic
            for (const regExKey in this.mapPubSubResponderRegEx) {
                if (this.mapPubSubResponderRegEx[regExKey].test(topic)) {
                    responder = this.mapPubSubResponders[regExKey];
                    // If no initial state defined then take the default from the regEx
                    this.mapPubSubResponderState[topic] =
                        (_a = this.mapPubSubResponderState[topic]) !== null && _a !== void 0 ? _a : this.mapPubSubResponderState[regExKey];
                    break;
                }
            }
        }
        if (!responder) {
            // If responder is *still* undefined then warning
            Logger.system.warn("RouterClient: no pubsub responder defined for incoming subscribe event", subscribeMessage);
        }
        else if (error) {
            Logger.system.warn(`RouterClient: pubsub error received from router service: ${error}`, subscribeMessage);
        }
        else {
            if (responder.subscribeCallback) {
                const hookMessage = {
                    ...subscribeMessage,
                    sendNotifyToSubscriber: (err, data) => {
                        this.sendNotifyToSubscriber(subscribeMessage, err, data);
                    },
                };
                subscribeMessage.data = this.mapPubSubResponderState[topic];
                responder.subscribeCallback(null, hookMessage); // invoke the callback (no error)
            }
            else {
                // since no subscribe callback defined, use default functionality
                this.sendNotifyToSubscriber(subscribeMessage, null, this.mapPubSubResponderState[topic]);
            }
        }
    }
    // for incoming unsubscribe: invoke unsubscribe callback for pubsub server
    invokeUnsubscribeEvent(unsubscribeMessage) {
        const { topic, subscribeID } = unsubscribeMessage.header;
        let responder = this.findResponder(topic);
        if (!responder) {
            // if responder is undefined then look for a matching RegEx topic
            for (const key in this.mapPubSubResponderRegEx) {
                if (this.mapPubSubResponderRegEx[key].test(topic)) {
                    responder = this.mapPubSubResponders[key];
                    break;
                }
            }
        }
        if (!responder) {
            // if responder is *still* undefined
            Logger.system.warn("RouterClient: no pubsub responder defined for incoming unsubscribe event", "TOPIC", topic, "UNSUBSCRIBE MESSAGE", unsubscribeMessage);
        }
        else {
            Logger.system.info(`RouterClient: incoming unsubscribe ${responder.unsubscribeCallback ? "with unsubscribeCallback hook" : ""}`, "TOPIC", topic, "UNSUBSCRIBE MESSAGE", unsubscribeMessage);
            if (responder.unsubscribeCallback) {
                const hookMessage = {
                    ...unsubscribeMessage,
                    removeSubscriber: () => {
                        this.removeSubscribers(topic, subscribeID);
                    },
                };
                responder.unsubscribeCallback(null, hookMessage); // invoke the callback (no error)
            }
            else {
                // since no unsubscribe callback defined, use default functionality
                this.removeSubscribers(topic, subscribeID);
            }
        }
    }
    // callback function for invokeSubscribePubSubCallback to notify new subscriber
    sendNotifyToSubscriber(subscribeMessage, err, notifyData) {
        const { topic, subscribeID } = subscribeMessage.header;
        // Don't send initial notification unless state has been explicitly defined (it may be null, so we have to explicitly check for undefined).
        // This matches functionality if this pubsub responder is in routerService;
        // This can happen because initialState is an optional parameter
        if (typeof notifyData !== "undefined") {
            let message = {
                header: {
                    origin: this.clientName,
                    type: "notify",
                    topic: topic,
                    subscribeID: subscribeID,
                },
                data: notifyData,
            };
            if (err)
                message.header.error = err;
            this.sendToRouterService(message);
        }
        if (!err) {
            // add new subscriber to list
            if (!(topic in this.pubsubListOfSubscribers)) {
                this.pubsubListOfSubscribers[topic] = [subscribeID];
            }
            else {
                this.pubsubListOfSubscribers[topic].push(subscribeID);
            }
            Logger.system.info("RouterClient: incoming subscription added", "TOPIC", topic, "MESSAGE");
        }
        else {
            Logger.system.warn("RouterClient: incoming subscription rejected by pubsub responder", "TOPIC", topic, "MESSAGE", this);
        }
    }
    // Removes the subscribeID from the array for the given subscription topic
    removeSubscribers(topic, subscribeID) {
        let removed = false;
        if (topic in this.pubsubListOfSubscribers) {
            const list = this.pubsubListOfSubscribers[topic];
            for (let i = 0; i < list.length; i++) {
                if (subscribeID === list[i]) {
                    list.splice(i, 1);
                    if (list.length === 0) {
                        delete this.pubsubListOfSubscribers[topic];
                    }
                    removed = true;
                    Logger.system.info("RouterClient: PubSub removeListener", "TOPIC", topic, "FROM", subscribeID);
                    break;
                }
            }
        }
        if (!removed) {
            Logger.system.warn(`RouterClient: tried to remove non-existent listener on ${topic} from ${subscribeID}`);
        }
    }
    // route incoming message to appropriate callback, which depends on the message type and channel
    routeIncomingMessage(incomingMessage) {
        const newTimeCalibrationHandler = () => {
            if (!this.timeCalibrationHandler) {
                setTimeout(() => {
                    newTimeCalibrationHandler();
                }, 10);
                return;
            }
            this.timeCalibrationHandler(incomingMessage);
        };
        Logger.system.verbose("Incoming Message Type", incomingMessage.header.type, incomingMessage);
        switch (incomingMessage.header.type) {
            case "transmit":
                this.invokeListeners(incomingMessage);
                break;
            case "query":
                this.invokeResponder(incomingMessage);
                break;
            case "queryResponse":
                this.invokeQueryResponse(incomingMessage);
                break;
            case "notify":
                this.invokeNotifyCallback(incomingMessage);
                break;
            case "publish":
                this.invokePublishEvent(incomingMessage);
                break;
            case "subscribe":
                this.invokeSubscribeEvent(incomingMessage);
                break;
            case "unsubscribe":
                this.invokeUnsubscribeEvent(incomingMessage);
                break;
            case "timeCalibration":
                // Sometimes when multiple BrowserViews are added to the same window in quick succession (e.g. when loading dashbar at startup)
                // It gets in here before this function exists. So wait for the function to exist.
                newTimeCalibrationHandler();
                break;
            case "initialHandshakeResponse":
                this.handshakeHandler();
                break;
            default:
        }
    }
    // connects to event-router service. will retry various ways if needed
    asyncConnectToEventRouter(routerConfig, transportName, cb) {
        Logger.system.debug(`Name of transport to establish: ${transportName}`);
        let myTimer = null;
        let myRetryCounter = 0;
        let isFinished = false;
        let handshakeFailedCount = 0;
        // Define getClientTransport to avoid warning about it being used before defined.
        let getClientTransport = null;
        const handshakeFailedHandler = () => {
            clearInterval(myTimer);
            handshakeFailedCount++;
            if (handshakeFailedCount <= 3) {
                Logger.console.error("WINDOW LIFECYCLE:STARTUP:RouterClient: failure to connect to router service. Retrying...", handshakeFailedCount, routerConfig);
                getClientTransport === null || getClientTransport === void 0 ? void 0 : getClientTransport();
            }
            else {
                const failureMessage = "A cross domain transport has failed to connect. Cross domain components may not work. Please contact your administrator.";
                Logger.console.error(failureMessage, routerConfig);
                toast("dev", "ONCE-SINCE-STARTUP", "FSBL-Internal-Transport-Failure", failureMessage);
            }
        };
        const sendHandshake = () => {
            var _a;
            const MaxRetryHandshake = Number.MAX_SAFE_INTEGER;
            Logger.system.debug("WINDOW LIFECYCLE:STARTUP:RouterClient: sendHandshake", myRetryCounter);
            const message = {
                header: {
                    origin: this.clientName,
                    type: "initialHandshake",
                },
            };
            (_a = Globals.fin) === null || _a === void 0 ? void 0 : _a.log(`preload: sending handshake attempt ${myRetryCounter}`);
            this.sendToRouterService(message);
            if (myRetryCounter++ > MaxRetryHandshake) {
                handshakeFailedHandler();
            }
        };
        const finished = () => {
            var _a, _b;
            (_a = Globals.fin) === null || _a === void 0 ? void 0 : _a.log("preload: received finished handshake");
            if (!isFinished) {
                // ensure only invoked once
                Logger.system.debug(`WINDOW LIFECYCLE:STARTUP:RouterClient connected: Starting ${this.clientName} with transport ${this.transport.identifier()}`);
                isFinished = true;
                clearInterval(myTimer);
                // this should not happen with proper startup order, which waits on routerClient to be ready
                for (let i = 0; i < this.queue.length; i++) {
                    Logger.system.debug("RouterClient: releasing queued msg");
                    const msg = this.queue[i];
                    // Queued messages won't yet have had a valid origin (clientName). We can replace it now that clientName has been definitively set.
                    this.transport.send({
                        ...msg,
                        header: {
                            ...msg.header,
                            origin: this.clientName,
                        },
                    });
                }
                // notify initialization is complete
                (_b = Globals.fin) === null || _b === void 0 ? void 0 : _b.log("preload: routerClient handshake completed");
                cb === null || cb === void 0 ? void 0 : cb();
            }
        };
        const transportReady = (transportObj) => {
            var _a;
            (_a = Globals.fin) === null || _a === void 0 ? void 0 : _a.log("preload: transportReady");
            const HandshakeRetryInterval = 1000;
            myRetryCounter = 0;
            Logger.system.debug("WINDOW LIFECYCLE:STARTUP:RouterClient:transport ready", "TRANSPORT OBJECT", transportObj);
            this.transport = transportObj;
            this.handshakeHandler = finished; // set function to receive handshake response
            sendHandshake();
            myTimer = setInterval(sendHandshake, HandshakeRetryInterval); // time to retry if response not received back from router service
        };
        const onFailureCallBack = () => {
            Logger.system.error("RouterClient Transport Failed -- onFailure() callbacks invoked.");
            this.isTransportFailed = true;
            // invoke all the parent callbacks registered for onFailure
            while (this.parentFailureCallbackQueue.length > 0) {
                Logger.system.debug("RouterClient parentFailure invoked");
                const nextParentCallback = this.parentFailureCallbackQueue.shift();
                nextParentCallback === null || nextParentCallback === void 0 ? void 0 : nextParentCallback();
            }
        };
        getClientTransport = () => {
            Logger.system.debug("WINDOW LIFECYCLE:STARTUP:RouterClient:getClientTransport", "ROUTER PARAMS:", routerConfig);
            // *** all incoming messages from underlying transport arrive here ***
            // although incoming transport information is available, it is not passed on because not needed
            const incomingMessageHandler = (incomingTransportInfo, message) => {
                message.header.lastClient = this.clientName; // add last client for diagnostics
                message.header.incomingTransportInfo = incomingTransportInfo;
                this.routeIncomingMessage(message);
            };
            RouterTransport.getTransport(routerConfig, transportName, incomingMessageHandler, this.clientName, "RouterService", onFailureCallBack)
                .then(transportReady)
                .catch((error) => {
                Logger.console.error("RouterClient getTransport error", error);
            });
        };
        // main code for this asyncConnectToEventRouter function -- only executed once
        getClientTransport();
    }
    // support function for addPubSubResponder -- add pubsub responder callbackHandler for the given channel
    addPubSubResponderCallBack(topic, subscribeCallback, publishCallback, unsubscribeCallback) {
        const topicAsString = topic.toString(); // toString() in case RegExp
        if (!this.mapPubSubResponders[topicAsString]) {
            if (topic instanceof RegExp) {
                this.mapPubSubResponderRegEx[topicAsString] = topic;
                Logger.system.info(`RouterClient: PubSub RegEx added for topic ${topicAsString}`);
            }
            this.mapPubSubResponders[topicAsString] = {
                subscribeCallback,
                publishCallback,
                unsubscribeCallback,
            };
            return true;
        }
        return false;
    }
    /**
     * Estimates offset to align the reference time with Router Service.  Does this by exchanging messages with RouterService, getting the service's time, and estimating communication delay.
     */
    calibrateTimeWithRouterService(callback) {
        const TARGET_HANDSHAKE_COUNT = 5;
        let handshakeCounter = 0;
        let offsetForFastest = 0;
        let fastestRRT = Infinity;
        const calibrationCalculation = (finalHandshakeMessage) => {
            const { clientBaseTime, serviceBaseTime } = finalHandshakeMessage;
            let timeOffset = 0;
            for (let i = 1; i < TARGET_HANDSHAKE_COUNT; i++) {
                const startClientTime = clientBaseTime[i - 1];
                const stopClientTime = clientBaseTime[i];
                const rtt = stopClientTime - startClientTime; // round-trip time
                const serviceTime = serviceBaseTime[i - 1];
                const offset = serviceTime - (startClientTime + rtt / 2);
                if (rtt < fastestRRT) {
                    fastestRRT = rtt;
                    offsetForFastest = offset;
                }
                timeOffset = timeOffset + offset;
                Logger.system.debug("calibrationCalculation Intermediate Values", "lastRRT", rtt, "lastOffset", offset, "fastestOffset", offsetForFastest, "fastestRRT", fastestRRT);
            }
            timeOffset = timeOffset / (TARGET_HANDSHAKE_COUNT - 1);
            Logger.system.debug("RouterClient calibrationCalculation", "Average Offset", timeOffset, "Chosen FastestOffset", offsetForFastest, finalHandshakeMessage);
            callback(offsetForFastest); // use the offset with the shortest RTT since it is often the most accurate
        };
        const timeCalibrationHandlerFunction = (calibration) => {
            handshakeCounter++;
            if (handshakeCounter > TARGET_HANDSHAKE_COUNT) {
                calibrationCalculation(calibration); // enough handshake data gather, so do the calibration
            }
            else {
                calibration.clientBaseTime.push(Performance.timing.navigationStart + Performance.now());
                const message = {
                    header: {
                        origin: this.clientName,
                        type: "timeCalibration",
                    },
                    clientBaseTime: calibration.clientBaseTime,
                    serviceBaseTime: calibration.serviceBaseTime,
                };
                this.sendToRouterService(message);
            }
        };
        const message = {
            header: {
                origin: this.clientName,
                type: "timeCalibration",
            },
            clientBaseTime: [],
            serviceBaseTime: [],
        };
        this.timeCalibrationHandler = timeCalibrationHandlerFunction; // used in routeIncomingMessage to route handshake response back to handler
        this.timeCalibrationHandler(message); // invoke first time to start exchanging handshakes; will be invoked each time handshake message received back from RouterService
    }
    /**
     * Starts the router client, creating a unique name to represent this instance, initiating the
     * transport that is specified in routerConfig, and then registering itself with the router
     * service. Most of this work occurs in the internal function `asyncConnectToEventRouter()`.
     */
    start(routerConfig) {
        return new Promise(async (resolve) => {
            var _a;
            if (this.started) {
                const warning = `RouterClient.start() called twice. Ignoring second call.`;
                Logger.system.warn(warning);
                return resolve(warning);
            }
            this.started = true;
            // The client name is also the origin name passed to router service.
            // NOTE: the second dot-separated token in the clientName must be the window name since the router service will
            // bind this name to the transport so the InteropService can use for authentication
            this.clientName = `RouterClient.${routerConfig.windowName}.${uuidv4()}`;
            const logMessage = `"RouterClient Check: connecting new client for ${this.clientName}`;
            Logger.system.debug(logMessage);
            console.debug(logMessage);
            (_a = Globals.fin) === null || _a === void 0 ? void 0 : _a.log(logMessage);
            RouterTransport.setTransports();
            this.asyncConnectToEventRouter(routerConfig, routerConfig.transportName, () => {
                this.onReadyCallBack();
                resolve(null);
            });
        });
    }
    // See IRouterClient.ts
    disconnectFromService() {
        Logger.system.info("RouterClient.disconnectFromService");
        const message = {
            header: {
                origin: this.clientName,
                type: "disconnectFromService",
            },
        };
        this.sendToRouterService(message);
    }
    /**
     * Stops the router. This disconnects all responders and listeners and closes all transports.
     * Once stopped, a router may be started again.
     */
    stop() {
        // Remove all registered listeners from router service
        this.disconnectAll();
        // Close the currently open transport
        RouterTransport.close();
        this.started = false;
        this.disconnectFromService();
    }
    // ///////////////////////////////////////////
    // Public Functions -- The Router Client API
    // ///////////////////////////////////////////
    // See IRouterClient.ts
    onReady(cb) {
        return new Promise((resolve) => {
            const sendSignal = () => {
                if (cb)
                    cb();
                resolve();
            };
            if (this.isRouterReady) {
                sendSignal();
            }
            else {
                this.parentReadyCallbackQueue.push(sendSignal);
            }
        });
    }
    // See IRouterClient.ts
    onFailure(cb) {
        return new Promise((resolve) => {
            const sendSignal = () => {
                if (cb)
                    cb();
                resolve();
            };
            if (this.isTransportFailed) {
                sendSignal();
            }
            else {
                this.parentFailureCallbackQueue.push(sendSignal);
            }
        });
    }
    // See IRouterClient.ts
    addListener(channel, eventHandler) {
        Logger.system.info("RouterClient.addListener", "CHANNEL", channel);
        // for addListener: add a callbackHandler into the specified map (which depends on listener type) for the given channel
        const listeners = this.mapListeners[channel];
        if (!listeners || listeners.length === 0) {
            this.mapListeners[channel] = [eventHandler];
            const message = {
                header: {
                    origin: this.clientName,
                    type: "addListener",
                    channel,
                },
            };
            this.sendToRouterService(message);
        }
        else {
            if (listeners.some((listener) => eventHandler === listener)) {
                Logger.debug("RouterClient.addListener() Ignoring duplicate listener with identical callback", channel, eventHandler);
            }
            else {
                listeners.push(eventHandler);
            }
        }
        return () => {
            this.removeListener(channel, eventHandler);
        };
    }
    // See IRouterClient.ts
    transmit(toChannel, data, options) {
        if (!Logger.channelFilter(toChannel)) {
            Logger.system.info("RouterClient.transmit", "TO CHANNEL", toChannel, "DATA", data);
        }
        const message = {
            header: {
                origin: this.clientName,
                type: "transmit",
                channel: toChannel,
            },
            data,
            options,
        };
        this.sendToRouterService(message);
    }
    // See IRouterClient.ts
    removeListener(channel, eventHandler) {
        Logger.system.info("RouterClient.removeListener", "CHANNEL", channel, "EVENT HANDLER", eventHandler);
        if (!this.mapListeners[channel]) {
            Logger.system.debug("Attempt to remove non existent listener", channel, eventHandler);
            console.log("Attempt to remove non existent listener", channel, eventHandler);
            return;
        }
        // Remove any matching listeners from the listener array
        this.mapListeners[channel] = this.mapListeners[channel].flatMap((listener) => {
            if (listener === eventHandler) {
                return [];
            }
            else {
                return [listener];
            }
        });
        // If there are no more listeners for this channel then let the router service
        // know that we are no longer interested in this channel.
        if (!this.mapListeners[channel].length) {
            delete this.mapListeners[channel];
            const message = {
                header: {
                    origin: this.clientName,
                    type: "removeListener",
                    channel,
                },
            };
            this.sendToRouterService(message);
        }
    }
    // See IRouterClient.ts
    addResponder(channel, queryEventHandler) {
        Logger.system.info("RouterClient.addResponder", "CHANNEL", channel);
        // add responder callbackHandler for the given channel
        if (!this.mapQueryResponders[channel]) {
            this.mapQueryResponders[channel] = queryEventHandler;
            const message = {
                header: {
                    origin: this.clientName,
                    type: "addResponder",
                    channel,
                },
            };
            this.sendToRouterService(message);
        }
        else {
            const warning = `RouterClient.addResponder: Responder already locally defined for channel ${channel}`;
            Logger.system.warn(warning);
            queryEventHandler(warning);
        }
    }
    // See IRouterClient.ts for overloads
    // Here we have a complicated type definition to tackle both overloads
    query(responderChannel, queryEvent, queryParams, responseEventHandler) {
        var _a, _b;
        const newQueryID = `${this.clientName}.${uuidv4()}.${responderChannel}`;
        const timestamp = Performance.timing.navigationStart + Performance.now();
        const navstart = Performance.timing.navigationStart;
        const timenow = Performance.now(); // these timer values used for logging diagnostics
        Logger.system.info("RouterClient.query", "RESPONDER CHANNEL", responderChannel, "QUERY EVENT", queryEvent, "PARAMS", queryParams, "QUERY ID", newQueryID, { timestamp, navstart, timenow });
        // 4th parameter if it exists, otherwise 3rd parameter if it's a function, otherwise null
        const handler = responseEventHandler !== null && responseEventHandler !== void 0 ? responseEventHandler : (typeof queryParams === "function" ? queryParams : null);
        const timeout = (_a = queryParams === null || queryParams === void 0 ? void 0 : queryParams.timeout) !== null && _a !== void 0 ? _a : 20000;
        // Normally the central logger will log a warning when no responder exists for a query but
        // sometimes there are unavoidable race conditions. Use this parameter to work around those conditions
        // along with timeout = 0
        const logWhenNoResponder = (_b = queryParams === null || queryParams === void 0 ? void 0 : queryParams.logWhenNoResponder) !== null && _b !== void 0 ? _b : true;
        const promiseResolver = (resolve) => {
            // add a callbackHandler into the query-response map for the given queryID
            this.mapQueryResponses[newQueryID] = (err, response) => {
                handler === null || handler === void 0 ? void 0 : handler(err, response);
                resolve({ err, response });
            };
            // Waiting on timeouts can cause unit tests to hang (if an unintended query is triggered)
            if (!Globals.runningInFSBLUnitTest && timeout > 0) {
                // add timer to wait for query response
                // The RouterService is responsible for monitoring long queries and will log to central logger.
                // So this local warning is really only in play when this app itself has gotten disconnected from the router
                // therefore it is only logged to console, so as to avoid a mess of duplicate logs
                this.mapQueryResponseTimeOut[newQueryID] = setTimeout(() => {
                    console.warn(`RouterClient: timeout after ${timeout} ms waiting on query response on channel ${responderChannel} for queryID ${newQueryID} from client ${this.clientName}`, "from event", queryEvent, "with params", queryParams);
                    delete this.mapQueryResponseTimeOut[newQueryID];
                }, timeout);
            }
            const message = {
                header: {
                    origin: this.clientName,
                    type: "query",
                    queryID: newQueryID,
                    channel: responderChannel,
                    logWhenNoResponder,
                    timeout,
                },
                data: queryEvent,
            };
            this.sendToRouterService(message);
        };
        return new Promise(promiseResolver);
    }
    // See IRouterClient.ts
    removeResponder(responderChannel) {
        Logger.system.info("RouterClient.removeResponder", "RESPONDER CHANNEL", responderChannel);
        if (this.mapQueryResponders[responderChannel]) {
            delete this.mapQueryResponders[responderChannel];
            const message = {
                header: {
                    origin: this.clientName,
                    type: "removeResponder",
                    channel: responderChannel,
                },
            };
            this.sendToRouterService(message);
        }
    }
    // See IRouterClient.ts
    addPubSubResponder(topic, initialState, params, callback) {
        let error;
        let response;
        Logger.system.info("RouterClient.addPubSubResponder", "TOPIC", topic, "INITIAL STATE", initialState, "PARAMS", params);
        params = params || {};
        // if this pubsub responder doesn't define any of the special callback handlers, then the pubsub can be handled in the router service
        if (typeof params.subscribeCallback === "undefined" &&
            typeof params.publishCallback === "undefined" &&
            typeof params.unsubscribeCallback === "undefined") {
            initialState = initialState || {};
            // this message indicates the pubsub will be handled within the router service
            const message = {
                header: {
                    origin: this.clientName,
                    type: "addPubSubResponder",
                    topic: topic.toString(),
                    handleOnServer: true,
                },
                data: initialState,
            };
            this.sendToRouterService(message);
            response = "success";
        }
        else {
            const status = this.addPubSubResponderCallBack(topic, params.subscribeCallback, params.publishCallback, params.unsubscribeCallback);
            if (status) {
                initialState = initialState || {};
                this.mapPubSubResponderState[topic.toString()] = clone(initialState);
                // this message indicates the pubsub will be handled locally withing this router client
                const message = {
                    header: {
                        origin: this.clientName,
                        type: "addPubSubResponder",
                        topic: topic.toString(),
                        handleOnServer: false,
                    },
                    data: initialState,
                };
                this.sendToRouterService(message);
                response = "success";
            }
            else {
                error = `RouterClient.addPubSubResponder: Responder already locally defined for topic ${topic}`;
                Logger.system.error(error);
            }
        }
        if (callback) {
            callback(error, response);
        }
    }
    // See IRouterClient.ts
    removePubSubResponder(topic) {
        Logger.system.info("RouterClient.removePubSubResponder", "TOPIC", topic);
        const topicAsString = topic.toString(); // string or RegExp
        if (this.mapPubSubResponders[topicAsString]) {
            delete this.mapPubSubResponders[topicAsString]; // remove responder
            delete this.mapPubSubResponderState[topicAsString]; // remove corresponding state
            delete this.mapPubSubResponderRegEx[topicAsString]; // remove regex maybe
            const message = {
                header: {
                    origin: this.clientName,
                    type: "removePubSubResponder",
                    topic,
                },
            };
            this.sendToRouterService(message);
        }
        else {
            Logger.system.warn(`RouterClient.removePubSubResponder failed: Could not find responder for topic ${topic}`);
        }
    }
    // See IRouterClient.ts
    subscribe(topic, notifyCallback) {
        Logger.system.info("RouterClient.subscribe", "TOPIC", topic);
        const subscribeID = `${this.clientName}.${uuidv4()}`;
        this.mapSubscribersID[subscribeID] = notifyCallback;
        this.mapSubscribersTopic[subscribeID] = topic;
        const message = {
            header: {
                origin: this.clientName,
                type: "subscribe",
                subscribeID,
                topic,
            },
        };
        this.sendToRouterService(message);
        return { subscribeID, topic };
    }
    // See IRouterClient.ts
    publish(topic, data) {
        Logger.system.info("RouterClient.publish", "TOPIC", topic, "EVENT", data);
        const message = {
            header: {
                origin: this.clientName,
                type: "publish",
                topic,
            },
            data: data,
        };
        this.sendToRouterService(message);
    }
    // See IRouterClient.ts
    unsubscribe(subscribeIDStruct) {
        Logger.system.info("RouterClient.unsubscribe", "SUBSCRIBE ID", subscribeIDStruct);
        // outgoing Unsubscribe: remove subscriber callbackHandler for the given channel
        const { subscribeID } = subscribeIDStruct;
        if (this.mapSubscribersID[subscribeID]) {
            delete this.mapSubscribersID[subscribeID];
            delete this.mapSubscribersTopic[subscribeID];
            const message = {
                header: {
                    origin: this.clientName,
                    type: "unsubscribe",
                    subscribeID: subscribeIDStruct.subscribeID,
                    topic: subscribeIDStruct.topic,
                },
            };
            this.sendToRouterService(message);
        }
        else {
            Logger.system.warn(`RouterClient.unsubscribe: Could not find subscribeID for topic ${subscribeIDStruct.topic}`);
        }
    }
    /**
     * Test an incoming router message to see if it originated from the same origin (e.g. a trusted source...not cross-domain). Currently same origin is known only because a sharedWorker transport is used (by definition SharedWorkers do not work cross-domain).  This means any message coming in over the Inter-application Bus will not be trusted; however, by default all same-origin components and services connect to the router using a SharedWorker transport.
     *
     * ```javascript
     * FSBL.Clients.RouterClient.trustedMessage(incomingRouterMessage);
     * ```
     * @param {object} incomingMessage an incoming router message (e.g. transmit, query, notification) to test to see if trusted.
     */
    trustedMessage(incomingMessage) {
        var _a;
        Logger.system.debug("RouterClient.trustedMessage header", incomingMessage.header);
        return (_a = incomingMessage.header.originMetadata) === null || _a === void 0 ? void 0 : _a.transportInfo.transportID.includes("SharedWorker");
    }
    // See IRouterClient.ts
    disconnectAll() {
        Logger.system.info("RouterClient.disconnectAll");
        Object.keys(this.mapListeners).forEach((channel) => {
            const handlers = Array.from(new Set(this.mapListeners[channel])); // de-dupe to avoid warning messages
            handlers.forEach((handler) => {
                this.removeListener(channel, handler);
            });
        });
        Object.keys(this.mapQueryResponders).forEach((channel) => {
            this.removeResponder(channel);
        });
        Object.keys(this.mapPubSubResponders).forEach((topic) => {
            this.removePubSubResponder(topic);
        });
        Object.keys(this.mapSubscribersID).forEach((subscribeID) => {
            const topic = this.mapSubscribersTopic[subscribeID];
            this.unsubscribe({ topic, subscribeID });
        });
    }
}
/**
 * @ignore
 */
export const RouterController = {
    /**
     * Used to start the router. This is called by entry points (FSBLDesktop.ts, FSBLFreestanding.ts, FSBLService.ts [ in baseService, serviceManager's bootEngine.ts, logger's main.jsx ]  )
     * @private
     */
    start: (params) => {
        const routerConnection = singleton("RouterClient", RouterConnection);
        Logger.setRouterClient(routerConnection);
        return routerConnection.start(params);
    },
    /**
     * Used to stop the router. Generally used for unit testing.
     * @private
     */
    stop: () => {
        const routerConnection = singleton("RouterClient", RouterConnection);
        routerConnection.stop();
    },
    /**
     * Used internally by Logger client
     */
    calibrateTimeWithRouterService(callback) {
        const routerConnection = singleton("RouterClient", RouterConnection);
        routerConnection.calibrateTimeWithRouterService(callback);
    },
};
/**
 * @language javascript, java, dotnet
 * @deprecated Don't use this method. (Apps that use the FSBL API and wait on the global `onReady` event are guaranteed that the router will be ready). Internal code
 * should wait on Startup.onReady().
 *
 * Checks if Router is ready. May be invoked multiple times. Invokes optional callback and resolves promise when ready, which may be immediately.  Router is not ready until underlying transport to Router Service is ready.
 *
 * @param {function} [cb] Optional callback function to invoke when Router is ready.
 * @return {Promise} Promise resolves when Router is ready
 */
export const onReady = (cb) => {
    Logger.system.deprecated("You no longer need to call RouterClient.onReady()");
    const routerConnection = singleton("RouterClient", RouterConnection);
    return routerConnection.onReady(cb);
};
/**
 * @language javascript, java, dotnet
 * May be invoked multiple times. Invokes optional callback and resolves promise if router client fails, typically due to underlying transport failure.
 *
 * @param {function} [cb] Optional callback function to invoke if the routerClient fail due to a loss of connectivity.
 * @return {Promise} Promise resolves on Router failure
 */
export const onFailure = (cb) => {
    const routerConnection = singleton("RouterClient", RouterConnection);
    return routerConnection.onFailure(cb);
};
/**
 * @language javascript, java, dotnet
 * Adds a listener for incoming transmit events on the specified channel. Each of the incoming events will trigger the specified event handler. The number of listeners is not limited (either local to this Finsemble window or in a separate Finsemble window).
 *
 * See [`transmit`](#transmit) for sending a corresponding event message to the listener. See [`removeListener`](#removelistener) to remove the listener.
 *
 * ```javascript
 * FSBL.Clients.RouterClient.addListener("SomeChannelName", function (error, response) {
 * 	if (error) {
 *  		Logger.system.error("ChannelA Error: " + JSON.stringify(error));
 * 	} else {
 *  		var data = response.data;
 *  		if (response.originatedHere()) {
 *   			Logger.system.warn("Received a message from the calling window, ignoring");
 *  		} else {
 * 			// Logic to perform with data
 * 		}
 * 	}
 * });
 * ```
 * @param {string} channel A unique string to identify the channel (must match corresponding transmit channel name).
 * @param {function} eventHandler A callback handling any possible error and the response. Use <b>response.originatedHere()</b> to determine if the message came from the same window. (see example below).
 * @return {RouterListenerUnsubscribe} Returns a function that can be called to unsubscribe (convenient alternative to removeListener)
 */
export const addListener = (channel, eventHandler) => {
    const routerConnection = singleton("RouterClient", RouterConnection);
    return routerConnection.addListener(channel, eventHandler);
};
/**
 * @language javascript, java, dotnet
 * Transmits data to all listeners on the specified channel. If there are no listeners on the channel, the event is discarded without error. All listeners on the channel in this Finsemble window and other Finsemble windows will receive the transmit.
 *
 * See [`addListener`](#addlistener) to add a listener to receive the transmit.
 *
 * ```javascript
 * FSBL.Clients.RouterClient.transmit("SomeChannelName", event);
 * ```
 * @param {string} toChannel A unique string to identify the channel (must match corresponding listener channel name).
 * @param {any} data An object or primitive type to be transmitted.
 * @param {object} options An object containing options for your transmission.
 * @param {boolean} options.suppressWarnings By default, the Router will log warnings if you transmit to a channel with no listeners. Set this to true to eliminate those warnings.
 */
export const transmit = (toChannel, data, options) => {
    const routerConnection = singleton("RouterClient", RouterConnection);
    routerConnection.transmit(toChannel, data, options);
};
/**
 * @language javascript, java, dotnet
 * Removes an event listener from the specified channel for the specific event handler; only listeners created locally can be removed.
 *
 * See [`addListener`](#addlistener) for corresponding add of a listener.
 *
 * @param {string} channel The unique channel name from which to remove the listener.
 * @param {function} eventHandler Function used for the event handler when the listener was added.
 */
export const removeListener = (channel, eventHandler) => {
    const routerConnection = singleton("RouterClient", RouterConnection);
    routerConnection.removeListener(channel, eventHandler);
};
/**
 * @language javascript, java, dotnet
 * Adds a query responder to the specified channel. The responder's `queryEventHandler` function will receive all incoming queries for the specified channel (whether from this Finsemble window or remote Finsemble windows).
 *
 * :::note
 * Only one responder is allowed per channel within Finsemble.
 * :::
 *
 * See [`query`](#query) for sending a corresponding query-event message to this responder.
 *
 * ```javascript
 * FSBL.Clients.RouterClient.addResponder("ResponderChannelName", function (error, queryMessage) {
 *	if (error) {
 *		Logger.system.log('addResponder failed: ' + JSON.stringify(error));
 *	} else {
 *		console.log("incoming data=" + queryMessage.data);
 * 		var response="Back at ya"; // Responses can be objects or strings
 * 		queryMessage.sendQueryResponse(null, response); // The callback must respond, else a timeout will occur on the querying client.
 * 	}
 * });
 * ```
 * @param {string} channel A unique string to identify the channel (must match corresponding query channel name); only one responder is allowed per channel.
 * @param {StandardCallback} queryEventHandler A function to handle the incoming query (see example); note the incoming `queryMessage` contains a function to send the response.
 */
export const addResponder = (channel, queryEventHandler) => {
    const routerConnection = singleton("RouterClient", RouterConnection);
    routerConnection.addResponder(channel, queryEventHandler);
};
/**
 * @language javascript, java, dotnet
 * Sends a query to the responder listening on the specified channel. The responder may be in this Finsemble window or another Finsemble window.
 *
 *
 * See [`addResponder`](#addresponser) to add a responder to receive the query.
 *
 * ```javascript
 * FSBL.Clients.RouterClient.query("someChannelName", {}, function (error, queryResponseMessage) {
 *	if (error) {
 *		Logger.system.log('query failed: ' + JSON.stringify(error));
 *	} else {
 *		// process income query response message
 *		var responseData = queryResponseMessage.data;
 *		Logger.system.log('query response: ' + JSON.stringify(queryResponseMessage));
 *	}
 * });
 *
 * FSBL.Clients.RouterClient.query("someChannelName", { queryKey: "abc123"}, { timeout: 1000 }, function (error, queryResponseMessage) {
 *	if (!error) {
 *		// process income query response message
 *		var responseData = queryResponseMessage.data;
 *	}
 * });
 * ```
 * @param {string} responderChannel A unique string that identifies the channel (must match the channel name on which a responder is listening).
 * @param {object} queryEvent The event message sent to the responder.
 * @param {function} responseEventHandler An event handler to receive the query response (sent from a responder that is listening on this channel).
 */
export const query = (responderChannel, queryEvent, queryParams, responseEventHandler) => {
    const routerConnection = singleton("RouterClient", RouterConnection);
    return routerConnection.query(responderChannel, queryEvent, queryParams, responseEventHandler);
};
/**
 * @language javascript, java, dotnet
 * Removes the query responder from the specified channel. Only a locally added responder can be removed (i.e., a responder defined in the same component or service).
 *
 * See [`addResponder()`](#addresponder) for adding a query responder.
 *
 * ```javascript
 * FSBL.Clients.RouterClient.removeResponder("someChannelName");
 * ```
 * @param {string} responderChannel String identifying the channel from which to remove the responder.
 */
export const removeResponder = (responderChannel) => {
    const routerConnection = singleton("RouterClient", RouterConnection);
    routerConnection.removeResponder(responderChannel);
};
/**
 * @language javascript, java, dotnet
 * Adds a PubSub responder for the specified topic (either a specific string or a regular-expression match). PubSub
 * topics are stateful. New subscribers to the topic are automatically provided the most recent state.
 * Once a responder has been added, other apps can subscribe to and publish state on the topic
 *
 * See [`publish()`](#publish) and [`subscribe()`](#subscribe).
 *
 * Only one responder may be set per topic across the entire system (otherwise a warning will show in Central Logger).
 * If another responder is set with an overlapping regular-expression then the responder with an exact string is still guaranteed to get the messages
 * (the regex responder will not receive messages that match a responder with a matching string).
 *
 * > If multiple responders are set with _overlapping_ regular-expressions, then the receiver is _indeterminate_.
 *
 * When initializing a responder, hooks can be provided to intercept and manipulate publish, subscribe, and unsubscribe events (see parameters).
 *
 * ```javascript
 * // Example, initializing a responder without any optional hooks:
 * FSBL.Clients.RouterClient.addPubSubResponder("topicABC", { "State": "start" });
 *
 * // Example, initializing a responder with a regex for matching topics:
 * FSBL.Clients.RouterClient.addPubSubResponder(new RegExp("topicA*"), { "State": "start" });
 *
 * // Example, initializing a responder with all three hooks and an initial state:
 * FSBL.Clients.RouterClient.addPubSubResponder("topicABC", { "State": "start" },
 * 	{
 * 		subscribeCallback:subscribeCallback,
 * 		publishCallback:publishCallback,
 * 		unsubscribeCallback:unsubscribeCallback
 * 	});
 *
 * // Example publish hook:
 * function publishCallback(error, publish) {
 * 	if (publish) {
 * 		// `publish.data` contains the data that the originating app published.
 * 		console.log(publish.data);
 *
 * 		// This function must be called to complete the publication. Set the first parameter to `null`
 * 		// to complete the publication. Set the first parameter to a string in order to suppress the publication
 * 		// and return an error to the publisher.
 * 		//
 * 		// The second parameter must contain the data that you wish to publish. This data will be sent to all
 * 		// subscribers including the originating app. It can be either the originating state (from `publish.data`)
 * 		// or a different/manipulated state. This will become the new current state for the topic.
 * 		publish.sendNotifyToAllSubscribers(null, { someOtherData: "blah"});
 * 	}
 * }
 *
 * // Example subscribe hook:
 * function subscribeCallback(error, subscribe) {
 * 	if (subscribe) {
 * 		// This function must be called to accept or reject the subscription. Send an error code as the first parameter
 * 		// to reject. Send null as the first parameter to accept. Once accepted, the subscriber will receive
 * 		// the current topic's state unless the second parameter is provided which will override what is sent
 * 		// to the subscriber (but not override the topic's actual internal state).
 * 		subscribe.sendNotifyToSubscriber(null, { "NOTIFICATION-STATE": "One" });
 * 	}
 * }
 *
 * // Example unsubscribe hook:
 * function unsubscribeCallback(error, unsubscribe) {
 * 	if (unsubscribe) {
 * 		// This function must be called to accept or reject the unsubscription. Send null (or nothing) as the first
 * 		// parameter to accept the unsubscription. Send an error code to reject it.
 * 		unsubscribe.removeSubscriber(null);
 * 	}
 * }
 * ```
 * @param string|RegExp topic A unique topic for this responder, or a RegEx (e.g. '/abc.+/') to handle a set of topics.
 * @param {object} initialState The initial state for the topic (defaults to `{}`).
 * @param {object} params Optional hooks
 * @param {function} params.publishCallback This hook is called whenever a remote app publishes new state.
 * `[arg2].sendNotifyToAllSubscribers()` must be called to complete the publication but can send a different state than what was published by the originating app
 * (the originating publishing app will receive this state on its subscriptions).
 * See example publish hook.
 *
 * @param {function} params.subscribeCallback This hook is called whenever a remote app subscribes to this responder's topic.
 * It can be used to deny subscriptions or to run independent code for any new subscription.
 * `[arg2].sendNotifyToSubscriber()` must be called in order for the responder to complete the subscription, and can optionally provide
 * the subscribing app with a different initial than the topic's current state.
 * See example subscribe hook.
 *
 * @param {function} params.unsubscribeCallback This hook is called whenever a remote app unsubscribes from this responder's topic.
 * `[arg2].removeSubscriber()` must be called in order to complete the unsubscription.
 * See example unsubscribe hook.
 *
 * @param {function} callback An optional callback function which should accept a possible error.
 * If `addPubSubResponder()` failed then the first parameter to the callback will contain the error condition.
 * The most common error is when adding a responder for a topic that already has a responder:
 * "RouterClient.addPubSubResponder: Responder already locally defined for topic ${topic}";
 */
export const addPubSubResponder = (topic, initialState, params, callback) => {
    const routerConnection = singleton("RouterClient", RouterConnection);
    routerConnection.addPubSubResponder(topic, initialState, params, callback);
};
/**
 * @language javascript, java, dotnet
 * Removes a PubSub responder from the specified topic. Only locally created responders (i.e. created in the local window) can be removed.
 *
 * See [`addPubSubResponder`](#addpubsubresponder) for corresponding addition of a PubSub responder.
 *
 * ```javascript
 * FSBL.Clients.RouterClient.removePubSubResponder("topicABC");
 * ```
 * @param {string} topic Unique topic for responder being removed (may be RegEx, but if so much be exact RegEx used previously with `addPubSubResponder`).
 */
export const removePubSubResponder = (topic) => {
    const routerConnection = singleton("RouterClient", RouterConnection);
    routerConnection.removePubSubResponder(topic);
};
/**
 * @language javascript, java, dotnet
 * Subscribe to a PubSub responder. Each responder topic can have many subscribers (local in this window or remote in other windows). Each subscriber immediately (but asynchronously) receives back current state in a notify; new notifications are received for each publish sent to the same topic.
 *
 * See [`addPubSubResponder`](#addpubsubresponder) for corresponding addition of a PubSub responder to handle the subscribe. See [`publish()`](#publish) for how to publish data to subscribers.
 *
 * ```javascript
 * var subscribeId = RouterClient.subscribe("topicABC", function(err, notify) {
 *	if (!err) {
 *		var notificationStateData = notify.data;
 *		// do something with notify data
 *	}
 * });
 * ```
 * @param {string} topic A unique string representing the topic being subscribed to.
 * @param {function} notifyCallback Invoked on immediately upon subscription, then again on each incoming notification for the specified topic.
 * @returns {object} An object containing a unique id representing the subscription. This can be used later to unsubscribe.
 */
export const subscribe = (topic, notifyCallback) => {
    const routerConnection = singleton("RouterClient", RouterConnection);
    return routerConnection.subscribe(topic, notifyCallback);
};
/**
 * @language javascript, java, dotnet
 * Publish to a PubSub Responder, which will trigger a corresponding notification to all subscribers (local in this window or remote in other windows). There may be multiple publishers for a topic (again, in same window or remote windows).
 *
 * See [`addPubSubResponder`](#addpubsubresponder) for corresponding addition of a PubSub publisher (i.e., sending notifications to all subscriber). See [`Subscribe`](#subscribe) for corresponding subscription published results (in the form of a `Notify` event).
 *
 * ```javascript
 * FSBL.Clients.RouterClient.publish("topicABC", topicState);
 * ```
 * @param {string} topic A unique string representing the topic being published.
 * @param {object} event The topic state to be published to all subscribers (unless the PubSub responder optionally modifies in between).
 */
export const publish = (topic, event) => {
    const routerConnection = singleton("RouterClient", RouterConnection);
    routerConnection.publish(topic, event);
};
/**
 * @language javascript, java, dotnet
 * Unsubscribes from a PubSub responder so no more notifications are received (but doesn't affect other subscriptions). Only works from the window the PubSub responder was created in.
 *
 * See [`subscribe`](#subscribe) for corresponding subscription being removed.
 *
 * ```javascript
 * FSBL.Clients.RouterClient.unsubscribe(subscribeId);
 * ```
 * @param {object} subscribeIDStruct
 * @param {string} subscribeIDStruct.subscribeID The id returned from the corresponding subscription to the topic.
 * @param {string} subscribeIDStruct.topic The topic for the subscription.
 */
export const unsubscribe = (subscribeIDStruct) => {
    const routerConnection = singleton("RouterClient", RouterConnection);
    routerConnection.unsubscribe(subscribeIDStruct);
};
/**
 * @language javascript, java, dotnet
 * Tests an incoming Router message to see if it originated from the same origin (i.e., a trusted source; not cross-domain).
 *
 * Currently, the origin of an incoming Router message is determined by way of the SharedWorker transport. By definition, SharedWorkers do not work across domains.
 * This means any message coming in over the transport will not be trusted. However, by default, all same-origin components
 * and services connect to the Router using a SharedWorker transport.
 *
 * ```javascript
 * FSBL.Clients.RouterClient.trustedMessage(incomingRouterMessage);
 * ```
 * @param {object} incomingMessage An incoming Router message (e.g., transmit, query, notification) to test to see if trusted.
 */
export const trustedMessage = (incomingMessage) => {
    const routerConnection = singleton("RouterClient", RouterConnection);
    return routerConnection.trustedMessage(incomingMessage);
};
/**
 * @language javascript, java, dotnet
 * Removes all listeners, responders, and subscribers for this router client -- automatically called when the client is shutting down. Can be called multiple times.
 */
export const disconnectAll = () => {
    const routerConnection = singleton("RouterClient", RouterConnection);
    routerConnection.disconnectAll();
};
/**
 * Used internally by Logger client
 * @ignore
 */
export const calibrateTimeWithRouterService = (callback) => {
    const routerConnection = singleton("RouterClient", RouterConnection);
    routerConnection.calibrateTimeWithRouterService(callback);
};
/**
 * @ignore
 */
export const RouterClient = {
    onReady,
    onFailure,
    addListener,
    transmit,
    removeListener,
    addResponder,
    query,
    removeResponder,
    addPubSubResponder,
    removePubSubResponder,
    subscribe,
    publish,
    unsubscribe,
    trustedMessage,
    disconnectAll,
    calibrateTimeWithRouterService,
};
/**
 * @ignore
 */
export default RouterClient;
//# sourceMappingURL=routerClient.js.map