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));