Bug 1170760 part 8. Add subclassing support to Promise::All. r=baku,efaust

This commit is contained in:
Boris Zbarsky 2015-11-25 15:48:09 -05:00
parent 7c7786c7ac
commit 53b93863f6
5 changed files with 411 additions and 18 deletions

View File

@ -1250,24 +1250,319 @@ NS_INTERFACE_MAP_END
NS_IMPL_CYCLE_COLLECTION(AllResolveElementFunction, mCountdownHolder)
/* static */ already_AddRefed<Promise>
Promise::All(const GlobalObject& aGlobal, JS::Handle<JS::Value> aThisv,
const Sequence<JS::Value>& aIterable, ErrorResult& aRv)
static const JSClass PromiseAllDataHolderClass = {
"PromiseAllDataHolder", JSCLASS_HAS_RESERVED_SLOTS(3)
};
// Slot indices for objects of class PromiseAllDataHolderClass.
#define DATA_HOLDER_REMAINING_ELEMENTS_SLOT 0
#define DATA_HOLDER_VALUES_ARRAY_SLOT 1
#define DATA_HOLDER_RESOLVE_FUNCTION_SLOT 2
// Slot indices for PromiseAllResolveElement.
// The RESOLVE_ELEMENT_INDEX_SLOT stores our index unless we've already been
// called. Then it stores INT32_MIN (which is never a valid index value).
#define RESOLVE_ELEMENT_INDEX_SLOT 0
// The RESOLVE_ELEMENT_DATA_HOLDER_SLOT slot stores an object of class
// PromiseAllDataHolderClass.
#define RESOLVE_ELEMENT_DATA_HOLDER_SLOT 1
static bool
PromiseAllResolveElement(JSContext* aCx, unsigned aArgc, JS::Value* aVp)
{
JSContext* cx = aGlobal.Context();
// Implements
// http://www.ecma-international.org/ecma-262/6.0/#sec-promise.all-resolve-element-functions
//
// See the big comment about compartments in Promise::All "Substep 4" that
// explains what compartments the various stuff here lives in.
JS::CallArgs args = CallArgsFromVp(aArgc, aVp);
nsTArray<RefPtr<Promise>> promiseList;
for (uint32_t i = 0; i < aIterable.Length(); ++i) {
JS::Rooted<JS::Value> value(cx, aIterable.ElementAt(i));
RefPtr<Promise> nextPromise = Promise::Resolve(aGlobal, aThisv, value, aRv);
MOZ_ASSERT(!aRv.Failed());
promiseList.AppendElement(Move(nextPromise));
// Step 1.
int32_t index =
js::GetFunctionNativeReserved(&args.callee(),
RESOLVE_ELEMENT_INDEX_SLOT).toInt32();
// Step 2.
if (index == INT32_MIN) {
args.rval().setUndefined();
return true;
}
return Promise::All(aGlobal, promiseList, aRv);
// Step 3.
js::SetFunctionNativeReserved(&args.callee(),
RESOLVE_ELEMENT_INDEX_SLOT,
JS::Int32Value(INT32_MIN));
// Step 4 already done.
// Step 5.
JS::Rooted<JSObject*> dataHolder(aCx,
&js::GetFunctionNativeReserved(&args.callee(),
RESOLVE_ELEMENT_DATA_HOLDER_SLOT).toObject());
JS::Rooted<JS::Value> values(aCx,
js::GetReservedSlot(dataHolder, DATA_HOLDER_VALUES_ARRAY_SLOT));
// Step 6, effectively.
JS::Rooted<JS::Value> resolveFunc(aCx,
js::GetReservedSlot(dataHolder, DATA_HOLDER_RESOLVE_FUNCTION_SLOT));
// Step 7.
int32_t remainingElements =
js::GetReservedSlot(dataHolder, DATA_HOLDER_REMAINING_ELEMENTS_SLOT).toInt32();
// Step 8.
JS::Rooted<JSObject*> valuesObj(aCx, &values.toObject());
if (!JS_DefineElement(aCx, valuesObj, index, args.get(0), JSPROP_ENUMERATE)) {
return false;
}
// Step 9.
remainingElements -= 1;
js::SetReservedSlot(dataHolder, DATA_HOLDER_REMAINING_ELEMENTS_SLOT,
JS::Int32Value(remainingElements));
// Step 10.
if (remainingElements == 0) {
return JS::Call(aCx, JS::UndefinedHandleValue, resolveFunc,
JS::HandleValueArray(values), args.rval());
}
// Step 11.
args.rval().setUndefined();
return true;
}
/* static */ void
Promise::All(const GlobalObject& aGlobal, JS::Handle<JS::Value> aThisv,
JS::Handle<JS::Value> aIterable,
JS::MutableHandle<JS::Value> aRetval, ErrorResult& aRv)
{
// Implements http://www.ecma-international.org/ecma-262/6.0/#sec-promise.all
nsCOMPtr<nsIGlobalObject> global =
do_QueryInterface(aGlobal.GetAsSupports());
if (!global) {
aRv.Throw(NS_ERROR_UNEXPECTED);
return;
}
JSContext* cx = aGlobal.Context();
// Steps 1-5: nothing to do. Note that the @@species bits got removed in
// https://github.com/tc39/ecma262/pull/211
// Step 6.
PromiseCapability capability(cx);
NewPromiseCapability(cx, global, aThisv, true, capability, aRv);
// Step 7.
if (aRv.Failed()) {
return;
}
MOZ_ASSERT(aThisv.isObject(), "How did NewPromiseCapability succeed?");
JS::Rooted<JSObject*> constructorObj(cx, &aThisv.toObject());
// After this point we have a useful promise value in "capability", so just go
// ahead and put it in our retval now. Every single return path below would
// want to do that anyway.
aRetval.set(capability.PromiseValue());
if (!MaybeWrapValue(cx, aRetval)) {
aRv.NoteJSContextException();
return;
}
// The arguments we're going to be passing to "then" on each loop iteration.
// The second one we know already; the first one will be created on each
// iteration of the loop.
JS::AutoValueArray<2> callbackFunctions(cx);
callbackFunctions[1].set(capability.mReject);
// Steps 8 and 9.
JS::ForOfIterator iter(cx);
if (!iter.init(aIterable, JS::ForOfIterator::AllowNonIterable)) {
capability.RejectWithException(cx, aRv);
return;
}
if (!iter.valueIsIterable()) {
ThrowErrorMessage(cx, MSG_PROMISE_ARG_NOT_ITERABLE,
"Argument of Promise.all");
capability.RejectWithException(cx, aRv);
return;
}
// Step 10 doesn't need to be done, because ForOfIterator handles it
// for us.
// Now we jump over to
// http://www.ecma-international.org/ecma-262/6.0/#sec-performpromiseall
// and do its steps.
// Substep 4. Create our data holder that holds all the things shared across
// every step of the iterator. In particular, this holds the
// remainingElementsCount (as an integer reserved slot), the array of values,
// and the resolve function from our PromiseCapability.
//
// We have to be very careful about which compartments we create things in
// here. In particular, we have to maintain the invariant that anything
// stored in a reserved slot is same-compartment with the object whose
// reserved slot it's in. But we want to create the values array in the
// Promise reflector compartment, because that array can get exposed to code
// that has access to the Promise reflector (in particular code from that
// compartment), and that should work, even if the Promise reflector
// compartment is less-privileged than our caller compartment.
//
// So the plan is as follows: Create the values array in the promise reflector
// compartment. Create the PromiseAllResolveElement function and the data
// holder in our current compartment. Store a cross-compartment wrapper to
// the values array in the holder. This should be OK because the only things
// we hand the PromiseAllResolveElement function to are the "then" calls we do
// and in the case when the reflector compartment is not the current
// compartment those are happening over Xrays anyway, which means they get the
// canonical "then" function and content can't see our
// PromiseAllResolveElement.
JS::Rooted<JSObject*> dataHolder(cx);
dataHolder = JS_NewObjectWithGivenProto(cx, &PromiseAllDataHolderClass,
nullptr);
if (!dataHolder) {
capability.RejectWithException(cx, aRv);
return;
}
JS::Rooted<JSObject*> reflectorGlobal(cx, global->GetGlobalJSObject());
JS::Rooted<JSObject*> valuesArray(cx);
{ // Scope for JSAutoCompartment.
JSAutoCompartment ac(cx, reflectorGlobal);
valuesArray = JS_NewArrayObject(cx, 0);
}
if (!valuesArray) {
// It's important that we've exited the JSAutoCompartment by now, before
// calling RejectWithException and possibly invoking capability.mReject.
capability.RejectWithException(cx, aRv);
return;
}
// The values array as a value we can pass to a function in our current
// compartment, or store in the holder's reserved slot.
JS::Rooted<JS::Value> valuesArrayVal(cx, JS::ObjectValue(*valuesArray));
if (!MaybeWrapObjectValue(cx, &valuesArrayVal)) {
capability.RejectWithException(cx, aRv);
return;
}
js::SetReservedSlot(dataHolder, DATA_HOLDER_REMAINING_ELEMENTS_SLOT,
JS::Int32Value(1));
js::SetReservedSlot(dataHolder, DATA_HOLDER_VALUES_ARRAY_SLOT,
valuesArrayVal);
js::SetReservedSlot(dataHolder, DATA_HOLDER_RESOLVE_FUNCTION_SLOT,
capability.mResolve);
// Substep 5.
CheckedInt32 index = 0;
// Substep 6.
JS::Rooted<JS::Value> nextValue(cx);
while (true) {
bool done;
// Steps a, b, c, e, f, g.
if (!iter.next(&nextValue, &done)) {
capability.RejectWithException(cx, aRv);
return;
}
// Step d.
if (done) {
int32_t remainingCount =
js::GetReservedSlot(dataHolder,
DATA_HOLDER_REMAINING_ELEMENTS_SLOT).toInt32();
remainingCount -= 1;
if (remainingCount == 0) {
JS::Rooted<JS::Value> ignored(cx);
if (!JS::Call(cx, JS::UndefinedHandleValue, capability.mResolve,
JS::HandleValueArray(valuesArrayVal), &ignored)) {
capability.RejectWithException(cx, aRv);
}
return;
}
js::SetReservedSlot(dataHolder, DATA_HOLDER_REMAINING_ELEMENTS_SLOT,
JS::Int32Value(remainingCount));
// We're all set for now!
return;
}
// Step h.
{ // Scope for the JSAutoCompartment we need to work with valuesArray. We
// mostly do this for performance; we could go ahead and do the define via
// a cross-compartment proxy instead...
JSAutoCompartment ac(cx, valuesArray);
if (!JS_DefineElement(cx, valuesArray, index.value(),
JS::UndefinedHandleValue, JSPROP_ENUMERATE)) {
// Have to go back into the caller compartment before we try to touch
// capability.mReject. Luckily, capability.mReject is guaranteed to be
// an object in the right compartment here.
JSAutoCompartment ac2(cx, &capability.mReject.toObject());
capability.RejectWithException(cx, aRv);
return;
}
}
// Step i. Sadly, we can't take a shortcut here even if
// capability.mNativePromise exists, because someone could have overridden
// "resolve" on the canonical Promise constructor.
JS::Rooted<JS::Value> nextPromise(cx);
if (!JS_CallFunctionName(cx, constructorObj, "resolve",
JS::HandleValueArray(nextValue),
&nextPromise)) {
// Step j.
capability.RejectWithException(cx, aRv);
return;
}
// Step k.
JS::Rooted<JSObject*> resolveElement(cx);
JSFunction* resolveFunc =
js::NewFunctionWithReserved(cx, PromiseAllResolveElement,
1 /* nargs */, 0 /* flags */, nullptr);
if (!resolveFunc) {
capability.RejectWithException(cx, aRv);
return;
}
resolveElement = JS_GetFunctionObject(resolveFunc);
// Steps l-p.
js::SetFunctionNativeReserved(resolveElement,
RESOLVE_ELEMENT_INDEX_SLOT,
JS::Int32Value(index.value()));
js::SetFunctionNativeReserved(resolveElement,
RESOLVE_ELEMENT_DATA_HOLDER_SLOT,
JS::ObjectValue(*dataHolder));
// Step q.
int32_t remainingElements =
js::GetReservedSlot(dataHolder, DATA_HOLDER_REMAINING_ELEMENTS_SLOT).toInt32();
js::SetReservedSlot(dataHolder, DATA_HOLDER_REMAINING_ELEMENTS_SLOT,
JS::Int32Value(remainingElements + 1));
// Step r. And now we don't know whether nextPromise has an overridden
// "then" method, so no shortcuts here either.
callbackFunctions[0].setObject(*resolveElement);
JS::Rooted<JSObject*> nextPromiseObj(cx);
JS::Rooted<JS::Value> ignored(cx);
if (!JS_ValueToObject(cx, nextPromise, &nextPromiseObj) ||
!JS_CallFunctionName(cx, nextPromiseObj, "then", callbackFunctions,
&ignored)) {
// Step s.
capability.RejectWithException(cx, aRv);
}
// Step t.
index += 1;
if (!index.isValid()) {
// Let's just claim OOM.
aRv.Throw(NS_ERROR_OUT_OF_MEMORY);
capability.RejectWithException(cx, aRv);
}
}
}
/* static */ already_AddRefed<Promise>

View File

@ -186,9 +186,10 @@ public:
already_AddRefed<Promise>
Catch(JSContext* aCx, AnyCallback* aRejectCallback, ErrorResult& aRv);
static already_AddRefed<Promise>
static void
All(const GlobalObject& aGlobal, JS::Handle<JS::Value> aThisv,
const Sequence<JS::Value>& aIterable, ErrorResult& aRv);
JS::Handle<JS::Value> aIterable, JS::MutableHandle<JS::Value> aRetval,
ErrorResult& aRv);
static already_AddRefed<Promise>
All(const GlobalObject& aGlobal,

View File

@ -100,6 +100,79 @@ function testRace4() {
).then(nextTest);
}
function testAll1() {
var p = win.Promise.all(new win.Array(1, 2));
p.then(
function(arg) {
ok(arg instanceof win.Array, "Should get an Array from Promise.all (1)");
is(arg[0], 1, "First entry of Promise.all return value should be correct (1)");
is(arg[1], 2, "Second entry of Promise.all return value should be correct (1)");
},
function(e) {
ok(false, "testAll1 threw exception: " + e);
}
).then(nextTest);
}
function testAll2() {
var p = win.Promise.all(
new Array(win.Promise.resolve(1), win.Promise.resolve(2)));
p.then(
function(arg) {
ok(arg instanceof win.Array, "Should get an Array from Promise.all (2)");
is(arg[0], 1, "First entry of Promise.all return value should be correct (2)");
is(arg[1], 2, "Second entry of Promise.all return value should be correct (2)");
},
function(e) {
ok(false, "testAll2 threw exception: " + e);
}
).then(nextTest);
}
function testAll3() {
// This works with a chrome-side array because we do the iteration
// while still in the Xray compartment.
var p = win.Promise.all([1, 2]);
p.then(
function(arg) {
ok(arg instanceof win.Array, "Should get an Array from Promise.all (3)");
is(arg[0], 1, "First entry of Promise.all return value should be correct (3)");
is(arg[1], 2, "Second entry of Promise.all return value should be correct (3)");
},
function(e) {
ok(false, "testAll3 threw exception: " + e);
}
).then(nextTest);
}
function testAll4() {
// This works with both content-side and chrome-side Promises because we want
// it to and go to some lengths to make it work.
var p = win.Promise.all([Promise.resolve(1), win.Promise.resolve(2)]);
p.then(
function(arg) {
ok(arg instanceof win.Array, "Should get an Array from Promise.all (4)");
is(arg[0], 1, "First entry of Promise.all return value should be correct (4)");
is(arg[1], 2, "Second entry of Promise.all return value should be correct (4)");
},
function(e) {
ok(false, "testAll4 threw exception: " + e);
}
).then(nextTest);
}
function testAll5() {
var p = win.Promise.all(new win.Array());
p.then(
function(arg) {
ok(arg instanceof win.Array, "Should get an Array from Promise.all (5)");
},
function(e) {
ok(false, "testAll5 threw exception: " + e);
}
).then(nextTest);
}
var tests = [
testLoadComplete,
testHaveXray,
@ -107,6 +180,11 @@ var tests = [
testRace2,
testRace3,
testRace4,
testAll1,
testAll2,
testAll3,
testAll4,
testAll5,
];
function nextTest() {

View File

@ -39,8 +39,13 @@ interface _Promise {
[NewObject]
Promise<any> catch([TreatNonCallableAsNull] optional AnyCallback? rejectCallback = null);
[NewObject]
static Promise<any> all(sequence<any> iterable);
// Have to use "any" (or "object", but "any" is simpler) as the type to
// support the subclassing behavior, since nothing actually requires the
// return value of PromiseSubclass.all to be a Promise object. As a result,
// we also have to do our argument conversion manually, because we want to
// convert its exceptions into rejections.
[NewObject, Throws]
static any all(optional any iterable);
// Have to use "any" (or "object", but "any" is simpler) as the type to
// support the subclassing behavior, since nothing actually requires the

View File

@ -150,4 +150,18 @@ promise_test(function testPromiseRaceNoSpecies() {
});
}, "Promise.race without species behavior");
promise_test(function testPromiseAll() {
clearLog();
var p = LoggingPromise.all(new LoggingIterable([1, 2]));
var log = takeLog();
assert_array_equals(log, ["All 1", "Constructor 1",
"Next 1", "Resolve 1",
"Next 2", "Resolve 2",
"Next 3"]);
assert_true(p instanceof LoggingPromise);
return p.then(function(arg) {
assert_array_equals(arg, [1, 2]);
});
}, "Promise.all behavior");
</script>