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 = {};


Reply via email to