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; });