Bug 1035060 - Implement AbortablePromise. r=bz

This commit is contained in:
Yuan Xulei 2014-09-12 10:18:49 +08:00
parent 6a9b2b1e98
commit fcfee0071f
14 changed files with 434 additions and 40 deletions

View File

@ -91,6 +91,10 @@ DOMInterfaces = {
'nativeType': 'mozilla::dom::Activity',
},
'MozAbortablePromise': {
'nativeType': 'mozilla::dom::AbortablePromise',
},
'AbstractWorker': {
'concrete': False
},

View File

@ -0,0 +1,118 @@
/* -*- Mode: c++; c-basic-offset: 2; indent-tabs-mode: nil; tab-width: 40 -*- */
/* vim: set ts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* 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/. */
#include "mozilla/dom/AbortablePromise.h"
#include "mozilla/dom/AbortablePromiseBinding.h"
#include "mozilla/dom/PromiseNativeAbortCallback.h"
#include "mozilla/ErrorResult.h"
#include "nsThreadUtils.h"
#include "PromiseCallback.h"
namespace mozilla {
namespace dom {
NS_IMPL_CYCLE_COLLECTING_ADDREF(PromiseNativeAbortCallback)
NS_IMPL_CYCLE_COLLECTING_RELEASE(PromiseNativeAbortCallback)
NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(PromiseNativeAbortCallback)
NS_INTERFACE_MAP_ENTRY(nsISupports)
NS_INTERFACE_MAP_END
NS_IMPL_CYCLE_COLLECTION_0(PromiseNativeAbortCallback)
NS_IMPL_ADDREF_INHERITED(AbortablePromise, Promise)
NS_IMPL_RELEASE_INHERITED(AbortablePromise, Promise)
NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION_INHERITED(AbortablePromise)
NS_INTERFACE_MAP_END_INHERITING(Promise)
NS_IMPL_CYCLE_COLLECTION_INHERITED(AbortablePromise, Promise, mAbortCallback)
AbortablePromise::AbortablePromise(nsIGlobalObject* aGlobal,
PromiseNativeAbortCallback& aAbortCallback)
: Promise(aGlobal)
, mAbortCallback(&aAbortCallback)
{
}
AbortablePromise::AbortablePromise(nsIGlobalObject* aGlobal)
: Promise(aGlobal)
{
}
AbortablePromise::~AbortablePromise()
{
}
/* static */ already_AddRefed<AbortablePromise>
AbortablePromise::Create(nsIGlobalObject* aGlobal,
PromiseNativeAbortCallback& aAbortCallback,
ErrorResult& aRv)
{
nsRefPtr<AbortablePromise> p = new AbortablePromise(aGlobal, aAbortCallback);
p->CreateWrapper(aRv);
if (aRv.Failed()) {
return nullptr;
}
return p.forget();
}
JSObject*
AbortablePromise::WrapObject(JSContext* aCx)
{
return MozAbortablePromiseBinding::Wrap(aCx, this);
}
/* static */ already_AddRefed<AbortablePromise>
AbortablePromise::Constructor(const GlobalObject& aGlobal, PromiseInit& aInit,
AbortCallback& aAbortCallback, ErrorResult& aRv)
{
nsCOMPtr<nsIGlobalObject> global;
global = do_QueryInterface(aGlobal.GetAsSupports());
if (!global) {
aRv.Throw(NS_ERROR_UNEXPECTED);
return nullptr;
}
nsRefPtr<AbortablePromise> promise = new AbortablePromise(global);
promise->CreateWrapper(aRv);
if (aRv.Failed()) {
return nullptr;
}
promise->CallInitFunction(aGlobal, aInit, aRv);
if (aRv.Failed()) {
return nullptr;
}
promise->mAbortCallback = &aAbortCallback;
return promise.forget();
}
void
AbortablePromise::Abort()
{
if (IsPending()) {
return;
}
MaybeReject(NS_ERROR_ABORT);
nsCOMPtr<nsIRunnable> runnable =
NS_NewRunnableMethod(this, &AbortablePromise::DoAbort);
Promise::DispatchToMainOrWorkerThread(runnable);
}
void
AbortablePromise::DoAbort()
{
if (mAbortCallback.HasWebIDLCallback()) {
ErrorResult rv;
mAbortCallback.GetWebIDLCallback()->Call(rv);
return;
}
mAbortCallback.GetXPCOMCallback()->Call();
}
} // namespace dom
} // namespace mozilla

View File

@ -0,0 +1,66 @@
/* -*- Mode: c++; c-basic-offset: 2; indent-tabs-mode: nil; tab-width: 40 -*- */
/* vim: set ts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* 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/. */
#ifndef mozilla_dom_AbortablePromise_h__
#define mozilla_dom_AbortablePromise_h__
#include "js/TypeDecls.h"
#include "mozilla/dom/Promise.h"
#include "mozilla/dom/CallbackObject.h"
namespace mozilla {
namespace dom {
class AbortCallback;
class PromiseNativeAbortCallback;
class AbortablePromise
: public Promise
{
public:
NS_DECL_ISUPPORTS_INHERITED
NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(AbortablePromise, Promise)
public:
// It is the same as Promise::Create except that this takes an extra
// aAbortCallback parameter to set the abort callback handler.
static already_AddRefed<AbortablePromise>
Create(nsIGlobalObject* aGlobal, PromiseNativeAbortCallback& aAbortCallback,
ErrorResult& aRv);
protected:
// Constructor used to create native AbortablePromise with C++.
AbortablePromise(nsIGlobalObject* aGlobal,
PromiseNativeAbortCallback& aAbortCallback);
// Constructor used to create AbortablePromise for JavaScript. It should be
// called by the static AbortablePromise::Constructor.
AbortablePromise(nsIGlobalObject* aGlobal);
virtual ~AbortablePromise();
public:
virtual JSObject*
WrapObject(JSContext* aCx) MOZ_OVERRIDE;
static already_AddRefed<AbortablePromise>
Constructor(const GlobalObject& aGlobal, PromiseInit& aInit,
AbortCallback& aAbortCallback, ErrorResult& aRv);
void Abort();
private:
void DoAbort();
// The callback functions to abort the promise.
CallbackObjectHolder<AbortCallback,
PromiseNativeAbortCallback> mAbortCallback;
};
} // namespace dom
} // namespace mozilla
#endif // mozilla_dom_AbortablePromise_h__

View File

@ -325,26 +325,33 @@ Promise::WrapObject(JSContext* aCx)
already_AddRefed<Promise>
Promise::Create(nsIGlobalObject* aGlobal, ErrorResult& aRv)
{
AutoJSAPI jsapi;
if (!jsapi.Init(aGlobal)) {
aRv.Throw(NS_ERROR_UNEXPECTED);
nsRefPtr<Promise> p = new Promise(aGlobal);
p->CreateWrapper(aRv);
if (aRv.Failed()) {
return nullptr;
}
return p.forget();
}
void
Promise::CreateWrapper(ErrorResult& aRv)
{
AutoJSAPI jsapi;
if (!jsapi.Init(mGlobal)) {
aRv.Throw(NS_ERROR_UNEXPECTED);
return;
}
JSContext* cx = jsapi.cx();
nsRefPtr<Promise> p = new Promise(aGlobal);
JS::Rooted<JS::Value> ignored(cx);
if (!WrapNewBindingObject(cx, p, &ignored)) {
if (!WrapNewBindingObject(cx, this, &ignored)) {
JS_ClearPendingException(cx);
aRv.Throw(NS_ERROR_OUT_OF_MEMORY);
return nullptr;
return;
}
// Need the .get() bit here to get template deduction working right
dom::PreserveWrapper(p.get());
return p.forget();
dom::PreserveWrapper(this);
}
void
@ -483,8 +490,6 @@ Promise::CreateThenableFunction(JSContext* aCx, Promise* aPromise, uint32_t aTas
Promise::Constructor(const GlobalObject& aGlobal,
PromiseInit& aInit, ErrorResult& aRv)
{
JSContext* cx = aGlobal.Context();
nsCOMPtr<nsIGlobalObject> global;
global = do_QueryInterface(aGlobal.GetAsSupports());
if (!global) {
@ -497,20 +502,34 @@ Promise::Constructor(const GlobalObject& aGlobal,
return nullptr;
}
JS::Rooted<JSObject*> resolveFunc(cx,
CreateFunction(cx, aGlobal.Get(), promise,
PromiseCallback::Resolve));
if (!resolveFunc) {
aRv.Throw(NS_ERROR_UNEXPECTED);
promise->CallInitFunction(aGlobal, aInit, aRv);
if (aRv.Failed()) {
return nullptr;
}
return promise.forget();
}
void
Promise::CallInitFunction(const GlobalObject& aGlobal,
PromiseInit& aInit, ErrorResult& aRv)
{
JSContext* cx = aGlobal.Context();
JS::Rooted<JSObject*> resolveFunc(cx,
CreateFunction(cx, aGlobal.Get(), this,
PromiseCallback::Resolve));
if (!resolveFunc) {
aRv.Throw(NS_ERROR_UNEXPECTED);
return;
}
JS::Rooted<JSObject*> rejectFunc(cx,
CreateFunction(cx, aGlobal.Get(), promise,
CreateFunction(cx, aGlobal.Get(), this,
PromiseCallback::Reject));
if (!rejectFunc) {
aRv.Throw(NS_ERROR_UNEXPECTED);
return nullptr;
return;
}
aInit.Call(resolveFunc, rejectFunc, aRv, CallbackObject::eRethrowExceptions);
@ -524,13 +543,11 @@ Promise::Constructor(const GlobalObject& aGlobal,
// function Promise(arg) { try { arg(a, b); } catch (e) { this.reject(e); }}
if (!JS_WrapValue(cx, &value)) {
aRv.Throw(NS_ERROR_UNEXPECTED);
return nullptr;
return;
}
promise->MaybeRejectInternal(cx, value);
MaybeRejectInternal(cx, value);
}
return promise.forget();
}
/* static */ already_AddRefed<Promise>

View File

@ -50,9 +50,9 @@ public:
Notify(JSContext* aCx, workers::Status aStatus) MOZ_OVERRIDE;
};
class Promise MOZ_FINAL : public nsISupports,
public nsWrapperCache,
public SupportsWeakPtr<Promise>
class Promise : public nsISupports,
public nsWrapperCache,
public SupportsWeakPtr<Promise>
{
friend class NativePromiseCallback;
friend class PromiseResolverTask;
@ -65,8 +65,6 @@ class Promise MOZ_FINAL : public nsISupports,
friend class ThenableResolverTask;
friend class WrapperPromiseCallback;
~Promise();
public:
NS_DECL_CYCLE_COLLECTING_ISUPPORTS
NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(Promise)
@ -159,11 +157,32 @@ public:
void AppendNativeHandler(PromiseNativeHandler* aRunnable);
private:
protected:
// Do NOT call this unless you're Promise::Create. I wish we could enforce
// that from inside this class too, somehow.
explicit Promise(nsIGlobalObject* aGlobal);
virtual ~Promise();
// Queue an async task to current main or worker thread.
static void
DispatchToMainOrWorkerThread(nsIRunnable* aRunnable);
// Do JS-wrapping after Promise creation.
void CreateWrapper(ErrorResult& aRv);
// Create the JS resolving functions of resolve() and reject(). And provide
// references to the two functions by calling PromiseInit passed from Promise
// constructor.
void CallInitFunction(const GlobalObject& aGlobal, PromiseInit& aInit,
ErrorResult& aRv);
bool IsPending()
{
return mResolvePending;
}
private:
friend class PromiseDebugging;
enum PromiseState {
@ -189,10 +208,6 @@ private:
mResult = aValue;
}
// Queue an async task to current main or worker thread.
static void
DispatchToMainOrWorkerThread(nsIRunnable* aRunnable);
// This method processes promise's resolve/reject callbacks with promise's
// result. It's executed when the resolver.resolve() or resolver.reject() is
// called or when the promise already has a result and new callbacks are

View File

@ -20,13 +20,7 @@ NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(PromiseCallback)
NS_INTERFACE_MAP_ENTRY(nsISupports)
NS_INTERFACE_MAP_END
NS_IMPL_CYCLE_COLLECTION_CLASS(PromiseCallback)
NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(PromiseCallback)
NS_IMPL_CYCLE_COLLECTION_UNLINK_END
NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(PromiseCallback)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
NS_IMPL_CYCLE_COLLECTION_0(PromiseCallback)
PromiseCallback::PromiseCallback()
{

View File

@ -0,0 +1,36 @@
/* -*- Mode: c++; c-basic-offset: 2; indent-tabs-mode: nil; tab-width: 40 -*- */
/* vim: set ts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* 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/. */
#ifndef mozilla_dom_PromiseNativeAbortCallback_h
#define mozilla_dom_PromiseNativeAbortCallback_h
#include "nsISupports.h"
namespace mozilla {
namespace dom {
/*
* PromiseNativeAbortCallback allows C++ to react to an AbortablePromise being
* aborted.
*/
class PromiseNativeAbortCallback : public nsISupports
{
protected:
virtual ~PromiseNativeAbortCallback()
{ }
public:
// Implemented in AbortablePromise.cpp.
NS_DECL_CYCLE_COLLECTING_ISUPPORTS
NS_DECL_CYCLE_COLLECTION_CLASS(PromiseNativeAbortCallback)
virtual void Call() = 0;
};
} // namespace dom
} // namespace mozilla
#endif // mozilla_dom_PromiseNativeAbortCallback_h

View File

@ -5,13 +5,16 @@
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
EXPORTS.mozilla.dom += [
'AbortablePromise.h',
'Promise.h',
'PromiseDebugging.h',
'PromiseNativeAbortCallback.h',
'PromiseNativeHandler.h',
'PromiseWorkerProxy.h',
]
UNIFIED_SOURCES += [
'AbortablePromise.cpp',
'Promise.cpp',
'PromiseCallback.cpp',
'PromiseDebugging.cpp'

View File

@ -4,3 +4,4 @@
[test_promise.html]
[test_promise_utils.html]
[test_resolve.html]
[test_abortable_promise.html]

View File

@ -0,0 +1,115 @@
<!--
Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/
-->
<html>
<head>
<title>Promise.resolve(anything) Test</title>
<script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
</head>
<body>
<p id="display"></p>
<div id="content" style="display: none">
</div>
<pre id="test">
<script type="application/javascript"><!--
SimpleTest.waitForExplicitFinish();
SpecialPowers.pushPrefEnv({"set": [["dom.abortablepromise.enabled", true]]},
runTest);
var gTests = [testPending, testResolved, testRejected];
function runTest() {
if (gTests.length == 0) {
SimpleTest.finish();
return;
}
new Promise(gTests.shift()).then(runTest, SimpleTest.finish);
}
// Aborting pending promise should first reject the promise and then call the
// abortable callback.
// The test succeeds once both the rejection handler and the abort handler have
// been called.
function testPending(succeed, fail) {
var rejected = false;
var aborted = false;
var p = new MozAbortablePromise(function(resolve, reject) {
// Wait for a while so that the promise can be aborted before resolved.
SimpleTest.executeSoon(function() {
resolve(0);
});
}, function abortable() {
aborted = true;
ok(true, "Promise aborted.");
if (rejected) {
succeed();
}
});
p.then(function() {
ok(false, "Failed to abort pending promise.");
fail();
}, function(what) {
rejected = true;
var isAbortException = (what && what.name) == "NS_ERROR_ABORT";
ok(isAbortException, "Should have NS_ERROR_ABORT exception");
if (!isAbortException) {
fail();
}
if (aborted) {
succeed();
}
});
// Abort the promise on creation.
p.abort();
}
// Do nothing when aborting resolved promise.
function testResolved(succeed, fail) {
var p = new MozAbortablePromise(function(resolve, reject) {
resolve();
}, function abortable() {
ok(false, "Should not abort a resolved promise.");
fail();
});
p.then(function() {
ok(true, "Promise resolved.");
// Wait for a while to ensure abort handle won't be called.
setTimeout(succeed, 1000);
}, function(what) {
ok(false, "Failed to resolve promise: " + what);
fail();
});
p.abort();
}
// Do nothing when aborting rejected promise.
function testRejected(succeed, fail) {
var p = new MozAbortablePromise(function(resolve, reject) {
reject(0);
}, function abortable() {
ok(false, "Should not abort a rejected promise.");
fail();
});
p.then(function() {
ok(false, "Failed to reject promise.");
fail();
}, function(what) {
is(what, 0, "promise rejected: " + what);
// Wait for a while to ensure abort handle won't be called.
setTimeout(succeed, 1000);
});
p.abort();
}
// -->
</script>
</pre>
</body>
</html>

View File

@ -118,6 +118,8 @@ var legacyMozPrefixedInterfaces =
// IMPORTANT: Do not change the list below without review from a DOM peer!
var interfaceNamesInGlobalScope =
[
// IMPORTANT: Do not change this list without review from a DOM peer!
{name: "MozAbortablePromise", pref: "dom.abortablepromise.enabled"},
// IMPORTANT: Do not change this list without review from a DOM peer!
{name: "AlarmsManager", pref: "dom.mozAlarms.enabled"},
// IMPORTANT: Do not change this list without review from a DOM peer!

View File

@ -0,0 +1,19 @@
/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* This Source Code Form is subject to the terms of the Mozilla Public
* 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/.
*/
callback AbortCallback = void ();
[Constructor(PromiseInit init, AbortCallback abortCallback),
Pref="dom.abortablepromise.enabled"]
interface MozAbortablePromise : _Promise {
/*
* Aborts the promise.
* If the promise has not been resolved or rejected, it should be rejected
* with an Exception of type abort and then AbortCallback is called
* asynchronously. Otherwise, nothing should be done.
*/
void abort();
};

View File

@ -16,6 +16,7 @@ PREPROCESSED_WEBIDL_FILES = [
]
WEBIDL_FILES = [
'AbortablePromise.webidl',
'AbstractWorker.webidl',
'ActivityRequestHandler.webidl',
'AlarmsManager.webidl',

View File

@ -107,6 +107,9 @@ pref("offline-apps.quota.warn", 51200);
// cache compression turned off for now - see bug #715198
pref("browser.cache.compression_level", 0);
// Whether or not MozAbortablePromise is enabled.
pref("dom.abortablepromise.enabled", false);
// Whether or not testing features are enabled.
pref("dom.quotaManager.testing", false);