2019年3月8日 星期五

一個 promise 再包裝的例子



'use strict';

/**
 * Promise
 *
 * Inspired by https://gist.github.com/RubaXa/8501359 from RubaXa <trash@rubaxa.org>
 *
 * @param {Function} handler   Called as handler(resolve: Function, reject: Function)
 * @param {Promise} [parent]   Parent promise for propagation of cancel and timeout
 */
function Promise(handler, parent) {
    var me = this;

    if (!(this instanceof Promise)) {
        throw new SyntaxError('Constructor must be called with the new operator');
    }

    if (typeof handler !== 'function') {
        throw new SyntaxError('Function parameter handler(resolve, reject) missing');
    }

    var _onSuccess = [];
    var _onFail = [];

    // status
    this.resolved = false;
    this.rejected = false;
    this.pending = true;

    /**
     * Process onSuccess and onFail callbacks: add them to the queue.
     * Once the promise is resolve, the function _promise is replace.
     * @param {Function} onSuccess
     * @param {Function} onFail
     * @private
     */
    var _process = function (onSuccess, onFail) {
        _onSuccess.push(onSuccess);
        _onFail.push(onFail);
    };

    /**
     * Add an onSuccess callback and optionally an onFail callback to the Promise
     * @param {Function} onSuccess
     * @param {Function} [onFail]
     * @returns {Promise} promise
     */
    this.then = function (onSuccess, onFail) {
        return new Promise(function (resolve, reject) {
            var s = onSuccess ? _then(onSuccess, resolve, reject) : resolve;
            var f = onFail ? _then(onFail, resolve, reject) : reject;

            _process(s, f);
        }, me);
    };

    /**
     * Resolve the promise
     * @param {*} result
     * @type {Function}
     */
    var _resolve = function (result) {
        // update status
        me.resolved = true;
        me.rejected = false;
        me.pending = false;

        _onSuccess.forEach(function (fn) {
            fn(result);
        });

        _process = function (onSuccess, onFail) {
            onSuccess(result);
        };

        _resolve = _reject = function () { };

        return me;
    };

    /**
     * Reject the promise
     * @param {Error} error
     * @type {Function}
     */
    var _reject = function (error) {
        // update status
        me.resolved = false;
        me.rejected = true;
        me.pending = false;

        _onFail.forEach(function (fn) {
            fn(error);
        });

        _process = function (onSuccess, onFail) {
            onFail(error);
        };

        _resolve = _reject = function () { }

        return me;
    };

    /**
     * Cancel te promise. This will reject the promise with a CancellationError
     * @returns {Promise} self
     */
    this.cancel = function () {
        if (parent) {
            parent.cancel();
        } else {
            _reject(new CancellationError());
        }
        return me;
    };

    /**
     * Set a timeout for the promise. If the promise is not resolved within
     * the time, the promise will be cancelled and a TimeoutError is thrown.
     * If the promise is resolved in time, the timeout is removed.
     * @param {number} delay     Delay in milliseconds
     * @returns {Promise} self
     */
    this.timeout = function (delay) {
        if (parent) {
            parent.timeout(delay);
        } else {
            var timer = setTimeout(function () {
                _reject(new TimeoutError('Promise timed out after ' + delay + ' ms'));
            }, delay);

            me.always(function () {
                clearTimeout(timer);
            });
        }
        return me;
    };

    // attach handler passing the resolve and reject functions
    handler(function (result) {
        _resolve(result);
    }, function (error) {
        _reject(error);
    });
}

/**
 * Execute given callback, then call resolve/reject based on the returned result
 * @param {Function} callback
 * @param {Function} resolve
 * @param {Function} reject
 * @returns {Function}
 * @private
 */
function _then(callback, resolve, reject) {
    return function (result) {
        try {
            var res = callback(result);
            if (res && typeof res.then === 'function' && typeof res['catch'] === 'function') {
                // method returned a promise
                res.then(resolve, reject);
            } else {
                resolve(res);
            }
        } catch (error) {
            reject(error);
        }
    }
}

/**
 * Add an onFail callback to the Promise
 * @param {Function} onFail
 * @returns {Promise} promise
 */
Promise.prototype['catch'] = function (onFail) {
    return this.then(null, onFail);
};

// TODO: add support for Promise.catch(Error, callback)
// TODO: add support for Promise.catch(Error, Error, callback)

/**
 * Execute given callback when the promise either resolves or rejects.
 * @param {Function} fn
 * @returns {Promise} promise
 */
Promise.prototype.always = function (fn) {
    return this.then(fn, fn);
};

/**
 * Create a promise which resolves when all provided promises are resolved,
 * and fails when any of the promises resolves.
 * @param {Promise[]} promises
 * @returns {Promise} promise
 */
Promise.all = function (promises) {
    return new Promise(function (resolve, reject) {
        var remaining = promises.length,
                results = [];

        if (remaining) {
            promises.forEach(function (p, i) {
                p.then(function (result) {
                    results[i] = result;
                    remaining--;
                    if (remaining == 0) {
                        resolve(results);
                    }
                }, function (error) {
                    remaining = 0;
                    reject(error);
                });
            });
        } else {
            resolve(results);
        }
    });
};

/**
 * Create a promise resolver
 * @returns {{promise: Promise, resolve: Function, reject: Function}} resolver
 */
Promise.defer = function () {
    var resolver = {};

    resolver.promise = new Promise(function (resolve, reject) {
        resolver.resolve = resolve;
        resolver.reject = reject;
    });

    return resolver;
};

/**
 * Create a cancellation error
 * @param {String} [message]
 * @extends Error
 */
function CancellationError(message) {
    this.message = message || 'promise cancelled';
    this.stack = (new Error()).stack;
}

CancellationError.prototype = new Error();
CancellationError.prototype.constructor = Error;
CancellationError.prototype.name = 'CancellationError';

Promise.CancellationError = CancellationError;


/**
 * Create a timeout error
 * @param {String} [message]
 * @extends Error
 */
function TimeoutError(message) {
    this.message = message || 'timeout exceeded';
    this.stack = (new Error()).stack;
}

TimeoutError.prototype = new Error();
TimeoutError.prototype.constructor = Error;
TimeoutError.prototype.name = 'TimeoutError';

Promise.TimeoutError = TimeoutError;


module.exports = Promise;