LP#1729610: return new OpenSRF status if backlog queue fills up
[OpenSRF.git] / src / javascript / opensrf.js
1 /* -----------------------------------------------------------------------
2  * Copyright (C) 2008  Georgia Public Library Service
3  * Bill Erickson <erickson@esilibrary.com>
4  *  
5  * This program is free software; you can redistribute it and/or
6  * modify it under the terms of the GNU General Public License
7  * as published by the Free Software Foundation; either version 2
8  * of the License, or (at your option) any later version.
9  * 
10  * This program is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13  * GNU General Public License for more details.
14  * ----------------------------------------------------------------------- */
15
16 /* -----------------------------------------------------------------------
17  * Portions of this file are Copyright (c) Jon Nylander
18  *
19  * jsTimezoneDetect is released under the MIT License
20  *  - http://www.opensource.org/licenses/mit-license.php
21  *
22  * For usage and examples, visit: http://pellepim.bitbucket.org/jstz/
23  * ----------------------------------------------------------------------- */
24 (function(e){var t=function(){"use strict";var e="s",n=function(e){var t=-e.getTimezoneOffset();return t!==null?t:0},r=function(e,t,n){var r=new Date;return e!==undefined&&r.setFullYear(e),r.setDate(n),r.setMonth(t),r},i=function(e){return n(r(e,0,2))},s=function(e){return n(r(e,5,2))},o=function(e){var t=e.getMonth()>7?s(e.getFullYear()):i(e.getFullYear()),r=n(e);return t-r!==0},u=function(){var t=i(),n=s(),r=i()-s();return r<0?t+",1":r>0?n+",1,"+e:t+",0"},a=function(){var e=u();return new t.TimeZone(t.olson.timezones[e])},f=function(e){var t=new Date(2010,6,15,1,0,0,0),n={"America/Denver":new Date(2011,2,13,3,0,0,0),"America/Mazatlan":new Date(2011,3,3,3,0,0,0),"America/Chicago":new Date(2011,2,13,3,0,0,0),"America/Mexico_City":new Date(2011,3,3,3,0,0,0),"America/Asuncion":new Date(2012,9,7,3,0,0,0),"America/Santiago":new Date(2012,9,3,3,0,0,0),"America/Campo_Grande":new Date(2012,9,21,5,0,0,0),"America/Montevideo":new Date(2011,9,2,3,0,0,0),"America/Sao_Paulo":new Date(2011,9,16,5,0,0,0),"America/Los_Angeles":new Date(2011,2,13,8,0,0,0),"America/Santa_Isabel":new Date(2011,3,5,8,0,0,0),"America/Havana":new Date(2012,2,10,2,0,0,0),"America/New_York":new Date(2012,2,10,7,0,0,0),"Asia/Beirut":new Date(2011,2,27,1,0,0,0),"Europe/Helsinki":new Date(2011,2,27,4,0,0,0),"Europe/Istanbul":new Date(2011,2,28,5,0,0,0),"Asia/Damascus":new Date(2011,3,1,2,0,0,0),"Asia/Jerusalem":new Date(2011,3,1,6,0,0,0),"Asia/Gaza":new Date(2009,2,28,0,30,0,0),"Africa/Cairo":new Date(2009,3,25,0,30,0,0),"Pacific/Auckland":new Date(2011,8,26,7,0,0,0),"Pacific/Fiji":new Date(2010,11,29,23,0,0,0),"America/Halifax":new Date(2011,2,13,6,0,0,0),"America/Goose_Bay":new Date(2011,2,13,2,1,0,0),"America/Miquelon":new Date(2011,2,13,5,0,0,0),"America/Godthab":new Date(2011,2,27,1,0,0,0),"Europe/Moscow":t,"Asia/Yekaterinburg":t,"Asia/Omsk":t,"Asia/Krasnoyarsk":t,"Asia/Irkutsk":t,"Asia/Yakutsk":t,"Asia/Vladivostok":t,"Asia/Kamchatka":t,"Europe/Minsk":t,"Australia/Perth":new Date(2008,10,1,1,0,0,0)};return n[e]};return{determine:a,date_is_dst:o,dst_start_for:f}}();t.TimeZone=function(e){"use strict";var n={"America/Denver":["America/Denver","America/Mazatlan"],"America/Chicago":["America/Chicago","America/Mexico_City"],"America/Santiago":["America/Santiago","America/Asuncion","America/Campo_Grande"],"America/Montevideo":["America/Montevideo","America/Sao_Paulo"],"Asia/Beirut":["Asia/Beirut","Europe/Helsinki","Europe/Istanbul","Asia/Damascus","Asia/Jerusalem","Asia/Gaza"],"Pacific/Auckland":["Pacific/Auckland","Pacific/Fiji"],"America/Los_Angeles":["America/Los_Angeles","America/Santa_Isabel"],"America/New_York":["America/Havana","America/New_York"],"America/Halifax":["America/Goose_Bay","America/Halifax"],"America/Godthab":["America/Miquelon","America/Godthab"],"Asia/Dubai":["Europe/Moscow"],"Asia/Dhaka":["Asia/Yekaterinburg"],"Asia/Jakarta":["Asia/Omsk"],"Asia/Shanghai":["Asia/Krasnoyarsk","Australia/Perth"],"Asia/Tokyo":["Asia/Irkutsk"],"Australia/Brisbane":["Asia/Yakutsk"],"Pacific/Noumea":["Asia/Vladivostok"],"Pacific/Tarawa":["Asia/Kamchatka"],"Africa/Johannesburg":["Asia/Gaza","Africa/Cairo"],"Asia/Baghdad":["Europe/Minsk"]},r=e,i=function(){var e=n[r],i=e.length,s=0,o=e[0];for(;s<i;s+=1){o=e[s];if(t.date_is_dst(t.dst_start_for(o))){r=o;return}}},s=function(){return typeof n[r]!="undefined"};return s()&&i(),{name:function(){return r}}},t.olson={},t.olson.timezones={"-720,0":"Etc/GMT+12","-660,0":"Pacific/Pago_Pago","-600,1":"America/Adak","-600,0":"Pacific/Honolulu","-570,0":"Pacific/Marquesas","-540,0":"Pacific/Gambier","-540,1":"America/Anchorage","-480,1":"America/Los_Angeles","-480,0":"Pacific/Pitcairn","-420,0":"America/Phoenix","-420,1":"America/Denver","-360,0":"America/Guatemala","-360,1":"America/Chicago","-360,1,s":"Pacific/Easter","-300,0":"America/Bogota","-300,1":"America/New_York","-270,0":"America/Caracas","-240,1":"America/Halifax","-240,0":"America/Santo_Domingo","-240,1,s":"America/Santiago","-210,1":"America/St_Johns","-180,1":"America/Godthab","-180,0":"America/Argentina/Buenos_Aires","-180,1,s":"America/Montevideo","-120,0":"Etc/GMT+2","-120,1":"Etc/GMT+2","-60,1":"Atlantic/Azores","-60,0":"Atlantic/Cape_Verde","0,0":"Etc/UTC","0,1":"Europe/London","60,1":"Europe/Berlin","60,0":"Africa/Lagos","60,1,s":"Africa/Windhoek","120,1":"Asia/Beirut","120,0":"Africa/Johannesburg","180,0":"Asia/Baghdad","180,1":"Europe/Moscow","210,1":"Asia/Tehran","240,0":"Asia/Dubai","240,1":"Asia/Baku","270,0":"Asia/Kabul","300,1":"Asia/Yekaterinburg","300,0":"Asia/Karachi","330,0":"Asia/Kolkata","345,0":"Asia/Kathmandu","360,0":"Asia/Dhaka","360,1":"Asia/Omsk","390,0":"Asia/Rangoon","420,1":"Asia/Krasnoyarsk","420,0":"Asia/Jakarta","480,0":"Asia/Shanghai","480,1":"Asia/Irkutsk","525,0":"Australia/Eucla","525,1,s":"Australia/Eucla","540,1":"Asia/Yakutsk","540,0":"Asia/Tokyo","570,0":"Australia/Darwin","570,1,s":"Australia/Adelaide","600,0":"Australia/Brisbane","600,1":"Asia/Vladivostok","600,1,s":"Australia/Sydney","630,1,s":"Australia/Lord_Howe","660,1":"Asia/Kamchatka","660,0":"Pacific/Noumea","690,0":"Pacific/Norfolk","720,1,s":"Pacific/Auckland","720,0":"Pacific/Tarawa","765,1,s":"Pacific/Chatham","780,0":"Pacific/Tongatapu","780,1,s":"Pacific/Apia","840,0":"Pacific/Kiritimati"},typeof exports!="undefined"?exports.jstz=t:e.jstz=t})(this);
25
26 /* session states */
27 var OSRF_APP_SESSION_CONNECTED = 0;
28 var OSRF_APP_SESSION_CONNECTING = 1;
29 var OSRF_APP_SESSION_DISCONNECTED = 2;
30
31 /* types of transport layers */
32 var OSRF_TRANSPORT_TYPE_XHR = 1;
33 var OSRF_TRANSPORT_TYPE_XMPP = 2;
34 var OSRF_TRANSPORT_TYPE_WS = 3;
35 var OSRF_TRANSPORT_TYPE_WS_SHARED = 4;
36
37 /* message types */
38 var OSRF_MESSAGE_TYPE_REQUEST = 'REQUEST';
39 var OSRF_MESSAGE_TYPE_STATUS = 'STATUS';
40 var OSRF_MESSAGE_TYPE_RESULT = 'RESULT';
41 var OSRF_MESSAGE_TYPE_CONNECT = 'CONNECT';
42 var OSRF_MESSAGE_TYPE_DISCONNECT = 'DISCONNECT';
43
44 /* message statuses */
45 var OSRF_STATUS_CONTINUE = 100;
46 var OSRF_STATUS_OK = 200;
47 var OSRF_STATUS_ACCEPTED = 202;
48 var OSRF_STATUS_NOCONTENT = 204;
49 var OSRF_STATUS_COMPLETE = 205;
50 var OSRF_STATUS_PARTIAL = 206;
51 var OSRF_STATUS_REDIRECTED = 307;
52 var OSRF_STATUS_BADREQUEST = 400;
53 var OSRF_STATUS_UNAUTHORIZED = 401;
54 var OSRF_STATUS_FORBIDDEN = 403;
55 var OSRF_STATUS_NOTFOUND = 404;
56 var OSRF_STATUS_NOTALLOWED = 405;
57 var OSRF_STATUS_TIMEOUT = 408;
58 var OSRF_STATUS_EXPFAILED = 417;
59 var OSRF_STATUS_INTERNALSERVERERROR = 500;
60 var OSRF_STATUS_NOTIMPLEMENTED = 501;
61 var OSRF_STATUS_SERVICEUNAVAILABLE = 503;
62 var OSRF_STATUS_VERSIONNOTSUPPORTED = 505;
63
64 // TODO: get path from ./configure prefix
65 var SHARED_WORKER_LIB = '/js/dojo/opensrf/opensrf_ws_shared.js'; 
66
67 /* The following classes map directly to network-serializable opensrf objects */
68
69 function osrfMessage(hash) {
70     this.hash = hash;
71     if(!this.hash.locale)
72         this.hash.locale = OpenSRF.locale || 'en-US';
73     if(!this.hash.tz)
74         this.hash.tz = jstz.determine().name()
75     this._encodehash = true;
76 }
77 osrfMessage.prototype.threadTrace = function(d) { 
78     if(arguments.length == 1) 
79         this.hash.threadTrace = d; 
80     return this.hash.threadTrace; 
81 };
82 osrfMessage.prototype.type = function(d) { 
83     if(arguments.length == 1) 
84         this.hash.type = d; 
85     return this.hash.type; 
86 };
87 osrfMessage.prototype.payload = function(d) { 
88     if(arguments.length == 1) 
89         this.hash.payload = d; 
90     return this.hash.payload; 
91 };
92 osrfMessage.prototype.locale = function(d) { 
93     if(arguments.length == 1) 
94         this.hash.locale = d; 
95     return this.hash.locale; 
96 };
97 osrfMessage.prototype.tz = function(d) { 
98     if(arguments.length == 1) 
99         this.hash.tz = d; 
100     return this.hash.tz; 
101 };
102 osrfMessage.prototype.api_level = function(d) { 
103     if(arguments.length == 1) 
104         this.hash.api_level = d; 
105     return this.hash.api_level; 
106 };
107 osrfMessage.prototype.serialize = function() {
108     return {
109         "__c":"osrfMessage",
110         "__p": {
111             'threadTrace' : this.hash.threadTrace,
112             'type' : this.hash.type,
113             'payload' : (this.hash.payload) ? this.hash.payload.serialize() : 'null',
114             'locale' : this.hash.locale,
115             'tz' : this.hash.tz,
116             'api_level' : this.hash.api_level
117         }
118     };
119 };
120
121 function osrfMethod(hash) {
122     this.hash = hash;
123     this._encodehash = true;
124 }
125 osrfMethod.prototype.method = function(d) {
126     if(arguments.length == 1) 
127         this.hash.method = d; 
128     return this.hash.method; 
129 };
130 osrfMethod.prototype.params = function(d) {
131     if(arguments.length == 1) 
132         this.hash.params = d; 
133     return this.hash.params; 
134 };
135 osrfMethod.prototype.serialize = function() {
136     return {
137         "__c":"osrfMethod",
138         "__p": {
139             'method' : this.hash.method,
140             'params' : this.hash.params
141         }
142     };
143 };
144
145 function osrfMethodException(hash) {
146     this.hash = hash;
147     this._encodehash = true;
148 }
149 osrfMethodException.prototype.status = function(d) {
150     if(arguments.length == 1) 
151         this.hash.status = d; 
152     return this.hash.status; 
153 };
154 osrfMethodException.prototype.statusCode = function(d) {
155     if(arguments.length == 1) 
156         this.hash.statusCode = d; 
157     return this.hash.statusCode; 
158 };
159 function osrfConnectStatus(hash) { 
160     this.hash = hash;
161     this._encodehash = true;
162 }
163 osrfConnectStatus.prototype.status = function(d) {
164     if(arguments.length == 1) 
165         this.hash.status = d; 
166     return this.hash.status; 
167 };
168 osrfConnectStatus.prototype.statusCode = function(d) {
169     if(arguments.length == 1) 
170         this.hash.statusCode = d; 
171     return this.hash.statusCode; 
172 };
173 function osrfResult(hash) {
174     this.hash = hash;
175     this._encodehash = true;
176 }
177 osrfResult.prototype.status = function(d) {
178     if(arguments.length == 1) 
179         this.hash.status = d; 
180     return this.hash.status; 
181 };
182 osrfResult.prototype.statusCode = function(d) {
183     if(arguments.length == 1) 
184         this.hash.statusCode = d; 
185     return this.hash.statusCode; 
186 };
187 osrfResult.prototype.content = function(d) {
188     if(arguments.length == 1) 
189         this.hash.content = d; 
190     return this.hash.content; 
191 };
192 function osrfResultPartial(hash) {
193     this.hash = hash;
194     this._encodehash = true;
195 }
196 osrfResultPartial.prototype.status = function(d) {
197     if(arguments.length == 1) 
198         this.hash.status = d; 
199     return this.hash.status; 
200 };
201 osrfResultPartial.prototype.statusCode = function(d) {
202     if(arguments.length == 1) 
203         this.hash.statusCode = d; 
204     return this.hash.statusCode; 
205 };
206 osrfResultPartial.prototype.content = function(d) {
207     if(arguments.length == 1) 
208         this.hash.content = d; 
209     return this.hash.content; 
210 };
211 function osrfResultPartialComplete(hash) {
212     this.hash = hash;
213     this._encodehash = true;
214 }
215 osrfResultPartialComplete.prototype.status = function(d) {
216     if(arguments.length == 1) 
217         this.hash.status = d; 
218     return this.hash.status; 
219 };
220 osrfResultPartialComplete.prototype.statusCode = function(d) {
221     if(arguments.length == 1) 
222         this.hash.statusCode = d; 
223     return this.hash.statusCode; 
224 };
225 osrfResultPartialComplete.prototype.content = function(d) {
226     if(arguments.length == 1) 
227         this.hash.content = d; 
228     return this.hash.content; 
229 };
230
231 function osrfServerError(hash) { 
232     this.hash = hash;
233     this._encodehash = true;
234 }
235 osrfServerError.prototype.status = function(d) {
236     if(arguments.length == 1) 
237         this.hash.status = d; 
238     return this.hash.status; 
239 };
240 osrfServerError.prototype.statusCode = function(d) {
241     if(arguments.length == 1) 
242         this.hash.statusCode = d; 
243     return this.hash.statusCode; 
244 };
245 function osrfContinueStatus(hash) { 
246     this.hash = hash;
247     this._encodehash = true;
248 }
249 osrfContinueStatus.prototype.status = function(d) {
250     if(arguments.length == 1) 
251         this.hash.status = d; 
252     return this.hash.status; 
253 };
254 osrfContinueStatus.prototype.statusCode = function(d) {
255     if(arguments.length == 1) 
256         this.hash.statusCode = d; 
257     return this.hash.statusCode; 
258 };
259
260 OpenSRF = {};
261 OpenSRF.tz = jstz.determine().name();
262 OpenSRF.locale = null;
263 OpenSRF.api_level = 1;
264
265 /* makes cls a subclass of pcls */
266 OpenSRF.set_subclass = function(cls, pcls) {
267     var str = cls+'.prototype = new '+pcls+'();';
268     str += cls+'.prototype.constructor = '+cls+';';
269     str += cls+'.baseClass = '+pcls+'.prototype.constructor;';
270     str += cls+'.prototype["super"] = '+pcls+'.prototype;';
271     eval(str);
272 };
273
274
275 /* general session superclass */
276 OpenSRF.Session = function() {
277     this.remote_id = null;
278     this.state = OSRF_APP_SESSION_DISCONNECTED;
279 };
280
281 OpenSRF.Session.transport = OSRF_TRANSPORT_TYPE_XHR;
282 OpenSRF.Session.cache = {};
283
284 OpenSRF.Session.find_session = function(thread_trace) {
285     return OpenSRF.Session.cache[thread_trace];
286 };
287 OpenSRF.Session.prototype.cleanup = function() {
288     delete OpenSRF.Session.cache[this.thread];
289 };
290
291 OpenSRF.Session.prototype.send = function(osrf_msg, args) {
292     args = (args) ? args : {};
293     switch(OpenSRF.Session.transport) {
294         case OSRF_TRANSPORT_TYPE_WS:
295             return this.send_ws(osrf_msg);
296         case OSRF_TRANSPORT_TYPE_WS_SHARED:
297             return this.send_ws_shared(osrf_msg);
298         case OSRF_TRANSPORT_TYPE_XHR:
299             return this.send_xhr(osrf_msg, args);
300         case OSRF_TRANSPORT_TYPE_XMPP:
301             return this.send_xmpp(osrf_msg, args);
302     }
303 };
304
305 OpenSRF.Session.prototype.send_xhr = function(osrf_msg, args) {
306     args.thread = this.thread;
307     args.rcpt = this.remote_id;
308     args.rcpt_service = this.service;
309     new OpenSRF.XHRequest(osrf_msg, args).send();
310 };
311
312 OpenSRF.websocketConnected = function() {
313     return OpenSRF.sharedWebsocketConnected || (
314         OpenSRF.websocketConnection && 
315         OpenSRF.websocketConnection.connected()
316     );
317 }
318
319 OpenSRF.Session.prototype.send_ws = function(osrf_msg) {
320
321     // XXX there appears to be a bug in Chromium where loading the
322     // same page multiple times (without a refresh or cache clear)
323     // causes the SharedWorker to fail to instantiate on 
324     // every other page load.  Disabling SharedWorker's entirely
325     // for now.
326     if (false /* ^-- */ && typeof SharedWorker == 'function' 
327
328         /*
329          * https://bugzilla.mozilla.org/show_bug.cgi?id=504553#c73
330          * Firefox does not yet support WebSockets in worker threads
331          */
332         && !navigator.userAgent.match(/Firefox/)
333     ) {
334         // vanilla websockets requested, but this browser supports
335         // shared workers, so use those instead.
336         return this.send_ws_shared(osrf_msg);
337     }
338
339     // otherwise, use a per-tab connection
340
341     if (!OpenSRF.websocketConnection) {
342         this.setup_single_ws();
343     }
344
345     var json = js2JSON({
346         service : this.service,
347         thread : this.thread,
348         osrf_msg : [osrf_msg.serialize()]
349     });
350
351     OpenSRF.websocketConnection.send(json);
352 };
353
354 OpenSRF.Session.prototype.setup_single_ws = function() {
355     OpenSRF.websocketConnection = new OpenSRF.WebSocket();
356
357     OpenSRF.websocketConnection.onmessage = function(msg) {
358         try {
359             var msg = JSON2js(msg);
360         } catch(E) {
361             console.error(
362                 "Error parsing JSON in shared WS response: " + msg);
363             throw E;
364         }
365
366         if (msg.transport_error) {
367             // Websockets gateway returns bounced messages (e.g. for
368             // requets to unavailable services) with a transport_error
369             // flag set.  
370             console.error(
371                 'Websocket request failed with a transport error', msg);
372
373             var ses = OpenSRF.Session.find_session(msg.thread); 
374             if (ses) {
375                 if (msg.osrf_msg && msg.osrf_msg[0]) {
376                     var req = ses.find_request(msg.osrf_msg[0].threadTrace());
377                     if (req) {
378                         var handler = req.ontransporterror || req.onerror;
379                         if (handler) {
380                             handler('Service ' + ses.service + ' unavailable');
381                         }
382                     }
383                 }
384             }
385             return; // No viable error handlers
386         }
387
388         OpenSRF.Stack.push(                                                        
389             new OpenSRF.NetMessage(                                                
390                null, null, msg.thread, null, msg.osrf_msg)                        
391         ); 
392
393         return;
394     }
395 }
396
397 OpenSRF.Session.setup_shared_ws = function() {
398     OpenSRF.Session.transport = OSRF_TRANSPORT_TYPE_WS_SHARED;
399
400     OpenSRF.sharedWSWorker = new SharedWorker(SHARED_WORKER_LIB);
401
402     OpenSRF.sharedWSWorker.port.addEventListener('message', function(e) {                          
403         var data = e.data;
404
405         if (data.action == 'message') {
406             // pass all inbound message up the opensrf stack
407
408             OpenSRF.sharedWebsocketConnected = true;
409             var msg;
410             try {
411                 msg = JSON2js(data.message);
412             } catch(E) {
413                 console.error(
414                     "Error parsing JSON in shared WS response: " + msg);
415                 throw E;
416             }
417             OpenSRF.Stack.push(                                                        
418                 new OpenSRF.NetMessage(                                                
419                    null, null, msg.thread, null, msg.osrf_msg)                        
420             ); 
421
422             return;
423         }
424
425
426         if (data.action == 'event') {
427             if (data.type.match(/onclose|onerror/)) {
428                 OpenSRF.sharedWebsocketConnected = false;
429                 if (OpenSRF.onWebSocketClosed)
430                     OpenSRF.onWebSocketClosed();
431                 if (data.type.match(/onerror/)) 
432                     throw new Error(data.message);
433             }
434         }
435     });
436
437     OpenSRF.sharedWSWorker.port.start();   
438 }
439
440 OpenSRF.Session.prototype.send_ws_shared = function(message) {
441
442     if (!OpenSRF.sharedWSWorker) 
443         OpenSRF.Session.setup_shared_ws();
444
445     var json = js2JSON({
446         service : this.service,
447         thread : this.thread,
448         osrf_msg : [message.serialize()]
449     });
450
451     OpenSRF.sharedWSWorker.port.postMessage({
452         action : 'message', 
453         // pass the thread additionally as a stand-alone value so the
454         // worker can more efficiently inspect it.
455         thread : this.thread,
456         message : json
457     });
458 }
459
460
461 OpenSRF.Session.prototype.send_xmpp = function(osrf_msg, args) {
462     alert('xmpp transport not implemented');
463 };
464
465
466 /* client sessions make requests */
467 OpenSRF.ClientSession = function(service) {
468     this.service = service;
469     this.remote_id = null;
470     this.locale = OpenSRF.locale || 'en-US';
471     this.tz = OpenSRF.tz;
472     this.last_id = 0;
473     this.requests = [];
474     this.onconnect = null;
475     this.thread = Math.random() + '' + new Date().getTime();
476     OpenSRF.Session.cache[this.thread] = this;
477 };
478 OpenSRF.set_subclass('OpenSRF.ClientSession', 'OpenSRF.Session');
479
480
481 OpenSRF.ClientSession.prototype.connect = function(args) {
482     args = (args) ? args : {};
483     this.remote_id = null;
484
485     if (this.state == OSRF_APP_SESSION_CONNECTED) {
486         if (args.onconnect) args.onconnect();
487         return true;
488     }
489
490     if(args.onconnect) {
491         this.onconnect = args.onconnect;
492
493     } else {
494         /* if no handler is provided, make this a synchronous call */
495         this.timeout = (args.timeout) ? args.timeout : 5;
496     }
497
498     message = new osrfMessage({
499         'threadTrace' : this.last_id++, 
500         'type' : OSRF_MESSAGE_TYPE_CONNECT
501     });
502
503     this.send(message, {'timeout' : this.timeout});
504
505     if(this.onconnect || this.state == OSRF_APP_SESSION_CONNECTED)
506         return true;
507
508     return false;
509 };
510
511 OpenSRF.ClientSession.prototype.disconnect = function(args) {
512
513     if (this.state == OSRF_APP_SESSION_CONNECTED) {
514         this.send(
515             new osrfMessage({
516                 'threadTrace' : this.last_id++,
517                 'type' : OSRF_MESSAGE_TYPE_DISCONNECT
518             })
519         );
520     }
521
522     this.remote_id = null;
523     this.state = OSRF_APP_SESSION_DISCONNECTED;
524 };
525
526
527 OpenSRF.ClientSession.prototype.request = function(args) {
528     
529     if(this.state != OSRF_APP_SESSION_CONNECTED)
530         this.remote_id = null;
531         
532     if(typeof args == 'string') { 
533         params = [];
534         for(var i = 1; i < arguments.length; i++)
535             params.push(arguments[i]);
536
537         args = {
538             method : args, 
539             params : params
540         };
541     } else {
542         if(typeof args == 'undefined')
543             args = {};
544     }
545
546     var req = new OpenSRF.Request(this, this.last_id++, args);
547     this.requests.push(req);
548     return req;
549 };
550
551 OpenSRF.ClientSession.prototype.find_request = function(reqid) {
552     for(var i = 0; i < this.requests.length; i++) {
553         var req = this.requests[i];
554         if(req.reqid == reqid)
555             return req;
556     }
557     return null;
558 };
559
560 OpenSRF.Request = function(session, reqid, args) {
561     this.session = session;
562     this.reqid = reqid;
563
564     /* callbacks */
565     this.onresponse = args.onresponse;
566     this.oncomplete = args.oncomplete;
567     this.onerror = args.onerror;
568     this.onmethoderror = args.onmethoderror;
569     this.ontransporterror = args.ontransporterror;
570
571     this.method = args.method;
572     this.params = args.params;
573     this.timeout = args.timeout;
574     this.api_level = args.api_level || OpenSRF.api_level;
575     this.response_queue = [];
576     this.part_response_buffer = '';
577     this.complete = false;
578 };
579
580 OpenSRF.Request.prototype.peek_last = function(timeout) {
581     if(this.response_queue.length > 0) {
582         var x = this.response_queue.pop();
583         this.response_queue.push(x);
584         return x;
585     }
586     return null;
587 };
588
589 OpenSRF.Request.prototype.peek = function(timeout) {
590     if(this.response_queue.length > 0)
591         return this.response_queue[0];
592     return null;
593 };
594
595 OpenSRF.Request.prototype.recv = function(timeout) {
596     if(this.response_queue.length > 0)
597         return this.response_queue.shift();
598     return null;
599 };
600
601 OpenSRF.Request.prototype.send = function() {
602     method = new osrfMethod({'method':this.method, 'params':this.params});
603     message = new osrfMessage({
604         'threadTrace' : this.reqid, 
605         'type' : OSRF_MESSAGE_TYPE_REQUEST, 
606         'payload' : method, 
607         'locale' : this.session.locale,
608         'tz' : this.session.tz,
609         'api_level' : this.api_level
610     });
611
612     this.session.send(message, {
613         'timeout' : this.timeout,
614         'onresponse' : this.onresponse,
615         'oncomplete' : this.oncomplete,
616         'onerror' : this.onerror,
617         'onmethoderror' : this.onmethoderror,
618         'ontransporterror' : this.ontransporterror
619     });
620 };
621
622 OpenSRF.NetMessage = function(to, from, thread, body, osrf_msg) {
623     this.to = to;
624     this.from = from;
625     this.thread = thread;
626     this.body = body;
627     this.osrf_msg = osrf_msg;
628 };
629
630 OpenSRF.Stack = function() {
631 };
632
633 // global inbound message queue
634 OpenSRF.Stack.queue = [];
635
636 // XXX testing
637 function log(msg) {
638     try {
639         dump(msg + '\n'); // xulrunner
640     } catch(E) {
641         console.log(msg);
642     }
643 }
644
645 // ses may be passed to us by the network handler
646 OpenSRF.Stack.push = function(net_msg, callbacks) {
647     var ses = OpenSRF.Session.find_session(net_msg.thread); 
648     if (!ses) return;
649     ses.remote_id = net_msg.from;
650
651     // NetMessage's from websocket connections are parsed before they get here
652     osrf_msgs = net_msg.osrf_msg;
653
654     if (!osrf_msgs) {
655
656         try {
657             osrf_msgs = JSON2js(net_msg.body);
658
659             // TODO: pretty sure we don't need this..
660             if (OpenSRF.Session.transport == OSRF_TRANSPORT_TYPE_WS) {
661                 // WebSocketRequests wrap the content
662                 osrf_msgs = osrf_msgs.osrf_msg;
663             }
664
665         } catch(E) {
666             log('Error parsing OpenSRF message body as JSON: ' + net_msg.body + '\n' + E);
667
668             /** UGH
669               * For unknown reasons, the Content-Type header will occasionally
670               * be included in the XHR.responseText for multipart/mixed messages.
671               * When this happens, strip the header and newlines from the message
672               * body and re-parse.
673               */
674             net_msg.body = net_msg.body.replace(/^.*\n\n/, '');
675             log('Cleaning up and retrying...');
676
677             try {
678                 osrf_msgs = JSON2js(net_msg.body);
679             } catch(E2) {
680                 log('Unable to clean up message, giving up: ' + net_msg.body);
681                 return;
682             }
683         }
684     }
685
686     // push the latest responses onto the end of the inbound message queue
687     for(var i = 0; i < osrf_msgs.length; i++)
688         OpenSRF.Stack.queue.push({msg : osrf_msgs[i], ses : ses});
689
690     // continue processing responses, oldest to newest
691     while(OpenSRF.Stack.queue.length) {
692         var data = OpenSRF.Stack.queue.shift();
693         OpenSRF.Stack.handle_message(data.ses, data.msg);
694     }
695 };
696
697 OpenSRF.Stack.handle_message = function(ses, osrf_msg) {
698     
699     var req = ses.find_request(osrf_msg.threadTrace());
700
701     var payload = osrf_msg.payload();
702     var status = payload.statusCode();
703     var status_text = payload.status();
704
705     if(osrf_msg.type() == OSRF_MESSAGE_TYPE_STATUS) {
706
707
708         if(status == OSRF_STATUS_COMPLETE) {
709             if(req) {
710                 req.complete = true;
711                 if(req.oncomplete && !req.oncomplete_called) {
712                     req.oncomplete_called = true;
713                     return req.oncomplete(req);
714                 }
715             }
716         }
717
718         if(status == OSRF_STATUS_OK) {
719             ses.state = OSRF_APP_SESSION_CONNECTED;
720
721             /* call the connect callback */
722             if(ses.onconnect && !ses.onconnect_called) {
723                 ses.onconnect_called = true;
724                 return ses.onconnect();
725             }
726         }
727
728         // capture all 400's and 500's as method errors
729         if ((status+'').match(/^4/) || (status+'').match(/^5/)) {
730             if(req && req.onmethoderror) 
731                 return req.onmethoderror(req, status, status_text);
732         }
733     }
734
735     if(osrf_msg.type() == OSRF_MESSAGE_TYPE_RESULT) {
736         req = ses.find_request(osrf_msg.threadTrace());
737         if(req) {
738             if (status == OSRF_STATUS_PARTIAL) {
739                 req.part_response_buffer += payload.content()
740                 return; // we're just collecting a big chunked payload
741             } else if (status == OSRF_STATUS_NOCONTENT) {
742                 payload.content( JSON2js(req.part_response_buffer) );
743                 payload.statusCode( OSRF_STATUS_OK );
744                 req.part_response_buffer = '';
745             }
746             req.response_queue.push(payload);
747             if(req.onresponse) 
748                 return req.onresponse(req);
749         }
750     }
751 };
752
753