'use strict'; /** * Promise * * Inspired by https://gist.github.com/RubaXa/8501359 from RubaXa * * @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;