[OpenSRF-GIT] OpenSRF branch master updated. 2677f8e815a61f9b808bb57647f6ec9d448f0268

Evergreen Git git at git.evergreen-ils.org
Tue Aug 19 19:02:27 EDT 2014


This is an automated email from the git hooks/post-receive script. It was
generated because a ref change was pushed to the repository containing
the project "OpenSRF".

The branch, master has been updated
       via  2677f8e815a61f9b808bb57647f6ec9d448f0268 (commit)
       via  755a58642281246df280877f7ad480503b1ddc4b (commit)
       via  c65c6d9f91c5372360619004c8602d2a262dfb8b (commit)
       via  17ae5ca5e6db6700dc4b61599c4394f80e727711 (commit)
       via  9fdef970f3785e958090f8edf4ad37ece4459343 (commit)
       via  f43286bf11119c731ee0dbe9b5b740d8715ace66 (commit)
       via  0cf0a8a5fe61228995f75d0f0c6e4f4731888c40 (commit)
       via  8120314b86d2b6cafe2f5fd968b4475cc187acfb (commit)
       via  4ce075beeb5f81b8eb4c2cb2669b3a99ebab40ba (commit)
       via  77c7f5889e8f31038cd732feb3e6057a0d3788b5 (commit)
       via  bb424c1424193e4db340e3f6e17a939dc1dd821a (commit)
       via  9f2d35e6f6b6ec5a3db109adb518cf270d49c683 (commit)
       via  b1b3bf60f613b94d1455afb54dff4ad4a7e95ddd (commit)
       via  6765c6395b0fb2a1c501f7c94a04cfacc7d460db (commit)
       via  f5ada2850552560a24e473e02532e75870ab7307 (commit)
       via  2bcead2d3e1c560a30c5b9beba6d1cbfe58778ed (commit)
       via  5631bbdfa0f9a4fe2ea1b238c5e2ffee4b606dc1 (commit)
       via  0054ea6684a933037af1cc3bc6358c7096dc051c (commit)
       via  e375a1e31b87a3d645b6da05e83f2a29f885f1fc (commit)
       via  0a0d3f616c9531c7931c365e1912cbcf6358441b (commit)
       via  aa1c088bd45a254290ad202875eb87c4bd4eeb2a (commit)
       via  a64f10c4183a495ec3912458c9b6268856e8fe47 (commit)
       via  a02360aadfcd113cbec88d9c2455e42fdd74e536 (commit)
       via  bef394a7c24df54f1e63fc3e83cd473195a46c3a (commit)
       via  fdb255a92f9fa687a50bed05ef918523cf902d8b (commit)
       via  d546d7eacb183ba2ddd0c0ba5dc281dc5086ae81 (commit)
       via  e4ef36f385c4f3b83ac4b49f2b07ee19c3166ff0 (commit)
       via  32ab4b133c9c96780e8b202ab1efe46bbf321c3b (commit)
       via  0f3aa6480d2fbc9645571e057dac3f3be08709d1 (commit)
       via  de238b740f3a54dacd07610d8e187f417bf5b677 (commit)
       via  f990a29db95d9b1c06efa22c1b3f4fbc43206571 (commit)
       via  27707398e955b8a8a2df1a5311aebc19b8eb1708 (commit)
       via  cc42cb62c47edabd693e491ad0d939970d7dbc53 (commit)
       via  9e455c227be32bed4a16e6dab7045b6424e2ba15 (commit)
       via  a77eb22c27183d23fb08ed40bc75469d8c54b884 (commit)
       via  1dafbe7512f086a58212fcc66c07e348647f31ad (commit)
      from  cb56fd32eb43f037c4126e1398b0e9cd546d9f19 (commit)

Those revisions listed above that are new to this repository have
not appeared on any other notification email; so we list those
revisions in full, below.

- Log -----------------------------------------------------------------
commit 2677f8e815a61f9b808bb57647f6ec9d448f0268
Author: Bill Erickson <berick at esilibrary.com>
Date:   Mon Aug 4 14:26:36 2014 -0400

    LP#1268619: disable shared workers pending browser issues
    
    There appears to be a bug in Chromium where loading the same page
    multiple times (without a refresh or cache clear) causes the
    SharedWorker to fail to instantiate on every other page load.
    Further research pending.  Disabling SharedWorker's entirely for
    now.
    
    Note, to replicate, load a page using shared workers, focus the
    browser address bar, hit Enter to load the page again.  The shared
    worker will fail to load on every other page load, though it will
    appear to the SharedWorker caller (opensrf.js) that the port is open.
    
    Signed-off-by: Bill Erickson <berick at esilibrary.com>
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>

diff --git a/src/javascript/opensrf.js b/src/javascript/opensrf.js
index 29cf6bf..ecc012d 100644
--- a/src/javascript/opensrf.js
+++ b/src/javascript/opensrf.js
@@ -257,7 +257,12 @@ OpenSRF.websocketConnected = function() {
 
 OpenSRF.Session.prototype.send_ws = function(osrf_msg) {
 
-    if (typeof SharedWorker == 'function' 
+    // XXX there appears to be a bug in Chromium where loading the
+    // same page multiple times (without a refresh or cache clear)
+    // causes the SharedWorker to fail to instantiate on 
+    // every other page load.  Disabling SharedWorker's entirely
+    // for now.
+    if (false /* ^-- */ && typeof SharedWorker == 'function' 
 
         /*
          * https://bugzilla.mozilla.org/show_bug.cgi?id=504553#c73

commit 755a58642281246df280877f7ad480503b1ddc4b
Author: Bill Erickson <berick at esilibrary.com>
Date:   Wed Jul 30 12:05:36 2014 -0400

    LP#1268619: update JS/WS/SSL code comment
    
    Signed-off-by: Bill Erickson <berick at esilibrary.com>
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>

diff --git a/src/javascript/opensrf_ws_shared.js b/src/javascript/opensrf_ws_shared.js
index 36c5baf..ad30dda 100644
--- a/src/javascript/opensrf_ws_shared.js
+++ b/src/javascript/opensrf_ws_shared.js
@@ -106,10 +106,7 @@ function send_to_websocket(message) {
 
     // we have no websocket or an invalid websocket.  build a new one.
 
-    // TODO:
-    // assume non-SSL for now.  SSL silently dies if the cert is
-    // invalid and has not been added as an exception.  need to
-    // explain / document / avoid this better.
+    // assume SSL at all times
     var path = 'wss://' + location.host + ':' + 
         WEBSOCKET_PORT_SSL + WEBSOCKET_URL_PATH;
 

commit c65c6d9f91c5372360619004c8602d2a262dfb8b
Author: Bill Erickson <berick at esilibrary.com>
Date:   Thu Jul 10 11:29:49 2014 -0400

    LP#1268619: JS status codes can come across as numbers; stringify for match
    
    Signed-off-by: Bill Erickson <berick at esilibrary.com>
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>

diff --git a/src/javascript/opensrf.js b/src/javascript/opensrf.js
index a31c550..29cf6bf 100644
--- a/src/javascript/opensrf.js
+++ b/src/javascript/opensrf.js
@@ -633,7 +633,7 @@ OpenSRF.Stack.handle_message = function(ses, osrf_msg) {
         }
 
         // capture all 400's and 500's as method errors
-        if (status.match(/^4/) || status.match(/^5/)) {
+        if ((status+'').match(/^4/) || (status+'').match(/^5/)) {
             if(req && req.onmethoderror) 
                 return req.onmethoderror(req, status, status_text);
         }

commit 17ae5ca5e6db6700dc4b61599c4394f80e727711
Author: Bill Erickson <berick at esilibrary.com>
Date:   Tue Jul 8 09:41:12 2014 -0400

    LP#1268619: JS libs capture all method errors
    
    Instead of selecting specific errors to report as method errors, report
    all API calls which return 400 or 500-series errors as method errors.
    
    Signed-off-by: Bill Erickson <berick at esilibrary.com>
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>

diff --git a/src/javascript/opensrf.js b/src/javascript/opensrf.js
index 8036c5a..a31c550 100644
--- a/src/javascript/opensrf.js
+++ b/src/javascript/opensrf.js
@@ -632,7 +632,8 @@ OpenSRF.Stack.handle_message = function(ses, osrf_msg) {
             }
         }
 
-        if(status == OSRF_STATUS_NOTFOUND || status == OSRF_STATUS_INTERNALSERVERERROR) {
+        // capture all 400's and 500's as method errors
+        if (status.match(/^4/) || status.match(/^5/)) {
             if(req && req.onmethoderror) 
                 return req.onmethoderror(req, status, status_text);
         }

commit 9fdef970f3785e958090f8edf4ad37ece4459343
Author: Bill Erickson <berick at esilibrary.com>
Date:   Sun May 4 15:58:17 2014 -0400

    LP#1268619: websocket: avoid sharedworker for firefox 29
    
    https://bugzilla.mozilla.org/show_bug.cgi?id=504553#c73
    
    Avoid using SharedWorkers when useragent match "Firefox".  FF supports
    shared workers, but it does not yet support WebSockets within shared
    workers.
    
    Signed-off-by: Bill Erickson <berick at esilibrary.com>
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>

diff --git a/src/javascript/opensrf.js b/src/javascript/opensrf.js
index f9b0895..8036c5a 100644
--- a/src/javascript/opensrf.js
+++ b/src/javascript/opensrf.js
@@ -257,7 +257,14 @@ OpenSRF.websocketConnected = function() {
 
 OpenSRF.Session.prototype.send_ws = function(osrf_msg) {
 
-    if (typeof SharedWorker == 'function') {
+    if (typeof SharedWorker == 'function' 
+
+        /*
+         * https://bugzilla.mozilla.org/show_bug.cgi?id=504553#c73
+         * Firefox does not yet support WebSockets in worker threads
+         */
+        && !navigator.userAgent.match(/Firefox/)
+    ) {
         // vanilla websockets requested, but this browser supports
         // shared workers, so use those instead.
         return this.send_ws_shared(osrf_msg);
@@ -305,7 +312,6 @@ OpenSRF.Session.setup_shared_ws = function() {
 
     OpenSRF.sharedWSWorker.port.addEventListener('message', function(e) {                          
         var data = e.data;
-        console.debug('sharedWSWorker received message of type: ' + data.action);
 
         if (data.action == 'message') {
             // pass all inbound message up the opensrf stack
@@ -329,7 +335,6 @@ OpenSRF.Session.setup_shared_ws = function() {
 
 
         if (data.action == 'event') {
-            console.debug('event type is ' + data.type);
             if (data.type.match(/onclose|onerror/)) {
                 OpenSRF.sharedWebsocketConnected = false;
                 if (OpenSRF.onWebSocketClosed)

commit f43286bf11119c731ee0dbe9b5b740d8715ace66
Author: Bill Erickson <berick at esilibrary.com>
Date:   Thu Apr 10 08:50:13 2014 -0400

    LP#1268619: websockets: detect connectedness of JS default sockets
    
    Signed-off-by: Bill Erickson <berick at esilibrary.com>
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>

diff --git a/src/javascript/opensrf.js b/src/javascript/opensrf.js
index 997b33f..f9b0895 100644
--- a/src/javascript/opensrf.js
+++ b/src/javascript/opensrf.js
@@ -248,6 +248,13 @@ OpenSRF.Session.prototype.send_xhr = function(osrf_msg, args) {
     new OpenSRF.XHRequest(osrf_msg, args).send();
 };
 
+OpenSRF.websocketConnected = function() {
+    return OpenSRF.sharedWebsocketConnected || (
+        OpenSRF.websocketConnection && 
+        OpenSRF.websocketConnection.connected()
+    );
+}
+
 OpenSRF.Session.prototype.send_ws = function(osrf_msg) {
 
     if (typeof SharedWorker == 'function') {
@@ -303,6 +310,7 @@ OpenSRF.Session.setup_shared_ws = function() {
         if (data.action == 'message') {
             // pass all inbound message up the opensrf stack
 
+            OpenSRF.sharedWebsocketConnected = true;
             var msg;
             try {
                 msg = JSON2js(data.message);
@@ -319,8 +327,16 @@ OpenSRF.Session.setup_shared_ws = function() {
             return;
         }
 
-        if (data.action == 'error') {
-            throw new Error(data.message);
+
+        if (data.action == 'event') {
+            console.debug('event type is ' + data.type);
+            if (data.type.match(/onclose|onerror/)) {
+                OpenSRF.sharedWebsocketConnected = false;
+                if (OpenSRF.onWebSocketClosed)
+                    OpenSRF.onWebSocketClosed();
+                if (data.type.match(/onerror/)) 
+                    throw new Error(data.message);
+            }
         }
     });
 
diff --git a/src/javascript/opensrf_ws.js b/src/javascript/opensrf_ws.js
index 352bd2a..74fc40e 100644
--- a/src/javascript/opensrf_ws.js
+++ b/src/javascript/opensrf_ws.js
@@ -21,6 +21,13 @@ OpenSRF.WebSocket = function() {
     this.pending_messages = [];
 }
 
+OpenSRF.WebSocket.prototype.connected = function() {
+    return (
+        this.socket && 
+        this.socket.readyState == this.socket.OPEN
+    );
+}
+
 /**
  * If our global socket is already open, use it.  Otherwise, queue the 
  * message for delivery after the socket is open.
@@ -28,7 +35,7 @@ OpenSRF.WebSocket = function() {
 OpenSRF.WebSocket.prototype.send = function(message) {
     var self = this;
 
-    if (this.socket && this.socket.readyState == this.socket.OPEN) {
+    if (this.connected()) {
         // this.socket connection is viable.  send our message now.
         this.socket.send(message);
         return;
@@ -91,5 +98,7 @@ OpenSRF.WebSocket.prototype.send = function(message) {
     this.socket.onclose = function() {
         console.debug('closing websocket');
         self.socket = null;
+        if (OpenSRF.onWebSocketClosed)
+            OpenSRF.onWebSocketClosed();
     }
 }
diff --git a/src/javascript/opensrf_ws_shared.js b/src/javascript/opensrf_ws_shared.js
index ff0b586..36c5baf 100644
--- a/src/javascript/opensrf_ws_shared.js
+++ b/src/javascript/opensrf_ws_shared.js
@@ -128,6 +128,7 @@ function send_to_websocket(message) {
         var msg;
         while ( (msg = pending_ws_messages.shift()) )
             websocket.send(msg);
+
     }
 
     websocket.onmessage = function(evt) {
@@ -189,7 +190,7 @@ function send_to_websocket(message) {
     websocket.onerror = function(evt) {
         var err = "WebSocket Error " + evt;
         console.error(err);
-        broadcast({action : 'error', message : err});
+        broadcast({action : 'event', type : 'onerror', message : err});
         websocket.close(); // connection is no good; reset.
     }
 
@@ -203,6 +204,7 @@ function send_to_websocket(message) {
         console.debug('closing websocket');
         websocket = null;
         thread_port_map = {};
+        broadcast({action : 'event', type : 'onclose'});
     }
 }
 

commit 0cf0a8a5fe61228995f75d0f0c6e4f4731888c40
Author: Bill Erickson <berick at esilibrary.com>
Date:   Mon Apr 7 12:04:33 2014 -0400

    LP#1268619: websockets: auto-upgrade to shared workers; SSL-always
    
    Signed-off-by: Bill Erickson <berick at esilibrary.com>
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>

diff --git a/src/javascript/opensrf.js b/src/javascript/opensrf.js
index db6e49d..997b33f 100644
--- a/src/javascript/opensrf.js
+++ b/src/javascript/opensrf.js
@@ -249,13 +249,51 @@ OpenSRF.Session.prototype.send_xhr = function(osrf_msg, args) {
 };
 
 OpenSRF.Session.prototype.send_ws = function(osrf_msg) {
-    new OpenSRF.WebSocketRequest(
-        this, 
-        function(wsreq) {wsreq.send(osrf_msg)} // onopen
-    );
+
+    if (typeof SharedWorker == 'function') {
+        // vanilla websockets requested, but this browser supports
+        // shared workers, so use those instead.
+        return this.send_ws_shared(osrf_msg);
+    }
+
+    // otherwise, use a per-tab connection
+
+    if (!OpenSRF.websocketConnection) {
+        this.setup_single_ws();
+    }
+
+    var json = js2JSON({
+        service : this.service,
+        thread : this.thread,
+        osrf_msg : [message.serialize()]
+    });
+
+    OpenSRF.websocketConnection.send(json);
 };
 
+OpenSRF.Session.prototype.setup_single_ws = function() {
+    OpenSRF.websocketConnection = new OpenSRF.WebSocket();
+
+    OpenSRF.websocketConnection.onmessage = function(msg) {
+        try {
+            var msg = JSON2js(msg);
+        } catch(E) {
+            console.error(
+                "Error parsing JSON in shared WS response: " + msg);
+            throw E;
+        }
+        OpenSRF.Stack.push(                                                        
+            new OpenSRF.NetMessage(                                                
+               null, null, msg.thread, null, msg.osrf_msg)                        
+        ); 
+
+        return;
+    }
+}
+
 OpenSRF.Session.setup_shared_ws = function() {
+    OpenSRF.Session.transport = OSRF_TRANSPORT_TYPE_WS_SHARED;
+
     OpenSRF.sharedWSWorker = new SharedWorker(SHARED_WORKER_LIB);
 
     OpenSRF.sharedWSWorker.port.addEventListener('message', function(e) {                          
diff --git a/src/javascript/opensrf_ws.js b/src/javascript/opensrf_ws.js
new file mode 100644
index 0000000..352bd2a
--- /dev/null
+++ b/src/javascript/opensrf_ws.js
@@ -0,0 +1,95 @@
+/* -----------------------------------------------------------------------
+ * Copyright (C) 2014  Equinox Software, Inc.
+ * Bill Erickson <berick at esilibrary.com>
+ *  
+ * 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.
+ * ----------------------------------------------------------------------- */
+
+
+var WEBSOCKET_URL_PATH = '/osrf-websocket-translator';
+var WEBSOCKET_PORT_SSL = 7682;
+
+OpenSRF.WebSocket = function() {
+    this.pending_messages = [];
+}
+
+/**
+ * If our global socket is already open, use it.  Otherwise, queue the 
+ * message for delivery after the socket is open.
+ */
+OpenSRF.WebSocket.prototype.send = function(message) {
+    var self = this;
+
+    if (this.socket && this.socket.readyState == this.socket.OPEN) {
+        // this.socket connection is viable.  send our message now.
+        this.socket.send(message);
+        return;
+    }
+
+    // no viable connection. queue our outbound messages for future delivery.
+    this.pending_messages.push(message);
+    console.log('pending count ' + this.pending_messages.length);
+
+    if (this.socket && this.socket.readyState == this.socket.CONNECTING) {
+        // we are already in the middle of a setup call.  
+        // our queued message will be delivered after setup completes.
+        return;
+    }
+
+    // we have no websocket or an invalid websocket.  build a new one.
+
+    var path = 'wss://' + location.host + ':' + 
+        WEBSOCKET_PORT_SSL + WEBSOCKET_URL_PATH;
+
+    console.debug('connecting websocket to ' + path);
+
+    try {
+        this.socket = new WebSocket(path);
+    } catch(E) {
+        console.log('Error creating WebSocket for path ' + path + ' : ' + E);
+        throw new Error(E);
+    }
+
+    this.socket.onopen = function() {
+        console.debug('websocket.onopen()');
+        // deliver any queued messages
+        var msg;
+        console.log('pending count ' + self.pending_messages.length);
+        while ( (msg = self.pending_messages.shift()) )
+            self.socket.send(msg);
+    }
+
+    this.socket.onmessage = function(evt) {
+        self.onmessage(evt.data);
+    }
+
+    /**
+     * Websocket error handler.  This type of error indicates a probelem
+     * with the connection.  I.e. it's not port-specific. 
+     * Broadcast to all ports.
+     */
+    this.socket.onerror = function(evt) {
+        var err = "WebSocket Error " + evt + ' : ' + evt.data;
+        self.socket.close(); // connection is no good; reset.
+        throw new Error(err); 
+    }
+
+    /**
+     * Called when the websocket connection is closed.
+     *
+     * Once a websocket is closed, it will be re-opened the next time
+     * a message delivery attempt is made.  Clean up and prepare to reconnect.
+     */
+    this.socket.onclose = function() {
+        console.debug('closing websocket');
+        self.socket = null;
+    }
+}
diff --git a/src/javascript/opensrf_ws_shared.js b/src/javascript/opensrf_ws_shared.js
index 4b90dc8..ff0b586 100644
--- a/src/javascript/opensrf_ws_shared.js
+++ b/src/javascript/opensrf_ws_shared.js
@@ -25,7 +25,6 @@
  */
 
 var WEBSOCKET_URL_PATH = '/osrf-websocket-translator';
-var WEBSOCKET_PORT = 7680; // TODO: remove.  all traffic should use SSL.
 var WEBSOCKET_PORT_SSL = 7682;
 var WEBSOCKET_MAX_THREAD_PORT_CACHE_SIZE = 1000;
 
@@ -111,7 +110,8 @@ function send_to_websocket(message) {
     // assume non-SSL for now.  SSL silently dies if the cert is
     // invalid and has not been added as an exception.  need to
     // explain / document / avoid this better.
-    var path = 'ws://' + location.host + ':' + WEBSOCKET_PORT + WEBSOCKET_URL_PATH;
+    var path = 'wss://' + location.host + ':' + 
+        WEBSOCKET_PORT_SSL + WEBSOCKET_URL_PATH;
 
     console.debug('connecting websocket to ' + path);
 
@@ -187,10 +187,10 @@ function send_to_websocket(message) {
      * Broadcast to all ports.
      */
     websocket.onerror = function(evt) {
-        var err = "WebSocket Error " + evt + ' : ' + evt.data;
+        var err = "WebSocket Error " + evt;
+        console.error(err);
         broadcast({action : 'error', message : err});
         websocket.close(); // connection is no good; reset.
-        throw new Error(err); 
     }
 
     /**

commit 8120314b86d2b6cafe2f5fd968b4475cc187acfb
Author: Bill Erickson <berick at esilibrary.com>
Date:   Tue Mar 11 17:25:19 2014 -0400

    LP#1268619: websockets: gateway code repairs & confing options
    
    * avoid unneccessary and wrong incantation of apr_thread_exit.  The two
      sub-threads now both live for the duration of the process.
    * to be safe, create thread mutex before threads
    
    Signed-off-by: Bill Erickson <berick at esilibrary.com>
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>

diff --git a/README.websockets b/README.websockets
index 69a56c0..15f38b1 100644
--- a/README.websockets
+++ b/README.websockets
@@ -23,10 +23,11 @@ Websockets installation instructions for Debian
 
 # OPTIONAL: add these configuration variables to
 # /etc/apache2-websockets/envvars and adjust as needed.
-# export OSRF_WEBSOCKET_IDLE_TIMEOUT=60
+# export OSRF_WEBSOCKET_IDLE_TIMEOUT=120
 # export OSRF_WEBSOCKET_IDLE_CHECK_INTERVAL=5
 # export OSRF_WEBSOCKET_CONFIG_FILE=/openils/conf/opensrf_core.xml
 # export OSRF_WEBSOCKET_CONFIG_CTXT=gateway
+# export OSRF_WEBSOCKET_MAX_REQUEST_WAIT_TIME=600
 #
 # IDLE_TIMEOUT specifies how long we will allow a client to stay connected
 # while idle.  A longer timeout means less network traffic (from fewer
@@ -36,6 +37,12 @@ Websockets installation instructions for Debian
 # IDLE_CHECK_INTERVAL specifies how often we wake to check the idle status
 # of the connected client.
 #
+# MAX_REQUEST_WAIT_TIME is the maximum amount of time the gateway will
+# wait before declaring a client as idle when there is a long-running
+# outstanding request, yet no other activity is occurring.  This is
+# primarily a fail-safe to allow idle timeouts when one or more requests
+# died on the server, and thus no response was ever delivered to the gateway.
+#
 # Both specified in seconds
 #
 # CONFIG_FILE / CTXT are the standard opensrf core config options.
diff --git a/src/gateway/osrf_websocket_translator.c b/src/gateway/osrf_websocket_translator.c
index 5b9d607..ef8d4af 100644
--- a/src/gateway/osrf_websocket_translator.c
+++ b/src/gateway/osrf_websocket_translator.c
@@ -85,17 +85,43 @@
 #include "opensrf/osrfConfig.h"
 
 #define MAX_THREAD_SIZE 64
-#define RECIP_BUF_SIZE 128
+#define RECIP_BUF_SIZE 256
 #define WEBSOCKET_TRANSLATOR_INGRESS "ws-translator-v1"
 
+// maximun number of active, CONNECTed opensrf sessions allowed. in
+// practice, this number will be very small, rarely reaching double
+// digits.  This is just a security back-stop.  A client trying to open
+// this many connections is almost certainly attempting to DOS the
+// gateway / server.  We may want to lower this further.
+#define MAX_ACTIVE_STATEFUL_SESSIONS 128
 
 // default values, replaced during setup (below) as needed.
 static char* config_file = "/openils/conf/opensrf_core.xml";
 static char* config_ctxt = "gateway";
-static time_t idle_timeout_interval = 60; 
+
+static time_t idle_timeout_interval = 120; 
 static time_t idle_check_interval = 5;
 static time_t last_activity_time = 0;
 
+// Generally, we do not disconnect the client (as idle) if there is a
+// request in flight.  However, we need to have an upper bound on the
+// amount of time we will wait for in-flight requests to complete to
+// avoid leaving an effectively idle connection open after a request
+// died on the backend and no response was received.
+// Note that if other activity occurs while a long-running request
+// is active, the wait time will get reset with each new activity. 
+// This is OK, though, because the goal of max_request_wait_time
+// is not to chop requests off at the knees, it's to allow the client
+// to timeout as idle when only a single long-running request is active
+// and preventing timeout.
+static time_t max_request_wait_time = 600;
+
+// Incremented with every REQUEST, decremented with every COMPLETE.
+// Gives us a rough picture of the number of reqests we've sent to 
+// the server vs. the number for which a completed response has been 
+// received.
+static int requests_in_flight = 0;
+
 // true if we've received a signal to start graceful shutdown
 static int shutdown_requested = 0; 
 static void sigusr1_handler(int sig);
@@ -130,16 +156,18 @@ typedef struct _osrfWebsocketTranslator {
      * map internally means the caller never need know about
      * internal XMPP addresses and the server doesn't have to 
      * verify caller-specified recipient addresses.  It's
-     * all managed internally.
+     * all managed internally.  This is only used for stateful
+     * (CONNECT'ed) session.  Stateless sessions need not 
+     * track the recipient, since they are one-off calls.
      */
-    apr_hash_t *session_cache; 
+    apr_hash_t *stateful_session_cache; 
 
     /**
-     * session_pool contains the key/value pairs stored in
-     * the session_cache.  The pool is regularly destroyed
+     * stateful_session_pool contains the key/value pairs stored in
+     * the stateful_session_cache.  The pool is regularly destroyed
      * and re-created to avoid long-term memory consumption
      */
-    apr_pool_t *session_pool;
+    apr_pool_t *stateful_session_pool;
 
     /**
      * Thread responsible for collecting responses on the opensrf
@@ -181,31 +209,25 @@ static char recipient_buf[RECIP_BUF_SIZE]; // reusable recipient buffer
 
 static void clear_cached_recipient(const char* thread) {
     apr_pool_t *pool = NULL;                                                
+    request_rec *r = trans->server->request(trans->server);
 
-    if (apr_hash_get(trans->session_cache, thread, APR_HASH_KEY_STRING)) {
+    if (apr_hash_get(trans->stateful_session_cache, thread, APR_HASH_KEY_STRING)) {
 
         osrfLogDebug(OSRF_LOG_MARK, "WS removing cached recipient on disconnect");
 
         // remove it from the hash
-        apr_hash_set(trans->session_cache, thread, APR_HASH_KEY_STRING, NULL);
-
-        if (apr_hash_count(trans->session_cache) == 0) {
-            osrfLogDebug(OSRF_LOG_MARK, "WS re-setting session_pool");
-
-            // memory accumulates in the session_pool as sessions are cached then 
-            // un-cached.  Un-caching removes strings from the hash, but not the 
-            // pool itself.  That only happens when the pool is destroyed. Here
-            // we destroy the session pool to clear any lingering memory, then
-            // re-create it for future caching.
-            apr_pool_destroy(trans->session_pool);
-    
-            if (apr_pool_create(&pool, NULL) != APR_SUCCESS) {
-                osrfLogError(OSRF_LOG_MARK, "WS Unable to create session_pool");
-                trans->session_pool = NULL;
-                return;
-            }
-
-            trans->session_pool = pool;
+        apr_hash_set(trans->stateful_session_cache, thread, APR_HASH_KEY_STRING, NULL);
+
+        if (apr_hash_count(trans->stateful_session_cache) == 0) {
+            osrfLogDebug(OSRF_LOG_MARK, "WS re-setting stateful_session_pool");
+
+            // memory accumulates in the stateful_session_pool as
+            // sessions are cached then un-cached.  Un-caching removes
+            // strings from the hash, but not from the pool.  Clear the
+            // pool here. note: apr_pool_clear does not free memory, it
+            // reclaims it for use again within the pool.  This is more
+            // effecient than freeing and allocating every time.
+            apr_pool_clear(trans->stateful_session_pool);
         }
     }
 }
@@ -233,30 +255,44 @@ void* osrf_responder_thread_main_body(transport_message *tmsg) {
             the correct recipient. */
         if (one_msg && one_msg->m_type == STATUS) {
 
+            if (one_msg->status_code == OSRF_STATUS_OK) {
 
-            // only cache recipients if the client is still connected
-            if (trans->client_connected && 
-                    one_msg->status_code == OSRF_STATUS_OK) {
-
-                if (!apr_hash_get(trans->session_cache, 
+                if (!apr_hash_get(trans->stateful_session_cache, 
                         tmsg->thread, APR_HASH_KEY_STRING)) {
 
-                    osrfLogDebug(OSRF_LOG_MARK, 
-                        "WS caching sender thread=%s, sender=%s", 
-                        tmsg->thread, tmsg->sender);
+                    apr_size_t ses_size = 
+                        apr_hash_count(trans->stateful_session_cache);
 
-                    apr_hash_set(trans->session_cache, 
-                        apr_pstrdup(trans->session_pool, tmsg->thread),
-                        APR_HASH_KEY_STRING, 
-                        apr_pstrdup(trans->session_pool, tmsg->sender));
+                    if (ses_size < MAX_ACTIVE_STATEFUL_SESSIONS) {
+
+                        osrfLogDebug(OSRF_LOG_MARK, "WS caching sender "
+                            "thread=%s, sender=%s; concurrent=%d", 
+                            tmsg->thread, tmsg->sender, ses_size);
+
+                        apr_hash_set(trans->stateful_session_cache, 
+                            apr_pstrdup(trans->stateful_session_pool, tmsg->thread),
+                            APR_HASH_KEY_STRING, 
+                            apr_pstrdup(trans->stateful_session_pool, tmsg->sender));
+
+                    } else {
+                        osrfLogWarning(OSRF_LOG_MARK, 
+                            "WS max concurrent sessions (%d) reached.  "
+                            "Current session will not be tracked",
+                            MAX_ACTIVE_STATEFUL_SESSIONS
+                        );
+                    }
                 }
 
             } else {
 
                 // connection timed out; clear the cached recipient
-                // regardless of whether the client is still connected
-                if (one_msg->status_code == OSRF_STATUS_TIMEOUT)
+                if (one_msg->status_code == OSRF_STATUS_TIMEOUT) {
                     clear_cached_recipient(tmsg->thread);
+
+                } else {
+                    if (one_msg->status_code == OSRF_STATUS_COMPLETE)
+                        requests_in_flight--;
+                }
             }
         }
     }
@@ -265,16 +301,7 @@ void* osrf_responder_thread_main_body(transport_message *tmsg) {
     // newly created osrfList.  We only need to free the list and 
     // the individual osrfMessage's will be freed along with it
     osrfListFree(msg_list);
-
-    if (!trans->client_connected) {
-
-        osrfLogInfo(OSRF_LOG_MARK, 
-            "WS discarding response for thread=%s", tmsg->thread);
-
-        return;
-    }
     
-    // client is still connected. 
     // relay the response messages to the client
     jsonObject *msg_wrapper = NULL;
     char *msg_string = NULL;
@@ -303,9 +330,67 @@ void* osrf_responder_thread_main_body(transport_message *tmsg) {
 }
 
 /**
+ * Responder thread main body.
+ * Collects responses from the opensrf network and relays them to the 
+ * websocket caller.
+ */
+void* APR_THREAD_FUNC osrf_responder_thread_main(apr_thread_t *thread, void *data) {
+
+    transport_message *tmsg;
+    while (1) {
+
+        if (apr_thread_mutex_unlock(trans->mutex) != APR_SUCCESS) {
+            osrfLogError(OSRF_LOG_MARK, "WS error un-locking thread mutex");
+            return NULL;
+        }
+
+        // wait for a response
+        tmsg = client_recv(osrf_handle, -1);
+
+        if (!tmsg) continue; // interrupt
+
+        if (trans->client_connected) {
+
+            if (apr_thread_mutex_lock(trans->mutex) != APR_SUCCESS) {
+                osrfLogError(OSRF_LOG_MARK, "WS error locking thread mutex");
+                return NULL;
+            }
+
+            osrfLogForceXid(tmsg->osrf_xid);
+            osrf_responder_thread_main_body(tmsg);
+            last_activity_time = time(NULL);
+        }
+
+        message_free(tmsg);                                                         
+    }
+
+    return NULL;
+}
+
+static int active_connection_count() {
+
+    if (requests_in_flight) {
+
+        time_t now = time(NULL);
+        time_t difference = now - last_activity_time;
+
+        if (difference >= max_request_wait_time) {
+            osrfLogWarning(OSRF_LOG_MARK, 
+                "%d In-flight request(s) took longer than %d seconds "
+                "to complete.  Treating request as dead and moving on.",
+                requests_in_flight, 
+                max_request_wait_time
+            );
+            requests_in_flight = 0;
+        }
+    }
+
+    return requests_in_flight;
+}
+
+/**
  * Sleep and regularly wake to see if the process has been idle for too
  * long.  If so, send a disconnect to the client.
- *
  */
 void* APR_THREAD_FUNC osrf_idle_timeout_thread_main(
         apr_thread_t *thread, void *data) {
@@ -327,7 +412,7 @@ void* APR_THREAD_FUNC osrf_idle_timeout_thread_main(
         // During graceful shtudown, we may wait up to 
         // idle_check_interval seconds before initiating shutdown.
         sleep(sleep_time);
-        
+
         if (apr_thread_mutex_lock(trans->mutex) != APR_SUCCESS) {
             osrfLogError(OSRF_LOG_MARK, "WS error locking thread mutex");
             return NULL;
@@ -339,8 +424,8 @@ void* APR_THREAD_FUNC osrf_idle_timeout_thread_main(
             continue;
         }
 
-        // do we have any active conversations with the connected client?
-        int active_count = apr_hash_count(trans->session_cache);
+        // do we have any active stateful conversations with the client?
+        int active_count = active_connection_count();
 
         if (active_count) {
 
@@ -385,7 +470,7 @@ void* APR_THREAD_FUNC osrf_idle_timeout_thread_main(
             time_t difference = now - last_activity_time;
 
             osrfLogDebug(OSRF_LOG_MARK, 
-                "WS has been idle for %d seconds", difference);
+                "WS connection idle for %d seconds", difference);
 
             if (difference < idle_timeout_interval) {
                 // Last activity occurred within the idle timeout interval.
@@ -412,40 +497,90 @@ void* APR_THREAD_FUNC osrf_idle_timeout_thread_main(
     return NULL;
 }
 
-/**
- * Responder thread main body.
- * Collects responses from the opensrf network and relays them to the 
- * websocket caller.
- */
-void* APR_THREAD_FUNC osrf_responder_thread_main(apr_thread_t *thread, void *data) {
+static int build_startup_data(const WebSocketServer *server) {
 
-    transport_message *tmsg;
-    while (1) {
+    apr_pool_t *main_pool = NULL;                                                
+    apr_pool_t *stateful_session_pool = NULL;                                                
+    apr_thread_t *thread = NULL;
+    apr_threadattr_t *thread_attr = NULL;
+    apr_thread_mutex_t *mutex = NULL;
+    request_rec *r = server->request(server);
 
-        if (apr_thread_mutex_unlock(trans->mutex) != APR_SUCCESS) {
-            osrfLogError(OSRF_LOG_MARK, "WS error un-locking thread mutex");
-            return NULL;
-        }
+    // create a pool for our translator data
+    // Do not use r->pool as the parent, since r->pool will be freed
+    // when the current client disconnects.
+    if (apr_pool_create(&main_pool, NULL) != APR_SUCCESS) {
+        osrfLogError(OSRF_LOG_MARK, "WS Unable to create apr_pool");
+        return 1;
+    }
 
-        // wait for a response
-        tmsg = client_recv(osrf_handle, -1);
+    trans = (osrfWebsocketTranslator*) 
+        apr_palloc(main_pool, sizeof(osrfWebsocketTranslator));
 
-        if (!tmsg) continue; // early exit on interrupt
+    if (trans == NULL) {
+        osrfLogError(OSRF_LOG_MARK, "WS Unable to create translator");
+        return 1;
+    }
 
-        if (apr_thread_mutex_lock(trans->mutex) != APR_SUCCESS) {
-            osrfLogError(OSRF_LOG_MARK, "WS error locking thread mutex");
-            return NULL;
-        }
+    trans->server = server;
+    trans->main_pool = main_pool;
+    trans->osrf_router = osrfConfigGetValue(NULL, "/router_name");                      
+    trans->osrf_domain = osrfConfigGetValue(NULL, "/domain");
 
-        osrfLogForceXid(tmsg->osrf_xid);
-        osrf_responder_thread_main_body(tmsg);
-        message_free(tmsg);                                                         
-        last_activity_time = time(NULL);
+    // opensrf session / recipient cache
+    trans->stateful_session_cache = apr_hash_make(trans->main_pool);
+    if (trans->stateful_session_cache == NULL) {
+        osrfLogError(OSRF_LOG_MARK, "WS unable to create session cache");
+        return 1;
     }
 
-    return NULL;
-}
+    // opensrf session / recipient string pool; cleared regularly
+    // the only data entering this pools are the session strings.
+    if (apr_pool_create(&stateful_session_pool, trans->main_pool) != APR_SUCCESS) {
+        osrfLogError(OSRF_LOG_MARK, "WS Unable to create apr_pool");
+        return NULL;
+    }
+    trans->stateful_session_pool = stateful_session_pool;
+
+    if (apr_thread_mutex_create(
+            &mutex, APR_THREAD_MUTEX_UNNESTED, 
+            trans->main_pool) != APR_SUCCESS) {
+        osrfLogError(OSRF_LOG_MARK, "WS unable to create thread mutex");
+        return 1;
+    }
+    trans->mutex = mutex;
+
+    // responder thread
+    if ( (apr_threadattr_create(&thread_attr, trans->main_pool) == APR_SUCCESS) &&
+         (apr_threadattr_detach_set(thread_attr, 0) == APR_SUCCESS) &&
+         (apr_thread_create(&thread, thread_attr, 
+                osrf_responder_thread_main, trans, trans->main_pool) == APR_SUCCESS)) {
+
+        trans->responder_thread = thread;
+        
+    } else {
+        osrfLogError(OSRF_LOG_MARK, "WS unable to create responder thread");
+        return 1;
+    }
+
+    // idle timeout thread
+    thread = NULL; // reset
+    thread_attr = NULL; // reset
+    if ( (apr_threadattr_create(&thread_attr, trans->main_pool) == APR_SUCCESS) &&
+         (apr_threadattr_detach_set(thread_attr, 0) == APR_SUCCESS) &&
+         (apr_thread_create(&thread, thread_attr, 
+            osrf_idle_timeout_thread_main, trans, trans->main_pool) == APR_SUCCESS)) {
+
+        osrfLogDebug(OSRF_LOG_MARK, "WS created idle timeout thread");
+        trans->idle_timeout_thread = thread;
+        
+    } else {
+        osrfLogError(OSRF_LOG_MARK, "WS unable to create idle timeout thread");
+        return 1;
+    }
 
+    return APR_SUCCESS;
+}
 
 
 /**
@@ -453,14 +588,8 @@ void* APR_THREAD_FUNC osrf_responder_thread_main(apr_thread_t *thread, void *dat
  * session cache and session pool.
  */
 int child_init(const WebSocketServer *server) {
-
-    apr_pool_t *pool = NULL;                                                
-    apr_thread_t *thread = NULL;
-    apr_threadattr_t *thread_attr = NULL;
-    apr_thread_mutex_t *mutex = NULL;
     request_rec *r = server->request(server);
 
-
     // osrf_handle will already be connected if this is not the first request
     // served by this process.
     if ( !(osrf_handle = osrfSystemGetTransportClient()) ) {
@@ -479,6 +608,21 @@ int child_init(const WebSocketServer *server) {
         ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, 
             "WS: timeout set to %d", idle_timeout_interval);
 
+        timeout = getenv("OSRF_WEBSOCKET_MAX_REQUEST_WAIT_TIME");
+        if (timeout) {
+            if (!atoi(timeout)) {
+                ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, 
+                    "WS: invalid OSRF_WEBSOCKET_MAX_REQUEST_WAIT_TIME: %s", 
+                    timeout
+                );
+            } else {
+                max_request_wait_time = (time_t) atoi(timeout);
+            }
+        }
+
+        ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, 
+            "WS: max request wait time set to %d", max_request_wait_time);
+
         char* interval = getenv("OSRF_WEBSOCKET_IDLE_CHECK_INTERVAL");
         if (interval) {
             if (!atoi(interval)) {
@@ -524,76 +668,7 @@ int child_init(const WebSocketServer *server) {
         osrf_handle = osrfSystemGetTransportClient();
     }
 
-    // create a standalone pool for our translator data
-    if (apr_pool_create(&pool, NULL) != APR_SUCCESS) {
-        osrfLogError(OSRF_LOG_MARK, "WS Unable to create apr_pool");
-        return 1;
-    }
-
-    // allocate our static translator instance
-    trans = (osrfWebsocketTranslator*) 
-        apr_palloc(pool, sizeof(osrfWebsocketTranslator));
-
-    if (trans == NULL) {
-        osrfLogError(OSRF_LOG_MARK, "WS Unable to create translator");
-        return 1;
-    }
-
-    trans->main_pool = pool;
-    trans->server = server;
-    trans->osrf_router = osrfConfigGetValue(NULL, "/router_name");                      
-    trans->osrf_domain = osrfConfigGetValue(NULL, "/domain");
-
-    trans->session_cache = apr_hash_make(pool);
-
-    if (trans->session_cache == NULL) {
-        osrfLogError(OSRF_LOG_MARK, "WS unable to create session cache");
-        return 1;
-    }
-
-    // Create the responder thread.  Once created, 
-    // it runs for the lifetime of this process.
-    if ( (apr_threadattr_create(&thread_attr, trans->main_pool) == APR_SUCCESS) &&
-         (apr_threadattr_detach_set(thread_attr, 0) == APR_SUCCESS) &&
-         (apr_thread_create(&thread, thread_attr, 
-                osrf_responder_thread_main, trans, trans->main_pool) == APR_SUCCESS)) {
-
-        trans->responder_thread = thread;
-        
-    } else {
-        osrfLogError(OSRF_LOG_MARK, "WS unable to create responder thread");
-        return 1;
-    }
-
-    // Create the idle timeout thread, which lives for the lifetime
-    // of the process.
-    thread = NULL; // reset
-    thread_attr = NULL; // reset
-    if ( (apr_threadattr_create(&thread_attr, trans->main_pool) == APR_SUCCESS) &&
-         (apr_threadattr_detach_set(thread_attr, 0) == APR_SUCCESS) &&
-         (apr_thread_create(&thread, thread_attr, 
-            osrf_idle_timeout_thread_main, trans, trans->main_pool) == APR_SUCCESS)) {
-
-        osrfLogDebug(OSRF_LOG_MARK, "WS created idle timeout thread");
-        trans->idle_timeout_thread = thread;
-        
-    } else {
-        osrfLogError(OSRF_LOG_MARK, "WS unable to create idle timeout thread");
-        return 1;
-    }
-
-
-    if (apr_thread_mutex_create(
-            &mutex, APR_THREAD_MUTEX_UNNESTED, 
-            trans->main_pool) != APR_SUCCESS) {
-        osrfLogError(OSRF_LOG_MARK, "WS unable to create thread mutex");
-        return 1;
-    }
-
-    trans->mutex = mutex;
-
     signal(SIGUSR1, sigusr1_handler);
-
     return APR_SUCCESS;
 }
 
@@ -602,32 +677,23 @@ int child_init(const WebSocketServer *server) {
  */
 void* CALLBACK on_connect_handler(const WebSocketServer *server) {
     request_rec *r = server->request(server);
-    apr_pool_t *pool;
-    apr_thread_t *thread = NULL;
-    apr_threadattr_t *thread_attr = NULL;
 
-    const char* client_ip = get_client_ip(r);
-    osrfLogInfo(OSRF_LOG_MARK, "WS connect from %s", client_ip);
+    if (!trans) { // first connection
 
-    if (!trans) {
-        // first connection
-        if (child_init(server) != APR_SUCCESS) {
+        // connect to opensrf
+        if (child_init(server) != APR_SUCCESS)
             return NULL;
-        }
-    }
 
-    // create a standalone pool for the session cache values
-    // this pool will be destroyed and re-created regularly
-    // to clear session memory
-    if (apr_pool_create(&pool, r->pool) != APR_SUCCESS) {
-        osrfLogError(OSRF_LOG_MARK, "WS Unable to create apr_pool");
-        return NULL;
+        // build pools, thread data, and the translator
+        if (build_startup_data(server) != APR_SUCCESS)
+            return NULL;
     }
 
-    trans->session_pool = pool;
-    trans->client_connected = 1;
-    last_activity_time = time(NULL);
+    const char* client_ip = get_client_ip(r);
+    osrfLogInfo(OSRF_LOG_MARK, "WS connect from %s", client_ip);
 
+    last_activity_time = time(NULL);
+    trans->client_connected = 1;
     return trans;
 }
 
@@ -702,6 +768,7 @@ static char* extract_inbound_messages(
                 }
                 osrfLogActivity(OSRF_LOG_MARK, "%s", act->buf);
                 buffer_free(act);
+                requests_in_flight++;
                 break;
             }
 
@@ -793,7 +860,7 @@ static size_t on_message_handler_body(void *data,
         // since clients can provide their own threads at session start time,
         // the presence of a thread does not guarantee a cached recipient
         recipient = (char*) apr_hash_get(
-            trans->session_cache, thread, APR_HASH_KEY_STRING);
+            trans->stateful_session_cache, thread, APR_HASH_KEY_STRING);
 
         if (recipient) {
             osrfLogDebug(OSRF_LOG_MARK, "WS found cached recipient %s", recipient);
@@ -837,7 +904,6 @@ static size_t on_message_handler_body(void *data,
     free(msg_body);
 
     last_activity_time = time(NULL);
-
     return OK;
 }
 
@@ -867,46 +933,26 @@ static size_t CALLBACK on_message_handler(void *data,
 void CALLBACK on_disconnect_handler(
     void *data, const WebSocketServer *server) {
 
-    osrfWebsocketTranslator *trans = (osrfWebsocketTranslator*) data;
+    // if the threads wake up during disconnect, this tells 
+    // them to go back to sleep.
     trans->client_connected = 0;
 
-    // timeout thread is recreated w/ each new connection
-    apr_thread_exit(trans->idle_timeout_thread, APR_SUCCESS);
-    trans->idle_timeout_thread = NULL;
-    
-    // ensure no errant session data is sticking around
-    apr_hash_clear(trans->session_cache);
-
-    // strictly speaking, this pool will get destroyed when
-    // r->pool is destroyed, but it doesn't hurt to explicitly
-    // destroy it ourselves.
-    apr_pool_destroy(trans->session_pool);
-    trans->session_pool = NULL;
-
     request_rec *r = server->request(server);
-
     osrfLogInfo(OSRF_LOG_MARK, "WS disconnect from %s", get_client_ip(r)); 
-}
-
-/**
- * Be nice and clean up our mess
- */
-void CALLBACK on_destroy_handler(WebSocketPlugin *plugin) {
-    if (trans) {
-        apr_thread_exit(trans->responder_thread, APR_SUCCESS);
-        apr_thread_mutex_destroy(trans->mutex);
-        if (trans->session_pool)
-            apr_pool_destroy(trans->session_pool);
-        apr_pool_destroy(trans->main_pool);
-    }
 
-    trans = NULL;
+    // Clear any lingering session data
+    // NOTE: we could apr_pool_destroy the stateful_session_pool to truly free
+    // the memory, but since there is a limit to the size of the pool
+    // (max_concurrent_sessions), the memory cannot grow unbounded, 
+    // so there's no need.
+    apr_hash_clear(trans->stateful_session_cache);
+    apr_pool_clear(trans->stateful_session_pool);
 }
 
 static WebSocketPlugin osrf_websocket_plugin = {
     sizeof(WebSocketPlugin),
     WEBSOCKET_PLUGIN_VERSION_0,
-    on_destroy_handler,
+    NULL, // on_destroy_handler
     on_connect_handler,
     on_message_handler,
     on_disconnect_handler

commit 4ce075beeb5f81b8eb4c2cb2669b3a99ebab40ba
Author: Bill Erickson <berick at esilibrary.com>
Date:   Mon Mar 10 15:55:24 2014 -0400

    LP#1268619: websockets: apply syslog name in gateway
    
    Signed-off-by: Bill Erickson <berick at esilibrary.com>
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>

diff --git a/src/gateway/osrf_websocket_translator.c b/src/gateway/osrf_websocket_translator.c
index 0d06bd4..5b9d607 100644
--- a/src/gateway/osrf_websocket_translator.c
+++ b/src/gateway/osrf_websocket_translator.c
@@ -520,6 +520,7 @@ int child_init(const WebSocketServer *server) {
             return 1;
         }
 
+        osrfLogSetAppname("osrf_websocket_translator");
         osrf_handle = osrfSystemGetTransportClient();
     }
 

commit 77c7f5889e8f31038cd732feb3e6057a0d3788b5
Author: Bill Erickson <berick at esilibrary.com>
Date:   Mon Mar 10 08:41:45 2014 -0400

    LP#1268619: websockets: README typo repairs
    
    Patch from Warren Layton to repair path to 'envvars' and clarify some
    text.
    
    Signed-off-by: Bill Erickson <berick at esilibrary.com>
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>

diff --git a/README.websockets b/README.websockets
index 639e080..69a56c0 100644
--- a/README.websockets
+++ b/README.websockets
@@ -22,7 +22,7 @@ Websockets installation instructions for Debian
 % cp /path/to/OpenSRF/examples/apache2/websockets/apache2.conf /etc/apache2-websockets/
 
 # OPTIONAL: add these configuration variables to
-# /etc/init.d/apache2-websockets/envvars and adjust as needed.
+# /etc/apache2-websockets/envvars and adjust as needed.
 # export OSRF_WEBSOCKET_IDLE_TIMEOUT=60
 # export OSRF_WEBSOCKET_IDLE_CHECK_INTERVAL=5
 # export OSRF_WEBSOCKET_CONFIG_FILE=/openils/conf/opensrf_core.xml
@@ -41,6 +41,6 @@ Websockets installation instructions for Debian
 # CONFIG_FILE / CTXT are the standard opensrf core config options.
 
 # After OpenSRF is up and running (or after any re-install),
-# fire up the secondary Apache instance errors will appear in
+# fire up the secondary Apache instance. Errors will appear in
 # /var/log/apache2-websockets/error.log
 % /etc/init.d/apache2-websockets start

commit bb424c1424193e4db340e3f6e17a939dc1dd821a
Author: Bill Erickson <berick at esilibrary.com>
Date:   Thu Mar 6 15:47:36 2014 -0500

    LP#1268619: websockets : additional apache config docs
    
    Signed-off-by: Bill Erickson <berick at esilibrary.com>
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>

diff --git a/README.websockets b/README.websockets
index 0b94fd3..639e080 100644
--- a/README.websockets
+++ b/README.websockets
@@ -1,46 +1,27 @@
-
-Websockets installation instructions for Debian:
+Websockets installation instructions for Debian
 
 # TODO: Most of this can be scripted.
-# TODO: Better handling of external dependencies (websocket_plugin.h).  
 
-# as root
+# ! as root !
+# Perform these steps after installing OpenSRF.
+
+# install the apache-websocket module
+% cd tmp # or wherever
+% git clone https://github.com/disconnect/apache-websocket
+% cd apache-websocket
+% apxs2 -i -a -c mod_websocket.c
 
+# create the websocket Apache instance
 # see also /usr/share/doc/apache2/README.multiple-instances
 % sh /usr/share/doc/apache2.2-common/examples/setup-instance websockets
 
-% cp examples/apache2/websockets.conf /etc/apache2-websockets/sites-available/
-
-# activate the websockets configuration
-% a2ensite-websockets websockets.conf 
-
-# deactivate the default site
-% a2dissite-websockets default 
-
-# remove most of the mods with this shell script
-
-MODS=$(apache2ctl-websockets -M | grep shared | grep -v 'Syntax OK' | sed 's/_module//g' | cut -d' ' -f2 | xargs);
-for mod in $MODS; do
-    if [ $mod = 'mime' -o $mod = 'ssl' -o $mod = 'websocket' ]; then
-        echo "* Leaving module $mod in place";
-    else
-        echo "* Disabling module $mod";
-        a2dismod-websockets $mod;
-    fi;
-done
+# remove from the main apache instance
+% a2dismod websocket
 
-# follow the instructions for installing Apache mod_websockets at
-# https://github.com/disconnect/apache-websocket
+# update configs
+% cp /path/to/OpenSRF/examples/apache2/websockets/apache2.conf /etc/apache2-websockets/
 
-# copy the headers into place so OpenSRF can compile
-% cp $LOCATION_OF_APACHE_WEBSOCKET_CHECKOUT/websocket_plugin.h src/gateway/
-
-# install OpenSRF
-
-# remove the websocket module from the default OpenSRF Apache instance
-% a2dismod osrf_websocket_translator
-
-# optional: add these configuration variables to 
+# OPTIONAL: add these configuration variables to
 # /etc/init.d/apache2-websockets/envvars and adjust as needed.
 # export OSRF_WEBSOCKET_IDLE_TIMEOUT=60
 # export OSRF_WEBSOCKET_IDLE_CHECK_INTERVAL=5
@@ -49,7 +30,7 @@ done
 #
 # IDLE_TIMEOUT specifies how long we will allow a client to stay connected
 # while idle.  A longer timeout means less network traffic (from fewer
-# websocket CONNECT calls), but it also means more Apache processes are 
+# websocket CONNECT calls), but it also means more Apache processes are
 # tied up doing nothing.
 #
 # IDLE_CHECK_INTERVAL specifies how often we wake to check the idle status
@@ -59,8 +40,7 @@ done
 #
 # CONFIG_FILE / CTXT are the standard opensrf core config options.
 
-# After OpenSRF is up and running, fire up the secondary Apache instance
-# errors will appear in /var/log/apache2-websockets/error.log
-% /etc/init.d/apache2-websockets restart
-
-
+# After OpenSRF is up and running (or after any re-install),
+# fire up the secondary Apache instance errors will appear in
+# /var/log/apache2-websockets/error.log
+% /etc/init.d/apache2-websockets start
diff --git a/examples/apache2/websockets.conf b/examples/apache2/websockets/apache2.conf
similarity index 58%
rename from examples/apache2/websockets.conf
rename to examples/apache2/websockets/apache2.conf
index 6b8433a..0aaca73 100644
--- a/examples/apache2/websockets.conf
+++ b/examples/apache2/websockets/apache2.conf
@@ -1,19 +1,47 @@
-# :vim set syntax apache                                                        
-#
-# This is the top-level configuration file for the 
-# apache2-websockets instance.  For example, in Debian
-# this file lives in /etc/apache2-websockets/sites-available/
-                                                                                
-LogLevel info
-# - log locally                                                                 
-CustomLog /var/log/apache2-websockets/access.log combined                       
-ErrorLog /var/log/apache2-websockets/error.log
-# Add the PID to the error log (Apache 2.4 only)
-# ErrorLogFormat "[%t] [%P] [%l] [pid %P] %F: %E: [client %a] %M"                
-                                                                                
-# ----------------------------------------------------------------------------------
-# Set up our SSL virtual host                                                   
-# ----------------------------------------------------------------------------------
+# This is the main Apache server configuration file for the OpenSRF
+# WebSockets gateway.  
+
+# if we don't want to run as "opensrf", change the LockFile
+LockFile ${APACHE_LOCK_DIR}/accept.lock
+PidFile ${APACHE_PID_FILE}
+User ${APACHE_RUN_USER}
+Group ${APACHE_RUN_GROUP}
+
+DefaultType None
+HostnameLookups Off
+ErrorLog ${APACHE_LOG_DIR}/error.log
+LogLevel warn
+
+# only affects the initial connection, which should be quick.
+Timeout 30
+
+# WebSockets is KeepAlive on steroids
+KeepAlive Off
+
+<IfModule mpm_prefork_module>
+    StartServers          5
+    MinSpareServers       5
+    MaxSpareServers      20
+    # use ServerLimit to raise beyond 256
+    MaxClients          256
+    MaxRequestsPerChild   0
+</IfModule>
+
+# include the exact mods we need
+Include mods-available/mime.load
+Include mods-available/mime.conf
+# SSL requires mime
+Include mods-available/ssl.load
+Include mods-available/ssl.conf
+Include mods-available/websocket.load
+
+LogFormat "%v:%p %h %l %u %t \"%r\" %>s %O \"%{Referer}i\" \"%{User-Agent}i\"" vhost_combined
+LogFormat "%h %l %u %t \"%r\" %>s %O \"%{Referer}i\" \"%{User-Agent}i\"" combined
+LogFormat "%h %l %u %t \"%r\" %>s %O" common
+LogFormat "%{Referer}i -> %U" referer
+LogFormat "%{User-agent}i" agent
+
+# WebSockets via SSL
 Listen 7682
 NameVirtualHost *:7682                                                          
 <VirtualHost *:7682>                                                            
@@ -29,6 +57,7 @@ NameVirtualHost *:7682
     SSLCertificateKeyFile /etc/apache2/ssl/server.key
 </VirtualHost>                                                                  
                                                                                 
+# WebSockets via non-SSL
 Listen 7680
 NameVirtualHost *:7680                                                          
 <VirtualHost *:7680>                                                            
@@ -37,6 +66,7 @@ NameVirtualHost *:7680
     DocumentRoot /var/www                                                       
 </VirtualHost>                                                                  
                                                                                 
+# OpenSRF WebSockets gateway
 <Location /osrf-websocket-translator>                                           
     SetHandler websocket-handler                                                
     WebSocketHandler /usr/lib/apache2/modules/osrf_websocket_translator.so osrf_websocket_init

commit 9f2d35e6f6b6ec5a3db109adb518cf270d49c683
Author: Bill Erickson <berick at esilibrary.com>
Date:   Thu Mar 6 15:05:12 2014 -0500

    LP#1268619: websockets: apache conf -> info logging
    
    Signed-off-by: Bill Erickson <berick at esilibrary.com>
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>

diff --git a/examples/apache2/websockets.conf b/examples/apache2/websockets.conf
index ae598a1..6b8433a 100644
--- a/examples/apache2/websockets.conf
+++ b/examples/apache2/websockets.conf
@@ -4,7 +4,7 @@
 # apache2-websockets instance.  For example, in Debian
 # this file lives in /etc/apache2-websockets/sites-available/
                                                                                 
-LogLevel debug                                                                  
+LogLevel info
 # - log locally                                                                 
 CustomLog /var/log/apache2-websockets/access.log combined                       
 ErrorLog /var/log/apache2-websockets/error.log

commit b1b3bf60f613b94d1455afb54dff4ad4a7e95ddd
Author: Bill Erickson <berick at esilibrary.com>
Date:   Wed Mar 5 08:40:05 2014 -0500

    LP#1268619: websockets: remove single-tab JS WS implementation
    
    It was falling behind the shared lib in bug fixes and features.  A
    per-tab WS implementation is (maybe) a dangerous thing to have around,
    as well, since it encourages /many/ connections.  Can resurrect later if
    needed.
    
    Signed-off-by: Bill Erickson <berick at esilibrary.com>
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>

diff --git a/src/javascript/opensrf_ws.js b/src/javascript/opensrf_ws.js
deleted file mode 100644
index 4df7c8f..0000000
--- a/src/javascript/opensrf_ws.js
+++ /dev/null
@@ -1,165 +0,0 @@
-/* -----------------------------------------------------------------------
- * Copyright (C) 2012  Equinox Software, Inc.
- * Bill Erickson <berick at esilibrary.com>
- *  
- * 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;
-};
-
-
-
-

commit 6765c6395b0fb2a1c501f7c94a04cfacc7d460db
Author: Bill Erickson <berick at esilibrary.com>
Date:   Tue Mar 4 14:10:59 2014 -0500

    LP#1268619: websockets: shared worker path; JS api_level
    
    Signed-off-by: Bill Erickson <berick at esilibrary.com>
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>

diff --git a/src/javascript/opensrf.js b/src/javascript/opensrf.js
index d578aef..db6e49d 100644
--- a/src/javascript/opensrf.js
+++ b/src/javascript/opensrf.js
@@ -48,6 +48,9 @@ var OSRF_STATUS_INTERNALSERVERERROR = 500;
 var OSRF_STATUS_NOTIMPLEMENTED = 501;
 var OSRF_STATUS_VERSIONNOTSUPPORTED = 505;
 
+// TODO: get path from ./configure prefix
+var SHARED_WORKER_LIB = '/js/dojo/opensrf/opensrf_ws_shared.js'; 
+
 /* The following classes map directly to network-serializable opensrf objects */
 
 function osrfMessage(hash) {
@@ -76,6 +79,11 @@ osrfMessage.prototype.locale = function(d) {
         this.hash.locale = d; 
     return this.hash.locale; 
 };
+osrfMessage.prototype.api_level = function(d) { 
+    if(arguments.length == 1) 
+        this.hash.api_level = d; 
+    return this.hash.api_level; 
+};
 osrfMessage.prototype.serialize = function() {
     return {
         "__c":"osrfMessage",
@@ -83,7 +91,8 @@ osrfMessage.prototype.serialize = function() {
             'threadTrace' : this.hash.threadTrace,
             'type' : this.hash.type,
             'payload' : (this.hash.payload) ? this.hash.payload.serialize() : 'null',
-            'locale' : this.hash.locale
+            'locale' : this.hash.locale,
+            'api_level' : this.hash.api_level
         }
     };
 };
@@ -190,6 +199,7 @@ osrfContinueStatus.prototype.statusCode = function(d) {
 
 OpenSRF = {};
 OpenSRF.locale = null;
+OpenSRF.api_level = 1;
 
 /* makes cls a subclass of pcls */
 OpenSRF.set_subclass = function(cls, pcls) {
@@ -207,10 +217,9 @@ OpenSRF.Session = function() {
     this.state = OSRF_APP_SESSION_DISCONNECTED;
 };
 
-//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];
 };
@@ -247,17 +256,23 @@ OpenSRF.Session.prototype.send_ws = function(osrf_msg) {
 };
 
 OpenSRF.Session.setup_shared_ws = function() {
-    // TODO path
-    OpenSRF.sharedWSWorker = new SharedWorker('opensrf_ws_shared.js'); 
+    OpenSRF.sharedWSWorker = new SharedWorker(SHARED_WORKER_LIB);
 
     OpenSRF.sharedWSWorker.port.addEventListener('message', function(e) {                          
         var data = e.data;
-        console.log('sharedWSWorker received message ' + data.action);
+        console.debug('sharedWSWorker received message of type: ' + data.action);
 
         if (data.action == 'message') {
             // pass all inbound message up the opensrf stack
 
-            var msg = JSON2js(data.message); // TODO: json error handling
+            var msg;
+            try {
+                msg = JSON2js(data.message);
+            } catch(E) {
+                console.error(
+                    "Error parsing JSON in shared WS response: " + msg);
+                throw E;
+            }
             OpenSRF.Stack.push(                                                        
                 new OpenSRF.NetMessage(                                                
                    null, null, msg.thread, null, msg.osrf_msg)                        
@@ -407,6 +422,7 @@ OpenSRF.Request = function(session, reqid, args) {
     this.method = args.method;
     this.params = args.params;
     this.timeout = args.timeout;
+    this.api_level = args.api_level || OpenSRF.api_level;
     this.response_queue = [];
     this.complete = false;
 };
@@ -438,7 +454,8 @@ OpenSRF.Request.prototype.send = function() {
         'threadTrace' : this.reqid, 
         'type' : OSRF_MESSAGE_TYPE_REQUEST, 
         'payload' : method, 
-        'locale' : this.session.locale
+        'locale' : this.session.locale,
+        'api_level' : this.api_level
     });
 
     this.session.send(message, {

commit f5ada2850552560a24e473e02532e75870ab7307
Author: Bill Erickson <berick at esilibrary.com>
Date:   Mon Mar 3 16:23:44 2014 -0500

    LP#1268619: websockets: initial C libs api_level support
    
    Signed-off-by: Bill Erickson <berick at esilibrary.com>
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>

diff --git a/src/libopensrf/osrf_message.c b/src/libopensrf/osrf_message.c
index 364c875..3681dfa 100644
--- a/src/libopensrf/osrf_message.c
+++ b/src/libopensrf/osrf_message.c
@@ -425,6 +425,9 @@ jsonObject* osrfMessageToJSON( const osrfMessage* msg ) {
 	if (msg->sender_ingress != NULL) 
 		jsonObjectSetKey(json, "ingress", jsonNewObject(msg->sender_ingress));
 
+	if (msg->protocol > 0) 
+		jsonObjectSetKey(json, "api_level", jsonNewNumberObject(msg->protocol));
+
 	switch(msg->m_type) {
 
 		case CONNECT:
@@ -620,7 +623,7 @@ static osrfMessage* deserialize_one_message( const jsonObject* obj ) {
 
 	// Get the protocol, defaulting to zero.
 	int protocol = 0;
-	tmp = jsonObjectGetKeyConst( obj, "protocol" );
+	tmp = jsonObjectGetKeyConst( obj, "api_level" );
 	if(tmp) {
 		const char* proto = jsonObjectGetString(tmp);
 		if( proto ) {

commit 2bcead2d3e1c560a30c5b9beba6d1cbfe58778ed
Author: Bill Erickson <berick at esilibrary.com>
Date:   Mon Mar 3 16:01:58 2014 -0500

    LP#1268619: websockets : gateway log inbound messages at internal
    
    Signed-off-by: Bill Erickson <berick at esilibrary.com>
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>

diff --git a/src/gateway/osrf_websocket_translator.c b/src/gateway/osrf_websocket_translator.c
index 7aa8676..0d06bd4 100644
--- a/src/gateway/osrf_websocket_translator.c
+++ b/src/gateway/osrf_websocket_translator.c
@@ -653,6 +653,8 @@ static char* extract_inbound_messages(
 
     // here we do an extra json round-trip to get the data
     // in a form osrf_message_deserialize can understand
+    // TODO: consider a version of osrf_message_init which can 
+    // accept a jsonObject* instead of a JSON string.
     char *osrf_msg_json = jsonObjectToJSON(osrf_msg);
     osrf_message_deserialize(osrf_msg_json, msg_list, num_msgs);
     free(osrf_msg_json);
@@ -749,6 +751,8 @@ static size_t on_message_handler_body(void *data,
     memcpy(buf, buffer, buffer_size);
     buf[buffer_size] = '\0';
 
+    osrfLogInternal(OSRF_LOG_MARK, "WS received inbound message: %s", buf);
+
     msg_wrapper = jsonParse(buf);
 
     if (msg_wrapper == NULL) {
@@ -816,6 +820,9 @@ static size_t on_message_handler_body(void *data,
     msg_body = extract_inbound_messages(
         r, service, thread, recipient, osrf_msg);
 
+    osrfLogInternal(OSRF_LOG_MARK, 
+        "WS relaying inbound message: %s", msg_body);
+
     transport_message *tmsg = message_init(
         msg_body, NULL, thread, recipient, NULL);
 

commit 5631bbdfa0f9a4fe2ea1b238c5e2ffee4b606dc1
Author: Bill Erickson <berick at esilibrary.com>
Date:   Mon Mar 3 12:06:12 2014 -0500

    LP#1268619: websocket : add JS lib to makefile
    
    Signed-off-by: Bill Erickson <berick at esilibrary.com>
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>

diff --git a/src/Makefile.am b/src/Makefile.am
index 282f577..7def8b1 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -36,7 +36,7 @@ endif
 if INSTALLJAVASCRIPT
 MAYBE_JAVASCRIPT = javascript
 jsdir = $(prefix)/lib/javascript
-js_SCRIPTS = javascript/DojoSRF.js javascript/JSON_v1.js javascript/md5.js javascript/opensrf.js javascript/opensrf_xhr.js javascript/opensrf_xmpp.js
+js_SCRIPTS = javascript/DojoSRF.js javascript/JSON_v1.js javascript/md5.js javascript/opensrf.js javascript/opensrf_xhr.js javascript/opensrf_xmpp.js javascript/opensrf_ws_shared.js
 endif
 
 if BUILDCORE

commit 0054ea6684a933037af1cc3bc6358c7096dc051c
Author: Bill Erickson <berick at esilibrary.com>
Date:   Mon Mar 3 10:29:23 2014 -0500

    LP#1268619: websocket: avoid module auto configuration
    
    We don't want osrf_websocket_translator to be directly loaded as a
    module, since it is not an apache module, but a shared library loaded by
    an apache module (mod_websockets).  This is especially true of the default
    apache instance.
    
    Signed-off-by: Bill Erickson <berick at esilibrary.com>
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>

diff --git a/src/gateway/Makefile.am b/src/gateway/Makefile.am
index 04a7632..54da170 100644
--- a/src/gateway/Makefile.am
+++ b/src/gateway/Makefile.am
@@ -37,7 +37,7 @@ install-exec-local:
 	$(MKDIR_P) $(DESTDIR)$(AP_LIBEXECDIR)
 	$(APXS2) -i -S LIBEXECDIR=$(DESTDIR)$(AP_LIBEXECDIR) -a @srcdir@/osrf_json_gateway.la
 	$(APXS2) -i -S LIBEXECDIR=$(DESTDIR)$(AP_LIBEXECDIR) -a @srcdir@/osrf_http_translator.la
-	$(APXS2) -n osrf_websocket_translator -i -S LIBEXECDIR=$(DESTDIR)$(AP_LIBEXECDIR) -a @srcdir@/osrf_websocket_translator.la
+	$(APXS2) -n osrf_websocket_translator -i -S LIBEXECDIR=$(DESTDIR)$(AP_LIBEXECDIR) @srcdir@/osrf_websocket_translator.la
 
 clean-local:
 	rm -f @srcdir@/osrf_http_translator.la @srcdir@/osrf_http_translator.lo @srcdir@/osrf_http_translator.slo @srcdir@/osrf_json_gateway.la @srcdir@/osrf_json_gateway.lo @srcdir@/osrf_json_gateway.slo  @srcdir@/osrf_websocket_translator.la @srcdir@/osrf_websocket_translator.lo @srcdir@/osrf_websocket_translator.slo

commit e375a1e31b87a3d645b6da05e83f2a29f885f1fc
Author: Bill Erickson <berick at esilibrary.com>
Date:   Mon Mar 3 10:05:26 2014 -0500

    LP#1268619: websocket : apache gateway minor fixes
    
    Replace remaining remote_ip calls with a get_client_ip() function which
    operates for apache2 and apache4.
    
    Make log functions for applying a log trace value accept const char*'s
    
    Signed-off-by: Bill Erickson <berick at esilibrary.com>
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>

diff --git a/include/opensrf/log.h b/include/opensrf/log.h
index 71b0d60..78ba808 100644
--- a/include/opensrf/log.h
+++ b/include/opensrf/log.h
@@ -107,9 +107,9 @@ void osrfLogCleanup( void );
 
 void osrfLogClearXid( void );
 
-void osrfLogSetXid(char* xid);
+void osrfLogSetXid(const char* xid);
 
-void osrfLogForceXid(char* xid);
+void osrfLogForceXid(const char* xid);
 
 void osrfLogMkXid( void );
 
diff --git a/src/gateway/osrf_websocket_translator.c b/src/gateway/osrf_websocket_translator.c
index 6a0caac..7aa8676 100644
--- a/src/gateway/osrf_websocket_translator.c
+++ b/src/gateway/osrf_websocket_translator.c
@@ -68,12 +68,6 @@
  * export OSRF_WEBSOCKET_CONFIG_CTXT=gateway
  */
 
-/**
- * TODO:
- * short-timeout mode for brick detachment where inactivity timeout drops way 
- * down for graceful disconnects.
- */
-
 #include <stdlib.h>
 #include <signal.h>
 #include <unistd.h>
@@ -111,6 +105,14 @@ static void sigusr1_handler(int sig) {
     osrfLogInfo(OSRF_LOG_MARK, "WS received SIGUSR1 - Graceful Shutdown");
 }
 
+static const char* get_client_ip(const request_rec* r) {
+#ifdef APACHE_MIN_24
+    return r->connection->client_ip;
+#else
+    return r->connection->remote_ip;
+#endif
+}
+
 typedef struct _osrfWebsocketTranslator {
 
     /** Our handle for communicating with the caller */
@@ -603,12 +605,7 @@ void* CALLBACK on_connect_handler(const WebSocketServer *server) {
     apr_thread_t *thread = NULL;
     apr_threadattr_t *thread_attr = NULL;
 
-#ifdef APACHE_MIN_24
-    char* client_ip = r->connection->client_ip;
-#else
-    char* client_ip = r->connection->remote_ip;
-#endif
-
+    const char* client_ip = get_client_ip(r);
     osrfLogInfo(OSRF_LOG_MARK, "WS connect from %s", client_ip);
 
     if (!trans) {
@@ -674,7 +671,7 @@ static char* extract_inbound_messages(
                 growing_buffer* act = buffer_init(128);
                 char* method = msg->method_name;
                 buffer_fadd(act, "[%s] [%s] %s %s", 
-                    r->connection->remote_ip, "", service, method);
+                    get_client_ip(r), "", service, method);
 
                 const jsonObject* obj = NULL;
                 int i = 0;
@@ -880,9 +877,7 @@ void CALLBACK on_disconnect_handler(
 
     request_rec *r = server->request(server);
 
-    osrfLogInfo(OSRF_LOG_MARK, 
-        "WS disconnect from %s", r->connection->remote_ip); 
-        //"WS disconnect from %s", r->connection->client_ip); // apache 2.4
+    osrfLogInfo(OSRF_LOG_MARK, "WS disconnect from %s", get_client_ip(r)); 
 }
 
 /**
diff --git a/src/libopensrf/log.c b/src/libopensrf/log.c
index f298c65..27eb6f3 100644
--- a/src/libopensrf/log.c
+++ b/src/libopensrf/log.c
@@ -126,7 +126,7 @@ void osrfLogClearXid( void ) { _osrfLogSetXid(""); }
 	@brief Store a transaction id, unless running as a client process.
 	@param xid Pointer to the new transaction id
 */
-void osrfLogSetXid(char* xid) {
+void osrfLogSetXid(const char* xid) {
    if(!_osrfLogIsClient) _osrfLogSetXid(xid);
 }
 
@@ -134,7 +134,7 @@ void osrfLogSetXid(char* xid) {
 	@brief Store a transaction id for future use, whether running as a client or as a server.
 	@param xid Pointer to the new transaction id
 */
-void osrfLogForceXid(char* xid) {
+void osrfLogForceXid(const char* xid) {
    _osrfLogSetXid(xid);
 }
 

commit 0a0d3f616c9531c7931c365e1912cbcf6358441b
Author: Bill Erickson <berick at esilibrary.com>
Date:   Mon Mar 3 09:42:03 2014 -0500

    LP#1268619: websocket : more JS api docs
    
    Signed-off-by: Bill Erickson <berick at esilibrary.com>
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>

diff --git a/src/javascript/opensrf_ws_shared.js b/src/javascript/opensrf_ws_shared.js
index fe63a2a..4b90dc8 100644
--- a/src/javascript/opensrf_ws_shared.js
+++ b/src/javascript/opensrf_ws_shared.js
@@ -13,23 +13,54 @@
  * GNU General Public License for more details.
  * ----------------------------------------------------------------------- */
 
+
+/**
+ * Shared WebSocket communication layer.  Each browser tab registers with
+ * this code all inbound / outbound messages are delivered through a
+ * single websocket connection managed within.
+ *
+ * Messages take the form : {action : my_action, message : my_message}
+ * actions for tab-generated messages may be "message" or "close".
+ * actions for messages generated within may be "message" or "error"
+ */
+
 var WEBSOCKET_URL_PATH = '/osrf-websocket-translator';
-var WEBSOCKET_PORT = 7680;
+var WEBSOCKET_PORT = 7680; // TODO: remove.  all traffic should use SSL.
 var WEBSOCKET_PORT_SSL = 7682;
+var WEBSOCKET_MAX_THREAD_PORT_CACHE_SIZE = 1000;
 
-// set of shared ports (i.e. browser tabs)
+/**
+ * Collection of shared ports (browser tabs)
+ */
 var connected_ports = {};
+
+/**
+ * Each port gets a local identifier so we have an easy way to refer to 
+ * it later.
+ */
 var port_identifier = 0;
 
-// maps osrf message threads to a port index in connected_ports
+// maps osrf message threads to a port index in connected_ports.
+// this is how we know which browser tab to deliver messages to.
 var thread_port_map = {}; 
 
-// our shared websocket
+/**
+ * Browser-global, shared websocket connection.
+ */
 var websocket;
 
-// pending messages awaiting a successful websocket connection
+/** 
+ * Pending messages awaiting a successful websocket connection
+ *
+ * instead of asking the caller to pass messages after a connection
+ * is made, queue the messages for the caller and deliver them
+ * after the connection is established.
+ */
 var pending_ws_messages = [];
 
+/** 
+ * Deliver the message blob to the specified port (tab)
+ */
 function send_msg_to_port(ident, msg) {
     console.debug('sending msg to port ' + ident + ' : ' + msg.action);
     try {
@@ -42,19 +73,25 @@ function send_msg_to_port(ident, msg) {
     }
 }
 
-// send a message to all listeners
+/**
+ * Send a message blob to all ports (tabs)
+ */
 function broadcast(msg) {
     for (var ident in connected_ports)
       send_msg_to_port(ident, msg);
 }
 
 
-// opens the websocket connection
-// port_ident refers to the requesting port
+/**
+ * Opens the websocket connection.
+ *
+ * If our global socket is already open, use it.  Otherwise, queue the 
+ * message for delivery after the socket is open.
+ */
 function send_to_websocket(message) {
 
     if (websocket && websocket.readyState == websocket.OPEN) {
-        // websocket connection is viable.  send our message.
+        // websocket connection is viable.  send our message now.
         websocket.send(message);
         return;
     }
@@ -96,11 +133,11 @@ function send_to_websocket(message) {
     websocket.onmessage = function(evt) {
         var message = evt.data;
 
-        // this is a hack to avoid having to run JSON2js multiple 
-        // times on the same message.  Hopefully match() is faster.
-        // We can't use JSON_v1 within a shared worker for marshalling
-        // messages, because it has no knowledge of application-level
-        // class hints in this environment.
+        // this is sort of a hack to avoid having to run JSON2js
+        // multiple times on the same message.  Hopefully match() is
+        // faster.  Note: We can't use JSON_v1 within a shared worker
+        // for marshalling messages, because it has no knowledge of
+        // application-level class hints in this environment.
         var thread;
         var match = message.match(/"thread":"(.*?)"/);
         if (!match || !(thread = match[1])) {
@@ -130,26 +167,50 @@ function send_to_websocket(message) {
          * (see above).  Only the port expecting a message with the given 
          * thread will honor the message, all other ports will drop it 
          * silently.  We could just broadcastfor every messsage, but this 
-         * is more efficient.
+         * is presumably more efficient.
+         *
+         * If for some reason this fails to work as expected, we could add
+         * a new tab->ws message type for marking a thread as complete.
+         * My hunch is this will be faster, since it will require a lot
+         * fewer cross-tab messages overall.
          */
-        if (Object.keys(thread_port_map).length > 1000) 
+        if (Object.keys(thread_port_map).length > 
+                WEBSOCKET_MAX_THREAD_PORT_CACHE_SIZE) {
+            console.debug('resetting thread_port_map');
             thread_port_map = {};
+        }
     }
 
+    /**
+     * Websocket error handler.  This type of error indicates a probelem
+     * with the connection.  I.e. it's not port-specific. 
+     * Broadcast to all ports.
+     */
     websocket.onerror = function(evt) {
         var err = "WebSocket Error " + evt + ' : ' + evt.data;
-        // propagate to all ports so it can be logged, etc. 
         broadcast({action : 'error', message : err});
-        throw new Error(err);
+        websocket.close(); // connection is no good; reset.
+        throw new Error(err); 
     }
 
+    /**
+     * Called when the websocket connection is closed.
+     *
+     * Once a websocket is closed, it will be re-opened the next time
+     * a message delivery attempt is made.  Clean up and prepare to reconnect.
+     */
     websocket.onclose = function() {
         console.debug('closing websocket');
         websocket = null;
+        thread_port_map = {};
     }
 }
 
-// called when a new port (tab) is opened
+/**
+ * New port (tab) opened handler
+ *
+ * Apply the port identifier and message handlers.
+ */
 onconnect = function(e) {
     var port = e.ports[0];
 
@@ -169,7 +230,9 @@ onconnect = function(e) {
         } 
 
         if (messsage.action == 'close') {
-            // TODO: add me to body onunload in calling pages.
+            // TODO: all browser tabs need an onunload handler which sends
+            // a action=close message, so that the port may be removed from
+            // the conected_ports collection.
             delete connected_ports[port_ident];
             console.debug('closed port ' + port_ident + 
                 '; ' + Object.keys(connected_ports).length + ' remaining');

commit aa1c088bd45a254290ad202875eb87c4bd4eeb2a
Author: Bill Erickson <berick at esilibrary.com>
Date:   Thu Jan 16 11:41:01 2014 -0500

    LP#1268619: websockets : added config docs to install readme
    
    Signed-off-by: Bill Erickson <berick at esilibrary.com>
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>

diff --git a/README.websockets b/README.websockets
index c9f2391..0b94fd3 100644
--- a/README.websockets
+++ b/README.websockets
@@ -40,6 +40,25 @@ done
 # remove the websocket module from the default OpenSRF Apache instance
 % a2dismod osrf_websocket_translator
 
+# optional: add these configuration variables to 
+# /etc/init.d/apache2-websockets/envvars and adjust as needed.
+# export OSRF_WEBSOCKET_IDLE_TIMEOUT=60
+# export OSRF_WEBSOCKET_IDLE_CHECK_INTERVAL=5
+# export OSRF_WEBSOCKET_CONFIG_FILE=/openils/conf/opensrf_core.xml
+# export OSRF_WEBSOCKET_CONFIG_CTXT=gateway
+#
+# IDLE_TIMEOUT specifies how long we will allow a client to stay connected
+# while idle.  A longer timeout means less network traffic (from fewer
+# websocket CONNECT calls), but it also means more Apache processes are 
+# tied up doing nothing.
+#
+# IDLE_CHECK_INTERVAL specifies how often we wake to check the idle status
+# of the connected client.
+#
+# Both specified in seconds
+#
+# CONFIG_FILE / CTXT are the standard opensrf core config options.
+
 # After OpenSRF is up and running, fire up the secondary Apache instance
 # errors will appear in /var/log/apache2-websockets/error.log
 % /etc/init.d/apache2-websockets restart
diff --git a/src/gateway/osrf_websocket_translator.c b/src/gateway/osrf_websocket_translator.c
index e2cfc98..6a0caac 100644
--- a/src/gateway/osrf_websocket_translator.c
+++ b/src/gateway/osrf_websocket_translator.c
@@ -98,7 +98,7 @@
 // default values, replaced during setup (below) as needed.
 static char* config_file = "/openils/conf/opensrf_core.xml";
 static char* config_ctxt = "gateway";
-static time_t idle_timeout_interval = 300; 
+static time_t idle_timeout_interval = 60; 
 static time_t idle_check_interval = 5;
 static time_t last_activity_time = 0;
 

commit a64f10c4183a495ec3912458c9b6268856e8fe47
Author: Bill Erickson <berick at esilibrary.com>
Date:   Tue Jan 14 16:25:20 2014 -0500

    LP#1268619: websocket JS libs: reconnect and auto-connect
    
    JS clients no longer need to explicitly call the shared WS setup
    routine.  It happens under the covers now.
    
    JS client detects disconnects and reconnects when the next message send
    attempt occurs.
    
    Signed-off-by: Bill Erickson <berick at esilibrary.com>
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>

diff --git a/src/javascript/opensrf.js b/src/javascript/opensrf.js
index 994fbe6..d578aef 100644
--- a/src/javascript/opensrf.js
+++ b/src/javascript/opensrf.js
@@ -246,7 +246,7 @@ OpenSRF.Session.prototype.send_ws = function(osrf_msg) {
     );
 };
 
-OpenSRF.Session.setup_shared_ws = function(onconnect) {
+OpenSRF.Session.setup_shared_ws = function() {
     // TODO path
     OpenSRF.sharedWSWorker = new SharedWorker('opensrf_ws_shared.js'); 
 
@@ -266,11 +266,6 @@ OpenSRF.Session.setup_shared_ws = function(onconnect) {
             return;
         }
 
-        if (data.action == 'socket_connected') {
-            if (onconnect) onconnect();
-            return;
-        }
-
         if (data.action == 'error') {
             throw new Error(data.message);
         }
@@ -281,6 +276,9 @@ OpenSRF.Session.setup_shared_ws = function(onconnect) {
 
 OpenSRF.Session.prototype.send_ws_shared = function(message) {
 
+    if (!OpenSRF.sharedWSWorker) 
+        OpenSRF.Session.setup_shared_ws();
+
     var json = js2JSON({
         service : this.service,
         thread : this.thread,
diff --git a/src/javascript/opensrf_ws_shared.js b/src/javascript/opensrf_ws_shared.js
index 93c0d89..fe63a2a 100644
--- a/src/javascript/opensrf_ws_shared.js
+++ b/src/javascript/opensrf_ws_shared.js
@@ -27,6 +27,9 @@ var thread_port_map = {};
 // our shared websocket
 var websocket;
 
+// pending messages awaiting a successful websocket connection
+var pending_ws_messages = [];
+
 function send_msg_to_port(ident, msg) {
     console.debug('sending msg to port ' + ident + ' : ' + msg.action);
     try {
@@ -34,7 +37,7 @@ function send_msg_to_port(ident, msg) {
     } catch(E) {
         // some browsers (Opera) throw an exception when messaging
         // a disconnected port.
-        console.log('unable to send msg to port ' + ident);
+        console.debug('unable to send msg to port ' + ident);
         delete connected_ports[ident];
     }
 }
@@ -48,43 +51,46 @@ function broadcast(msg) {
 
 // opens the websocket connection
 // port_ident refers to the requesting port
-function open_websocket(port_ident) {
-    var port = connected_ports[port_ident];
-
-    if (websocket) {
-        switch (websocket.readyState) {
-
-            case websocket.CONNECTING:
-                // nothing to do.  The port will get notified on open
-                return;
-
-            case websocket.OPEN:
-                // websocket is already open, let the connecting.
-                // other ports have been notified already, so
-                // no broadcast is required.
-                send_msg_to_port(port_ident, {action : 'socket_connected'});
-                return;
-
-            default:
-                // websocket is no longer connected.  We need a new socket.
-                websocket = null;
-        }
+function send_to_websocket(message) {
+
+    if (websocket && websocket.readyState == websocket.OPEN) {
+        // websocket connection is viable.  send our message.
+        websocket.send(message);
+        return;
+    }
+
+    // no viable connection. queue our outbound messages for future delivery.
+    pending_ws_messages.push(message);
+
+    if (websocket && websocket.readyState == websocket.CONNECTING) {
+        // we are already in the middle of a setup call.  
+        // our queued message will be delivered after setup completes.
+        return;
     }
 
+    // we have no websocket or an invalid websocket.  build a new one.
+
     // TODO:
     // assume non-SSL for now.  SSL silently dies if the cert is
-    // invalid and has not been added as an exception.
-    var path = 'ws://' + location.host + ':' + 
-        WEBSOCKET_PORT + WEBSOCKET_URL_PATH
+    // invalid and has not been added as an exception.  need to
+    // explain / document / avoid this better.
+    var path = 'ws://' + location.host + ':' + WEBSOCKET_PORT + WEBSOCKET_URL_PATH;
 
-    console.log('connecting websocket to ' + path);
+    console.debug('connecting websocket to ' + path);
 
-    websocket = new WebSocket(path);
+    try {
+        websocket = new WebSocket(path);
+    } catch(E) {
+        console.log('Error creating WebSocket for path ' + path + ' : ' + E);
+        throw new Error(E);
+    }
 
     websocket.onopen = function() {
-        // tell all ports the websocket is open and ready
-        console.log('websocket.onopen()');
-        broadcast({action : 'socket_connected'});
+        console.debug('websocket.onopen()');
+        // deliver any queued messages
+        var msg;
+        while ( (msg = pending_ws_messages.shift()) )
+            websocket.send(msg);
     }
 
     websocket.onmessage = function(evt) {
@@ -92,6 +98,9 @@ function open_websocket(port_ident) {
 
         // this is a hack to avoid having to run JSON2js multiple 
         // times on the same message.  Hopefully match() is faster.
+        // We can't use JSON_v1 within a shared worker for marshalling
+        // messages, because it has no knowledge of application-level
+        // class hints in this environment.
         var thread;
         var match = message.match(/"thread":"(.*?)"/);
         if (!match || !(thread = match[1])) {
@@ -112,17 +121,19 @@ function open_websocket(port_ident) {
         }
 
         /* poor man's memory management.  We are not cleaning up our
-         * thread_port_map as we go, because that gets messy.  Instead,
-         * after the map has reached a certain size, clear it.  If any
-         * pending messages are afield that depend on the map, they 
-         * will be broadcast to all ports on arrival (see above).  Only the 
-         * port expecting a message with the given thread will honor the 
-         * message, all other ports will drop it silently.  We could just 
-         * do that for every messsage, but this is more efficient.
+         * thread_port_map as we go, because that would require parsing
+         * and analyzing every message to look for opensrf statuses.  
+         * parsing messages adds overhead (see also above comments about
+         * JSON_v1.js).  So, instead, after the map has reached a certain 
+         * size, clear it.  If any pending messages are afield that depend 
+         * on the map, they will be broadcast to all ports on arrival 
+         * (see above).  Only the port expecting a message with the given 
+         * thread will honor the message, all other ports will drop it 
+         * silently.  We could just broadcastfor every messsage, but this 
+         * is more efficient.
          */
-        if (Object.keys(thread_port_map).length > 1000) {
+        if (Object.keys(thread_port_map).length > 1000) 
             thread_port_map = {};
-        }
     }
 
     websocket.onerror = function(evt) {
@@ -133,7 +144,8 @@ function open_websocket(port_ident) {
     }
 
     websocket.onclose = function() {
-        console.log('closing websocket');
+        console.debug('closing websocket');
+        websocket = null;
     }
 }
 
@@ -152,14 +164,14 @@ onconnect = function(e) {
 
         if (data.action == 'message') {
             thread_port_map[data.thread] = port_ident;
-            websocket.send(data.message);
+            send_to_websocket(data.message);
             return;
         } 
 
         if (messsage.action == 'close') {
             // TODO: add me to body onunload in calling pages.
             delete connected_ports[port_ident];
-            console.log('closed port ' + port_ident + 
+            console.debug('closed port ' + port_ident + 
                 '; ' + Object.keys(connected_ports).length + ' remaining');
             return;
         }
@@ -168,9 +180,7 @@ onconnect = function(e) {
 
     port.start();
 
-    console.log('added port ' + port_ident + 
+    console.debug('added port ' + port_ident + 
       '; ' + Object.keys(connected_ports).length + ' total');
-
-    open_websocket(port_ident);
 }
 

commit a02360aadfcd113cbec88d9c2455e42fdd74e536
Author: Bill Erickson <berick at esilibrary.com>
Date:   Tue Jan 14 16:22:23 2014 -0500

    LP#1268619: websocket translator idle timeout / graceful shutdown
    
    Added support for an idle timeout and idle check interval configuration
    variables.  These allow each websocket apache process to kick off
    clients that have been connected and are idle for too long, thus hogging
    a process unnecessarily.
    
    Added a SIGUSR1 signal handler which forces the idle timeout to be very
    low and a short re-check period so that the client can be kicked as soon
    as there are no open conversations.
    
    Signed-off-by: Bill Erickson <berick at esilibrary.com>
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>

diff --git a/src/gateway/osrf_websocket_translator.c b/src/gateway/osrf_websocket_translator.c
index b430871..e2cfc98 100644
--- a/src/gateway/osrf_websocket_translator.c
+++ b/src/gateway/osrf_websocket_translator.c
@@ -29,24 +29,43 @@
  *   "osrf_msg" : [<osrf_msg>, <osrf_msg>, ...]   // required
  * }
  *
- * Each translator operates with two threads.  One thread receives messages
+ * Each translator operates with three threads.  One thread receives messages
  * from the websocket client, translates, and relays them to the opensrf 
  * network. The second thread collects responses from the opensrf network and 
- * relays them back to the websocket client.
+ * relays them back to the websocket client.  The third thread inspects
+ * the idle timeout interval t see if it's time to drop the idle client.
  *
- * After the initial setup, all thread actions occur within a thread mutex.
- * The desired affect is a non-threaded application that uses threads for 
- * the sole purpose of having one thread listening for incoming data, while
- * a second thread listens for responses.  When either thread awakens, it's
- * the only thread in town until it goes back to sleep (i.e. listening on 
+ * After the initial setup, all thread actions occur within a thread
+ * mutex.  The desired affect is a non-threaded application that uses
+ * threads for the sole purpose of having one thread listening for
+ * incoming data, while a second thread listens for responses, and a
+ * third checks the idle timeout.  When any thread awakens, it's the
+ * only thread in town until it goes back to sleep (i.e. listening on
  * its socket for data).
  *
- * Note that with a "thread", which allows us to identify the opensrf session,
- * the caller does not need to provide a recipient address.  The "service" is
- * only required to start a new opensrf session.  After the sesession is 
- * started, all future communication is based solely on the thread.  However,
- * the "service" should be passed by the caller for all requests to ensure it
- * is properly logged in the activity log.
+ * Note that with the opensrf "thread", which allows us to identify the
+ * opensrf session, the caller does not need to provide a recipient
+ * address.  The "service" is only required to start a new opensrf
+ * session.  After the sesession is started, all future communication is
+ * based solely on the thread.  However, the "service" should be passed
+ * by the caller for all requests to ensure it is properly logged in the
+ * activity log.
+ *
+ * Every inbound and outbound message updates the last_activity_time.
+ * A separate thread wakes periodically to see if the time since the
+ * last_activity_time exceeds the configured idle_timeout_interval.  If
+ * so, a disconnect is sent to the client, completing the conversation.
+ *
+ * Configuration goes directly into the Apache envvars file.  
+ * (e.g. /etc/apache2-websockets/envvars).  As of today, it's not 
+ * possible to leverage Apache configuration directives directly,
+ * since this is not an Apache module, but a shared library loaded
+ * by an apache module.  This includes SetEnv / SetEnvIf.
+ *
+ * export OSRF_WEBSOCKET_IDLE_TIMEOUT=300
+ * export OSRF_WEBSOCKET_IDLE_CHECK_INTERVAL=5
+ * export OSRF_WEBSOCKET_CONFIG_FILE=/openils/conf/opensrf_core.xml
+ * export OSRF_WEBSOCKET_CONFIG_CTXT=gateway
  */
 
 /**
@@ -55,7 +74,11 @@
  * down for graceful disconnects.
  */
 
+#include <stdlib.h>
+#include <signal.h>
+#include <unistd.h>
 #include "httpd.h"
+#include "http_log.h"
 #include "apr_strings.h"
 #include "apr_thread_proc.h"
 #include "apr_hash.h"
@@ -71,6 +94,23 @@
 #define RECIP_BUF_SIZE 128
 #define WEBSOCKET_TRANSLATOR_INGRESS "ws-translator-v1"
 
+
+// default values, replaced during setup (below) as needed.
+static char* config_file = "/openils/conf/opensrf_core.xml";
+static char* config_ctxt = "gateway";
+static time_t idle_timeout_interval = 300; 
+static time_t idle_check_interval = 5;
+static time_t last_activity_time = 0;
+
+// true if we've received a signal to start graceful shutdown
+static int shutdown_requested = 0; 
+static void sigusr1_handler(int sig);
+static void sigusr1_handler(int sig) {                                       
+    shutdown_requested = 1; 
+    signal(SIGUSR1, sigusr1_handler);
+    osrfLogInfo(OSRF_LOG_MARK, "WS received SIGUSR1 - Graceful Shutdown");
+}
+
 typedef struct _osrfWebsocketTranslator {
 
     /** Our handle for communicating with the caller */
@@ -106,6 +146,14 @@ typedef struct _osrfWebsocketTranslator {
     apr_thread_t *responder_thread;
 
     /**
+     * Thread responsible for checking inactivity timeout.
+     * If no activitity occurs within the configured interval,
+     * a disconnect is sent to the client and the connection
+     * is terminated.
+     */
+    apr_thread_t *idle_timeout_thread;
+
+    /**
      * All message handling code is wrapped in a thread mutex such
      * that all actions (after the initial setup) are serialized
      * to minimize the possibility of multi-threading snafus.
@@ -160,7 +208,6 @@ static void clear_cached_recipient(const char* thread) {
     }
 }
 
-
 void* osrf_responder_thread_main_body(transport_message *tmsg) {
 
     osrfList *msg_list = NULL;
@@ -251,7 +298,116 @@ void* osrf_responder_thread_main_body(transport_message *tmsg) {
 
     free(msg_string);
     jsonObjectFree(msg_wrapper);
+}
+
+/**
+ * Sleep and regularly wake to see if the process has been idle for too
+ * long.  If so, send a disconnect to the client.
+ *
+ */
+void* APR_THREAD_FUNC osrf_idle_timeout_thread_main(
+        apr_thread_t *thread, void *data) {
+
+    // sleep time defaults to the check interval, but may 
+    // be shortened during shutdown.
+    int sleep_time = idle_check_interval;
+    int shutdown_loops = 0;
+
+    while (1) {
+
+        if (apr_thread_mutex_unlock(trans->mutex) != APR_SUCCESS) {
+            osrfLogError(OSRF_LOG_MARK, "WS error un-locking thread mutex");
+            return NULL;
+        }
+
+        // note: receiving a signal (e.g. SIGUSR1) will not interrupt
+        // this sleep(), since it's running within its own thread.
+        // During graceful shtudown, we may wait up to 
+        // idle_check_interval seconds before initiating shutdown.
+        sleep(sleep_time);
+        
+        if (apr_thread_mutex_lock(trans->mutex) != APR_SUCCESS) {
+            osrfLogError(OSRF_LOG_MARK, "WS error locking thread mutex");
+            return NULL;
+        }
+
+        // no client is connected.  reset sleep time go back to sleep.
+        if (!trans->client_connected) {
+            sleep_time = idle_check_interval;
+            continue;
+        }
+
+        // do we have any active conversations with the connected client?
+        int active_count = apr_hash_count(trans->session_cache);
+
+        if (active_count) {
+
+            if (shutdown_requested) {
+                // active conversations means we can't shut down.  
+                // shorten the check interval to re-check more often.
+                shutdown_loops++;
+                osrfLogDebug(OSRF_LOG_MARK, 
+                    "WS: %d active conversation(s) found in shutdown after "
+                    "%d attempts.  Sleeping...", shutdown_loops, active_count
+                );
+
+                if (shutdown_loops > 30) {
+                    // this is clearly a long-running conversation, let's
+                    // check less frequently to avoid excessive logging.
+                    sleep_time = 3;
+                } else {
+                    sleep_time = 1;
+                }
+            } 
+
+            // active conversations means keep going.  There's no point in
+            // checking the idle time (below) if we're mid-conversation
+            continue;
+        }
+
+        // no active conversations
+             
+        if (shutdown_requested) {
+            // there's no need to reset the shutdown vars (loops/requested)
+            // SIGUSR1 is Apaches reload signal, which means this process
+            // will be going away as soon as the client is disconnected.
+
+            osrfLogInfo(OSRF_LOG_MARK,
+                "WS: no active conversations remain in shutdown; "
+                    "closing client connection");
+
+        } else { 
+            // see how long we've been idle.  If too long, kick the client
+
+            time_t now = time(NULL);
+            time_t difference = now - last_activity_time;
+
+            osrfLogDebug(OSRF_LOG_MARK, 
+                "WS has been idle for %d seconds", difference);
+
+            if (difference < idle_timeout_interval) {
+                // Last activity occurred within the idle timeout interval.
+                continue;
+            }
+
+            // idle timeout exceeded
+            osrfLogDebug(OSRF_LOG_MARK, 
+                "WS: idle timeout exceeded.  now=%d / last=%d; " 
+                "closing client connection", now, last_activity_time);
+        }
+
+
+        // send a disconnect to the client, which will come back around
+        // to cause our on_disconnect_handler to run.
+        osrfLogDebug(OSRF_LOG_MARK, "WS: sending close() to client");
+        trans->server->close(trans->server);
+
+        // client will be going away, reset sleep time
+        sleep_time = idle_check_interval;
+    }
 
+    // should never get here
+    return NULL;
 }
 
 /**
@@ -282,6 +438,7 @@ void* APR_THREAD_FUNC osrf_responder_thread_main(apr_thread_t *thread, void *dat
         osrfLogForceXid(tmsg->osrf_xid);
         osrf_responder_thread_main_body(tmsg);
         message_free(tmsg);                                                         
+        last_activity_time = time(NULL);
     }
 
     return NULL;
@@ -300,17 +457,64 @@ int child_init(const WebSocketServer *server) {
     apr_threadattr_t *thread_attr = NULL;
     apr_thread_mutex_t *mutex = NULL;
     request_rec *r = server->request(server);
-        
-    osrfLogDebug(OSRF_LOG_MARK, "WS child_init");
+
 
     // osrf_handle will already be connected if this is not the first request
     // served by this process.
     if ( !(osrf_handle = osrfSystemGetTransportClient()) ) {
-        char* config_file = "/openils/conf/opensrf_core.xml";
-        char* config_ctx = "gateway"; //TODO config
-        if (!osrfSystemBootstrapClientResc(config_file, config_ctx, "websocket")) {   
+        
+        // load config values from the env
+        char* timeout = getenv("OSRF_WEBSOCKET_IDLE_TIMEOUT");
+        if (timeout) {
+            if (!atoi(timeout)) {
+                ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, 
+                    "WS: invalid OSRF_WEBSOCKET_IDLE_TIMEOUT: %s", timeout);
+            } else {
+                idle_timeout_interval = (time_t) atoi(timeout);
+            }
+        }
+
+        ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, 
+            "WS: timeout set to %d", idle_timeout_interval);
+
+        char* interval = getenv("OSRF_WEBSOCKET_IDLE_CHECK_INTERVAL");
+        if (interval) {
+            if (!atoi(interval)) {
+                ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, 
+                    "WS: invalid OSRF_WEBSOCKET_IDLE_CHECK_INTERVAL: %s", 
+                    interval
+                );
+            } else {
+                idle_check_interval = (time_t) atoi(interval);
+            }
+        } 
+
+        ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, 
+            "WS: idle check interval set to %d", idle_check_interval);
+
+      
+        char* cfile = getenv("OSRF_WEBSOCKET_CONFIG_FILE");
+        if (cfile) {
+            ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r,
+                "WS: config file set to %s", cfile);
+            config_file = cfile;
+        }
+
+        char* ctxt = getenv("OSRF_WEBSOCKET_CONFIG_CTXT");
+        if (ctxt) {
+            ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, 
+                "WS: config context set to %s", ctxt);
+            config_ctxt = ctxt;
+        }
+
+        // connect to opensrf
+        if (!osrfSystemBootstrapClientResc(
+                config_file, config_ctxt, "websocket")) {   
+
             osrfLogError(OSRF_LOG_MARK, 
-                "WS unable to bootstrap OpenSRF client with config %s", config_file); 
+                "WS unable to bootstrap OpenSRF client with config %s "
+                "and context %s", config_file, config_ctxt
+            ); 
             return 1;
         }
 
@@ -323,7 +527,6 @@ int child_init(const WebSocketServer *server) {
         return 1;
     }
 
-
     // allocate our static translator instance
     trans = (osrfWebsocketTranslator*) 
         apr_palloc(pool, sizeof(osrfWebsocketTranslator));
@@ -359,6 +562,24 @@ int child_init(const WebSocketServer *server) {
         return 1;
     }
 
+    // Create the idle timeout thread, which lives for the lifetime
+    // of the process.
+    thread = NULL; // reset
+    thread_attr = NULL; // reset
+    if ( (apr_threadattr_create(&thread_attr, trans->main_pool) == APR_SUCCESS) &&
+         (apr_threadattr_detach_set(thread_attr, 0) == APR_SUCCESS) &&
+         (apr_thread_create(&thread, thread_attr, 
+            osrf_idle_timeout_thread_main, trans, trans->main_pool) == APR_SUCCESS)) {
+
+        osrfLogDebug(OSRF_LOG_MARK, "WS created idle timeout thread");
+        trans->idle_timeout_thread = thread;
+        
+    } else {
+        osrfLogError(OSRF_LOG_MARK, "WS unable to create idle timeout thread");
+        return 1;
+    }
+
+
     if (apr_thread_mutex_create(
             &mutex, APR_THREAD_MUTEX_UNNESTED, 
             trans->main_pool) != APR_SUCCESS) {
@@ -368,6 +589,8 @@ int child_init(const WebSocketServer *server) {
 
     trans->mutex = mutex;
 
+    signal(SIGUSR1, sigusr1_handler);
+
     return APR_SUCCESS;
 }
 
@@ -377,10 +600,16 @@ int child_init(const WebSocketServer *server) {
 void* CALLBACK on_connect_handler(const WebSocketServer *server) {
     request_rec *r = server->request(server);
     apr_pool_t *pool;
+    apr_thread_t *thread = NULL;
+    apr_threadattr_t *thread_attr = NULL;
 
-    osrfLogInfo(OSRF_LOG_MARK, 
-        "WS connect from %s", r->connection->remote_ip); 
-        //"WS connect from %s", r->connection->client_ip); // apache 2.4
+#ifdef APACHE_MIN_24
+    char* client_ip = r->connection->client_ip;
+#else
+    char* client_ip = r->connection->remote_ip;
+#endif
+
+    osrfLogInfo(OSRF_LOG_MARK, "WS connect from %s", client_ip);
 
     if (!trans) {
         // first connection
@@ -399,6 +628,8 @@ void* CALLBACK on_connect_handler(const WebSocketServer *server) {
 
     trans->session_pool = pool;
     trans->client_connected = 1;
+    last_activity_time = time(NULL);
+
     return trans;
 }
 
@@ -600,6 +831,8 @@ static size_t on_message_handler_body(void *data,
     jsonObjectFree(msg_wrapper);
     free(msg_body);
 
+    last_activity_time = time(NULL);
+
     return OK;
 }
 
@@ -631,6 +864,10 @@ void CALLBACK on_disconnect_handler(
 
     osrfWebsocketTranslator *trans = (osrfWebsocketTranslator*) data;
     trans->client_connected = 0;
+
+    // timeout thread is recreated w/ each new connection
+    apr_thread_exit(trans->idle_timeout_thread, APR_SUCCESS);
+    trans->idle_timeout_thread = NULL;
     
     // ensure no errant session data is sticking around
     apr_hash_clear(trans->session_cache);
@@ -655,7 +892,8 @@ void CALLBACK on_destroy_handler(WebSocketPlugin *plugin) {
     if (trans) {
         apr_thread_exit(trans->responder_thread, APR_SUCCESS);
         apr_thread_mutex_destroy(trans->mutex);
-        apr_pool_destroy(trans->session_pool);
+        if (trans->session_pool)
+            apr_pool_destroy(trans->session_pool);
         apr_pool_destroy(trans->main_pool);
     }
 

commit bef394a7c24df54f1e63fc3e83cd473195a46c3a
Author: Bill Erickson <berick at esilibrary.com>
Date:   Fri Jan 10 11:57:29 2014 -0500

    LP#1268619: websockets: support WS via shared web workers
    
    This allows mutiple browser tabs to share the same websocket connection.
    
    TODO: add tab (port) disconnect handler
    TODO: more docs / examples
    
    Signed-off-by: Bill Erickson <berick at esilibrary.com>
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>

diff --git a/src/javascript/opensrf.js b/src/javascript/opensrf.js
index 4d435a1..994fbe6 100644
--- a/src/javascript/opensrf.js
+++ b/src/javascript/opensrf.js
@@ -22,6 +22,7 @@ var OSRF_APP_SESSION_DISCONNECTED = 2;
 var OSRF_TRANSPORT_TYPE_XHR = 1;
 var OSRF_TRANSPORT_TYPE_XMPP = 2;
 var OSRF_TRANSPORT_TYPE_WS = 3;
+var OSRF_TRANSPORT_TYPE_WS_SHARED = 4;
 
 /* message types */
 var OSRF_MESSAGE_TYPE_REQUEST = 'REQUEST';
@@ -222,6 +223,8 @@ OpenSRF.Session.prototype.send = function(osrf_msg, args) {
     switch(OpenSRF.Session.transport) {
         case OSRF_TRANSPORT_TYPE_WS:
             return this.send_ws(osrf_msg);
+        case OSRF_TRANSPORT_TYPE_WS_SHARED:
+            return this.send_ws_shared(osrf_msg);
         case OSRF_TRANSPORT_TYPE_XHR:
             return this.send_xhr(osrf_msg, args);
         case OSRF_TRANSPORT_TYPE_XMPP:
@@ -243,6 +246,57 @@ OpenSRF.Session.prototype.send_ws = function(osrf_msg) {
     );
 };
 
+OpenSRF.Session.setup_shared_ws = function(onconnect) {
+    // TODO path
+    OpenSRF.sharedWSWorker = new SharedWorker('opensrf_ws_shared.js'); 
+
+    OpenSRF.sharedWSWorker.port.addEventListener('message', function(e) {                          
+        var data = e.data;
+        console.log('sharedWSWorker received message ' + data.action);
+
+        if (data.action == 'message') {
+            // pass all inbound message up the opensrf stack
+
+            var msg = JSON2js(data.message); // TODO: json error handling
+            OpenSRF.Stack.push(                                                        
+                new OpenSRF.NetMessage(                                                
+                   null, null, msg.thread, null, msg.osrf_msg)                        
+            ); 
+
+            return;
+        }
+
+        if (data.action == 'socket_connected') {
+            if (onconnect) onconnect();
+            return;
+        }
+
+        if (data.action == 'error') {
+            throw new Error(data.message);
+        }
+    });
+
+    OpenSRF.sharedWSWorker.port.start();   
+}
+
+OpenSRF.Session.prototype.send_ws_shared = function(message) {
+
+    var json = js2JSON({
+        service : this.service,
+        thread : this.thread,
+        osrf_msg : [message.serialize()]
+    });
+
+    OpenSRF.sharedWSWorker.port.postMessage({
+        action : 'message', 
+        // pass the thread additionally as a stand-alone value so the
+        // worker can more efficiently inspect it.
+        thread : this.thread,
+        message : json
+    });
+}
+
+
 OpenSRF.Session.prototype.send_xmpp = function(osrf_msg, args) {
     alert('xmpp transport not implemented');
 };
@@ -436,6 +490,7 @@ OpenSRF.Stack.push = function(net_msg, callbacks) {
         try {
             osrf_msgs = JSON2js(net_msg.body);
 
+            // TODO: pretty sure we don't need this..
             if (OpenSRF.Session.transport == OSRF_TRANSPORT_TYPE_WS) {
                 // WebSocketRequests wrap the content
                 osrf_msgs = osrf_msgs.osrf_msg;
diff --git a/src/javascript/opensrf_ws_shared.js b/src/javascript/opensrf_ws_shared.js
new file mode 100644
index 0000000..93c0d89
--- /dev/null
+++ b/src/javascript/opensrf_ws_shared.js
@@ -0,0 +1,176 @@
+/* -----------------------------------------------------------------------
+ * Copyright (C) 2014  Equinox Software, Inc.
+ * Bill Erickson <berick at esilibrary.com>
+ *  
+ * 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.
+ * ----------------------------------------------------------------------- */
+
+var WEBSOCKET_URL_PATH = '/osrf-websocket-translator';
+var WEBSOCKET_PORT = 7680;
+var WEBSOCKET_PORT_SSL = 7682;
+
+// set of shared ports (i.e. browser tabs)
+var connected_ports = {};
+var port_identifier = 0;
+
+// maps osrf message threads to a port index in connected_ports
+var thread_port_map = {}; 
+
+// our shared websocket
+var websocket;
+
+function send_msg_to_port(ident, msg) {
+    console.debug('sending msg to port ' + ident + ' : ' + msg.action);
+    try {
+        connected_ports[ident].postMessage(msg);
+    } catch(E) {
+        // some browsers (Opera) throw an exception when messaging
+        // a disconnected port.
+        console.log('unable to send msg to port ' + ident);
+        delete connected_ports[ident];
+    }
+}
+
+// send a message to all listeners
+function broadcast(msg) {
+    for (var ident in connected_ports)
+      send_msg_to_port(ident, msg);
+}
+
+
+// opens the websocket connection
+// port_ident refers to the requesting port
+function open_websocket(port_ident) {
+    var port = connected_ports[port_ident];
+
+    if (websocket) {
+        switch (websocket.readyState) {
+
+            case websocket.CONNECTING:
+                // nothing to do.  The port will get notified on open
+                return;
+
+            case websocket.OPEN:
+                // websocket is already open, let the connecting.
+                // other ports have been notified already, so
+                // no broadcast is required.
+                send_msg_to_port(port_ident, {action : 'socket_connected'});
+                return;
+
+            default:
+                // websocket is no longer connected.  We need a new socket.
+                websocket = null;
+        }
+    }
+
+    // TODO:
+    // assume non-SSL for now.  SSL silently dies if the cert is
+    // invalid and has not been added as an exception.
+    var path = 'ws://' + location.host + ':' + 
+        WEBSOCKET_PORT + WEBSOCKET_URL_PATH
+
+    console.log('connecting websocket to ' + path);
+
+    websocket = new WebSocket(path);
+
+    websocket.onopen = function() {
+        // tell all ports the websocket is open and ready
+        console.log('websocket.onopen()');
+        broadcast({action : 'socket_connected'});
+    }
+
+    websocket.onmessage = function(evt) {
+        var message = evt.data;
+
+        // this is a hack to avoid having to run JSON2js multiple 
+        // times on the same message.  Hopefully match() is faster.
+        var thread;
+        var match = message.match(/"thread":"(.*?)"/);
+        if (!match || !(thread = match[1])) {
+            throw new Error("Websocket message malformed; no thread: " + message);
+        }
+
+        console.debug('websocket received message for thread ' + thread);
+
+        var port_msg = {action: 'message', message : message};
+        var port_ident = thread_port_map[thread];
+
+        if (port_ident) {
+            send_msg_to_port(port_ident, port_msg);
+        } else {
+            // don't know who it's for, broadcast and let the ports
+            // sort it out for themselves.
+            broadcast(port_msg);
+        }
+
+        /* poor man's memory management.  We are not cleaning up our
+         * thread_port_map as we go, because that gets messy.  Instead,
+         * after the map has reached a certain size, clear it.  If any
+         * pending messages are afield that depend on the map, they 
+         * will be broadcast to all ports on arrival (see above).  Only the 
+         * port expecting a message with the given thread will honor the 
+         * message, all other ports will drop it silently.  We could just 
+         * do that for every messsage, but this is more efficient.
+         */
+        if (Object.keys(thread_port_map).length > 1000) {
+            thread_port_map = {};
+        }
+    }
+
+    websocket.onerror = function(evt) {
+        var err = "WebSocket Error " + evt + ' : ' + evt.data;
+        // propagate to all ports so it can be logged, etc. 
+        broadcast({action : 'error', message : err});
+        throw new Error(err);
+    }
+
+    websocket.onclose = function() {
+        console.log('closing websocket');
+    }
+}
+
+// called when a new port (tab) is opened
+onconnect = function(e) {
+    var port = e.ports[0];
+
+    // we have no way of identifying ports within the message handler,
+    // so we apply an identifier to each and toss that into a closer.
+    var port_ident = port_identifier++;
+    connected_ports[port_ident] = port;
+
+    // message handler
+    port.addEventListener('message', function(e) {
+        var data = e.data;
+
+        if (data.action == 'message') {
+            thread_port_map[data.thread] = port_ident;
+            websocket.send(data.message);
+            return;
+        } 
+
+        if (messsage.action == 'close') {
+            // TODO: add me to body onunload in calling pages.
+            delete connected_ports[port_ident];
+            console.log('closed port ' + port_ident + 
+                '; ' + Object.keys(connected_ports).length + ' remaining');
+            return;
+        }
+
+    }, false);
+
+    port.start();
+
+    console.log('added port ' + port_ident + 
+      '; ' + Object.keys(connected_ports).length + ' total');
+
+    open_websocket(port_ident);
+}
+

commit fdb255a92f9fa687a50bed05ef918523cf902d8b
Author: Bill Erickson <berick at esilibrary.com>
Date:   Thu Jan 9 15:00:03 2014 -0500

    LP#1268619: websocket: do not disconnect ws() on osrf disconnect message
    
    Signed-off-by: Bill Erickson <berick at esilibrary.com>
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>

diff --git a/src/javascript/opensrf.js b/src/javascript/opensrf.js
index 813bef8..4d435a1 100644
--- a/src/javascript/opensrf.js
+++ b/src/javascript/opensrf.js
@@ -305,11 +305,6 @@ OpenSRF.ClientSession.prototype.disconnect = function(args) {
 
     this.remote_id = null;
     this.state = OSRF_APP_SESSION_DISCONNECTED;
-
-    if (this.websocket) {
-        this.websocket.close();
-        delete this.websocket;
-    }
 };
 
 

commit d546d7eacb183ba2ddd0c0ba5dc281dc5086ae81
Author: Bill Erickson <berick at esilibrary.com>
Date:   Tue Feb 5 10:22:34 2013 -0500

    LP#1268619: temporary websocket installer README
    
    Signed-off-by: Bill Erickson <berick at esilibrary.com>
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>

diff --git a/README.websockets b/README.websockets
new file mode 100644
index 0000000..c9f2391
--- /dev/null
+++ b/README.websockets
@@ -0,0 +1,47 @@
+
+Websockets installation instructions for Debian:
+
+# TODO: Most of this can be scripted.
+# TODO: Better handling of external dependencies (websocket_plugin.h).  
+
+# as root
+
+# see also /usr/share/doc/apache2/README.multiple-instances
+% sh /usr/share/doc/apache2.2-common/examples/setup-instance websockets
+
+% cp examples/apache2/websockets.conf /etc/apache2-websockets/sites-available/
+
+# activate the websockets configuration
+% a2ensite-websockets websockets.conf 
+
+# deactivate the default site
+% a2dissite-websockets default 
+
+# remove most of the mods with this shell script
+
+MODS=$(apache2ctl-websockets -M | grep shared | grep -v 'Syntax OK' | sed 's/_module//g' | cut -d' ' -f2 | xargs);
+for mod in $MODS; do
+    if [ $mod = 'mime' -o $mod = 'ssl' -o $mod = 'websocket' ]; then
+        echo "* Leaving module $mod in place";
+    else
+        echo "* Disabling module $mod";
+        a2dismod-websockets $mod;
+    fi;
+done
+
+# follow the instructions for installing Apache mod_websockets at
+# https://github.com/disconnect/apache-websocket
+
+# copy the headers into place so OpenSRF can compile
+% cp $LOCATION_OF_APACHE_WEBSOCKET_CHECKOUT/websocket_plugin.h src/gateway/
+
+# install OpenSRF
+
+# remove the websocket module from the default OpenSRF Apache instance
+% a2dismod osrf_websocket_translator
+
+# After OpenSRF is up and running, fire up the secondary Apache instance
+# errors will appear in /var/log/apache2-websockets/error.log
+% /etc/init.d/apache2-websockets restart
+
+

commit e4ef36f385c4f3b83ac4b49f2b07ee19c3166ff0
Author: Bill Erickson <berick at esilibrary.com>
Date:   Tue Feb 5 09:52:57 2013 -0500

    LP#1268619: Sample Websocket translator Apache 2.2 configuration
    
    Signed-off-by: Bill Erickson <berick at esilibrary.com>
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>

diff --git a/examples/apache2/websockets.conf b/examples/apache2/websockets.conf
new file mode 100644
index 0000000..ae598a1
--- /dev/null
+++ b/examples/apache2/websockets.conf
@@ -0,0 +1,43 @@
+# :vim set syntax apache                                                        
+#
+# This is the top-level configuration file for the 
+# apache2-websockets instance.  For example, in Debian
+# this file lives in /etc/apache2-websockets/sites-available/
+                                                                                
+LogLevel debug                                                                  
+# - log locally                                                                 
+CustomLog /var/log/apache2-websockets/access.log combined                       
+ErrorLog /var/log/apache2-websockets/error.log
+# Add the PID to the error log (Apache 2.4 only)
+# ErrorLogFormat "[%t] [%P] [%l] [pid %P] %F: %E: [client %a] %M"                
+                                                                                
+# ----------------------------------------------------------------------------------
+# Set up our SSL virtual host                                                   
+# ----------------------------------------------------------------------------------
+Listen 7682
+NameVirtualHost *:7682                                                          
+<VirtualHost *:7682>                                                            
+    DocumentRoot /var/www                                                       
+    ServerName localhost:7682                                                   
+    ServerAlias 127.0.0.1:7682                                                  
+    SSLEngine on                                                                
+    SSLHonorCipherOrder On                                                      
+    SSLCipherSuite ECDHE-RSA-AES256-SHA384:AES256-SHA256:RC4:HIGH:!MD5:!aNULL:!EDH:!AESGCM
+
+    # re-use the certs from the main apache instance
+    SSLCertificateFile /etc/apache2/ssl/server.crt
+    SSLCertificateKeyFile /etc/apache2/ssl/server.key
+</VirtualHost>                                                                  
+                                                                                
+Listen 7680
+NameVirtualHost *:7680                                                          
+<VirtualHost *:7680>                                                            
+    ServerName localhost:7680                                                   
+    ServerAlias 127.0.0.1:7680                                                  
+    DocumentRoot /var/www                                                       
+</VirtualHost>                                                                  
+                                                                                
+<Location /osrf-websocket-translator>                                           
+    SetHandler websocket-handler                                                
+    WebSocketHandler /usr/lib/apache2/modules/osrf_websocket_translator.so osrf_websocket_init
+</Location> 

commit 32ab4b133c9c96780e8b202ab1efe46bbf321c3b
Author: Bill Erickson <berick at esilibrary.com>
Date:   Fri Jan 25 12:38:13 2013 -0500

    LP#1268619: websocket gateway: improved memory mgt; logging
    
    Signed-off-by: Bill Erickson <berick at esilibrary.com>
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>

diff --git a/src/gateway/Makefile.am b/src/gateway/Makefile.am
index 27e9cc1..04a7632 100644
--- a/src/gateway/Makefile.am
+++ b/src/gateway/Makefile.am
@@ -37,7 +37,7 @@ install-exec-local:
 	$(MKDIR_P) $(DESTDIR)$(AP_LIBEXECDIR)
 	$(APXS2) -i -S LIBEXECDIR=$(DESTDIR)$(AP_LIBEXECDIR) -a @srcdir@/osrf_json_gateway.la
 	$(APXS2) -i -S LIBEXECDIR=$(DESTDIR)$(AP_LIBEXECDIR) -a @srcdir@/osrf_http_translator.la
-	$(APXS2) -i -S LIBEXECDIR=$(DESTDIR)$(AP_LIBEXECDIR) -a @srcdir@/osrf_websocket_translator.la
+	$(APXS2) -n osrf_websocket_translator -i -S LIBEXECDIR=$(DESTDIR)$(AP_LIBEXECDIR) -a @srcdir@/osrf_websocket_translator.la
 
 clean-local:
 	rm -f @srcdir@/osrf_http_translator.la @srcdir@/osrf_http_translator.lo @srcdir@/osrf_http_translator.slo @srcdir@/osrf_json_gateway.la @srcdir@/osrf_json_gateway.lo @srcdir@/osrf_json_gateway.slo  @srcdir@/osrf_websocket_translator.la @srcdir@/osrf_websocket_translator.lo @srcdir@/osrf_websocket_translator.slo
diff --git a/src/gateway/osrf_websocket_translator.c b/src/gateway/osrf_websocket_translator.c
index dc8653e..b430871 100644
--- a/src/gateway/osrf_websocket_translator.c
+++ b/src/gateway/osrf_websocket_translator.c
@@ -168,8 +168,7 @@ void* osrf_responder_thread_main_body(transport_message *tmsg) {
     int i;
 
     osrfLogDebug(OSRF_LOG_MARK, 
-        "WS received opensrf response for thread=%s, xid=%s", 
-            tmsg->thread, tmsg->osrf_xid);
+        "WS received opensrf response for thread=%s", tmsg->thread);
 
     // first we need to perform some maintenance
     msg_list = osrfMessageDeserialize(tmsg->body, NULL);
@@ -213,19 +212,18 @@ void* osrf_responder_thread_main_body(transport_message *tmsg) {
         }
     }
 
-    // maintenance is done
-    msg_list->freeItem = osrfMessageFree;
+    // osrfMessageDeserialize applies the freeItem handler to the 
+    // newly created osrfList.  We only need to free the list and 
+    // the individual osrfMessage's will be freed along with it
     osrfListFree(msg_list);
 
     if (!trans->client_connected) {
 
-        osrfLogDebug(OSRF_LOG_MARK, 
-            "WS discarding response for thread=%s, xid=%s", 
-            tmsg->thread, tmsg->osrf_xid);
+        osrfLogInfo(OSRF_LOG_MARK, 
+            "WS discarding response for thread=%s", tmsg->thread);
 
         return;
     }
-
     
     // client is still connected. 
     // relay the response messages to the client
@@ -240,8 +238,8 @@ void* osrf_responder_thread_main_body(transport_message *tmsg) {
 
     if (tmsg->is_error) {
         osrfLogError(OSRF_LOG_MARK, 
-            "WS received jabber error message in response to thread=%s and xid=%s", 
-            tmsg->thread, tmsg->osrf_xid);
+            "WS received jabber error message in response to thread=%s", 
+            tmsg->thread);
         jsonObjectSetKey(msg_wrapper, "transport_error", jsonNewBoolObject(1));
     }
 
@@ -281,6 +279,7 @@ void* APR_THREAD_FUNC osrf_responder_thread_main(apr_thread_t *thread, void *dat
             return NULL;
         }
 
+        osrfLogForceXid(tmsg->osrf_xid);
         osrf_responder_thread_main_body(tmsg);
         message_free(tmsg);                                                         
     }
@@ -379,7 +378,7 @@ void* CALLBACK on_connect_handler(const WebSocketServer *server) {
     request_rec *r = server->request(server);
     apr_pool_t *pool;
 
-    osrfLogDebug(OSRF_LOG_MARK, 
+    osrfLogInfo(OSRF_LOG_MARK, 
         "WS connect from %s", r->connection->remote_ip); 
         //"WS connect from %s", r->connection->client_ip); // apache 2.4
 
@@ -479,8 +478,6 @@ static char* extract_inbound_messages(
                 clear_cached_recipient(thread);
                 break;
         }
-
-        osrfMessageFree(msg);
     }
 
     char* finalMsg = osrfMessageSerializeBatch(msg_list, num_msgs);
@@ -513,6 +510,10 @@ static size_t on_message_handler_body(void *data,
 
     if (buffer_size <= 0) return OK;
 
+    // generate a new log trace for this request. it 
+    // may be replaced by a client-provided trace below.
+    osrfLogMkXid();
+
     osrfLogDebug(OSRF_LOG_MARK, "WS received message size=%d", buffer_size);
 
     // buffer may not be \0-terminated, which jsonParse requires
@@ -546,13 +547,7 @@ static size_t on_message_handler_body(void *data,
             return HTTP_BAD_REQUEST;
         }
 
-        // TODO: make this work with non-client and make this call accept 
-        // const char*'s.  casting to (char*) for now to silence warnings.
-        osrfLogSetXid((char*) log_xid); 
-
-    } else {
-        // generate a new log trace id for this relay
-        osrfLogMkXid();
+        osrfLogForceXid(log_xid);
     }
 
     if (thread) {
@@ -587,8 +582,8 @@ static size_t on_message_handler_body(void *data,
     }
 
     osrfLogDebug(OSRF_LOG_MARK, 
-        "WS relaying message thread=%s, xid=%s, recipient=%s", 
-            thread, osrfLogGetXid(), recipient);
+        "WS relaying message to opensrf thread=%s, recipient=%s", 
+            thread, recipient);
 
     msg_body = extract_inbound_messages(
         r, service, thread, recipient, osrf_msg);
@@ -648,7 +643,7 @@ void CALLBACK on_disconnect_handler(
 
     request_rec *r = server->request(server);
 
-    osrfLogDebug(OSRF_LOG_MARK, 
+    osrfLogInfo(OSRF_LOG_MARK, 
         "WS disconnect from %s", r->connection->remote_ip); 
         //"WS disconnect from %s", r->connection->client_ip); // apache 2.4
 }
diff --git a/src/javascript/opensrf_ws.js b/src/javascript/opensrf_ws.js
index 62302be..4df7c8f 100644
--- a/src/javascript/opensrf_ws.js
+++ b/src/javascript/opensrf_ws.js
@@ -65,7 +65,7 @@ OpenSRF.WebSocketConnection.prototype.setupSocket = function() {
 
 /** default onmessage handler: push the message up the opensrf stack */
 OpenSRF.WebSocketConnection.default_onmessage = function(evt) {
-    console.log('receiving: ' + evt.data);
+    //console.log('receiving: ' + evt.data);
     var msg = JSON2js(evt.data);
     OpenSRF.Stack.push(
         new OpenSRF.NetMessage(
@@ -153,7 +153,7 @@ OpenSRF.WebSocketRequest.prototype.send = function(message) {
     };
 
     var json = js2JSON(wrapper);
-    console.log('sending: ' + json);
+    //console.log('sending: ' + json);
 
     // drop it on the wire
     this.wsc.socket.send(json);

commit 0f3aa6480d2fbc9645571e057dac3f3be08709d1
Author: Bill Erickson <berick at esilibrary.com>
Date:   Mon Jan 21 11:34:57 2013 -0500

    LP#1268619: websockets; free temporary osrf msgs; minor comment change
    
    Signed-off-by: Bill Erickson <berick at esilibrary.com>
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>

diff --git a/src/gateway/osrf_websocket_translator.c b/src/gateway/osrf_websocket_translator.c
index 6bf8066..dc8653e 100644
--- a/src/gateway/osrf_websocket_translator.c
+++ b/src/gateway/osrf_websocket_translator.c
@@ -26,7 +26,7 @@
  *   "service"  : "opensrf.foo", // required
  *   "thread"   : "123454321",   // AKA thread. required for follow-up requests; max 64 chars.
  *   "log_xid"  : "123..32",     // optional log trace ID, max 64 chars;
- *   "osrf_msg" : {<osrf_msg>}   // required
+ *   "osrf_msg" : [<osrf_msg>, <osrf_msg>, ...]   // required
  * }
  *
  * Each translator operates with two threads.  One thread receives messages
@@ -214,6 +214,7 @@ void* osrf_responder_thread_main_body(transport_message *tmsg) {
     }
 
     // maintenance is done
+    msg_list->freeItem = osrfMessageFree;
     osrfListFree(msg_list);
 
     if (!trans->client_connected) {
@@ -478,9 +479,17 @@ static char* extract_inbound_messages(
                 clear_cached_recipient(thread);
                 break;
         }
+
+        osrfMessageFree(msg);
     }
 
-    return osrfMessageSerializeBatch(msg_list, num_msgs);
+    char* finalMsg = osrfMessageSerializeBatch(msg_list, num_msgs);
+
+    // clean up our messages
+    for(i = 0; i < num_msgs; i++) 
+        osrfMessageFree(msg_list[i]);
+
+    return finalMsg;
 }
 
 /**

commit de238b740f3a54dacd07610d8e187f417bf5b677
Author: Bill Erickson <berick at esilibrary.com>
Date:   Mon Dec 10 14:33:14 2012 -0500

    LP#1268619: websocket; docs, more memory mgmt
    
    Signed-off-by: Bill Erickson <berick at esilibrary.com>
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>

diff --git a/src/gateway/osrf_websocket_translator.c b/src/gateway/osrf_websocket_translator.c
index 35f986d..6bf8066 100644
--- a/src/gateway/osrf_websocket_translator.c
+++ b/src/gateway/osrf_websocket_translator.c
@@ -15,14 +15,15 @@
  */
 
 /**
- * Dumb websocket <-> opensrf gateway.  Wrapped opensrf messages are extracted
+ * websocket <-> opensrf gateway.  Wrapped opensrf messages are extracted
  * and relayed to the opensrf network.  Responses are pulled from the opensrf
- * network and passed back to the client.  No attempt is made to understand
- * the contents of the messages.
+ * network and passed back to the client.  Messages are analyzed to determine
+ * when a connect/disconnect occurs, so that the cache of recipients can be
+ * properly managed.  We also activity-log REQUEST messages.
  *
  * Messages to/from the websocket client take the following form:
  * {
- *   "service"  : "opensrf.foo", // required for new sessions (inbound only)
+ *   "service"  : "opensrf.foo", // required
  *   "thread"   : "123454321",   // AKA thread. required for follow-up requests; max 64 chars.
  *   "log_xid"  : "123..32",     // optional log trace ID, max 64 chars;
  *   "osrf_msg" : {<osrf_msg>}   // required
@@ -33,18 +34,19 @@
  * network. The second thread collects responses from the opensrf network and 
  * relays them back to the websocket client.
  *
- * The main thread reads from socket A (apache) and writes to socket B 
- * (openesrf), while the responder thread reads from B and writes to A.  The 
- * apr data structures used are threadsafe.  For now, no thread mutex's are 
- * used.
+ * After the initial setup, all thread actions occur within a thread mutex.
+ * The desired affect is a non-threaded application that uses threads for 
+ * the sole purpose of having one thread listening for incoming data, while
+ * a second thread listens for responses.  When either thread awakens, it's
+ * the only thread in town until it goes back to sleep (i.e. listening on 
+ * its socket for data).
  *
  * Note that with a "thread", which allows us to identify the opensrf session,
  * the caller does not need to provide a recipient address.  The "service" is
  * only required to start a new opensrf session.  After the sesession is 
- * started, all future communication is based solely on the thread.  
- *
- * We use jsonParseRaw and jsonObjectToJSONRaw since this service does not care 
- * about the contents of the messages.
+ * started, all future communication is based solely on the thread.  However,
+ * the "service" should be passed by the caller for all requests to ensure it
+ * is properly logged in the activity log.
  */
 
 /**
@@ -70,22 +72,63 @@
 #define WEBSOCKET_TRANSLATOR_INGRESS "ws-translator-v1"
 
 typedef struct _osrfWebsocketTranslator {
+
+    /** Our handle for communicating with the caller */
     const WebSocketServer *server;
-    apr_pool_t *main_pool; // standalone per-process pool
-    apr_pool_t *session_pool; // child of r->pool; per-session
+    
+    /**
+     * Standalone, per-process APR pool.  Primarily
+     * there for managing thread data, which lasts 
+     * the duration of the process.
+     */
+    apr_pool_t *main_pool;
+
+    /**
+     * Map of thread => drone-xmpp-address.  Maintaining this
+     * map internally means the caller never need know about
+     * internal XMPP addresses and the server doesn't have to 
+     * verify caller-specified recipient addresses.  It's
+     * all managed internally.
+     */
     apr_hash_t *session_cache; 
+
+    /**
+     * session_pool contains the key/value pairs stored in
+     * the session_cache.  The pool is regularly destroyed
+     * and re-created to avoid long-term memory consumption
+     */
+    apr_pool_t *session_pool;
+
+    /**
+     * Thread responsible for collecting responses on the opensrf
+     * network and relaying them back to the caller
+     */
     apr_thread_t *responder_thread;
+
+    /**
+     * All message handling code is wrapped in a thread mutex such
+     * that all actions (after the initial setup) are serialized
+     * to minimize the possibility of multi-threading snafus.
+     */
     apr_thread_mutex_t *mutex;
+
+    /**
+     * True if a websocket client is currently connected
+     */
     int client_connected;
+
+    /** OpenSRF jouter name */
     char* osrf_router;
+
+    /** OpenSRF domain */
     char* osrf_domain;
+
 } osrfWebsocketTranslator;
 
 static osrfWebsocketTranslator *trans = NULL;
 static transport_client *osrf_handle = NULL;
 static char recipient_buf[RECIP_BUF_SIZE]; // reusable recipient buffer
 
-
 static void clear_cached_recipient(const char* thread) {
     apr_pool_t *pool = NULL;                                                
 
@@ -101,11 +144,11 @@ static void clear_cached_recipient(const char* thread) {
 
             // memory accumulates in the session_pool as sessions are cached then 
             // un-cached.  Un-caching removes strings from the hash, but not the 
-            // pool itself.  That only happens when the pool is destroyed. destroy 
-            // the session pool to clear any lingering memory
+            // pool itself.  That only happens when the pool is destroyed. Here
+            // we destroy the session pool to clear any lingering memory, then
+            // re-create it for future caching.
             apr_pool_destroy(trans->session_pool);
     
-            // create a standalone pool for our translator data
             if (apr_pool_create(&pool, NULL) != APR_SUCCESS) {
                 osrfLogError(OSRF_LOG_MARK, "WS Unable to create session_pool");
                 trans->session_pool = NULL;
@@ -118,7 +161,6 @@ static void clear_cached_recipient(const char* thread) {
 }
 
 
-
 void* osrf_responder_thread_main_body(transport_message *tmsg) {
 
     osrfList *msg_list = NULL;
@@ -175,7 +217,6 @@ void* osrf_responder_thread_main_body(transport_message *tmsg) {
     osrfListFree(msg_list);
 
     if (!trans->client_connected) {
-        // responses received after client disconnect are discarded
 
         osrfLogDebug(OSRF_LOG_MARK, 
             "WS discarding response for thread=%s, xid=%s", 
@@ -185,7 +226,8 @@ void* osrf_responder_thread_main_body(transport_message *tmsg) {
     }
 
     
-    // client is still connected; relay the messages to the client
+    // client is still connected. 
+    // relay the response messages to the client
     jsonObject *msg_wrapper = NULL;
     char *msg_string = NULL;
 
@@ -196,16 +238,15 @@ void* osrf_responder_thread_main_body(transport_message *tmsg) {
     jsonObjectSetKey(msg_wrapper, "osrf_msg", jsonParseRaw(tmsg->body));
 
     if (tmsg->is_error) {
-        fprintf(stderr,  
+        osrfLogError(OSRF_LOG_MARK, 
             "WS received jabber error message in response to thread=%s and xid=%s", 
             tmsg->thread, tmsg->osrf_xid);
-        fflush(stderr);
         jsonObjectSetKey(msg_wrapper, "transport_error", jsonNewBoolObject(1));
     }
 
     msg_string = jsonObjectToJSONRaw(msg_wrapper);
 
-    // deliver the wrapped message json to the websocket client
+    // drop the JSON on the outbound wire
     trans->server->send(trans->server, MESSAGE_TYPE_TEXT, 
         (unsigned char*) msg_string, strlen(msg_string));
 
@@ -249,7 +290,8 @@ void* APR_THREAD_FUNC osrf_responder_thread_main(apr_thread_t *thread, void *dat
 
 
 /**
- * Allocate the session cache and create the responder thread
+ * Connect to OpenSRF, create the main pool, responder thread
+ * session cache and session pool.
  */
 int child_init(const WebSocketServer *server) {
 
@@ -341,14 +383,15 @@ void* CALLBACK on_connect_handler(const WebSocketServer *server) {
         //"WS connect from %s", r->connection->client_ip); // apache 2.4
 
     if (!trans) {
+        // first connection
         if (child_init(server) != APR_SUCCESS) {
             return NULL;
         }
     }
 
     // create a standalone pool for the session cache values
-    // this pool will be destroyed and re-created regularly to 
-    // clear session memory
+    // this pool will be destroyed and re-created regularly
+    // to clear session memory
     if (apr_pool_create(&pool, r->pool) != APR_SUCCESS) {
         osrfLogError(OSRF_LOG_MARK, "WS Unable to create apr_pool");
         return NULL;
@@ -577,24 +620,25 @@ static size_t CALLBACK on_message_handler(void *data,
 
 
 /**
- * Release all memory allocated from the translator pool and kill the pool.
+ * Clear the session cache, release the session pool
  */
 void CALLBACK on_disconnect_handler(
     void *data, const WebSocketServer *server) {
 
     osrfWebsocketTranslator *trans = (osrfWebsocketTranslator*) data;
     trans->client_connected = 0;
+    
+    // ensure no errant session data is sticking around
+    apr_hash_clear(trans->session_cache);
 
-    /*
-    It's not necessary to destroy our session_pool, since
-    it's a child of the apache request_rec pool, which is 
-    destroyed after client disconnect.
+    // strictly speaking, this pool will get destroyed when
+    // r->pool is destroyed, but it doesn't hurt to explicitly
+    // destroy it ourselves.
     apr_pool_destroy(trans->session_pool);
-    */
-    
     trans->session_pool = NULL;
 
     request_rec *r = server->request(server);
+
     osrfLogDebug(OSRF_LOG_MARK, 
         "WS disconnect from %s", r->connection->remote_ip); 
         //"WS disconnect from %s", r->connection->client_ip); // apache 2.4
@@ -607,6 +651,7 @@ void CALLBACK on_destroy_handler(WebSocketPlugin *plugin) {
     if (trans) {
         apr_thread_exit(trans->responder_thread, APR_SUCCESS);
         apr_thread_mutex_destroy(trans->mutex);
+        apr_pool_destroy(trans->session_pool);
         apr_pool_destroy(trans->main_pool);
     }
 

commit f990a29db95d9b1c06efa22c1b3f4fbc43206571
Author: Bill Erickson <berick at esilibrary.com>
Date:   Mon Nov 12 16:46:19 2012 -0500

    LP#1268619: websocket translator
    
    * starting packet inspection
    * activity log; recipient removal
    * only cache connected recipients; use request_rec pool for session_pool parent
    * wrap all thread work in mutex
    * session memory goodness
    
    Signed-off-by: Bill Erickson <berick at esilibrary.com>
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>

diff --git a/src/gateway/osrf_websocket_translator.c b/src/gateway/osrf_websocket_translator.c
index b6205d8..35f986d 100644
--- a/src/gateway/osrf_websocket_translator.c
+++ b/src/gateway/osrf_websocket_translator.c
@@ -67,13 +67,15 @@
 
 #define MAX_THREAD_SIZE 64
 #define RECIP_BUF_SIZE 128
+#define WEBSOCKET_TRANSLATOR_INGRESS "ws-translator-v1"
 
 typedef struct _osrfWebsocketTranslator {
     const WebSocketServer *server;
     apr_pool_t *main_pool; // standalone per-process pool
-    apr_pool_t *session_pool; // child of trans->main_pool; per-session
+    apr_pool_t *session_pool; // child of r->pool; per-session
     apr_hash_t *session_cache; 
     apr_thread_t *responder_thread;
+    apr_thread_mutex_t *mutex;
     int client_connected;
     char* osrf_router;
     char* osrf_domain;
@@ -84,80 +86,168 @@ static transport_client *osrf_handle = NULL;
 static char recipient_buf[RECIP_BUF_SIZE]; // reusable recipient buffer
 
 
-/**
- * Responder thread main body.
- * Collects responses from the opensrf network and relays them to the 
- * websocket caller.
- */
-void* APR_THREAD_FUNC osrf_responder_thread_main(apr_thread_t *thread, void *data) {
+static void clear_cached_recipient(const char* thread) {
+    apr_pool_t *pool = NULL;                                                
 
-    transport_message *tmsg;
-    jsonObject *msg_wrapper;
-    char *msg_string;
+    if (apr_hash_get(trans->session_cache, thread, APR_HASH_KEY_STRING)) {
 
-    while (1) {
+        osrfLogDebug(OSRF_LOG_MARK, "WS removing cached recipient on disconnect");
 
-        tmsg = client_recv(osrf_handle, -1);
+        // remove it from the hash
+        apr_hash_set(trans->session_cache, thread, APR_HASH_KEY_STRING, NULL);
 
-        if (!tmsg) continue; // early exit on interrupt
-        
-        // discard responses received after client disconnect
-        if (!trans->client_connected) {
-            osrfLogDebug(OSRF_LOG_MARK, 
-                "WS discarding response for thread=%s, xid=%s", 
-                tmsg->thread, tmsg->osrf_xid);
-            message_free(tmsg);                                                         
-            continue; 
+        if (apr_hash_count(trans->session_cache) == 0) {
+            osrfLogDebug(OSRF_LOG_MARK, "WS re-setting session_pool");
+
+            // memory accumulates in the session_pool as sessions are cached then 
+            // un-cached.  Un-caching removes strings from the hash, but not the 
+            // pool itself.  That only happens when the pool is destroyed. destroy 
+            // the session pool to clear any lingering memory
+            apr_pool_destroy(trans->session_pool);
+    
+            // create a standalone pool for our translator data
+            if (apr_pool_create(&pool, NULL) != APR_SUCCESS) {
+                osrfLogError(OSRF_LOG_MARK, "WS Unable to create session_pool");
+                trans->session_pool = NULL;
+                return;
+            }
+
+            trans->session_pool = pool;
         }
+    }
+}
+
+
+
+void* osrf_responder_thread_main_body(transport_message *tmsg) {
 
+    osrfList *msg_list = NULL;
+    osrfMessage *one_msg = NULL;
+    int i;
+
+    osrfLogDebug(OSRF_LOG_MARK, 
+        "WS received opensrf response for thread=%s, xid=%s", 
+            tmsg->thread, tmsg->osrf_xid);
+
+    // first we need to perform some maintenance
+    msg_list = osrfMessageDeserialize(tmsg->body, NULL);
+
+    for (i = 0; i < msg_list->size; i++) {
+        one_msg = OSRF_LIST_GET_INDEX(msg_list, i);
 
         osrfLogDebug(OSRF_LOG_MARK, 
-            "WS received opensrf response for thread=%s, xid=%s", 
-                tmsg->thread, tmsg->osrf_xid);
-
-        // build the wrapper object
-        msg_wrapper = jsonNewObject(NULL);
-        jsonObjectSetKey(msg_wrapper, "thread", jsonNewObject(tmsg->thread));
-        jsonObjectSetKey(msg_wrapper, "log_xid", jsonNewObject(tmsg->osrf_xid));
-        jsonObjectSetKey(msg_wrapper, "osrf_msg", jsonParseRaw(tmsg->body));
-
-        if (tmsg->is_error) {
-            fprintf(stderr,  
-                "WS received jabber error message in response to thread=%s and xid=%s", 
-                tmsg->thread, tmsg->osrf_xid);
-            fflush(stderr);
-            jsonObjectSetKey(msg_wrapper, "transport_error", jsonNewBoolObject(1));
+            "WS returned response of type %d", one_msg->m_type);
+
+        /*  if our client just successfully connected to an opensrf service,
+            cache the sender so that future calls on this thread will use
+            the correct recipient. */
+        if (one_msg && one_msg->m_type == STATUS) {
+
+
+            // only cache recipients if the client is still connected
+            if (trans->client_connected && 
+                    one_msg->status_code == OSRF_STATUS_OK) {
+
+                if (!apr_hash_get(trans->session_cache, 
+                        tmsg->thread, APR_HASH_KEY_STRING)) {
+
+                    osrfLogDebug(OSRF_LOG_MARK, 
+                        "WS caching sender thread=%s, sender=%s", 
+                        tmsg->thread, tmsg->sender);
+
+                    apr_hash_set(trans->session_cache, 
+                        apr_pstrdup(trans->session_pool, tmsg->thread),
+                        APR_HASH_KEY_STRING, 
+                        apr_pstrdup(trans->session_pool, tmsg->sender));
+                }
+
+            } else {
+
+                // connection timed out; clear the cached recipient
+                // regardless of whether the client is still connected
+                if (one_msg->status_code == OSRF_STATUS_TIMEOUT)
+                    clear_cached_recipient(tmsg->thread);
+            }
         }
+    }
+
+    // maintenance is done
+    osrfListFree(msg_list);
+
+    if (!trans->client_connected) {
+        // responses received after client disconnect are discarded
+
+        osrfLogDebug(OSRF_LOG_MARK, 
+            "WS discarding response for thread=%s, xid=%s", 
+            tmsg->thread, tmsg->osrf_xid);
+
+        return;
+    }
+
+    
+    // client is still connected; relay the messages to the client
+    jsonObject *msg_wrapper = NULL;
+    char *msg_string = NULL;
+
+    // build the wrapper object
+    msg_wrapper = jsonNewObject(NULL);
+    jsonObjectSetKey(msg_wrapper, "thread", jsonNewObject(tmsg->thread));
+    jsonObjectSetKey(msg_wrapper, "log_xid", jsonNewObject(tmsg->osrf_xid));
+    jsonObjectSetKey(msg_wrapper, "osrf_msg", jsonParseRaw(tmsg->body));
+
+    if (tmsg->is_error) {
+        fprintf(stderr,  
+            "WS received jabber error message in response to thread=%s and xid=%s", 
+            tmsg->thread, tmsg->osrf_xid);
+        fflush(stderr);
+        jsonObjectSetKey(msg_wrapper, "transport_error", jsonNewBoolObject(1));
+    }
+
+    msg_string = jsonObjectToJSONRaw(msg_wrapper);
+
+    // deliver the wrapped message json to the websocket client
+    trans->server->send(trans->server, MESSAGE_TYPE_TEXT, 
+        (unsigned char*) msg_string, strlen(msg_string));
+
+    free(msg_string);
+    jsonObjectFree(msg_wrapper);
+
+}
+
+/**
+ * Responder thread main body.
+ * Collects responses from the opensrf network and relays them to the 
+ * websocket caller.
+ */
+void* APR_THREAD_FUNC osrf_responder_thread_main(apr_thread_t *thread, void *data) {
 
-        msg_string = jsonObjectToJSONRaw(msg_wrapper);
+    transport_message *tmsg;
+    while (1) {
 
-        // deliver the wrapped message json to the websocket client
-        trans->server->send(trans->server, MESSAGE_TYPE_TEXT, 
-            (unsigned char*) msg_string, strlen(msg_string));
+        if (apr_thread_mutex_unlock(trans->mutex) != APR_SUCCESS) {
+            osrfLogError(OSRF_LOG_MARK, "WS error un-locking thread mutex");
+            return NULL;
+        }
 
-        // capture the true message sender
-        // TODO: this will grow to add one entry per client session.  
-        // need to ensure that connected-sessions don't last /too/ long or create 
-        // a last-touched timeout mechanism to periodically remove old  entries
-        if (!apr_hash_get(trans->session_cache, tmsg->thread, APR_HASH_KEY_STRING)) {
+        // wait for a response
+        tmsg = client_recv(osrf_handle, -1);
 
-            osrfLogDebug(OSRF_LOG_MARK, 
-                "WS caching sender thread=%s, sender=%s", tmsg->thread, tmsg->sender);
+        if (!tmsg) continue; // early exit on interrupt
 
-            apr_hash_set(trans->session_cache, 
-                apr_pstrdup(trans->session_pool, tmsg->thread),
-                APR_HASH_KEY_STRING, 
-                apr_pstrdup(trans->session_pool, tmsg->sender));
+        if (apr_thread_mutex_lock(trans->mutex) != APR_SUCCESS) {
+            osrfLogError(OSRF_LOG_MARK, "WS error locking thread mutex");
+            return NULL;
         }
 
-        free(msg_string);
-        jsonObjectFree(msg_wrapper);
+        osrf_responder_thread_main_body(tmsg);
         message_free(tmsg);                                                         
     }
 
     return NULL;
 }
 
+
+
 /**
  * Allocate the session cache and create the responder thread
  */
@@ -166,6 +256,7 @@ int child_init(const WebSocketServer *server) {
     apr_pool_t *pool = NULL;                                                
     apr_thread_t *thread = NULL;
     apr_threadattr_t *thread_attr = NULL;
+    apr_thread_mutex_t *mutex = NULL;
     request_rec *r = server->request(server);
         
     osrfLogDebug(OSRF_LOG_MARK, "WS child_init");
@@ -205,6 +296,13 @@ int child_init(const WebSocketServer *server) {
     trans->osrf_router = osrfConfigGetValue(NULL, "/router_name");                      
     trans->osrf_domain = osrfConfigGetValue(NULL, "/domain");
 
+    trans->session_cache = apr_hash_make(pool);
+
+    if (trans->session_cache == NULL) {
+        osrfLogError(OSRF_LOG_MARK, "WS unable to create session cache");
+        return 1;
+    }
+
     // Create the responder thread.  Once created, 
     // it runs for the lifetime of this process.
     if ( (apr_threadattr_create(&thread_attr, trans->main_pool) == APR_SUCCESS) &&
@@ -219,6 +317,15 @@ int child_init(const WebSocketServer *server) {
         return 1;
     }
 
+    if (apr_thread_mutex_create(
+            &mutex, APR_THREAD_MUTEX_UNNESTED, 
+            trans->main_pool) != APR_SUCCESS) {
+        osrfLogError(OSRF_LOG_MARK, "WS unable to create thread mutex");
+        return 1;
+    }
+
+    trans->mutex = mutex;
+
     return APR_SUCCESS;
 }
 
@@ -239,31 +346,104 @@ void* CALLBACK on_connect_handler(const WebSocketServer *server) {
         }
     }
 
-    // create a standalone pool for the session cache values, which will be
-    // destroyed on client disconnect.
-    if (apr_pool_create(&pool, trans->main_pool) != APR_SUCCESS) {
+    // create a standalone pool for the session cache values
+    // this pool will be destroyed and re-created regularly to 
+    // clear session memory
+    if (apr_pool_create(&pool, r->pool) != APR_SUCCESS) {
         osrfLogError(OSRF_LOG_MARK, "WS Unable to create apr_pool");
         return NULL;
     }
 
     trans->session_pool = pool;
-    trans->session_cache = apr_hash_make(trans->session_pool);
-
-    if (trans->session_cache == NULL) {
-        osrfLogError(OSRF_LOG_MARK, "WS unable to create session cache");
-        return NULL;
-    }
-
     trans->client_connected = 1;
     return trans;
 }
 
 
+/** 
+ * for each inbound opensrf message:
+ * 1. Stamp the ingress
+ * 2. REQUEST: log it as activity
+ * 3. DISCONNECT: remove the cached recipient
+ * then re-string-ify for xmpp delivery
+ */
+
+static char* extract_inbound_messages(
+        const request_rec *r, 
+        const char* service, 
+        const char* thread, 
+        const char* recipient, 
+        const jsonObject *osrf_msg) {
+
+    int i;
+    int num_msgs = osrf_msg->size;
+    osrfMessage* msg;
+    osrfMessage* msg_list[num_msgs];
+
+    // here we do an extra json round-trip to get the data
+    // in a form osrf_message_deserialize can understand
+    char *osrf_msg_json = jsonObjectToJSON(osrf_msg);
+    osrf_message_deserialize(osrf_msg_json, msg_list, num_msgs);
+    free(osrf_msg_json);
+
+    // should we require the caller to always pass the service?
+    if (service == NULL) service = "";
+
+    for(i = 0; i < num_msgs; i++) {
+        msg = msg_list[i];
+        osrfMessageSetIngress(msg, WEBSOCKET_TRANSLATOR_INGRESS);
+
+        switch(msg->m_type) {
+
+            case REQUEST: {
+                const jsonObject* params = msg->_params;
+                growing_buffer* act = buffer_init(128);
+                char* method = msg->method_name;
+                buffer_fadd(act, "[%s] [%s] %s %s", 
+                    r->connection->remote_ip, "", service, method);
+
+                const jsonObject* obj = NULL;
+                int i = 0;
+                const char* str;
+                int redactParams = 0;
+                while( (str = osrfStringArrayGetString(log_protect_arr, i++)) ) {
+                    if(!strncmp(method, str, strlen(str))) {
+                        redactParams = 1;
+                        break;
+                    }
+                }
+                if(redactParams) {
+                    OSRF_BUFFER_ADD(act, " **PARAMS REDACTED**");
+                } else {
+                    i = 0;
+                    while((obj = jsonObjectGetIndex(params, i++))) {
+                        char* str = jsonObjectToJSON(obj);
+                        if( i == 1 )
+                            OSRF_BUFFER_ADD(act, " ");
+                        else
+                            OSRF_BUFFER_ADD(act, ", ");
+                        OSRF_BUFFER_ADD(act, str);
+                        free(str);
+                    }
+                }
+                osrfLogActivity(OSRF_LOG_MARK, "%s", act->buf);
+                buffer_free(act);
+                break;
+            }
+
+            case DISCONNECT:
+                clear_cached_recipient(thread);
+                break;
+        }
+    }
+
+    return osrfMessageSerializeBatch(msg_list, num_msgs);
+}
 
 /**
  * Parse opensrf request and relay the request to the opensrf network.
  */
-static size_t CALLBACK on_message_handler(void *data,
+static size_t on_message_handler_body(void *data,
                 const WebSocketServer *server, const int type, 
                 unsigned char *buffer, const size_t buffer_size) {
 
@@ -277,6 +457,7 @@ static size_t CALLBACK on_message_handler(void *data,
     const char *log_xid = NULL;
     char *msg_body = NULL;
     char *recipient = NULL;
+    int i;
 
     if (buffer_size <= 0) return OK;
 
@@ -287,7 +468,7 @@ static size_t CALLBACK on_message_handler(void *data,
     memcpy(buf, buffer, buffer_size);
     buf[buffer_size] = '\0';
 
-    msg_wrapper = jsonParseRaw(buf);
+    msg_wrapper = jsonParse(buf);
 
     if (msg_wrapper == NULL) {
         osrfLogWarning(OSRF_LOG_MARK, "WS Invalid JSON: %s", buf);
@@ -353,20 +534,21 @@ static size_t CALLBACK on_message_handler(void *data,
         }
     }
 
-    // TODO: activity log entry? -- requires message analysis
     osrfLogDebug(OSRF_LOG_MARK, 
         "WS relaying message thread=%s, xid=%s, recipient=%s", 
             thread, osrfLogGetXid(), recipient);
 
-    msg_body = jsonObjectToJSONRaw(osrf_msg);
+    msg_body = extract_inbound_messages(
+        r, service, thread, recipient, osrf_msg);
 
     transport_message *tmsg = message_init(
         msg_body, NULL, thread, recipient, NULL);
 
-    message_set_osrf_xid(tmsg, osrfLogGetXid());                                
-    client_send_message(osrf_handle, tmsg);                                   
-    osrfLogClearXid();
+    message_set_osrf_xid(tmsg, osrfLogGetXid());
+    client_send_message(osrf_handle, tmsg);
+
 
+    osrfLogClearXid();
     message_free(tmsg);                                                         
     jsonObjectFree(msg_wrapper);
     free(msg_body);
@@ -374,6 +556,25 @@ static size_t CALLBACK on_message_handler(void *data,
     return OK;
 }
 
+static size_t CALLBACK on_message_handler(void *data,
+                const WebSocketServer *server, const int type, 
+                unsigned char *buffer, const size_t buffer_size) {
+
+    if (apr_thread_mutex_lock(trans->mutex) != APR_SUCCESS) {
+        osrfLogError(OSRF_LOG_MARK, "WS error locking thread mutex");
+        return 1; // TODO: map to apr_status_t value?
+    }
+
+    apr_status_t stat = on_message_handler_body(data, server, type, buffer, buffer_size);
+
+    if (apr_thread_mutex_unlock(trans->mutex) != APR_SUCCESS) {
+        osrfLogError(OSRF_LOG_MARK, "WS error locking thread mutex");
+        return 1;
+    }
+
+    return stat;
+}
+
 
 /**
  * Release all memory allocated from the translator pool and kill the pool.
@@ -384,10 +585,14 @@ void CALLBACK on_disconnect_handler(
     osrfWebsocketTranslator *trans = (osrfWebsocketTranslator*) data;
     trans->client_connected = 0;
 
-    apr_hash_clear(trans->session_cache);
+    /*
+    It's not necessary to destroy our session_pool, since
+    it's a child of the apache request_rec pool, which is 
+    destroyed after client disconnect.
     apr_pool_destroy(trans->session_pool);
+    */
+    
     trans->session_pool = NULL;
-    trans->session_cache = NULL;
 
     request_rec *r = server->request(server);
     osrfLogDebug(OSRF_LOG_MARK, 
@@ -401,6 +606,7 @@ void CALLBACK on_disconnect_handler(
 void CALLBACK on_destroy_handler(WebSocketPlugin *plugin) {
     if (trans) {
         apr_thread_exit(trans->responder_thread, APR_SUCCESS);
+        apr_thread_mutex_destroy(trans->mutex);
         apr_pool_destroy(trans->main_pool);
     }
 

commit 27707398e955b8a8a2df1a5311aebc19b8eb1708
Author: Bill Erickson <berick at esilibrary.com>
Date:   Thu Nov 8 12:36:16 2012 -0500

    LP#1268619: websocket JS additions
    
    Signed-off-by: Bill Erickson <berick at esilibrary.com>
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>

diff --git a/src/javascript/opensrf.js b/src/javascript/opensrf.js
index 408f0af..813bef8 100644
--- a/src/javascript/opensrf.js
+++ b/src/javascript/opensrf.js
@@ -206,9 +206,8 @@ OpenSRF.Session = function() {
     this.state = OSRF_APP_SESSION_DISCONNECTED;
 };
 
-OpenSRF.Session.transport = OSRF_TRANSPORT_TYPE_WS;
-if (true || typeof WebSocket == 'undefined')
-    OpenSRF.Session.transport = OSRF_TRANSPORT_TYPE_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) {
@@ -222,7 +221,7 @@ 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, args);
+            return this.send_ws(osrf_msg);
         case OSRF_TRANSPORT_TYPE_XHR:
             return this.send_xhr(osrf_msg, args);
         case OSRF_TRANSPORT_TYPE_XMPP:
@@ -237,18 +236,11 @@ OpenSRF.Session.prototype.send_xhr = function(osrf_msg, args) {
     new OpenSRF.XHRequest(osrf_msg, args).send();
 };
 
-OpenSRF.Session.prototype.send_ws = function(osrf_msg, args) {
-    args.session = this;
-    if (this.websocket) {
-        this.websocket.args = args; // callbacks
-        this.websocket.send(osrf_msg);
-    } else {
-        this.websocket = new OpenSRF.WSRequest(
-            this, args, function(wsreq) {
-                wsreq.send(osrf_msg);
-            }
-        );
-    }
+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) {
@@ -262,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');
@@ -412,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() {
@@ -435,49 +428,59 @@ function log(msg) {
 }
 
 // ses may be passed to us by the network handler
-OpenSRF.Stack.push = function(net_msg, callbacks, ses) {
-    if (!ses) ses = OpenSRF.Session.find_session(net_msg.thread); 
+OpenSRF.Stack.push = function(net_msg, callbacks) {
+    var ses = OpenSRF.Session.find_session(net_msg.thread); 
     if (!ses) return;
     ses.remote_id = net_msg.from;
-    osrf_msgs = [];
 
-    try {
-        osrf_msgs = JSON2js(net_msg.body);
+    // NetMessage's from websocket connections are parsed before they get here
+    osrf_msgs = net_msg.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...');
+    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) {
 
@@ -486,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);
                 }
             }
         }
@@ -507,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
index d522834..62302be 100644
--- a/src/javascript/opensrf_ws.js
+++ b/src/javascript/opensrf_ws.js
@@ -13,92 +13,153 @@
  * GNU General Public License for more details.
  * ----------------------------------------------------------------------- */
 
-var WS_PATH = '/osrf-websocket';
+// 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;
+};
 
-/**
- * onopen is required. no data can be sent 
- * until the async connection dance completes.
- */
-OpenSRF.WSRequest = function(session, args, onopen) {
-    this.session = session;
-    this.args = args;
+// global pool of connection objects; name => connection map
+OpenSRF.WebSocketConnection.pool = {};
 
-    var proto = location.protocol == 'https' ? 'wss' : 'ws';
+OpenSRF.WebSocketConnection.defaultConnection = function() {
+    return OpenSRF.WebSocketConnection.pool['default'];
+}
 
-    var path = proto + '://' + location.host + 
-        WS_PATH + '?service=' + this.session.service;
+/**
+ * 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.ws = new WebSocket(path);
+        this.socket = new WebSocket(this.path);
     } catch(e) {
-        throw new Error("WebSocket() not supported in this browser " + e);
-    }
-
-    var self = this;
-
-    this.ws.onopen = function(evt) {
-        onopen(self);
+        throw new Error("WebSocket() not supported in this browser: " + e);
     }
 
-    this.ws.onmessage = function(evt) {
-        self.core_handler(evt.data);
-    }
+    this.socket.onopen = this.handlers.onopen;
+    this.socket.onmessage = this.handlers.onmessage;
+    this.socket.onerror = this.handlers.onerror;
+    this.socket.onclose = this.handlers.onclose;
+};
 
-    this.ws.onerror = function(evt) {
-        self.transport_error_handler(evt.data);
-    }
+/** 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)
+    );
+};
 
-    this.ws.onclose = function(evt) {
-    }
+/** default error handler */
+OpenSRF.WebSocketConnection.default_onerror = function(evt) {
+    throw new Error("WebSocket Error " + evt + ' : ' + evt.data);
 };
 
-OpenSRF.WSRequest.prototype.send = function(message) {
-    //console.log('sending: ' + js2JSON([message.serialize()]));
-    this.last_message = message;
-    this.ws.send(js2JSON([message.serialize()]));
-    return this;
+
+/** shut it down */
+OpenSRF.WebSocketConnection.prototype.destroy = function() {
+    this.socket.close();
+    delete OpenSRF.WebSocketConnection.pool[this.name];
 };
 
-OpenSRF.WSRequest.prototype.close = function() {
-    try { this.ws.close(); } catch(e) {}
+/**
+ * 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.WSRequest.prototype.core_handler = function(json) {
-    //console.log('received: ' + json);
+OpenSRF.WebSocketRequest.prototype.setupConnection = function(args) {
+    var self = this;
 
-    OpenSRF.Stack.push(
-        new OpenSRF.NetMessage(null, null, '', json),
-        {
-            onresponse : this.args.onresponse,
-            oncomplete : this.args.oncomplete,
-            onerror : this.args.onerror,
-            onmethoderror : this.method_error_handler()
-        },
-        this.args.session
-    );
-};
+    var cname = args.name || 'default';
+    this.wsc = OpenSRF.WebSocketConnection.pool[cname];
 
+    if (this.wsc) { // we have a WebSocketConnection.  
 
-OpenSRF.WSRequest.prototype.method_error_handler = function() {
-    var self = this;
-    return function(req, status, status_text) {
-        if(self.args.onmethoderror) 
-            self.args.onmethoderror(req, status, status_text);
+        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;
 
-        if(self.args.onerror)  {
-            self.args.onerror(
-                self.last_message, self.session.service, '');
+            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();
         }
-    };
-};
 
-OpenSRF.WSRequest.prototype.transport_error_handler = function(msg) {
-    if(this.args.ontransporterror) {
-        this.args.ontransporterror(msg);
-    }
-    if(this.args.onerror) {
-        this.args.onerror(msg, this.session.service, '');
+    } 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;
 };
 
 
+
+

commit cc42cb62c47edabd693e491ad0d939970d7dbc53
Author: Bill Erickson <berick at esilibrary.com>
Date:   Tue Oct 30 15:51:18 2012 -0400

    LP#1268619: track websocket_plugin.h locally until replaced by makefile.install process
    
    Signed-off-by: Bill Erickson <berick at esilibrary.com>
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>

diff --git a/src/gateway/websocket_plugin.h b/src/gateway/websocket_plugin.h
new file mode 100644
index 0000000..419d47b
--- /dev/null
+++ b/src/gateway/websocket_plugin.h
@@ -0,0 +1,130 @@
+/*
+ * Copyright 2010-2011 self.disconnect
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#if !defined(_MOD_WEBSOCKET_H_)
+#define _MOD_WEBSOCKET_H_
+
+#include <stdlib.h>
+
+#if defined(__cplusplus)
+extern "C"
+{
+#endif
+
+#if defined(_WIN32)
+#define EXPORT __declspec(dllexport)
+#define CALLBACK __stdcall
+#else
+#define EXPORT
+#define CALLBACK
+#endif
+
+#define MESSAGE_TYPE_INVALID  -1
+#define MESSAGE_TYPE_TEXT      0
+#define MESSAGE_TYPE_BINARY  128
+#define MESSAGE_TYPE_CLOSE   255
+#define MESSAGE_TYPE_PING    256
+#define MESSAGE_TYPE_PONG    257
+
+    struct _WebSocketServer;
+
+    typedef struct request_rec *(CALLBACK * WS_Request)
+                                (const struct _WebSocketServer *server);
+
+    typedef const char *(CALLBACK * WS_Header_Get)
+                        (const struct _WebSocketServer *server,
+                         const char *key);
+
+    typedef void (CALLBACK * WS_Header_Set)
+                 (const struct _WebSocketServer *server,
+                  const char *key,
+                  const char *value);
+
+    typedef size_t (CALLBACK * WS_Protocol_Count)
+                   (const struct _WebSocketServer *server);
+
+    typedef const char *(CALLBACK * WS_Protocol_Index)
+                        (const struct _WebSocketServer *server,
+                         const size_t index);
+
+    typedef void (CALLBACK * WS_Protocol_Set)
+                 (const struct _WebSocketServer *server,
+                  const char *protocol);
+
+    typedef size_t (CALLBACK * WS_Send)
+                   (const struct _WebSocketServer *server,
+                    const int type,
+                    const unsigned char *buffer,
+                    const size_t buffer_size);
+
+    typedef void (CALLBACK * WS_Close)
+                 (const struct _WebSocketServer *server);
+
+#define WEBSOCKET_SERVER_VERSION_1 1
+
+    typedef struct _WebSocketServer
+    {
+        unsigned int size;
+        unsigned int version;
+        struct _WebSocketState *state;
+        WS_Request request;
+        WS_Header_Get header_get;
+        WS_Header_Set header_set;
+        WS_Protocol_Count protocol_count;
+        WS_Protocol_Index protocol_index;
+        WS_Protocol_Set protocol_set;
+        WS_Send send;
+        WS_Close close;
+    } WebSocketServer;
+
+    struct _WebSocketPlugin;
+
+    typedef struct _WebSocketPlugin *(CALLBACK * WS_Init)
+                                     ();
+    typedef void (CALLBACK * WS_Destroy)
+                 (struct _WebSocketPlugin *plugin);
+
+    typedef void *(CALLBACK * WS_OnConnect)
+                  (const WebSocketServer *server); /* Returns plugin_private */
+
+    typedef size_t (CALLBACK * WS_OnMessage)
+                   (void *plugin_private,
+                    const WebSocketServer *server,
+                    const int type,
+                    unsigned char *buffer,
+                    const size_t buffer_size);
+
+    typedef void (CALLBACK * WS_OnDisconnect)
+                 (void *plugin_private,
+                  const WebSocketServer *server);
+
+#define WEBSOCKET_PLUGIN_VERSION_0 0
+
+  typedef struct _WebSocketPlugin
+  {
+      unsigned int size;
+      unsigned int version;
+      WS_Destroy destroy;
+      WS_OnConnect on_connect;
+      WS_OnMessage on_message;
+      WS_OnDisconnect on_disconnect;
+  } WebSocketPlugin;
+
+#if defined(__cplusplus)
+}
+#endif
+
+#endif                          /* _MOD_WEBSOCKET_H_ */

commit 9e455c227be32bed4a16e6dab7045b6424e2ba15
Author: Bill Erickson <berick at esilibrary.com>
Date:   Mon Oct 29 17:27:44 2012 -0400

    LP#1268619: websocket gateway repairs and cleanup
    
    * use jsonObjectFree() on jsonObjets, not free();
    * removed some debugging logs
    * accommodate API changes for Apache 2.4
    * safer logging:
    
      Avoid using ap_log_rerror, in particular referencing server->request
      from the responder thread, since the request_rec will be invalid after
      on_disconnect is called.
    
    Signed-off-by: Bill Erickson <berick at esilibrary.com>
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>

diff --git a/src/gateway/osrf_websocket_translator.c b/src/gateway/osrf_websocket_translator.c
index fd19f2d..b6205d8 100644
--- a/src/gateway/osrf_websocket_translator.c
+++ b/src/gateway/osrf_websocket_translator.c
@@ -54,12 +54,11 @@
  */
 
 #include "httpd.h"
-#include "http_log.h"
-#include "http_log.h"
 #include "apr_strings.h"
 #include "apr_thread_proc.h"
 #include "apr_hash.h"
 #include "websocket_plugin.h"
+#include "opensrf/log.h"
 #include "opensrf/osrf_json.h"
 #include "opensrf/transport_client.h"
 #include "opensrf/transport_message.h"
@@ -68,12 +67,10 @@
 
 #define MAX_THREAD_SIZE 64
 #define RECIP_BUF_SIZE 128
-static char recipient_buf[RECIP_BUF_SIZE]; // reusable recipient buffer
-static transport_client *osrf_handle = NULL;
 
 typedef struct _osrfWebsocketTranslator {
     const WebSocketServer *server;
-    apr_pool_t *main_pool; // standline per-process pool
+    apr_pool_t *main_pool; // standalone per-process pool
     apr_pool_t *session_pool; // child of trans->main_pool; per-session
     apr_hash_t *session_cache; 
     apr_thread_t *responder_thread;
@@ -83,6 +80,8 @@ typedef struct _osrfWebsocketTranslator {
 } osrfWebsocketTranslator;
 
 static osrfWebsocketTranslator *trans = NULL;
+static transport_client *osrf_handle = NULL;
+static char recipient_buf[RECIP_BUF_SIZE]; // reusable recipient buffer
 
 
 /**
@@ -92,38 +91,41 @@ static osrfWebsocketTranslator *trans = NULL;
  */
 void* APR_THREAD_FUNC osrf_responder_thread_main(apr_thread_t *thread, void *data) {
 
-    request_rec *r = trans->server->request(trans->server);
+    transport_message *tmsg;
     jsonObject *msg_wrapper;
     char *msg_string;
 
     while (1) {
 
-        transport_message *msg = client_recv(osrf_handle, -1);
-        if (!msg) continue; // early exit on interrupt
+        tmsg = client_recv(osrf_handle, -1);
+
+        if (!tmsg) continue; // early exit on interrupt
         
         // discard responses received after client disconnect
         if (!trans->client_connected) {
-            ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, 
+            osrfLogDebug(OSRF_LOG_MARK, 
                 "WS discarding response for thread=%s, xid=%s", 
-                    msg->thread, msg->osrf_xid);
-            message_free(msg);                                                         
+                tmsg->thread, tmsg->osrf_xid);
+            message_free(tmsg);                                                         
             continue; 
         }
 
-        ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, 
+
+        osrfLogDebug(OSRF_LOG_MARK, 
             "WS received opensrf response for thread=%s, xid=%s", 
-                msg->thread, msg->osrf_xid);
+                tmsg->thread, tmsg->osrf_xid);
 
         // build the wrapper object
         msg_wrapper = jsonNewObject(NULL);
-        jsonObjectSetKey(msg_wrapper, "thread", jsonNewObject(msg->thread));
-        jsonObjectSetKey(msg_wrapper, "log_xid", jsonNewObject(msg->osrf_xid));
-        jsonObjectSetKey(msg_wrapper, "osrf_msg", jsonParseRaw(msg->body));
+        jsonObjectSetKey(msg_wrapper, "thread", jsonNewObject(tmsg->thread));
+        jsonObjectSetKey(msg_wrapper, "log_xid", jsonNewObject(tmsg->osrf_xid));
+        jsonObjectSetKey(msg_wrapper, "osrf_msg", jsonParseRaw(tmsg->body));
 
-        if (msg->is_error) {
-            ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, 
+        if (tmsg->is_error) {
+            fprintf(stderr,  
                 "WS received jabber error message in response to thread=%s and xid=%s", 
-                    msg->thread, msg->osrf_xid);
+                tmsg->thread, tmsg->osrf_xid);
+            fflush(stderr);
             jsonObjectSetKey(msg_wrapper, "transport_error", jsonNewBoolObject(1));
         }
 
@@ -135,21 +137,22 @@ void* APR_THREAD_FUNC osrf_responder_thread_main(apr_thread_t *thread, void *dat
 
         // capture the true message sender
         // TODO: this will grow to add one entry per client session.  
-        // need a last-touched timeout mechanism to periodically remove old entries
-        if (!apr_hash_get(trans->session_cache, msg->thread, APR_HASH_KEY_STRING)) {
+        // need to ensure that connected-sessions don't last /too/ long or create 
+        // a last-touched timeout mechanism to periodically remove old  entries
+        if (!apr_hash_get(trans->session_cache, tmsg->thread, APR_HASH_KEY_STRING)) {
 
-            ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, 
-                "WS caching sender thread=%s, sender=%s", msg->thread, msg->sender);
+            osrfLogDebug(OSRF_LOG_MARK, 
+                "WS caching sender thread=%s, sender=%s", tmsg->thread, tmsg->sender);
 
             apr_hash_set(trans->session_cache, 
-                apr_pstrdup(trans->session_pool, msg->thread),
+                apr_pstrdup(trans->session_pool, tmsg->thread),
                 APR_HASH_KEY_STRING, 
-                apr_pstrdup(trans->session_pool, msg->sender));
+                apr_pstrdup(trans->session_pool, tmsg->sender));
         }
 
         free(msg_string);
         jsonObjectFree(msg_wrapper);
-        message_free(msg);                                                         
+        message_free(tmsg);                                                         
     }
 
     return NULL;
@@ -165,7 +168,7 @@ int child_init(const WebSocketServer *server) {
     apr_threadattr_t *thread_attr = NULL;
     request_rec *r = server->request(server);
         
-    ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, "WS child_init");
+    osrfLogDebug(OSRF_LOG_MARK, "WS child_init");
 
     // osrf_handle will already be connected if this is not the first request
     // served by this process.
@@ -173,7 +176,7 @@ int child_init(const WebSocketServer *server) {
         char* config_file = "/openils/conf/opensrf_core.xml";
         char* config_ctx = "gateway"; //TODO config
         if (!osrfSystemBootstrapClientResc(config_file, config_ctx, "websocket")) {   
-            ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r,                              
+            osrfLogError(OSRF_LOG_MARK, 
                 "WS unable to bootstrap OpenSRF client with config %s", config_file); 
             return 1;
         }
@@ -183,7 +186,7 @@ int child_init(const WebSocketServer *server) {
 
     // create a standalone pool for our translator data
     if (apr_pool_create(&pool, NULL) != APR_SUCCESS) {
-        ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "WS Unable to create apr_pool");
+        osrfLogError(OSRF_LOG_MARK, "WS Unable to create apr_pool");
         return 1;
     }
 
@@ -193,7 +196,7 @@ int child_init(const WebSocketServer *server) {
         apr_palloc(pool, sizeof(osrfWebsocketTranslator));
 
     if (trans == NULL) {
-        ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "WS Unable to create translator");
+        osrfLogError(OSRF_LOG_MARK, "WS Unable to create translator");
         return 1;
     }
 
@@ -202,8 +205,8 @@ int child_init(const WebSocketServer *server) {
     trans->osrf_router = osrfConfigGetValue(NULL, "/router_name");                      
     trans->osrf_domain = osrfConfigGetValue(NULL, "/domain");
 
-    // Create the responder thread.  Once created, it runs for the lifetime
-    // of this process.
+    // Create the responder thread.  Once created, 
+    // it runs for the lifetime of this process.
     if ( (apr_threadattr_create(&thread_attr, trans->main_pool) == APR_SUCCESS) &&
          (apr_threadattr_detach_set(thread_attr, 0) == APR_SUCCESS) &&
          (apr_thread_create(&thread, thread_attr, 
@@ -212,8 +215,7 @@ int child_init(const WebSocketServer *server) {
         trans->responder_thread = thread;
         
     } else {
-        ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, 
-            "WS unable to create responder thread");
+        osrfLogError(OSRF_LOG_MARK, "WS unable to create responder thread");
         return 1;
     }
 
@@ -227,8 +229,9 @@ void* CALLBACK on_connect_handler(const WebSocketServer *server) {
     request_rec *r = server->request(server);
     apr_pool_t *pool;
 
-    ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, 
-        "WS connect from %s", r->connection->remote_ip);
+    osrfLogDebug(OSRF_LOG_MARK, 
+        "WS connect from %s", r->connection->remote_ip); 
+        //"WS connect from %s", r->connection->client_ip); // apache 2.4
 
     if (!trans) {
         if (child_init(server) != APR_SUCCESS) {
@@ -239,26 +242,18 @@ void* CALLBACK on_connect_handler(const WebSocketServer *server) {
     // create a standalone pool for the session cache values, which will be
     // destroyed on client disconnect.
     if (apr_pool_create(&pool, trans->main_pool) != APR_SUCCESS) {
-        ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, 
-            "WS Unable to create apr_pool");
+        osrfLogError(OSRF_LOG_MARK, "WS Unable to create apr_pool");
         return NULL;
     }
 
     trans->session_pool = pool;
     trans->session_cache = apr_hash_make(trans->session_pool);
 
-    ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, 
-        "WS created new pool %x", trans->session_pool);
-
     if (trans->session_cache == NULL) {
-        ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, 
-            "WS unable to create session cache");
+        osrfLogError(OSRF_LOG_MARK, "WS unable to create session cache");
         return NULL;
     }
 
-    ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, 
-        "WS created new hash %x", trans->session_cache);
-
     trans->client_connected = 1;
     return trans;
 }
@@ -285,8 +280,7 @@ static size_t CALLBACK on_message_handler(void *data,
 
     if (buffer_size <= 0) return OK;
 
-    ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, 
-        "WS received message size=%d", buffer_size);
+    osrfLogDebug(OSRF_LOG_MARK, "WS received message size=%d", buffer_size);
 
     // buffer may not be \0-terminated, which jsonParse requires
     char buf[buffer_size + 1];
@@ -296,8 +290,7 @@ static size_t CALLBACK on_message_handler(void *data,
     msg_wrapper = jsonParseRaw(buf);
 
     if (msg_wrapper == NULL) {
-        ap_log_rerror(APLOG_MARK, 
-            APLOG_NOTICE, 0, r, "WS Invalid JSON: %s", buf);
+        osrfLogWarning(OSRF_LOG_MARK, "WS Invalid JSON: %s", buf);
         return HTTP_BAD_REQUEST;
     }
 
@@ -313,13 +306,17 @@ static size_t CALLBACK on_message_handler(void *data,
         log_xid = jsonObjectGetString(tmp_obj);
 
     if (log_xid) {
+
         // use the caller-provide log trace id
         if (strlen(log_xid) > MAX_THREAD_SIZE) {
-            ap_log_rerror(APLOG_MARK, APLOG_NOTICE, 
-                0, r, "WS log_xid exceeds max length");
+            osrfLogWarning(OSRF_LOG_MARK, "WS log_xid exceeds max length");
             return HTTP_BAD_REQUEST;
         }
-        osrfLogSetXid(log_xid); // TODO: make with with non-client
+
+        // TODO: make this work with non-client and make this call accept 
+        // const char*'s.  casting to (char*) for now to silence warnings.
+        osrfLogSetXid((char*) log_xid); 
+
     } else {
         // generate a new log trace id for this relay
         osrfLogMkXid();
@@ -328,8 +325,7 @@ static size_t CALLBACK on_message_handler(void *data,
     if (thread) {
 
         if (strlen(thread) > MAX_THREAD_SIZE) {
-            ap_log_rerror(APLOG_MARK, APLOG_NOTICE, 
-                0, r, "WS thread exceeds max length");
+            osrfLogWarning(OSRF_LOG_MARK, "WS thread exceeds max length");
             return HTTP_BAD_REQUEST;
         }
 
@@ -339,8 +335,7 @@ static size_t CALLBACK on_message_handler(void *data,
             trans->session_cache, thread, APR_HASH_KEY_STRING);
 
         if (recipient) {
-            ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, 
-                "WS found cached recipient %s", recipient);
+            osrfLogDebug(OSRF_LOG_MARK, "WS found cached recipient %s", recipient);
         }
     }
 
@@ -353,14 +348,13 @@ static size_t CALLBACK on_message_handler(void *data,
             recipient = recipient_buf;
 
         } else {
-            ap_log_rerror(APLOG_MARK, APLOG_NOTICE, 
-                0, r, "WS Unable to determine recipient");
+            osrfLogWarning(OSRF_LOG_MARK, "WS Unable to determine recipient");
             return HTTP_BAD_REQUEST;
         }
     }
 
     // TODO: activity log entry? -- requires message analysis
-    ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, 
+    osrfLogDebug(OSRF_LOG_MARK, 
         "WS relaying message thread=%s, xid=%s, recipient=%s", 
             thread, osrfLogGetXid(), recipient);
 
@@ -374,7 +368,7 @@ static size_t CALLBACK on_message_handler(void *data,
     osrfLogClearXid();
 
     message_free(tmsg);                                                         
-    free(msg_wrapper);
+    jsonObjectFree(msg_wrapper);
     free(msg_body);
 
     return OK;
@@ -396,14 +390,15 @@ void CALLBACK on_disconnect_handler(
     trans->session_cache = NULL;
 
     request_rec *r = server->request(server);
-    ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, 
-        "WS disconnect from %s", r->connection->remote_ip);
+    osrfLogDebug(OSRF_LOG_MARK, 
+        "WS disconnect from %s", r->connection->remote_ip); 
+        //"WS disconnect from %s", r->connection->client_ip); // apache 2.4
 }
 
+/**
+ * Be nice and clean up our mess
+ */
 void CALLBACK on_destroy_handler(WebSocketPlugin *plugin) {
-    fprintf(stderr, "WS on_destroy_handler()\n");
-    fflush(stderr);
-
     if (trans) {
         apr_thread_exit(trans->responder_thread, APR_SUCCESS);
         apr_pool_destroy(trans->main_pool);

commit a77eb22c27183d23fb08ed40bc75469d8c54b884
Author: Bill Erickson <berick at esilibrary.com>
Date:   Fri Oct 26 15:06:13 2012 -0400

    LP#1268619: Apache websocket translator module
    
    Signed-off-by: Bill Erickson <berick at esilibrary.com>
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>

diff --git a/src/gateway/Makefile.am b/src/gateway/Makefile.am
index 425a824..27e9cc1 100644
--- a/src/gateway/Makefile.am
+++ b/src/gateway/Makefile.am
@@ -15,7 +15,9 @@ if APACHE_MIN_24
 HAVE_APACHE_MIN_24 = -DAPACHE_MIN_24
 endif
 
-EXTRA_DIST = @srcdir@/apachetools.c @srcdir@/apachetools.h @srcdir@/osrf_json_gateway.c @srcdir@/osrf_http_translator.c
+EXTRA_DIST = @srcdir@/apachetools.c @srcdir@/apachetools.h \
+	@srcdir@/osrf_json_gateway.c @srcdir@/osrf_http_translator.c  \
+	@srcdir@/osrf_websocket_translator.c
 
 AM_CFLAGS = -D_LARGEFILE64_SOURCE $(HAVE_APACHE_MIN_24) -Wall -I at abs_top_srcdir@/include/ -I$(LIBXML2_HEADERS) -I$(APACHE2_HEADERS) -I$(APR_HEADERS)
 AM_LDFLAGS = -L$(LIBDIR) -L at top_builddir@/src/libopensrf
@@ -31,9 +33,11 @@ install-exec-local:
 	fi
 	$(APXS2) -c $(DEF_LDLIBS) $(AM_CFLAGS) $(AM_LDFLAGS) @srcdir@/osrf_json_gateway.c apachetools.c apachetools.h libopensrf.so
 	$(APXS2) -c $(DEF_LDLIBS) $(AM_CFLAGS) $(AM_LDFLAGS) @srcdir@/osrf_http_translator.c apachetools.c apachetools.h libopensrf.so
+	$(APXS2) -c $(DEF_LDLIBS) $(AM_CFLAGS) $(AM_LDFLAGS) @srcdir@/osrf_websocket_translator.c apachetools.c apachetools.h libopensrf.so
 	$(MKDIR_P) $(DESTDIR)$(AP_LIBEXECDIR)
 	$(APXS2) -i -S LIBEXECDIR=$(DESTDIR)$(AP_LIBEXECDIR) -a @srcdir@/osrf_json_gateway.la
 	$(APXS2) -i -S LIBEXECDIR=$(DESTDIR)$(AP_LIBEXECDIR) -a @srcdir@/osrf_http_translator.la
+	$(APXS2) -i -S LIBEXECDIR=$(DESTDIR)$(AP_LIBEXECDIR) -a @srcdir@/osrf_websocket_translator.la
 
 clean-local:
-	rm -f @srcdir@/osrf_http_translator.la @srcdir@/osrf_http_translator.lo @srcdir@/osrf_http_translator.slo @srcdir@/osrf_json_gateway.la @srcdir@/osrf_json_gateway.lo @srcdir@/osrf_json_gateway.slo
+	rm -f @srcdir@/osrf_http_translator.la @srcdir@/osrf_http_translator.lo @srcdir@/osrf_http_translator.slo @srcdir@/osrf_json_gateway.la @srcdir@/osrf_json_gateway.lo @srcdir@/osrf_json_gateway.slo  @srcdir@/osrf_websocket_translator.la @srcdir@/osrf_websocket_translator.lo @srcdir@/osrf_websocket_translator.slo
diff --git a/src/gateway/osrf_websocket_translator.c b/src/gateway/osrf_websocket_translator.c
new file mode 100644
index 0000000..fd19f2d
--- /dev/null
+++ b/src/gateway/osrf_websocket_translator.c
@@ -0,0 +1,427 @@
+/* -----------------------------------------------------------------------
+ * Copyright 2012 Equinox Software, Inc.
+ * Bill Erickson <berick at esilibrary.com>
+ *
+ * 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.
+ * -----------------------------------------------------------------------
+ */
+
+/**
+ * Dumb websocket <-> opensrf gateway.  Wrapped opensrf messages are extracted
+ * and relayed to the opensrf network.  Responses are pulled from the opensrf
+ * network and passed back to the client.  No attempt is made to understand
+ * the contents of the messages.
+ *
+ * Messages to/from the websocket client take the following form:
+ * {
+ *   "service"  : "opensrf.foo", // required for new sessions (inbound only)
+ *   "thread"   : "123454321",   // AKA thread. required for follow-up requests; max 64 chars.
+ *   "log_xid"  : "123..32",     // optional log trace ID, max 64 chars;
+ *   "osrf_msg" : {<osrf_msg>}   // required
+ * }
+ *
+ * Each translator operates with two threads.  One thread receives messages
+ * from the websocket client, translates, and relays them to the opensrf 
+ * network. The second thread collects responses from the opensrf network and 
+ * relays them back to the websocket client.
+ *
+ * The main thread reads from socket A (apache) and writes to socket B 
+ * (openesrf), while the responder thread reads from B and writes to A.  The 
+ * apr data structures used are threadsafe.  For now, no thread mutex's are 
+ * used.
+ *
+ * Note that with a "thread", which allows us to identify the opensrf session,
+ * the caller does not need to provide a recipient address.  The "service" is
+ * only required to start a new opensrf session.  After the sesession is 
+ * started, all future communication is based solely on the thread.  
+ *
+ * We use jsonParseRaw and jsonObjectToJSONRaw since this service does not care 
+ * about the contents of the messages.
+ */
+
+/**
+ * TODO:
+ * short-timeout mode for brick detachment where inactivity timeout drops way 
+ * down for graceful disconnects.
+ */
+
+#include "httpd.h"
+#include "http_log.h"
+#include "http_log.h"
+#include "apr_strings.h"
+#include "apr_thread_proc.h"
+#include "apr_hash.h"
+#include "websocket_plugin.h"
+#include "opensrf/osrf_json.h"
+#include "opensrf/transport_client.h"
+#include "opensrf/transport_message.h"
+#include "opensrf/osrf_system.h"                                                
+#include "opensrf/osrfConfig.h"
+
+#define MAX_THREAD_SIZE 64
+#define RECIP_BUF_SIZE 128
+static char recipient_buf[RECIP_BUF_SIZE]; // reusable recipient buffer
+static transport_client *osrf_handle = NULL;
+
+typedef struct _osrfWebsocketTranslator {
+    const WebSocketServer *server;
+    apr_pool_t *main_pool; // standline per-process pool
+    apr_pool_t *session_pool; // child of trans->main_pool; per-session
+    apr_hash_t *session_cache; 
+    apr_thread_t *responder_thread;
+    int client_connected;
+    char* osrf_router;
+    char* osrf_domain;
+} osrfWebsocketTranslator;
+
+static osrfWebsocketTranslator *trans = NULL;
+
+
+/**
+ * Responder thread main body.
+ * Collects responses from the opensrf network and relays them to the 
+ * websocket caller.
+ */
+void* APR_THREAD_FUNC osrf_responder_thread_main(apr_thread_t *thread, void *data) {
+
+    request_rec *r = trans->server->request(trans->server);
+    jsonObject *msg_wrapper;
+    char *msg_string;
+
+    while (1) {
+
+        transport_message *msg = client_recv(osrf_handle, -1);
+        if (!msg) continue; // early exit on interrupt
+        
+        // discard responses received after client disconnect
+        if (!trans->client_connected) {
+            ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, 
+                "WS discarding response for thread=%s, xid=%s", 
+                    msg->thread, msg->osrf_xid);
+            message_free(msg);                                                         
+            continue; 
+        }
+
+        ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, 
+            "WS received opensrf response for thread=%s, xid=%s", 
+                msg->thread, msg->osrf_xid);
+
+        // build the wrapper object
+        msg_wrapper = jsonNewObject(NULL);
+        jsonObjectSetKey(msg_wrapper, "thread", jsonNewObject(msg->thread));
+        jsonObjectSetKey(msg_wrapper, "log_xid", jsonNewObject(msg->osrf_xid));
+        jsonObjectSetKey(msg_wrapper, "osrf_msg", jsonParseRaw(msg->body));
+
+        if (msg->is_error) {
+            ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, 
+                "WS received jabber error message in response to thread=%s and xid=%s", 
+                    msg->thread, msg->osrf_xid);
+            jsonObjectSetKey(msg_wrapper, "transport_error", jsonNewBoolObject(1));
+        }
+
+        msg_string = jsonObjectToJSONRaw(msg_wrapper);
+
+        // deliver the wrapped message json to the websocket client
+        trans->server->send(trans->server, MESSAGE_TYPE_TEXT, 
+            (unsigned char*) msg_string, strlen(msg_string));
+
+        // capture the true message sender
+        // TODO: this will grow to add one entry per client session.  
+        // need a last-touched timeout mechanism to periodically remove old entries
+        if (!apr_hash_get(trans->session_cache, msg->thread, APR_HASH_KEY_STRING)) {
+
+            ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, 
+                "WS caching sender thread=%s, sender=%s", msg->thread, msg->sender);
+
+            apr_hash_set(trans->session_cache, 
+                apr_pstrdup(trans->session_pool, msg->thread),
+                APR_HASH_KEY_STRING, 
+                apr_pstrdup(trans->session_pool, msg->sender));
+        }
+
+        free(msg_string);
+        jsonObjectFree(msg_wrapper);
+        message_free(msg);                                                         
+    }
+
+    return NULL;
+}
+
+/**
+ * Allocate the session cache and create the responder thread
+ */
+int child_init(const WebSocketServer *server) {
+
+    apr_pool_t *pool = NULL;                                                
+    apr_thread_t *thread = NULL;
+    apr_threadattr_t *thread_attr = NULL;
+    request_rec *r = server->request(server);
+        
+    ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, "WS child_init");
+
+    // osrf_handle will already be connected if this is not the first request
+    // served by this process.
+    if ( !(osrf_handle = osrfSystemGetTransportClient()) ) {
+        char* config_file = "/openils/conf/opensrf_core.xml";
+        char* config_ctx = "gateway"; //TODO config
+        if (!osrfSystemBootstrapClientResc(config_file, config_ctx, "websocket")) {   
+            ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r,                              
+                "WS unable to bootstrap OpenSRF client with config %s", config_file); 
+            return 1;
+        }
+
+        osrf_handle = osrfSystemGetTransportClient();
+    }
+
+    // create a standalone pool for our translator data
+    if (apr_pool_create(&pool, NULL) != APR_SUCCESS) {
+        ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "WS Unable to create apr_pool");
+        return 1;
+    }
+
+
+    // allocate our static translator instance
+    trans = (osrfWebsocketTranslator*) 
+        apr_palloc(pool, sizeof(osrfWebsocketTranslator));
+
+    if (trans == NULL) {
+        ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "WS Unable to create translator");
+        return 1;
+    }
+
+    trans->main_pool = pool;
+    trans->server = server;
+    trans->osrf_router = osrfConfigGetValue(NULL, "/router_name");                      
+    trans->osrf_domain = osrfConfigGetValue(NULL, "/domain");
+
+    // Create the responder thread.  Once created, it runs for the lifetime
+    // of this process.
+    if ( (apr_threadattr_create(&thread_attr, trans->main_pool) == APR_SUCCESS) &&
+         (apr_threadattr_detach_set(thread_attr, 0) == APR_SUCCESS) &&
+         (apr_thread_create(&thread, thread_attr, 
+                osrf_responder_thread_main, trans, trans->main_pool) == APR_SUCCESS)) {
+
+        trans->responder_thread = thread;
+        
+    } else {
+        ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, 
+            "WS unable to create responder thread");
+        return 1;
+    }
+
+    return APR_SUCCESS;
+}
+
+/**
+ * Create the per-client translator
+ */
+void* CALLBACK on_connect_handler(const WebSocketServer *server) {
+    request_rec *r = server->request(server);
+    apr_pool_t *pool;
+
+    ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, 
+        "WS connect from %s", r->connection->remote_ip);
+
+    if (!trans) {
+        if (child_init(server) != APR_SUCCESS) {
+            return NULL;
+        }
+    }
+
+    // create a standalone pool for the session cache values, which will be
+    // destroyed on client disconnect.
+    if (apr_pool_create(&pool, trans->main_pool) != APR_SUCCESS) {
+        ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, 
+            "WS Unable to create apr_pool");
+        return NULL;
+    }
+
+    trans->session_pool = pool;
+    trans->session_cache = apr_hash_make(trans->session_pool);
+
+    ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, 
+        "WS created new pool %x", trans->session_pool);
+
+    if (trans->session_cache == NULL) {
+        ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, 
+            "WS unable to create session cache");
+        return NULL;
+    }
+
+    ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, 
+        "WS created new hash %x", trans->session_cache);
+
+    trans->client_connected = 1;
+    return trans;
+}
+
+
+
+/**
+ * Parse opensrf request and relay the request to the opensrf network.
+ */
+static size_t CALLBACK on_message_handler(void *data,
+                const WebSocketServer *server, const int type, 
+                unsigned char *buffer, const size_t buffer_size) {
+
+    request_rec *r = server->request(server);
+
+    jsonObject *msg_wrapper = NULL; // free me
+    const jsonObject *tmp_obj = NULL;
+    const jsonObject *osrf_msg = NULL;
+    const char *service = NULL;
+    const char *thread = NULL;
+    const char *log_xid = NULL;
+    char *msg_body = NULL;
+    char *recipient = NULL;
+
+    if (buffer_size <= 0) return OK;
+
+    ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, 
+        "WS received message size=%d", buffer_size);
+
+    // buffer may not be \0-terminated, which jsonParse requires
+    char buf[buffer_size + 1];
+    memcpy(buf, buffer, buffer_size);
+    buf[buffer_size] = '\0';
+
+    msg_wrapper = jsonParseRaw(buf);
+
+    if (msg_wrapper == NULL) {
+        ap_log_rerror(APLOG_MARK, 
+            APLOG_NOTICE, 0, r, "WS Invalid JSON: %s", buf);
+        return HTTP_BAD_REQUEST;
+    }
+
+    osrf_msg = jsonObjectGetKeyConst(msg_wrapper, "osrf_msg");
+
+    if (tmp_obj = jsonObjectGetKeyConst(msg_wrapper, "service")) 
+        service = jsonObjectGetString(tmp_obj);
+
+    if (tmp_obj = jsonObjectGetKeyConst(msg_wrapper, "thread")) 
+        thread = jsonObjectGetString(tmp_obj);
+
+    if (tmp_obj = jsonObjectGetKeyConst(msg_wrapper, "log_xid")) 
+        log_xid = jsonObjectGetString(tmp_obj);
+
+    if (log_xid) {
+        // use the caller-provide log trace id
+        if (strlen(log_xid) > MAX_THREAD_SIZE) {
+            ap_log_rerror(APLOG_MARK, APLOG_NOTICE, 
+                0, r, "WS log_xid exceeds max length");
+            return HTTP_BAD_REQUEST;
+        }
+        osrfLogSetXid(log_xid); // TODO: make with with non-client
+    } else {
+        // generate a new log trace id for this relay
+        osrfLogMkXid();
+    }
+
+    if (thread) {
+
+        if (strlen(thread) > MAX_THREAD_SIZE) {
+            ap_log_rerror(APLOG_MARK, APLOG_NOTICE, 
+                0, r, "WS thread exceeds max length");
+            return HTTP_BAD_REQUEST;
+        }
+
+        // since clients can provide their own threads at session start time,
+        // the presence of a thread does not guarantee a cached recipient
+        recipient = (char*) apr_hash_get(
+            trans->session_cache, thread, APR_HASH_KEY_STRING);
+
+        if (recipient) {
+            ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, 
+                "WS found cached recipient %s", recipient);
+        }
+    }
+
+    if (!recipient) {
+
+        if (service) {
+            int size = snprintf(recipient_buf, RECIP_BUF_SIZE - 1,
+                "%s@%s/%s", trans->osrf_router, trans->osrf_domain, service);                                    
+            recipient_buf[size] = '\0';                                          
+            recipient = recipient_buf;
+
+        } else {
+            ap_log_rerror(APLOG_MARK, APLOG_NOTICE, 
+                0, r, "WS Unable to determine recipient");
+            return HTTP_BAD_REQUEST;
+        }
+    }
+
+    // TODO: activity log entry? -- requires message analysis
+    ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, 
+        "WS relaying message thread=%s, xid=%s, recipient=%s", 
+            thread, osrfLogGetXid(), recipient);
+
+    msg_body = jsonObjectToJSONRaw(osrf_msg);
+
+    transport_message *tmsg = message_init(
+        msg_body, NULL, thread, recipient, NULL);
+
+    message_set_osrf_xid(tmsg, osrfLogGetXid());                                
+    client_send_message(osrf_handle, tmsg);                                   
+    osrfLogClearXid();
+
+    message_free(tmsg);                                                         
+    free(msg_wrapper);
+    free(msg_body);
+
+    return OK;
+}
+
+
+/**
+ * Release all memory allocated from the translator pool and kill the pool.
+ */
+void CALLBACK on_disconnect_handler(
+    void *data, const WebSocketServer *server) {
+
+    osrfWebsocketTranslator *trans = (osrfWebsocketTranslator*) data;
+    trans->client_connected = 0;
+
+    apr_hash_clear(trans->session_cache);
+    apr_pool_destroy(trans->session_pool);
+    trans->session_pool = NULL;
+    trans->session_cache = NULL;
+
+    request_rec *r = server->request(server);
+    ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, 
+        "WS disconnect from %s", r->connection->remote_ip);
+}
+
+void CALLBACK on_destroy_handler(WebSocketPlugin *plugin) {
+    fprintf(stderr, "WS on_destroy_handler()\n");
+    fflush(stderr);
+
+    if (trans) {
+        apr_thread_exit(trans->responder_thread, APR_SUCCESS);
+        apr_pool_destroy(trans->main_pool);
+    }
+
+    trans = NULL;
+}
+
+static WebSocketPlugin osrf_websocket_plugin = {
+    sizeof(WebSocketPlugin),
+    WEBSOCKET_PLUGIN_VERSION_0,
+    on_destroy_handler,
+    on_connect_handler,
+    on_message_handler,
+    on_disconnect_handler
+};
+
+extern EXPORT WebSocketPlugin * CALLBACK osrf_websocket_init() {
+    return &osrf_websocket_plugin;
+}
+

commit 1dafbe7512f086a58212fcc66c07e348647f31ad
Author: Bill Erickson <berick at esilibrary.com>
Date:   Thu Sep 20 15:55:04 2012 -0400

    LP#1268619: OpenSRF JS websockets plugin
    
    Adds an opensrf_ws.js handler.  Requries some small modifications to
    opensrf.js.  Load opensrf_ws.js in DojoSRF.
    
    For now, to use, add to script:
    
    OpenSRF.Session.transport = OSRF_TRANSPORT_TYPE_WS;
    
    Signed-off-by: Bill Erickson <berick at esilibrary.com>
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>

diff --git a/src/javascript/DojoSRF.js b/src/javascript/DojoSRF.js
index 42578f8..7e3e5c8 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 c0e454c..408f0af 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,10 @@ 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;
+if (true || typeof WebSocket == 'undefined')
+    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 +221,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, args);
         case OSRF_TRANSPORT_TYPE_XHR:
             return this.send_xhr(osrf_msg, args);
         case OSRF_TRANSPORT_TYPE_XMPP:
@@ -231,8 +237,22 @@ OpenSRF.Session.prototype.send_xhr = function(osrf_msg, args) {
     new OpenSRF.XHRequest(osrf_msg, args).send();
 };
 
+OpenSRF.Session.prototype.send_ws = function(osrf_msg, args) {
+    args.session = this;
+    if (this.websocket) {
+        this.websocket.args = args; // callbacks
+        this.websocket.send(osrf_msg);
+    } else {
+        this.websocket = new OpenSRF.WSRequest(
+            this, args, function(wsreq) {
+                wsreq.send(osrf_msg);
+            }
+        );
+    }
+};
+
 OpenSRF.Session.prototype.send_xmpp = function(osrf_msg, args) {
-    alert('xmpp transport not yet implemented');
+    alert('xmpp transport not implemented');
 };
 
 
@@ -254,15 +274,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 +296,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;
+    }
 };
 
 
@@ -397,9 +434,10 @@ function log(msg) {
     }
 }
 
-OpenSRF.Stack.push = function(net_msg, callbacks) {
-    var ses = OpenSRF.Session.find_session(net_msg.thread); 
-    if(!ses) return;
+// ses may be passed to us by the network handler
+OpenSRF.Stack.push = function(net_msg, callbacks, ses) {
+    if (!ses) ses = OpenSRF.Session.find_session(net_msg.thread); 
+    if (!ses) return;
     ses.remote_id = net_msg.from;
     osrf_msgs = [];
 
diff --git a/src/javascript/opensrf_ws.js b/src/javascript/opensrf_ws.js
new file mode 100644
index 0000000..d522834
--- /dev/null
+++ b/src/javascript/opensrf_ws.js
@@ -0,0 +1,104 @@
+/* -----------------------------------------------------------------------
+ * Copyright (C) 2012  Equinox Software, Inc.
+ * Bill Erickson <berick at esilibrary.com>
+ *  
+ * 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.
+ * ----------------------------------------------------------------------- */
+
+var WS_PATH = '/osrf-websocket';
+
+/**
+ * onopen is required. no data can be sent 
+ * until the async connection dance completes.
+ */
+OpenSRF.WSRequest = function(session, args, onopen) {
+    this.session = session;
+    this.args = args;
+
+    var proto = location.protocol == 'https' ? 'wss' : 'ws';
+
+    var path = proto + '://' + location.host + 
+        WS_PATH + '?service=' + this.session.service;
+
+    try {
+        this.ws = new WebSocket(path);
+    } catch(e) {
+        throw new Error("WebSocket() not supported in this browser " + e);
+    }
+
+    var self = this;
+
+    this.ws.onopen = function(evt) {
+        onopen(self);
+    }
+
+    this.ws.onmessage = function(evt) {
+        self.core_handler(evt.data);
+    }
+
+    this.ws.onerror = function(evt) {
+        self.transport_error_handler(evt.data);
+    }
+
+    this.ws.onclose = function(evt) {
+    }
+};
+
+OpenSRF.WSRequest.prototype.send = function(message) {
+    //console.log('sending: ' + js2JSON([message.serialize()]));
+    this.last_message = message;
+    this.ws.send(js2JSON([message.serialize()]));
+    return this;
+};
+
+OpenSRF.WSRequest.prototype.close = function() {
+    try { this.ws.close(); } catch(e) {}
+}
+
+OpenSRF.WSRequest.prototype.core_handler = function(json) {
+    //console.log('received: ' + json);
+
+    OpenSRF.Stack.push(
+        new OpenSRF.NetMessage(null, null, '', json),
+        {
+            onresponse : this.args.onresponse,
+            oncomplete : this.args.oncomplete,
+            onerror : this.args.onerror,
+            onmethoderror : this.method_error_handler()
+        },
+        this.args.session
+    );
+};
+
+
+OpenSRF.WSRequest.prototype.method_error_handler = function() {
+    var self = this;
+    return function(req, status, status_text) {
+        if(self.args.onmethoderror) 
+            self.args.onmethoderror(req, status, status_text);
+
+        if(self.args.onerror)  {
+            self.args.onerror(
+                self.last_message, self.session.service, '');
+        }
+    };
+};
+
+OpenSRF.WSRequest.prototype.transport_error_handler = function(msg) {
+    if(this.args.ontransporterror) {
+        this.args.ontransporterror(msg);
+    }
+    if(this.args.onerror) {
+        this.args.onerror(msg, this.session.service, '');
+    }
+};
+
+

-----------------------------------------------------------------------

Summary of changes:
 README.websockets                        |   53 ++
 examples/apache2/websockets/apache2.conf |   73 +++
 include/opensrf/log.h                    |    4 +-
 src/Makefile.am                          |    2 +-
 src/gateway/Makefile.am                  |    8 +-
 src/gateway/osrf_websocket_translator.c  |  964 ++++++++++++++++++++++++++++++
 src/gateway/websocket_plugin.h           |  130 ++++
 src/javascript/DojoSRF.js                |    1 +
 src/javascript/opensrf.js                |  265 +++++++--
 src/javascript/opensrf_ws.js             |  104 ++++
 src/javascript/opensrf_ws_shared.js      |  248 ++++++++
 src/libopensrf/log.c                     |    4 +-
 src/libopensrf/osrf_message.c            |    5 +-
 13 files changed, 1805 insertions(+), 56 deletions(-)
 create mode 100644 README.websockets
 create mode 100644 examples/apache2/websockets/apache2.conf
 create mode 100644 src/gateway/osrf_websocket_translator.c
 create mode 100644 src/gateway/websocket_plugin.h
 create mode 100644 src/javascript/opensrf_ws.js
 create mode 100644 src/javascript/opensrf_ws_shared.js


hooks/post-receive
-- 
OpenSRF


More information about the opensrf-commits mailing list