Author: johnh
Date: Fri May 8 18:16:20 2009
New Revision: 773059
URL: http://svn.apache.org/viewvc?rev=773059&view=rev
Log:
rpc.js-related improvements.
1. Implementation of RMR, a new transport for all browsers currently using IFPC
(notably Chrome < 2 and Safari < 4). This transport requires no active relay,
instead constructing one from the receiver's protocol://host:port + robots.txt.
See code and SHINDIG-1050, which this resolves, for details.
2. Improvements to rpctest harness for testing rpc functionality.
3. Removing window['console']['log'] hooks in favor of standard gadgets.log
functions.
4. Improving sameDomain transport code to do string matching rather than using
exception handling for control flow.
Modified:
incubator/shindig/trunk/features/src/main/javascript/features/rpc/rpc.js
incubator/shindig/trunk/javascript/container/rpctest_container.html
incubator/shindig/trunk/javascript/container/rpctest_gadget.html
incubator/shindig/trunk/javascript/container/rpctest_perf.js
Modified:
incubator/shindig/trunk/features/src/main/javascript/features/rpc/rpc.js
URL:
http://svn.apache.org/viewvc/incubator/shindig/trunk/features/src/main/javascript/features/rpc/rpc.js?rev=773059&r1=773058&r2=773059&view=diff
==============================================================================
--- incubator/shindig/trunk/features/src/main/javascript/features/rpc/rpc.js
(original)
+++ incubator/shindig/trunk/features/src/main/javascript/features/rpc/rpc.js
Fri May 8 18:16:20 2009
@@ -16,7 +16,6 @@
* specific language governing permissions and limitations under the License.
*/
-/*global gadgets, console */
/**
* @fileoverview Remote procedure call library for gadget-to-container,
* container-to-gadget, and gadget-to-gadget (thru container) communication.
@@ -33,6 +32,7 @@
// General constants.
var CALLBACK_NAME = '__cb';
var DEFAULT_NAME = '';
+ var ACK_SVC_NAME = '__ack';
// Consts for FrameElement.
var FE_G2C_CHANNEL = '__g2c_rpc';
@@ -47,12 +47,23 @@
var NIX_HANDLE_MESSAGE = 'GRPC____NIXVBS_handle_message';
var NIX_CREATE_CHANNEL = 'GRPC____NIXVBS_create_channel';
+ // Consts for RMR, including time in ms RMR uses to poll for
+ // its relay frame to be created, and the max # of polls it does.
+ var RMR_SEARCH_TIMEOUT = 500;
+ var RMR_MAX_POLLS = 10;
+
// JavaScript reference to the NIX VBScript wrappers.
// Gadgets will have but a single channel under
// nix_channels['..'] while containers will have a channel
// per gadget stored under the gadget's ID.
var nix_channels = {};
+ // JavaScript references to the channel objects used by RMR.
+ // Gadgets will have but a single channel under
+ // rmr_channels['..'] while containers will have a channel
+ // per gadget stored under the gadget's ID.
+ var rmr_channels = {};
+
var services = {};
var iframePool = [];
var relayUrl = {};
@@ -63,7 +74,6 @@
var setup = {};
var sameDomain = {};
var params = {};
- var relayChannel;
// Load the authentication token for speaking to the container
// from the gadget's parameters, or default to '0' if not found.
@@ -164,6 +174,27 @@
* fe: Gecko-specific frameElement trick.
* - Firefox 1+
*
+ * + For WebKit-based browsers, the security model does not allow for any
+ * known "native" hacks for conducting cross browser communication.
However,
+ * a variation of the IFPC (see below) can be used, entitled "RMR". RMR is
+ * a technique that uses the resize event of the iframe to indicate that a
+ * message was sent (instead of the much slower/performance heavy polling
+ * technique used when a defined relay page is not avaliable). Simply put,
+ * RMR uses the same "pass the message by the URL hash" trick that IFPC
+ * uses to send a message, but instead of having an active relay page that
+ * runs a piece of code when it is loaded, RMR merely changes the URL
+ * of the relay page (which does not even have to exist on the domain)
+ * and then notifies the other party by resizing the relay iframe. RMR
+ * exploits the fact that iframes in the dom of page A can be resized
+ * by page A while the onresize event will be fired in the DOM of page B,
+ * thus providing a single bit channel indicating "message sent to you".
+ * This method has the added benefit that the relay need not be active,
+ * nor even exist: a 404 suffices just as well.
+ *
+ * rmr: WebKit-specific resizing trick.
+ * - Safari 2+
+ * - Chrome 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,
@@ -176,18 +207,17 @@
* *must* be called before the call method is used.
*
* ifpc: Iframe-based method, utilizing a relay page, to send a message.
+ * - No known major browsers still use this method. Slated for
removal.
*/
function getRelayChannel() {
return typeof window.postMessage === 'function' ? 'wpm' :
typeof document.postMessage === 'function' ? 'dpm' :
window.ActiveXObject ? 'nix' :
+ navigator.userAgent.indexOf('WebKit') > 0 ? 'rmr' :
navigator.product === 'Gecko' ? 'fe' :
'ifpc';
}
- // Pick the most efficient RPC relay mechanism
- relayChannel = getRelayChannel();
-
/**
* Helper function to process an RPC request
* @param {Object} rpc RPC request object
@@ -367,15 +397,65 @@
}
}
+ /**
+ * Helper method returning a canonicalized schema://host[:port] for
+ * a given input URL, provided as a string. Used to compute convenient
+ * relay URLs and to determine whether a call is coming from the same
+ * domain as its receiver (bypassing the try/catch capability detection
+ * flow, thereby obviating Firebug and other tools reporting an exception).
+ *
+ * @param {string} url Base URL to canonicalize.
+ */
+ function getDomainRoot(url) {
+ if (!url) {
+ return "";
+ }
+ url = url.toLowerCase();
+ if (url.indexOf("//") == 0) {
+ url = window.location.protocol + ":" + url;
+ }
+ if (url.indexOf("http://") != 0 &&
+ url.indexOf("https://") != 0) {
+ // Assumed to be schemaless. Default to current protocol.
+ url = window.location.protocol + "://" + url;
+ }
+ // At this point we guarantee that "://" is in the URL and defines
+ // current protocol. Skip past this to search for host:port.
+ var host = url.substring(url.indexOf("://") + 3);
+
+ // Find the first slash char, delimiting the host:port.
+ var slashPos = host.indexOf("/");
+ if (slashPos != -1) {
+ host = host.substring(0, slashPos);
+ }
+
+ var protocol = url.substring(0, url.indexOf("://"));
+
+ // Use port only if it's not default for the protocol.
+ var portStr = "";
+ var portPos = host.indexOf(":");
+ if (portPos != -1) {
+ var port = host.substring(portPos + 1);
+ host = host.substring(0, portPos);
+ if ((protocol === "http" && port !== "80") ||
+ (protocol === "https" && port !== "443")) {
+ portStr = ":" + port;
+ }
+ }
+
+ // Return <protocol>://<host>[<port>]
+ return protocol + "://" + host + portStr;
+ }
+
+ // 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() {
- if (window['console'] && window['console']['log']) {
- window['console']['log']('Unknown RPC service: ' + this.s);
- }
+ gadgets.warn('Unknown RPC service: ' + this.s);
};
// Create a Special RPC handler for callbacks.
@@ -395,32 +475,36 @@
* of the channel once they send their first messages.
*/
function setupFrame(frameId, token) {
- var frame; // used below
-
if (setup[frameId]) {
return;
}
if (relayChannel === 'fe') {
try {
- frame = document.getElementById(frameId);
+ var frame = document.getElementById(frameId);
frame[FE_G2C_CHANNEL] = function(args) {
process(gadgets.json.parse(args));
};
} catch (e1) {
- // Something went wrong. System will fallback to
- // IFPC.
+ // Something went wrong. System will fallback to IFPC.
}
}
if (relayChannel === 'nix') {
try {
- frame = document.getElementById(frameId);
+ var frame = document.getElementById(frameId);
var wrapper = window[NIX_GET_WRAPPER](frameId, token);
frame.contentWindow.opener = wrapper;
} catch (e2) {
- // Something went wrong. System will fallback to
- // IFPC.
+ // Something went wrong. System will fallback to IFPC.
+ }
+ }
+
+ if (relayChannel === 'rmr') {
+ try {
+ setupRmr(frameId);
+ } catch (e3) {
+ // Something went wrong. System will fallback to IFPC.
}
}
@@ -428,103 +512,482 @@
}
/**
- * Encodes arguments for the legacy IFPC wire format.
+ * Append an RMR relay frame to the document. This allows the receiver
+ * to start receiving messages.
*
- * @param {Object} args
- * @return {String} the encoded args
+ * @param {object} channelFrame Relay frame to add to the DOM body.
+ * @param {string} relayUri Base URI for the frame.
+ * @param {string} Data to pass along to the frame.
*/
- function encodeLegacyData(args) {
- var stringify = gadgets.json.stringify;
- var argsEscaped = [];
- for(var i = 0, j = args.length; i < j; ++i) {
- argsEscaped.push(encodeURIComponent(stringify(args[i])));
+ function appendRmrFrame(channelFrame, relayUri, data) {
+ var appendFn = function() {
+ // Append the iframe.
+ document.body.appendChild(channelFrame);
+
+ // Set the src of the iframe to 'about:blank' first and then set it
+ // to the relay URI. This prevents the iframe from maintaining a src
+ // to the 'old' relay URI if the page is returned to from another.
+ // In other words, this fixes the bfcache issue that causes the iframe's
+ // src property to not be updated despite us assigning it a new value
here.
+ channelFrame.src = 'about:blank';
+ channelFrame.src = relayUri + '#' + data;
+ }
+
+ if (document.body) {
+ appendFn();
+ } else {
+ // Common gadget case: attaching header during in-gadget handshake,
+ // when we may still be in script in head. Attach onload.
+ if (gadgets && gadgets.util && gadgets.util.registerOnLoadHandler) {
+ gadgets.util.registerOnLoadHandler(function() { appendFn(); });
+ } else {
+ var oldOnloadFn = window.onload;
+ window.onload = function () {
+ oldOnloadFn();
+ appendFn();
+ }
+ }
}
- return argsEscaped.join('&');
}
/**
- * Helper function to emit an invisible IFrame.
- * @param {String} src SRC attribute of the IFrame to emit.
- * @private
+ * Sets up the RMR transport frame for the given frameId. For gadgets
+ * calling containers, the frameId should be '..'.
+ *
+ * @param {string} frameId The ID of the frame.
*/
- function emitInvisibleIframe(src) {
- var iframe;
- // Recycle IFrames
- for (var i = iframePool.length - 1; i >=0; --i) {
- var ifr = iframePool[i];
+ function setupRmr(frameId) {
+ if (typeof rmr_channels[frameId] === "object") {
+ // Sanity check. Already done.
+ return;
+ }
+
+ var channelFrame = document.createElement('iframe');
+ var frameStyle = channelFrame.style;
+ frameStyle.position = 'absolute';
+ frameStyle.top = '0px';
+ frameStyle.border = '0';
+ frameStyle.opacity = '0';
+
+ // The width here is important as RMR
+ // makes use of the resize handler for the frame.
+ // Do not modify unless you test thoroughly!
+ frameStyle.width = '10px'
+ frameStyle.height = '1px';
+ channelFrame.id = 'rmrtransport-' + frameId;
+ channelFrame.name = channelFrame.id;
+
+ // Determine the relay uri by taking the existing one,
+ // removing the path and appending robots.txt. It is
+ // not important if robots.txt actually exists, since RMR
+ // browsers treat 404s as legitimate for the purposes of
+ // this communication.
+ var relayUri =
+ getDomainRoot(relayUrl[frameId]) + '/robots.txt';
+
+ rmr_channels[frameId] = {
+ frame: channelFrame,
+ receiveWindow: null,
+ relayUri: relayUri,
+ searchCounter : 0,
+ width: 10,
+
+ // Waiting means "waiting for acknowledgement to be received."
+ // Acknowledgement always comes as a special ACK_SVC_NAME
+ // message having been received. This message is received
+ // during handshake in different ways by the container and
+ // gadget, and by normal RMR message passing once the handshake
+ // is complete.
+ waiting: true,
+ queue: [],
+
+ // Number of non-ACK messages that have been sent to the recipient
+ // and have been acknowledged.
+ sendId: 0,
+
+ // Number of messages received and processed from the sender.
+ // This is the number that accompanies every ACK to tell the
+ // sender to clear its queue.
+ recvId: 0,
+ };
+
+ if (frameId !== '..') {
+ // Container always appends a relay to the gadget, before
+ // the gadget appends its own relay back to container. The
+ // gadget, in the meantime, refuses to attach the container
+ // relay until it finds this one. Thus, the container knows
+ // for certain that gadget to container communication is set
+ // up by the time it finds its own relay. In addition to
+ // establishing a reliable handshake protocol, this also
+ // makes it possible for the gadget to send an initial batch
+ // of messages to the container ASAP.
+ appendRmrFrame(channelFrame, relayUri, getRmrData(frameId));
+ }
+
+ // Start searching for our own frame on the other page.
+ conductRmrSearch(frameId);
+ }
+
+ /**
+ * Searches for a relay frame, created by the sender referenced by
+ * frameId, with which this context receives messages. Once
+ * found with proper permissions, attaches a resize handler which
+ * signals messages to be sent.
+ *
+ * @param {string} frameId Frame ID of the prospective sender.
+ */
+ function conductRmrSearch(frameId) {
+ var channelWindow = null;
+
+ // Increment the search counter.
+ rmr_channels[frameId].searchCounter++;
+
+ if (frameId === '..') {
+ // We are a gadget.
+ channelWindow = window.parent.frames['rmrtransport-' + window.name];
+ } else {
+ // We are a container.
+ channelWindow = window.frames[frameId].frames['rmrtransport-..'];
+ }
+
+ var status = false;
+
+ if (channelWindow) {
+ // We have a valid reference to "our" RMR transport frame.
+ // Register the proper event handlers.
+ status = registerRmrChannel(frameId, channelWindow);
+ }
+
+ if (!status) {
+ // Not found yet. Continue searching, but only if the counter
+ // has not reached the threshold.
+ if (rmr_channels[frameId].searchCounter > RMR_MAX_POLLS) {
+ // If we reach this point, then RMR has failed and we
+ // fall back to IFPC.
+ return;
+ }
+
+ setTimeout(function() {
+ conductRmrSearch(frameId);
+ }, RMR_SEARCH_TIMEOUT);
+ }
+ }
+
+ /**
+ * Attempts to conduct an RPC call to the specified
+ * target with the specified data via the RMR
+ * 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} serviceName Name of the service to call.
+ * @param {String} from Module Id of the calling provider.
+ * @param {Object} rpc The RPC data for this call.
+ */
+ function callRmr(targetId, serviceName, from, rpc) {
+ var handler = null;
+
+ if (from !== '..') {
+ // Call from gadget to the container.
+ handler = rmr_channels['..'];
+ } else {
+ // Call from container to the gadget.
+ handler = rmr_channels[targetId];
+ }
+
+ if (handler) {
+ // Queue the current message if not ACK.
+ // ACK is always sent through getRmrData(...).
+ if (serviceName !== ACK_SVC_NAME) {
+ handler.queue.push(rpc);
+ }
+
+ if (handler.waiting ||
+ (handler.queue.length === 0 &&
+ !(serviceName === ACK_SVC_NAME && rpc && rpc.ackAlone === true))) {
+ // If we are awaiting a response from any previously-sent messages,
+ // or if we don't have anything new to send, just return.
+ // Note that we don't short-return if we're ACKing just-received
+ // messages.
+ return;
+ }
+
+ if (handler.queue.length > 0) {
+ handler.waiting = true;
+ }
+
+ var url = handler.relayUri + "#" + getRmrData(targetId);
+
try {
- if (ifr && (ifr.recyclable || ifr.readyState === 'complete')) {
- ifr.parentNode.removeChild(ifr);
- if (window.ActiveXObject) {
- // For MSIE, delete any iframes that are no longer being used. MSIE
- // cannot reuse the IFRAME because a navigational click sound will
- // be triggered when we set the SRC attribute.
- // Other browsers scan the pool for a free iframe to reuse.
- iframePool[i] = ifr = null;
- iframePool.splice(i, 1);
- } else {
- ifr.recyclable = false;
- iframe = ifr;
- break;
- }
- }
+ // Update the URL with the message.
+ handler.frame.contentWindow.location = url;
+
+ // Resize the frame.
+ var newWidth = handler.width == 10 ? 20 : 10;
+ handler.frame.style.width = newWidth + 'px';
+ handler.width = newWidth;
+
+ // Done.
+ return;
} catch (e) {
- // Ignore; IE7 throws an exception when trying to read readyState and
- // readyState isn't set.
+ // Something about location-setting or resizing failed.
+ // This should never happen, but if it does, fall back to
+ // IFPC for now.
}
}
- // Create IFrame if necessary
- if (!iframe) {
- iframe = document.createElement('iframe');
- iframe.style.border = iframe.style.width = iframe.style.height = '0px';
- iframe.style.visibility = 'hidden';
- iframe.style.position = 'absolute';
- iframe.onload = function() { this.recyclable = true; };
- iframePool.push(iframe);
+
+ // If we have reached this point, something has failed
+ // with the RMR method, so we default to using
+ // IFPC for this call.
+ callIfpc(targetId, serviceName, from, gadgets.json.stringify(rpc));
+ }
+
+ /**
+ * Returns as a string the data to be appended to an RMR relay frame,
+ * constructed from the current request queue plus an ACK message indicating
+ * the currently latest-processed message ID.
+ *
+ * @param {string} toFrameId Frame whose sendable queued data to retrieve.
+ */
+ function getRmrData(toFrameId) {
+ var channel = rmr_channels[toFrameId];
+ var rmrData = {id: channel.sendId};
+ if (channel) {
+ rmrData.d = Array.prototype.slice.call(channel.queue, 0);
+ rmrData.d.push({s:ACK_SVC_NAME, id:channel.recvId});
}
- iframe.src = src;
- setTimeout(function() { document.body.appendChild(iframe); }, 0);
+ return gadgets.json.stringify(rmrData);
}
/**
- * Conducts an RPC call to the specified
- * target with the specified data via the IFPC
- * method.
+ * Retrieve data from the channel keyed by the given frameId,
+ * processing it as a batch. All processed data is assumed to have been
+ * generated by getRmrData(...), pairing that method with this.
*
- * @param {String} targetId Module Id of the RPC service provider.
- * @param {String} serviceName Service name to call.
- * @param {String} from Module Id of the calling provider.
- * @param {Object} rpcData The RPC data for this call.
- * @param {Array.<Object>} callArgs Original arguments to call()
+ * @param {string} fromFrameId Frame from which data is being retrieved.
*/
- function callIfpc(targetId, serviceName, from, rpcData, callArgs) {
- // 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);
+ function processRmrData(fromFrameId) {
+ var channel = rmr_channels[fromFrameId];
+ var data = channel.receiveWindow.location.hash.substring(1);
+
+ // Decode the RPC object array.
+ var rpcObj = gadgets.json.parse(decodeURIComponent(data)) || {};
+ var rpcArray = rpcObj.d || [];
+
+ var nonAckReceived = false;
+ var noLongerWaiting = false;
+
+ var numBypassed = 0;
+ var numToBypass = (channel.recvId - rpcObj.id);
+ for (var i = 0; i < rpcArray.length; ++i) {
+ var rpc = rpcArray[i];
+
+ // If we receive an ACK message, then mark the current
+ // handler as no longer waiting and send out the next
+ // queued message.
+ if (rpc.s === ACK_SVC_NAME) {
+ if (channel.waiting) {
+ noLongerWaiting = true;
+ }
- if (!relay) {
- if (window['console'] && window['console']['log']) {
- window['console']['log']('No relay file assigned for IFPC');
+ channel.waiting = false;
+ var newlyAcked = Math.max(0, rpc.id - channel.sendId);
+ channel.queue.splice(0, newlyAcked);
+ channel.sendId = Math.max(channel.sendId, rpc.id || 0);
+ continue;
+ }
+
+ // If we get here, we've received > 0 non-ACK messages to
+ // process. Indicate this bit for later.
+ nonAckReceived = true;
+
+ // Bypass any messages already received.
+ if (++numBypassed <= numToBypass) {
+ continue;
}
+
+ ++channel.recvId;
+ process(rpc); // actually dispatch the message
}
- // 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(
- callArgs))])].join('');
- } else {
- // Format: #targetId & sourc...@callid & packetNum & packetId &
packetData
- src = [relay, '#', targetId, '&', from, '@', callId,
- '&1&0&', encodeURIComponent(rpcData)].join('');
+ // Send an ACK indicating that we got/processed the message(s).
+ // Do so if we've received a message to process or if we were waiting
+ // before but a received ACK has cleared our waiting bit, and we have
+ // more messages to send. Performing this operation causes additional
+ // messages to be sent.
+ if (nonAckReceived ||
+ (noLongerWaiting && channel.queue.length > 0)) {
+ var from = (fromFrameId === '..') ? window.name : '..';
+ callRmr(fromFrameId, ACK_SVC_NAME, from, {ackAlone: nonAckReceived});
}
+ }
- // Conduct the IFPC call by creating the Iframe with
- // the relay URL and appended message.
- emitInvisibleIframe(src);
+ /**
+ * Registers the RMR channel handler for the given frameId and associated
+ * channel window.
+ *
+ * @param {string} frameId The ID of the frame for which this channel is
being
+ * registered.
+ * @param {Object} channelWindow The window of the receive frame for this
+ * channel, if any.
+ *
+ * @return {boolean} True if the frame was setup successfully, false
+ * otherwise.
+ */
+ function registerRmrChannel(frameId, channelWindow) {
+ var channel = rmr_channels[frameId];
+
+ // Verify that the channel is ready for receiving.
+ try {
+ var canAccess = false;
+
+ // Check to see if the document is in the window. For Chrome, this
+ // will return 'false' if the channelWindow is inaccessible by this
+ // piece of JavaScript code, meaning that the URL of the channelWindow's
+ // parent iframe has not yet changed from 'about:blank'. We do this
+ // check this way because any true *access* on the channelWindow object
+ // will raise a security exception, which, despite the try-catch, still
+ // gets reported to the debugger (it does not break execution, the try
+ // handles that problem, but it is still reported, which is bad form).
+ // This check always succeeds in Safari 3.1 regardless of the state of
+ // the window.
+ canAccess = 'document' in channelWindow;
+
+ if (!canAccess) {
+ return false;
+ }
+
+ // Check to see if the document is an object. For Safari 3.1, this will
+ // return undefined if the page is still inaccessible. Unfortunately,
this
+ // *will* raise a security issue in the debugger.
+ // TODO Find a way around this problem.
+ canAccess = typeof channelWindow['document'] == 'object';
+
+ if (!canAccess) {
+ return false;
+ }
+
+ // Once we get here, we know we can access the document (and anything
else)
+ // on the window object. Therefore, we check to see if the location is
+ // still about:blank (this takes care of the Safari 3.2 case).
+ var loc = channelWindow.location.href;
+
+ // Check if this is about:blank for Safari.
+ if (loc === 'about:blank') {
+ return false;
+ }
+ } catch (ex) {
+ // For some reason, the iframe still points to about:blank. We try
+ // again in a bit.
+ return false;
+ }
+
+ // Save a reference to the receive window.
+ channel.receiveWindow = channelWindow;
+
+ // Register the onresize handler.
+ channelWindow.onresize = function() {
+ processRmrData(frameId);
+ };
+
+ if (frameId === '..') {
+ // Gadget to container. Signal to the container that the gadget
+ // is ready to receive messages by attaching the g -> c relay.
+ // As a nice optimization, pass along any gadget to container
+ // queued messages that have backed up since then. ACK is enqueued in
+ // getRmrData to ensure that the container's waiting flag is set to false
+ // (this happens in the below code run on the container side).
+ appendRmrFrame(channel.frame, channel.relayUri, getRmrData(frameId));
+ }
+
+ // Attempt to process the messages that the other party may have set in
+ // the resize frame's hash. For gadget to container, this is the gadget
+ // finding the container-set IFRAME, which has an ACK attached to it.
+ // For gadget to container, the gadget may have enqueued messages already,
+ // and sent them in the above code. This too will have an ACK message.
+ // The net effect is that both sides of the connection will have their
+ // "waiting" bit set to false, so they're ready to send messages.
+ processRmrData(frameId);
+
+ return true;
+ }
+
+ /**
+ * Encodes arguments for the legacy IFPC wire format.
+ *
+ * @param {Object} args
+ * @return {String} the encoded args
+ */
+ function encodeLegacyData(args) {
+ var stringify = gadgets.json.stringify;
+ var argsEscaped = [];
+ for(var i = 0, j = args.length; i < j; ++i) {
+ argsEscaped.push(encodeURIComponent(stringify(args[i])));
+ }
+ return argsEscaped.join('&');
+ }
+
+ /**
+ * Helper function to process an RPC request
+ * @param {Object} rpc RPC request object
+ * @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 don't do type coercion here because all entries in the authToken
+ // object are strings, as are all url params. See setAuthToken(...).
+ if (authToken[rpc.f] !== rpc.t) {
+ throw new Error("Invalid auth token. " +
+ authToken[rpc.f] + " vs " + rpc.t);
+ }
+ }
+
+ // If there is a callback for this service, attach a callback function
+ // to the rpc context object for asynchronous rpc services.
+ //
+ // Synchronous rpc request handlers should simply ignore it and return a
+ // value as usual.
+ // Asynchronous rpc request handlers, on the other hand, should pass its
+ // result to this callback function and not return a value on exit.
+ //
+ // For example, the following rpc handler passes the first parameter back
+ // to its rpc client with a one-second delay.
+ //
+ // function asyncRpcHandler(param) {
+ // var me = this;
+ // setTimeout(function() {
+ // me.callback(param);
+ // }, 1000);
+ // }
+ if (rpc.c) {
+ rpc.callback = function(result) {
+ gadgets.rpc.call(rpc.f, CALLBACK_NAME, null, rpc.c, result);
+ };
+ }
+
+ // Call the requested RPC service.
+ var result = (services[rpc.s] ||
+ services[DEFAULT_NAME]).apply(rpc, rpc.a);
+
+ // If the rpc request handler returns a value, immediately pass it back
+ // to the callback. Otherwise, do nothing, assuming that the rpc handler
+ // will make an asynchronous call later.
+ if (rpc.c && typeof result != 'undefined') {
+ gadgets.rpc.call(rpc.f, CALLBACK_NAME, null, rpc.c, result);
+ }
+ }
}
/**
@@ -644,7 +1107,89 @@
callIfpc(targetId, serviceName, from, rpcData, callArgs);
}
+ /**
+ * 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} serviceName Service name to call.
+ * @param {String} from Module Id of the calling provider.
+ * @param {Object} rpcData The RPC data for this call.
+ * @param {Array.<Object>} callArgs Original arguments to call()
+ */
+ function callIfpc(targetId, serviceName, from, rpcData, callArgs) {
+ // 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) {
+ gadgets.warn('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(
+ callArgs))])].join('');
+ } else {
+ // Format: #targetId & sourc...@callid & 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
+ */
+ function emitInvisibleIframe(src) {
+ var iframe;
+ // Recycle IFrames
+ for (var i = iframePool.length - 1; i >=0; --i) {
+ var ifr = iframePool[i];
+ try {
+ if (ifr && (ifr.recyclable || ifr.readyState === 'complete')) {
+ ifr.parentNode.removeChild(ifr);
+ if (window.ActiveXObject) {
+ // For MSIE, delete any iframes that are no longer being used. MSIE
+ // cannot reuse the IFRAME because a navigational click sound will
+ // be triggered when we set the SRC attribute.
+ // Other browsers scan the pool for a free iframe to reuse.
+ iframePool[i] = ifr = null;
+ iframePool.splice(i, 1);
+ } else {
+ ifr.recyclable = false;
+ iframe = ifr;
+ break;
+ }
+ }
+ } catch (e) {
+ // Ignore; IE7 throws an exception when trying to read readyState and
+ // readyState isn't set.
+ }
+ }
+ // Create IFrame if necessary
+ if (!iframe) {
+ iframe = document.createElement('iframe');
+ iframe.style.border = iframe.style.width = iframe.style.height = '0px';
+ iframe.style.visibility = 'hidden';
+ iframe.style.position = 'absolute';
+ iframe.onload = function() { this.recyclable = true; };
+ iframePool.push(iframe);
+ }
+ iframe.src = src;
+ setTimeout(function() { document.body.appendChild(iframe); }, 0);
+ }
/**
* Attempts to make an rpc by calling the target's receive method directly.
@@ -660,8 +1205,14 @@
function callSameDomain(target, rpc) {
if (typeof sameDomain[target] === 'undefined') {
// Seed with a negative, typed value to avoid
- // hitting this code path repeatedly
+ // hitting this code path repeatedly.
sameDomain[target] = false;
+ var targetRelay = gadgets.rpc.getRelayUrl(target);
+ if (getDomainRoot(targetRelay) !== getDomainRoot(window.location.href)) {
+ // Not worth trying -- avoid the error and just return.
+ return false;
+ }
+
var targetEl = null;
if (target === '..') {
targetEl = parent;
@@ -672,7 +1223,8 @@
// If this succeeds, then same-domain policy applied
sameDomain[target] = targetEl.gadgets.rpc.receiveSameDomain;
} catch (e) {
- // Usual case: different domains
+ // Shouldn't happen due to domain root check. Caught just in case.
+ gadgets.warn("Unexpected: same domain call failed.");
}
}
@@ -693,14 +1245,16 @@
function init(config) {
// Allow for wild card parent relay files as long as it's from a
// white listed domain. This is enforced by the rendering servlet.
- if (config.rpc.parentRelayUrl.substring(0, 7) === 'http://') {
+ if (config.rpc.parentRelayUrl.substring(0, 7) === 'http://' ||
+ config.rpc.parentRelayUrl.substring(0, 8) === 'https://' ||
+ config.rpc.parentRelayUrl.substring(0, 2) === '//') {
relayUrl['..'] = config.rpc.parentRelayUrl;
} else {
// It's a relative path, and we must append to the parent.
// We're relying on the server validating the parent parameter in this
// case. Because of this, parent may only be passed in the query, not
// the fragment.
- var params = document.location.search.substring(0).split("&");
+ var params = document.location.search.substring(1).split("&");
var parentParam = "";
for (var i = 0, param; (param = params[i]); ++i) {
// Only the first parent can be validated.
@@ -714,6 +1268,13 @@
// code to ignore rpc calls since they cannot work without a
// relay URL with host qualification.
relayUrl['..'] = parentParam + config.rpc.parentRelayUrl;
+
+ // RMR requires active gadget-to-container initialization.
+ // We assume that any rendering context where gadgets.config
+ // is defined and a parent param is provided (gets here) is a gadget.
+ if (relayChannel === 'rmr') {
+ setupRmr('..');
+ }
}
}
useLegacyProtocol['..'] = !!config.rpc.useLegacyProtocol;
@@ -734,12 +1295,14 @@
* @member gadgets.rpc
*/
register: function(serviceName, handler) {
- if (serviceName === CALLBACK_NAME) {
- throw new Error("Cannot overwrite callback service");
+ if (serviceName === CALLBACK_NAME ||
+ serviceName === ACK_SVC_NAME) {
+ throw new Error("Cannot overwrite callback/ack service");
}
if (serviceName === DEFAULT_NAME) {
- throw new Error("Cannot overwrite default service: use
registerDefault");
+ throw new Error("Cannot overwrite default service:"
+ + " use registerDefault");
}
services[serviceName] = handler;
@@ -752,12 +1315,14 @@
* @member gadgets.rpc
*/
unregister: function(serviceName) {
- if (serviceName === CALLBACK_NAME) {
- throw new Error("Cannot delete callback service");
+ if (serviceName === CALLBACK_NAME ||
+ serviceName === ACK_SVC_NAME) {
+ throw new Error("Cannot delete callback/ack service");
}
if (serviceName === DEFAULT_NAME) {
- throw new Error("Cannot delete default service: use
unregisterDefault");
+ throw new Error("Cannot delete default service:"
+ + " use unregisterDefault");
}
delete services[serviceName];
@@ -791,7 +1356,7 @@
* At present this means IFPC or WPM.
*/
forceParentVerifiable: function() {
- if (relayChannel !== 'wpm') {
+ if (relayChannel !== 'wpm' && relayChannel !== 'rmr') {
relayChannel = 'ifpc';
}
},
@@ -836,7 +1401,6 @@
}
var rpcData = gadgets.json.stringify(rpc);
-
var channelType = relayChannel;
// If we are told to use the legacy format, then we must
@@ -868,6 +1432,10 @@
callFrameElement(targetId, serviceName, from, rpcData, rpc.a);
break;
+ case 'rmr': // use RMR.
+ callRmr(targetId, serviceName, from, rpc);
+ break;
+
default: // use 'ifpc' as a fallback mechanism.
callIfpc(targetId, serviceName, from, rpcData, rpc.a);
break;
@@ -965,4 +1533,3 @@
}
};
}();
-
Modified: incubator/shindig/trunk/javascript/container/rpctest_container.html
URL:
http://svn.apache.org/viewvc/incubator/shindig/trunk/javascript/container/rpctest_container.html?rev=773059&r1=773058&r2=773059&view=diff
==============================================================================
--- incubator/shindig/trunk/javascript/container/rpctest_container.html
(original)
+++ incubator/shindig/trunk/javascript/container/rpctest_container.html Fri May
8 18:16:20 2009
@@ -32,7 +32,8 @@
If your servers are on localhost:8080 and localhost:8081, then hit:
http://localhost:8080/gadgets/files/container/rpctest_container.html? \
http://localhost:8081/gadgets/files/container/rpctest_gadget.html& \
- http://localhost:8081/gadgets/files/container/rpc_relay.uncompressed.html
+ http://localhost:8081/gadgets/files/container/rpc_relay.uncompressed.html& \
+ [ http://localhost:8080/gadgets/files/container/rpc_relay.uncompressed.html ]
(Note the backslashes should be removed, as they exist for formatting only.)
@@ -53,12 +54,12 @@
// and optionally the relay URL as arg 2
var pageArgs = window.location.search.substring(1).split('&');
var gadgetUrl = pageArgs[0];
- var secret = Math.round(Math.random()*10000000);
+ var secret = Math.round(Math.random() * 10000000);
if (pageArgs[1]) {
gadgets.rpc.setRelayUrl('gadget', pageArgs[1]);
}
- var containerRelay = pageArgs[2] || '';
- container.innerHTML = "<iframe id='gadget' name='gadget' height=300
width=300 src='" + gadgetUrl + "?parent=" + containerRelay + "#rpctoken=" +
secret + "'></iframe>";
+ var containerRelay = pageArgs[2] || window.location.href;
+ container.innerHTML = "<iframe id='gadget' name='gadget' height=400
width=800 src='" + gadgetUrl + "?parent=" + containerRelay + "#rpctoken=" +
secret + "'></iframe>";
gadgets.rpc.setAuthToken('gadget', secret);
document.getElementById('relaymethod').innerHTML =
gadgets.rpc.getRelayChannel();
@@ -67,7 +68,7 @@
};
</script>
</head>
- <body onload="initTest();">
+ <body style="background-color: #cccccc" onload="initTest();">
<div>gadgets.rpc Performance: Container Page (method: <span
id="relaymethod"></span>)</div><hr/>
<div>Test<br/>
<ul>
Modified: incubator/shindig/trunk/javascript/container/rpctest_gadget.html
URL:
http://svn.apache.org/viewvc/incubator/shindig/trunk/javascript/container/rpctest_gadget.html?rev=773059&r1=773058&r2=773059&view=diff
==============================================================================
--- incubator/shindig/trunk/javascript/container/rpctest_gadget.html (original)
+++ incubator/shindig/trunk/javascript/container/rpctest_gadget.html Fri May 8
18:16:20 2009
@@ -19,6 +19,18 @@
<html>
<head>
<title>gadgets.rpc Performance Tests: Gadget</title>
+ <script>
+ // Fake a version of gadgets.config that rpc.js uses for configuration.
+ var gadgets = {};
+ gadgets.config = {
+ register: function(rpc, requiredConfig, callback) {
+ // rpc === "rpc", requiredConfig is ignored here.
+ // Just call the callback (function init(...) in rpc.js)
+ // with a dummy config object.
+ callback({ rpc: { parentRelayUrl: "" } });
+ }
+ };
+ </script>
<script src="../../js/rpc.js?c=1&debug=1"></script>
<script src="rpctest_perf.js"></script>
<script>
@@ -63,5 +75,6 @@
Messages/second: <span id="results_msgs_per_sec"></span><br/>
Bytes/second: <span id="results_bytes_per_sec"></span>
</div>
+ <script>gadgets.util.runOnLoadHandlers();</script>
</body>
</html>
Modified: incubator/shindig/trunk/javascript/container/rpctest_perf.js
URL:
http://svn.apache.org/viewvc/incubator/shindig/trunk/javascript/container/rpctest_perf.js?rev=773059&r1=773058&r2=773059&view=diff
==============================================================================
--- incubator/shindig/trunk/javascript/container/rpctest_perf.js (original)
+++ incubator/shindig/trunk/javascript/container/rpctest_perf.js Fri May 8
18:16:20 2009
@@ -1,4 +1,4 @@
-<!--
+/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
@@ -15,7 +15,7 @@
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
--->
+ */
var perfStats = null;
var currentRun = {};