diff --git a/dom/base/DOMCursor.h b/dom/base/DOMCursor.h index 08bd987354b9..6c4694399241 100644 --- a/dom/base/DOMCursor.h +++ b/dom/base/DOMCursor.h @@ -43,6 +43,11 @@ protected: private: DOMCursor() MOZ_DELETE; + // Calling Then() on DOMCursor is a mistake, since the DOMCursor object + // should not have a .then() method from JS' point of view. + already_AddRefed + Then(JSContext* aCx, AnyCallback* aResolveCallback, + AnyCallback* aRejectCallback, ErrorResult& aRv) MOZ_DELETE; nsCOMPtr mCallback; bool mFinished; diff --git a/dom/base/DOMRequest.cpp b/dom/base/DOMRequest.cpp index a6c21cb47703..00bd45e8c253 100644 --- a/dom/base/DOMRequest.cpp +++ b/dom/base/DOMRequest.cpp @@ -10,11 +10,14 @@ #include "nsThreadUtils.h" #include "DOMCursor.h" #include "nsIDOMEvent.h" +#include "mozilla/dom/Promise.h" +using mozilla::dom::AnyCallback; using mozilla::dom::DOMError; using mozilla::dom::DOMRequest; using mozilla::dom::DOMRequestService; using mozilla::dom::DOMCursor; +using mozilla::dom::Promise; using mozilla::AutoSafeJSContext; DOMRequest::DOMRequest(nsPIDOMWindow* aWindow) @@ -25,16 +28,24 @@ DOMRequest::DOMRequest(nsPIDOMWindow* aWindow) { } +DOMRequest::~DOMRequest() +{ + mResult.setUndefined(); + mozilla::DropJSObjects(this); +} + NS_IMPL_CYCLE_COLLECTION_CLASS(DOMRequest) NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(DOMRequest, DOMEventTargetHelper) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mError) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mPromise) NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(DOMRequest, DOMEventTargetHelper) NS_IMPL_CYCLE_COLLECTION_UNLINK(mError) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mPromise) tmp->mResult = JSVAL_VOID; NS_IMPL_CYCLE_COLLECTION_UNLINK_END @@ -107,6 +118,10 @@ DOMRequest::FireSuccess(JS::Handle aResult) mResult = aResult; FireEvent(NS_LITERAL_STRING("success"), false, false); + + if (mPromise) { + mPromise->MaybeResolve(mResult); + } } void @@ -120,6 +135,10 @@ DOMRequest::FireError(const nsAString& aError) mError = new DOMError(GetOwner(), aError); FireEvent(NS_LITERAL_STRING("error"), true, true); + + if (mPromise) { + mPromise->MaybeRejectBrokenly(mError); + } } void @@ -133,6 +152,10 @@ DOMRequest::FireError(nsresult aError) mError = new DOMError(GetOwner(), aError); FireEvent(NS_LITERAL_STRING("error"), true, true); + + if (mPromise) { + mPromise->MaybeRejectBrokenly(mError); + } } void @@ -147,6 +170,10 @@ DOMRequest::FireDetailedError(DOMError* aError) mError = aError; FireEvent(NS_LITERAL_STRING("error"), true, true); + + if (mPromise) { + mPromise->MaybeRejectBrokenly(mError); + } } void @@ -175,6 +202,31 @@ DOMRequest::RootResultVal() mozilla::HoldJSObjects(this); } +already_AddRefed +DOMRequest::Then(JSContext* aCx, AnyCallback* aResolveCallback, + AnyCallback* aRejectCallback, ErrorResult& aRv) +{ + if (!mPromise) { + mPromise = Promise::Create(DOMEventTargetHelper::GetParentObject(), aRv); + if (aRv.Failed()) { + return nullptr; + } + if (mDone) { + // Since we create mPromise lazily, it's possible that the DOMRequest object + // has already fired its success/error event. In that case we should + // manually resolve/reject mPromise here. mPromise will take care of + // calling the callbacks on |promise| as needed. + if (mError) { + mPromise->MaybeRejectBrokenly(mError); + } else { + mPromise->MaybeResolve(mResult); + } + } + } + + return mPromise->Then(aCx, aResolveCallback, aRejectCallback, aRv); +} + NS_IMPL_ISUPPORTS(DOMRequestService, nsIDOMRequestService) NS_IMETHODIMP @@ -253,7 +305,7 @@ public: const JS::Value& aResult) { NS_ASSERTION(NS_IsMainThread(), "Wrong thread!"); - AutoSafeJSContext cx; + ThreadsafeAutoSafeJSContext cx; nsRefPtr asyncTask = new FireSuccessAsyncTask(cx, aRequest, aResult); if (NS_FAILED(NS_DispatchToMainThread(asyncTask))) { NS_WARNING("Failed to dispatch to main thread!"); diff --git a/dom/base/DOMRequest.h b/dom/base/DOMRequest.h index cd19dc02adca..e3d152ed2e99 100644 --- a/dom/base/DOMRequest.h +++ b/dom/base/DOMRequest.h @@ -18,12 +18,16 @@ namespace mozilla { namespace dom { +class AnyCallback; +class Promise; + class DOMRequest : public DOMEventTargetHelper, public nsIDOMDOMRequest { protected: JS::Heap mResult; nsRefPtr mError; + nsRefPtr mPromise; bool mDone; public: @@ -67,6 +71,9 @@ public: IMPL_EVENT_HANDLER(success) IMPL_EVENT_HANDLER(error) + already_AddRefed + Then(JSContext* aCx, AnyCallback* aResolveCallback, + AnyCallback* aRejectCallback, ErrorResult& aRv); void FireSuccess(JS::Handle aResult); void FireError(const nsAString& aError); @@ -76,11 +83,7 @@ public: explicit DOMRequest(nsPIDOMWindow* aWindow); protected: - virtual ~DOMRequest() - { - mResult = JSVAL_VOID; - mozilla::DropJSObjects(this); - } + virtual ~DOMRequest(); void FireEvent(const nsAString& aType, bool aBubble, bool aCancelable); diff --git a/dom/base/test/test_domcursor.html b/dom/base/test/test_domcursor.html index bb975a67f373..d33581f124e6 100644 --- a/dom/base/test/test_domcursor.html +++ b/dom/base/test/test_domcursor.html @@ -51,6 +51,7 @@ var tests = [ ok("readyState" in req, "cursor has readyState"); ok("done" in req, "cursor has finished"); ok("continue" in req, "cursor has continue"); + ok(!("then" in req), "cursor should not have a then method"); is(req.readyState, "pending", "readyState is pending"); is(req.result, undefined, "result is undefined"); diff --git a/dom/base/test/test_domrequest.html b/dom/base/test/test_domrequest.html index 0620ad52b1e7..85636ba52972 100644 --- a/dom/base/test/test_domrequest.html +++ b/dom/base/test/test_domrequest.html @@ -17,59 +17,212 @@ var reqserv = SpecialPowers.getDOMRequestService(); ok("createRequest" in reqserv, "appears to be a service"); -// create a request -var req = reqserv.createRequest(window); -ok("result" in req, "request has result"); -ok("error" in req, "request has error"); -ok("onsuccess" in req, "request has onsuccess"); -ok("onerror" in req, "request has onerror"); -ok("readyState" in req, "request has readyState"); +function testBasics() { + // create a request + var req = reqserv.createRequest(window); + ok("result" in req, "request has result"); + ok("error" in req, "request has error"); + ok("onsuccess" in req, "request has onsuccess"); + ok("onerror" in req, "request has onerror"); + ok("readyState" in req, "request has readyState"); + ok("then" in req, "request has then"); -is(req.readyState, "pending", "readyState is pending"); -is(req.result, undefined, "result is undefined"); -is(req.onsuccess, null, "onsuccess is null"); -is(req.onerror, null, "onerror is null"); + is(req.readyState, "pending", "readyState is pending"); + is(req.result, undefined, "result is undefined"); + is(req.onsuccess, null, "onsuccess is null"); + is(req.onerror, null, "onerror is null"); -// fire success -var ev = null; -req.onsuccess = function(e) { - ev = e; + runTest(); } -reqserv.fireSuccess(req, "my result"); -ok(ev, "got success event"); -is(ev.type, "success", "correct type during success"); -is(ev.target, req, "correct target during success"); -is(req.readyState, "done", "correct readyState after success"); -is(req.error, null, "correct error after success"); -is(req.result, "my result", "correct result after success"); -// fire error -req = reqserv.createRequest(window); -ev = null; -req.onerror = function(e) { - ev = e; +function testSuccess() { + // fire success + var req = reqserv.createRequest(window); + var ev = null; + req.onsuccess = function(e) { + ev = e; + } + var result = null; + var promise = req.then(function(r) { + is(r, "my result", "correct result when resolving the promise"); + result = r; + runTest(); + }, function(e) { + ok(false, "promise should not be rejected"); + runTest(); + }); + ok(promise instanceof Promise, "then() should return a Promise"); + reqserv.fireSuccess(req, "my result"); + ok(ev, "got success event"); + is(ev.type, "success", "correct type during success"); + is(ev.target, req, "correct target during success"); + is(req.readyState, "done", "correct readyState after success"); + is(req.error, null, "correct error after success"); + is(req.result, "my result", "correct result after success"); + is(result, null, "Promise should not be resolved synchronously"); } -reqserv.fireError(req, "OhMyError"); -ok(ev, "got error event"); -is(ev.type, "error", "correct type during error"); -is(ev.target, req, "correct target during error"); -is(req.readyState, "done", "correct readyState after error"); -is(req.error.name, "OhMyError", "correct error after error"); -is(req.result, undefined, "correct result after error"); -// fire detailed error -req = reqserv.createRequest(window); -ev = null; -req.onerror = function(e) { - ev = e; -}; -reqserv.fireDetailedError(req, new DOMError("detailedError")); -ok(ev, "got error event"); -is(ev.type, "error", "correct type during error"); -is(ev.target, req, "correct target during error"); -is(req.readyState, "done", "correct readyState after error"); -is(req.error.name, "detailedError", "correct error after error"); -is(req.result, undefined, "correct result after error"); +function testError() { + // fire error + var req = reqserv.createRequest(window); + var ev = null; + req.onerror = function(e) { + ev = e; + } + var error = null; + var promise = req.then(function(r) { + ok(false, "promise should not be resolved"); + runTest(); + }, function(e) { + ok(e instanceof DOMError, "got error rejection"); + ok(e === req.error, "got correct error when rejecting the promise"); + error = e; + runTest(); + }); + ok(promise instanceof Promise, "then() should return a Promise"); + reqserv.fireError(req, "OhMyError"); + ok(ev, "got error event"); + is(ev.type, "error", "correct type during error"); + is(ev.target, req, "correct target during error"); + is(req.readyState, "done", "correct readyState after error"); + is(req.error.name, "OhMyError", "correct error after error"); + is(req.result, undefined, "correct result after error"); + is(error, null, "Promise should not be rejected synchronously"); +} + +function testDetailedError() { + // fire detailed error + var req = reqserv.createRequest(window); + var ev = null; + req.onerror = function(e) { + ev = e; + }; + var error = null; + var promise = req.then(function(r) { + ok(false, "promise should not be resolved"); + runTest(); + }, function(e) { + ok(e instanceof DOMError, "got error rejection"); + ok(e === req.error, "got correct error when rejecting the promise"); + error = e; + runTest(); + }); + ok(promise instanceof Promise, "then() should return a Promise"); + reqserv.fireDetailedError(req, new DOMError("detailedError")); + ok(ev, "got error event"); + is(ev.type, "error", "correct type during error"); + is(ev.target, req, "correct target during error"); + is(req.readyState, "done", "correct readyState after error"); + is(req.error.name, "detailedError", "correct error after error"); + is(req.result, undefined, "correct result after error"); + is(error, null, "Promise should not be rejected synchronously"); +} + +function testThenAfterSuccess() { + // fire success + var req = reqserv.createRequest(window); + var ev = null; + req.onsuccess = function(e) { + ev = e; + } + reqserv.fireSuccess(req, "my result"); + ok(ev, "got success event"); + is(ev.type, "success", "correct type during success"); + is(ev.target, req, "correct target during success"); + is(req.readyState, "done", "correct readyState after success"); + is(req.error, null, "correct error after success"); + is(req.result, "my result", "correct result after success"); + var result = null; + var promise = req.then(function(r) { + is(r, "my result", "correct result when resolving the promise"); + result = r; + runTest(); + }, function(e) { + ok(false, "promise should not be rejected"); + runTest(); + }); + ok(promise instanceof Promise, "then() should return a Promise"); + is(result, null, "Promise should not be resolved synchronously"); +} + +function testThenAfterError() { + // fire error + var req = reqserv.createRequest(window); + var ev = null; + req.onerror = function(e) { + ev = e; + } + reqserv.fireError(req, "OhMyError"); + ok(ev, "got error event"); + is(ev.type, "error", "correct type during error"); + is(ev.target, req, "correct target during error"); + is(req.readyState, "done", "correct readyState after error"); + is(req.error.name, "OhMyError", "correct error after error"); + is(req.result, undefined, "correct result after error"); + var error = null; + var promise = req.then(function(r) { + ok(false, "promise should not be resolved"); + runTest(); + }, function(e) { + ok(e instanceof DOMError, "got error rejection"); + ok(e === req.error, "got correct error when rejecting the promise"); + error = e; + runTest(); + }); + ok(promise instanceof Promise, "then() should return a Promise"); + is(error, null, "Promise should not be rejected synchronously"); +} + +function testDetailedError() { + // fire detailed error + var req = reqserv.createRequest(window); + var ev = null; + req.onerror = function(e) { + ev = e; + }; + var error = null; + var promise = req.then(function(r) { + ok(false, "promise should not be resolved"); + runTest(); + }, function(e) { + ok(e instanceof DOMError, "got error rejection"); + ok(e === req.error, "got correct error when rejecting the promise"); + error = e; + runTest(); + }); + ok(promise instanceof Promise, "then() should return a Promise"); + reqserv.fireDetailedError(req, new DOMError("detailedError")); + ok(ev, "got error event"); + is(ev.type, "error", "correct type during error"); + is(ev.target, req, "correct target during error"); + is(req.readyState, "done", "correct readyState after error"); + is(req.error.name, "detailedError", "correct error after error"); + is(req.result, undefined, "correct result after error"); + is(error, null, "Promise should not be rejected synchronously"); +} + +var tests = [ + testBasics, + testSuccess, + testError, + testDetailedError, + testThenAfterSuccess, + testThenAfterError, +]; + +function runTest() { + if (!tests.length) { + SimpleTest.finish(); + return; + } + + var test = tests.shift(); + test(); +} + +SimpleTest.waitForExplicitFinish(); +runTest(); + diff --git a/dom/bindings/Bindings.conf b/dom/bindings/Bindings.conf index 20ae2c5903ab..511924ba72a1 100644 --- a/dom/bindings/Bindings.conf +++ b/dom/bindings/Bindings.conf @@ -352,6 +352,10 @@ DOMInterfaces = { 'headerFile': 'mozilla/dom/DOMRect.h', }, +'DOMRequest': { + 'implicitJSContext': [ 'then' ], +}, + 'DOMSettableTokenList': { 'nativeType': 'nsDOMSettableTokenList', }, diff --git a/dom/bindings/ToJSValue.h b/dom/bindings/ToJSValue.h index 1f365ddde360..415a411dd92a 100644 --- a/dom/bindings/ToJSValue.h +++ b/dom/bindings/ToJSValue.h @@ -236,6 +236,24 @@ ToJSValue(JSContext* aCx, JS::Handle aArgument, return MaybeWrapValue(aCx, aValue); } +// Accept existing JS values on the Heap (which may not be same-compartment with us +inline bool +ToJSValue(JSContext* aCx, JS::Heap aArgument, + JS::MutableHandle aValue) +{ + aValue.set(aArgument); + return MaybeWrapValue(aCx, aValue); +} + +// Accept existing rooted JS values (which may not be same-compartment with us +inline bool +ToJSValue(JSContext* aCx, JS::Rooted aArgument, + JS::MutableHandle aValue) +{ + aValue.set(aArgument); + return MaybeWrapValue(aCx, aValue); +} + // Accept nsresult, for use in rejections, and create an XPCOM // exception object representing that nsresult. bool diff --git a/dom/webidl/DOMCursor.webidl b/dom/webidl/DOMCursor.webidl index b18a75539bdf..3ad36a76fa1a 100644 --- a/dom/webidl/DOMCursor.webidl +++ b/dom/webidl/DOMCursor.webidl @@ -3,8 +3,10 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -interface DOMCursor : DOMRequest { +interface DOMCursor : EventTarget { readonly attribute boolean done; [Throws] void continue(); }; + +DOMCursor implements DOMRequestShared; diff --git a/dom/webidl/DOMRequest.webidl b/dom/webidl/DOMRequest.webidl index a93704bd2211..039446904563 100644 --- a/dom/webidl/DOMRequest.webidl +++ b/dom/webidl/DOMRequest.webidl @@ -5,7 +5,8 @@ enum DOMRequestReadyState { "pending", "done" }; -interface DOMRequest : EventTarget { +[NoInterfaceObject] +interface DOMRequestShared { readonly attribute DOMRequestReadyState readyState; readonly attribute any result; @@ -14,3 +15,13 @@ interface DOMRequest : EventTarget { attribute EventHandler onsuccess; attribute EventHandler onerror; }; + +interface DOMRequest : EventTarget { + // The [TreatNonCallableAsNull] annotation is required since then() should do + // nothing instead of throwing errors when non-callable arguments are passed. + [NewObject, Throws] + Promise then([TreatNonCallableAsNull] optional AnyCallback? fulfillCallback = null, + [TreatNonCallableAsNull] optional AnyCallback? rejectCallback = null); +}; + +DOMRequest implements DOMRequestShared; diff --git a/js/public/RootingAPI.h b/js/public/RootingAPI.h index 275d06005c26..bc0277c66b29 100644 --- a/js/public/RootingAPI.h +++ b/js/public/RootingAPI.h @@ -220,7 +220,7 @@ class Heap : public js::HeapBase * that will be used for both lvalue and rvalue copies, so we can simply * omit the rvalue variant. */ - explicit Heap(const Heap &p) { init(p.ptr); } + Heap(const Heap &p) { init(p.ptr); } ~Heap() { if (js::GCMethods::needsPostBarrier(ptr))