Bug 941920 - Implement full Promise API in Promise.jsm. r=paolo

This commit is contained in:
Brandon Benvie 2014-03-31 14:43:07 +02:00
parent 5d81933f75
commit 118b2ca6d7
2 changed files with 226 additions and 44 deletions

View File

@ -340,6 +340,21 @@ Promise.prototype.then = function (aOnResolve, aOnReject)
return handler.nextPromise;
};
/**
* Invokes `promise.then` with undefined for the resolve handler and a given
* reject handler.
*
* @param aOnReject
* The rejection handler.
*
* @return A new pending promise returned.
*
* @see Promise.prototype.then
*/
Promise.prototype.catch = function (aOnReject)
{
return this.then(undefined, aOnReject);
};
/**
* Creates a new pending promise and provides methods to resolve or reject it.
@ -375,6 +390,10 @@ Promise.resolve = function (aValue)
"the Task and return its promise.");
}
if (aValue instanceof Promise) {
return aValue;
}
return new Promise((aResolve) => aResolve(aValue));
};
@ -419,40 +438,62 @@ Promise.all = function (aValues)
throw new Error("Promise.all() expects an iterable.");
}
if (!Array.isArray(aValues)) {
aValues = [...aValues];
}
return new Promise((resolve, reject) => {
let values = Array.isArray(aValues) ? aValues : [...aValues];
let countdown = values.length;
let resolutionValues = new Array(countdown);
if (!aValues.length) {
return Promise.resolve([]);
}
let countdown = aValues.length;
let deferred = Promise.defer();
let resolutionValues = new Array(countdown);
function checkForCompletion(aValue, aIndex) {
resolutionValues[aIndex] = aValue;
if (--countdown === 0) {
deferred.resolve(resolutionValues);
if (!countdown) {
resolve(resolutionValues);
return;
}
}
for (let i = 0; i < aValues.length; i++) {
let index = i;
let value = aValues[i];
let resolve = val => checkForCompletion(val, index);
if (value && typeof(value.then) == "function") {
value.then(resolve, deferred.reject);
} else {
// Given value is not a promise, forward it as a resolution value.
resolve(value);
function checkForCompletion(aValue, aIndex) {
resolutionValues[aIndex] = aValue;
if (--countdown === 0) {
resolve(resolutionValues);
}
}
for (let i = 0; i < values.length; i++) {
let index = i;
let value = values[i];
let resolver = val => checkForCompletion(val, index);
if (value && typeof(value.then) == "function") {
value.then(resolver, reject);
} else {
// Given value is not a promise, forward it as a resolution value.
resolver(value);
}
}
});
};
/**
* Returns a promise that is resolved or rejected when the first value is
* resolved or rejected, taking on the value or reason of that promise.
*
* @param aValues
* Iterable of values or promises that may be pending, resolved, or
* rejected. When any is resolved or rejected, the returned promise will
* be resolved or rejected as to the given value or reason.
*
* @return A new promise that is fulfilled when any values are resolved or
* rejected. Its resolution value will be forwarded from the resolution
* value or rejection reason.
*/
Promise.race = function (aValues)
{
if (aValues == null || typeof(aValues["@@iterator"]) != "function") {
throw new Error("Promise.race() expects an iterable.");
}
return deferred.promise;
return new Promise((resolve, reject) => {
for (let value of aValues) {
Promise.resolve(value).then(resolve, reject);
}
});
};
Object.freeze(Promise);

View File

@ -84,7 +84,7 @@ let tests = [];
// This function is useful if the promise itself is
// not returned.
let observe_failures = function observe_failures(promise) {
promise.then(null, function onReject(reason) {
promise.catch(function onReject(reason) {
test.do_throw("Observed failure in test " + test + ": " + reason);
});
};
@ -404,8 +404,7 @@ tests.push(
}
);
promise = promise.then(
null,
promise = promise.catch(
function onReject(reason) {
do_check_eq(reason, boom2, "Rejection was propagated with the correct " +
"reason, through a promise");
@ -436,7 +435,7 @@ tests.push(
);
}));
// Test sequences of |then|
// Test sequences of |then| and |catch|
tests.push(
make_promise_test(function test_chaining(test) {
let error_1 = new Error("Error 1");
@ -461,8 +460,7 @@ tests.push(
);
// Check that returning from the promise produces a resolution
promise = promise.then(
null,
promise = promise.catch(
function onReject() {
do_throw("Incorrect rejection");
}
@ -488,16 +486,14 @@ tests.push(
}
);
promise = promise.then(
null,
promise = promise.catch(
function onReject(reason) {
do_check_true(reason == error_1, "Reason was propagated correctly");
throw error_2;
}
);
promise = promise.then(
null,
promise = promise.catch(
function onReject(reason) {
do_check_true(reason == error_2, "Throwing an error altered the reason " +
"as expected");
@ -538,12 +534,15 @@ tests.push(
tests.push(
make_promise_test(function test_resolve(test) {
const RESULT = "arbitrary value";
let promise = Promise.resolve(RESULT).then(
let p1 = Promise.resolve(RESULT);
let p2 = Promise.resolve(p1);
do_check_eq(p1, p2, "Promise.resolve used on a promise just returns the promise");
return p1.then(
function onResolve(result) {
do_check_eq(result, RESULT, "Promise.resolve propagated the correct result");
}
);
return promise;
}));
// Test that Promise.resolve throws when its argument is an async function.
@ -667,9 +666,9 @@ tests.push(
make_promise_test(function all_resolve_no_promises(test) {
try {
Promise.all(null);
do_check_true(false, "all() should only accept arrays.");
do_check_true(false, "all() should only accept iterables");
} catch (e) {
do_check_true(true, "all() fails when first the arg is not an array.");
do_check_true(true, "all() fails when first the arg is not an iterable");
}
let p1 = Promise.all([]).then(
@ -689,6 +688,148 @@ tests.push(
return Promise.all([p1, p2]);
}));
// Test that Promise.all() handles non-array iterables
tests.push(
make_promise_test(function all_iterable(test) {
function* iterable() {
yield 1;
yield 2;
yield 3;
}
return Promise.all(iterable()).then(
function onResolve([val1, val2, val3]) {
do_check_eq(val1, 1);
do_check_eq(val2, 2);
do_check_eq(val3, 3);
},
function onReject() {
do_throw("all() unexpectedly rejected");
}
);
}));
// Test that throwing from the iterable passed to Promise.all() rejects the
// promise returned by Promise.all()
tests.push(
make_promise_test(function all_iterable_throws(test) {
function* iterable() {
throw 1;
}
return Promise.all(iterable()).then(
function onResolve() {
do_throw("all() unexpectedly resolved");
},
function onReject(reason) {
do_check_eq(reason, 1, "all() rejects when the iterator throws");
}
);
}));
// Test that Promise.race() resolves with the first available resolution value
tests.push(
make_promise_test(function race_resolve(test) {
let p1 = Promise.resolve(1);
let p2 = Promise.resolve().then(() => 2);
return Promise.race([p1, p2]).then(
function onResolve(value) {
do_check_eq(value, 1);
}
);
}));
// Test that passing only values (not promises) to Promise.race() works
tests.push(
make_promise_test(function race_resolve_no_promises(test) {
try {
Promise.race(null);
do_check_true(false, "race() should only accept iterables");
} catch (e) {
do_check_true(true, "race() fails when first the arg is not an iterable");
}
return Promise.race([1, 2, 3]).then(
function onResolve(value) {
do_check_eq(value, 1);
}
);
}));
// Test that Promise.race() never resolves when passed an empty iterable
tests.push(
make_promise_test(function race_resolve_never(test) {
return new Promise(resolve => {
Promise.race([]).then(
function onResolve() {
do_throw("race() unexpectedly resolved");
},
function onReject() {
do_throw("race() unexpectedly rejected");
}
);
// Approximate "never" so we don't have to solve the halting problem.
do_timeout(200, resolve);
});
}));
// Test that Promise.race() handles non-array iterables.
tests.push(
make_promise_test(function race_iterable(test) {
function* iterable() {
yield 1;
yield 2;
yield 3;
}
return Promise.race(iterable()).then(
function onResolve(value) {
do_check_eq(value, 1);
},
function onReject() {
do_throw("race() unexpectedly rejected");
}
);
}));
// Test that throwing from the iterable passed to Promise.race() rejects the
// promise returned by Promise.race()
tests.push(
make_promise_test(function race_iterable_throws(test) {
function* iterable() {
throw 1;
}
return Promise.race(iterable()).then(
function onResolve() {
do_throw("race() unexpectedly resolved");
},
function onReject(reason) {
do_check_eq(reason, 1, "race() rejects when the iterator throws");
}
);
}));
// Test that rejecting one of the promises passed to Promise.race() rejects the
// promise returned by Promise.race()
tests.push(
make_promise_test(function race_reject(test) {
let p1 = Promise.reject(1);
let p2 = Promise.resolve(2);
let p3 = Promise.resolve(3);
return Promise.race([p1, p2, p3]).then(
function onResolve() {
do_throw("race() unexpectedly resolved");
},
function onReject(reason) {
do_check_eq(reason, 1, "race() rejects when given a rejected promise");
}
);
}));
// Test behavior of the Promise constructor.
tests.push(
make_promise_test(function test_constructor(test) {
@ -803,7 +944,7 @@ tests.push(
}, null);
do_print("Setting wait for second promise");
return promise2.then(null, error => {return 3;})
return promise2.catch(error => {return 3;})
.then(
count => {
shouldExitNestedEventLoop = true;
@ -930,7 +1071,7 @@ make_promise_test(function test_caught_is_not_reported() {
let promise = wait_for_uncaught([salt], 500);
(function() {
let uncaught = Promise.reject("This error, on the other hand, is caught " + salt);
uncaught.then(null, function() { /* ignore rejection */});
uncaught.catch(function() { /* ignore rejection */});
uncaught = null;
})();
// Isolate this in a function to increase likelihood that the gc will