1 (function (GCN) { 2 3 'use strict'; 4 5 /** 6 * Enqueue a method into the given chainback objects's call chain. If a 7 * mutex is locking this chain, then place the call in the queued calls 8 * instead. 9 * 10 * @private 11 * @param {Chainback} chainback The object whose queue we want to push 12 * the given method to. 13 * @param {function} method The method to chain. 14 */ 15 function addCallToQueue(chainback, method) { 16 if (!chainback.__gcnmutex__) { 17 chainback.__gcncallqueue__.push(method); 18 } else { 19 chainback.__gcncallchain__.push(method); 20 if (0 === chainback.__gcnajaxcount__ 21 && 1 === chainback.__gcncallchain__.length) { 22 method.call(chainback); 23 } 24 } 25 } 26 27 /** 28 * Dequeue the function at the top of the given chainback's call chain and 29 * invoke the next function in the queue. 30 * 31 * @private 32 * @param {Chainback} chainback 33 */ 34 function callNext(chainback) { 35 // Waiting for an ajax call to complete. Go away, and try again when 36 // another call completes. 37 if (chainback.__gcnajaxcount__ > 0) { 38 return; 39 } 40 41 if (0 === chainback.__gcncallchain__.length 42 && 0 === chainback.__gcncallqueue__.length) { 43 return; // We should never reach here. Just so you know... 44 } 45 46 // Discard the empty shell... 47 chainback.__gcncallchain__.shift(); 48 49 // Load and fire the next bullet... 50 if (chainback.__gcncallchain__.length) { 51 chainback.__gcncallchain__[0].call(chainback); 52 } else if (chainback.__gcncallqueue__.length) { 53 chainback.__gcncallqueue__.shift().call(chainback); 54 } 55 } 56 57 /** 58 * Wraps the given method in a closure that provides scaffolding to chain 59 * invocations of the method correctly. 60 * 61 * @private 62 * @param {function} method The original function we want to wrap. 63 * @param {string} name The method name as it was defined in its object. 64 * @return {function} A function that wraps the original function. 65 */ 66 function makeMethodChainable(method, name) { 67 return function () { 68 var args = arguments; 69 var that = this; 70 var func = function () { 71 method.apply(that, args); 72 callNext(that); 73 }; 74 func.__gcncallname__ = name; // For debugging 75 addCallToQueue(this, func); 76 return this; 77 }; 78 } 79 80 /** 81 * The Chainback constructor. 82 * 83 * Surfaces the chainback constructor in such a way as to be able to use 84 * call apply() on it. 85 * 86 * http://stackoverflow.com/questions/1606797/use-of-apply-with-new-operator-is-this-possible 87 * 88 * @private 89 * @param {Chainback} ctor The Chainback class we wish to initialize. 90 * @param {Array} args 91 * @param {object} continuation 92 * @return {Chainback} 93 */ 94 var Chainback = (function () { 95 var Chainback = function (ctor, args) { 96 return ctor.apply(this, args); 97 }; 98 return function (ctor, args, continuation) { 99 Chainback.prototype = ctor.prototype; 100 return new Chainback(ctor, args); 101 }; 102 }()); 103 104 /** 105 * Get a chainback instance's current channel. 106 * 107 * @param {Chainback} chainback 108 * @return {number} The channel id or 0 if no channel is set. 109 */ 110 function getChannel(chainback) { 111 return chainback._channel || 0; 112 } 113 114 /** 115 * Get an instance of the given chainback class from its constructor's 116 * cache. If the object for this hash does not exist in the cache, then 117 * instantiate a new object, place into the cache using the hash as the 118 * cache key. 119 * 120 * If no hash is passed to this function, then the Chainback instance that 121 * is returned will not be fully realized. It will be a "fetus" instance 122 * that has yet to be bound to an id. Once it receives an id it will keep 123 * it for the remainder of its life. These "fetus" instances are not be 124 * placed in the cache until they have aquired an id. 125 * 126 * @ignore 127 * @private 128 * @param {Chainback} ctor A chainback constructor. 129 * @param {string} hash A hash string that represents this chainback. 130 * @param {Chainback} callee The Chainback instance from which this 131 * invocation originated. 132 * @param {Array.<*>} args Arguments that will be applied to the chainback 133 * when (re-) initializing it. This array should 134 * contain the following elements in the following 135 * order: 136 * id : string|array 137 * success : function|null 138 * error : function|null 139 * setting : object 140 * @return {Chainback} 141 */ 142 GCN.getChainback = function (ctor, hash, callee, args) { 143 var chainback = hash && ctor.__gcncache__[hash]; 144 if (chainback) { 145 // Reset the cached instance and re-initialize it. 146 chainback._chain = callee; 147 return chainback._init.apply(chainback, args); 148 } 149 args.push(callee); 150 var isFetus = !hash; 151 var hasCallee = !!callee; 152 if (isFetus && hasCallee) { 153 // TODO: Creating a hash form just the ctor argument is 154 // insufficient. We must also consider 155 // ctor._needsChainedHash. For example, createTag() will 156 // cause a new chanback to be created which will have a hash 157 // value of 'TagAPI:id' but it should be 158 // 'Page::id::TagAPI:id'. 159 hash = ctor._makeHash(getChannel(callee) + '/' + GCN.uniqueId(ctor.__chainbacktype__ + '-unique-')); 160 chainback = callee.__gcntempcache__[hash]; 161 if (!chainback) { 162 chainback = 163 callee.__gcntempcache__[hash] = 164 new Chainback(ctor, args); 165 } 166 return chainback; 167 } 168 return new Chainback(ctor, args); 169 }; 170 171 /** 172 * @ignore 173 * Create a class which allows for chainable callback methods. 174 * @private 175 * @param {object<string, *>} props Definition of the class to be created. 176 * All function are wrapped to allow them 177 * to be as chainable callbacks unless 178 * their name is prefixed with a "!" . 179 * @return {Chainback} 180 */ 181 GCN.defineChainback = function (props) { 182 183 /** 184 * @ignore 185 * @TODO: use named arguments 186 * 187 * @constructor 188 * @param {number|string} id 189 * @param {?function(Chainback)} success 190 * @param {?function(GCNError):boolean} error 191 * @param {?object} chainlink 192 */ 193 var chainback = function () { 194 var args = Array.prototype.slice.call(arguments); 195 196 this._chain = args.pop(); 197 this._channel = this._chain ? this._chain._channel : GCN.channel(); 198 199 // Please note: We prefix and suffix these values with a double 200 // underscore because they are not to be relied on whatsoever 201 // outside of this file! Although they need to be carried around 202 // on chainback instances, they are nevertheless soley for internal 203 // wiring. 204 this.__gcnmutex__ = true; 205 this.__gcncallchain__ = []; 206 this.__gcncallqueue__ = []; 207 this.__gcntempcache__ = {}; 208 209 // This is used to synchronize ajax calls with non-ajax calls in a 210 // chainback call queue. 211 // 212 // It serves as a type of countdown latch, or reverse counting 213 // semaphore (msdn.microsoft.com/en-us/magazine/cc163427.aspx). 214 // 215 // Upon each invocation of `_queueAjax()', on a chainback object, 216 // its `__gcnajaxcount__' counter will be incremented. And each 217 // time a queued ajax call completes (successfully or otherwise), 218 // the counter is decremented. Before any chainback object can 219 // move on to the next call in its call queue, it will check the 220 // value of this counter to determine whether it is permitted to do 221 // so. If the counter's value is 0, access is granted; otherwise 222 // the requesting chainback will wait until a pending ajax call 223 // completes to trigger a retry. 224 this.__gcnajaxcount__ = 0; 225 226 var obj = args[0]; 227 var ids; 228 229 switch (jQuery.type(obj)) { 230 case 'null': 231 case 'undefined': 232 break; 233 case 'object': 234 if (typeof obj.id !== 'undefined') { 235 ids = [obj.id]; 236 } 237 break; 238 case 'array': 239 ids = obj; 240 break; 241 default: 242 ids = [obj]; 243 } 244 245 // If one or more id is provided in the instantialization of this 246 // object, only then will this instance be added to its class' 247 // cache. 248 if (ids) { 249 this._setHash(ids.sort().join(',')); 250 this._addToCache(); 251 } 252 253 this._init.apply(this, args); 254 }; 255 256 /** 257 * Causes all promises that are made by this Chainback object, as well 258 * as all derived promises, to be resolved before the given success 259 * callback is invoked to receive this object in a "fulfilled" state. 260 * If a problem is encountered at any point during the resolution of a 261 * promise in the resolution chain, the error function is called, and 262 * further resolution is aborted. 263 * 264 * @private 265 * @param {function(Chainback)} success Callback function to be invoked 266 * when all the promises on which 267 * this object depends have been 268 * completed. 269 * @param {function(GCNError):boolean=} error Optional custom error 270 * handler. 271 */ 272 chainback.prototype._fulfill = function (success, error) { 273 if (this._chain) { 274 this._chain._fulfill(success, error); 275 } else { 276 success(this); 277 } 278 }; 279 280 // inheritance 281 if (props._extends) { 282 var inheritance = (jQuery.type(props._extends) === 'array') 283 ? props._extends 284 : [props._extends]; 285 var i; 286 for (i = 0; i < inheritance.length; i++) { 287 jQuery.extend(chainback.prototype, inheritance[i].prototype); 288 } 289 delete props._extends; 290 } 291 292 // static fields and methods 293 jQuery.extend(chainback, { 294 295 /** 296 * @private 297 * @static 298 * @type {object<string, Chainback>} An associative array holding 299 * instances of this class. Each 300 * instance is mapped against a 301 * hash key generated through 302 * `_makehash()'. 303 */ 304 __gcncache__: {}, 305 306 /** 307 * @private 308 * @static 309 * @type {string} A string that represents this chainback's type. 310 * It is used in generating hashs for instances of 311 * this class. 312 */ 313 __chainbacktype__: props.__chainbacktype__ || 314 Math.random().toString(32), 315 316 /** 317 * @private 318 * @static 319 * @type {boolean} Whether or not we need to use the hash of this 320 * object's parent chainback object in order to 321 * generate a unique hash key when instantiating 322 * objects for this class. 323 */ 324 _needsChainedHash: false, 325 326 /** 327 * Given the arguments "one", "two", "three", will return something 328 * like: "one::two::ChainbackType:three". 329 * 330 * @private 331 * @static 332 * @param {...string} One or more strings to concatenate into the 333 * hash. 334 * @return {string} The hash string. 335 */ 336 _makeHash: function () { 337 var ids = Array.prototype.slice.call(arguments); 338 var id = ids.pop(); 339 ids.push(chainback.__chainbacktype__ + ':' + id); 340 return ids.join('::'); 341 } 342 }); 343 344 var DONT_MERGE = { 345 __gcnmutex__ : true, 346 __gcnorigin__ : true, 347 __gcncallchain__ : true, 348 __gcncallqueue__ : true, 349 __gcnajaxcount__ : true, 350 __gcntempcache__ : true 351 }; 352 353 // Prototype chainback methods and properties 354 355 jQuery.extend(chainback.prototype, { 356 357 /** 358 * @type {Chainback} Each object holds a reference to its 359 * constructor. 360 */ 361 _constructor: chainback, 362 363 /** 364 * Facilitates chaining from one chainback object to another. 365 * 366 * Uses "chainlink" objects to grow and internal linked list of 367 * chainback objects which make up a sort of callee chain. 368 * 369 * A link is created every time a context switch happens 370 * (ie: moving from one API to another). Consider the following: 371 * 372 * page('1').tags().tag('content').render('#content'); 373 * 374 * Accomplishing the above chain of execution will involve 3 375 * different chainable APIs, and 2 different API switches: a page 376 * API flows into a tags collection API, which in turn flows to a 377 * tag API. This method is invoked each time that the exposed API 378 * mutates in this way. 379 * 380 * @private 381 * @param {Chainback} ctor The Chainback class we want to continue 382 * with. 383 * @param {number|string|Array.<number|string>|object} settings 384 * If this argument is not defined, a random hash will be 385 * generated as the object's hash. 386 * An object can be provided instead of an id to directly 387 * instantiate it from JSON data received from the server. 388 * @param {function} success 389 * @param {function} error 390 * @return {Chainback} 391 * @throws UNKNOWN_ARGUMENT If `settings' is not a number, string, 392 * array or object. 393 */ 394 _continue: function (ctor, settings, success, error) { 395 // Is this a fully realized Chainback, or is it a Chainback 396 // which has yet to determine which id it is bound to, from its 397 // parent? 398 var isFetus = false; 399 var ids; 400 var hashInputs = []; 401 402 switch (jQuery.type(settings)) { 403 case 'undefined': 404 case 'null': 405 isFetus = true; 406 break; 407 case 'array': 408 ids = settings.sort().join(','); 409 break; 410 case 'number': 411 case 'string': 412 ids = settings; 413 break; 414 case 'object': 415 ids = settings.id; 416 break; 417 default: 418 GCN.error('UNKNOWN_ARGUMENT', 419 'Don\'t know what to do with the object ' + settings); 420 return; 421 } 422 423 var hash; 424 if (isFetus) { 425 hash = null; 426 } else { 427 var channel = getChannel(this); 428 hash = ctor._needsChainedHash 429 ? ctor._makeHash(this.__gcnhash__, channel + '/' + ids) 430 : ctor._makeHash(channel + '/' + ids); 431 } 432 433 var chainback = GCN.getChainback(ctor, hash, this, 434 [settings, success, error, {}]); 435 436 return chainback; 437 }, 438 439 /** 440 * Terminates any further exection of the functions that remain in 441 * the call queue. 442 * 443 * TODO: Kill all ajax calls. 444 * 445 * @private 446 * @return {Array.<function>} A list of functions that we in this 447 * Chainback's call queue when an abort 448 * happend. 449 */ 450 _abort: function () { 451 this._clearCache(); 452 var callchain = 453 this.__gcncallchain__.concat(this.__gcncallqueue__); 454 this.__gcnmutex__ = true; 455 this.__gcncallchain__ = []; 456 this.__gcncallqueue__ = []; 457 return callchain; 458 }, 459 460 /** 461 * Gets the chainback from which this object was `_continue'd() 462 * from. 463 * 464 * @private 465 * @param {Chainback} 466 * @return {Chainback} This Chainback's ancestor. 467 */ 468 _ancestor: function () { 469 return this._chain; 470 }, 471 472 /** 473 * Locks the semaphore. 474 * 475 * @private 476 * @return {Chainback} This Chainback. 477 */ 478 _procure: function () { 479 this.__gcnmutex__ = false; 480 return this; 481 }, 482 483 /** 484 * Unlocks the semaphore. 485 * 486 * @private 487 * @return {Chainback} This Chainback. 488 */ 489 _vacate: function () { 490 this.__gcnmutex__ = true; 491 this.__release__(); 492 return this; 493 }, 494 495 /** 496 * Halts and forks the main call chain of this chainback object. 497 * Creates a derivitive object that will be used to accomplish 498 * operations that need to complete before the main chain is 499 * permitted to proceed. Before execution on the main chainback 500 * object is restarted, the forked derivitive object is merged into 501 * the original chainback instance. 502 * 503 * @private 504 * @return {Chainback} A derivitive Chainback object forked from 505 * this Chainback instance. 506 */ 507 _fork: function () { 508 var that = this; 509 this._procure(); 510 var Fork = function ChainbackFork() { 511 var prop; 512 for (prop in that) { 513 if (that.hasOwnProperty(prop) && !DONT_MERGE[prop]) { 514 this[prop] = that[prop]; 515 } 516 } 517 this.__gcnorigin__ = that; 518 this.__gcnmutex__ = true; 519 this.__gcncallchain__ = []; 520 this.__gcncallqueue__ = []; 521 }; 522 Fork.prototype = new this._constructor(); 523 return new Fork(); 524 }, 525 526 /** 527 * Transfers the state of this derivitive into its origin. 528 * 529 * @private 530 */ 531 _merge: function () { 532 if (!this.__gcnorigin__) { 533 return; 534 } 535 var origin = this.__gcnorigin__; 536 var prop; 537 for (prop in this) { 538 if (this.hasOwnProperty(prop) && !DONT_MERGE[prop]) { 539 origin[prop] = this[prop]; 540 } 541 } 542 origin._vacate(); 543 return origin; 544 }, 545 546 /** 547 * Wraps jQuery's `ajax' method. Queues the callbacks in the chain 548 * call so that they can be invoked synchonously. Without blocking 549 * the browser thread. 550 * 551 * @private 552 * @param {object} settings 553 */ 554 _queueAjax: function (settings) { 555 if (settings.json) { 556 settings.data = JSON.stringify(settings.json); 557 delete settings.json; 558 } 559 560 settings.dataType = 'json'; 561 settings.contentType = 'application/json; charset=utf-8'; 562 settings.error = (function (onError) { 563 return function (xhr, status, error) { 564 var throwException = true; 565 if (onError) { 566 throwException = onError(GCN.createError('HTTP_ERROR', error, xhr)); 567 } 568 if (throwException !== false) { 569 GCN.error('AJAX_ERROR', error, xhr); 570 } 571 }; 572 }(settings.error)); 573 574 // Duck-type the complete callback, or add one if not provided. 575 // We use complete to forward the continuation because it is 576 // the last callback to be executed the jQuery ajax callback 577 // sequence. 578 settings.complete = (function (chainback, onComplete, opts) { 579 return function () { 580 --chainback.__gcnajaxcount__; 581 onComplete.apply(chainback, arguments); 582 callNext(chainback); 583 }; 584 }(this, settings.complete || function () {}, settings)); 585 586 ++this.__gcnajaxcount__; 587 588 GCN.ajax(settings); 589 }, 590 591 /** 592 * Clears the cache for this individual object. 593 * 594 * @private 595 * @return {Chainback} This Chainback. 596 */ 597 _clearCache: function () { 598 if (chainback.__gcncache__[this.__gcnhash__]) { 599 delete chainback.__gcncache__[this.__gcnhash__]; 600 } 601 return this; 602 }, 603 604 /** 605 * Add this object to the cache, using its hash as the key. 606 * 607 * @private 608 * @return {Chainback} This Chainback. 609 */ 610 _addToCache: function () { 611 this._constructor.__gcncache__[this.__gcnhash__] = this; 612 return this; 613 }, 614 615 /** 616 * Removes the given chainback instance from the temporary cache, 617 * usually after the chainback instance has matured from a "fetus" 618 * into a fully realized chainback object. 619 * 620 * @param {Chainback} instance The chainback instance to remove. 621 * @return {boolean} True if this chainback instance was found and 622 * removed, false if it could not be found. 623 */ 624 _removeFromTempCache: function (instance) { 625 var hash; 626 var cache = instance._ancestor().__gcntempcache__; 627 for (hash in cache) { 628 if (cache.hasOwnProperty(hash) && instance === cache[hash]) { 629 delete cache[hash]; 630 return true; 631 } 632 } 633 return false; 634 }, 635 636 /** 637 * @private 638 * @param {string|number} str 639 * @return {Chainback} This Chainback. 640 */ 641 _setHash: function (str) { 642 var ctor = this._constructor; 643 var addAncestorHash = ctor._needsChainedHash && this._chain; 644 var channel = getChannel(this); 645 var hash = addAncestorHash 646 ? ctor._makeHash(this._chain.__gcnhash__, channel + '/' + str) 647 : ctor._makeHash(channel + '/' + str); 648 this.__gcnhash__ = hash; 649 return this; 650 }, 651 652 /** 653 * Invokes the given callback function while ensuring that any 654 * exceptions that occur during the invocation of the callback, 655 * will be caught and allow the Chainback object to complete its 656 * all remaining queued calls. 657 * 658 * @param {function} callback The function to invoke. 659 * @param {Array.<*>=} args A list of object that will be passed as 660 * arguments into the callback function. 661 */ 662 _invoke: function (callback, args) { 663 if (typeof callback !== 'function') { 664 return; 665 } 666 try { 667 if (args && args.length) { 668 callback.apply(null, args); 669 } else { 670 callback(); 671 } 672 } catch (ex) { 673 setTimeout(function () { 674 throw ex; 675 }, 1); 676 } 677 } 678 679 }); 680 681 /** 682 * Causes the chainback call queue to start running again once a lock 683 * has been released. This function is defined here because it needs 684 * to be made chainable. 685 * 686 * @private 687 */ 688 props.__release__ = function () {}; 689 690 var propName; 691 var propValue; 692 693 // Generates the chainable callback methods. Transforms all functions 694 // whose names do not start with the "!" character into chainable 695 // callback prototype methods. 696 for (propName in props) { 697 if (props.hasOwnProperty(propName)) { 698 propValue = props[propName]; 699 if (jQuery.type(propValue) === 'function' && 700 propName.charAt(0) !== '!') { 701 chainback.prototype[propName] = 702 makeMethodChainable(propValue, propName); 703 } else { 704 if (propName.charAt(0) === '!') { 705 propName = propName.substring(1, propName.length); 706 } 707 chainback.prototype[propName] = propValue; 708 } 709 } 710 } 711 712 return chainback; 713 }; 714 715 }(GCN)); 716