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