Source: promise.js

'use strict';
var
Legio = require("legio"),
construct = require("legio/construct"),
Task = require("./task");

/** @module legio-async/promise */

/**
 * @constructor
 * @alias module:legio-async/promise
 */
var Promise = construct({
  init: function () {
    this.onFulfilledHandlers = [];
    this.onRejectedHandlers = [];
    this.onNotifiedHandlers = [];

    this.changes = [];

    this.promises = [];
  },

  /** @lends module:legio-async/promise.prototype */
  proto: {
    /** @type {Boolean} */
    pending: true,
    /** @type {Thenable} */
    awaiting: null,
    /** @type {Boolean} */
    fulfilled: false,
    /** @type {Boolean} */
    rejected: false,

    /**
     * @param {Function} [onFulfilled]
     * @param {Function} [onRejected]
     * @returns {Promise}
     */
    then: function (onFulfilled, onRejected) {
      var prom = new Promise();

      // On fulfilled
      if (!Function.is(onFulfilled)) {
        onFulfilled = function (val) {
          prom.fulfill(val);
        };
      }
      if (this.pending) {
        this.onFulfilledHandlers.push(onFulfilled);
      }
      else if (this.fulfilled) {
        Task.run(this.runHandler.bind(this, onFulfilled, prom, this.value));
      }

      // On rejected
      if (!Function.is(onRejected)) {
        onRejected = function (val) {
          prom.reject(val);
        };
      }
      if (this.pending) {
        this.onRejectedHandlers.push(onRejected);
      }
      else if (this.rejected) {
        Task.run(this.runHandler.bind(this, onRejected, prom, this.value));
      }

      if (this.pending) {
        this.promises.push(prom);
      }

      return prom;
    },

    /**
     * @param {Function} onRejected
     * @returns {Promise}
     */
    failed: function (handler) {
      return this.then(null, handler);
    },

    /**
     * @param {Function} onSettled
     * @returns {Promise}
     */
    settled: function (handler) {
      return this.then(handler, handler);
    },

    /**
     * @param {Function} onNotified
     * @returns {this}
     */
    notified: function (onNotified) {
      if (Function.is(onNotified) && this.pending) {
        this.onNotifiedHandlers.push(onNotified);
      }

      return this;
    },

    /**
     * Runs the given function after the fulfillment with the value as a parameter
     * and stores the result as a new value.
     * @param {Function} fn
     * @returns {this}
     */
    run: function (fn) {
      if (Function.is(fn)) {
        if (this.pending) {
          this.changes.push(fn);
        }
        else if (this.fulfilled) {
          this.value = fn(this.value);
        }
      }

      return this;
    },

    /**
     * @param {*} value
     * @returns {Boolean} A boolean indicating whether the promise was fulfilled.
     */
    fulfill: function (val) {
      if (this.pending && !this.awaiting) {
        this.pending = false;
        this.fulfilled = true;

        for (var i = 0; i < this.changes.length; ++i) {
          var fn = this.changes[i];

          val = fn(val);
        }

        this.value = val;

        this.emitEvent(this.onFulfilledHandlers, val);
        this.clear();

        return true;
      }
      return false;
    },

    /**
     * @param {*} reason
     * @returns {Boolean} A boolean indicating whether the promise was rejected.
     */
    reject: function (val) {
      if (this.pending && !this.awaiting) {
        this.pending = false;
        this.rejected = true;
        this.value = val;

        this.emitEvent(this.onRejectedHandlers, val);
        this.clear();

        return true;
      }
      return false;
    },

    adoptState: function (thenable, then, isPromise) {
      var
      self = this,
      resolve = isPromise ? this.fulfill : this.resolve;

      this.awaiting = thenable;

      then.call(
        thenable,
        function (val) {
          if (self.awaiting === thenable) {
            self.awaiting = null;
            resolve.call(self, val);
          }
        },
        function (val) {
          if (self.awaiting === thenable) {
            self.awaiting = null;
            self.reject(val);
          }
        }
      );
    },

    /**
     * @param {*} x
     */
    resolve: function (val) {
      if (this.awaiting) {
        return;
      }

      if (this === val) {
        this.reject(new TypeError("Can't resolve a promise with the same promise!"));
        return;
      }

      if (val) {
        if (val instanceof Promise) {
          this.adoptState(val, val.then, true);
          return;
        }

        if ((Object.isAny(val) || Function.is(val))) {
          try {
            var then = val.then;
            if (Function.is(then)) {
              this.adoptState(val, then);
              return;
            }
          }
          catch (ex) {
            if (this.awaiting === val) {
              this.awaiting = null;
            }
            this.reject(ex);
            return;
          }
        }
      }

      this.fulfill(val);
    },

    /**
     * @param {*} value
     * @returns {Boolean} A boolean indicating whether the promise was notified.
     */
    notify: function (val) {
      if (this.pending) {
        this.emitEvent(this.onNotifiedHandlers, val, true);

        return true;
      }
      return false;
    },

    /**
     * @returns {Function}
     */
    bindFulfill: function () {
      return this.fulfill.bindList(this, arguments);
    },

    /**
     * @returns {Function}
     */
    bindReject: function () {
      return this.reject.bindList(this, arguments);
    },

    /**
     * @returns {Function}
     */
    bindResolve: function () {
      return this.resolve.bindList(this, arguments);
    },

    /**
     * @returns {Function}
     */
    bindNotify: function () {
      return this.notify.bindList(this, arguments);
    },

    emitEvent: function (handlers, val, notification) {
      var self = this, promises = this.promises;
      Task.run(function () {
        for (var i = 0, j = handlers.length; i < j; ++i) {
          self.runHandler(handlers[i], notification ? self : promises[i], val, notification);
        }
      });
    },
    runHandler: function (handler, promise, val, notification) {
      var hasPromise = promise instanceof Promise;

      try {
        var res = handler(val);

        if (!notification && hasPromise) {
          promise.resolve(res);
        }
      }
      catch (ex) {
        if (hasPromise) {
          promise.reject(ex);
        }
      }
    },

    clear: function () {
      delete this.onFulfilledHandlers;
      delete this.onRejectedHandlers;
      delete this.onNotifiedHandlers;

      delete this.changes;

      delete this.promises;
    },

    /**
     * @param {Function} fn A function in the node-async-style form (err, res)
     * @returns {Promise}
     */
    nodeifyThen: function (fn) {
      return this.then(
        function (val) {
          fn(null, val);
        },
        function (err) {
          fn(err);
        }
      );
    },

    /**
     * Returns a function in the node-async-style form which when called resolves the promise
     * @returns {Function}
     */
    nodeifyResolve: function () {
      var self = this;
      return function (err, res) {
        if (err) {
          self.reject(err);
          return;
        }

        self.fulfill(res);
      };
    }
  },

  /** @lends module:legio-async/promise */
  own: {
    /**
     * @param {Promise[]} list
     * @returns {Promise}
     */
    all: function (list, awaitResolution) {
      var
      wrapper = new Promise(),

      len, count, res,

      rejected = false, reason,

      resolve = function () {
        if (wrapper.pending && --count === 0) {
          if (rejected) {
            wrapper.reject(reason);
          }
          else {
            wrapper.fulfill(res);
          }
        }
      },
      tryFulfill = function (key, val) {
        res[key] = val;

        resolve();
      },
      reject = awaitResolution ? function (err) {
        rejected = true;
        reason = err;

        resolve();
      } : wrapper.bindReject();

      if (Array.is(list)) {
        count = len = list.length;
        res = [];

        for (var i = 0; i < len; ++i) {
          list[i].then(tryFulfill.bind(null, i), reject);
        }
      }
      else {
        var keys = Object.keys(list);

        count = len = keys.length;
        res = {};

        for (var i = 0; i < len; ++i) {
          var key = keys[i];

          list[key].then(tryFulfill.bind(null, key), reject);
        }
      }

      return wrapper;
    },

    /**
     * @param {Promise[]} list
     * @returns {Promise}
     */
    allSettled: function (list) {
      return Promise.all(list, true);
    },

    /**
     * @param {Thenable} thenable
     * @returns {Promise}
     */
    when: function (thenable) {
      var prom = new Promise();

      prom.resolve(thenable);

      return prom;
    }
  }
});

var PromiseProto = Promise.prototype;

/**
 * Alias for {@link module:legio-async/promise#then}
 * @alias module:legio-async/promise#done
 * @function
 */
PromiseProto.done = PromiseProto.then;

/**
 * Alias for {@link module:legio-async/promise#failed}
 * @alias module:legio-async/promise#catch
 * @function
 */
PromiseProto["catch"] = PromiseProto.failed;

/**
 * Alias for {@link module:legio-async/promise#settled}
 * @alias module:legio-async/promise#finally
 * @function
 */
PromiseProto["finally"] = PromiseProto.settled;

module.exports = Promise;