Source: common/network/dispatcher.js

define(["lib/jquery",               "lib/underscore",           "common/util/log",
        "common/util/id_generator", "common/util/binding_list", "./tracer"],
function($,                          _,                          Log,
         IDGenerator,                BindingList,                Tracer) {

    "use strict";

    var logger = Log("Dispatcher");

    /**
     * @privconstructor
     * @class
     * A Dispatcher handles communication between the currently running Lens
     * app and other apps and services. Currently, Dispatchers are only used
     * by apps to communicate with embedded Lens apps created with
     * {@link Lens.Embed}.
     *
     * @name Dispatcher
     */
    var Dispatcher = function(transport) {
        /** @alias Dispatcher.prototype */
        var self = {};

        // Map of [event name] -> [BindingList of subscriber functions].
        var subscribers = {};

        // Map of [nonce] -> [callback] for RPC that are awaiting a response.
        var nonces = {};

        // Map of [RPC name] -> {handler: [function], async: [boolean]} for
        // client-side RPCs.
        var rpcHandlers = {};

        // Called when a message comes in over the WebSocket.
        transport.messageReceived(function(message) {
            var data = JSON.parse(message);

            logger.debug("message received:", data);
            Tracer.post("message received", data.trace);

            handleMessage(data);
        });

        // Called with the parsed data from a message.
        var handleMessage = function(message) {
            if(message.type === "response") {
                // RPC response. Invoke the callback and remove the nonce.
                if(nonces[message.nonce]) {
                    nonces[message.nonce](message.data, message.trace);
                    delete nonces[message.nonce];
                }
                else {
                    logger.debug("ignored RPC response with non-existant nonce ", message.nonce);
                }
            }
            else if(message.type === "event") {
                // Standard event. Call subscribers.
                if(subscribers[message.name]) {
                    subscribers[message.name].callAll(message.data, message.trace);
                }
            }
            else if(message.type === "request" && rpcHandlers[message.name]) {
                var handler = rpcHandlers[message.name];

                // we use _.once to make sure RPCs can't return multiple times
                var reply = _.once(function(return_data) {
                    sendMessage({
                        name: message.name,
                        type: "response",
                        nonce: message.nonce,
                        data: return_data,
                        trace: message.trace
                    });
                });

                if(handler.async) {
                    handler.handler(message.data, reply, message.trace);
                }
                else {
                    reply(handler.handler(message.data, message.trace));
                }
            }
        };

        // sends a message over the websocket as JSON
        var sendMessage = function(message) {
            logger.debug("sending message:", message);
            var json = JSON.stringify(message);
            transport.send(json);

            // sending the message to local subscribers.
            // We do this after sending over the websocket. That way, if the
            // listener modifies the message, it won't change what gets sent
            handleMessage(message);
        };

        /**
         * Subscribes a function to be called whenever this Dispatcher
         * receives a particular event. Use {@link Dispatcher#fireEvent} on the
         * other side to fire an event.
         *
         * @param  {String} eventName
         *     Name of the event.
         * @param  {Function} subscriber
         *     Function to call when the event is received. Gets the message
         *     data as the first argument and the trace id as the second
         *     argument.
         *
         * @return {Binding}
         *     A binding, allowing the subscription to be cancelled later.
         */
        self.subscribe = function(eventName, subscriber) {
            if(!subscribers[eventName]) {
                subscribers[eventName] = BindingList();
            }

            return subscribers[eventName].add(subscriber);
        };

        /**
         * Fires an event. Use {@link Dispatcher#subscribe} on the other side
         * to listen for events. The event will be fired locally, as well, so
         * any subscribers for the event attached to this Dispatcher will
         * be called.
         *
         * @param {String} name      Name of the event.
         * @param {Object} [data]    Event data.
         * @param {String} [traceId] A trace ID for the event. If not given, one
         *                           will be randomly generated.
         */
        self.fireEvent = function(name, data, traceId) {
            if(!traceId) {
                traceId = IDGenerator.traceId();
            }

            sendMessage({
                name: name,
                type: "event",
                data: (data || {}),
                trace: traceId
            });
        };

        /**
         * Calls an RPC. Use {@link Dispatcher#registerRPC} or
         * {@link Dispatcher#registerAsyncRPC} to register a handler for an
         * RPC.
         *
         * @param  {String}   name      Name of the RPC.
         * @param  {Object}   data      Arbitrary data argument.
         * @param  {Function} callback  Callback that receives the result of the
         *                              RPC.
         * @param  {String}   [traceId] A trace ID for the event. If not given,
         *                              one will be randomly generated.
         */
        self.callRPC = function(name, data, callback, traceId) {
            var id = IDGenerator.uuid();

            nonces[id] = callback;
            if(!traceId) {
                traceId = IDGenerator.traceId();
            }

            sendMessage({
                name: name,
                type: "request",
                nonce: id,
                data: data,
                trace: traceId
            });
        };

        /**
         * Creates a new RPC. This version requires that the RPC synchronously
         * returns a value, see {@link Dispatcher#registerAsyncRPC} for a
         * version that allows for asynchrony.
         *
         * @param  {String}   name
         *   Name of the RPC.
         * @param  {Function} handler
         *   Gets the a data object as the first argument and the trace id as
         *   the second argument. Must return the result of the RPC.
         */
        self.registerRPC = function(name, handler) {
            rpcHandlers[name] = {handler: handler, async: false};
        };

        /**
         * Creates a new RPC. This version requires that the RPC asynchronously
         * returns a value, see {@link Dispatcher#registerAsyncRPC} for a
         * version that allows for synchrony.
         *
         * @param  {String}   name
         *   Name of the RPC.
         * @param  {Function} handler
         *   Gets the a data object as the first argument, a callback as the
         *   second argument, and a trace id as the third argument. Must invoke
         *   the callback, passing in the result of the RPC.
         */
        self.registerAsyncRPC = function(name, handler) {
            rpcHandlers[name] = {handler: handler, async: true};
        };

        Object.freeze(self);
        return self;
    };

    _.extend(Dispatcher, /** @lends Dispatcher */ {

        /**
         * Creates a new Dispatcher transport to communicate over a websocket.
         * @param  {String} url URL to connect to.
         * @private
         */
        _WebsocketTransport: function(url) {
            var self = {};

            // the websocket
            var ws = new WebSocket(url);

            // Array of messages to be sent once the websocket opens
            var messageQueue = [];

            // message subscribers
            var messageHandlers = BindingList();

            // whether the websocket is open
            var isOpen = false;


            // Bind handlers to websocket events

            ws.onopen = function() {
                logger.info("WebSocket", url, "is open.");
                isOpen = true;

                // send all queued messages and clear the queue
                _.each(messageQueue, function(msg) {
                    self.send(msg);
                });

                messageQueue = [];
            };

            ws.onclose = function() {
                logger.info("WebSocket", url, "is closed.");
            };

            ws.onmessage = function(messageEvent) {
                messageHandlers.callAll(messageEvent.data);
            };


            // Required API for dispatcher

            self.messageReceived = function(fn) {
                return messageHandlers.add(fn);
            };

            self.send = function(message) {
                if(!isOpen) {
                    // websocket's closed, queue the message
                    messageQueue.push(message);
                } else {
                    // websocket's open, send it
                    ws.send(message);
                }
            };

            Object.freeze(self);
            return self;
        },

        /**
         * Creates a new Dispatcher transport to with another window via
         * postMessage.
         *
         * @param  {Window | HTMLIFrameElement} otherWindowOrIframe A window to communicate with.
         * @private
         */
        _PostMessageTransport: function(otherWindowOrIframe) {
            var self = {};
            var messageHandlers = BindingList();
            var messageQueue = [];
            var otherWindow = null;

            if(otherWindowOrIframe instanceof HTMLIFrameElement) {
                // iframe may or may not be loaded. If it's loaded, extract
                // the window. If it's not, wait until it's loaded to extract
                // the window, and fire off queued messages.
                if(otherWindowOrIframe.contentWindow) {
                    otherWindow = otherWindowOrIframe.contentWindow;
                }
                else {
                    $(otherWindowOrIframe).load(function() {
                        otherWindow = otherWindowOrIframe.contentWindow;

                        // send all queued messages and clear the queue
                        _.each(messageQueue, function(msg) {
                            self.send(msg);
                        });

                        messageQueue = [];
                    });
                }
            }
            else {
                // we have a window, just use that
                otherWindow = otherWindowOrIframe;
            }

            // Required API for Dispatcher

            self.messageReceived = function(fn) {
                return messageHandlers.add(fn);
            };

            self.send = function(message) {
                if(!otherWindow) {
                    // websocket's closed, queue the message
                    messageQueue.push(message);
                } else {
                    // websocket's open, send it
                    otherWindow.postMessage(message, "*");
                }
            };

            // handle incoming message

            window.addEventListener("message", function(evt) {
                if(evt.source === otherWindow) {
                    messageHandlers.callAll(evt.data);
                }
            });

            Object.freeze(self);
            return self;
        },

        /**
         * Creates a new Dispatcher transport to communicate with windows,
         * tabs, extensions, or apps via chrome.runtime.connect.
         *
         * This transport can only be used in priviledged code (e.g. code
         * inside content scripts, extensions, or apps), as it requires access
         * to chrome.runtime.connect.
         * @private
         */
        _ChromeRuntimeTransport: function(opts) {
            var self = {};
            var messageHandlers = BindingList();
            var webpageConnection = null;

            // Required API for Dispatcher

            self.messageReceived = function(fn) {
                return messageHandlers.add(fn);
            };

            self.send = function(message) {
                if(webpageConnection) {
                    webpageConnection.postMessage(message);
                }
                else {
                    logger.warn("Tried to send message before receiving a connection: ", message);
                }
            };

            // handle incoming connection
            chrome.runtime.onConnectExternal.addListener(function(conn) {
                if(opts.onReconnect) {
                    opts.onReconnect();
                }

                webpageConnection = conn;

                conn.onMessage.addListener(function(message) {
                    messageHandlers.callAll(message);
                });

                conn.onDisconnect.addListener(function() {
                    webpageConnection = false;
                });
            });

            Object.freeze(self);
            return self;
        },
    });

    /**
     * A Dispatcher that's connected to the Bridge via WebSocket.
     * @type {Dispatcher}
     * @private
     */
    Dispatcher._bridge = Dispatcher(Dispatcher._WebsocketTransport("ws://localhost:58646/"));

    // send a registration message
    Dispatcher._bridge.fireEvent("app_registration", {lens_version: "1.0.0"});

    return Dispatcher;


});