From aab6f533c8b63ae19c49df0661bb0ec35c58bf5e Mon Sep 17 00:00:00 2001 From: Bill Erickson Date: Tue, 5 Feb 2013 09:54:28 -0500 Subject: [PATCH] OpenSRF Websockets JS library The API for the websockets library is very similar to that of the XHR/Translator library. The primary differences are that there is always 1 open websocket connection which manages all communication (unless another socket is explicitly opened) and *all* communication is required to be asyncronous. To enable websockets by default, add this to the top of your script: OpenSRF.Session.transport = OSRF_TRANSPORT_TYPE_WS; Signed-off-by: Bill Erickson --- src/javascript/DojoSRF.js | 1 + src/javascript/opensrf.js | 129 +++++++++++++++++---------- src/javascript/opensrf_ws.js | 165 +++++++++++++++++++++++++++++++++++ 3 files changed, 250 insertions(+), 45 deletions(-) create mode 100644 src/javascript/opensrf_ws.js diff --git a/src/javascript/DojoSRF.js b/src/javascript/DojoSRF.js index 42578f8d..7e3e5c81 100644 --- a/src/javascript/DojoSRF.js +++ b/src/javascript/DojoSRF.js @@ -10,6 +10,7 @@ if(!dojo._hasResource['DojoSRF']){ dojo.require('opensrf.JSON_v1', true); dojo.require('opensrf.opensrf', true); dojo.require('opensrf.opensrf_xhr', true); + dojo.require('opensrf.opensrf_ws', true); OpenSRF.session_cache = {}; OpenSRF.CachedClientSession = function ( app ) { diff --git a/src/javascript/opensrf.js b/src/javascript/opensrf.js index c0e454ca..813bef8a 100644 --- a/src/javascript/opensrf.js +++ b/src/javascript/opensrf.js @@ -21,6 +21,7 @@ var OSRF_APP_SESSION_DISCONNECTED = 2; /* types of transport layers */ var OSRF_TRANSPORT_TYPE_XHR = 1; var OSRF_TRANSPORT_TYPE_XMPP = 2; +var OSRF_TRANSPORT_TYPE_WS = 3; /* message types */ var OSRF_MESSAGE_TYPE_REQUEST = 'REQUEST'; @@ -205,7 +206,9 @@ OpenSRF.Session = function() { this.state = OSRF_APP_SESSION_DISCONNECTED; }; -OpenSRF.Session.transport = OSRF_TRANSPORT_TYPE_XHR; /* default to XHR */ +//OpenSRF.Session.transport = OSRF_TRANSPORT_TYPE_WS; +OpenSRF.Session.transport = OSRF_TRANSPORT_TYPE_XHR; + OpenSRF.Session.cache = {}; OpenSRF.Session.find_session = function(thread_trace) { return OpenSRF.Session.cache[thread_trace]; @@ -217,6 +220,8 @@ OpenSRF.Session.prototype.cleanup = function() { OpenSRF.Session.prototype.send = function(osrf_msg, args) { args = (args) ? args : {}; switch(OpenSRF.Session.transport) { + case OSRF_TRANSPORT_TYPE_WS: + return this.send_ws(osrf_msg); case OSRF_TRANSPORT_TYPE_XHR: return this.send_xhr(osrf_msg, args); case OSRF_TRANSPORT_TYPE_XMPP: @@ -231,8 +236,15 @@ OpenSRF.Session.prototype.send_xhr = function(osrf_msg, args) { new OpenSRF.XHRequest(osrf_msg, args).send(); }; +OpenSRF.Session.prototype.send_ws = function(osrf_msg) { + new OpenSRF.WebSocketRequest( + this, + function(wsreq) {wsreq.send(osrf_msg)} // onopen + ); +}; + OpenSRF.Session.prototype.send_xmpp = function(osrf_msg, args) { - alert('xmpp transport not yet implemented'); + alert('xmpp transport not implemented'); }; @@ -242,9 +254,9 @@ OpenSRF.ClientSession = function(service) { this.remote_id = null; this.locale = OpenSRF.locale || 'en-US'; this.last_id = 0; - this.thread = Math.random() + '' + new Date().getTime(); this.requests = []; this.onconnect = null; + this.thread = Math.random() + '' + new Date().getTime(); OpenSRF.Session.cache[this.thread] = this; }; OpenSRF.set_subclass('OpenSRF.ClientSession', 'OpenSRF.Session'); @@ -254,15 +266,21 @@ OpenSRF.ClientSession.prototype.connect = function(args) { args = (args) ? args : {}; this.remote_id = null; - if(args.onconnect) + if (this.state == OSRF_APP_SESSION_CONNECTED) { + if (args.onconnect) args.onconnect(); + return true; + } + + if(args.onconnect) { this.onconnect = args.onconnect; - /* if no handler is provided, make this a synchronous call */ - if(!this.onconnect) + } else { + /* if no handler is provided, make this a synchronous call */ this.timeout = (args.timeout) ? args.timeout : 5; + } message = new osrfMessage({ - 'threadTrace' : this.reqid, + 'threadTrace' : this.last_id++, 'type' : OSRF_MESSAGE_TYPE_CONNECT }); @@ -270,17 +288,28 @@ OpenSRF.ClientSession.prototype.connect = function(args) { if(this.onconnect || this.state == OSRF_APP_SESSION_CONNECTED) return true; + return false; }; OpenSRF.ClientSession.prototype.disconnect = function(args) { - this.send( - new osrfMessage({ - 'threadTrace' : this.reqid, - 'type' : OSRF_MESSAGE_TYPE_DISCONNECT - }) - ); + + if (this.state == OSRF_APP_SESSION_CONNECTED) { + this.send( + new osrfMessage({ + 'threadTrace' : this.last_id++, + 'type' : OSRF_MESSAGE_TYPE_DISCONNECT + }) + ); + } + this.remote_id = null; + this.state = OSRF_APP_SESSION_DISCONNECTED; + + if (this.websocket) { + this.websocket.close(); + delete this.websocket; + } }; @@ -375,11 +404,12 @@ OpenSRF.Request.prototype.send = function() { }); }; -OpenSRF.NetMessage = function(to, from, thread, body) { +OpenSRF.NetMessage = function(to, from, thread, body, osrf_msg) { this.to = to; this.from = from; this.thread = thread; this.body = body; + this.osrf_msg = osrf_msg; }; OpenSRF.Stack = function() { @@ -397,49 +427,60 @@ function log(msg) { } } +// ses may be passed to us by the network handler OpenSRF.Stack.push = function(net_msg, callbacks) { var ses = OpenSRF.Session.find_session(net_msg.thread); - if(!ses) return; + if (!ses) return; ses.remote_id = net_msg.from; - osrf_msgs = []; - - try { - osrf_msgs = JSON2js(net_msg.body); - } catch(E) { - log('Error parsing OpenSRF message body as JSON: ' + net_msg.body + '\n' + E); + // NetMessage's from websocket connections are parsed before they get here + osrf_msgs = net_msg.osrf_msg; - /** UGH - * For unknown reasons, the Content-Type header will occasionally - * be included in the XHR.responseText for multipart/mixed messages. - * When this happens, strip the header and newlines from the message - * body and re-parse. - */ - net_msg.body = net_msg.body.replace(/^.*\n\n/, ''); - log('Cleaning up and retrying...'); + if (!osrf_msgs) { try { osrf_msgs = JSON2js(net_msg.body); - } catch(E2) { - log('Unable to clean up message, giving up: ' + net_msg.body); - return; + + if (OpenSRF.Session.transport == OSRF_TRANSPORT_TYPE_WS) { + // WebSocketRequests wrap the content + osrf_msgs = osrf_msgs.osrf_msg; + } + + } catch(E) { + log('Error parsing OpenSRF message body as JSON: ' + net_msg.body + '\n' + E); + + /** UGH + * For unknown reasons, the Content-Type header will occasionally + * be included in the XHR.responseText for multipart/mixed messages. + * When this happens, strip the header and newlines from the message + * body and re-parse. + */ + net_msg.body = net_msg.body.replace(/^.*\n\n/, ''); + log('Cleaning up and retrying...'); + + try { + osrf_msgs = JSON2js(net_msg.body); + } catch(E2) { + log('Unable to clean up message, giving up: ' + net_msg.body); + return; + } } } // push the latest responses onto the end of the inbound message queue for(var i = 0; i < osrf_msgs.length; i++) - OpenSRF.Stack.queue.push({msg : osrf_msgs[i], callbacks : callbacks, ses : ses}); + OpenSRF.Stack.queue.push({msg : osrf_msgs[i], ses : ses}); // continue processing responses, oldest to newest while(OpenSRF.Stack.queue.length) { var data = OpenSRF.Stack.queue.shift(); - OpenSRF.Stack.handle_message(data.ses, data.msg, data.callbacks); + OpenSRF.Stack.handle_message(data.ses, data.msg); } }; -OpenSRF.Stack.handle_message = function(ses, osrf_msg, callbacks) { +OpenSRF.Stack.handle_message = function(ses, osrf_msg) { - var req = null; + var req = ses.find_request(osrf_msg.threadTrace()); if(osrf_msg.type() == OSRF_MESSAGE_TYPE_STATUS) { @@ -448,12 +489,11 @@ OpenSRF.Stack.handle_message = function(ses, osrf_msg, callbacks) { var status_text = payload.status(); if(status == OSRF_STATUS_COMPLETE) { - req = ses.find_request(osrf_msg.threadTrace()); if(req) { req.complete = true; - if(callbacks.oncomplete && !req.oncomplete_called) { + if(req.oncomplete && !req.oncomplete_called) { req.oncomplete_called = true; - return callbacks.oncomplete(req); + return req.oncomplete(req); } } } @@ -469,18 +509,17 @@ OpenSRF.Stack.handle_message = function(ses, osrf_msg, callbacks) { } if(status == OSRF_STATUS_NOTFOUND || status == OSRF_STATUS_INTERNALSERVERERROR) { - req = ses.find_request(osrf_msg.threadTrace()); - if(callbacks.onmethoderror) - return callbacks.onmethoderror(req, status, status_text); + if(req && req.onmethoderror) + return req.onmethoderror(req, status, status_text); } } if(osrf_msg.type() == OSRF_MESSAGE_TYPE_RESULT) { - req = ses.find_request(osrf_msg.threadTrace()); if(req) { req.response_queue.push(osrf_msg.payload()); - if(callbacks.onresponse) - return callbacks.onresponse(req); + if(req.onresponse) { + return req.onresponse(req); + } } } }; diff --git a/src/javascript/opensrf_ws.js b/src/javascript/opensrf_ws.js new file mode 100644 index 00000000..4df7c8fc --- /dev/null +++ b/src/javascript/opensrf_ws.js @@ -0,0 +1,165 @@ +/* ----------------------------------------------------------------------- + * Copyright (C) 2012 Equinox Software, Inc. + * Bill Erickson + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * ----------------------------------------------------------------------- */ + +// opensrf defaults +var WEBSOCKET_URL_PATH = '/osrf-websocket-translator'; +var WEBSOCKET_PORT = 7680; +var WEBSOCKET_PORT_SSL = 7682; + + +// Create the websocket and connect to the server +// args.onopen is required +// if args.default is true, use the default connection +OpenSRF.WebSocketConnection = function(args, handlers) { + args = args || {}; + this.handlers = handlers; + + var secure = (args.ssl || location.protocol == 'https'); + var path = args.path || WEBSOCKET_URL_PATH; + var port = args.port || (secure ? WEBSOCKET_PORT_SSL : WEBSOCKET_PORT); + var host = args.host || location.host; + var proto = (secure) ? 'wss' : 'ws'; + this.path = proto + '://' + host + ':' + port + path; + + this.setupSocket(); + OpenSRF.WebSocketConnection.pool[args.name] = this; +}; + +// global pool of connection objects; name => connection map +OpenSRF.WebSocketConnection.pool = {}; + +OpenSRF.WebSocketConnection.defaultConnection = function() { + return OpenSRF.WebSocketConnection.pool['default']; +} + +/** + * create a new WebSocket. useful for new connections or + * applying a new socket to an existing connection (whose + * socket was disconnected) + */ +OpenSRF.WebSocketConnection.prototype.setupSocket = function() { + + try { + this.socket = new WebSocket(this.path); + } catch(e) { + throw new Error("WebSocket() not supported in this browser: " + e); + } + + this.socket.onopen = this.handlers.onopen; + this.socket.onmessage = this.handlers.onmessage; + this.socket.onerror = this.handlers.onerror; + this.socket.onclose = this.handlers.onclose; +}; + +/** default onmessage handler: push the message up the opensrf stack */ +OpenSRF.WebSocketConnection.default_onmessage = function(evt) { + //console.log('receiving: ' + evt.data); + var msg = JSON2js(evt.data); + OpenSRF.Stack.push( + new OpenSRF.NetMessage( + null, null, msg.thread, null, msg.osrf_msg) + ); +}; + +/** default error handler */ +OpenSRF.WebSocketConnection.default_onerror = function(evt) { + throw new Error("WebSocket Error " + evt + ' : ' + evt.data); +}; + + +/** shut it down */ +OpenSRF.WebSocketConnection.prototype.destroy = function() { + this.socket.close(); + delete OpenSRF.WebSocketConnection.pool[this.name]; +}; + +/** + * Creates the request object, but does not connect or send anything + * until the first call to send(). + */ +OpenSRF.WebSocketRequest = function(session, onopen, connectionArgs) { + this.session = session; + this.onopen = onopen; + this.setupConnection(connectionArgs || {}); +} + +OpenSRF.WebSocketRequest.prototype.setupConnection = function(args) { + var self = this; + + var cname = args.name || 'default'; + this.wsc = OpenSRF.WebSocketConnection.pool[cname]; + + if (this.wsc) { // we have a WebSocketConnection. + + switch (this.wsc.socket.readyState) { + + case this.wsc.socket.CONNECTING: + // replace the original onopen handler with a new combined handler + var orig_open = this.wsc.socket.onopen; + this.wsc.socket.onopen = function() { + orig_open(); + self.onopen(self); + }; + break; + + case this.wsc.socket.OPEN: + // user is expecting an onopen event. socket is + // already open, so we have to manufacture one. + this.onopen(this); + break; + + default: + console.log('WebSocket is no longer connecting; reconnecting'); + this.wsc.setupSocket(); + } + + } else { // no connection found + + if (cname == 'default' || args.useDefaultHandlers) { // create the default handle + + this.wsc = new OpenSRF.WebSocketConnection( + {name : cname}, { + onopen : function(evt) {if (self.onopen) self.onopen(self)}, + onmessage : OpenSRF.WebSocketConnection.default_onmessage, + onerror : OpenSRF.WebSocketRequest.default_onerror, + onclose : OpenSRF.WebSocketRequest.default_onclose + } + ); + + } else { + throw new Error("No such WebSocketConnection '" + cname + "'"); + } + } +} + + +OpenSRF.WebSocketRequest.prototype.send = function(message) { + var wrapper = { + service : this.session.service, + thread : this.session.thread, + osrf_msg : [message.serialize()] + }; + + var json = js2JSON(wrapper); + //console.log('sending: ' + json); + + // drop it on the wire + this.wsc.socket.send(json); + return this; +}; + + + + -- 2.43.2