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("Already fired!");
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         chainBoth : function(callback) {
160             var p = this.chain(callback);
161             this.addErrback(function(results) {
162                 callback.call(this, results).then(hitch(p, "callback"), hitch(p, "errback"));
163             });
164             return p;
165         }
166 
167 
168     }
169 });
170 
171 
172 var PromiseList = define(Promise, {
173     instance : {
174         /** @lends comb.PromiseList.prototype */
175 
176         /*@private*/
177         __results : null,
178 
179         /*@private*/
180         __errors : null,
181 
182         /*@private*/
183         __promiseLength : 0,
184 
185         /*@private*/
186         __defLength : 0,
187 
188         /*@private*/
189         __firedLength : 0,
190 
191         /**
192          *
193          *  PromiseList object used for handling a list of Promises
194          * @example         var myFunc = function(){
195          *              var promise = new Promise();
196          *              //callback the promise after 10 Secs
197          *              setTimeout(hitch(promise, "callback"), 10000);
198          *              return promise;
199          *          }
200          *          var myFunc2 = function(){
201          *              var promises =[];
202          *              for(var i = 0; i < 10; i++){
203          *                  promises.push(myFunc);
204          *              }
205          *              //create a new promise list with all 10 promises
206          *              return new PromiseList(promises);
207          *          }
208          *          var pl = new comb.PomiseList([myFunc(), myFunc2()]);
209          *          pl.then(do something...)
210          *          pl.addCallback(do something...)
211          *          pl.cain(myfunc).then(do something...)
212          *          pl.cain(myfunc).addCallback(do something...)
213          *
214          *  @param {comb.Promise[]} [defs=[]] the list of promises
215          * @constructs
216          * @augments comb.Promise
217          * @memberOf comb
218          * */
219         constructor : function(defs) {
220             this.__defLength = defs.length;
221             this.__errors = [];
222             this.__results = [];
223             this._super(arguments);
224             defs.forEach(this.__addPromise, this);
225         },
226 
227         /**
228          * Add a promise to our chain
229          * @private
230          * @param promise the promise to add to our chain
231          * @param i the index of the promise in our chain
232          */
233         __addPromise : function(promise, i) {
234             promise.addCallback(hitch(this, function() {
235                 var args = Array.prototype.slice.call(arguments);
236                 args.unshift(i);
237                 this.callback.apply(this, args);
238             }));
239             promise.addErrback(hitch(this, function() {
240                 var args = Array.prototype.slice.call(arguments);
241                 args.unshift(i);
242                 this.errback.apply(this, args);
243             }));
244         },
245 
246         /**
247          * Resolves the promise
248          * @private
249          */
250         __resolve : function() {
251             if (!this.__fired) {
252                 this.__fired = true;
253                 var cbs = this.__errors.length ? this.__errorCbs : this.__cbs,
254                     len = cbs.length, i,
255                     results = this.__errors.length ? this.__errors : this.__results;
256                 for (i = 0; i < len; i++) {
257                     cbs[i].call(this, results);
258                 }
259             }
260         },
261 
262         addCallback : function(cb) {
263             if (cb) {
264                 if (this.__fired && !this.__errors.length) {
265                     cb.call(this, this.__results);
266                 } else {
267                     this.__cbs.push(cb);
268                 }
269             }
270             return this;
271         },
272 
273         addErrback : function(cb) {
274             if (cb) {
275                 if (this.__fired && this.__errors.length) {
276                     cb.call(this, this.__errors);
277                 } else {
278                     this.__errorCbs.push(cb);
279                 }
280             }
281             return this;
282         },
283 
284 
285         callback : function(i) {
286             if (this.__fired) {
287                 throw new Error("Already fired!");
288             }
289             this.__results[i] = (Array.prototype.slice.call(arguments));
290             this.__firedLength++;
291             if (this.__firedLength == this.__defLength) {
292                 this.__resolve();
293             }
294             return this;
295         },
296 
297 
298         errback : function(i) {
299             if (this.__fired) {
300                 throw new Error("Already fired!");
301             }
302             this.__errors[i] = Array.prototype.slice.call(arguments);
303             this.__firedLength++;
304             if (this.__firedLength == this.__defLength) {
305                 this.__resolve();
306             }
307             return this;
308         }
309 
310     }
311 });
312 exports.Promise = Promise;
313 exports.PromiseList = PromiseList;
314 
315 var createHandler = function(obj, stack) {
316     return {
317         getOwnPropertyDescriptor: function(name) {
318             var desc = Object.getOwnPropertyDescriptor(obj, name);
319             // a trapping proxy's properties must always be configurable
320             if (desc !== undefined) {
321                 desc.configurable = true;
322             }
323             return desc;
324         },
325         getPropertyDescriptor:  function(name) {
326             var desc = Object.getPropertyDescriptor(obj, name); // not in ES5
327             // a trapping proxy's properties must always be configurable
328             if (desc !== undefined) {
329                 desc.configurable = true;
330             }
331             return desc;
332         },
333         getOwnPropertyNames: function() {
334             throw new Error("Cannot get own property names")
335         },
336         getPropertyNames: function() {
337             throw new Error("Cannot get property names")
338         },
339         defineProperty: function(name, desc) {
340             throw new Error("Cannot define property")
341         },
342         delete: function(name) {
343             throw new Error("Cannot delete");
344         },
345         fix:          function() {
346             throw new Error("Cannot fix");
347         },
348 
349         has: function(name) {
350             return name in obj;
351         },
352         hasOwn: function(name) {
353             return ({}).hasOwnProperty.call(obj, name);
354         },
355         get: function(receiver, name) {
356             var newStack = [];
357             var handle = getHandler({}, newStack);
358             var wrapper = base.createFunctionWrapper(handle, function(m) {
359                 var i = stack[stack.indexOf(item)];
360                 i[1].push(base.argsToArray(arguments));
361                 return wrapper;
362             });
363             var item = [obj, [name], newStack, wrapper];
364             stack.push(item);
365             return wrapper;
366         },
367         set: function(receiver, name, val) {
368             stack.push([obj, ["__set__", [name, val]], [], {}]);
369             return true;
370         }, // bad behavior when set fails in non-strict mode
371         enumerate:    function() {
372             throw new Error("Cannot enumerate");
373         },
374         keys: function() {
375             throw new Error("Cannot retrieve keys")
376         }
377 
378     }
379 };
380 
381 var getHandler = function(obj, stack) {
382     return base.isObject(obj) ? base.handlerProxy({}, createHandler(obj, stack)) : obj;
383 };
384 
385 var getValueFromArrayMap = function(arg, pMap) {
386     var result;
387     if (base.isProxy(arg)) {
388         var results = pMap.filter(function(p) {
389             return p[0] == arg;
390         });
391         if (results.length) {
392             result = results[0][1];
393         }
394     } else {
395         result = arg;
396     }
397     return result;
398 };
399 
400 var executeStack = function(stack, pMap) {
401     var ret = new Promise(), l = stack.length, results = [];
402     pMap = pMap || [];
403     var errored = false;
404     if (l) {
405         var exec = function(i) {
406             if (i < l) {
407                 var curr = stack[i], obj = getValueFromArrayMap(curr[0], pMap), m = curr[1], name = m[0], args = m[1], subStack = curr[2], handle = curr[3];
408                 var p;
409                 try {
410                     if (name == "__set__") {
411                         p = obj[args[0]] = getValueFromArrayMap(args[1], pMap);
412                     } else if (base.isUndefined(args)) {
413                         p = obj[name];
414                     } else {
415                         var f;
416                         if (!base.isUndefined((f = obj[name])) && base.isFunction(f)) {
417                             p = f.apply(obj, args.map(function(arg) {
418                                 return getValueFromArrayMap(arg, pMap);
419                             }));
420                         }
421                     }
422                 } catch(e) {
423                     errored = true;
424                     return ret.errback(new Error(e));
425                 }
426 
427                 if (base.isInstanceOf(p, Promise)) {
428                     p.then(hitch(this, function(res) {
429                         if (subStack.length) {
430                             subStack.forEach(function(ss) {
431                                 ss[0] = res;
432                             });
433                             executeStack(subStack, pMap).then(hitch(this, function(sres) {
434                                 pMap.push([handle, res]);
435                                 results.push(sres);
436                                 !errored && exec(++i);
437                             }), hitch(ret, "errback"));
438                         } else {
439                             pMap.push([handle, res]);
440                             results.push(res);
441                             !errored && exec(++i);
442                         }
443                     }), hitch(ret, "errback"));
444                 } else {
445                     if (subStack.length) {
446                         subStack.forEach(function(ss) {
447                             ss[0] = p;
448                         });
449                         executeStack(subStack, pMap).then(hitch(this, function(sres) {
450                             pMap.push([handle, p]);
451                             results.push(sres);
452                             !errored && exec(++i);
453                         }), hitch(ret, "errback"));
454                     } else {
455                         pMap.push([handle, p]);
456                         results.push(p);
457                         !errored && exec(++i);
458                     }
459                 }
460             } else {
461                 !errored && ret.callback(results, pMap);
462             }
463         };
464         exec(0);
465     } else {
466         ret.callback(results, pMap);
467     }
468     return ret;
469 };
470 
471 base.merge(exports, {
472     /**
473      * @lends comb
474      */
475 
476     /**
477      * This method allows one to code asynchronous code in a synchronous manner.
478      *
479      * @example
480      * var a = {
481      *      promiseMethod : function(){
482      *          var ret = new Promise();
483      *          setTimeout(comb.hitch(ret, "callback", 10), 1000);
484      *          return ret;
485      *      }
486      * }
487      *
488      * var p = comb.executeInOrder(a, function(a){
489      *      var x = a.promiseMethod();
490      *      var y = a.promiseMethod();
491      *      return [x, y];
492      * });
493      *
494      * p.then(function(ret){
495      *    console.log(ret) //=>[10, 10];
496      * });
497      * @param {Object...} args variable number of objects.
498      * @param {Funciton} cb the function to callback to execute in order
499      * @returns comb.Promise
500      *
501      */
502     executeInOrder : function(args, cb) {
503         args = base.argsToArray(arguments);
504         cb = base.isFunction(args[args.length - 1]) ? args.pop() : null;
505         var ret = new Promise();
506         if (cb) {
507             var stack = [];
508             var newArgs = args.map(function(a) {
509                 return getHandler(a, stack);
510             });
511             var cbRet = cb.apply(null, newArgs);
512             executeStack(stack).then(function(results, pMap) {
513                 if (base.isUndefined(cbRet)) {
514                     ret.callback(results);
515                 }
516                 else {
517                     var cbResults;
518                     if (base.isArray(cbRet)) {
519                         cbResults = cbRet.map(
520                             function(arg) {
521                                 return getValueFromArrayMap(arg, pMap);
522                             }).filter(function(r) {
523                                 return !base.isUndefined(r)
524                             });
525                     } else if (base.isHash(cbRet)) {
526                         cbResults = {};
527                         for (var i in cbRet) {
528                             cbResults[i] = getValueFromArrayMap(cbRet[i], pMap);
529                         }
530                     } else if (base.isProxy(cbRet)) {
531                         cbResults = getValueFromArrayMap(cbRet, pMap);
532                     } else {
533                         cbResults = cbRet;
534                     }
535                     ret.callback(cbResults);
536                 }
537             }, hitch(ret, "errback"));
538 
539         } else {
540             ret.callback();
541         }
542         return ret;
543     };
544 
545 })
546 ;