LP#1709710: Make chunk sizing smart about XML quoting
[opensrf-equinox.git] / src / libopensrf / osrf_app_session.c
index 2662822..28242e7 100644 (file)
-#include <opensrf/osrf_app_session.h>
-#include <time.h>
+/**
+       @file osrf_app_session.c
+       @brief Implementation of osrfAppSession.
+*/
 
-/** Send the given message */
-static int _osrf_app_session_send( osrfAppSession*, osrf_message* msg );
+#include <time.h>
+#include "opensrf/osrf_app_session.h"
+#include "opensrf/osrf_stack.h"
+
+static char* current_ingress = NULL;
+
+struct osrf_app_request_struct {
+       /** The controlling session. */
+       struct osrf_app_session_struct* session;
+
+       /** Request id.  It is the same as the thread_trace of the REQUEST message
+               for which it was created.
+       */
+       int request_id;
+       /** True if we have received a 'request complete' message from our request. */
+       int complete;
+       /** The original REQUEST message payload. */
+       osrfMessage* payload;
+       /** Linked list of responses to the request. */
+       osrfMessage* result;
+
+    /** Buffer used to collect partial response messages */
+    growing_buffer* part_response_buffer;
+
+       /** Boolean; if true, then a call that is waiting on a response will reset the
+       timeout and set this variable back to false. */
+       int reset_timeout;
+       /** Linkage pointers for a linked list.  We maintain a hash table of pending requests,
+           and each slot of the hash table is a doubly linked list. */
+       osrfAppRequest* next;
+       osrfAppRequest* prev;
+};
+
+static inline unsigned int request_id_hash( int req_id );
+static osrfAppRequest* find_app_request( const osrfAppSession* session, int req_id );
+static void add_app_request( osrfAppSession* session, osrfAppRequest* req );
+
+/* Send the given message */
+static int _osrf_app_session_send( osrfAppSession*, osrfMessage* msg );
 
 static int osrfAppSessionMakeLocaleRequest(
                osrfAppSession* session, const jsonObject* params, const char* method_name,
-               int protocol, string_array* param_strings, char* locale );
+               int protocol, osrfStringArray* param_strings, char* locale );
 
-/* the global app_session cache */
-osrfHash* osrfAppSessionCache = NULL;
+/** @brief The global session cache.
+
+       Key: session_id.  Data: osrfAppSession.
+*/
+static osrfHash* osrfAppSessionCache = NULL;
 
-// --------------------------------------------------------------------------
 // --------------------------------------------------------------------------
 // Request API
 // --------------------------------------------------------------------------
 
-/** Allocates and initializes a new app_request object */
+/**
+       @brief Create a new osrfAppRequest.
+       @param session Pointer to the osrfAppSession that will own the new osrfAppRequest.
+       @param msg Pointer to the osrfMessage representing the request.
+       @return Pointer to the new osrfAppRequest.
+
+       The calling code is responsible for freeing the osrfAppRequest by calling
+       _osrf_app_request_free().
+*/
 static osrfAppRequest* _osrf_app_request_init(
-               osrfAppSession* session, osrf_message* msg ) {
+               osrfAppSession* session, osrfMessage* msg ) {
 
-       osrfAppRequest* req =
-               (osrfAppRequest*) safe_malloc(sizeof(osrfAppRequest));
+       osrfAppRequest* req = safe_malloc(sizeof(osrfAppRequest));
 
-       req->session            = session;
-       req->request_id = msg->thread_trace;
-       req->complete           = 0;
-       req->payload            = msg;
-       req->result                     = NULL;
+       req->session        = session;
+       req->request_id     = msg->thread_trace;
+       req->complete       = 0;
+       req->payload        = msg;
+       req->result         = NULL;
        req->reset_timeout  = 0;
+       req->next           = NULL;
+       req->prev           = NULL;
+       req->part_response_buffer = NULL;
 
        return req;
-
 }
 
 
-void osrfAppSessionCleanup() {
-       osrfHashFree(osrfAppSessionCache);      
-}
+/**
+       @brief Free an osrfAppRequest and everything it owns.
+       @param req Pointer to an osrfAppRequest.
+*/
+static void _osrf_app_request_free( osrfAppRequest * req ) {
+       if( req ) {
+               if( req->payload )
+                       osrfMessageFree( req->payload );
+
+               /* Free the messages in the result queue */
+               osrfMessage* next_msg;
+               while( req->result ) {
+                       next_msg = req->result->next;
+                       osrfMessageFree( req->result );
+                       req->result = next_msg;
+               }
+
+        if (req->part_response_buffer)
+            buffer_free(req->part_response_buffer);
 
-/** Frees memory used by an app_request object */
-static void _osrf_app_request_free( void * req ){
-       if( req == NULL ) return;
-       osrfAppRequest* r = (osrfAppRequest*) req;
-       if( r->payload ) osrf_message_free( r->payload );
-       free( r );
+               free( req );
+       }
 }
 
-/** Pushes the given message onto the list of 'responses' to this request */
-static void _osrf_app_request_push_queue( osrfAppRequest* req, osrf_message* result ){
-       if(req == NULL || result == NULL) return;
-       osrfLogDebug( OSRF_LOG_MARK,  "App Session pushing request [%d] onto request queue", result->thread_trace );
+/**
+       @brief Append a new message to the list of responses to a request.
+       @param req Pointer to the osrfAppRequest for the original REQUEST message.
+       @param result Pointer to an osrfMessage received in response to the request.
+
+       For each osrfAppRequest we maintain a linked list of response messages, and traverse
+       it to find the end.
+*/
+static void _osrf_app_request_push_queue( osrfAppRequest* req, osrfMessage* result ){
+       if(req == NULL || result == NULL)
+               return;
+
+    if (result->status_code == OSRF_STATUS_PARTIAL) {
+        osrfLogDebug(OSRF_LOG_MARK, "received partial message response");
+
+        if (!req->part_response_buffer) {
+            // assume the max_chunk_size of the server matches ours for
+            // buffer initialization,  since the setting will usually be 
+            // a site-wide value.
+               req->part_response_buffer = buffer_init(OSRF_MSG_CHUNK_SIZE + 1);
+        }
+
+        const char* partial = jsonObjectGetString(result->_result_content);
+
+        if (partial != NULL) {
+            osrfLogDebug(OSRF_LOG_MARK, 
+                "adding %d bytes to response buffer", strlen(partial));
+        
+            // add the partial contents of the message to the buffer
+            buffer_add(req->part_response_buffer, partial);
+        }
+
+        // all done.  req and result are freed by the caller
+        return;
+
+    } else if (result->status_code == OSRF_STATUS_NOCONTENT) {
+        if (req->part_response_buffer && req->part_response_buffer->n_used) {
+
+            // part_response_buffer contains a stitched-together JSON string
+            osrfLogDebug(OSRF_LOG_MARK, 
+                "partial response complete, parsing %d bytes", 
+                req->part_response_buffer->n_used);
+
+            // coerce the partial-complete response into a standard RESULT.
+            osrf_message_set_status_info(result, NULL, "OK", OSRF_STATUS_OK);
+
+            // use the stitched-together JSON string as the result conten
+            osrf_message_set_result_content(
+                result, req->part_response_buffer->buf);
+
+            // free string, keep the buffer
+            buffer_reset(req->part_response_buffer); 
+
+        } else {
+            osrfLogDebug(OSRF_LOG_MARK, 
+                "Received OSRF_STATUS_NOCONTENT with no preceeding content");
+            return;
+        }
+    }
+
+       osrfLogDebug( OSRF_LOG_MARK, "App Session pushing request [%d] onto request queue",
+                       result->thread_trace );
+
        if(req->result == NULL) {
-               req->result = result;
+               req->result = result;   // Add the first node
 
        } else {
-               
-               osrf_message* ptr = req->result;
-               osrf_message* ptr2 = req->result->next;
+
+               // Find the last node in the list, and append the new node to it
+               osrfMessage* ptr = req->result;
+               osrfMessage* ptr2 = req->result->next;
                while( ptr2 ) {
                        ptr = ptr2;
                        ptr2 = ptr2->next;
@@ -66,61 +188,166 @@ static void _osrf_app_request_push_queue( osrfAppRequest* req, osrf_message* res
        }
 }
 
-/** Removes this app_request from our session request set */
-void osrf_app_session_request_finish( 
-               osrfAppSession* session, int req_id ){
+/**
+       @brief Remove an osrfAppRequest (identified by request_id) from an osrfAppSession.
+       @param session Pointer to the osrfAppSession that owns the osrfAppRequest.
+       @param req_id request_id of the osrfAppRequest to be removed.
+*/
+void osrf_app_session_request_finish( osrfAppSession* session, int req_id ) {
+
+       if( session ) {
+               // Search the hash table for the request in question
+               unsigned int index = request_id_hash( req_id );
+               osrfAppRequest* old_req = session->request_hash[ index ];
+               while( old_req ) {
+                       if( old_req->request_id == req_id )
+                               break;
+                       else
+                               old_req = old_req->next;
+               }
 
-       if(session == NULL) return;
-       osrfAppRequest* req = OSRF_LIST_GET_INDEX( session->request_queue, req_id );
-       if(req == NULL) return;
-       osrfListRemove( req->session->request_queue, req->request_id );
+               if( old_req ) {
+                       // Remove the request from the doubly linked list
+                       if( old_req->prev )
+                               old_req->prev->next = old_req->next;
+                       else
+                               session->request_hash[ index ] = old_req->next;
+
+                       if( old_req->next )
+                               old_req->next->prev = old_req->prev;
+
+                       _osrf_app_request_free( old_req );
+               }
+       }
+}
+
+/**
+       @brief Derive a hash key from a request id.
+       @param req_id The request id.
+       @return The corresponding hash key; an index into request_hash[].
+
+       If OSRF_REQUEST_HASH_SIZE is a power of two, then this calculation should
+       reduce to a binary AND.
+*/
+static inline unsigned int request_id_hash( int req_id ) {
+       return ((unsigned int) req_id ) % OSRF_REQUEST_HASH_SIZE;
+}
+
+/**
+       @brief Search for an osrfAppRequest in the hash table, given a request id.
+       @param session Pointer to the relevant osrfAppSession.
+       @param req_id The request_id of the osrfAppRequest being sought.
+       @return A pointer to the osrfAppRequest if found, or NULL if not.
+*/
+static osrfAppRequest* find_app_request( const osrfAppSession* session, int req_id ) {
+
+       osrfAppRequest* req = session->request_hash[ request_id_hash( req_id) ];
+       while( req ) {
+               if( req->request_id == req_id )
+                       break;
+               else
+                       req = req->next;
+       }
+
+       return req;
 }
 
+/**
+       @brief Add an osrfAppRequest to the hash table of a given osrfAppSession.
+       @param session Pointer to the session to which the request belongs.
+       @param req Pointer to the osrfAppRequest to be stored.
+
+       Find the right spot in the hash table; then add the request to the linked list at that
+       spot.  We just add it to the head of the list, without trying to maintain any particular
+       ordering.
+*/
+static void add_app_request( osrfAppSession* session, osrfAppRequest* req ) {
+       if( session && req ) {
+               unsigned int index = request_id_hash( req->request_id );
+               req->next = session->request_hash[ index ];
+               req->prev = NULL;
+               session->request_hash[ index ] = req;
+       }
+}
+
+/**
+       @brief Request a reset of the timeout period for a request.
+       @param session Pointer to the relevant osrfAppSession.
+       @param req_id Request ID of the request whose timeout is to be reset.
+
+       This happens when a client receives a STATUS message with a status code
+       OSRF_STATUS_CONTINUE; in effect the server is asking for more time.
 
+       The request to be reset is identified by the combination of session and request id.
+*/
 void osrf_app_session_request_reset_timeout( osrfAppSession* session, int req_id ) {
-       if(session == NULL) return;
+       if(session == NULL)
+               return;
        osrfLogDebug( OSRF_LOG_MARK, "Resetting request timeout %d", req_id );
-       osrfAppRequest* req = OSRF_LIST_GET_INDEX( session->request_queue, req_id );
-       if(req == NULL) return;
-       req->reset_timeout = 1;
+       osrfAppRequest* req = find_app_request( session, req_id );
+       if( req )
+               req->reset_timeout = 1;
 }
 
-/** Checks the receive queue for messages.  If any are found, the first
-  * is popped off and returned.  Otherwise, this method will wait at most timeout 
-  * seconds for a message to appear in the receive queue.  Once it arrives it is returned.
-  * If no messages arrive in the timeout provided, null is returned.
-  */
-static osrf_message* _osrf_app_request_recv( osrfAppRequest* req, int timeout ) {
+/**
+       @brief Fetch the next response message to a given previous request, subject to a timeout.
+       @param req Pointer to the osrfAppRequest representing the request.
+       @param timeout Maxmimum time to wait, in seconds.
+
+       @return Pointer to the next osrfMessage for this request, if one is available, or if it
+       becomes available before the end of the timeout; otherwise NULL;
+
+       If there is already a message available in the input queue for this request, dequeue and
+       return it immediately.  Otherwise wait up to timeout seconds until you either get an
+       input message for the specified request, run out of time, or encounter an error.
+
+       If the only message we receive for this request is a STATUS message with a status code
+       OSRF_STATUS_COMPLETE, then return NULL.  That means that the server has nothing further
+       to send in response to this request.
+
+       You may also receive other messages for other requests, and other sessions.  These other
+       messages will be wholly or partially processed behind the scenes while you wait for the
+       one you want.
+*/
+static osrfMessage* _osrf_app_request_recv( osrfAppRequest* req, int timeout ) {
 
        if(req == NULL) return NULL;
 
        if( req->result != NULL ) {
-               /* pop off the first message in the list */
-               osrf_message* tmp_msg = req->result;
+               /* Dequeue the next message in the list */
+               osrfMessage* tmp_msg = req->result;
                req->result = req->result->next;
                return tmp_msg;
        }
 
-       time_t start = time(NULL);      
+       time_t start = time(NULL);
        time_t remaining = (time_t) timeout;
 
+       // Wait repeatedly for input messages until you either receive one for the request
+       // you're interested in, run out of time, or encounter an error.
+       // Wait repeatedly because you may also receive messages for other requests, or for
+       // other sessions, and process them behind the scenes. These are not the messages
+       // you're looking for.
        while( remaining >= 0 ) {
                /* tell the session to wait for stuff */
-               osrfLogDebug( OSRF_LOG_MARK,  "In app_request receive with remaining time [%d]", (int) remaining );
+               osrfLogDebug( OSRF_LOG_MARK,  "In app_request receive with remaining time [%d]",
+                               (int) remaining );
+
 
                osrf_app_session_queue_wait( req->session, 0, NULL );
+               if(req->session->transport_error) {
+                       osrfLogError(OSRF_LOG_MARK, "Transport error in recv()");
+                       return NULL;
+               }
+
+               if( req->result != NULL ) { /* if we received any results for this request */
+                       /* dequeue the first message in the list */
+                       osrfLogDebug( OSRF_LOG_MARK, "app_request_recv received a message, returning it" );
+                       osrfMessage* ret_msg = req->result;
+                       req->result = ret_msg->next;
+                       if (ret_msg->sender_locale)
+                               osrf_app_session_set_locale(req->session, ret_msg->sender_locale);
 
-               if( req->result != NULL ) { /* if we received anything */
-                       /* pop off the first message in the list */
-                       osrfLogDebug( OSRF_LOG_MARK,  "app_request_recv received a message, returning it");
-                       osrf_message* ret_msg = req->result;
-                       osrf_message* tmp_msg = ret_msg->next;
-                       req->result = tmp_msg;
-                       if (ret_msg->sender_locale) {
-                               if (req->session->session_locale)
-                                       free(req->session->session_locale);
-                               req->session->session_locale = strdup(ret_msg->sender_locale);
-                       }
                        return ret_msg;
                }
 
@@ -129,23 +356,31 @@ static osrf_message* _osrf_app_request_recv( osrfAppRequest* req, int timeout )
 
                osrf_app_session_queue_wait( req->session, (int) remaining, NULL );
 
-               if( req->result != NULL ) { /* if we received anything */
-                       /* pop off the first message in the list */
+               if(req->session->transport_error) {
+                       osrfLogError(OSRF_LOG_MARK, "Transport error in recv()");
+                       return NULL;
+               }
+
+               if( req->result != NULL ) { /* if we received any results for this request */
+                       /* dequeue the first message in the list */
                        osrfLogDebug( OSRF_LOG_MARK,  "app_request_recv received a message, returning it");
-                       osrf_message* ret_msg = req->result;
-                       osrf_message* tmp_msg = ret_msg->next;
-                       req->result = tmp_msg;
-                       if (ret_msg->sender_locale) {
-                               if (req->session->session_locale)
-                                       free(req->session->session_locale);
-                               req->session->session_locale = strdup(ret_msg->sender_locale);
-                       }
+                       osrfMessage* ret_msg = req->result;
+                       req->result = ret_msg->next;
+                       if (ret_msg->sender_locale)
+                               osrf_app_session_set_locale(req->session, ret_msg->sender_locale);
+
                        return ret_msg;
                }
+
                if( req->complete )
                        return NULL;
 
+               // Determine how much time is left
                if(req->reset_timeout) {
+                       // We got a reprieve.  This happens when a client receives a STATUS message
+                       // with a status code OSRF_STATUS_CONTINUE.  We restart the timer from the
+                       // beginning -- but only once.  We reset reset_timeout to zero. so that a
+                       // second attempted reprieve will allow, at most, only one more second.
                        remaining = (time_t) timeout;
                        req->reset_timeout = 0;
                        osrfLogDebug( OSRF_LOG_MARK, "Received a timeout reset");
@@ -154,69 +389,147 @@ static osrf_message* _osrf_app_request_recv( osrfAppRequest* req, int timeout )
                }
        }
 
-       osrfLogInfo( OSRF_LOG_MARK, "Returning NULL from app_request_recv after timeout");
-       return NULL;
-}
+       // Timeout exhausted; no messages for the request in question
+       char* paramString = jsonObjectToJSON(req->payload->_params);
+       osrfLogInfo( OSRF_LOG_MARK, "Returning NULL from app_request_recv after timeout: %s %s",
+               req->payload->method_name, paramString);
+       free(paramString);
 
-/** Resend this requests original request message */
-static int _osrf_app_request_resend( osrfAppRequest* req ) {
-       if(req == NULL) return 0;
-       if(!req->complete) {
-               osrfLogDebug( OSRF_LOG_MARK,  "Resending request [%d]", req->request_id );
-               return _osrf_app_session_send( req->session, req->payload );
-       }
-       return 1;
+       return NULL;
 }
 
-
-
-// --------------------------------------------------------------------------
 // --------------------------------------------------------------------------
 // Session API
 // --------------------------------------------------------------------------
 
-/** returns a session from the global session hash */
+/**
+       @brief Install a copy of a locale string in a specified session.
+       @param session Pointer to the osrfAppSession in which the locale is to be installed.
+       @param locale The locale string to be copied and installed.
+       @return A pointer to the installed copy of the locale string.
+*/
 char* osrf_app_session_set_locale( osrfAppSession* session, const char* locale ) {
        if (!session || !locale)
                return NULL;
 
-       if(session->session_locale)
-               free(session->session_locale);
+       if(session->session_locale) {
+               if( strlen(session->session_locale) >= strlen(locale) ) {
+                       /* There's room available; just copy */
+                       strcpy(session->session_locale, locale);
+               } else {
+                       free(session->session_locale);
+                       session->session_locale = strdup( locale );
+               }
+       } else {
+               session->session_locale = strdup( locale );
+       }
 
-       session->session_locale = strdup( locale );
        return session->session_locale;
 }
 
-/** returns a session from the global session hash */
+/**
+       @brief Install a copy of a TZ string in a specified session.
+       @param session Pointer to the osrfAppSession in which the TZ is to be installed.
+       @param TZ The TZ string to be copied and installed.
+       @return A pointer to the installed copy of the TZ string.
+*/
+char* osrf_app_session_set_tz( osrfAppSession* session, const char* tz ) {
+       if (!session || !tz)
+               return NULL;
+
+       if(session->session_tz) {
+               if( strlen(session->session_tz) >= strlen(tz) ) {
+                       /* There's room available; just copy */
+                       strcpy(session->session_tz, tz);
+               } else {
+                       free(session->session_tz);
+                       session->session_tz = strdup( tz );
+               }
+       } else {
+               session->session_tz = strdup( tz );
+       }
+
+       return session->session_tz;
+}
+
+/**
+       @brief Install a copy of a ingress string as the new default.
+       @param session Pointer to the new strdup'ed default_ingress
+       @param ingress The ingress string to be copied and installed.
+*/
+char* osrfAppSessionSetIngress(const char* ingress) {
+       if (!ingress) return NULL;
+    if(current_ingress) 
+        free(current_ingress);
+    return current_ingress = strdup(ingress);
+}
+
+/**
+    @brief Returns the current ingress value
+    @return A pointer to the installed copy of the ingress string 
+*/
+const char* osrfAppSessionGetIngress() {
+    return current_ingress;
+}
+
+/**
+       @brief Find the osrfAppSession for a given session id.
+       @param session_id The session id to look for.
+       @return Pointer to the corresponding osrfAppSession if found, or NULL if not.
+
+       Search the global session cache for the specified session id.
+*/
 osrfAppSession* osrf_app_session_find_session( const char* session_id ) {
-       if(session_id) return osrfHashGet(osrfAppSessionCache, session_id);
+       if(session_id)
+               return osrfHashGet( osrfAppSessionCache, session_id );
        return NULL;
 }
 
 
-/** adds a session to the global session cache */
+/**
+       @brief Add a session to the global session cache, keyed by session id.
+       @param session Pointer to the osrfAppSession to be added.
+
+       If a cache doesn't exist yet, create one.  It's an osrfHash using session ids for the
+       key and osrfAppSessions for the data.
+*/
 static void _osrf_app_session_push_session( osrfAppSession* session ) {
-       if(!session) return;
-       if( osrfAppSessionCache == NULL ) osrfAppSessionCache = osrfNewHash();
-       if( osrfHashGet( osrfAppSessionCache, session->session_id ) ) return;
-       osrfHashSet( osrfAppSessionCache, session, session->session_id );
+       if( session ) {
+               if( osrfAppSessionCache == NULL )
+                       osrfAppSessionCache = osrfNewHash();
+               if( osrfHashGet( osrfAppSessionCache, session->session_id ) )
+                       return;   // A session with this id is already in the cache.  Shouldn't happen.
+               osrfHashSet( osrfAppSessionCache, session, session->session_id );
+       }
 }
 
-/** Allocates and initializes a new app_session */
+/**
+       @brief Create an osrfAppSession for a client.
+       @param remote_service Name of the service to which to connect
+       @return Pointer to the new osrfAppSession if successful, or NULL upon error.
 
-osrfAppSession* osrfAppSessionClientInit( const char* remote_service ) {
-       return osrf_app_client_session_init( remote_service );
-}
+       Allocate memory for an osrfAppSession, and initialize it as follows:
 
-osrfAppSession* osrf_app_client_session_init( const char* remote_service ) {
+       - For talking with Jabber, grab an existing transport_client.  It must have been
+       already set up by a prior call to osrfSystemBootstrapClientResc().
+       - Build a Jabber ID for addressing the service.
+       - Build a session ID based on a fine-grained timestamp and a process ID.  This ID is
+       intended to be unique across the system, but uniqueness is not strictly guaranteed.
+       - Initialize various other bits and scraps.
+       - Add the session to the global session cache.
+
+       Do @em not connect to the service at this point.
+*/
+osrfAppSession* osrfAppSessionClientInit( const char* remote_service ) {
 
        if (!remote_service) {
-               osrfLogWarning( OSRF_LOG_MARK, "No remote service specified in osrf_app_client_session_init");
+               osrfLogWarning( OSRF_LOG_MARK, "No remote service specified in osrfAppSessionClientInit");
                return NULL;
        }
 
        osrfAppSession* session = safe_malloc(sizeof(osrfAppSession));
 
+       // Grab an existing transport_client for talking with Jabber
        session->transport_handle = osrfSystemGetTransportClient();
        if( session->transport_handle == NULL ) {
                osrfLogWarning( OSRF_LOG_MARK, "No transport client for service 'client'");
@@ -224,10 +537,11 @@ osrfAppSession* osrf_app_client_session_init( const char* remote_service ) {
                return NULL;
        }
 
+       // Get a list of domain names from the config settings;
+       // ignore all but the first one in the list.
        osrfStringArray* arr = osrfNewStringArray(8);
-       osrfConfigGetValueList(NULL, arr, "/domains/domain");
-       char* domain = osrfStringArrayGetString(arr, 0);
-
+       osrfConfigGetValueList(NULL, arr, "/domain");
+       const char* domain = osrfStringArrayGetString(arr, 0);
        if (!domain) {
                osrfLogWarning( OSRF_LOG_MARK, "No domains specified in the OpenSRF config file");
                free( session );
@@ -235,6 +549,7 @@ osrfAppSession* osrf_app_client_session_init( const char* remote_service ) {
                return NULL;
        }
 
+       // Get a router name from the config settings.
        char* router_name = osrfConfigGetValue(NULL, "/router_name");
        if (!router_name) {
                osrfLogWarning( OSRF_LOG_MARK, "No router name specified in the OpenSRF config file");
@@ -246,12 +561,13 @@ osrfAppSession* osrf_app_client_session_init( const char* remote_service ) {
        char target_buf[512];
        target_buf[ 0 ] = '\0';
 
+       // Using the router name, domain, and service name,
+       // build a Jabber ID for addressing the service.
        int len = snprintf( target_buf, sizeof(target_buf), "%s@%s/%s",
                        router_name ? router_name : "(null)",
                        domain ? domain : "(null)",
                        remote_service ? remote_service : "(null)" );
        osrfStringArrayFree(arr);
-       //free(domain);
        free(router_name);
 
        if( len >= sizeof( target_buf ) ) {
@@ -260,12 +576,14 @@ osrfAppSession* osrf_app_client_session_init( const char* remote_service ) {
                return NULL;
        }
 
-       session->request_queue = osrfNewList();
-       session->request_queue->freeItem = &_osrf_app_request_free;
        session->remote_id = strdup(target_buf);
        session->orig_remote_id = strdup(session->remote_id);
        session->remote_service = strdup(remote_service);
        session->session_locale = NULL;
+       session->session_tz = NULL;
+       session->transport_error = 0;
+       session->panic = 0;
+       session->outbuf = NULL;   // Not used by client
 
        #ifdef ASSUME_STATELESS
        session->stateless = 1;
@@ -280,21 +598,45 @@ osrfAppSession* osrf_app_client_session_init( const char* remote_service ) {
 
        snprintf(id, sizeof(id), "%f.%d%ld", get_timestamp_millis(), (int)time(NULL), (long) getpid());
        session->session_id = strdup(id);
-       osrfLogDebug( OSRF_LOG_MARK,  "Building a new client session with id [%s] [%s]", 
+       osrfLogDebug( OSRF_LOG_MARK,  "Building a new client session with id [%s] [%s]",
                        session->remote_service, session->session_id );
 
        session->thread_trace = 0;
        session->state = OSRF_SESSION_DISCONNECTED;
        session->type = OSRF_SESSION_CLIENT;
-       //session->next = NULL;
 
        session->userData = NULL;
        session->userDataFree = NULL;
-       
+
+       // Initialize the hash table
+       int i;
+       for( i = 0; i < OSRF_REQUEST_HASH_SIZE; ++i )
+               session->request_hash[ i ] = NULL;
+
        _osrf_app_session_push_session( session );
        return session;
 }
 
+/**
+       @brief Create an osrfAppSession for a server.
+       @param session_id The session ID.  In practice this comes from the thread member of
+       the transport message from the client.
+       @param our_app The name of the service being provided.
+       @param remote_id Jabber ID of the client.
+       @return Pointer to the newly created osrfAppSession if successful, or NULL upon failure.
+
+       If there is already a session with the specified id, report an error.  Otherwise:
+
+       - Allocate memory for an osrfAppSession.
+       - For talking with Jabber, grab an existing transport_client.  It should have been
+       already set up by a prior call to osrfSystemBootstrapClientResc().
+       - Install a copy of the @a our_app string as remote_service.
+       - Install copies of the @a remote_id string as remote_id and orig_remote_id.
+       - Initialize various other bits and scraps.
+       - Add the session to the global session cache.
+
+       Do @em not respond to the client at this point.
+*/
 osrfAppSession* osrf_app_server_session_init(
                const char* session_id, const char* our_app, const char* remote_id ) {
 
@@ -302,24 +644,31 @@ osrfAppSession* osrf_app_server_session_init(
                        " and remote_id %s", session_id, our_app, remote_id );
 
        osrfAppSession* session = osrf_app_session_find_session( session_id );
-       if(session) return session;
+       if(session) {
+               osrfLogWarning( OSRF_LOG_MARK, "App session already exists for session id %s",
+                               session_id );
+               return NULL;
+       }
 
        session = safe_malloc(sizeof(osrfAppSession));
 
+       // Grab an existing transport_client for talking with Jabber
        session->transport_handle = osrfSystemGetTransportClient();
        if( session->transport_handle == NULL ) {
                osrfLogWarning( OSRF_LOG_MARK, "No transport client for service '%s'", our_app );
+               free(session);
                return NULL;
        }
 
+       // Decide from a config setting whether the session is stateless or not.  However
+       // this determination is pointless because it will immediately be overruled according
+       // to the compile-time macro ASSUME_STATELESS.
        int stateless = 0;
        char* statel = osrf_settings_host_value("/apps/%s/stateless", our_app );
-       if(statel) stateless = atoi(statel);
+       if( statel )
+               stateless = atoi( statel );
        free(statel);
 
-
-       session->request_queue = osrfNewList();
-       session->request_queue->freeItem = &_osrf_app_request_free;
        session->remote_id = strdup(remote_id);
        session->orig_remote_id = strdup(remote_id);
        session->session_id = strdup(session_id);
@@ -336,61 +685,107 @@ osrfAppSession* osrf_app_server_session_init(
        session->state = OSRF_SESSION_DISCONNECTED;
        session->type = OSRF_SESSION_SERVER;
        session->session_locale = NULL;
+       session->session_tz = NULL;
 
        session->userData = NULL;
        session->userDataFree = NULL;
-       
+       session->transport_error = 0;
+
+       // Initialize the hash table
+       int i;
+       for( i = 0; i < OSRF_REQUEST_HASH_SIZE; ++i )
+               session->request_hash[ i ] = NULL;
+
+       session->panic = 0;
+       session->outbuf = buffer_init( 4096 );
+
        _osrf_app_session_push_session( session );
        return session;
-
 }
 
+/**
+       @brief Create a REQUEST message, send it, and save it for future reference.
+       @param session Pointer to the current session, which has the addressing information.
+       @param params One way of specifying the parameters for the method.
+       @param method_name The name of the method to be called.
+       @param protocol Protocol.
+       @param param_strings Another way of specifying the parameters for the method.
+       @return The request ID of the resulting REQUEST message, or -1 upon error.
 
+       DEPRECATED.  Use osrfAppSessionSendRequest() instead.  It is identical except that it
+       doesn't use the param_strings argument, which is redundant, confusing, and unused.
 
-/** frees memory held by a session */
-static void _osrf_app_session_free( osrfAppSession* session ){
-       if(session==NULL)
-               return;
+       If @a params is non-NULL, use it to specify the parameters to the method.  Otherwise
+       use @a param_strings.
 
-       if( session->userDataFree && session->userData ) 
-               session->userDataFree(session->userData);
-       
-       if(session->session_locale)
-               free(session->session_locale);
+       If @a params points to a JSON_ARRAY, then pass each element of the array as a separate
+       parameter.  If @a params points to any other kind of jsonObject, pass it as a single
+       parameter.
 
-       free(session->remote_id);
-       free(session->orig_remote_id);
-       free(session->session_id);
-       free(session->remote_service);
-       osrfListFree(session->request_queue);
-       free(session);
-}
+       If @a params is NULL, and @a param_strings is not NULL, then each pointer in the
+       osrfStringArray must point to a JSON string encoding a parameter.  Pass them.
+
+       At this writing, all calls to this function use @a params to pass parameters, rather than
+       @a param_strings.
 
+       This function is a thin wrapper for osrfAppSessionMakeLocaleRequest().
+*/
 int osrfAppSessionMakeRequest(
                osrfAppSession* session, const jsonObject* params,
-               const char* method_name, int protocol, string_array* param_strings ) {
+               const char* method_name, int protocol, osrfStringArray* param_strings ) {
 
+       osrfLogWarning( OSRF_LOG_MARK, "Function osrfAppSessionMakeRequest() is deprecated; "
+                       "call osrfAppSessionSendRequest() instead" );
        return osrfAppSessionMakeLocaleRequest( session, params,
                        method_name, protocol, param_strings, NULL );
 }
 
-int osrf_app_session_make_req(
-               osrfAppSession* session, const jsonObject* params,
-               const char* method_name, int protocol, string_array* param_strings) {
+/**
+       @brief Create a REQUEST message, send it, and save it for future reference.
+       @param session Pointer to the current session, which has the addressing information.
+       @param params One way of specifying the parameters for the method.
+       @param method_name The name of the method to be called.
+       @param protocol Protocol.
+       @return The request ID of the resulting REQUEST message, or -1 upon error.
+
+       If @a params points to a JSON_ARRAY, then pass each element of the array as a separate
+       parameter.  If @a params points to any other kind of jsonObject, pass it as a single
+       parameter.
 
-       return osrfAppSessionMakeLocaleRequest(session, params,
-                       method_name, protocol, param_strings, NULL);
+       This function is a thin wrapper for osrfAppSessionMakeLocaleRequest().
+*/
+int osrfAppSessionSendRequest( osrfAppSession* session, const jsonObject* params,
+               const char* method_name, int protocol ) {
+
+       return osrfAppSessionMakeLocaleRequest( session, params,
+                method_name, protocol, NULL, NULL );
 }
 
+/**
+       @brief Create a REQUEST message, send it, and save it for future reference.
+       @param session Pointer to the current session, which has the addressing information.
+       @param params One way of specifying the parameters for the method.
+       @param method_name The name of the method to be called.
+       @param protocol Protocol.
+       @param param_strings Another way of specifying the parameters for the method.
+       @param locale Pointer to a locale string.
+       @return The request ID of the resulting REQUEST message, or -1 upon error.
+
+       See the discussion of osrfAppSessionSendRequest(), which at this writing is the only
+       place that calls this function, except for the similar but deprecated function
+       osrfAppSessionMakeRequest().
+
+       At this writing, the @a param_strings and @a locale parameters are always NULL.
+*/
 static int osrfAppSessionMakeLocaleRequest(
                osrfAppSession* session, const jsonObject* params, const char* method_name,
-               int protocol, string_array* param_strings, char* locale ) {
+               int protocol, osrfStringArray* param_strings, char* locale ) {
 
        if(session == NULL) return -1;
 
        osrfLogMkXid();
 
-       osrf_message* req_msg = osrf_message_init( REQUEST, ++(session->thread_trace), protocol );
+       osrfMessage* req_msg = osrf_message_init( REQUEST, ++(session->thread_trace), protocol );
        osrf_message_set_method(req_msg, method_name);
 
        if (locale) {
@@ -399,6 +794,12 @@ static int osrfAppSessionMakeLocaleRequest(
                osrf_message_set_locale(req_msg, session->session_locale);
        }
 
+       osrf_message_set_tz(req_msg, session->session_tz);
+
+       if (!current_ingress)
+               osrfAppSessionSetIngress("opensrf");
+       osrfMessageSetIngress(req_msg, current_ingress);
+
        if(params) {
                osrf_message_set_params(req_msg, params);
 
@@ -408,82 +809,119 @@ static int osrfAppSessionMakeLocaleRequest(
                        int i;
                        for(i = 0; i!= param_strings->size ; i++ ) {
                                osrf_message_add_param(req_msg,
-                                       string_array_get_string(param_strings,i));
+                                       osrfStringArrayGetString(param_strings,i));
                        }
                }
        }
 
        osrfAppRequest* req = _osrf_app_request_init( session, req_msg );
        if(_osrf_app_session_send( session, req_msg ) ) {
-               osrfLogWarning( OSRF_LOG_MARK,  "Error sending request message [%d]", session->thread_trace );
+               osrfLogWarning( OSRF_LOG_MARK,  "Error sending request message [%d]",
+                               session->thread_trace );
+               _osrf_app_request_free(req);
                return -1;
        }
 
        osrfLogDebug( OSRF_LOG_MARK,  "Pushing [%d] onto request queue for session [%s] [%s]",
                        req->request_id, session->remote_service, session->session_id );
-       osrfListSet( session->request_queue, req, req->request_id ); 
+       add_app_request( session, req );
        return req->request_id;
 }
 
+/**
+       @brief Mark an osrfAppRequest (identified by session and ID) as complete.
+       @param session Pointer to the osrfAppSession that owns the request.
+       @param request_id Request ID of the osrfAppRequest.
+*/
 void osrf_app_session_set_complete( osrfAppSession* session, int request_id ) {
        if(session == NULL)
                return;
 
-       osrfAppRequest* req = OSRF_LIST_GET_INDEX( session->request_queue, request_id );
-       if(req) req->complete = 1;
+       osrfAppRequest* req = find_app_request( session, request_id );
+       if(req)
+               req->complete = 1;
 }
 
+/**
+       @brief Determine whether a osrfAppRequest, identified by session and ID, is complete.
+       @param session Pointer to the osrfAppSession that owns the request.
+       @param request_id Request ID of the osrfAppRequest.
+       @return Non-zero if the request is complete; zero if it isn't, or if it can't be found.
+*/
 int osrf_app_session_request_complete( const osrfAppSession* session, int request_id ) {
        if(session == NULL)
                return 0;
-       osrfAppRequest* req = OSRF_LIST_GET_INDEX( session->request_queue, request_id );
+
+       osrfAppRequest* req = find_app_request( session, request_id );
        if(req)
                return req->complete;
+
        return 0;
 }
 
-
-/** Resets the remote connection id to that of the original*/
+/**
+       @brief Reset the remote ID of a session to its original remote ID.
+       @param session Pointer to the osrfAppSession to be reset.
+*/
 void osrf_app_session_reset_remote( osrfAppSession* session ){
        if( session==NULL )
                return;
 
-       free(session->remote_id);
        osrfLogDebug( OSRF_LOG_MARK,  "App Session [%s] [%s] resetting remote id to %s",
                        session->remote_service, session->session_id, session->orig_remote_id );
 
-       session->remote_id = strdup(session->orig_remote_id);
+       osrf_app_session_set_remote( session, session->orig_remote_id );
 }
 
+/**
+       @brief Set a session's remote ID to a specified value.
+       @param session Pointer to the osrfAppSession whose remote ID is to be set.
+       @param remote_id Pointer to the new remote id.
+*/
 void osrf_app_session_set_remote( osrfAppSession* session, const char* remote_id ) {
-       if(session == NULL)
+       if( session == NULL || remote_id == NULL )
                return;
-       if( session->remote_id )
-               free(session->remote_id );
-       session->remote_id = strdup( remote_id );
-}
 
-/** pushes the given message into the result list of the app_request
-  with the given request_id */
-int osrf_app_session_push_queue( 
-               osrfAppSession* session, osrf_message* msg ){
-       if(session == NULL || msg == NULL) return 0;
-
-       osrfAppRequest* req = OSRF_LIST_GET_INDEX( session->request_queue, msg->thread_trace );
-       if(req == NULL) return 0;
-       _osrf_app_request_push_queue( req, msg );
-
-       return 0;
+       if( session->remote_id ) {
+               if( strlen(session->remote_id) >= strlen(remote_id) ) {
+                       // There's enough room; just copy it
+                       strcpy(session->remote_id, remote_id);
+               } else {
+                       free(session->remote_id );
+                       session->remote_id = strdup( remote_id );
+               }
+       } else
+               session->remote_id = strdup( remote_id );
 }
 
-int osrfAppSessionConnect( osrfAppSession* session ) { 
-       return osrf_app_session_connect(session);
+/**
+       @brief Append an osrfMessage to the list of responses to an osrfAppRequest.
+       @param session Pointer to the osrfAppSession that owns the request.
+       @param msg Pointer to the osrfMessage to be added.
+
+       The thread_trace member of the osrfMessage is the request_id of the osrfAppRequest.
+       Find the corresponding request in the session and append the osrfMessage to its list.
+*/
+void osrf_app_session_push_queue( osrfAppSession* session, osrfMessage* msg ) {
+       if( session && msg ) {
+               osrfAppRequest* req = find_app_request( session, msg->thread_trace );
+               if( req )
+                       _osrf_app_request_push_queue( req, msg );
+       }
 }
 
+/**
+       @brief Connect to the remote service.
+       @param session Pointer to the osrfAppSession for the service.
+       @return 1 if successful, or 0 if not.
+
+       If already connected, exit immediately, reporting success.  Otherwise, build a CONNECT
+       message and send it to the service.  Wait for up to five seconds for an acknowledgement.
+
+       The timeout value is currently hard-coded.  Perhaps it should be configurable.
+*/
+int osrfAppSessionConnect( osrfAppSession* session ) {
 
-/** Attempts to connect to the remote service */
-int osrf_app_session_connect(osrfAppSession* session){
-       
        if(session == NULL)
                return 0;
 
@@ -496,18 +934,27 @@ int osrf_app_session_connect(osrfAppSession* session){
        osrfLogDebug( OSRF_LOG_MARK,  "AppSession connecting to %s", session->remote_id );
 
        /* defaulting to protocol 1 for now */
-       osrf_message* con_msg = osrf_message_init( CONNECT, session->thread_trace, 1 );
+       osrfMessage* con_msg = osrf_message_init( CONNECT, session->thread_trace, 1 );
+
+       // Address this message to the router
        osrf_app_session_reset_remote( session );
        session->state = OSRF_SESSION_CONNECTING;
        int ret = _osrf_app_session_send( session, con_msg );
-       osrf_message_free(con_msg);
-       if(ret) return 0;
+       osrfMessageFree(con_msg);
+       if(ret)
+               return 0;
 
-       time_t start = time(NULL);      
+       time_t start = time(NULL);
        time_t remaining = (time_t) timeout;
 
+       // Wait for the acknowledgement.  We look for it repeatedly because, under the covers,
+       // we may receive and process messages other than the one we're looking for.
        while( session->state != OSRF_SESSION_CONNECTED && remaining >= 0 ) {
                osrf_app_session_queue_wait( session, remaining, NULL );
+               if(session->transport_error) {
+                       osrfLogError(OSRF_LOG_MARK, "cannot communicate with %s", session->remote_service);
+                       return 0;
+               }
                remaining -= (int) (time(NULL) - start);
        }
 
@@ -520,9 +967,15 @@ int osrf_app_session_connect(osrfAppSession* session){
        return 1;
 }
 
+/**
+       @brief Disconnect from the remote service.  No response is expected.
+       @param session Pointer to the osrfAppSession to be disconnected.
+       @return 1 in all cases.
 
-
-/** Disconnects from the remote service */
+       If we're already disconnected, return immediately without doing anything.  Likewise if
+       we have a stateless session and we're in the process of connecting.  Otherwise, send a
+       DISCONNECT message to the service.
+*/
 int osrf_app_session_disconnect( osrfAppSession* session){
        if(session == NULL)
                return 1;
@@ -531,38 +984,69 @@ int osrf_app_session_disconnect( osrfAppSession* session){
                return 1;
 
        if(session->stateless && session->state != OSRF_SESSION_CONNECTED) {
-               osrfLogDebug( OSRF_LOG_MARK,  
-                               "Exiting disconnect on stateless session %s", 
+               osrfLogDebug( OSRF_LOG_MARK,
+                               "Exiting disconnect on stateless session %s",
                                session->session_id);
                return 1;
        }
 
        osrfLogDebug(OSRF_LOG_MARK,  "AppSession disconnecting from %s", session->remote_id );
 
-       osrf_message* dis_msg = osrf_message_init( DISCONNECT, session->thread_trace, 1 );
+       osrfMessage* dis_msg = osrf_message_init( DISCONNECT, session->thread_trace, 1 );
        _osrf_app_session_send( session, dis_msg );
        session->state = OSRF_SESSION_DISCONNECTED;
 
-       osrf_message_free( dis_msg );
+       osrfMessageFree( dis_msg );
        osrf_app_session_reset_remote( session );
        return 1;
 }
 
+/**
+       @brief Resend a request message, as specified by session and request id.
+       @param session Pointer to the osrfAppSession.
+       @param req_id Request ID for the request to be resent.
+       @return Zero if successful, or if the specified request cannot be found; 1 if the
+       request is already complete, or if the attempt to resend the message fails.
+
+       The choice of return codes may seem seem capricious, but at this writing nothing
+       pays any attention to the return code anyway.
+*/
 int osrf_app_session_request_resend( osrfAppSession* session, int req_id ) {
-       osrfAppRequest* req = OSRF_LIST_GET_INDEX( session->request_queue, req_id );
-       return _osrf_app_request_resend( req );
-}
+       osrfAppRequest* req = find_app_request( session, req_id );
+
+       int rc;
+       if(req == NULL) {
+               rc = 0;
+       } else if(!req->complete) {
+               osrfLogDebug( OSRF_LOG_MARK, "Resending request [%d]", req->request_id );
+               rc = _osrf_app_session_send( req->session, req->payload );
+       } else {
+               rc = 1;
+       }
 
+       return rc;
+}
 
-static int osrfAppSessionSendBatch( osrfAppSession* session, osrf_message* msgs[], int size ) {
+/**
+       @brief Send one or more osrfMessages to the remote service or client.
+       @param session Pointer to the osrfAppSession responsible for sending the message(s).
+       @param msgs Pointer to an array of pointers to osrfMessages.
+       @param size How many messages to send.
+       @return 0 upon success, or -1 upon failure.
+*/
+static int osrfAppSessionSendBatch( osrfAppSession* session, osrfMessage* msgs[], int size ) {
 
-       if( !(session && msgs && size > 0) ) return 0;
+       if( !(session && msgs && size > 0) ) return -1;
        int retval = 0;
 
        osrfMessage* msg = msgs[0];
 
        if(msg) {
 
+               // First grab and process any input messages, for any app session.  This gives us
+               // a chance to see any CONNECT or DISCONNECT messages that may have arrived.  We
+               // may also see some unrelated messages, but we have to process those sooner or
+               // later anyway, so we might as well do it now.
                osrf_app_session_queue_wait( session, 0, NULL );
 
                if(session->state != OSRF_SESSION_CONNECTED)  {
@@ -570,168 +1054,403 @@ static int osrfAppSessionSendBatch( osrfAppSession* session, osrf_message* msgs[
                        if(session->stateless) { /* stateless session always send to the root listener */
                                osrf_app_session_reset_remote(session);
 
-                       } else { 
+                       } else {
 
                                /* do an auto-connect if necessary */
                                if( ! session->stateless &&
-                                       (msg->m_type != CONNECT) && 
+                                       (msg->m_type != CONNECT) &&
                                        (msg->m_type != DISCONNECT) &&
                                        (session->state != OSRF_SESSION_CONNECTED) ) {
 
-                                       if(!osrf_app_session_connect( session )) 
-                                               return 0;
+                                       if(!osrfAppSessionConnect( session ))
+                                               return -1;
                                }
                        }
                }
        }
 
+       // Translate the collection of osrfMessages into a JSON array
        char* string = osrfMessageSerializeBatch(msgs, size);
 
+       // Send the JSON as the payload of a transport_message
        if( string ) {
+               retval = osrfSendTransportPayload( session, string );
+               free(string);
+       }
 
-               transport_message* t_msg = message_init( 
-                               string, "", session->session_id, session->remote_id, NULL );
-      message_set_osrf_xid( t_msg, osrfLogGetXid() );
-
-               retval = client_send_message( session->transport_handle, t_msg );
+       return retval;
+}
 
-               if( retval ) osrfLogError(OSRF_LOG_MARK, "client_send_message failed");
+/**
+       @brief Split a given string into one or more transport result messages and send it
+       @param session Pointer to the osrfAppSession responsible for sending the message(s).
+       @param request_id Request ID of the osrfAppRequest.
+       @param payload A string to be sent via Jabber.
+       @param payload_size length of payload
+       @param chunk_size chunk_size to use
+
+       @return 0 upon success, or -1 upon failure.
+*/
+int osrfSendChunkedResult(
+        osrfAppSession* session, int request_id, const char* payload,
+        size_t payload_size, size_t chunk_size ) {
+
+       // chunking payload
+       int i;
+       for (i = 0; i < payload_size; i += chunk_size) {
+               osrfMessage* msg = osrf_message_init(RESULT, request_id, 1);
+               osrf_message_set_status_info(msg,
+                       "osrfResultPartial",
+                       "Partial Response",
+                       OSRF_STATUS_PARTIAL
+               );
+
+               // see how long this chunk is.  If this is the last
+               // chunk, it will likely be less than chunk_size
+               int partial_size = strlen(&payload[i]);
+               if (partial_size > chunk_size)
+                       partial_size = chunk_size;
+
+               // substr(data, i, partial_size)
+               char partial_buf[partial_size + 1];
+               memcpy(partial_buf, &payload[i], partial_size);
+               partial_buf[partial_size] = '\0';
+
+               // package the partial chunk as a JSON string object
+               jsonObject*  partial_obj = jsonNewObject(partial_buf);
+               osrf_message_set_result(msg, partial_obj);
+               jsonObjectFree(partial_obj);
+
+               // package the osrf message within an array then
+               // serialize to json for delivery
+               jsonObject* arr = jsonNewObject(NULL);
+
+               // msg json freed when arr is freed
+               jsonObjectPush(arr, osrfMessageToJSON(msg));
+               char* json = jsonObjectToJSON(arr);
+
+               osrfSendTransportPayload(session, json);
+               osrfMessageFree(msg);
+               jsonObjectFree(arr);
+               free(json);
+       }
 
-               osrfLogInfo(OSRF_LOG_MARK, "[%s] sent %d bytes of data to %s",
-                       session->remote_service, strlen(string), t_msg->recipient );
+       // all chunks sent; send the final partial-complete msg
+       osrfMessage* msg = osrf_message_init(RESULT, request_id, 1);
+       osrf_message_set_status_info(msg,
+               "osrfResultPartialComplete",
+               "Partial Response Finalized",
+               OSRF_STATUS_NOCONTENT
+       );
+
+       jsonObject* arr = jsonNewObject(NULL);
+       jsonObjectPush(arr, osrfMessageToJSON(msg));
+       char* json = jsonObjectToJSON(arr);
+       osrfSendTransportPayload(session, json);
+       osrfMessageFree(msg);
+       jsonObjectFree(arr);
+       free(json);
 
-               osrfLogDebug(OSRF_LOG_MARK, "Sent: %s", string );
+       return 0;
+}
 
-               free(string);
-               message_free( t_msg );
+/**
+       @brief Wrap a given string in a transport message and send it.
+       @param session Pointer to the osrfAppSession responsible for sending the message(s).
+       @param payload A string to be sent via Jabber.
+       @return 0 upon success, or -1 upon failure.
+
+       In practice the payload is normally a JSON string, but this function assumes nothing
+       about it.
+*/
+int osrfSendTransportPayload( osrfAppSession* session, const char* payload ) {
+       transport_message* t_msg = message_init(
+               payload, "", session->session_id, session->remote_id, NULL );
+       message_set_osrf_xid( t_msg, osrfLogGetXid() );
+
+       int retval = client_send_message( session->transport_handle, t_msg );
+       if( retval ) {
+               osrfLogError( OSRF_LOG_MARK, "client_send_message failed, exit()ing immediately" );
+               exit(99);
        }
 
-       return retval; 
-}
+       osrfLogInfo(OSRF_LOG_MARK, "[%s] sent %d bytes of data to %s",
+               session->remote_service, strlen( payload ), t_msg->recipient );
 
+       osrfLogDebug( OSRF_LOG_MARK, "Sent: %s", payload );
+
+       message_free( t_msg );
+       return retval;
+}
 
+/**
+       @brief Send a single osrfMessage to the remote service or client.
+       @param session Pointer to the osrfAppSession.
+       @param msg Pointer to the osrfMessage to be sent.
+       @return zero upon success, or 1 upon failure.
 
-static int _osrf_app_session_send( osrfAppSession* session, osrf_message* msg ){
-       if( !(session && msg) ) return 0;
+       A thin wrapper.  Create an array of one element, and pass it to osrfAppSessionSendBatch().
+*/
+static int _osrf_app_session_send( osrfAppSession* session, osrfMessage* msg ){
+       if( !(session && msg) )
+               return 1;
        osrfMessage* a[1];
        a[0] = msg;
-       return osrfAppSessionSendBatch( session, a, 1 );
+       return  - osrfAppSessionSendBatch( session, a, 1 );
 }
 
 
+/**
+       @brief Wait for any input messages to arrive, and process them as needed.
+       @param session Pointer to the osrfAppSession whose transport_session we will use.
+       @param timeout How many seconds to wait for the first input message.
+       @param recvd Pointer to an boolean int.  If you receive at least one message, set the boolean
+       to true; otherwise set it to false.
+       @return 0 upon success (even if a timeout occurs), or -1 upon failure.
+
+       A thin wrapper for osrf_stack_process().  The timeout applies only to the first
+       message; process subsequent messages if they are available, but don't wait for them.
+
+       The first parameter identifies an osrfApp session, but all we really use it for is to
+       get a pointer to the transport_session.  Typically, a given process opens only a single
+       transport_session (to talk to the Jabber server), and all app sessions in that process
+       use the same transport_session.
 
+       Hence this function indiscriminately waits for input messages for all osrfAppSessions
+       tied to the same Jabber session, not just the one specified.
 
-/**  Waits up to 'timeout' seconds for some data to arrive.
-  * Any data that arrives will be processed according to its
-  * payload and message type.  This method will return after
-  * any data has arrived.
-  */
+       Dispatch each message to the appropriate processing routine, depending on its type
+       and contents, and on whether we're acting as a client or as a server for that message.
+       For example, a response to a request may be appended to the input queue of the
+       relevant request.  A server session receiving a REQUEST message may execute the
+       requested method.  And so forth.
+*/
 int osrf_app_session_queue_wait( osrfAppSession* session, int timeout, int* recvd ){
        if(session == NULL) return 0;
-       osrfLogDebug(OSRF_LOG_MARK,  "AppSession in queue_wait with timeout %d", timeout );
-       return osrf_stack_entry_point(session->transport_handle, timeout, recvd);
+       osrfLogDebug(OSRF_LOG_MARK, "AppSession in queue_wait with timeout %d", timeout );
+       return osrf_stack_process(session->transport_handle, timeout, recvd);
 }
 
-/** Disconnects (if client) and removes the given session from the global session cache 
-  * ! This free's all attached app_requests ! 
-  */
-void osrfAppSessionFree( osrfAppSession* ses ) {
-       osrf_app_session_destroy( ses );
-}
+/**
+       @brief Shut down and destroy an osrfAppSession.
+       @param session Pointer to the osrfAppSession to be destroyed.
+
+       If this is a client session, send a DISCONNECT message.
 
+       Remove the session from the global session cache.
 
-void osrf_app_session_destroy( osrfAppSession* session ){
+       Free all associated resources, including any pending osrfAppRequests.
+*/
+void osrfAppSessionFree( osrfAppSession* session ){
        if(session == NULL) return;
 
-       osrfLogDebug(OSRF_LOG_MARK,  "AppSession [%s] [%s] destroying self and deleting requests", 
+       /* Disconnect */
+
+       osrfLogDebug(OSRF_LOG_MARK,  "AppSession [%s] [%s] destroying self and deleting requests",
                        session->remote_service, session->session_id );
-       if(session->type == OSRF_SESSION_CLIENT 
-                       && session->state != OSRF_SESSION_DISCONNECTED ) { /* disconnect if we're a client */
-               osrf_message* dis_msg = osrf_message_init( DISCONNECT, session->thread_trace, 1 );
-               _osrf_app_session_send( session, dis_msg ); 
-               osrf_message_free(dis_msg);
+       /* disconnect if we're a client */
+       if(session->type == OSRF_SESSION_CLIENT
+                       && session->state != OSRF_SESSION_DISCONNECTED ) {
+               osrfMessage* dis_msg = osrf_message_init( DISCONNECT, session->thread_trace, 1 );
+               _osrf_app_session_send( session, dis_msg );
+               osrfMessageFree(dis_msg);
        }
 
+       /* Remove self from the global session cache */
+
        osrfHashRemove( osrfAppSessionCache, session->session_id );
-       _osrf_app_session_free( session );
-}
 
-osrf_message* osrfAppSessionRequestRecv(
-               osrfAppSession* session, int req_id, int timeout ) {
-       return osrf_app_session_request_recv( session, req_id, timeout );
+       /* Free the memory */
+
+       if( session->userDataFree && session->userData )
+               session->userDataFree(session->userData);
+
+       if(session->session_locale)
+               free(session->session_locale);
+
+       if(session->session_tz)
+               free(session->session_tz);
+
+       free(session->remote_id);
+       free(session->orig_remote_id);
+       free(session->session_id);
+       free(session->remote_service);
+
+       // Free the request hash
+       int i;
+       for( i = 0; i < OSRF_REQUEST_HASH_SIZE; ++i ) {
+               osrfAppRequest* app = session->request_hash[ i ];
+               while( app ) {
+                       osrfAppRequest* next = app->next;
+                       _osrf_app_request_free( app );
+                       app = next;
+               }
+       }
+
+       if( session->outbuf )
+               buffer_free( session->outbuf );
+
+       free(session);
 }
-osrf_message* osrf_app_session_request_recv( 
+
+/**
+       @brief Wait for a response to a given request, subject to a timeout.
+       @param session Pointer to the osrfAppSession that owns the request.
+       @param req_id Request ID for the request.
+       @param timeout How many seconds to wait.
+       @return A pointer to the received osrfMessage if one arrives; otherwise NULL.
+
+       A thin wrapper.  Given a session and a request ID, look up the corresponding request
+       and pass it to _osrf_app_request_recv().
+*/
+osrfMessage* osrfAppSessionRequestRecv(
                osrfAppSession* session, int req_id, int timeout ) {
        if(req_id < 0 || session == NULL)
                return NULL;
-       osrfAppRequest* req = OSRF_LIST_GET_INDEX( session->request_queue, req_id );
+       osrfAppRequest* req = find_app_request( session, req_id );
        return _osrf_app_request_recv( req, timeout );
 }
 
+/**
+       @brief In response to a specified request, send a payload of data to a client.
+       @param ses Pointer to the osrfAppSession that owns the request.
+       @param requestId Request ID of the osrfAppRequest.
+       @param data Pointer to a jsonObject containing the data payload.
+       @return 0 upon success, or -1 upon failure.
 
+       Translate the jsonObject to a JSON string, and send it wrapped in a RESULT message.
 
+       The only failure detected is if either of the two pointer parameters is NULL.
+*/
 int osrfAppRequestRespond( osrfAppSession* ses, int requestId, const jsonObject* data ) {
-       if(!ses || ! data ) return -1;
+       if( !ses || ! data )
+               return -1;
 
-       osrf_message* msg = osrf_message_init( RESULT, requestId, 1 );
+       osrfMessage* msg = osrf_message_init( RESULT, requestId, 1 );
        osrf_message_set_status_info( msg, NULL, "OK", OSRF_STATUS_OK );
        char* json = jsonObjectToJSON( data );
 
        osrf_message_set_result_content( msg, json );
-       _osrf_app_session_send( ses, msg ); 
+       _osrf_app_session_send( ses, msg );
 
        free(json);
-       osrf_message_free( msg );
+       osrfMessageFree( msg );
 
        return 0;
 }
 
 
-int osrfAppRequestRespondComplete( 
+/**
+       @brief Send one or two messages to a client in response to a specified request.
+       @param ses Pointer to the osrfAppSession that owns the request.
+       @param requestId Request ID of the osrfAppRequest.
+       @param data Pointer to a jsonObject containing the data payload.
+       @return  Zero in all cases.
+
+       If the @a data parameter is not NULL, translate the jsonObject into a JSON string, and
+       incorporate that string into a RESULT message as as the payload .  Also build a STATUS
+       message indicating that the response is complete.  Send both messages bundled together
+       in the same transport_message.
+
+       If the @a data parameter is NULL, send only a STATUS message indicating that the response
+       is complete.
+*/
+int osrfAppRequestRespondComplete(
                osrfAppSession* ses, int requestId, const jsonObject* data ) {
 
-       osrf_message* payload = osrf_message_init( RESULT, requestId, 1 );
-       osrf_message_set_status_info( payload, NULL, "OK", OSRF_STATUS_OK );
+       osrfMessage* status = osrf_message_init( STATUS, requestId, 1);
+       osrf_message_set_status_info( status, "osrfConnectStatus", "Request Complete",
+                       OSRF_STATUS_COMPLETE );
 
-       osrf_message* status = osrf_message_init( STATUS, requestId, 1);
-       osrf_message_set_status_info( status, "osrfConnectStatus", "Request Complete", OSRF_STATUS_COMPLETE );
-       
        if (data) {
-               char* json = jsonObjectToJSON( data );
-               osrf_message_set_result_content( payload, json );
-               free(json);
+               char* json = jsonObjectToJSON(data);
+               size_t raw_size = strlen(json);
+               size_t extra_size = osrfXmlEscapingLength(json);
+               size_t data_size = raw_size + extra_size;
+               size_t chunk_size = OSRF_MSG_CHUNK_SIZE;
+
+               if (data_size > chunk_size) // calculate an escape-scaled chunk size
+                       chunk_size = ((double)raw_size / (double)data_size) * (double)chunk_size;
+
+               if (chunk_size > 0 && chunk_size < raw_size) {
+                       // chunking -- response message exceeds max message size.
+                       // break it up into chunks for partial delivery
+
+                       osrfSendChunkedResult(ses, requestId, json, raw_size, chunk_size);
+                       osrfAppSessionSendBatch( ses, &status, 1 );
+
+               } else {
+                       // message doesn't need to be chunked
+                       osrfMessage* payload = osrf_message_init( RESULT, requestId, 1 );
+                       osrf_message_set_status_info( payload, NULL, "OK", OSRF_STATUS_OK );
+
+                       osrf_message_set_result_content( payload, json );
 
-               osrfMessage* ms[2];
-               ms[0] = payload;
-               ms[1] = status;
+                       osrfMessage* ms[2];
+                       ms[0] = payload;
+                       ms[1] = status;
 
-               osrfAppSessionSendBatch( ses, ms, 2 );
+                       osrfAppSessionSendBatch( ses, ms, 2 );
+
+                       osrfMessageFree( payload );
+               }
+
+               free(json);
 
-               osrf_message_free( payload );
        } else {
                osrfAppSessionSendBatch( ses, &status, 1 );
        }
 
-       osrf_message_free( status );
+       osrfMessageFree( status );
 
        return 0;
 }
 
+/**
+       @brief Send a STATUS message, for a specified request, back to the client.
+       @param ses Pointer to the osrfAppSession connected to the client.
+       @param type A numeric code denoting the status.
+       @param name A string naming the status.
+       @param reqId The request ID of the request.
+       @param message A brief message describing the status.
+       @return 0 upon success, or -1 upon failure.
+
+       The only detected failure is when the @a ses parameter is NULL.
+*/
 int osrfAppSessionStatus( osrfAppSession* ses, int type,
                const char* name, int reqId, const char* message ) {
 
        if(ses) {
-               osrf_message* msg = osrf_message_init( STATUS, reqId, 1);
+               osrfMessage* msg = osrf_message_init( STATUS, reqId, 1);
                osrf_message_set_status_info( msg, name, message, type );
-               _osrf_app_session_send( ses, msg ); 
-               osrf_message_free( msg );
+               _osrf_app_session_send( ses, msg );
+               osrfMessageFree( msg );
                return 0;
-       }
-       return -1;
+       } else
+               return -1;
 }
 
+/**
+       @brief Free the global session cache.
+
+       Note that the osrfHash that implements the global session cache does @em not have a
+       callback function installed for freeing its cargo.  As a result, any remaining
+       osrfAppSessions are leaked, along with all the osrfAppRequests and osrfMessages they
+       own.
+*/
+void osrfAppSessionCleanup( void ) {
+       osrfHashFree(osrfAppSessionCache);
+       osrfAppSessionCache = NULL;
+}
 
+/**
+       @brief Arrange for immediate termination of the process.
+       @param ses Pointer to the current osrfAppSession.
 
-
-
-
+       Typical use case: a server drone loses its database connection, thereby becoming useless.
+       It terminates so that it will not receive further requests, being unable to service them.
+*/
+void osrfAppSessionPanic( osrfAppSession* ses ) {
+       if( ses )
+               ses->panic = 1;
+}