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 @@
};
+