mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-10-31 22:25:30 +00:00
410 lines
10 KiB
JavaScript
410 lines
10 KiB
JavaScript
/*
|
|
* 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
|
|
*
|
|
* @param {*=} aTrace A debugging value
|
|
*
|
|
* @constructor
|
|
*/
|
|
function Promise(aTrace) {
|
|
this._status = Promise.PENDING;
|
|
this._value = undefined;
|
|
this._onSuccessHandlers = [];
|
|
this._onErrorHandlers = [];
|
|
this._trace = aTrace;
|
|
|
|
// Debugging help
|
|
if (Promise.Debug._debug) {
|
|
this._id = Promise.Debug._nextId++;
|
|
Promise.Debug._outstanding[this._id] = this;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Debugging options and tools.
|
|
*/
|
|
Promise.Debug = {
|
|
/**
|
|
* Set current debugging mode.
|
|
*
|
|
* @param {boolean} value If |true|, maintain _nextId, _outstanding, _recent.
|
|
* Otherwise, cleanup debugging data.
|
|
*/
|
|
setDebug: function(value) {
|
|
Promise.Debug._debug = value;
|
|
if (!value) {
|
|
Promise.Debug._outstanding = [];
|
|
Promise.Debug._recent = [];
|
|
}
|
|
},
|
|
|
|
_debug: false,
|
|
|
|
/**
|
|
* We give promises and ID so we can track which are outstanding.
|
|
*/
|
|
_nextId: 0,
|
|
|
|
/**
|
|
* Outstanding promises. Handy for debugging (only).
|
|
*/
|
|
_outstanding: [],
|
|
|
|
/**
|
|
* Recently resolved promises. Also for debugging only.
|
|
*/
|
|
_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 <tt>this</tt> 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) {
|
|
Promise._error("Promise complete.", "Attempted ", name, "() with ", data);
|
|
Promise._error("Previous status: ", this._status, ", value =", this._value);
|
|
throw new Error('Promise already complete');
|
|
}
|
|
|
|
if (list.length == 0 && status == Promise.ERROR) {
|
|
var frame;
|
|
var text;
|
|
|
|
//Complain if a rejection is ignored
|
|
//(this is the equivalent of an empty catch-all clause)
|
|
Promise._error("Promise rejection ignored and silently dropped", data);
|
|
if (data.stack) {// This looks like an exception. Try harder to display it
|
|
if (data.fileName && data.lineNumber) {
|
|
Promise._error("Error originating at", data.fileName,
|
|
", line", data.lineNumber );
|
|
}
|
|
try {
|
|
for (frame = data.stack; frame; frame = frame.caller) {
|
|
text += frame + "\n";
|
|
}
|
|
Promise._error("Attempting to extract exception stack", text);
|
|
} catch (x) {
|
|
Promise._error("Could not extract exception stack.");
|
|
}
|
|
} else {
|
|
Promise._error("Exception stack not available.");
|
|
}
|
|
if (Components && Components.stack) {
|
|
try {
|
|
text = "";
|
|
for (frame = Components.stack; frame; frame = frame.caller) {
|
|
text += frame + "\n";
|
|
}
|
|
Promise._error("Attempting to extract current stack", text);
|
|
} catch (x) {
|
|
Promise._error("Could not extract current stack.");
|
|
}
|
|
} else {
|
|
Promise._error("Current stack not available.");
|
|
}
|
|
}
|
|
|
|
|
|
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.Debug._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;
|
|
};
|
|
|
|
/**
|
|
* Log an error on the most appropriate channel.
|
|
*
|
|
* If the console is available, this method uses |console.warn|. Otherwise,
|
|
* this method falls back to |dump|.
|
|
*
|
|
* @param {...*} items Items to log.
|
|
*/
|
|
Promise._error = null;
|
|
if (typeof console != "undefined" && console.warn) {
|
|
Promise._error = function() {
|
|
var args = Array.prototype.slice.call(arguments);
|
|
args.unshift("Promise");
|
|
console.warn.call(console, args);
|
|
};
|
|
} else {
|
|
Promise._error = function() {
|
|
var i;
|
|
var len = arguments.length;
|
|
dump("Promise: ");
|
|
for (i = 0; i < len; ++i) {
|
|
dump(arguments[i]+" ");
|
|
}
|
|
dump("\n");
|
|
};
|
|
}
|
|
|
|
/**
|
|
* 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;
|
|
};
|
|
|
|
/**
|
|
* Trap errors.
|
|
*
|
|
* This function serves as an asynchronous counterpart to |catch|.
|
|
*
|
|
* Example:
|
|
* myPromise.chainPromise(a) //May reject
|
|
* .chainPromise(b) //May reject
|
|
* .chainPromise(c) //May reject
|
|
* .trap(d) //Catch any rejection from a, b or c
|
|
* .chainPromise(e) //If either a, b and c or
|
|
* //d has resolved, execute
|
|
*
|
|
* Scenario 1:
|
|
* If a, b, c resolve, e is executed as if d had not been added.
|
|
*
|
|
* Scenario 2:
|
|
* If a, b or c rejects, d is executed. If d resolves, we proceed
|
|
* with e as if nothing had happened. Otherwise, we proceed with
|
|
* the rejection of d.
|
|
*
|
|
* @param {Function} aTrap Called if |this| promise is rejected,
|
|
* with one argument: the rejection.
|
|
* @return {Promise} A new promise. This promise resolves if all
|
|
* previous promises have resolved or if |aTrap| succeeds.
|
|
*/
|
|
Promise.prototype.trap = function(aTrap) {
|
|
var promise = new Promise();
|
|
var resolve = Promise.prototype.resolve.bind(promise);
|
|
var reject = function(aRejection) {
|
|
try {
|
|
//Attempt to handle issue
|
|
var result = aTrap.call(aTrap, aRejection);
|
|
promise.resolve(result);
|
|
} catch (x) {
|
|
promise.reject(x);
|
|
}
|
|
};
|
|
this.then(resolve, reject);
|
|
return promise;
|
|
};
|
|
|
|
/**
|
|
* Execute regardless of errors.
|
|
*
|
|
* This function serves as an asynchronous counterpart to |finally|.
|
|
*
|
|
* Example:
|
|
* myPromise.chainPromise(a) //May reject
|
|
* .chainPromise(b) //May reject
|
|
* .chainPromise(c) //May reject
|
|
* .always(d) //Executed regardless
|
|
* .chainPromise(e)
|
|
*
|
|
* Whether |a|, |b| or |c| resolve or reject, |d| is executed.
|
|
*
|
|
* @param {Function} aTrap Called regardless of whether |this|
|
|
* succeeds or fails.
|
|
* @return {Promise} A new promise. This promise holds the same
|
|
* resolution/rejection as |this|.
|
|
*/
|
|
Promise.prototype.always = function(aTrap) {
|
|
var promise = new Promise();
|
|
var resolve = function(result) {
|
|
try {
|
|
aTrap.call(aTrap);
|
|
promise.resolve(result);
|
|
} catch (x) {
|
|
promise.reject(x);
|
|
}
|
|
};
|
|
var reject = function(result) {
|
|
try {
|
|
aTrap.call(aTrap);
|
|
promise.reject(result);
|
|
} catch (x) {
|
|
promise.reject(result);
|
|
}
|
|
};
|
|
this.then(resolve, reject);
|
|
return promise;
|
|
};
|
|
|
|
|
|
Promise.prototype.toString = function() {
|
|
var status;
|
|
switch (this._status) {
|
|
case Promise.PENDING:
|
|
status = "pending";
|
|
break;
|
|
case Promise.SUCCESS:
|
|
status = "resolved";
|
|
break;
|
|
case Promise.ERROR:
|
|
status = "rejected";
|
|
break;
|
|
default:
|
|
status = "invalid status: "+this._status;
|
|
}
|
|
return "[Promise " + this._id + " (" + status + ")]";
|
|
};
|