Author: johnh
Date: Tue Jul 8 19:02:21 2008
New Revision: 675082
URL: http://svn.apache.org/viewvc?rev=675082&view=rev
Log:
Refactoring rpc.js for cleaner handling of transport types (SHIDNIG-410).
The library selects the appropriate transport mechanism for the browser, sets
up its
static requirements in setupChannel(), initializes per-gadget communication in
setupFrame() (some methods),
and makes calls via callX(), where X = method.
Code flow is thus simplified and makes it easier to potentially add new
transports in the future.
Modified:
incubator/shindig/trunk/features/rpc/rpc.js
Modified: incubator/shindig/trunk/features/rpc/rpc.js
URL:
http://svn.apache.org/viewvc/incubator/shindig/trunk/features/rpc/rpc.js?rev=675082&r1=675081&r2=675082&view=diff
==============================================================================
--- incubator/shindig/trunk/features/rpc/rpc.js (original)
+++ incubator/shindig/trunk/features/rpc/rpc.js Tue Jul 8 19:02:21 2008
@@ -18,7 +18,9 @@
/**
* @fileoverview Remote procedure call library for gadget-to-container,
- * container-to-gadget, and gadget-to-gadget communication.
+ * container-to-gadget, and gadget-to-gadget (thru container) communication.
+ *
+ *
*/
var gadgets = gadgets || {};
@@ -29,6 +31,14 @@
* @name gadgets.rpc
*/
gadgets.rpc = function() {
+ // General constants.
+ var CALLBACK_NAME = '__cb';
+ var DEFAULT_NAME = '';
+
+ // Consts for FrameElement.
+ var FE_G2C_CHANNEL = '__g2c_rpc';
+ var FE_C2G_CHANNEL = '__c2g_rpc';
+
var services = {};
var iframePool = [];
var relayUrl = {};
@@ -36,28 +46,115 @@
var authToken = {};
var callId = 0;
var callbacks = {};
+ var setup = {};
+
+ var params = {};
+
+ // Load the authentication token for speaking to the container
+ // from the gadget's parameters, or default to '0' if not found.
+ if (gadgets.util) {
+ params = gadgets.util.getUrlParameters();
+ }
- var params = gadgets.util.getUrlParameters();
authToken['..'] = params.rpctoken || params.ifpctok || 0;
- // Pick the most efficient RPC relay mechanism
- var relayChannel = typeof document.postMessage === 'function' ? 'dpm' :
- typeof window.postMessage === 'function' ? 'wpm' :
- 'ifpc';
- if (relayChannel === 'dpm' || relayChannel === 'wpm') {
- window.addEventListener('message', function(packet) {
- // TODO validate packet.domain for security reasons
- process(gadgets.json.parse(packet.data));
- }, false);
+ /*
+ * Return a short code representing the best available cross-domain
+ * message transport available to the browser.
+ *
+ * + For those browsers that support native messaging (various
implementations
+ * of the HTML5 postMessage method), use that. Officially defined at
+ * http://www.whatwg.org/specs/web-apps/current-work/multipage/comms.html.
+ *
+ * postMessage is a native implementation of XDC. A page registers that
+ * it would like to receive messages by listening the the "message" event
+ * on the window (document in DPM) object. In turn, another page can
+ * raise that event by calling window.postMessage (document.postMessage
+ * in DPM) with a string representing the message and a string
+ * indicating on which domain the receiving page must be to receive
+ * the message. The target page will then have its "message" event raised
+ * if the domain matches and can, in turn, check the origin of the message
+ * and process the data contained within.
+ *
+ * wpm: postMessage on the window object.
+ * - Internet Explorer 8+
+ * - Safari (latest nightlies as of 26/6/2008)
+ * - Firefox 3+
+ * - Opera 9+
+ *
+ * dpm: postMessage on the document object.
+ * - Opera 8+
+ *
+ * + For Gecko-based browsers, the security model allows a child to call a
+ * function on the frameElement of the iframe, even if the child is in
+ * a different domain. This method is dubbed "frameElement" (fe).
+ *
+ * The ability to add and call such functions on the frameElement allows
+ * a bidirectional channel to be setup via the adding of simple function
+ * references on the frameElement object itself. In this implementation,
+ * when the container sets up the authentication information for that
gadget
+ * (by calling setAuth(...)) it as well adds a special function on the
+ * gadget's iframe. This function can then be used by the gadget to send
+ * messages to the container. In turn, when the gadget tries to send a
+ * message, it checks to see if this function has its own function stored
+ * that can be used by the container to call the gadget. If not, the
+ * function is created and subsequently used by the container.
+ * Note that as a result, FE can only be used by a container to call a
+ * particular gadget *after* that gadget has called the container at
+ * least once via FE.
+ *
+ * fe: Gecko-specific frameElement trick.
+ * - Firefox 1+
+ *
+ * + For all others, we have a fallback mechanism known as "ifpc". IFPC
+ * exploits the fact that while same-origin policy prohibits a frame from
+ * accessing members on a window not in the same domain, that frame can,
+ * however, navigate the window heirarchy (via parent). This is exploited
by
+ * having a page on domain A that wants to talk to domain B create an
iframe
+ * on domain B pointing to a special relay file and with a message encoded
+ * after the hash (#). This relay, in turn, finds the page on domain B, and
+ * can call a receipt function with the message given to it. The relay URL
+ * used by each caller is set via the gadgets.rpc.setRelayUrl(..) and
+ * *must* be called before the call method is used.
+ *
+ * ifpc: Iframe-based method, utilizing a relay page, to send a message.
+ */
+ function getRelayChannel() {
+ return typeof window.postMessage === 'function' ? 'wpm' :
+ typeof document.postMessage === 'function' ? 'dpm' :
+ navigator.product === 'Gecko' ? 'fe' :
+ 'ifpc';
}
- // Default RPC handler
- services[''] = function() {
+ /**
+ * Conducts any initial global work necessary to setup the
+ * channel type chosen.
+ */
+ function setupChannel() {
+ // If the channel type is one of the native
+ // postMessage based ones, setup the handler to receive
+ // messages.
+ if (relayChannel === 'dpm' || relayChannel === 'wpm') {
+ window.addEventListener('message', function(packet) {
+ // TODO validate packet.domain for security reasons
+ process(gadgets.json.parse(packet.data));
+ }, false);
+ }
+ }
+
+ // Pick the most efficient RPC relay mechanism
+ var relayChannel = getRelayChannel();
+
+ // Conduct any setup necessary for the chosen channel.
+ setupChannel();
+
+ // Create the Default RPC handler.
+ services[DEFAULT_NAME] = function() {
throw new Error('Unknown RPC service: ' + this.s);
};
- // Special RPC handler for callbacks
- services['__cb'] = function(callbackId, result) {
+ // Create a Special RPC handler for callbacks.
+ services[CALLBACK_NAME] = function(callbackId, result) {
var callback = callbacks[callbackId];
if (callback) {
delete callbacks[callbackId];
@@ -66,6 +163,33 @@
};
/**
+ * Conducts any frame-specific work necessary to setup
+ * the channel type chosen. This method is called when
+ * the container page first registers the gadget in the
+ * RPC mechanism. Gadgets, in turn, will complete the setup
+ * of the channel once they send their first messages.
+ */
+ function setupFrame(frameId) {
+ if (setup[frameId]) {
+ return;
+ }
+
+ if (relayChannel === 'fe') {
+ try {
+ var frame = document.getElementById(frameId);
+ frame[FE_G2C_CHANNEL] = function(args) {
+ process(gadgets.json.parse(args));
+ };
+ } catch (e) {
+ // Something went wrong. System will fallback to
+ // IFPC.
+ }
+ }
+
+ setup[frameId] = true;
+ }
+
+ /**
* Encodes arguments for the legacy IFPC wire format.
*
* @param {Object} args
@@ -86,8 +210,17 @@
* @private
*/
function process(rpc) {
+ //
+ // RPC object contents:
+ // s: Service Name
+ // f: From
+ // c: The callback ID or 0 if none.
+ // a: The arguments for this RPC call.
+ // t: The authentication token.
+ //
if (rpc && typeof rpc.s === 'string' && typeof rpc.f === 'string' &&
rpc.a instanceof Array) {
+
// Validate auth token.
if (authToken[rpc.f]) {
// We allow type coercion here because all the url params are strings.
@@ -96,44 +229,105 @@
}
}
- // The Gecko engine used by FireFox etc. allows an IFrame to directly
call
- // methods on the frameElement property added by the container page even
- // if their domains don't match.
- // Here we try to set up a relay channel using the frameElement technique
- // to greatly reduce the latency of cross-domain calls if the postMessage
- // method is not supported.
- if (relayChannel === 'ifpc') {
- if (rpc.f === '..') {
- // Container-to-gadget call
- try {
- var fel = window.frameElement;
- if (typeof fel.__g2c_rpc === 'function' &&
- typeof fel.__g2c_rpc.__c2g_rpc != 'function') {
- fel.__g2c_rpc.__c2g_rpc = function(args) {
- process(gadgets.json.parse(args));
- };
- }
- } catch (e) {
- }
- } else {
- // Gadget-to-container call
- var iframe = document.getElementById(rpc.f);
- if (iframe && typeof iframe.__g2c_rpc != 'function') {
- iframe.__g2c_rpc = function(args) {
+ // Call the requested RPC service.
+ var result = (services[rpc.s] ||
+ services[DEFAULT_NAME]).apply(rpc, rpc.a);
+
+ // If there is a callback for this service, initiate it as well.
+ if (rpc.c) {
+ gadgets.rpc.call(rpc.f, CALLBACK_NAME, null, rpc.c, result);
+ }
+ }
+ }
+
+ /**
+ * Attempts to conduct an RPC call to the specified
+ * target with the specified data via the FrameElement
+ * method. If this method fails, the system attempts again
+ * using the known default of IFPC.
+ *
+ * @param {String} targetId Module Id of the RPC service provider.
+ * @param {String} from Module Id of the calling provider.
+ * @param {Object} rpcData The RPC data for this call.
+ */
+ function callFrameElement(targetId, from, rpcData) {
+ try {
+ if (from != '..') {
+ // Call from gadget to the container.
+ var fe = window.frameElement;
+
+ if (typeof fe[FE_G2C_CHANNEL] === 'function') {
+ // Complete the setup of the FE channel if need be.
+ if (typeof fe[FE_G2C_CHANNEL][FE_C2G_CHANNEL] !== 'function') {
+ fe[FE_G2C_CHANNEL][FE_C2G_CHANNEL] = function(args) {
process(gadgets.json.parse(args));
};
}
+
+ // Conduct the RPC call.
+ fe[FE_G2C_CHANNEL](rpcData);
+ return;
}
- }
+ } else {
+ // Call from container to gadget[targetId].
+ var frame = document.getElementById(targetId);
- var result = (services[rpc.s] || services['']).apply(rpc, rpc.a);
- if (rpc.c) {
- gadgets.rpc.call(rpc.f, '__cb', null, rpc.c, result);
+ if (typeof frame[FE_G2C_CHANNEL] === 'function' &&
+ typeof frame[FE_G2C_CHANNEL][FE_C2G_CHANNEL] === 'function') {
+
+ // Conduct the RPC call.
+ frame[FE_G2C_CHANNEL][FE_C2G_CHANNEL](rpcData);
+ return;
+ }
}
+ } catch (e) {
}
+
+ // If we have reached this point, something has failed
+ // with the FrameElement method, so we default to using
+ // IFPC for this call.
+ callIfpc(targetId, from, rpcData);
}
/**
+ * Conducts an RPC call to the specified
+ * target with the specified data via the IFPC
+ * method.
+ *
+ * @param {String} targetId Module Id of the RPC service provider.
+ * @param {String} from Module Id of the calling provider.
+ * @param {Object} rpcData The RPC data for this call.
+ */
+ function callIfpc(targetId, from, rpcData) {
+ // Retrieve the relay file used by IFPC. Note that
+ // this must be set before the call, and so we conduct
+ // an extra check to ensure it is not blank.
+ var relay = gadgets.rpc.getRelayUrl(targetId);
+
+ if (!relay) {
+ throw new Error('No relay file assigned for IFPC');
+ }
+
+ // The RPC mechanism supports two formats for IFPC (legacy and current).
+ var src = null;
+ if (useLegacyProtocol[targetId]) {
+ // Format: #iframe_id&callId&num_packets&packet_num&block_of_data
+ src = [relay, '#', encodeLegacyData([from, callId, 1, 0,
+ encodeLegacyData([from, serviceName, '', '', from].concat(
+ Array.prototype.slice.call(arguments, 3)))])].join('');
+ } else {
+ // Format: #targetId & [EMAIL PROTECTED] & packetNum & packetId &
packetData
+ src = [relay, '#', targetId, '&', from, '@', callId,
+ '&1&0&', encodeURIComponent(rpcData)].join('');
+ }
+
+ // Conduct the IFPC call by creating the Iframe with
+ // the relay URL and appended message.
+ emitInvisibleIframe(src);
+ }
+
+
+ /**
* Helper function to emit an invisible IFrame.
* @param {String} src SRC attribute of the IFrame to emit.
* @private
@@ -221,6 +415,15 @@
* @member gadgets.rpc
*/
register: function(serviceName, handler) {
+ if (serviceName == CALLBACK_NAME) {
+ throw new Error("Cannot overwrite callback service");
+ }
+
+ if (serviceName == DEFAULT_NAME) {
+ throw new Error("Cannot overwrite default service:"
+ + " use registerDefault");
+ }
+
services[serviceName] = handler;
},
@@ -231,6 +434,15 @@
* @member gadgets.rpc
*/
unregister: function(serviceName) {
+ if (serviceName == CALLBACK_NAME) {
+ throw new Error("Cannot delete callback service");
+ }
+
+ if (serviceName == DEFAULT_NAME) {
+ throw new Error("Cannot delete default service:"
+ + " use unregisterDefault");
+ }
+
delete services[serviceName];
},
@@ -272,12 +484,14 @@
if (callback) {
callbacks[callId] = callback;
}
- var from;
+
+ // Default to the container calling.
+ var from = '..';
+
if (targetId === '..') {
from = window.name;
- } else {
- from = '..';
}
+
// Not used by legacy, create it anyway...
var rpcData = gadgets.json.stringify({
s: serviceName,
@@ -287,53 +501,33 @@
t: authToken[targetId]
});
+ var channelType = relayChannel;
+
+ // If we are told to use the legacy format, then we must
+ // default to IFPC.
if (useLegacyProtocol[targetId]) {
- relayChannel = 'ifpc';
+ channelType = 'ifpc';
}
- switch (relayChannel) {
- case 'dpm': // use document.postMessage
- var targetDoc = targetId === '..' ? parent.document :
- frames[targetId].document;
- targetDoc.postMessage(rpcData);
- break;
- case 'wpm': // use window.postMessage
- var targetWin = targetId === '..' ? parent : frames[targetId];
- targetWin.postMessage(rpcData, "*");
- break;
- default: // use 'ifpc' as a fallback mechanism
- var relay = gadgets.rpc.getRelayUrl(targetId);
- // TODO split message if too long
- var src;
- if (useLegacyProtocol[targetId]) {
- // #iframe_id&callId&num_packets&packet_num&block_of_data
- src = [relay, '#', encodeLegacyData([from, callId, 1, 0,
- encodeLegacyData([from, serviceName, '', '', from].concat(
- Array.prototype.slice.call(arguments, 3)))])].join('');
- } else {
- // Try the frameElement channel if available
- try {
- if (from === '..') {
- // Container-to-gadget
- var iframe = document.getElementById(targetId);
- if (typeof iframe.__g2c_rpc.__c2g_rpc === 'function') {
- iframe.__g2c_rpc.__c2g_rpc(rpcData);
- return;
- }
- } else {
- // Gadget-to-container
- if (typeof window.frameElement.__g2c_rpc === 'function') {
- window.frameElement.__g2c_rpc(rpcData);
- return;
- }
- }
- } catch (e) {
- }
- // # targetId & [EMAIL PROTECTED] & packetNum & packetId & packetData
- src = [relay, '#', targetId, '&', from, '@', callId,
- '&1&0&', encodeURIComponent(rpcData)].join('');
- }
- emitInvisibleIframe(src);
+ switch (channelType) {
+ case 'dpm': // use document.postMessage.
+ var targetDoc = targetId === '..' ? parent.document :
+ frames[targetId].document;
+ targetDoc.postMessage(rpcData);
+ break;
+
+ case 'wpm': // use window.postMessage.
+ var targetWin = targetId === '..' ? parent : frames[targetId];
+ targetWin.postMessage(rpcData, "*");
+ break;
+
+ case 'fe': // use FrameElement.
+ callFrameElement(targetId, from, rpcData);
+ break;
+
+ default: // use 'ifpc' as a fallback mechanism.
+ callIfpc(targetId, from, rpcData);
+ break;
}
},
@@ -372,15 +566,13 @@
*/
setAuthToken: function(targetId, token) {
authToken[targetId] = token;
+ setupFrame(targetId);
},
/**
* Gets the RPC relay mechanism.
- * @return {String} RPC relay mechanism. Supported types:
- * 'wpm' - Use window.postMessage (defined by HTML5)
- * 'dpm' - Use document.postMessage (defined by an early
- * draft of HTML5 and implemented by Opera)
- * 'ifpc' - Use invisible IFrames
+ * @return {String} RPC relay mechanism. See above for
+ * a list of supported types.
*
* @member gadgets.rpc
*/