mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-08 20:47:44 +00:00
006d532a8a
Using JSAPI is tedious and error-prone. It's much better to handle that once in the Promise code and allow callers to pass C++ objects.
1094 lines
31 KiB
C++
1094 lines
31 KiB
C++
/* -*- 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/Promise.h"
|
|
|
|
#include "jsfriendapi.h"
|
|
#include "mozilla/dom/OwningNonNull.h"
|
|
#include "mozilla/dom/PromiseBinding.h"
|
|
#include "mozilla/Preferences.h"
|
|
#include "PromiseCallback.h"
|
|
#include "PromiseNativeHandler.h"
|
|
#include "nsContentUtils.h"
|
|
#include "WorkerPrivate.h"
|
|
#include "WorkerRunnable.h"
|
|
#include "nsJSPrincipals.h"
|
|
#include "nsJSUtils.h"
|
|
#include "nsPIDOMWindow.h"
|
|
#include "nsJSEnvironment.h"
|
|
|
|
namespace mozilla {
|
|
namespace dom {
|
|
|
|
using namespace workers;
|
|
|
|
NS_IMPL_ISUPPORTS0(PromiseNativeHandler)
|
|
|
|
// PromiseTask
|
|
|
|
// This class processes the promise's callbacks with promise's result.
|
|
class PromiseTask MOZ_FINAL : public nsRunnable
|
|
{
|
|
public:
|
|
PromiseTask(Promise* aPromise)
|
|
: mPromise(aPromise)
|
|
{
|
|
MOZ_ASSERT(aPromise);
|
|
MOZ_COUNT_CTOR(PromiseTask);
|
|
}
|
|
|
|
~PromiseTask()
|
|
{
|
|
MOZ_COUNT_DTOR(PromiseTask);
|
|
}
|
|
|
|
NS_IMETHOD Run()
|
|
{
|
|
mPromise->mTaskPending = false;
|
|
mPromise->RunTask();
|
|
return NS_OK;
|
|
}
|
|
|
|
private:
|
|
nsRefPtr<Promise> mPromise;
|
|
};
|
|
|
|
class WorkerPromiseTask MOZ_FINAL : public WorkerSameThreadRunnable
|
|
{
|
|
public:
|
|
WorkerPromiseTask(WorkerPrivate* aWorkerPrivate, Promise* aPromise)
|
|
: WorkerSameThreadRunnable(aWorkerPrivate)
|
|
, mPromise(aPromise)
|
|
{
|
|
MOZ_ASSERT(aPromise);
|
|
MOZ_COUNT_CTOR(WorkerPromiseTask);
|
|
}
|
|
|
|
~WorkerPromiseTask()
|
|
{
|
|
MOZ_COUNT_DTOR(WorkerPromiseTask);
|
|
}
|
|
|
|
bool
|
|
WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate)
|
|
{
|
|
mPromise->mTaskPending = false;
|
|
mPromise->RunTask();
|
|
return true;
|
|
}
|
|
|
|
private:
|
|
nsRefPtr<Promise> mPromise;
|
|
};
|
|
|
|
class PromiseResolverMixin
|
|
{
|
|
public:
|
|
PromiseResolverMixin(Promise* aPromise,
|
|
JS::Handle<JS::Value> aValue,
|
|
Promise::PromiseState aState)
|
|
: mPromise(aPromise)
|
|
, mValue(aValue)
|
|
, mState(aState)
|
|
{
|
|
MOZ_ASSERT(aPromise);
|
|
MOZ_ASSERT(mState != Promise::Pending);
|
|
MOZ_COUNT_CTOR(PromiseResolverMixin);
|
|
|
|
JSContext* cx = nsContentUtils::GetDefaultJSContextForThread();
|
|
|
|
/* It's safe to use unsafeGet() here: the unsafeness comes from the
|
|
* possibility of updating the value of mJSObject without triggering the
|
|
* barriers. However if the value will always be marked, post barriers
|
|
* unnecessary. */
|
|
JS_AddNamedValueRootRT(JS_GetRuntime(cx), mValue.unsafeGet(),
|
|
"PromiseResolverMixin.mValue");
|
|
}
|
|
|
|
virtual ~PromiseResolverMixin()
|
|
{
|
|
NS_ASSERT_OWNINGTHREAD(PromiseResolverMixin);
|
|
MOZ_COUNT_DTOR(PromiseResolverMixin);
|
|
|
|
JSContext* cx = nsContentUtils::GetDefaultJSContextForThread();
|
|
|
|
/* It's safe to use unsafeGet() here: the unsafeness comes from the
|
|
* possibility of updating the value of mJSObject without triggering the
|
|
* barriers. However if the value will always be marked, post barriers
|
|
* unnecessary. */
|
|
JS_RemoveValueRootRT(JS_GetRuntime(cx), mValue.unsafeGet());
|
|
}
|
|
|
|
protected:
|
|
void
|
|
RunInternal()
|
|
{
|
|
NS_ASSERT_OWNINGTHREAD(PromiseResolverMixin);
|
|
mPromise->RunResolveTask(
|
|
JS::Handle<JS::Value>::fromMarkedLocation(mValue.address()),
|
|
mState, Promise::SyncTask);
|
|
}
|
|
|
|
private:
|
|
nsRefPtr<Promise> mPromise;
|
|
JS::Heap<JS::Value> mValue;
|
|
Promise::PromiseState mState;
|
|
NS_DECL_OWNINGTHREAD;
|
|
};
|
|
|
|
// This class processes the promise's callbacks with promise's result.
|
|
class PromiseResolverTask MOZ_FINAL : public nsRunnable,
|
|
public PromiseResolverMixin
|
|
{
|
|
public:
|
|
PromiseResolverTask(Promise* aPromise,
|
|
JS::Handle<JS::Value> aValue,
|
|
Promise::PromiseState aState)
|
|
: PromiseResolverMixin(aPromise, aValue, aState)
|
|
{}
|
|
|
|
~PromiseResolverTask()
|
|
{}
|
|
|
|
NS_IMETHOD Run()
|
|
{
|
|
RunInternal();
|
|
return NS_OK;
|
|
}
|
|
};
|
|
|
|
class WorkerPromiseResolverTask MOZ_FINAL : public WorkerSameThreadRunnable,
|
|
public PromiseResolverMixin
|
|
{
|
|
public:
|
|
WorkerPromiseResolverTask(WorkerPrivate* aWorkerPrivate,
|
|
Promise* aPromise,
|
|
JS::Handle<JS::Value> aValue,
|
|
Promise::PromiseState aState)
|
|
: WorkerSameThreadRunnable(aWorkerPrivate),
|
|
PromiseResolverMixin(aPromise, aValue, aState)
|
|
{}
|
|
|
|
~WorkerPromiseResolverTask()
|
|
{}
|
|
|
|
bool
|
|
WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate)
|
|
{
|
|
RunInternal();
|
|
return true;
|
|
}
|
|
};
|
|
|
|
// Promise
|
|
|
|
NS_IMPL_CYCLE_COLLECTION_CLASS(Promise)
|
|
|
|
NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(Promise)
|
|
tmp->MaybeReportRejected();
|
|
NS_IMPL_CYCLE_COLLECTION_UNLINK(mGlobal)
|
|
NS_IMPL_CYCLE_COLLECTION_UNLINK(mResolveCallbacks);
|
|
NS_IMPL_CYCLE_COLLECTION_UNLINK(mRejectCallbacks);
|
|
tmp->mResult = JS::UndefinedValue();
|
|
NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER
|
|
NS_IMPL_CYCLE_COLLECTION_UNLINK_END
|
|
|
|
NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(Promise)
|
|
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mGlobal)
|
|
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mResolveCallbacks);
|
|
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mRejectCallbacks);
|
|
NS_IMPL_CYCLE_COLLECTION_TRAVERSE_SCRIPT_OBJECTS
|
|
NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
|
|
|
|
NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN(Promise)
|
|
NS_IMPL_CYCLE_COLLECTION_TRACE_JSVAL_MEMBER_CALLBACK(mResult)
|
|
NS_IMPL_CYCLE_COLLECTION_TRACE_PRESERVED_WRAPPER
|
|
NS_IMPL_CYCLE_COLLECTION_TRACE_END
|
|
|
|
NS_IMPL_CYCLE_COLLECTING_ADDREF(Promise)
|
|
NS_IMPL_CYCLE_COLLECTING_RELEASE(Promise)
|
|
|
|
NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(Promise)
|
|
NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
|
|
NS_INTERFACE_MAP_ENTRY(nsISupports)
|
|
NS_INTERFACE_MAP_END
|
|
|
|
Promise::Promise(nsIGlobalObject* aGlobal)
|
|
: mGlobal(aGlobal)
|
|
, mResult(JS::UndefinedValue())
|
|
, mState(Pending)
|
|
, mTaskPending(false)
|
|
, mHadRejectCallback(false)
|
|
, mResolvePending(false)
|
|
{
|
|
MOZ_ASSERT(mGlobal);
|
|
|
|
mozilla::HoldJSObjects(this);
|
|
SetIsDOMBinding();
|
|
}
|
|
|
|
Promise::~Promise()
|
|
{
|
|
MaybeReportRejected();
|
|
mResult = JS::UndefinedValue();
|
|
mozilla::DropJSObjects(this);
|
|
}
|
|
|
|
JSObject*
|
|
Promise::WrapObject(JSContext* aCx, JS::Handle<JSObject*> aScope)
|
|
{
|
|
return PromiseBinding::Wrap(aCx, aScope, this);
|
|
}
|
|
|
|
JSObject*
|
|
Promise::GetOrCreateWrapper(JSContext* aCx)
|
|
{
|
|
if (JSObject* wrapper = GetWrapper()) {
|
|
return wrapper;
|
|
}
|
|
|
|
nsIGlobalObject* global = GetParentObject();
|
|
MOZ_ASSERT(global);
|
|
|
|
JS::Rooted<JSObject*> scope(aCx, global->GetGlobalJSObject());
|
|
if (!scope) {
|
|
JS_ReportError(aCx, "can't get scope");
|
|
return nullptr;
|
|
}
|
|
|
|
JS::Rooted<JS::Value> val(aCx);
|
|
if (!WrapNewBindingObject(aCx, scope, this, &val)) {
|
|
MOZ_ASSERT(JS_IsExceptionPending(aCx));
|
|
return nullptr;
|
|
}
|
|
|
|
return GetWrapper();
|
|
}
|
|
|
|
void
|
|
Promise::MaybeResolve(JSContext* aCx,
|
|
JS::Handle<JS::Value> aValue)
|
|
{
|
|
MaybeResolveInternal(aCx, aValue);
|
|
}
|
|
|
|
void
|
|
Promise::MaybeReject(JSContext* aCx,
|
|
JS::Handle<JS::Value> aValue)
|
|
{
|
|
MaybeRejectInternal(aCx, aValue);
|
|
}
|
|
|
|
static void
|
|
EnterCompartment(Maybe<JSAutoCompartment>& aAc, JSContext* aCx,
|
|
JS::Handle<JS::Value> aValue)
|
|
{
|
|
// FIXME Bug 878849
|
|
if (aValue.isObject()) {
|
|
JS::Rooted<JSObject*> rooted(aCx, &aValue.toObject());
|
|
aAc.construct(aCx, rooted);
|
|
}
|
|
}
|
|
|
|
enum {
|
|
SLOT_PROMISE = 0,
|
|
SLOT_DATA
|
|
};
|
|
|
|
/* static */ bool
|
|
Promise::JSCallback(JSContext* aCx, unsigned aArgc, JS::Value* aVp)
|
|
{
|
|
JS::CallArgs args = CallArgsFromVp(aArgc, aVp);
|
|
|
|
JS::Rooted<JS::Value> v(aCx,
|
|
js::GetFunctionNativeReserved(&args.callee(),
|
|
SLOT_PROMISE));
|
|
MOZ_ASSERT(v.isObject());
|
|
|
|
Promise* promise;
|
|
if (NS_FAILED(UNWRAP_OBJECT(Promise, &v.toObject(), promise))) {
|
|
return Throw(aCx, NS_ERROR_UNEXPECTED);
|
|
}
|
|
|
|
v = js::GetFunctionNativeReserved(&args.callee(), SLOT_DATA);
|
|
PromiseCallback::Task task = static_cast<PromiseCallback::Task>(v.toInt32());
|
|
|
|
if (task == PromiseCallback::Resolve) {
|
|
promise->MaybeResolveInternal(aCx, args.get(0));
|
|
} else {
|
|
promise->MaybeRejectInternal(aCx, args.get(0));
|
|
}
|
|
|
|
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)
|
|
{
|
|
JSFunction* func = js::NewFunctionWithReserved(aCx, JSCallback,
|
|
1 /* nargs */, 0 /* flags */,
|
|
aParent, 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);
|
|
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;
|
|
}
|
|
|
|
/* static */ already_AddRefed<Promise>
|
|
Promise::Constructor(const GlobalObject& aGlobal,
|
|
PromiseInit& aInit, ErrorResult& aRv)
|
|
{
|
|
JSContext* cx = aGlobal.GetContext();
|
|
|
|
nsCOMPtr<nsIGlobalObject> global;
|
|
global = do_QueryInterface(aGlobal.GetAsSupports());
|
|
if (!global) {
|
|
aRv.Throw(NS_ERROR_UNEXPECTED);
|
|
return nullptr;
|
|
}
|
|
|
|
nsRefPtr<Promise> promise = new Promise(global);
|
|
|
|
JS::Rooted<JSObject*> resolveFunc(cx,
|
|
CreateFunction(cx, aGlobal.Get(), promise,
|
|
PromiseCallback::Resolve));
|
|
if (!resolveFunc) {
|
|
aRv.Throw(NS_ERROR_UNEXPECTED);
|
|
return nullptr;
|
|
}
|
|
|
|
JS::Rooted<JSObject*> rejectFunc(cx,
|
|
CreateFunction(cx, aGlobal.Get(), promise,
|
|
PromiseCallback::Reject));
|
|
if (!rejectFunc) {
|
|
aRv.Throw(NS_ERROR_UNEXPECTED);
|
|
return nullptr;
|
|
}
|
|
|
|
aInit.Call(resolveFunc, rejectFunc, aRv, CallbackObject::eRethrowExceptions);
|
|
aRv.WouldReportJSException();
|
|
|
|
if (aRv.IsJSException()) {
|
|
JS::Rooted<JS::Value> value(cx);
|
|
aRv.StealJSException(cx, &value);
|
|
|
|
Maybe<JSAutoCompartment> ac;
|
|
EnterCompartment(ac, cx, value);
|
|
promise->MaybeRejectInternal(cx, value);
|
|
}
|
|
|
|
return promise.forget();
|
|
}
|
|
|
|
/* static */ already_AddRefed<Promise>
|
|
Promise::Resolve(const GlobalObject& aGlobal, JSContext* aCx,
|
|
JS::Handle<JS::Value> aValue, ErrorResult& aRv)
|
|
{
|
|
// If a Promise was passed, just return it.
|
|
if (aValue.isObject()) {
|
|
JS::Rooted<JSObject*> valueObj(aCx, &aValue.toObject());
|
|
Promise* nextPromise;
|
|
nsresult rv = UNWRAP_OBJECT(Promise, valueObj, nextPromise);
|
|
|
|
if (NS_SUCCEEDED(rv)) {
|
|
nsRefPtr<Promise> addRefed = nextPromise;
|
|
return addRefed.forget();
|
|
}
|
|
}
|
|
|
|
nsCOMPtr<nsIGlobalObject> global =
|
|
do_QueryInterface(aGlobal.GetAsSupports());
|
|
if (!global) {
|
|
aRv.Throw(NS_ERROR_UNEXPECTED);
|
|
return nullptr;
|
|
}
|
|
|
|
return Resolve(global, aCx, aValue, aRv);
|
|
}
|
|
|
|
/* static */ already_AddRefed<Promise>
|
|
Promise::Resolve(nsIGlobalObject* aGlobal, JSContext* aCx,
|
|
JS::Handle<JS::Value> aValue, ErrorResult& aRv)
|
|
{
|
|
nsRefPtr<Promise> promise = new Promise(aGlobal);
|
|
|
|
promise->MaybeResolveInternal(aCx, aValue);
|
|
return promise.forget();
|
|
}
|
|
|
|
/* static */ already_AddRefed<Promise>
|
|
Promise::Reject(const GlobalObject& aGlobal, JSContext* aCx,
|
|
JS::Handle<JS::Value> aValue, ErrorResult& aRv)
|
|
{
|
|
nsCOMPtr<nsIGlobalObject> global =
|
|
do_QueryInterface(aGlobal.GetAsSupports());
|
|
if (!global) {
|
|
aRv.Throw(NS_ERROR_UNEXPECTED);
|
|
return nullptr;
|
|
}
|
|
|
|
return Reject(global, aCx, aValue, aRv);
|
|
}
|
|
|
|
/* static */ already_AddRefed<Promise>
|
|
Promise::Reject(nsIGlobalObject* aGlobal, JSContext* aCx,
|
|
JS::Handle<JS::Value> aValue, ErrorResult& aRv)
|
|
{
|
|
nsRefPtr<Promise> promise = new Promise(aGlobal);
|
|
|
|
promise->MaybeRejectInternal(aCx, aValue);
|
|
return promise.forget();
|
|
}
|
|
|
|
already_AddRefed<Promise>
|
|
Promise::Then(AnyCallback* aResolveCallback, AnyCallback* aRejectCallback)
|
|
{
|
|
nsRefPtr<Promise> promise = new Promise(GetParentObject());
|
|
|
|
nsRefPtr<PromiseCallback> resolveCb =
|
|
PromiseCallback::Factory(promise, aResolveCallback, PromiseCallback::Resolve);
|
|
|
|
nsRefPtr<PromiseCallback> rejectCb =
|
|
PromiseCallback::Factory(promise, aRejectCallback, PromiseCallback::Reject);
|
|
|
|
AppendCallbacks(resolveCb, rejectCb);
|
|
|
|
return promise.forget();
|
|
}
|
|
|
|
already_AddRefed<Promise>
|
|
Promise::Catch(AnyCallback* aRejectCallback)
|
|
{
|
|
nsRefPtr<AnyCallback> resolveCb;
|
|
return Then(resolveCb, aRejectCallback);
|
|
}
|
|
|
|
/**
|
|
* The CountdownHolder class encapsulates Promise.all countdown functions and
|
|
* the countdown holder parts of the Promises spec. It maintains the result
|
|
* array and AllResolveHandlers use SetValue() to set the array indices.
|
|
*/
|
|
class CountdownHolder MOZ_FINAL : public nsISupports
|
|
{
|
|
public:
|
|
NS_DECL_CYCLE_COLLECTING_ISUPPORTS
|
|
NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(CountdownHolder)
|
|
|
|
CountdownHolder(const GlobalObject& aGlobal, Promise* aPromise, uint32_t aCountdown)
|
|
: mPromise(aPromise), mCountdown(aCountdown)
|
|
{
|
|
MOZ_ASSERT(aCountdown != 0);
|
|
JSContext* cx = aGlobal.GetContext();
|
|
JSAutoCompartment ac(cx, aGlobal.Get());
|
|
mValues = JS_NewArrayObject(cx, aCountdown);
|
|
mozilla::HoldJSObjects(this);
|
|
}
|
|
|
|
~CountdownHolder()
|
|
{
|
|
mozilla::DropJSObjects(this);
|
|
}
|
|
|
|
void SetValue(uint32_t index, const JS::Handle<JS::Value> aValue)
|
|
{
|
|
MOZ_ASSERT(mCountdown > 0);
|
|
|
|
JSContext* cx = nsContentUtils::GetDefaultJSContextForThread();
|
|
JSAutoCompartment ac(cx, mValues);
|
|
|
|
{
|
|
AutoDontReportUncaught silenceReporting(cx);
|
|
if (!JS_DefineElement(cx, mValues, index, aValue, nullptr, nullptr, JSPROP_ENUMERATE)) {
|
|
MOZ_ASSERT(JS_IsExceptionPending(cx));
|
|
JS::Rooted<JS::Value> exn(cx);
|
|
JS_GetPendingException(cx, &exn);
|
|
|
|
mPromise->MaybeReject(cx, exn);
|
|
}
|
|
}
|
|
|
|
--mCountdown;
|
|
if (mCountdown == 0) {
|
|
JS::Rooted<JS::Value> result(cx, JS::ObjectValue(*mValues));
|
|
mPromise->MaybeResolve(cx, result);
|
|
}
|
|
}
|
|
|
|
private:
|
|
nsRefPtr<Promise> mPromise;
|
|
uint32_t mCountdown;
|
|
JS::Heap<JSObject*> mValues;
|
|
};
|
|
|
|
NS_IMPL_CYCLE_COLLECTING_ADDREF(CountdownHolder)
|
|
NS_IMPL_CYCLE_COLLECTING_RELEASE(CountdownHolder)
|
|
NS_IMPL_CYCLE_COLLECTION_CLASS(CountdownHolder)
|
|
|
|
NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(CountdownHolder)
|
|
NS_INTERFACE_MAP_ENTRY(nsISupports)
|
|
NS_INTERFACE_MAP_END
|
|
|
|
NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN(CountdownHolder)
|
|
NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mValues)
|
|
NS_IMPL_CYCLE_COLLECTION_TRACE_END
|
|
|
|
NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(CountdownHolder)
|
|
NS_IMPL_CYCLE_COLLECTION_TRAVERSE_SCRIPT_OBJECTS
|
|
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mPromise)
|
|
NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
|
|
|
|
NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(CountdownHolder)
|
|
NS_IMPL_CYCLE_COLLECTION_UNLINK(mPromise)
|
|
tmp->mValues = nullptr;
|
|
NS_IMPL_CYCLE_COLLECTION_UNLINK_END
|
|
|
|
/**
|
|
* An AllResolveHandler is the per-promise part of the Promise.all() algorithm.
|
|
* Every Promise in the handler is handed an instance of this as a resolution
|
|
* handler and it sets the relevant index in the CountdownHolder.
|
|
*/
|
|
class AllResolveHandler MOZ_FINAL : public PromiseNativeHandler
|
|
{
|
|
public:
|
|
NS_DECL_CYCLE_COLLECTING_ISUPPORTS
|
|
NS_DECL_CYCLE_COLLECTION_CLASS(AllResolveHandler)
|
|
|
|
AllResolveHandler(CountdownHolder* aHolder, uint32_t aIndex)
|
|
: mCountdownHolder(aHolder), mIndex(aIndex)
|
|
{
|
|
MOZ_ASSERT(aHolder);
|
|
}
|
|
|
|
~AllResolveHandler()
|
|
{
|
|
}
|
|
|
|
void
|
|
ResolvedCallback(JS::Handle<JS::Value> aValue)
|
|
{
|
|
mCountdownHolder->SetValue(mIndex, aValue);
|
|
}
|
|
|
|
void
|
|
RejectedCallback(JS::Handle<JS::Value> aValue)
|
|
{
|
|
// Should never be attached to Promise as a reject handler.
|
|
MOZ_ASSERT(false, "AllResolveHandler should never be attached to a Promise's reject handler!");
|
|
}
|
|
|
|
private:
|
|
nsRefPtr<CountdownHolder> mCountdownHolder;
|
|
uint32_t mIndex;
|
|
};
|
|
|
|
NS_IMPL_CYCLE_COLLECTING_ADDREF(AllResolveHandler)
|
|
NS_IMPL_CYCLE_COLLECTING_RELEASE(AllResolveHandler)
|
|
|
|
NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(AllResolveHandler)
|
|
NS_INTERFACE_MAP_END_INHERITING(PromiseNativeHandler)
|
|
|
|
NS_IMPL_CYCLE_COLLECTION_1(AllResolveHandler, mCountdownHolder)
|
|
|
|
/* static */ already_AddRefed<Promise>
|
|
Promise::All(const GlobalObject& aGlobal, JSContext* aCx,
|
|
const Sequence<JS::Value>& aIterable, ErrorResult& aRv)
|
|
{
|
|
nsCOMPtr<nsIGlobalObject> global =
|
|
do_QueryInterface(aGlobal.GetAsSupports());
|
|
if (!global) {
|
|
aRv.Throw(NS_ERROR_UNEXPECTED);
|
|
return nullptr;
|
|
}
|
|
|
|
if (aIterable.Length() == 0) {
|
|
JS::Rooted<JSObject*> empty(aCx, JS_NewArrayObject(aCx, 0));
|
|
if (!empty) {
|
|
aRv.Throw(NS_ERROR_OUT_OF_MEMORY);
|
|
return nullptr;
|
|
}
|
|
JS::Rooted<JS::Value> value(aCx, JS::ObjectValue(*empty));
|
|
return Promise::Resolve(aGlobal, aCx, value, aRv);
|
|
}
|
|
|
|
nsRefPtr<Promise> promise = new Promise(global);
|
|
nsRefPtr<CountdownHolder> holder =
|
|
new CountdownHolder(aGlobal, promise, aIterable.Length());
|
|
|
|
nsRefPtr<PromiseCallback> rejectCb = new RejectPromiseCallback(promise);
|
|
|
|
for (uint32_t i = 0; i < aIterable.Length(); ++i) {
|
|
JS::Rooted<JS::Value> value(aCx, aIterable.ElementAt(i));
|
|
nsRefPtr<Promise> nextPromise = Promise::Resolve(aGlobal, aCx, value, aRv);
|
|
|
|
MOZ_ASSERT(!aRv.Failed());
|
|
|
|
nsRefPtr<PromiseNativeHandler> resolveHandler =
|
|
new AllResolveHandler(holder, i);
|
|
|
|
nsRefPtr<PromiseCallback> resolveCb =
|
|
new NativePromiseCallback(resolveHandler, Resolved);
|
|
// Every promise gets its own resolve callback, which will set the right
|
|
// index in the array to the resolution value.
|
|
nextPromise->AppendCallbacks(resolveCb, rejectCb);
|
|
}
|
|
|
|
return promise.forget();
|
|
}
|
|
|
|
/* static */ already_AddRefed<Promise>
|
|
Promise::Race(const GlobalObject& aGlobal, JSContext* aCx,
|
|
const Sequence<JS::Value>& aIterable, ErrorResult& aRv)
|
|
{
|
|
nsCOMPtr<nsIGlobalObject> global =
|
|
do_QueryInterface(aGlobal.GetAsSupports());
|
|
if (!global) {
|
|
aRv.Throw(NS_ERROR_UNEXPECTED);
|
|
return nullptr;
|
|
}
|
|
|
|
nsRefPtr<Promise> promise = new Promise(global);
|
|
nsRefPtr<PromiseCallback> resolveCb = new ResolvePromiseCallback(promise);
|
|
nsRefPtr<PromiseCallback> rejectCb = new RejectPromiseCallback(promise);
|
|
|
|
for (uint32_t i = 0; i < aIterable.Length(); ++i) {
|
|
JS::Rooted<JS::Value> value(aCx, aIterable.ElementAt(i));
|
|
nsRefPtr<Promise> nextPromise = Promise::Resolve(aGlobal, aCx, value, aRv);
|
|
// According to spec, Resolve can throw, but our implementation never does.
|
|
// Well it does when window isn't passed on the main thread, but that is an
|
|
// implementation detail which should never be reached since we are checking
|
|
// for window above. Remove this when subclassing is supported.
|
|
MOZ_ASSERT(!aRv.Failed());
|
|
nextPromise->AppendCallbacks(resolveCb, rejectCb);
|
|
}
|
|
|
|
return promise.forget();
|
|
}
|
|
|
|
void
|
|
Promise::AppendNativeHandler(PromiseNativeHandler* aRunnable)
|
|
{
|
|
nsRefPtr<PromiseCallback> resolveCb =
|
|
new NativePromiseCallback(aRunnable, Resolved);
|
|
|
|
nsRefPtr<PromiseCallback> rejectCb =
|
|
new NativePromiseCallback(aRunnable, Rejected);
|
|
|
|
AppendCallbacks(resolveCb, rejectCb);
|
|
}
|
|
|
|
void
|
|
Promise::AppendCallbacks(PromiseCallback* aResolveCallback,
|
|
PromiseCallback* aRejectCallback)
|
|
{
|
|
if (aResolveCallback) {
|
|
mResolveCallbacks.AppendElement(aResolveCallback);
|
|
}
|
|
|
|
if (aRejectCallback) {
|
|
mHadRejectCallback = true;
|
|
mRejectCallbacks.AppendElement(aRejectCallback);
|
|
}
|
|
|
|
// If promise's state is resolved, queue a task to process our resolve
|
|
// callbacks with promise's result. If promise's state is rejected, queue a
|
|
// task to process our reject callbacks with promise's result.
|
|
if (mState != Pending && !mTaskPending) {
|
|
if (MOZ_LIKELY(NS_IsMainThread())) {
|
|
nsRefPtr<PromiseTask> task = new PromiseTask(this);
|
|
NS_DispatchToCurrentThread(task);
|
|
} else {
|
|
WorkerPrivate* worker = GetCurrentThreadWorkerPrivate();
|
|
MOZ_ASSERT(worker);
|
|
nsRefPtr<WorkerPromiseTask> task = new WorkerPromiseTask(worker, this);
|
|
task->Dispatch(worker->GetJSContext());
|
|
}
|
|
mTaskPending = true;
|
|
}
|
|
}
|
|
|
|
void
|
|
Promise::RunTask()
|
|
{
|
|
MOZ_ASSERT(mState != Pending);
|
|
|
|
nsTArray<nsRefPtr<PromiseCallback>> callbacks;
|
|
callbacks.SwapElements(mState == Resolved ? mResolveCallbacks
|
|
: mRejectCallbacks);
|
|
mResolveCallbacks.Clear();
|
|
mRejectCallbacks.Clear();
|
|
|
|
JSContext* cx = nsContentUtils::GetDefaultJSContextForThread();
|
|
JSAutoRequest ar(cx);
|
|
JS::Rooted<JS::Value> value(cx, mResult);
|
|
|
|
for (uint32_t i = 0; i < callbacks.Length(); ++i) {
|
|
callbacks[i]->Call(value);
|
|
}
|
|
}
|
|
|
|
void
|
|
Promise::MaybeReportRejected()
|
|
{
|
|
if (mState != Rejected || mHadRejectCallback || mResult.isUndefined()) {
|
|
return;
|
|
}
|
|
|
|
// Technically we should push this JSContext, but in reality the JS engine
|
|
// just uses it for string allocation here, so we can get away without it.
|
|
if (!mResult.isObject()) {
|
|
return;
|
|
}
|
|
JSContext* cx = nsContentUtils::GetDefaultJSContextForThread();
|
|
JSAutoRequest ar(cx);
|
|
JS::Rooted<JSObject*> obj(cx, &mResult.toObject());
|
|
JSAutoCompartment ac(cx, obj);
|
|
JSErrorReport* report = JS_ErrorFromException(cx, obj);
|
|
if (!report) {
|
|
return;
|
|
}
|
|
|
|
// Remains null in case of worker.
|
|
nsCOMPtr<nsPIDOMWindow> win;
|
|
bool isChromeError = false;
|
|
|
|
if (MOZ_LIKELY(NS_IsMainThread())) {
|
|
win =
|
|
do_QueryInterface(nsJSUtils::GetStaticScriptGlobal(obj));
|
|
nsIPrincipal* principal = nsContentUtils::GetObjectPrincipal(obj);
|
|
isChromeError = nsContentUtils::IsSystemPrincipal(principal);
|
|
} else {
|
|
WorkerPrivate* worker = GetCurrentThreadWorkerPrivate();
|
|
MOZ_ASSERT(worker);
|
|
isChromeError = worker->IsChromeWorker();
|
|
}
|
|
|
|
// Now post an event to do the real reporting async
|
|
// Since Promises preserve their wrapper, it is essential to nsRefPtr<> the
|
|
// AsyncErrorReporter, otherwise if the call to DispatchToMainThread fails, it
|
|
// will leak. See Bug 958684.
|
|
nsRefPtr<AsyncErrorReporter> r =
|
|
new AsyncErrorReporter(JS_GetObjectRuntime(obj),
|
|
report,
|
|
nullptr,
|
|
isChromeError,
|
|
win);
|
|
NS_DispatchToMainThread(r);
|
|
}
|
|
|
|
void
|
|
Promise::MaybeResolveInternal(JSContext* aCx,
|
|
JS::Handle<JS::Value> aValue,
|
|
PromiseTaskSync aAsynchronous)
|
|
{
|
|
if (mResolvePending) {
|
|
return;
|
|
}
|
|
|
|
ResolveInternal(aCx, aValue, aAsynchronous);
|
|
}
|
|
|
|
void
|
|
Promise::MaybeRejectInternal(JSContext* aCx,
|
|
JS::Handle<JS::Value> aValue,
|
|
PromiseTaskSync aAsynchronous)
|
|
{
|
|
if (mResolvePending) {
|
|
return;
|
|
}
|
|
|
|
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,
|
|
PromiseTaskSync aAsynchronous)
|
|
{
|
|
mResolvePending = true;
|
|
|
|
if (aValue.isObject()) {
|
|
AutoDontReportUncaught silenceReporting(aCx);
|
|
JS::Rooted<JSObject*> valueObj(aCx, &aValue.toObject());
|
|
|
|
// 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;
|
|
}
|
|
}
|
|
|
|
// If the synchronous flag is set, process our resolve callbacks with
|
|
// value. Otherwise, the synchronous flag is unset, queue a task to process
|
|
// own resolve callbacks with value. Otherwise, the synchronous flag is
|
|
// unset, queue a task to process our resolve callbacks with value.
|
|
RunResolveTask(aValue, Resolved, aAsynchronous);
|
|
}
|
|
|
|
void
|
|
Promise::RejectInternal(JSContext* aCx,
|
|
JS::Handle<JS::Value> aValue,
|
|
PromiseTaskSync aAsynchronous)
|
|
{
|
|
mResolvePending = true;
|
|
|
|
// If the synchronous flag is set, process our reject callbacks with
|
|
// value. Otherwise, the synchronous flag is unset, queue a task to process
|
|
// promise's reject callbacks with value.
|
|
RunResolveTask(aValue, Rejected, aAsynchronous);
|
|
}
|
|
|
|
void
|
|
Promise::RunResolveTask(JS::Handle<JS::Value> aValue,
|
|
PromiseState aState,
|
|
PromiseTaskSync aAsynchronous)
|
|
{
|
|
// If the synchronous flag is unset, queue a task to process our
|
|
// accept callbacks with value.
|
|
if (aAsynchronous == AsyncTask) {
|
|
if (MOZ_LIKELY(NS_IsMainThread())) {
|
|
nsRefPtr<PromiseResolverTask> task =
|
|
new PromiseResolverTask(this, aValue, aState);
|
|
NS_DispatchToCurrentThread(task);
|
|
} else {
|
|
WorkerPrivate* worker = GetCurrentThreadWorkerPrivate();
|
|
MOZ_ASSERT(worker);
|
|
nsRefPtr<WorkerPromiseResolverTask> task =
|
|
new WorkerPromiseResolverTask(worker, this, aValue, aState);
|
|
task->Dispatch(worker->GetJSContext());
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Promise.all() or Promise.race() implementations will repeatedly call
|
|
// Resolve/RejectInternal rather than using the Maybe... forms. Stop SetState
|
|
// from asserting.
|
|
if (mState != Pending) {
|
|
return;
|
|
}
|
|
|
|
SetResult(aValue);
|
|
SetState(aState);
|
|
RunTask();
|
|
}
|
|
|
|
bool
|
|
Promise::ArgumentToJSValue(const nsAString& aArgument,
|
|
JSContext* aCx,
|
|
JSObject* aScope,
|
|
JS::MutableHandle<JS::Value> aValue)
|
|
{
|
|
// XXXkhuey I'd love to use xpc::NonVoidStringToJsval here, but it requires
|
|
// a non-const nsAString for silly reasons.
|
|
nsStringBuffer* sharedBuffer;
|
|
if (!XPCStringConvert::ReadableToJSVal(aCx, aArgument, &sharedBuffer,
|
|
aValue)) {
|
|
return false;
|
|
}
|
|
|
|
if (sharedBuffer) {
|
|
NS_ADDREF(sharedBuffer);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
} // namespace dom
|
|
} // namespace mozilla
|