1 //
  2 // Copyright 2010, Tridium, Inc. All Rights Reserved.
  3 //
  4 
  5 /**
  6  * Network Communications for BajaScript.
  7  *
  8  * @author Gareth Johnson
  9  * @version 1.0.0.0
 10  */
 11 
 12 //JsLint options (see http://www.jslint.com )
 13 /*jslint rhino: true, onevar: false, plusplus: true, white: true, undef: false, nomen: false, eqeqeq: true,
 14 bitwise: true, regexp: true, newcap: true, immed: true, strict: false, indent: 2, vars: true, continue: true */
 15 
 16 // Globals for JsLint to ignore 
 17 /*global baja, JSON, BaseBajaObj, clearInterval, setInterval*/ 
 18  
 19 (function comm(baja, BaseBajaObj) {
 20 
 21   // Use ECMAScript 5 Strict Mode
 22   "use strict";
 23   
 24   // Create local for improved minification
 25   var strictArg = baja.strictArg,
 26       bajaDef = baja.def,
 27       Callback;
 28   
 29   /**
 30    * @namespace Baja Communications
 31    */
 32   baja.comm = new BaseBajaObj();
 33 
 34   ////////////////////////////////////////////////////////////////
 35   // Comms
 36   //////////////////////////////////////////////////////////////// 
 37   
 38   var requestIdCounter = 0, // Number used for generate unique response ids in BOX Messages
 39       pollRate = 2500, // Rate at which the Server Session is polled for events
 40       serverSession = null, // The main server session
 41       pollTicket = baja.clock.expiredTicket, // The ticket used to poll for events in the comms layer
 42       eventHandlers = {}, // Server Session event handlers
 43       commFail = function (err) {   // Comm fail handler
 44         baja.outln("Comms failed: " + err.name);
 45         if (!err.noReconnect) {
 46           baja.outln("Attempting reconnect...");
 47         }
 48       };
 49   
 50   /**
 51    * Set the comm fail function that gets called when the 
 52    * communications layer of BajaScript fails. 
 53    *
 54    * @name baja.comm.setCommFailCallback
 55    * @function
 56    *
 57    * @param func the comm fail error
 58    */
 59   baja.comm.setCommFailCallback = function (func) {
 60     strictArg(func, Function);
 61     commFail = func;
 62   };
 63   
 64   /**
 65    * Attempts a reconnection
 66    *
 67    * @private
 68    */
 69   baja.comm.reconnect = function () {
 70     throw new Error("baja.comm.reconnect not implemented");
 71   };
 72   
 73   function serverCommFail(err) {
 74     // If BajaScript has stopped then don't try to reconnect...
 75     if (baja.isStopping()) {
 76       return;
 77     }
 78   
 79     // Nullify the server session as this is no longer valid...
 80     serverSession = null;
 81   
 82     try {
 83       // Signal that comms have failed
 84       commFail(err);
 85     }
 86     catch (ignore) {}
 87     
 88     // Stop any further polling...
 89     pollTicket.cancel();
 90             
 91     function detectServer() {      
 92       var cb = new Callback(function ok() {
 93         // If we can get a connection then reconnect
 94         baja.comm.reconnect();
 95       },
 96       function fail(err) {
 97         if (!err.noReconnect) {
 98           // If the BOX Service is unavailable or we can't connect at all then schedule another
 99           // server test to detect when it comes back online...
100           if (err.delayReconnect) {   
101             // Schedule another test to try and detect the Server since we've got nothing back...
102             baja.clock.schedule(detectServer, 1000);  
103           }
104           else {
105             // If we've got some sort of status code back then the server could be there
106             // so best attempt a refresh          
107             baja.comm.reconnect();
108           }
109         }
110       });
111       
112       cb.addReq("sys", "hello", {});
113       cb.commit();
114     }
115     
116     // Unless specified otherwise, attempt a reconnect...
117     if (!err.noReconnect) {
118       detectServer(); 
119     }
120   }
121   
122   ////////////////////////////////////////////////////////////////
123   // BOX
124   ////////////////////////////////////////////////////////////////
125     
126   // BOX - Baja Object eXchange
127   // Define Communications layer
128   
129   /*
130   // A BOX frame
131   var boxFrame = {
132     "p": "box", // Protocol
133     "d": "stationName" // Destination (if blank, null or undefined, it's for the locally connected host/station)
134     "v": 1,     // Version
135     "m": [ {    // A frame contains an array of messages
136         // The body of each message (response must have some order and number for each message)
137         {
138           "r": 0,      // Response id (used to associate requests with responses). Used when HTTP isn't. -1 === unsolicited.
139           "t": "rt",   // Type (rt=request, rp=response, e=response error, u=unsolicited, )
140           "c": "sys",      // Channel (pluggable comms model)
141           "k": "getTypes", // Key in Channel
142           "b": {           // Message body
143           }  
144         }
145       }
146     ]
147   };
148   */
149 
150   var defaultErrorMessage = "Default Message";
151   
152   /**
153    * A Server Error.
154    * 
155    * @class
156    * @private
157    * @extends Error
158    * @param [message] the error message
159    */
160   baja.comm.ServerError = function (message) {
161     this.name = "ServerError";
162     this.message = message || defaultErrorMessage;
163   }.$extend(Error);
164   
165   /**
166    * A BOX Error.
167    * <p>
168    * A BOX Error is one that originated from the Server using the BOX protocol.
169    * 
170    * @class
171    * @private
172    * @extends baja.comm.ServerError
173    * @param [errorType] the type of error
174    * @param [message] the error message
175    */
176   baja.comm.BoxError = function (errorType, message) {
177     this.name = errorType;
178     this.message = message || defaultErrorMessage;
179   }.$extend(baja.comm.ServerError);
180   
181   /**
182    * A BOX frame to be stringified and sent up to the station.
183    * 
184    * @class
185    * @name baja.comm.BoxFrame
186    * @private
187    * @param {Array} messageQueue a queue of BOX messages.
188    */
189   baja.comm.BoxFrame = function (messageQueue) {
190     var msgs = [],
191         i;
192     
193     for (i = 0; i < messageQueue.length; ++i) {
194       if (messageQueue[i].m) {
195         msgs.push(messageQueue[i].m);
196       }
197     }
198         
199     this.$body = {
200         p: 'box', // protocol
201         v: 1,     // version
202         m: msgs   // messages (actual messages from a MessageQueue - no callbacks)
203     };
204   };
205   
206   /**
207    * Returns a JSON-stringified representation of this BOX frame, ready to be
208    * sent up to the station as-is.
209    *
210    * @returns {String} a string representation of this BOX frame in a format
211    * expected by the BOX servlet on the station.
212    */
213   baja.comm.BoxFrame.prototype.toString = function () {
214     return JSON.stringify(this.$body);
215   };
216   
217   /**
218    * Checks to see if this frame has no actual messages to send.
219    *
220    * @returns {Boolean} true if this frame has no messages.
221    */
222   baja.comm.BoxFrame.prototype.isEmpty = function () {
223     return this.$body.m.length === 0;
224   };
225   
226   /**
227    * Sends this frame up to the station. This is an override hook and MUST be
228    * implemented by a utility class; e.g. <code>browser.js</code> will
229    * implement this by using XMLHttpRequest to send this frame to the station
230    * via POST. This function will be called from 
231    * <code>baja.comm.Batch#doCommit</code> so any batch properties on the
232    * callback object do not need to be taken into account - just ok/fail.
233    * 
234    *
235    * @param {Boolean} async true if this frame should be sent asynchronously.
236    * @param {baja.comm.Batch|baja.comm.Callback|Object} callback an object
237    * containing ok/fail callbacks.
238    */
239   baja.comm.BoxFrame.prototype.send = function (async, callback) {
240     throw new Error("baja.comm.BoxFrame#send not implemented");
241   };
242      
243   /**
244    * @class Batch Network Call Capturing.
245    * <p>
246    * A Batch object can be used to batch up a number of network calls into 
247    * one network call. It's used for network efficient remote programming in
248    * BajaScript.
249    * <p>
250    * When specified, a batch object is typically an optional argument in a method
251    * that could make a network call.
252    * <pre>
253    *   var b = new baja.comm.Batch();
254    *   
255    *   // Invoke a number of Component Actions in one network call...
256    *   myComp.foo({batch: b});
257    *   myComp2.foo2({batch: b});
258    *
259    *   // Make a single network call that will invoke both Actions in one go...
260    *   b.commit();
261    * </pre>
262    *
263    * @name baja.comm.Batch
264    * @extends BaseBajaObj
265    */
266   baja.comm.Batch = function () {
267     this.$queue = [];
268     this.$committed = false;
269     this.$async = true;
270   }.$extend(BaseBajaObj);
271       
272   /**
273    * Add a BOX request message to the Batch Buffer.
274    * <p>
275    * This method is used internally by BajaScript.
276    *
277    * @private
278    *
279    * @param {String} channel  the BOX Channel name.
280    * @param {String} key  the BOX key in the Channel.
281    * @param {Object} body  the object that will be encoded to JSON set over the network.
282    * @param {baja.comm.Callback} callback  the callback. 'ok' or 'fail' is called on this object once network operations have completed.
283    */
284   baja.comm.Batch.prototype.addReq = function (channel, key, body, callback) {
285     if (this.$committed) {
286       throw new Error("Cannot add request to a commited Batch!");
287     }
288             
289     var m = { r:  requestIdCounter++,
290               t:  "rt",
291               c:  channel,
292               k:  key,
293               b:  body };
294                     
295     // Add messages
296     this.$queue.push({m: m, cb: callback });
297   };
298   
299   /**
300    * Add a Callback.
301    * <p>
302    * This adds a callback into the batch queue without a message to be sent. This is useful if a callback
303    * needs to be made halfway through batch processing.
304    * <p>
305    * Please note, this is a private method that's only recommended for use by Tridium developers!
306    *
307    * @private
308    *
309    * @param {Function} [ok]  the ok callback.
310    * @param {Function} [fail] the fail callback.
311    */
312   baja.comm.Batch.prototype.addCallback = function (ok, fail) {
313     if (this.$committed) {
314       throw new Error("Cannot add callback to a commited Batch!");
315     }
316       
317     // Add callback to outgoing messages
318     this.$queue.push({m: null, cb: new Callback(ok, fail)});
319   };
320   
321   function batchOk(batch, i, resp) {
322     // Bail if there's nothing more in the queue
323     if (i >= batch.$queue.length) {
324       return;
325     }
326     
327     var respMsg = null,
328         m = batch.$queue[i].m,   // Message
329         cb = batch.$queue[i].cb, // Callback
330         j,
331         boxError;
332     
333     try {
334       /**
335        * @ignore - When ok or fail has been called on this callback, process the next item in the queue
336        */
337       cb.$batchNext = function () {
338         cb.$batchNext = baja.noop;
339         batchOk(batch, ++i, resp);
340       };      
341       
342       // For callbacks that didn't have messages just call the ok handler
343       if (!m) {
344         try {
345           cb.ok();
346           return;
347         }
348         catch (notFoundErr) {
349           cb.fail(notFoundErr); 
350         }
351       }
352       
353       // Skip messages that weren't requests
354       if (m.t !== "rt") {
355         cb.ok();
356         return;
357       }
358       
359       // For messages that were requests, attempt to match a response
360       for (j = 0 ; j < resp.m.length; ++j) {
361         // Match up the request number      
362         if (resp.m[j].r === m.r) {
363           respMsg = resp.m[j];
364           break;
365         }
366       }
367     
368       // Process message if found
369       if (respMsg !== null) {
370         if (respMsg.t === "rp") {
371           // If we have a valid response then make the ok callback
372           try {
373             cb.ok(respMsg.b);
374           }
375           catch (okErr) {
376             cb.fail(okErr); 
377           }
378         }
379         else if (respMsg.t === "e") {
380           boxError = new baja.comm.BoxError(respMsg.et, respMsg.b);
381           
382           // Indicates the comms have completely failed...
383           if (respMsg.cf) {
384             // If the comms have failed then don't bother trying to reconnect
385             boxError.noReconnect = true;
386             
387             // Flag up some more information about the BOX Error
388             boxError.sessionLimit = respMsg.et === "BoxSessionLimitError";
389             boxError.fatalFault = respMsg.et === "BoxFatalFaultError";
390             boxError.nonOperational = respMsg.et === "BoxNonOperationalError";
391             
392             serverCommFail(boxError);
393           }
394           
395           // If we have a response error then make the fail callback
396           cb.fail(boxError);
397         }
398         else {
399           cb.fail(new Error("Fatal error reading BOX Frame: " + JSON.stringify(resp)));
400         }
401       }
402       else {
403         // If a response can't be found then invoke then invoke the failure callback
404         cb.fail(new Error("BOX Error: response not found for request: " + JSON.stringify(m)));
405       }
406     }
407     catch (failError) {
408       baja.error(failError);
409     }
410   }
411   
412   function batchFail(batch, i, err) {
413     // Bail if there's nothing more in the queue
414     if (i >= batch.$queue.length) {
415       return;
416     }
417     
418     var cb = batch.$queue[i].cb;
419     
420     try {
421       /**
422        * @ignore - When ok or fail has been called on this callback, process the next item in the queue
423        */
424       cb.$batchNext = function () {
425         cb.$batchNext = baja.noop;
426         batchFail(batch, ++i, err);
427       };
428       
429       cb.fail(err);
430     }
431     catch (failError) {
432       baja.error(failError);
433     }
434   }
435   
436   /**
437    * Performs the actual commit work for the <code>commit</code> and
438    * <code>commitSync</code> functions. Checks to see if this batch's queue
439    * contains any actual BOX messages that need to be sent to the station. 
440    * If not, it simply calls all the <code>ok</code> callbacks the queue
441    * contains. If there are messages to send, it assembles them into a
442    * <code>baja.comm.BoxFrame</code> and sends them up.
443    *
444    * @name baja.comm.Batch-doCommit
445    * @function
446    * 
447    * @private
448    * @inner
449    *
450    * @param {Boolean} async determines whether to send the frame to the station
451    * asynchronously (only applicable if there are actual messages to send).
452    */
453   var doCommit = function (async) {
454     // If BajaScript has fully stopped then don't send anymore comms requests...
455     if (baja.isStopped()) {
456       return;
457     }
458   
459     if (this.$committed) {
460       throw new Error("Cannot commit batch that's already committed!");
461     }
462   
463     this.$committed = true;
464     this.$async = async;
465   
466     var i,
467         queue = this.$queue,
468         frame = new baja.comm.BoxFrame(queue);             
469     
470     // If there aren't any messages to send then bail...
471     if (frame.isEmpty()) {
472       batchOk(this, 0); 
473     } 
474     else {
475       frame.send(async, this);
476     }
477   };
478   
479   /**
480    * Adds all messages to a frame and makes a synchronous network call.
481    * <p>
482    * It's always preferable to make an asynchronous network call. Otherwise
483    * any page will appear to block. However, there are some occassions where 
484    * synchronous network calls are necessary. This method can be used to make 
485    * these network calls.
486    * 
487    * @see baja.comm.Batch#commit
488    */
489   baja.comm.Batch.prototype.commitSync = function () {
490     doCommit.call(this, /*async*/false);
491   };
492     
493   /**
494    * Makes a Batch asynchronous network call. This should 
495    * <strong>always</strong> be used in preference to 
496    * {@link baja.comm.Batch#commitSync}.
497    */
498   baja.comm.Batch.prototype.commit = function () {
499     doCommit.call(this, /*async*/true);
500   };
501   
502   /**
503    * Return a copy of the Batch's messages array.
504    *
505    * @private
506    *
507    * @returns a copy of the Batch's Messages array.
508    */
509   baja.comm.Batch.prototype.getMessages = function () {
510     return this.$queue.slice(0);
511   };  
512   
513   /**
514    * Ok callback made one network call has successfully completed.
515    * <p>
516    * This gets invoked when the network call has completed successfully and a response has
517    * been received.
518    *
519    * @private
520    *
521    * @param {String} resp  the response text.
522    * @throws Error if a failure occurs.
523    */
524  baja.comm.Batch.prototype.ok = function (resp) { 
525     // Bail if the BajaScript engine has fully stopped
526     if (baja.isStopped()) {
527       return;
528     }
529   
530     // Decode the JSON
531     resp = JSON.parse(resp);
532    
533     if (resp.p !== "box") {
534       this.fail("Invalid BOX Frame. Protocol is not BOX");
535       return;
536     }
537     
538     // Process the response
539     batchOk(this,  0, resp); 
540   };
541   
542   /**
543    * Fail is called if one of the batched calls fails.
544    *
545    * @private
546    *
547    * @param err  the cause of the error.
548    */
549   baja.comm.Batch.prototype.fail = function (err) { 
550     // Bail if the BajaScript engine has fully stopped
551     if (baja.isStopping()) {
552       return;
553     }  
554     
555     // Fail all messages with error since the batch message itself failed
556     batchFail(this, 0, err); 
557   };
558         
559   /**
560    * Return true if this Batch object has no messages to send.
561    *
562    * @private
563    *
564    * @returns {Boolean}
565    */
566   baja.comm.Batch.prototype.isEmpty = function () {
567     return this.$queue.length === 0;
568   };
569   
570   /**
571    * Return true if this Batch has already been committed.
572    *
573    * @private
574    *
575    * @returns {Boolean}
576    */
577   baja.comm.Batch.prototype.isCommitted = function () {
578     return this.$committed;
579   };
580   
581   /**
582    * Return true if this Batch was committed asynchronously.
583    * <p>
584    * If the batch is not committed yet, this will return true.
585    *
586    * @private
587    *
588    * @returns {Boolean}
589    */
590   baja.comm.Batch.prototype.isAsync = function () {
591     return this.$async;
592   };
593     
594   ////////////////////////////////////////////////////////////////
595   // Callback
596   //////////////////////////////////////////////////////////////// 
597       
598   /**
599    * @class Network Callback.
600    * <p>
601    * A Callback object has ok and fail methods. It is used to make a comms call in
602    * BajaScript. When a comms call is made, additional callbacks may be needed to
603    * process the incoming data before calling the original ok or fail callbacks are 
604    * executed. Therefore, extra help functions have been added to this object
605    * to for convenience.
606    * <p>
607    * This method should only be used internally by BajaScript. It should never appear
608    * in the public API.
609    *
610    * @name baja.comm.Callback
611    * @extends BaseBajaObj
612    * @private
613    */  
614   baja.comm.Callback = function (ok, fail, batch) {
615     var that = this;
616     baja.comm.Callback.$super.apply(that, arguments);
617    
618     ok = ok || baja.ok;
619     fail = fail || baja.fail;
620     
621     that.$batch = batch || new baja.comm.Batch();
622     that.$batchNext = baja.noop;
623     
624     that.ok = function () {
625       // Invoke user's ok callback and then make a batch callback
626       try {
627         ok.apply(this, arguments);
628       }
629       finally {
630         that.$batchNext();
631       }
632     };
633     
634     that.fail = function () {
635       // Invoke the user's fail callback and then make a batch callback
636       try {
637         fail.apply(this, arguments);
638       }
639       finally {
640         that.$batchNext();
641       }
642     };
643         
644     // Make a note of whether the batch was originally defined
645     that.$orgBatchDef = typeof batch === "object";           
646   }.$extend(BaseBajaObj);
647     
648   /**
649    * Add an 'ok' callback.
650    * <p> 
651    * When calling some BajaScript that causes a network call, there will be other callbacks
652    * that need to be processed before the original 'user' callbacks are invoked. Therefore,
653    * 'addOk' and 'addFail' can be used to chain up extra Callbacks if needed.
654    * <pre>
655    *   // Create a callback object with user's original ok and fail function callbacks...
656    *   var cb = new baja.comm.Callback(ok, fail, batch);
657    *
658    *   // Add an intermediate callback...
659    *   cb.addOk(function (ok, fail, resp) {
660    *     // Process the response 'resp' object...
661    *     var val = processResponse(resp);
662    *     
663    *     // Now call 'ok' callback passed in with processed response...
664    *     ok(val);
665    *   });
666    * </pre>
667    * <p>
668    * Please note, that when adding an intermediate calllback, the 'ok' or 'fail' method passed
669    * in <strong>must</strong> be called at some point.
670    * <p>
671    * This method is also extremely useful for intermediate callbacks that need to make other asynchronous
672    * network calls before calling the user's original 'ok' or 'fail' callback functions.
673    *
674    * @private
675    *
676    * @see baja.comm.Callback#fail
677    *
678    * @param {Function} newOk  the callback Function. This function must accept three
679    *                          arguments including the current ok and fail function as well
680    *                          as any arguments specific to the callback function.
681    *                          By convention, the 'ok' or 'fail' functions passed in must be called
682    *                          after the new callback has finished.
683    */
684   baja.comm.Callback.prototype.addOk = function (newOk) {
685   
686     // Make a reference to the current ok and fail callback functions
687     var currentOk = this.ok,
688         currentFail = this.fail;
689     
690     // Create a new 'ok' callback closure. When invoked, the new 'ok' function will be called
691     // with the previous 'ok' and 'fail' functions passed in as arguments to the beginning of
692     // the function. By convention, the new callback ok function should invoke the 'ok' function
693     // passed into it as an argument (or fail if there is a problem with the data).
694     this.ok = function () {
695       var args = Array.prototype.slice.call(arguments);
696       args.splice(0, 0, currentOk, currentFail);
697       newOk.apply(this, args);
698     };
699   };
700   
701   /**
702    * Add a 'fail' callback.
703    * <p> 
704    * When calling some BajaScript that causes a network call, there will be other callbacks
705    * that need to be processed before the original 'user' callbacks are invoked. Therefore,
706    * 'addOk' and 'addFail' can be used to chain up extra Callbacks if needed.
707    * <pre>
708    *   // Create a callback object with user's original ok and fail function callbacks...
709    *   var cb = new baja.comm.Callback(ok, fail, batch);
710    *
711    *   // Add an intermediate callback...
712    *   cb.addFail(function (ok, fail, err) {
713    *     // Process the error messages before calling the original 'fail' callback...
714    *     var niceMsg = processError(err);
715    *     
716    *     // Now call 'fail' callback passed in with processed error message...
717    *     fail(niceMsg);
718    *   });
719    * </pre>
720    * <p>
721    * Please note, that when adding an intermediate calllback, the 'ok' or 'fail' method passed
722    * in <strong>must</strong> be called at some point.
723    * 
724    * @private
725    *
726    * @see baja.comm.Callback#fail
727    *
728    * @param {Function} newFail  the callback Function. This function must accept three
729    *                            arguments including the current ok and fail function as well
730    *                            as any arguments specific to the callback function.
731    *                            By convention, the 'ok' or 'fail' functions passed in must be called
732    *                            after the new callback has finished.
733    */
734   baja.comm.Callback.prototype.addFail = function (newFail) {
735     var currentOk = this.ok,
736         currentFail = this.fail;
737     this.fail = function () {
738       var args = Array.prototype.slice.call(arguments);
739       args.splice(0, 0, currentOk, currentFail);
740       newFail.apply(this, args);
741     };
742   };
743   
744   /**
745    * Return true if the batch object was originally defined when the batch was created.
746    * 
747    * @private
748    *
749    * @returns {Boolean}
750    */   
751   baja.comm.Callback.prototype.isOrgBatchDef = function () {
752     return this.$orgBatchDef;
753   };
754   
755   /**
756    * Add a request to the callback's batch object.
757    * 
758    * @private
759    *
760    * @see baja.comm.Batch#addReq
761    *
762    * @param {String} channel
763    * @param {String} key
764    * @param body
765    */   
766   baja.comm.Callback.prototype.addReq = function (channel, key, body) {    
767     this.$batch.addReq(channel, key, body, this);
768   };
769     
770   /**
771    * If a Batch object was originally passed in when this object was
772    * created then commit the Batch. Otherwise do nothing. 
773    * <p>
774    * Therefore, if a Batch was originally passed into this Callback
775    * object, it is assumed the caller will invoke the Batch commit method
776    * when appropriate (i.e. they've batched up all of the network requests
777    * that are going to be made).
778    *
779    * @private
780    *
781    * @see baja.comm.Batch#commit
782    * @see baja.comm.Callback#isOrgBatchDef
783    */
784   baja.comm.Callback.prototype.autoCommit = function () {
785     // If there was a batch object originally defined then don't commit
786     // as the batch will be committed elsewhere
787     if (!this.isOrgBatchDef()) {
788       this.commit();
789     }
790   };
791   
792   /**
793    * Commit the this object's Batch
794    * <p>
795    * This asynchronously makes a network call
796    * 
797    * @private
798    *
799    * @see baja.comm.Batch#commit
800    */
801   baja.comm.Callback.prototype.commit = function () {    
802     this.$batch.commit();
803   };
804   
805   /** 
806    * Return this object's batch object
807    *
808    * @private
809    *
810    * @returns {baja.comm.Batch}
811    */
812   baja.comm.Callback.prototype.getBatch = function () {    
813     return this.$batch;
814   };
815   
816   /**
817    * Synchronously commit this object's batch
818    * <p> 
819    * Please note that {@link baja.comm.Commit#commit} should always be used
820    * in preference as this will result in a synchronous network call that 
821    * will block everything else.
822    * 
823    * @private
824    *
825    * @see baja.comm.Callback#commit
826    */
827   baja.comm.Callback.prototype.commitSync = function () {    
828     this.$batch.commitSync();
829   };
830   
831   Callback = baja.comm.Callback;
832       
833   ////////////////////////////////////////////////////////////////
834   // Server Session
835   //////////////////////////////////////////////////////////////// 
836     
837   /**
838    * @class ServerSession implementation used for polling Server data.
839    *
840    * @name ServerSession
841    * @private
842    * @inner
843    */
844   var ServerSession = function (id) {
845     this.$id = id;
846   };
847   
848   /**
849    * Add a ServerSession make network request.
850    *
851    * @private
852    */
853   ServerSession.make = function (cb) {
854     cb.addReq("ssession", "make", {});
855   };
856         
857   /**
858    * Add a request to the SessionSession object.
859    *
860    * @private
861    *
862    * @param {String} key
863    * @param {baja.comm.Callback} cb
864    */
865   ServerSession.prototype.addReq = function (key, cb) {
866     return ServerSession.addReq(key, cb, { id: this.$id });
867   };
868   
869   /**
870    * Add a request to the SessionSession object.
871    *
872    * @private
873    *
874    * @param {String} key
875    * @param {baja.comm.Callback} cb
876    * @param arg call argument.
877    */
878   ServerSession.addReq = function (key, cb, arg) {
879     cb.addReq("ssession", key, arg);
880     return arg;
881   };
882   
883   /**
884    * Add an event handler for an eventHandlerId to this ServerSession.
885    * <p>
886    * This is used to listen to events from the ServerSession.
887    *
888    * @private
889    *
890    * @param {String} eventHandlerId
891    * @param {Function} eventHandler
892    */
893   ServerSession.addEventHandler = function (eventHandlerId, eventHandler) {
894     eventHandlers[eventHandlerId] = eventHandler;
895   };
896   
897   /**
898    * Remove an event handler from the ServerSession.
899    *
900    * @private
901    *
902    * @param {String} eventHandlerId
903    */
904   ServerSession.removeEventHandler = function (eventHandlerId) {
905     if (eventHandlers.hasOwnProperty(eventHandlerId)) {
906       delete eventHandlers[eventHandlerId];
907     }
908   };
909   
910   /**
911    * Return an event handler via its eventHandlerId.
912    *
913    * @private
914    *
915    * @param {String} eventHandlerId
916    * @returns {Function} event handler or null if cannot be found.
917    */
918   ServerSession.findEventHandler = function (eventHandlerId) {
919     return eventHandlers.hasOwnProperty(eventHandlerId) ? eventHandlers[eventHandlerId] : null;
920   };
921   
922   ////////////////////////////////////////////////////////////////
923   // Server Session Comms Calls
924   //////////////////////////////////////////////////////////////// 
925                     
926   /**  
927    * Makes the ServerSession.
928    *
929    * @private
930    *
931    * @param {baja.comm.Callback} cb
932    */
933   baja.comm.makeServerSession = function (cb) {
934     
935     // Add intermediate callbacks
936     cb.addOk(function (ok, fail, id) {    
937       try {        
938         serverSession = new ServerSession(id);
939         ok();
940       }
941       finally {
942         pollTicket = baja.clock.schedule(baja.comm.poll, pollRate);
943       }
944     });
945     
946     cb.addFail(function (ok, fail, err) {
947       try {
948         fail(err);
949       }
950       finally {
951         serverCommFail(err);
952       }
953     });
954     
955     // Make the ServerSession
956     ServerSession.make(cb); 
957     
958     // commit if we can
959     cb.autoCommit();        
960   };
961   
962   /**
963    * Make a Server Session Handler on the Server.
964    * <p>
965    * A Server Session represents the session between a BajaScript Client and the Server. Components can be created
966    * and mounted under the Server's Server Session. These are called Server Session Handler Components. Server Session Handler Components
967    * provide an architecture for Session based Components that can receive requests and responses. A Server Session Handler
968    * can also dispatch events to a BajaScript client for further processing. A good example of a Server Session Handler
969    * is the local Component Space BajaScript is connected too. The Server Session Handler API represents a useful
970    * abstract layer for other Space subsystems to be plugged into (i.e. Virtual Component Spaces).
971    * <p>
972    * Currently, the Server Session API is considered to be private and should only be used by Tridium framework developers.
973    *
974    * @private
975    * 
976    * @param {String} serverHandlerId  a unique String that will identify the Server Session Handler under the Server Session.
977    * @param {String} serverHandlerTypeSpec  the type spec (moduleName:typeName) of the Server Session Handler that will be mounted
978    *                                        under the Server Session.
979    * @param serverHandlerArg an initial argument to be passed into the Server Session Handler when it's created. This argument will be
980    *                         encoded to standard JSON.
981    * @param {Function} eventHandler an event handler callback function that will be called when any events are dispatched from the 
982    *                                  Server Session Handler.
983    * @param {baja.comm.Callback} cb the callback handler.
984    * @param {Boolean} [makeInBatch] set to true if the batch being used has the make present (hence the server session creation
985    *                                is part of this network call.
986    */
987   baja.comm.makeServerHandler = function (serverHandlerId, serverHandlerTypeSpec, serverHandlerArg, eventHandler, cb, makeInBatch) {         
988     var arg;
989         
990     // If ServerSession isn't available then throw the appropriate error
991     if (!serverSession) {
992       // If this flag is true then the Server Session creation is part of this network request
993       // hence the server session id will be picked up in the Server via the BoxContext
994       if (makeInBatch) {
995         arg = ServerSession.addReq("makessc", cb, {});
996       }
997       else {
998         throw new Error("ServerSession not currently available!");
999       }
1000     } 
1001     else {
1002       arg = serverSession.addReq("makessc", cb);
1003     }    
1004           
1005     ServerSession.addEventHandler(serverHandlerId, eventHandler);
1006         
1007     // Fill out other arguments
1008     arg.scid = serverHandlerId;
1009     arg.scts = serverHandlerTypeSpec;
1010     arg.scarg = serverHandlerArg;
1011     
1012     // commit if we can
1013     cb.autoCommit();    
1014   };  
1015   
1016   /**
1017    * Remove a Server Session Handler from the Server.
1018    * 
1019    * @private
1020    *
1021    * @see baja.comm.makeServerHandler
1022    *
1023    * @param {String} serverHandlerId the id of the Server Session Handler to remove from the Server.
1024    * @param {baja.comm.Callback} the callback handler.
1025    */
1026   baja.comm.removeServerHandler = function (serverHandlerId, cb) {   
1027     // If ServerSession isn't available then throw the appropriate error
1028     if (!serverSession) {
1029       throw new Error("ServerSession not currently available!");
1030     }
1031 
1032     ServerSession.removeEventHandler(serverHandlerId);
1033         
1034     // Make Server Session Request
1035     var arg = serverSession.addReq("removessc", cb);
1036     
1037     // Fill out other arguments
1038     arg.scid = serverHandlerId;
1039     
1040     // commit if we can
1041     cb.autoCommit();    
1042   };
1043   
1044   /**
1045    * Make an RPC call to the Server Session Handler on the Server.
1046    * 
1047    * @private
1048    *
1049    * @see baja.comm.makeServerHandler
1050    *
1051    * @param {String} serverHandlerId the id of the Server Session Handler.
1052    * @param {String} serverHandlerKey the key of the request handler to invoke on the Server Session Handler.
1053    * @param serverHandlerArg the argument to pass into the request handler invoked on the Server Session Handler.
1054    *                         This argument is encoded to JSON.
1055    * @param {baja.comm.Callback} cb the callback handler.
1056    * @param {Boolean} [makeInBatch] set to true if the batch being used has the make present (hence the server session creation
1057    *                                is part of this network call).
1058    */  
1059   baja.comm.serverHandlerCall = function (serverHandlerId, serverHandlerKey, serverHandlerArg, cb, makeInBatch) {   
1060     var arg;
1061   
1062     // If ServerSession isn't available then it's request make be in this batch so
1063     // allow it anyway
1064     if (!serverSession) {
1065       // If this flag is true then the Server Session creation is part of this network request
1066       // hence the server session id will be picked up in the Server via the BoxContext
1067       if (makeInBatch) {
1068         arg = ServerSession.addReq("callssc", cb, {});
1069       }
1070       else {
1071         throw new Error("ServerSession not currently available!");
1072       }
1073     }
1074     else {
1075       arg = serverSession.addReq("callssc", cb);
1076     }
1077             
1078     // Fill out other arguments
1079     arg.scid = serverHandlerId;
1080     arg.sck = serverHandlerKey;
1081     arg.scarg = serverHandlerArg;
1082     
1083     // commit if we can
1084     cb.autoCommit(); 
1085   };  
1086   
1087   ////////////////////////////////////////////////////////////////
1088   // Server Session Event Polling
1089   //////////////////////////////////////////////////////////////// 
1090     
1091   /**  
1092    * Polls the ServerSession for Changes.
1093    *
1094    * @private
1095    *
1096    * @param {Object} [cb]  callback
1097    */
1098   baja.comm.poll = function (cb) {
1099   
1100     // Cancel any existing poll timers
1101     pollTicket.cancel();
1102     
1103     // Bail if we're stopping
1104     if (baja.isStopping()) {
1105       return;
1106     }
1107     
1108     // Flag indicating whether this was called from the poll timer
1109     var fromTimer = false;
1110     
1111     // Ensure we have a callback
1112     if (!cb) {
1113       cb = new Callback(baja.ok, function fail(err) {
1114         baja.error(err);
1115       });
1116       fromTimer = true;
1117     }
1118          
1119     // Convience for scheduling next poll
1120     function sch() {
1121       if (serverSession) {
1122         pollTicket.cancel();
1123         pollTicket = baja.clock.schedule(baja.comm.poll, pollRate);
1124       }
1125     }
1126     
1127     // Bail if we haven't got a serverSession
1128     if (!serverSession) {
1129       throw new Error("No Server Session available");
1130     }
1131     
1132     cb.addOk(function (ok, fail, resp) {  
1133       // Bail if we're stopping
1134       if (baja.isStopping()) {
1135         return;
1136       }
1137 
1138       var newOk = function () {      
1139         try { 
1140           var i;        
1141           for (i = 0; i < resp.length; ++i) {
1142             // Look up the event handler using the Server Side Component Id
1143             var handler = ServerSession.findEventHandler(resp[i].scid);
1144             if (typeof handler === "function") {
1145               // Handle the events
1146               handler(resp[i].evs);  
1147             }
1148           }
1149           
1150           ok();
1151         }
1152         finally {
1153           sch();
1154         }
1155       };
1156       
1157       var newFail = function (err) {      
1158         try {
1159           fail(err);
1160         }
1161         finally {
1162           sch();
1163         }
1164       };
1165      
1166       // Pre-emptively scan the BSON for Types that don't exist yet or have Contracts loaded
1167       // and request them in one network call            
1168       var unknownTypes = baja.bson.scanForUnknownTypes(resp);            
1169       if (unknownTypes.length > 0) {
1170         var importBatch = new baja.comm.Batch();
1171         baja.importTypes({
1172           "typeSpecs": unknownTypes, 
1173           "ok": newOk, 
1174           "fail": newFail,
1175           "batch": importBatch
1176         });
1177                     
1178         if (fromTimer) {
1179           // If this was called from a timer then we can commit asynchonously.  
1180           importBatch.commit();
1181         }
1182         else {
1183           // If this was called from a direct invocation (i.e. a Space Sync) then it must 
1184           // run synchronously so it doesn't cause any problems with the batch end callback. 
1185           importBatch.commitSync();
1186         }
1187       }
1188       else {
1189         newOk();
1190       }
1191     });
1192      
1193     cb.addFail(function (ok, fail, err) {
1194       // TODO: Could make this more robust by explicity checking for server id not existing?
1195       if (fromTimer) {
1196         // Delay raising this error by a second (just in case we're trying to shutdown at the time)...
1197         baja.clock.schedule(function () {
1198           // If there's an error from a timer poll then something must have screwed up 
1199           // really badly...
1200           try {
1201             fail(err);
1202           }
1203           finally {
1204             serverCommFail(err);
1205           }
1206         }, 1000);
1207       }
1208       else {
1209         // If not from a timer then just fail as normal
1210         fail(err);
1211       }
1212     });
1213     
1214     serverSession.addReq("pollchgs", cb);
1215     
1216     // Commit if we're able too
1217     cb.autoCommit();
1218   };
1219   
1220   ////////////////////////////////////////////////////////////////
1221   // Comms Start and Stop
1222   //////////////////////////////////////////////////////////////// 
1223     
1224   /**
1225    * Start the Comms Engine.
1226    * <p>
1227    * This is called to start BajaScript.
1228    *
1229    * @private
1230    *
1231    * @param {Object} obj the Object Literal for the method's arguments.
1232    * @param {Function} [obj.started] function called once BajaScript has started.
1233    * @param {Array} [obj.typeSpecs] an array of type specs (moduleName:typeName) to import
1234    *                                on start up. This will import both Type and Contract information.
1235    * @param {Function} [obj.commFail] function called if the BOX communications fail.
1236    * @param {Boolean} [obj.navFile] if true, this will load the nav file for the user on start up.
1237    */ 
1238   baja.comm.start = function (obj) {          
1239     var started = obj.started || baja.ok,
1240         typeSpecs = obj.typeSpecs || [],
1241         batch = new baja.comm.Batch();
1242 
1243     commFail = obj.commFail || commFail;
1244     
1245     // Get the System Properties...
1246     var propsCb = new Callback(function ok(props) {     
1247       baja.initFromSysProps(props);
1248     },
1249     baja.fail, batch);
1250     propsCb.addReq("sys", "props", {});
1251         
1252     // Make sure we get the Contract for the root of the Station
1253     if (!typeSpecs.contains("baja:Station")) {
1254       typeSpecs.push("baja:Station");
1255     }
1256         
1257     // If specified, load the Nav File on start up
1258     if (obj.navFile) {
1259       baja.nav.navfile.load({batch: batch});
1260     }
1261     
1262     // Import all of the requested Type Specification
1263     baja.importTypes({
1264       "typeSpecs": typeSpecs, 
1265       "ok": baja.ok, 
1266       "fail": baja.fail, 
1267       "batch": batch
1268     });
1269     
1270     // Make the ServerSession
1271     baja.comm.makeServerSession(new Callback(baja.ok, baja.fail, batch)); 
1272     
1273     // Once we have a ServerSession, we can start off our connection to
1274     // to the local Component Space
1275     baja.station.init(batch); 
1276     
1277     // Call this once everything has finished
1278     batch.addCallback(started);
1279     
1280     // Commit all comms messages in one request
1281     batch.commit(); 
1282   };
1283   
1284   /**
1285    * Stop the Comms Engine.
1286    *
1287    * @private
1288    *
1289    * @param {Object} obj the Object Literal for the method's arguments.
1290    * @param {Function} [obj.stopped]  function to invoke after the comms have stopped.
1291    * @param {Function} [obj.preStop] function to invoke just before the comms have stopped.
1292    */
1293   baja.comm.stop = function (obj) {  
1294     var postStopFunc = obj.stopped || baja.ok;
1295       
1296     // Cancel the Ticket
1297     pollTicket.cancel();
1298     
1299     // Delete the ServerSession if it exists
1300     if (serverSession) {
1301       var cb = new Callback(postStopFunc, baja.ok);
1302       serverSession.addReq("del", cb);
1303       cb.commitSync();
1304       
1305       serverSession = null;
1306     }
1307           
1308       // TODO: Need to unsubscribe Components
1309   };        
1310     
1311   ////////////////////////////////////////////////////////////////
1312   // Registry Channel Comms
1313   //////////////////////////////////////////////////////////////// 
1314   
1315   /** 
1316    * Makes a network call for loading Type information.
1317    *
1318    * @private
1319    *
1320    * @param {String|Array} types  the TypeSpecs needed to be resolved.
1321    * @param {Boolean} encodeContracts encode Contracts for the given Types.
1322    * @param {baja.comm.Callback} cb callback handler. If the callback had a
1323    *                             batch object originally defined then the network call
1324    *                             will be made.
1325    * @param {Boolean} async true if this network should be made asynchronously.
1326    */
1327   baja.comm.loadTypes = function (types, encodeContracts, cb, async) {        
1328     if (typeof types === "string") {
1329       types = [types];
1330     }
1331           
1332     // Add a request message to the Builder
1333     cb.addReq("reg", "loadTypes", { t: types, ec: encodeContracts });
1334     
1335     if (!cb.isOrgBatchDef()) {
1336       if (async) {
1337         cb.commit();
1338       }
1339       else {
1340         cb.commitSync();
1341       }
1342     }
1343   };
1344   
1345   /** 
1346    * Makes a network call for loading Contract information.
1347    *
1348    * @private
1349    *
1350    * @param {String} typeSpec  the TypeSpec the Contract information is going to be fetched for.
1351    * @param {baja.comm.Callback} cb callback handler. If the callback had a
1352    *                             batch object originally defined then the network call
1353    *                             will be made.
1354    * @param {Boolean} async true if this network should be made asynchronously.
1355    */
1356   baja.comm.loadContract = function (typeSpec, cb, async) {            
1357       
1358     // Add a request message to the Builder
1359     cb.addReq("reg", "loadContract", typeSpec);
1360     
1361     if (!cb.isOrgBatchDef()) {
1362       if (async) {
1363         cb.commit();
1364       }
1365       else {
1366         cb.commitSync();
1367       }
1368     }
1369   };
1370   
1371   /** 
1372    * Makes a network call for getting concrete Type information.
1373    *
1374    * @private
1375    *
1376    * @param {String} typeSpec  the TypeSpec used for querying the concrete types.
1377    * @param {baja.comm.Callback} cb the callback handler. If the callback had a
1378    *                                batch object originally defined then the network call
1379    *                                will be made.
1380    */
1381   baja.comm.getConcreteTypes = function (typeSpec, cb) {            
1382       
1383     // Add a request message to the Builder
1384     cb.addReq("reg", "getConcreteTypes", typeSpec);
1385     
1386     // Commit if we're able too...
1387     cb.autoCommit();
1388   };
1389 
1390   ////////////////////////////////////////////////////////////////
1391   // ORD Channel Comms
1392   ////////////////////////////////////////////////////////////////
1393   
1394   /** 
1395    * Resolve an ORD.
1396    *
1397    * @private
1398    *
1399    * @param {baja.Ord} ord  the ORD to be resolved.
1400    * @param object  base Object.
1401    * @param {Object} cb  the callback handler.
1402    * @param {Object} options Object Literal options.
1403    */
1404   baja.comm.resolve = function (ord, object, cb, options) {             
1405             
1406     var bd = { // ORD Resolve Message Body
1407       o: ord.toString(), // ORD
1408       bo: object.getNavOrd().toString() // Base ORD
1409     };
1410     
1411     var cursor = options.cursor;
1412     
1413     // If cursor options are defined then use them...
1414     if (cursor) {
1415       bd.c = {
1416         of: cursor.offset,
1417         lm: cursor.limit 
1418       };
1419     }
1420           
1421     // Add Request Message
1422     cb.addReq("ord", "resolve", bd);
1423     
1424     // Commit if we're able too
1425     cb.autoCommit();
1426   }; 
1427   
1428   /** 
1429    * Resolve Cursor data for a Collection (or Table).
1430    *
1431    * @private
1432    *
1433    * @param {Object} bd  the body of the Comms message.
1434    * @param {baja.comm.Callback} cb  the callback handler.
1435    * @param {Object} options Object Literal options.
1436    */
1437   baja.comm.cursor = function (bd, cb, options) {
1438     bd.of = options.offset;
1439     bd.lm = options.limit;
1440       
1441     // Add Request Message
1442     cb.addReq("ord", "cursor", bd);
1443     
1444     // Commit if we're able too
1445     cb.autoCommit();
1446   }; 
1447 
1448   ////////////////////////////////////////////////////////////////
1449   // Sys Channel Comms
1450   ////////////////////////////////////////////////////////////////  
1451   
1452   /** 
1453    * Makes a network call loading Lexicon information.
1454    *
1455    * @private
1456    *
1457    * @param {String} module the module name of the lexicon.
1458    * @param {baja.comm.Callback} cb callback handler. If the callback had a
1459    *                             batch object originally defined then the network call
1460    *                             will be made.
1461    * @param {Boolean} async true if this network should be made asynchronously.
1462    */
1463   baja.comm.lex = function (module, cb, async) {            
1464       
1465     // Add a request message to the Builder
1466     cb.addReq("sys", "lex", module);
1467     
1468     if (!cb.isOrgBatchDef()) {
1469       if (async) {
1470         cb.commit();
1471       }
1472       else {
1473         cb.commitSync();
1474       }
1475     }
1476   };
1477   
1478   /** 
1479    * Makes a network call for the nav file.
1480    *
1481    * @private
1482    *
1483    * @param {baja.comm.Callback} cb callback handler. If the callback had a
1484    *                             batch object originally defined then the network call
1485    *                             will be made.
1486    */
1487   baja.comm.navFile = function (cb) {            
1488     // Add a request message to the Builder
1489     cb.addReq("sys", "navFile", {});
1490     
1491     // commit if we can
1492     cb.autoCommit();
1493   };
1494   
1495   /** 
1496    * Make a network call with the specified error. This will Log the error
1497    * in the Server.
1498    *
1499    * @private
1500    *
1501    * @param {String} error the error message to be logged in the Server.
1502    */
1503   baja.comm.error = function (error) {   
1504     // Make a network call but don't report back any errors if this doesn't work
1505     var cb = new Callback(baja.ok, baja.ok);
1506     
1507     // Add a request message to the Builder
1508     cb.addReq("sys", "error", error);
1509     
1510     // commit if we can
1511     cb.commit();
1512   };
1513   
1514 }(baja, BaseBajaObj));