THRIFT-2355 Add SSL and Web Socket Support to Node and JavaScript
Patch: Randy Abernethy
diff --git a/lib/js/src/thrift.js b/lib/js/src/thrift.js
index d605ab7..411eead 100644
--- a/lib/js/src/thrift.js
+++ b/lib/js/src/thrift.js
@@ -22,8 +22,16 @@
 /**
  * The Thrift namespace houses the Apache Thrift JavaScript library 
  * elements providing JavaScript bindings for the Apache Thrift RPC 
- * system. Users will typically only directly make use of the 
- * Transport and Protocol constructors.
+ * system. End users will typically only directly make use of the 
+ * Transport (TXHRTransport/TWebSocketTransport) and Protocol 
+ * (TJSONPRotocol/TBinaryProtocol) constructors.
+ * 
+ * Object methods beginning with a __ (e.g. __onOpen()) are internal 
+ * and should not be called outside of the object's own methods.
+ * 
+ * This library creates one global object: Thrift
+ * Code in this library must never create additional global identifiers,
+ * all features must be scoped within the Thrift namespace.
  * @namespace
  * @example
  *     var transport = new Thrift.Transport("http://localhost:8585");
@@ -108,7 +116,6 @@
                 length++;
             }
         }
-
         return length;
     },
 
@@ -266,19 +273,20 @@
 };
 
 /**
- * Initializes a Thrift Http[s] transport instance.
- * Note: If you do not specify a url then you must handle XHR on your own
- *       (this is how to use js bindings in a async fashion).
+ * Constructor Function for the XHR transport.
+ * If you do not specify a url then you must handle XHR operations on
+ * your own. This type can also be constructed using the Transport alias
+ * for backward compatibility.
  * @constructor
  * @param {string} [url] - The URL to connect to.
- * @classdesc The Apache Thrift Transport layer performs byte level I/O between RPC
- * clients and servers. The JavaScript Transport object type uses Http[s]/XHR and is
- * the sole browser based Thrift transport. Target servers must implement the http[s] 
- * transport (see: node.js example server).
+ * @classdesc The Apache Thrift Transport layer performs byte level I/O 
+ * between RPC clients and servers. The JavaScript TXHRTransport object 
+ * uses Http[s]/XHR. Target servers must implement the http[s] transport
+ * (see: node.js example server_http.js).
  * @example
- *     var transport = new Thrift.Transport("http://localhost:8585");
+ *     var transport = new Thrift.TXHRTransport("http://localhost:8585");
  */
-Thrift.Transport = function(url) {
+Thrift.Transport = Thrift.TXHRTransport = function(url) {
     this.url = url;
     this.wpos = 0;
     this.rpos = 0;
@@ -287,7 +295,7 @@
     this.recv_buf = '';
 };
 
-Thrift.Transport.prototype = {
+Thrift.TXHRTransport.prototype = {
     /**
      * Gets the browser specific XmlHttpRequest Object.
      * @returns {object} the browser XHR interface object
@@ -301,17 +309,17 @@
     },
 
     /**
-     * Sends the current XRH request if the transport was created with a URL and
-     * the async parameter if false. If the transport was not created with a URL
-     * or the async parameter is True or the URL is an empty string, the current 
-     * send buffer is returned.
+     * Sends the current XRH request if the transport was created with a URL 
+     * and the async parameter is false. If the transport was not created with
+     * a URL, or the async parameter is True and no callback is provided, or 
+     * the URL is an empty string, the current send buffer is returned.
      * @param {object} async - If true the current send buffer is returned.
-     * @param {object} callback - Optional async callback function, receives the call result.
+     * @param {object} callback - Optional async completion callback 
      * @returns {undefined|string} Nothing or the current send buffer.
      * @throws {string} If XHR fails.
      */
     flush: function(async, callback) {
-        //async mode
+        var self = this;
         if ((async && !callback) || this.url === undefined || this.url === '') {
             return this.send_buf;
         }
@@ -323,7 +331,18 @@
         }
 
         if (callback) {
-            xreq.onreadystatechange = callback;
+            //Ignore XHR callbacks until the data arrives, then call the
+            //  client's callback
+            xreq.onreadystatechange = 
+              (function() {
+                var clientCallback = callback;    
+                return function() {
+                  if (this.readyState == 4 && this.status == 200) {
+                    self.setRecvBuffer(this.responseText);
+                    clientCallback();
+                  }
+                };
+              }());
         }
 
         xreq.open('POST', this.url, !!async);
@@ -350,7 +369,7 @@
      * Creates a jQuery XHR object to be used for a Thrift server call.
      * @param {object} client - The Thrift Service client object generated by the IDL compiler.
      * @param {object} postData - The message to send to the server.
-     * @param {function} args - The function to call if the request suceeds.
+     * @param {function} args - The original call arguments with the success call back at the end.
      * @param {function} recv_method - The Thrift Service Client receive method for the call.
      * @returns {object} A new jQuery XHR object.
      * @throws {string} If the jQuery version is prior to 1.5 or if jQuery is not found.
@@ -385,6 +404,180 @@
     },
 
     /**
+     * Sets the buffer to provide the protocol when deserializing.
+     * @param {string} buf - The buffer to supply the protocol.
+     */
+    setRecvBuffer: function(buf) {
+        this.recv_buf = buf;
+        this.recv_buf_sz = this.recv_buf.length;
+        this.wpos = this.recv_buf.length;
+        this.rpos = 0;
+    },
+
+    /**
+     * Returns true if the transport is open, XHR always returns true.
+     * @readonly
+     * @returns {boolean} Always True.
+     */    
+    isOpen: function() {
+        return true;
+    },
+
+    /**
+     * Opens the transport connection, with XHR this is a nop.
+     */    
+    open: function() {},
+
+    /**
+     * Closes the transport connection, with XHR this is a nop.
+     */    
+    close: function() {},
+
+    /**
+     * Returns the specified number of characters from the response
+     * buffer.
+     * @param {number} len - The number of characters to return.
+     * @returns {string} Characters sent by the server.
+     */
+    read: function(len) {
+        var avail = this.wpos - this.rpos;
+
+        if (avail === 0) {
+            return '';
+        }
+
+        var give = len;
+
+        if (avail < len) {
+            give = avail;
+        }
+
+        var ret = this.read_buf.substr(this.rpos, give);
+        this.rpos += give;
+
+        //clear buf when complete?
+        return ret;
+    },
+
+    /**
+     * Returns the entire response buffer.
+     * @returns {string} Characters sent by the server.
+     */
+    readAll: function() {
+        return this.recv_buf;
+    },
+
+    /**
+     * Sets the send buffer to buf.
+     * @param {string} buf - The buffer to send.
+     */    
+    write: function(buf) {
+        this.send_buf = buf;
+    },
+
+    /**
+     * Returns the send buffer.
+     * @readonly
+     * @returns {string} The send buffer.
+     */ 
+    getSendBuffer: function() {
+        return this.send_buf;
+    }
+
+};
+
+
+/**
+ * Constructor Function for the WebSocket transport.
+ * @constructor
+ * @param {string} [url] - The URL to connect to.
+ * @classdesc The Apache Thrift Transport layer performs byte level I/O 
+ * between RPC clients and servers. The JavaScript TWebSocketTransport object 
+ * uses the WebSocket protocol. Target servers must implement WebSocket.
+ * (see: node.js example server_http.js).
+ * @example
+ *   var transport = new Thrift.TWebSocketTransport("http://localhost:8585");
+ */
+Thrift.TWebSocketTransport = function(url) {
+    this.__reset(url);
+};
+
+Thrift.TWebSocketTransport.prototype = {
+    __reset: function(url) {
+      this.url = url;             //Where to connect
+      this.socket = null;         //The web socket
+      this.callbacks = [];        //Pending callbacks
+      this.send_pending = [];     //Buffers/Callback pairs waiting to be sent
+      this.send_buf = '';         //Outbound data, immutable until sent
+      this.recv_buf = '';         //Inbound data
+      this.rb_wpos = 0;           //Network write position in receive buffer
+      this.rb_rpos = 0;           //Client read position in receive buffer
+    },
+
+    /**
+     * Sends the current WS request and registers callback. The async 
+     * parameter is ignored (WS flush is always async) and the callback 
+     * function parameter is required.
+     * @param {object} async - Ignored.
+     * @param {object} callback - The client completion callback.
+     * @returns {undefined|string} Nothing (undefined) 
+     */
+    flush: function(async, callback) {
+      var self = this;
+      if (this.isOpen()) {
+        //Send data and register a callback to invoke the client callback
+        this.socket.send(this.send_buf); 
+        this.callbacks.push((function() {
+          var clientCallback = callback;    
+          return function(msg) {
+            self.setRecvBuffer(msg);
+            clientCallback();
+          };
+        }()));
+      } else {
+        //Queue the send to go out __onOpen
+        this.send_pending.push({
+          buf: this.send_buf,
+          cb:  callback
+        });
+      }
+    },
+
+    __onOpen: function() { 
+       var self = this;
+       if (this.send_pending.length > 0) {
+          //If the user made calls before the connection was fully 
+          //open, send them now
+          this.send_pending.forEach(function(elem) {
+             this.socket.send(elem.buf);
+             this.callbacks.push((function() {
+               var clientCallback = elem.cb;    
+               return function(msg) {
+                  self.setRecvBuffer(msg);
+                  clientCallback();
+               };
+             }()));
+          });
+          this.send_pending = [];
+       }
+    },
+    
+    __onClose: function(evt) { 
+      this.__reset(this.url);
+    },
+     
+    __onMessage: function(evt) {
+      if (this.callbacks.length) {
+        this.callbacks.shift()(evt.data);
+      }
+    },
+     
+    __onError: function(evt) { 
+      console.log("Thrift WebSocket Error: " + evt.toString());
+      this.socket.close();
+    },
+
+    /**
      * Sets the buffer to use when receiving server responses.
      * @param {string} buf - The buffer to receive server responses.
      */
@@ -396,26 +589,36 @@
     },
 
     /**
-     * Returns true if the transport is open, in browser based JavaScript
-     * this function always returns true.
+     * Returns true if the transport is open
      * @readonly
-     * @returns {boolean} Always True.
+     * @returns {boolean} 
      */    
     isOpen: function() {
-        return true;
+        return this.socket && this.socket.readyState == this.socket.OPEN;
     },
 
     /**
-     * Opens the transport connection, in browser based JavaScript
-     * this function is a nop.
+     * Opens the transport connection
      */    
-    open: function() {},
+    open: function() {
+      //If OPEN/CONNECTING/CLOSING ignore additional opens
+      if (this.socket && this.socket.readyState != this.socket.CLOSED) {
+        return;
+      }
+      //If there is no socket or the socket is closed:
+      this.socket = new WebSocket(this.url);
+      this.socket.onopen = this.__onOpen.bind(this); 
+      this.socket.onmessage = this.__onMessage.bind(this); 
+      this.socket.onerror = this.__onError.bind(this); 
+      this.socket.onclose = this.__onClose.bind(this); 
+    },
 
     /**
-     * Closes the transport connection, in browser based JavaScript
-     * this function is a nop.
+     * Closes the transport connection
      */    
-    close: function() {},
+    close: function() {
+      this.socket.close();
+    },
 
     /**
      * Returns the specified number of characters from the response
@@ -1161,3 +1364,4 @@
 };
 
 
+