      1 var hitch = require("./base/functions").hitch,
      2     define = require("./define").define, base = require("./base");
      3
      4 var Promise = define(null, {
      5     instance:{
      6         /** @lends comb.Promise.prototype */
      7         __fired:false,
      8
      9         __results:null,
     10
     11         __error:null,
     12
     13         __errorCbs:null,
     14
     15         __cbs:null,
     16
     17         /**
     18          * Promise object used for handling a thread
     19          *@example
     20          *          var myFunc = function(){
     21          *              var promise = new Promise();
     22          *              //callback the promise after 10 Secs
     23          *              setTimeout(hitch(promise, "callback"), 10000);
     24          *              return promise;
     25          *          }
     26          *          var myFunc2 = function(){
     27          *              var promises =[];
     28          *              for(var i = 0; i < 10; i++){
     29          *                  promises.push(myFunc);
     30          *              }
     31          *              //create a new promise list with all 10 promises
     32          *              return new PromiseList(promises);
     33          *          }
     34          *
     35          *          myFunc.then(do something...)
     36          *          myFunc.addCallback(do something...)
     37          *          myFunc.cain(myfunc).then(do something...)
     38          *          myFunc.cain(myfunc).addCallback(do something...)
     39          *
     40          *          myFunc2.then(do something...)
     41          *          myFunc2.addCallback(do something...)
     42          *          myFunc2.cain(myfunc).then(do something...)
     43          *          myFunc2.cain(myfunc).addCallback(do something...)
     44          *  @memberOf comb
     45          * @constructs
     46          */
     47         constructor:function(){
     48             this.__errorCbs = [];
     49             this.__cbs = [];
     50         },
     51         /**
     52          * @private
     53          */
     54         __resolve:function(){
     55             if (!this.__fired) {
     56                 this.__fired = true;
     57                 var cbs = this.__error ? this.__errorCbs : this.__cbs,
     58                     len = cbs.length, i,
     59                     results = this.__error || this.__results;
     60                 for (i = 0; i < len; i++) {
     61                     cbs[i].apply(this, results);
     62                 }
     63             }
     64         },
     65
     66         /**
     67          * Add a callback to the callback chain of the promise
     68          *
     69          *
     70          * @param {Function} cb the function to callback when the promise is resolved
     71          */
     72         addCallback:function(cb){
     73             if (cb) {
     74                 if (this.__fired && this.__results) {
     75                     cb.apply(this, this.__results);
     76                 } else {
     77                     this.__cbs.push(cb);
     78                 }
     79             }
     80             return this;
     81         },
     82
     83
     84         /**
     85          * Add a callback to the errback chain of the promise
     86          *
     87          * @param {Function} cb the function to callback when the promise errors
     88          */
     89         addErrback:function(cb){
     90             if (cb) {
     91                 if (this.__fired && this.__error) {
     92                     cb.apply(this, this.__error);
     93                 } else {
     94                     this.__errorCbs.push(cb);
     95                 }
     96             }
     97         },
     98
     99         both:function(cb){
    100             this.addCallback(cb);
    101             this.addErrback(cb);
    102             return this;
    103         },
    104
    105         /**
    106          * When called all functions registered as callbacks are called with the passed in results.
    107          *
    108          * @param anything variable number of results to pass back to listeners of the promise
    109          */
    110         callback:function(){
    111             if (this.__fired) {
    112                 throw new Error("Already fired!");
    113             }
    114             this.__results = Array.prototype.slice.call(arguments);
    115             this.__resolve();
    116             return this;
    117         },
    118
    119         /**
    120          * When called all functions registered as errbacks are called with the passed in error(s)
    121          *
    122          * @param anything variable number of errors to pass back to listeners of the promise
    123          */
    124         errback:function(i){
    125             if (this.__fired) {
    126                 throw new Error(i);
    127             }
    128             this.__error = Array.prototype.slice.call(arguments);
    129             this.__resolve();
    130             return this;
    131         },
    132
    133         /**
    134          * Call to specify action to take after promise completes or errors
    135          *
    136          * @param {Function} [callback=null] function to call after the promise completes successfully
    137          * @param {Function} [errback=null] function to call if the promise errors
    138          */
    139         then:function(callback, errback){
    140             this.addCallback(callback);
    141             this.addErrback(errback);
    142             return this;
    143         },
    144
    145         /**
    146          * Call to chaining of promises
    147          * @param callback method to call that returns a promise to call after this one completes.
    148          * @param errback method to call if this promise errors.
    149          */
    150         chain:function(callback, errback){
    151             var promise = new Promise();
    152             this.addCallback(function(results){
    153                 callback.call(this, results).then(hitch(promise, "callback"), hitch(promise, "errback"));
    154             });
    155             this.addErrback(errback);
    156             return promise;
    157         },
    158
    159         /**
    160          * Applies the same function that returns a promise to both the callback and errback.
    161          *
    162          * @param {Function} callback function to call. This function must return a Promise
    163          *
    164          * @return {comb.Promise} a promise to continue chaining or to resolve with.
    165          *
    166          */
    167         chainBoth:function(callback){
    168             var p = this.chain(callback);
    169             this.addErrback(function(results){
    170                 callback.call(this, results).then(hitch(p, "callback"), hitch(p, "errback"));
    171             });
    172             return p;
    173         }
    174
    175
    176     }
    177 });
    178
    179
    180 var PromiseList = define(Promise, {
    181     instance:{
    182         /** @lends comb.PromiseList.prototype */
    183
    184         /*@private*/
    185         __results:null,
    186
    187         /*@private*/
    188         __errors:null,
    189
    190         /*@private*/
    191         __promiseLength:0,
    192
    193         /*@private*/
    194         __defLength:0,
    195
    196         /*@private*/
    197         __firedLength:0,
    198
    199         normalizeResults:false,
    200
    201         /**
    202          *
    203          *  PromiseList object used for handling a list of Promises
    204          * @example         var myFunc = function(){
    205          *              var promise = new Promise();
    206          *              //callback the promise after 10 Secs
    207          *              setTimeout(hitch(promise, "callback"), 10000);
    208          *              return promise;
    209          *          }
    210          *          var myFunc2 = function(){
    211          *              var promises =[];
    212          *              for(var i = 0; i < 10; i++){
    213          *                  promises.push(myFunc);
    214          *              }
    215          *              //create a new promise list with all 10 promises
    216          *              return new PromiseList(promises);
    217          *          }
    218          *          var pl = new comb.PomiseList([myFunc(), myFunc2()]);
    219          *          pl.then(do something...)
    220          *          pl.addCallback(do something...)
    221          *          pl.cain(myfunc).then(do something...)
    222          *          pl.cain(myfunc).addCallback(do something...)
    223          *
    224          *  @param {comb.Promise[]} [defs=[]] the list of promises
    225          * @constructs
    226          * @augments comb.Promise
    227          * @memberOf comb
    228          * */
    229         constructor:function(defs, normalizeResults){
    230             this.__defLength = defs.length;
    231             this.__errors = [];
    232             this.__results = [];
    233             this.normalizeResults = base.isBoolean(normalizeResults) ? normalizeResults : false;
    234             this._super(arguments);
    235             defs.forEach(this.__addPromise, this);
    236         },
    237
    238         /**
    239          * Add a promise to our chain
    240          * @private
    241          * @param promise the promise to add to our chain
    242          * @param i the index of the promise in our chain
    243          */
    244         __addPromise:function(promise, i){
    245             promise.addCallback(hitch(this, function(){
    246                 var args = Array.prototype.slice.call(arguments);
    247                 args.unshift(i);
    248                 this.callback.apply(this, args);
    249             }));
    250             promise.addErrback(hitch(this, function(){
    251                 var args = Array.prototype.slice.call(arguments);
    252                 args.unshift(i);
    253                 this.errback.apply(this, args);
    254             }));
    255         },
    256
    257         /**
    258          * Resolves the promise
    259          * @private
    260          */
    261         __resolve:function(){
    262             if (!this.__fired) {
    263                 this.__fired = true;
    264                 var cbs = this.__errors.length ? this.__errorCbs : this.__cbs,
    265                     len = cbs.length, i,
    266                     results = this.__errors.length ? this.__errors : this.__results;
    267                 for (i = 0; i < len; i++) {
    268                     cbs[i].call(this, results);
    269                 }
    270             }
    271         },
    272
    273         addCallback:function(cb){
    274             if (cb) {
    275                 if (this.__fired && !this.__errors.length) {
    276                     cb.call(this, this.__results);
    277                 } else {
    278                     this.__cbs.push(cb);
    279                 }
    280             }
    281             return this;
    282         },
    283
    284         addErrback:function(cb){
    285             if (cb) {
    286                 if (this.__fired && this.__errors.length) {
    287                     cb.call(this, this.__errors);
    288                 } else {
    289                     this.__errorCbs.push(cb);
    290                 }
    291             }
    292             return this;
    293         },
    294
    295
    296         callback:function(i){
    297             if (this.__fired) {
    298                 throw new Error("Already fired!");
    299             }
    300             var args = base.argsToArray(arguments);
    301             if (this.normalizeResults) {
    302                 args = args.slice(1);
    303                 args = args.length == 1 ? args.pop() : args;
    304             }
    305             this.__results[i] = args;
    306             this.__firedLength++;
    307             if (this.__firedLength == this.__defLength) {
    308                 this.__resolve();
    309             }
    310             return this;
    311         },
    312
    313
    314         errback:function(i){
    315             if (this.__fired) {
    316                 throw new Error("Already fired!");
    317             }
    318             var args = base.argsToArray(arguments);
    319             if (this.normalizeResults) {
    320                 args = args.slice(1);
    321                 args = args.length == 1 ? args.pop() : args;
    322             }
    323             this.__errors[i] = args;
    324             this.__firedLength++;
    325             if (this.__firedLength == this.__defLength) {
    326                 this.__resolve();
    327             }
    328             return this;
    329         }
    330
    331     }
    332 });
    333 exports.Promise = Promise;
    334 exports.PromiseList = PromiseList;
    335
    336
    337 var createHandler = function(obj, stack){
    338     return {
    339
    340         delete:function(name){
    341             stack.push([obj, ["__delete__", [name]], [], obj]);
    342             return true;
    343         },
    344         get:function(receiver, name){
    345             var newStack = [];
    346             var handle = getHandler({}, newStack);
    347             var wrapper = base.createFunctionWrapper(handle, function(m){
    348                 var i = stack[stack.indexOf(item)];
    349                 var args = base.argsToArray(arguments);
    350                 i[1].push(name === "__apply__" || name === "__new__" ? base.array.flatten(args) : args);
    351                 return wrapper;
    352             });
    353             var item = [obj, [name], newStack, wrapper];
    354             stack.push(item);
    355             return wrapper;
    356         },
    357         set:function(receiver, name, val){
    358             stack.push([obj, ["__set__", [name, val]], [], obj]);
    359             return true;
    360         }, // bad behavior when set fails in non-strict mode
    361         enumerate:function(){
    362             stack.push([obj, ["__enumerate__"], [], obj]);
    363             return [];
    364         }
    365
    366     }
    367 };
    368
    369 var functionHandler = function(handle, stack){
    370     var wrapper = base.createFunctionWrapper(handle, function(m){
    371         return handle.__apply__(base.argsToArray(arguments));
    372     }, function(){
    373         return handle.__new__(base.argsToArray(arguments));
    374     });
    375     return wrapper;
    376 };
    377
    378 var getHandler = function(obj, stack){
    379     var prox = base.isObject(obj) || base.isFunction(obj) ? base.handlerProxy({}, createHandler(obj, stack)) : obj, ret;
    380     if (base.isFunction(obj)) {
    381         return functionHandler(prox, stack);
    382     } else {
    383         ret = prox;
    384     }
    385     return ret;
    386 };
    387
    388 var getValueFromArrayMap = function(arg, pMap, handles){
    389     var result;
    390     if (base.isProxy(arg)) {
    391         var results = pMap.filter(function(p){
    392             return p[0] === arg;
    393         });
    394         if (results.length) {
    395             result = results[0][1];
    396         } else {
    397             !results.length && (results = handles.filter(function(h){
    398                 return h[1] === arg;
    399             }));
    400             if (results.length) {
    401                 result = results[0][0];
    402             }
    403         }
    404     } else if (base.isHash(arg) || base.isArray(arg)) {
    405         result = arg;
    406         for (var i in arg) {
    407             var a = arg[i];
    408             arg[i] = getValueFromArrayMap(a, pMap, handles);
    409         }
    410     } else {
    411         result = arg;
    412     }
    413     return result;
    414 };
    415
    416 var executeStack = function(stack, handles, pMap){
    417     var ret = new Promise(), l = stack.length, results = [];
    418     pMap = pMap || [];
    419     var errored = false;
    420     if (l) {
    421         var exec = function(i){
    422             if (i < l) {
    423                 var curr = stack[i], obj = getValueFromArrayMap(curr[0], pMap, handles), m = curr[1], name = m[0], args = m[1], subStack = curr[2], handle = curr[3];
    424                 var p;
    425                 try {
    426                     if (name === "__set__") {
    427                         p = (obj[args[0]] = getValueFromArrayMap(args[1], pMap, handles));
    428                     } else if (name === "__delete__") {
    429                         p = delete obj[args[0]];
    430                     } else if (name === "__keys__" || name === "__enumerate__") {
    431                         throw new Error(name.replace(/^__|__$/g, "") + " is not supported");
    432                     } else if (name === "__apply__") {
    433                         p = (obj.apply(obj, getValueFromArrayMap(args, pMap, handles)));
    434                     } else if (name === "__new__") {
    435                         try {
    436                             p = new obj();
    437                         } catch (ignore) {
    438                         }
    439                         obj.apply(p, getValueFromArrayMap(args, pMap, handles));
    440                     } else if (base.isUndefined(args)) {
    441                         p = obj[name];
    442                     } else {
    443                         var f;
    444                         if (!base.isUndefined((f = obj[name])) && base.isFunction(f)) {
    445                             p = f.apply(obj, args.map(function(arg){
    446                                 return getValueFromArrayMap(arg, pMap, handles);
    447                             }));
    448                         } else {
    449                             //Purposely call to throw an ERROR!!!!
    450                             obj[name]();
    451                         }
    452                     }
    453                 } catch (e) {
    454                     errored = true;
    455                     return ret.errback(e);
    456                 }
    457                 exports.when(p, hitch(this, function(res){
    458                     if (subStack.length) {
    459                         subStack.forEach(function(ss){
    460                             ss[0] = res;
    461                         });
    462                         executeStack(subStack, handles, pMap).then(hitch(this, function(sres){
    463                             pMap.push([handle, res]);
    464                             results.push(sres);
    465                             !errored && exec(++i);
    466                         }), hitch(ret, "errback"));
    467                     } else {
    468                         pMap.push([handle, res]);
    469                         results.push(res);
    470                         !errored && exec(++i);
    471                     }
    472                 }), hitch(ret, "errback"))
    473             } else {
    474                 !errored && ret.callback(results, pMap);
    475             }
    476         };
    477         exec(0);
    478     } else {
    479         ret.callback(results, pMap);
    480     }
    481     return ret;
    482 };
    483
    484 base.merge(exports, {
    485     /**
    486      * @lends comb
    487      */
    488
    489     /**
    490      * This method allows one to code asynchronous code in a synchronous manner.
    491      *
    492      *     <p>
    493      *         <b>
    494      *             Using Object.define[rest of name] on objects passed will result in unexpected behavior.</br>
    495      *             Enumerating passed in object keys is not currently supported. i.e for in loops on objects.
    496      *             using array enumeration methods will work though!!
    497      *         </b>
    498      *     </p>
    499      *
    500      * @example
    501      *
    502      * var staticValueFunction = function (value) {
    503      *      return comb.argsToArray(arguments).join(" ");
    504      * };
    505      *
    506      * var promiseValueFunction = function (value) {
    507      *      var ret = new comb.Promise();
    508      *      setTimeout(comb.hitch(ret, "callback", comb.argsToArray(arguments).join(" ")), 100);
    509      *      return ret;
    510      * };
    511      *
    512      * var hash = {
    513      *      staticValueFunction:staticValueFunction,
    514      *      promiseValueFunction:promiseValueFunction
    515      * };
    516      *
    517      * var p = comb.executeInOrder(hash, staticValueFunction, promiseValueFunction, function (hash, staticValueFunction, promiseValueFunction) {
    518      *     var toBe = staticValueFunction(promiseValueFunction("to"), "be");
    519      *     var notToBe = hash.promiseValueFunction("or", hash.staticValueFunction("not", toBe));
    520      *     return hash.promiseValueFunction(toBe, notToBe);
    521      * });
    522      * p.addCallback(function(ret){
    523      *     console.log(ret); //=>"to be or not to be"
    524      * });
    525      * @param {Object...} args variable number of objects.
    526      * @param {Function} cb the function to callback to execute in order
    527      * @returns comb.Promise
    528      *
    529      */
    530     executeInOrder:function(args, cb){
    531         args = base.argsToArray(arguments);
    532         cb = base.isFunction(args[args.length - 1]) ? args.pop() : null;
    533         var ret = new Promise();
    534         if (cb) {
    535             var stack = [];
    536             var newArgs = args.map(function(a){
    537                 return [a, getHandler(a, stack)];
    538             });
    539             var cbRet = cb.apply(null, newArgs.map(function(h){
    540                 return h[1];
    541             }));
    542             executeStack(stack, newArgs).then(function(results, pMap){
    543                 if (base.isUndefined(cbRet)) {
    544                     ret.callback(results);
    545                 }
    546                 else {
    547                     var cbResults;
    548                     if (base.isArray(cbRet)) {
    549                         cbResults = cbRet.map(
    550                             function(arg){
    551                                 return getValueFromArrayMap(arg, pMap, newArgs);
    552                             }).filter(function(r){
    553                                 return !base.isUndefined(r)
    554                             });
    555                     } else if (base.isHash(cbRet)) {
    556                         cbResults = {};
    557                         for (var i in cbRet) {
    558                             cbResults[i] = getValueFromArrayMap(cbRet[i], pMap, newArgs);
    559                         }
    560                     } else if (base.isProxy(cbRet)) {
    561                         cbResults = getValueFromArrayMap(cbRet, pMap, newArgs);
    562                     } else {
    563                         cbResults = cbRet;
    564                     }
    565                     ret.callback(cbResults);
    566                 }
    567             }, hitch(ret, "errback"));
    568
    569         } else {
    570             ret.callback();
    571         }
    572         return ret;
    573     },
    574
    575     /**
    576      * Tests if an object is like a promise (i.e. it contains then, addCallback, addErrback)
    577      * @param obj object to test
    578      */
    579     isPromiseLike:function(obj){
    580         return !base.isUndefinedOrNull(obj) && (base.isInstanceOf(obj, Promise) || (base.isFunction(obj.then) && base.isFunction(obj.addCallback) && base.isFunction(obj.addErrback)));
    581     },
    582
    583     /**
    584      * Waits for promise and non promise values to resolve and fires callback or errback appropriately.
    585      *
    586      * @example
    587      *  var a = "hello";
    588      *  var b = new comb.Promise().callback(world);
    589      *  comb.when(a, b) => called back with ["hello", "world"];
    590      *
    591      * @param {Anything...} args variable number of arguments to wait for
    592      * @param {Function} cb the callback function
    593      * @param {Function} eb the errback function
    594      * @returns {comb.Promise} a promise that is fired when all values have resolved
    595      */
    596     when:function(args, cb, eb){
    597         var args = base.argsToArray(arguments), p;
    598         eb = base.isFunction(args[args.length - 1]) ? args.pop() : null;
    599         cb = base.isFunction(args[args.length - 1]) ? args.pop() : null;
    600         if (eb && !cb) {
    601             cb = eb;
    602             eb = null;
    603         }
    604         if (!args.length) {
    605             p = new Promise().callback(args);
    606         } else if (args.length == 1) {
    607             args = args.pop();
    608             p = exports.isPromiseLike(args) ? args : new Promise().callback(args);
    609         } else {
    610             var p = new PromiseList(args.map(function(a){
    611                 return exports.isPromiseLike(a) ? a : new Promise().callback(a);
    612             }), true);
    613         }
    614         if (cb) {
    615             p.addCallback(cb);
    616         }
    617         if (eb) {
    618             p.addErrback(eb);
    619         }
    620         return p;
    621
    622     }
    623
    624 });
    625