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