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