mirror of
https://gitlab.winehq.org/wine/wine-gecko.git
synced 2024-09-13 09:24:08 -07:00
Bug 879245 - Implement thenables for Promises. r=bz
--HG-- extra : rebase_source : cde27792ae58b13c88367df7a99fc8981097eedd
This commit is contained in:
parent
dc3bd43a6e
commit
bb153a3ce5
@ -1316,6 +1316,25 @@ WrapCallThisObject(JSContext* cx, JS::Handle<JSObject*> scope, const T& p)
|
||||
return obj;
|
||||
}
|
||||
|
||||
/*
|
||||
* This specialized function simply wraps a JS::Rooted<> since
|
||||
* WrapNativeParent() is not applicable for JS objects.
|
||||
*/
|
||||
template<>
|
||||
inline JSObject*
|
||||
WrapCallThisObject<JS::Rooted<JSObject*>>(JSContext* cx,
|
||||
JS::Handle<JSObject*> scope,
|
||||
const JS::Rooted<JSObject*>& p)
|
||||
{
|
||||
JS::Rooted<JSObject*> obj(cx, p);
|
||||
|
||||
if (!JS_WrapObject(cx, &obj)) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
// Helper for calling WrapNewBindingObject with smart pointers
|
||||
// (nsAutoPtr/nsRefPtr/nsCOMPtr) or references.
|
||||
template <class T, bool isSmartPtr=HasgetMember<T>::Value>
|
||||
|
@ -293,11 +293,11 @@ EnterCompartment(Maybe<JSAutoCompartment>& aAc, JSContext* aCx,
|
||||
|
||||
enum {
|
||||
SLOT_PROMISE = 0,
|
||||
SLOT_TASK
|
||||
SLOT_DATA
|
||||
};
|
||||
|
||||
/* static */ bool
|
||||
Promise::JSCallback(JSContext *aCx, unsigned aArgc, JS::Value *aVp)
|
||||
Promise::JSCallback(JSContext* aCx, unsigned aArgc, JS::Value* aVp)
|
||||
{
|
||||
JS::CallArgs args = CallArgsFromVp(aArgc, aVp);
|
||||
|
||||
@ -311,7 +311,7 @@ Promise::JSCallback(JSContext *aCx, unsigned aArgc, JS::Value *aVp)
|
||||
return Throw(aCx, NS_ERROR_UNEXPECTED);
|
||||
}
|
||||
|
||||
v = js::GetFunctionNativeReserved(&args.callee(), SLOT_TASK);
|
||||
v = js::GetFunctionNativeReserved(&args.callee(), SLOT_DATA);
|
||||
PromiseCallback::Task task = static_cast<PromiseCallback::Task>(v.toInt32());
|
||||
|
||||
if (task == PromiseCallback::Resolve) {
|
||||
@ -323,6 +323,103 @@ Promise::JSCallback(JSContext *aCx, unsigned aArgc, JS::Value *aVp)
|
||||
return true;
|
||||
}
|
||||
|
||||
/*
|
||||
* Utilities for thenable callbacks.
|
||||
*
|
||||
* A thenable is a { then: function(resolve, reject) { } }.
|
||||
* `then` is called with a resolve and reject callback pair.
|
||||
* Since only one of these should be called at most once (first call wins), the
|
||||
* two keep a reference to each other in SLOT_DATA. When either of them is
|
||||
* called, the references are cleared. Further calls are ignored.
|
||||
*/
|
||||
namespace {
|
||||
void
|
||||
LinkThenableCallables(JSContext* aCx, JS::Handle<JSObject*> aResolveFunc,
|
||||
JS::Handle<JSObject*> aRejectFunc)
|
||||
{
|
||||
js::SetFunctionNativeReserved(aResolveFunc, SLOT_DATA,
|
||||
JS::ObjectValue(*aRejectFunc));
|
||||
js::SetFunctionNativeReserved(aRejectFunc, SLOT_DATA,
|
||||
JS::ObjectValue(*aResolveFunc));
|
||||
}
|
||||
|
||||
/*
|
||||
* Returns false if callback was already called before, otherwise breaks the
|
||||
* links and returns true.
|
||||
*/
|
||||
bool
|
||||
MarkAsCalledIfNotCalledBefore(JSContext* aCx, JS::Handle<JSObject*> aFunc)
|
||||
{
|
||||
JS::Value otherFuncVal = js::GetFunctionNativeReserved(aFunc, SLOT_DATA);
|
||||
|
||||
if (!otherFuncVal.isObject()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
JSObject* otherFuncObj = &otherFuncVal.toObject();
|
||||
MOZ_ASSERT(js::GetFunctionNativeReserved(otherFuncObj, SLOT_DATA).isObject());
|
||||
|
||||
// Break both references.
|
||||
js::SetFunctionNativeReserved(aFunc, SLOT_DATA, JS::UndefinedValue());
|
||||
js::SetFunctionNativeReserved(otherFuncObj, SLOT_DATA, JS::UndefinedValue());
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
Promise*
|
||||
GetPromise(JSContext* aCx, JS::Handle<JSObject*> aFunc)
|
||||
{
|
||||
JS::Value promiseVal = js::GetFunctionNativeReserved(aFunc, SLOT_PROMISE);
|
||||
|
||||
MOZ_ASSERT(promiseVal.isObject());
|
||||
|
||||
Promise* promise;
|
||||
UNWRAP_OBJECT(Promise, &promiseVal.toObject(), promise);
|
||||
return promise;
|
||||
}
|
||||
};
|
||||
|
||||
/*
|
||||
* Common bits of (JSCallbackThenableResolver/JSCallbackThenableRejecter).
|
||||
* Resolves/rejects the Promise if it is ok to do so, based on whether either of
|
||||
* the callbacks have been called before or not.
|
||||
*/
|
||||
/* static */ bool
|
||||
Promise::ThenableResolverCommon(JSContext* aCx, uint32_t aTask,
|
||||
unsigned aArgc, JS::Value* aVp)
|
||||
{
|
||||
JS::CallArgs args = CallArgsFromVp(aArgc, aVp);
|
||||
JS::Rooted<JSObject*> thisFunc(aCx, &args.callee());
|
||||
if (!MarkAsCalledIfNotCalledBefore(aCx, thisFunc)) {
|
||||
// A function from this pair has been called before.
|
||||
return true;
|
||||
}
|
||||
|
||||
Promise* promise = GetPromise(aCx, thisFunc);
|
||||
MOZ_ASSERT(promise);
|
||||
|
||||
if (aTask == PromiseCallback::Resolve) {
|
||||
promise->ResolveInternal(aCx, args.get(0), SyncTask);
|
||||
} else {
|
||||
promise->RejectInternal(aCx, args.get(0), SyncTask);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/* static */ bool
|
||||
Promise::JSCallbackThenableResolver(JSContext* aCx,
|
||||
unsigned aArgc, JS::Value* aVp)
|
||||
{
|
||||
return ThenableResolverCommon(aCx, PromiseCallback::Resolve, aArgc, aVp);
|
||||
}
|
||||
|
||||
/* static */ bool
|
||||
Promise::JSCallbackThenableRejecter(JSContext* aCx,
|
||||
unsigned aArgc, JS::Value* aVp)
|
||||
{
|
||||
return ThenableResolverCommon(aCx, PromiseCallback::Reject, aArgc, aVp);
|
||||
}
|
||||
|
||||
/* static */ JSObject*
|
||||
Promise::CreateFunction(JSContext* aCx, JSObject* aParent, Promise* aPromise,
|
||||
int32_t aTask)
|
||||
@ -342,7 +439,33 @@ Promise::CreateFunction(JSContext* aCx, JSObject* aParent, Promise* aPromise,
|
||||
}
|
||||
|
||||
js::SetFunctionNativeReserved(obj, SLOT_PROMISE, promiseObj);
|
||||
js::SetFunctionNativeReserved(obj, SLOT_TASK, JS::Int32Value(aTask));
|
||||
js::SetFunctionNativeReserved(obj, SLOT_DATA, JS::Int32Value(aTask));
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
/* static */ JSObject*
|
||||
Promise::CreateThenableFunction(JSContext* aCx, Promise* aPromise, uint32_t aTask)
|
||||
{
|
||||
JSNative whichFunc =
|
||||
aTask == PromiseCallback::Resolve ? JSCallbackThenableResolver :
|
||||
JSCallbackThenableRejecter ;
|
||||
|
||||
JSFunction* func = js::NewFunctionWithReserved(aCx, whichFunc,
|
||||
1 /* nargs */, 0 /* flags */,
|
||||
nullptr, nullptr);
|
||||
if (!func) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
JS::Rooted<JSObject*> obj(aCx, JS_GetFunctionObject(func));
|
||||
|
||||
JS::Rooted<JS::Value> promiseObj(aCx);
|
||||
if (!dom::WrapNewBindingObject(aCx, obj, aPromise, &promiseObj)) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
js::SetFunctionNativeReserved(obj, SLOT_PROMISE, promiseObj);
|
||||
|
||||
return obj;
|
||||
}
|
||||
@ -615,6 +738,16 @@ Promise::MaybeRejectInternal(JSContext* aCx,
|
||||
RejectInternal(aCx, aValue, aAsynchronous);
|
||||
}
|
||||
|
||||
void
|
||||
Promise::HandleException(JSContext* aCx)
|
||||
{
|
||||
JS::Rooted<JS::Value> exn(aCx);
|
||||
if (JS_GetPendingException(aCx, &exn)) {
|
||||
JS_ClearPendingException(aCx);
|
||||
RejectInternal(aCx, exn, SyncTask);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
Promise::ResolveInternal(JSContext* aCx,
|
||||
JS::Handle<JS::Value> aValue,
|
||||
@ -622,16 +755,61 @@ Promise::ResolveInternal(JSContext* aCx,
|
||||
{
|
||||
mResolvePending = true;
|
||||
|
||||
// TODO: Bug 879245 - Then-able objects
|
||||
if (aValue.isObject()) {
|
||||
JS::Rooted<JSObject*> valueObj(aCx, &aValue.toObject());
|
||||
Promise* nextPromise;
|
||||
nsresult rv = UNWRAP_OBJECT(Promise, valueObj, nextPromise);
|
||||
|
||||
if (NS_SUCCEEDED(rv)) {
|
||||
nsRefPtr<PromiseCallback> resolveCb = new ResolvePromiseCallback(this);
|
||||
nsRefPtr<PromiseCallback> rejectCb = new RejectPromiseCallback(this);
|
||||
nextPromise->AppendCallbacks(resolveCb, rejectCb);
|
||||
// Thenables.
|
||||
JS::Rooted<JS::Value> then(aCx);
|
||||
if (!JS_GetProperty(aCx, valueObj, "then", &then)) {
|
||||
HandleException(aCx);
|
||||
return;
|
||||
}
|
||||
|
||||
if (then.isObject() && JS_ObjectIsCallable(aCx, &then.toObject())) {
|
||||
JS::Rooted<JSObject*> resolveFunc(aCx,
|
||||
CreateThenableFunction(aCx, this, PromiseCallback::Resolve));
|
||||
|
||||
if (!resolveFunc) {
|
||||
HandleException(aCx);
|
||||
return;
|
||||
}
|
||||
|
||||
JS::Rooted<JSObject*> rejectFunc(aCx,
|
||||
CreateThenableFunction(aCx, this, PromiseCallback::Reject));
|
||||
if (!rejectFunc) {
|
||||
HandleException(aCx);
|
||||
return;
|
||||
}
|
||||
|
||||
LinkThenableCallables(aCx, resolveFunc, rejectFunc);
|
||||
|
||||
JS::Rooted<JSObject*> thenObj(aCx, &then.toObject());
|
||||
nsRefPtr<PromiseInit> thenCallback =
|
||||
new PromiseInit(thenObj, mozilla::dom::GetIncumbentGlobal());
|
||||
|
||||
ErrorResult rv;
|
||||
thenCallback->Call(valueObj, resolveFunc, rejectFunc,
|
||||
rv, CallbackObject::eRethrowExceptions);
|
||||
rv.WouldReportJSException();
|
||||
|
||||
if (rv.IsJSException()) {
|
||||
JS::Rooted<JS::Value> exn(aCx);
|
||||
rv.StealJSException(aCx, &exn);
|
||||
|
||||
bool couldMarkAsCalled = MarkAsCalledIfNotCalledBefore(aCx, resolveFunc);
|
||||
|
||||
// If we could mark as called, neither of the callbacks had been called
|
||||
// when the exception was thrown. So we can reject the Promise.
|
||||
if (couldMarkAsCalled) {
|
||||
Maybe<JSAutoCompartment> ac;
|
||||
EnterCompartment(ac, aCx, exn);
|
||||
RejectInternal(aCx, exn, Promise::SyncTask);
|
||||
}
|
||||
// At least one of resolveFunc or rejectFunc have been called, so ignore
|
||||
// the exception. FIXME(nsm): This should be reported to the error
|
||||
// console though, for debugging.
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
@ -151,10 +151,24 @@ private:
|
||||
// Static methods for the PromiseInit functions.
|
||||
static bool
|
||||
JSCallback(JSContext *aCx, unsigned aArgc, JS::Value *aVp);
|
||||
|
||||
static bool
|
||||
ThenableResolverCommon(JSContext* aCx, uint32_t /* PromiseCallback::Task */ aTask,
|
||||
unsigned aArgc, JS::Value* aVp);
|
||||
static bool
|
||||
JSCallbackThenableResolver(JSContext *aCx, unsigned aArgc, JS::Value *aVp);
|
||||
static bool
|
||||
JSCallbackThenableRejecter(JSContext *aCx, unsigned aArgc, JS::Value *aVp);
|
||||
|
||||
static JSObject*
|
||||
CreateFunction(JSContext* aCx, JSObject* aParent, Promise* aPromise,
|
||||
int32_t aTask);
|
||||
|
||||
static JSObject*
|
||||
CreateThenableFunction(JSContext* aCx, Promise* aPromise, uint32_t aTask);
|
||||
|
||||
void HandleException(JSContext* aCx);
|
||||
|
||||
nsRefPtr<nsPIDOMWindow> mWindow;
|
||||
|
||||
nsTArray<nsRefPtr<PromiseCallback> > mResolveCallbacks;
|
||||
|
@ -466,6 +466,104 @@ function promiseResolveNestedPromise() {
|
||||
});
|
||||
}
|
||||
|
||||
function promiseSimpleThenableResolve() {
|
||||
var thenable = { then: function(resolve) { resolve(5); } };
|
||||
var promise = new Promise(function(resolve, reject) {
|
||||
resolve(thenable);
|
||||
});
|
||||
|
||||
promise.then(function(v) {
|
||||
ok(v === 5, "promiseSimpleThenableResolve");
|
||||
runTest();
|
||||
}, function(e) {
|
||||
ok(false, "promiseSimpleThenableResolve: Should not reject");
|
||||
});
|
||||
}
|
||||
|
||||
function promiseSimpleThenableReject() {
|
||||
var thenable = { then: function(resolve, reject) { reject(5); } };
|
||||
var promise = new Promise(function(resolve, reject) {
|
||||
resolve(thenable);
|
||||
});
|
||||
|
||||
promise.then(function() {
|
||||
ok(false, "promiseSimpleThenableReject: Should not resolve");
|
||||
runTest();
|
||||
}, function(e) {
|
||||
ok(e === 5, "promiseSimpleThenableReject");
|
||||
runTest();
|
||||
});
|
||||
}
|
||||
|
||||
function promiseThenableThrowsBeforeCallback() {
|
||||
var thenable = { then: function(resolve) {
|
||||
throw new TypeError("Hi there");
|
||||
resolve(5);
|
||||
}};
|
||||
|
||||
var promise = Promise.resolve(thenable);
|
||||
promise.then(function(v) {
|
||||
ok(false, "promiseThenableThrowsBeforeCallback: Should've rejected");
|
||||
runTest();
|
||||
}, function(e) {
|
||||
ok(e instanceof TypeError, "promiseThenableThrowsBeforeCallback");
|
||||
runTest();
|
||||
});
|
||||
}
|
||||
|
||||
function promiseThenableThrowsAfterCallback() {
|
||||
var thenable = { then: function(resolve) {
|
||||
resolve(5);
|
||||
throw new TypeError("Hi there");
|
||||
}};
|
||||
|
||||
var promise = Promise.resolve(thenable);
|
||||
promise.then(function(v) {
|
||||
ok(v === 5, "promiseThenableThrowsAfterCallback");
|
||||
runTest();
|
||||
}, function(e) {
|
||||
ok(false, "promiseThenableThrowsAfterCallback: Should've resolved");
|
||||
runTest();
|
||||
});
|
||||
}
|
||||
|
||||
function promiseThenableRejectThenResolve() {
|
||||
var thenable = { then: function(resolve, reject) {
|
||||
reject(new TypeError("Hi there"));
|
||||
resolve(5);
|
||||
}};
|
||||
|
||||
var promise = Promise.resolve(thenable);
|
||||
promise.then(function(v) {
|
||||
ok(false, "promiseThenableRejectThenResolve should have rejected");
|
||||
runTest();
|
||||
}, function(e) {
|
||||
ok(e instanceof TypeError, "promiseThenableRejectThenResolve");
|
||||
runTest();
|
||||
});
|
||||
}
|
||||
|
||||
function promiseWithThenReplaced() {
|
||||
// Ensure that we call the 'then' on the promise and not the internal then.
|
||||
var promise = new Promise(function(resolve, reject) {
|
||||
resolve(5);
|
||||
});
|
||||
|
||||
// Rogue `then` always rejects.
|
||||
promise.then = function(onFulfill, onReject) {
|
||||
onReject(new TypeError("Foo"));
|
||||
}
|
||||
|
||||
var promise2 = Promise.resolve(promise);
|
||||
promise2.then(function(v) {
|
||||
ok(false, "promiseWithThenReplaced: Should've rejected");
|
||||
runTest();
|
||||
}, function(e) {
|
||||
ok(e instanceof TypeError, "promiseWithThenReplaced");
|
||||
runTest();
|
||||
});
|
||||
}
|
||||
|
||||
var tests = [ promiseResolve, promiseReject,
|
||||
promiseException, promiseGC, promiseAsync,
|
||||
promiseDoubleThen, promiseThenException,
|
||||
@ -485,6 +583,12 @@ var tests = [ promiseResolve, promiseReject,
|
||||
promiseThenNullResolveFunction,
|
||||
promiseCatchNoArg,
|
||||
promiseRejectNoHandler,
|
||||
promiseSimpleThenableResolve,
|
||||
promiseSimpleThenableReject,
|
||||
promiseThenableThrowsBeforeCallback,
|
||||
promiseThenableThrowsAfterCallback,
|
||||
promiseThenableRejectThenResolve,
|
||||
promiseWithThenReplaced,
|
||||
];
|
||||
|
||||
function runTest() {
|
||||
|
Loading…
Reference in New Issue
Block a user