/* * Copyright 2009-2011 Mozilla Foundation and contributors * Licensed under the New BSD license. See LICENSE.txt or: * http://opensource.org/licenses/BSD-3-Clause */ var EXPORTED_SYMBOLS = [ "Promise" ]; /** * Create an unfulfilled promise * @constructor */ function Promise() { this._status = Promise.PENDING; this._value = undefined; this._onSuccessHandlers = []; this._onErrorHandlers = []; // Debugging help this._id = Promise._nextId++; Promise._outstanding[this._id] = this; } /** * We give promises and ID so we can track which are outstanding */ Promise._nextId = 0; /** * Outstanding promises. Handy list for debugging only */ Promise._outstanding = []; /** * Recently resolved promises. Also for debugging only */ Promise._recent = []; /** * A promise can be in one of 2 states. * The ERROR and SUCCESS states are terminal, the PENDING state is the only * start state. */ Promise.ERROR = -1; Promise.PENDING = 0; Promise.SUCCESS = 1; /** * Yeay for RTTI */ Promise.prototype.isPromise = true; /** * Have we either been resolve()ed or reject()ed? */ Promise.prototype.isComplete = function() { return this._status != Promise.PENDING; }; /** * Have we resolve()ed? */ Promise.prototype.isResolved = function() { return this._status == Promise.SUCCESS; }; /** * Have we reject()ed? */ Promise.prototype.isRejected = function() { return this._status == Promise.ERROR; }; /** * Take the specified action of fulfillment of a promise, and (optionally) * a different action on promise rejection */ Promise.prototype.then = function(onSuccess, onError) { if (typeof onSuccess === 'function') { if (this._status === Promise.SUCCESS) { onSuccess.call(null, this._value); } else if (this._status === Promise.PENDING) { this._onSuccessHandlers.push(onSuccess); } } if (typeof onError === 'function') { if (this._status === Promise.ERROR) { onError.call(null, this._value); } else if (this._status === Promise.PENDING) { this._onErrorHandlers.push(onError); } } return this; }; /** * Like then() except that rather than returning this we return * a promise which resolves when the original promise resolves */ Promise.prototype.chainPromise = function(onSuccess) { var chain = new Promise(); chain._chainedFrom = this; this.then(function(data) { try { chain.resolve(onSuccess(data)); } catch (ex) { chain.reject(ex); } }, function(ex) { chain.reject(ex); }); return chain; }; /** * Supply the fulfillment of a promise */ Promise.prototype.resolve = function(data) { return this._complete(this._onSuccessHandlers, Promise.SUCCESS, data, 'resolve'); }; /** * Renege on a promise */ Promise.prototype.reject = function(data) { return this._complete(this._onErrorHandlers, Promise.ERROR, data, 'reject'); }; /** * Internal method to be called on resolve() or reject() * @private */ Promise.prototype._complete = function(list, status, data, name) { // Complain if we've already been completed if (this._status != Promise.PENDING) { if (typeof 'console' === 'object') { console.error('Promise complete. Attempted ' + name + '() with ', data); console.error('Prev status = ', this._status, ', value = ', this._value); } throw new Error('Promise already complete'); } this._status = status; this._value = data; // Call all the handlers, and then delete them list.forEach(function(handler) { handler.call(null, this._value); }, this); delete this._onSuccessHandlers; delete this._onErrorHandlers; // Remove the given {promise} from the _outstanding list, and add it to the // _recent list, pruning more than 20 recent promises from that list delete Promise._outstanding[this._id]; // The original code includes this very useful debugging aid, however there // is concern that it will create a memory leak, so we leave it out here. /* Promise._recent.push(this); while (Promise._recent.length > 20) { Promise._recent.shift(); } */ return this; }; /** * Takes an array of promises and returns a promise that that is fulfilled once * all the promises in the array are fulfilled * @param promiseList The array of promises * @return the promise that is fulfilled when all the array is fulfilled */ Promise.group = function(promiseList) { if (!Array.isArray(promiseList)) { promiseList = Array.prototype.slice.call(arguments); } // If the original array has nothing in it, return now to avoid waiting if (promiseList.length === 0) { return new Promise().resolve([]); } var groupPromise = new Promise(); var results = []; var fulfilled = 0; var onSuccessFactory = function(index) { return function(data) { results[index] = data; fulfilled++; // If the group has already failed, silently drop extra results if (groupPromise._status !== Promise.ERROR) { if (fulfilled === promiseList.length) { groupPromise.resolve(results); } } }; }; promiseList.forEach(function(promise, index) { var onSuccess = onSuccessFactory(index); var onError = groupPromise.reject.bind(groupPromise); promise.then(onSuccess, onError); }); return groupPromise; };