diff options
Diffstat (limited to 'dom/promise')
31 files changed, 7982 insertions, 0 deletions
diff --git a/dom/promise/Promise.cpp b/dom/promise/Promise.cpp new file mode 100644 index 000000000..bf1fa5f50 --- /dev/null +++ b/dom/promise/Promise.cpp @@ -0,0 +1,3264 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=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 "js/Debug.h" + +#include "mozilla/Atomics.h" +#include "mozilla/CycleCollectedJSContext.h" +#include "mozilla/OwningNonNull.h" +#include "mozilla/Preferences.h" + +#include "mozilla/dom/BindingUtils.h" +#include "mozilla/dom/DOMError.h" +#include "mozilla/dom/DOMException.h" +#include "mozilla/dom/DOMExceptionBinding.h" +#include "mozilla/dom/MediaStreamError.h" +#include "mozilla/dom/PromiseBinding.h" +#include "mozilla/dom/ScriptSettings.h" + +#include "jsfriendapi.h" +#include "js/StructuredClone.h" +#include "nsContentUtils.h" +#include "nsGlobalWindow.h" +#include "nsIScriptObjectPrincipal.h" +#include "nsJSEnvironment.h" +#include "nsJSPrincipals.h" +#include "nsJSUtils.h" +#include "nsPIDOMWindow.h" +#include "PromiseCallback.h" +#include "PromiseDebugging.h" +#include "PromiseNativeHandler.h" +#include "PromiseWorkerProxy.h" +#include "WorkerPrivate.h" +#include "WorkerRunnable.h" +#include "WrapperFactory.h" +#include "xpcpublic.h" +#ifdef MOZ_CRASHREPORTER +#include "nsExceptionHandler.h" +#endif + +namespace mozilla { +namespace dom { + +namespace { +// Generator used by Promise::GetID. +Atomic<uintptr_t> gIDGenerator(0); +} // namespace + +using namespace workers; + +#ifndef SPIDERMONKEY_PROMISE +// This class processes the promise's callbacks with promise's result. +class PromiseReactionJob final : public Runnable +{ +public: + PromiseReactionJob(Promise* aPromise, + PromiseCallback* aCallback, + const JS::Value& aValue) + : mPromise(aPromise) + , mCallback(aCallback) + , mValue(CycleCollectedJSContext::Get()->Context(), aValue) + { + MOZ_ASSERT(aPromise); + MOZ_ASSERT(aCallback); + MOZ_COUNT_CTOR(PromiseReactionJob); + } + + virtual + ~PromiseReactionJob() + { + NS_ASSERT_OWNINGTHREAD(PromiseReactionJob); + MOZ_COUNT_DTOR(PromiseReactionJob); + } + +protected: + NS_IMETHOD + Run() override + { + NS_ASSERT_OWNINGTHREAD(PromiseReactionJob); + + MOZ_ASSERT(mPromise->GetWrapper()); // It was preserved! + + AutoJSAPI jsapi; + if (!jsapi.Init(mPromise->GetWrapper())) { + return NS_ERROR_FAILURE; + } + JSContext* cx = jsapi.cx(); + + JS::Rooted<JS::Value> value(cx, mValue); + if (!MaybeWrapValue(cx, &value)) { + NS_WARNING("Failed to wrap value into the right compartment."); + JS_ClearPendingException(cx); + return NS_OK; + } + + JS::Rooted<JSObject*> asyncStack(cx, mPromise->mAllocationStack); + + { + Maybe<JS::AutoSetAsyncStackForNewCalls> sas; + if (asyncStack) { + sas.emplace(cx, asyncStack, "Promise"); + } + mCallback->Call(cx, value); + } + + return NS_OK; + } + +private: + RefPtr<Promise> mPromise; + RefPtr<PromiseCallback> mCallback; + JS::PersistentRooted<JS::Value> mValue; + NS_DECL_OWNINGTHREAD; +}; + +/* + * 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, Promise::SLOT_DATA, + JS::ObjectValue(*aRejectFunc)); + js::SetFunctionNativeReserved(aRejectFunc, Promise::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, Promise::SLOT_DATA); + + if (!otherFuncVal.isObject()) { + return false; + } + + JSObject* otherFuncObj = &otherFuncVal.toObject(); + MOZ_ASSERT(js::GetFunctionNativeReserved(otherFuncObj, + Promise::SLOT_DATA).isObject()); + + // Break both references. + js::SetFunctionNativeReserved(aFunc, Promise::SLOT_DATA, + JS::UndefinedValue()); + js::SetFunctionNativeReserved(otherFuncObj, Promise::SLOT_DATA, + JS::UndefinedValue()); + + return true; +} + +Promise* +GetPromise(JSContext* aCx, JS::Handle<JSObject*> aFunc) +{ + JS::Value promiseVal = js::GetFunctionNativeReserved(aFunc, + Promise::SLOT_PROMISE); + + MOZ_ASSERT(promiseVal.isObject()); + + Promise* promise; + UNWRAP_OBJECT(Promise, &promiseVal.toObject(), promise); + return promise; +} +} // namespace + +// Runnable to resolve thenables. +// Equivalent to the specification's ResolvePromiseViaThenableTask. +class PromiseResolveThenableJob final : public Runnable +{ +public: + PromiseResolveThenableJob(Promise* aPromise, + JS::Handle<JSObject*> aThenable, + PromiseInit* aThen) + : mPromise(aPromise) + , mThenable(CycleCollectedJSContext::Get()->Context(), aThenable) + , mThen(aThen) + { + MOZ_ASSERT(aPromise); + MOZ_COUNT_CTOR(PromiseResolveThenableJob); + } + + virtual + ~PromiseResolveThenableJob() + { + NS_ASSERT_OWNINGTHREAD(PromiseResolveThenableJob); + MOZ_COUNT_DTOR(PromiseResolveThenableJob); + } + +protected: + NS_IMETHOD + Run() override + { + NS_ASSERT_OWNINGTHREAD(PromiseResolveThenableJob); + + MOZ_ASSERT(mPromise->GetWrapper()); // It was preserved! + + AutoJSAPI jsapi; + // If we ever change which compartment we're working in here, make sure to + // fix the fast-path for resolved-with-a-Promise in ResolveInternal. + if (!jsapi.Init(mPromise->GetWrapper())) { + return NS_ERROR_FAILURE; + } + JSContext* cx = jsapi.cx(); + + JS::Rooted<JSObject*> resolveFunc(cx, + mPromise->CreateThenableFunction(cx, mPromise, PromiseCallback::Resolve)); + + if (!resolveFunc) { + mPromise->HandleException(cx); + return NS_OK; + } + + JS::Rooted<JSObject*> rejectFunc(cx, + mPromise->CreateThenableFunction(cx, mPromise, PromiseCallback::Reject)); + if (!rejectFunc) { + mPromise->HandleException(cx); + return NS_OK; + } + + LinkThenableCallables(cx, resolveFunc, rejectFunc); + + ErrorResult rv; + + JS::Rooted<JSObject*> rootedThenable(cx, mThenable); + + mThen->Call(rootedThenable, resolveFunc, rejectFunc, rv, + "promise thenable", CallbackObject::eRethrowExceptions, + mPromise->Compartment()); + + rv.WouldReportJSException(); + if (rv.Failed()) { + JS::Rooted<JS::Value> exn(cx); + { // Scope for JSAutoCompartment + + // Convert the ErrorResult to a JS exception object that we can reject + // ourselves with. This will be exactly the exception that would get + // thrown from a binding method whose ErrorResult ended up with + // whatever is on "rv" right now. + JSAutoCompartment ac(cx, mPromise->GlobalJSObject()); + DebugOnly<bool> conversionResult = ToJSValue(cx, rv, &exn); + MOZ_ASSERT(conversionResult); + } + + bool couldMarkAsCalled = MarkAsCalledIfNotCalledBefore(cx, 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) { + bool ok = JS_WrapValue(cx, &exn); + MOZ_ASSERT(ok); + if (!ok) { + NS_WARNING("Failed to wrap value into the right compartment."); + } + + mPromise->RejectInternal(cx, exn); + } + // 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 rv.StealNSResult(); + } + +private: + RefPtr<Promise> mPromise; + JS::PersistentRooted<JSObject*> mThenable; + RefPtr<PromiseInit> mThen; + NS_DECL_OWNINGTHREAD; +}; + +// A struct implementing +// <http://www.ecma-international.org/ecma-262/6.0/#sec-promisecapability-records>. +// While the spec holds on to these in some places, in practice those places +// don't actually need everything from this struct, so we explicitly grab +// members from it as needed in those situations. That allows us to make this a +// stack-only struct and keep the rooting simple. +// +// We also add an optimization for the (common) case when we discover that the +// Promise constructor we're supposed to use is in fact the canonical Promise +// constructor. In that case we will just set mNativePromise in our +// PromiseCapability and not set mPromise/mResolve/mReject; the correct +// callbacks will be the standard Promise ones, and we don't really want to +// synthesize JSFunctions for them in that situation. +struct MOZ_STACK_CLASS Promise::PromiseCapability +{ + explicit PromiseCapability(JSContext* aCx) + : mPromise(aCx) + , mResolve(aCx) + , mReject(aCx) + {} + + // Take an exception on aCx and try to convert it into a promise rejection. + // Note that this can result in a new exception being thrown on aCx, or an + // exception getting thrown on aRv. On entry to this method, aRv is assumed + // to not be a failure. This should only be called if NewPromiseCapability + // succeeded on this PromiseCapability. + void RejectWithException(JSContext* aCx, ErrorResult& aRv); + + // Return a JS::Value representing the promise. This should only be called if + // NewPromiseCapability succeeded on this PromiseCapability. It makes no + // guarantees about compartments (e.g. in the mNativePromise case it's in the + // compartment of the reflector, but in the mPromise case it might be in the + // compartment of some cross-compartment wrapper for a reflector). + JS::Value PromiseValue() const; + + // All the JS::Value fields of this struct are actually objects, but for our + // purposes it's simpler to store them as JS::Value. + + // [[Promise]]. + JS::Rooted<JSObject*> mPromise; + // [[Resolve]]. Value in the context compartment. + JS::Rooted<JS::Value> mResolve; + // [[Reject]]. Value in the context compartment. + JS::Rooted<JS::Value> mReject; + // If mNativePromise is non-null, we should use it, not mPromise. + RefPtr<Promise> mNativePromise; + +private: + // We don't want to allow creation of temporaries of this type, ever. + PromiseCapability(const PromiseCapability&) = delete; + PromiseCapability(PromiseCapability&&) = delete; +}; + +void +Promise::PromiseCapability::RejectWithException(JSContext* aCx, + ErrorResult& aRv) +{ + // This method basically implements + // http://www.ecma-international.org/ecma-262/6.0/#sec-ifabruptrejectpromise + // or at least the parts of it that happen if we have an abrupt completion. + + MOZ_ASSERT(!aRv.Failed()); + MOZ_ASSERT(mNativePromise || mPromise, + "NewPromiseCapability didn't succeed"); + + JS::Rooted<JS::Value> exn(aCx); + if (!JS_GetPendingException(aCx, &exn)) { + // This is an uncatchable exception, so can't be converted into a rejection. + // Just rethrow that on aRv. + aRv.ThrowUncatchableException(); + return; + } + + JS_ClearPendingException(aCx); + + // If we have a native promise, just reject it without trying to call out into + // JS. + if (mNativePromise) { + mNativePromise->MaybeRejectInternal(aCx, exn); + return; + } + + JS::Rooted<JS::Value> ignored(aCx); + if (!JS::Call(aCx, JS::UndefinedHandleValue, mReject, JS::HandleValueArray(exn), + &ignored)) { + aRv.NoteJSContextException(aCx); + } +} + +JS::Value +Promise::PromiseCapability::PromiseValue() const +{ + MOZ_ASSERT(mNativePromise || mPromise, + "NewPromiseCapability didn't succeed"); + + if (mNativePromise) { + return JS::ObjectValue(*mNativePromise->GetWrapper()); + } + + return JS::ObjectValue(*mPromise); +} + +#endif // SPIDERMONKEY_PROMISE + +// Promise + +NS_IMPL_CYCLE_COLLECTION_CLASS(Promise) + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(Promise) +#ifndef SPIDERMONKEY_PROMISE +#if defined(DOM_PROMISE_DEPRECATED_REPORTING) + tmp->MaybeReportRejectedOnce(); +#else + tmp->mResult = JS::UndefinedValue(); +#endif // defined(DOM_PROMISE_DEPRECATED_REPORTING) +#endif // SPIDERMONKEY_PROMISE + NS_IMPL_CYCLE_COLLECTION_UNLINK(mGlobal) +#ifndef SPIDERMONKEY_PROMISE + NS_IMPL_CYCLE_COLLECTION_UNLINK(mResolveCallbacks) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mRejectCallbacks) + NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER +#else // SPIDERMONKEY_PROMISE + tmp->mPromiseObj = nullptr; +#endif // SPIDERMONKEY_PROMISE +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(Promise) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mGlobal) +#ifndef SPIDERMONKEY_PROMISE + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mResolveCallbacks) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mRejectCallbacks) +#endif // SPIDERMONKEY_PROMISE + NS_IMPL_CYCLE_COLLECTION_TRAVERSE_SCRIPT_OBJECTS +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN(Promise) +#ifndef SPIDERMONKEY_PROMISE + NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mResult) + NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mAllocationStack) + NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mRejectionStack) + NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mFullfillmentStack) + NS_IMPL_CYCLE_COLLECTION_TRACE_PRESERVED_WRAPPER +#else // SPIDERMONKEY_PROMISE + NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mPromiseObj); +#endif // SPIDERMONKEY_PROMISE +NS_IMPL_CYCLE_COLLECTION_TRACE_END + +#ifndef SPIDERMONKEY_PROMISE +NS_IMPL_CYCLE_COLLECTION_CAN_SKIP_BEGIN(Promise) + if (tmp->IsBlack()) { + tmp->mResult.exposeToActiveJS(); + tmp->mAllocationStack.exposeToActiveJS(); + tmp->mRejectionStack.exposeToActiveJS(); + tmp->mFullfillmentStack.exposeToActiveJS(); + return true; + } +NS_IMPL_CYCLE_COLLECTION_CAN_SKIP_END + +NS_IMPL_CYCLE_COLLECTION_CAN_SKIP_IN_CC_BEGIN(Promise) + return tmp->IsBlackAndDoesNotNeedTracing(tmp); +NS_IMPL_CYCLE_COLLECTION_CAN_SKIP_IN_CC_END + +NS_IMPL_CYCLE_COLLECTION_CAN_SKIP_THIS_BEGIN(Promise) + return tmp->IsBlack(); +NS_IMPL_CYCLE_COLLECTION_CAN_SKIP_THIS_END +#endif // SPIDERMONKEY_PROMISE + +NS_IMPL_CYCLE_COLLECTING_ADDREF(Promise) +NS_IMPL_CYCLE_COLLECTING_RELEASE(Promise) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(Promise) +#ifndef SPIDERMONKEY_PROMISE + NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY +#endif // SPIDERMONKEY_PROMISE + NS_INTERFACE_MAP_ENTRY(nsISupports) + NS_INTERFACE_MAP_ENTRY(Promise) +NS_INTERFACE_MAP_END + +Promise::Promise(nsIGlobalObject* aGlobal) + : mGlobal(aGlobal) +#ifndef SPIDERMONKEY_PROMISE + , mResult(JS::UndefinedValue()) + , mAllocationStack(nullptr) + , mRejectionStack(nullptr) + , mFullfillmentStack(nullptr) + , mState(Pending) +#if defined(DOM_PROMISE_DEPRECATED_REPORTING) + , mHadRejectCallback(false) +#endif // defined(DOM_PROMISE_DEPRECATED_REPORTING) + , mTaskPending(false) + , mResolvePending(false) + , mIsLastInChain(true) + , mWasNotifiedAsUncaught(false) + , mID(0) +#else // SPIDERMONKEY_PROMISE + , mPromiseObj(nullptr) +#endif // SPIDERMONKEY_PROMISE +{ + MOZ_ASSERT(mGlobal); + + mozilla::HoldJSObjects(this); + +#ifndef SPIDERMONKEY_PROMISE + mCreationTimestamp = TimeStamp::Now(); +#endif // SPIDERMONKEY_PROMISE +} + +Promise::~Promise() +{ +#ifndef SPIDERMONKEY_PROMISE +#if defined(DOM_PROMISE_DEPRECATED_REPORTING) + MaybeReportRejectedOnce(); +#endif // defined(DOM_PROMISE_DEPRECATED_REPORTING) +#endif // SPIDERMONKEY_PROMISE + mozilla::DropJSObjects(this); +} + +#ifdef SPIDERMONKEY_PROMISE + +bool +Promise::WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto, + JS::MutableHandle<JSObject*> aWrapper) +{ +#ifdef DEBUG + binding_detail::AssertReflectorHasGivenProto(aCx, mPromiseObj, aGivenProto); +#endif // DEBUG + aWrapper.set(mPromiseObj); + return true; +} + +// static +already_AddRefed<Promise> +Promise::Create(nsIGlobalObject* aGlobal, ErrorResult& aRv) +{ + if (!aGlobal) { + aRv.Throw(NS_ERROR_UNEXPECTED); + return nullptr; + } + RefPtr<Promise> p = new Promise(aGlobal); + p->CreateWrapper(nullptr, aRv); + if (aRv.Failed()) { + return nullptr; + } + return p.forget(); +} + +// static +already_AddRefed<Promise> +Promise::Resolve(nsIGlobalObject* aGlobal, JSContext* aCx, + JS::Handle<JS::Value> aValue, ErrorResult& aRv) +{ + JSAutoCompartment ac(aCx, aGlobal->GetGlobalJSObject()); + JS::Rooted<JSObject*> p(aCx, + JS::CallOriginalPromiseResolve(aCx, aValue)); + if (!p) { + aRv.NoteJSContextException(aCx); + return nullptr; + } + + return CreateFromExisting(aGlobal, p); +} + +// static +already_AddRefed<Promise> +Promise::Reject(nsIGlobalObject* aGlobal, JSContext* aCx, + JS::Handle<JS::Value> aValue, ErrorResult& aRv) +{ + JSAutoCompartment ac(aCx, aGlobal->GetGlobalJSObject()); + JS::Rooted<JSObject*> p(aCx, + JS::CallOriginalPromiseReject(aCx, aValue)); + if (!p) { + aRv.NoteJSContextException(aCx); + return nullptr; + } + + return CreateFromExisting(aGlobal, p); +} + +// static +already_AddRefed<Promise> +Promise::All(const GlobalObject& aGlobal, + const nsTArray<RefPtr<Promise>>& aPromiseList, ErrorResult& aRv) +{ + nsCOMPtr<nsIGlobalObject> global; + global = do_QueryInterface(aGlobal.GetAsSupports()); + if (!global) { + aRv.Throw(NS_ERROR_UNEXPECTED); + return nullptr; + } + + JSContext* cx = aGlobal.Context(); + + JS::AutoObjectVector promises(cx); + if (!promises.reserve(aPromiseList.Length())) { + aRv.NoteJSContextException(cx); + return nullptr; + } + + for (auto& promise : aPromiseList) { + JS::Rooted<JSObject*> promiseObj(cx, promise->PromiseObj()); + // Just in case, make sure these are all in the context compartment. + if (!JS_WrapObject(cx, &promiseObj)) { + aRv.NoteJSContextException(cx); + return nullptr; + } + promises.infallibleAppend(promiseObj); + } + + JS::Rooted<JSObject*> result(cx, JS::GetWaitForAllPromise(cx, promises)); + if (!result) { + aRv.NoteJSContextException(cx); + return nullptr; + } + + return CreateFromExisting(global, result); +} + +void +Promise::Then(JSContext* aCx, + // aCalleeGlobal may not be in the compartment of aCx, when called over + // Xrays. + JS::Handle<JSObject*> aCalleeGlobal, + AnyCallback* aResolveCallback, AnyCallback* aRejectCallback, + JS::MutableHandle<JS::Value> aRetval, + ErrorResult& aRv) +{ + NS_ASSERT_OWNINGTHREAD(Promise); + + // Let's hope this does the right thing with Xrays... Ensure everything is + // just in the caller compartment; that ought to do the trick. In theory we + // should consider aCalleeGlobal, but in practice our only caller is + // DOMRequest::Then, which is not working with a Promise subclass, so things + // should be OK. + JS::Rooted<JSObject*> promise(aCx, PromiseObj()); + if (!JS_WrapObject(aCx, &promise)) { + aRv.NoteJSContextException(aCx); + return; + } + + JS::Rooted<JSObject*> resolveCallback(aCx); + if (aResolveCallback) { + resolveCallback = aResolveCallback->Callback(); + if (!JS_WrapObject(aCx, &resolveCallback)) { + aRv.NoteJSContextException(aCx); + return; + } + } + + JS::Rooted<JSObject*> rejectCallback(aCx); + if (aRejectCallback) { + rejectCallback = aRejectCallback->Callback(); + if (!JS_WrapObject(aCx, &rejectCallback)) { + aRv.NoteJSContextException(aCx); + return; + } + } + + JS::Rooted<JSObject*> retval(aCx); + retval = JS::CallOriginalPromiseThen(aCx, promise, resolveCallback, + rejectCallback); + if (!retval) { + aRv.NoteJSContextException(aCx); + return; + } + + aRetval.setObject(*retval); +} + +// We need a dummy function to pass to JS::NewPromiseObject. +static bool +DoNothingPromiseExecutor(JSContext*, unsigned aArgc, JS::Value* aVp) +{ + JS::CallArgs args = CallArgsFromVp(aArgc, aVp); + args.rval().setUndefined(); + return true; +} + +void +Promise::CreateWrapper(JS::Handle<JSObject*> aDesiredProto, ErrorResult& aRv) +{ + AutoJSAPI jsapi; + if (!jsapi.Init(mGlobal)) { + aRv.Throw(NS_ERROR_UNEXPECTED); + return; + } + JSContext* cx = jsapi.cx(); + + JSFunction* doNothingFunc = + JS_NewFunction(cx, DoNothingPromiseExecutor, /* nargs = */ 2, + /* flags = */ 0, nullptr); + if (!doNothingFunc) { + JS_ClearPendingException(cx); + aRv.Throw(NS_ERROR_OUT_OF_MEMORY); + return; + } + + JS::Rooted<JSObject*> doNothingObj(cx, JS_GetFunctionObject(doNothingFunc)); + mPromiseObj = JS::NewPromiseObject(cx, doNothingObj, aDesiredProto); + if (!mPromiseObj) { + JS_ClearPendingException(cx); + aRv.Throw(NS_ERROR_OUT_OF_MEMORY); + return; + } +} + +void +Promise::MaybeResolve(JSContext* aCx, + JS::Handle<JS::Value> aValue) +{ + NS_ASSERT_OWNINGTHREAD(Promise); + + JS::Rooted<JSObject*> p(aCx, PromiseObj()); + if (!JS::ResolvePromise(aCx, p, aValue)) { + // Now what? There's nothing sane to do here. + JS_ClearPendingException(aCx); + } +} + +void +Promise::MaybeReject(JSContext* aCx, + JS::Handle<JS::Value> aValue) +{ + NS_ASSERT_OWNINGTHREAD(Promise); + + JS::Rooted<JSObject*> p(aCx, PromiseObj()); + if (!JS::RejectPromise(aCx, p, aValue)) { + // Now what? There's nothing sane to do here. + JS_ClearPendingException(aCx); + } +} + +#define SLOT_NATIVEHANDLER 0 +#define SLOT_NATIVEHANDLER_TASK 1 + +enum class NativeHandlerTask : int32_t { + Resolve, + Reject +}; + +static bool +NativeHandlerCallback(JSContext* aCx, unsigned aArgc, JS::Value* aVp) +{ + JS::CallArgs args = CallArgsFromVp(aArgc, aVp); + + JS::Value v = js::GetFunctionNativeReserved(&args.callee(), + SLOT_NATIVEHANDLER); + MOZ_ASSERT(v.isObject()); + + JS::Rooted<JSObject*> obj(aCx, &v.toObject()); + PromiseNativeHandler* handler = nullptr; + if (NS_FAILED(UNWRAP_OBJECT(PromiseNativeHandler, &obj, handler))) { + return Throw(aCx, NS_ERROR_UNEXPECTED); + } + + v = js::GetFunctionNativeReserved(&args.callee(), SLOT_NATIVEHANDLER_TASK); + NativeHandlerTask task = static_cast<NativeHandlerTask>(v.toInt32()); + + if (task == NativeHandlerTask::Resolve) { + handler->ResolvedCallback(aCx, args.get(0)); + } else { + MOZ_ASSERT(task == NativeHandlerTask::Reject); + handler->RejectedCallback(aCx, args.get(0)); + } + + return true; +} + +static JSObject* +CreateNativeHandlerFunction(JSContext* aCx, JS::Handle<JSObject*> aHolder, + NativeHandlerTask aTask) +{ + JSFunction* func = js::NewFunctionWithReserved(aCx, NativeHandlerCallback, + /* nargs = */ 1, + /* flags = */ 0, nullptr); + if (!func) { + return nullptr; + } + + JS::Rooted<JSObject*> obj(aCx, JS_GetFunctionObject(func)); + + JS::ExposeObjectToActiveJS(aHolder); + js::SetFunctionNativeReserved(obj, SLOT_NATIVEHANDLER, + JS::ObjectValue(*aHolder)); + js::SetFunctionNativeReserved(obj, SLOT_NATIVEHANDLER_TASK, + JS::Int32Value(static_cast<int32_t>(aTask))); + + return obj; +} + +namespace { + +class PromiseNativeHandlerShim final : public PromiseNativeHandler +{ + RefPtr<PromiseNativeHandler> mInner; + + ~PromiseNativeHandlerShim() + { + } + +public: + explicit PromiseNativeHandlerShim(PromiseNativeHandler* aInner) + : mInner(aInner) + { + MOZ_ASSERT(mInner); + } + + void + ResolvedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue) override + { + mInner->ResolvedCallback(aCx, aValue); + mInner = nullptr; + } + + void + RejectedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue) override + { + mInner->RejectedCallback(aCx, aValue); + mInner = nullptr; + } + + bool + WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto, + JS::MutableHandle<JSObject*> aWrapper) + { + return PromiseNativeHandlerBinding::Wrap(aCx, this, aGivenProto, aWrapper); + } + + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_CLASS(PromiseNativeHandlerShim) +}; + +NS_IMPL_CYCLE_COLLECTION(PromiseNativeHandlerShim, mInner) + +NS_IMPL_CYCLE_COLLECTING_ADDREF(PromiseNativeHandlerShim) +NS_IMPL_CYCLE_COLLECTING_RELEASE(PromiseNativeHandlerShim) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(PromiseNativeHandlerShim) + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +} // anonymous namespace + +void +Promise::AppendNativeHandler(PromiseNativeHandler* aRunnable) +{ + NS_ASSERT_OWNINGTHREAD(Promise); + + AutoJSAPI jsapi; + if (NS_WARN_IF(!jsapi.Init(mGlobal))) { + // Our API doesn't allow us to return a useful error. Not like this should + // happen anyway. + return; + } + + // The self-hosted promise js may keep the object we pass to it alive + // for quite a while depending on when GC runs. Therefore, pass a shim + // object instead. The shim will free its inner PromiseNativeHandler + // after the promise has settled just like our previous c++ promises did. + RefPtr<PromiseNativeHandlerShim> shim = + new PromiseNativeHandlerShim(aRunnable); + + JSContext* cx = jsapi.cx(); + JS::Rooted<JSObject*> handlerWrapper(cx); + // Note: PromiseNativeHandler is NOT wrappercached. So we can't use + // ToJSValue here, because it will try to do XPConnect wrapping on it, sadly. + if (NS_WARN_IF(!shim->WrapObject(cx, nullptr, &handlerWrapper))) { + // Again, no way to report errors. + jsapi.ClearException(); + return; + } + + JS::Rooted<JSObject*> resolveFunc(cx); + resolveFunc = + CreateNativeHandlerFunction(cx, handlerWrapper, NativeHandlerTask::Resolve); + if (NS_WARN_IF(!resolveFunc)) { + jsapi.ClearException(); + return; + } + + JS::Rooted<JSObject*> rejectFunc(cx); + rejectFunc = + CreateNativeHandlerFunction(cx, handlerWrapper, NativeHandlerTask::Reject); + if (NS_WARN_IF(!rejectFunc)) { + jsapi.ClearException(); + return; + } + + JS::Rooted<JSObject*> promiseObj(cx, PromiseObj()); + if (NS_WARN_IF(!JS::AddPromiseReactions(cx, promiseObj, resolveFunc, + rejectFunc))) { + jsapi.ClearException(); + return; + } +} + +void +Promise::HandleException(JSContext* aCx) +{ + JS::Rooted<JS::Value> exn(aCx); + if (JS_GetPendingException(aCx, &exn)) { + JS_ClearPendingException(aCx); + // This is only called from MaybeSomething, so it's OK to MaybeReject here, + // unlike in the version that's used when !SPIDERMONKEY_PROMISE. + MaybeReject(aCx, exn); + } +} + +// static +already_AddRefed<Promise> +Promise::CreateFromExisting(nsIGlobalObject* aGlobal, + JS::Handle<JSObject*> aPromiseObj) +{ + MOZ_ASSERT(js::GetObjectCompartment(aGlobal->GetGlobalJSObject()) == + js::GetObjectCompartment(aPromiseObj)); + RefPtr<Promise> p = new Promise(aGlobal); + p->mPromiseObj = aPromiseObj; + return p.forget(); +} + +#else // SPIDERMONKEY_PROMISE + +JSObject* +Promise::WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) +{ + return PromiseBinding::Wrap(aCx, this, aGivenProto); +} + +already_AddRefed<Promise> +Promise::Create(nsIGlobalObject* aGlobal, ErrorResult& aRv, + JS::Handle<JSObject*> aDesiredProto) +{ + if (!aGlobal) { + aRv.Throw(NS_ERROR_UNEXPECTED); + return nullptr; + } + RefPtr<Promise> p = new Promise(aGlobal); + p->CreateWrapper(aDesiredProto, aRv); + if (aRv.Failed()) { + return nullptr; + } + return p.forget(); +} + +void +Promise::CreateWrapper(JS::Handle<JSObject*> aDesiredProto, ErrorResult& aRv) +{ + AutoJSAPI jsapi; + if (!jsapi.Init(mGlobal)) { + aRv.Throw(NS_ERROR_UNEXPECTED); + return; + } + JSContext* cx = jsapi.cx(); + + JS::Rooted<JS::Value> wrapper(cx); + if (!GetOrCreateDOMReflector(cx, this, &wrapper, aDesiredProto)) { + JS_ClearPendingException(cx); + aRv.Throw(NS_ERROR_OUT_OF_MEMORY); + return; + } + + dom::PreserveWrapper(this); + + // Now grab our allocation stack + if (!CaptureStack(cx, mAllocationStack)) { + JS_ClearPendingException(cx); + aRv.Throw(NS_ERROR_OUT_OF_MEMORY); + return; + } + + JS::RootedObject obj(cx, &wrapper.toObject()); + JS::dbg::onNewPromise(cx, obj); +} + +void +Promise::MaybeResolve(JSContext* aCx, + JS::Handle<JS::Value> aValue) +{ + NS_ASSERT_OWNINGTHREAD(Promise); + + MaybeResolveInternal(aCx, aValue); +} + +void +Promise::MaybeReject(JSContext* aCx, + JS::Handle<JS::Value> aValue) +{ + NS_ASSERT_OWNINGTHREAD(Promise); + + MaybeRejectInternal(aCx, aValue); +} + +#endif // SPIDERMONKEY_PROMISE + +void +Promise::MaybeResolveWithUndefined() +{ + NS_ASSERT_OWNINGTHREAD(Promise); + + MaybeResolve(JS::UndefinedHandleValue); +} + +void +Promise::MaybeReject(const RefPtr<MediaStreamError>& aArg) { + NS_ASSERT_OWNINGTHREAD(Promise); + + MaybeSomething(aArg, &Promise::MaybeReject); +} + +void +Promise::MaybeRejectWithUndefined() +{ + NS_ASSERT_OWNINGTHREAD(Promise); + + MaybeSomething(JS::UndefinedHandleValue, &Promise::MaybeReject); +} + +#ifdef SPIDERMONKEY_PROMISE +void +Promise::ReportRejectedPromise(JSContext* aCx, JS::HandleObject aPromise) +{ + MOZ_ASSERT(!js::IsWrapper(aPromise)); + + MOZ_ASSERT(JS::GetPromiseState(aPromise) == JS::PromiseState::Rejected); + + JS::Rooted<JS::Value> result(aCx, JS::GetPromiseResult(aPromise)); + + js::ErrorReport report(aCx); + if (!report.init(aCx, result, js::ErrorReport::NoSideEffects)) { + JS_ClearPendingException(aCx); + return; + } + + RefPtr<xpc::ErrorReport> xpcReport = new xpc::ErrorReport(); + bool isMainThread = MOZ_LIKELY(NS_IsMainThread()); + bool isChrome = isMainThread ? nsContentUtils::IsSystemPrincipal(nsContentUtils::ObjectPrincipal(aPromise)) + : GetCurrentThreadWorkerPrivate()->IsChromeWorker(); + nsGlobalWindow* win = isMainThread ? xpc::WindowGlobalOrNull(aPromise) : nullptr; + xpcReport->Init(report.report(), report.toStringResult().c_str(), isChrome, + win ? win->AsInner()->WindowID() : 0); + + // Now post an event to do the real reporting async + NS_DispatchToMainThread(new AsyncErrorReporter(xpcReport)); +} +#endif // defined(SPIDERMONKEY_PROMISE) + +bool +Promise::PerformMicroTaskCheckpoint() +{ + MOZ_ASSERT(NS_IsMainThread(), "Wrong thread!"); + + CycleCollectedJSContext* context = CycleCollectedJSContext::Get(); + + // On the main thread, we always use the main promise micro task queue. + std::queue<nsCOMPtr<nsIRunnable>>& microtaskQueue = + context->GetPromiseMicroTaskQueue(); + + if (microtaskQueue.empty()) { + return false; + } + + AutoSlowOperation aso; + + do { + nsCOMPtr<nsIRunnable> runnable = microtaskQueue.front().forget(); + MOZ_ASSERT(runnable); + + // This function can re-enter, so we remove the element before calling. + microtaskQueue.pop(); + nsresult rv = runnable->Run(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return false; + } + aso.CheckForInterrupt(); + context->AfterProcessMicrotask(); + } while (!microtaskQueue.empty()); + + return true; +} + +void +Promise::PerformWorkerMicroTaskCheckpoint() +{ + MOZ_ASSERT(!NS_IsMainThread(), "Wrong thread!"); + + CycleCollectedJSContext* context = CycleCollectedJSContext::Get(); + if (!context) { + return; + } + + for (;;) { + // For a normal microtask checkpoint, we try to use the debugger microtask + // queue first. If the debugger queue is empty, we use the normal microtask + // queue instead. + std::queue<nsCOMPtr<nsIRunnable>>* microtaskQueue = + &context->GetDebuggerPromiseMicroTaskQueue(); + + if (microtaskQueue->empty()) { + microtaskQueue = &context->GetPromiseMicroTaskQueue(); + if (microtaskQueue->empty()) { + break; + } + } + + nsCOMPtr<nsIRunnable> runnable = microtaskQueue->front().forget(); + MOZ_ASSERT(runnable); + + // This function can re-enter, so we remove the element before calling. + microtaskQueue->pop(); + nsresult rv = runnable->Run(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + context->AfterProcessMicrotask(); + } +} + +void +Promise::PerformWorkerDebuggerMicroTaskCheckpoint() +{ + MOZ_ASSERT(!NS_IsMainThread(), "Wrong thread!"); + + CycleCollectedJSContext* context = CycleCollectedJSContext::Get(); + if (!context) { + return; + } + + for (;;) { + // For a debugger microtask checkpoint, we always use the debugger microtask + // queue. + std::queue<nsCOMPtr<nsIRunnable>>* microtaskQueue = + &context->GetDebuggerPromiseMicroTaskQueue(); + + if (microtaskQueue->empty()) { + break; + } + + nsCOMPtr<nsIRunnable> runnable = microtaskQueue->front().forget(); + MOZ_ASSERT(runnable); + + // This function can re-enter, so we remove the element before calling. + microtaskQueue->pop(); + nsresult rv = runnable->Run(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + context->AfterProcessMicrotask(); + } +} + +#ifndef SPIDERMONKEY_PROMISE + +/* 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) { + if (!promise->CaptureStack(aCx, promise->mFullfillmentStack)) { + return false; + } + promise->MaybeResolveInternal(aCx, args.get(0)); + } else { + promise->MaybeRejectInternal(aCx, args.get(0)); + if (!promise->CaptureStack(aCx, promise->mRejectionStack)) { + return false; + } + } + + args.rval().setUndefined(); + return true; +} + +/* + * 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. + args.rval().setUndefined(); + return true; + } + + Promise* promise = GetPromise(aCx, thisFunc); + MOZ_ASSERT(promise); + + if (aTask == PromiseCallback::Resolve) { + promise->ResolveInternal(aCx, args.get(0)); + } else { + promise->RejectInternal(aCx, args.get(0)); + } + + args.rval().setUndefined(); + 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, Promise* aPromise, int32_t aTask) +{ + // If this function ever changes, make sure to update + // WrapperPromiseCallback::GetDependentPromise. + JSFunction* func = js::NewFunctionWithReserved(aCx, JSCallback, + 1 /* nargs */, 0 /* flags */, + nullptr); + if (!func) { + return nullptr; + } + + JS::Rooted<JSObject*> obj(aCx, JS_GetFunctionObject(func)); + + JS::Rooted<JS::Value> promiseObj(aCx); + if (!dom::GetOrCreateDOMReflector(aCx, aPromise, &promiseObj)) { + return nullptr; + } + + JS::ExposeValueToActiveJS(promiseObj); + 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); + if (!func) { + return nullptr; + } + + JS::Rooted<JSObject*> obj(aCx, JS_GetFunctionObject(func)); + + JS::Rooted<JS::Value> promiseObj(aCx); + if (!dom::GetOrCreateDOMReflector(aCx, aPromise, &promiseObj)) { + return nullptr; + } + + JS::ExposeValueToActiveJS(promiseObj); + js::SetFunctionNativeReserved(obj, SLOT_PROMISE, promiseObj); + + return obj; +} + +/* static */ already_AddRefed<Promise> +Promise::Constructor(const GlobalObject& aGlobal, PromiseInit& aInit, + ErrorResult& aRv, JS::Handle<JSObject*> aDesiredProto) +{ + nsCOMPtr<nsIGlobalObject> global; + global = do_QueryInterface(aGlobal.GetAsSupports()); + if (!global) { + aRv.Throw(NS_ERROR_UNEXPECTED); + return nullptr; + } + + RefPtr<Promise> promise = Create(global, aRv, aDesiredProto); + if (aRv.Failed()) { + return nullptr; + } + + 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, this, + PromiseCallback::Resolve)); + if (!resolveFunc) { + aRv.Throw(NS_ERROR_UNEXPECTED); + return; + } + + JS::Rooted<JSObject*> rejectFunc(cx, + CreateFunction(cx, this, + PromiseCallback::Reject)); + if (!rejectFunc) { + aRv.Throw(NS_ERROR_UNEXPECTED); + return; + } + + aInit.Call(resolveFunc, rejectFunc, aRv, "promise initializer", + CallbackObject::eRethrowExceptions, Compartment()); + aRv.WouldReportJSException(); + + if (aRv.Failed()) { + if (aRv.IsUncatchableException()) { + // Just propagate this to the caller. + return; + } + + // There are two possibilities here. Either we've got a rethrown exception, + // or we reported that already and synthesized a generic NS_ERROR_FAILURE on + // the ErrorResult. In the former case, it doesn't much matter how we get + // the exception JS::Value from the ErrorResult to us, since we'll just end + // up wrapping it into the right compartment as needed if we hand it to + // someone. But in the latter case we have to ensure that the new exception + // object we create is created in our reflector compartment, not in our + // current compartment, because in the case when we're a Promise constructor + // called over Xrays creating it in the current compartment would mean + // rejecting with a value that can't be accessed by code that can call + // then() on this Promise. + // + // Luckily, MaybeReject(aRv) does exactly what we want here: it enters our + // reflector compartment before trying to produce a JS::Value from the + // ErrorResult. + MaybeReject(aRv); + } +} + +#define GET_CAPABILITIES_EXECUTOR_RESOLVE_SLOT 0 +#define GET_CAPABILITIES_EXECUTOR_REJECT_SLOT 1 + +namespace { +bool +GetCapabilitiesExecutor(JSContext* aCx, unsigned aArgc, JS::Value* aVp) +{ + // Implements + // http://www.ecma-international.org/ecma-262/6.0/#sec-getcapabilitiesexecutor-functions + // except we store the [[Resolve]] and [[Reject]] in our own internal slots, + // not in a PromiseCapability. The PromiseCapability will then read them from + // us. + JS::CallArgs args = CallArgsFromVp(aArgc, aVp); + + // Step 1 is an assert. + + // Step 2 doesn't need to be done, because it's just giving a name to the + // PromiseCapability record which is supposed to be stored in an internal + // slot. But we don't store that at all, per the comment above; we just + // directly store its [[Resolve]] and [[Reject]] members. + + // Steps 3 and 4. + if (!js::GetFunctionNativeReserved(&args.callee(), + GET_CAPABILITIES_EXECUTOR_RESOLVE_SLOT).isUndefined() || + !js::GetFunctionNativeReserved(&args.callee(), + GET_CAPABILITIES_EXECUTOR_REJECT_SLOT).isUndefined()) { + ErrorResult rv; + rv.ThrowTypeError<MSG_PROMISE_CAPABILITY_HAS_SOMETHING_ALREADY>(); + return !rv.MaybeSetPendingException(aCx); + } + + // Step 5. + js::SetFunctionNativeReserved(&args.callee(), + GET_CAPABILITIES_EXECUTOR_RESOLVE_SLOT, + args.get(0)); + + // Step 6. + js::SetFunctionNativeReserved(&args.callee(), + GET_CAPABILITIES_EXECUTOR_REJECT_SLOT, + args.get(1)); + + // Step 7. + args.rval().setUndefined(); + return true; +} +} // anonymous namespace + +/* static */ void +Promise::NewPromiseCapability(JSContext* aCx, nsIGlobalObject* aGlobal, + JS::Handle<JS::Value> aConstructor, + bool aForceCallbackCreation, + PromiseCapability& aCapability, + ErrorResult& aRv) +{ + // Implements + // http://www.ecma-international.org/ecma-262/6.0/#sec-newpromisecapability + + if (!aConstructor.isObject() || + !JS::IsConstructor(&aConstructor.toObject())) { + aRv.ThrowTypeError<MSG_ILLEGAL_PROMISE_CONSTRUCTOR>(); + return; + } + + // Step 2 is a note. + // Step 3 is already done because we got the PromiseCapability passed in. + + // Optimization: Check whether constructor is in fact the canonical + // Promise constructor for aGlobal. + JS::Rooted<JSObject*> global(aCx, aGlobal->GetGlobalJSObject()); + { + // Scope for the JSAutoCompartment, since we need to enter the compartment + // of global to get constructors from it. Save the compartment we used to + // be in, though; we'll need it later. + JS::Rooted<JSObject*> callerGlobal(aCx, JS::CurrentGlobalOrNull(aCx)); + JSAutoCompartment ac(aCx, global); + + // Now wrap aConstructor into the compartment of aGlobal, so comparing it to + // the canonical Promise for that compartment actually makes sense. + JS::Rooted<JS::Value> constructorValue(aCx, aConstructor); + if (!MaybeWrapObjectValue(aCx, &constructorValue)) { + aRv.NoteJSContextException(aCx); + return; + } + + JSObject* defaultCtor = PromiseBinding::GetConstructorObject(aCx); + if (!defaultCtor) { + aRv.NoteJSContextException(aCx); + return; + } + if (defaultCtor == &constructorValue.toObject()) { + // This is the canonical Promise constructor. + aCapability.mNativePromise = Promise::Create(aGlobal, aRv); + if (aForceCallbackCreation) { + // We have to be a bit careful here. We want to create these functions + // in the compartment in which they would be created if we actually + // invoked the constructor via JS::Construct below. That means our + // callerGlobal compartment if aConstructor is an Xray and the reflector + // compartment of the promise we're creating otherwise. But note that + // our callerGlobal compartment is precisely the reflector compartment + // unless the call was done over Xrays, because the reflector + // compartment comes from xpc::XrayAwareCalleeGlobal. So we really just + // want to create these functions in the callerGlobal compartment. + MOZ_ASSERT(xpc::WrapperFactory::IsXrayWrapper(&aConstructor.toObject()) || + callerGlobal == global); + JSAutoCompartment ac2(aCx, callerGlobal); + + JSObject* resolveFuncObj = + CreateFunction(aCx, aCapability.mNativePromise, + PromiseCallback::Resolve); + if (!resolveFuncObj) { + aRv.NoteJSContextException(aCx); + return; + } + aCapability.mResolve.setObject(*resolveFuncObj); + + JSObject* rejectFuncObj = + CreateFunction(aCx, aCapability.mNativePromise, + PromiseCallback::Reject); + if (!rejectFuncObj) { + aRv.NoteJSContextException(aCx); + return; + } + aCapability.mReject.setObject(*rejectFuncObj); + } + return; + } + } + + // Step 4. + // We can create our get-capabilities function in the calling compartment. It + // will work just as if we did |new promiseConstructor(function(a,b){}). + // Notably, if we're called over Xrays that's all fine, because we will end up + // creating the callbacks in the caller compartment in that case. + JSFunction* getCapabilitiesFunc = + js::NewFunctionWithReserved(aCx, GetCapabilitiesExecutor, + 2 /* nargs */, + 0 /* flags */, + nullptr); + if (!getCapabilitiesFunc) { + aRv.Throw(NS_ERROR_OUT_OF_MEMORY); + return; + } + + JS::Rooted<JSObject*> getCapabilitiesObj(aCx); + getCapabilitiesObj = JS_GetFunctionObject(getCapabilitiesFunc); + + // Step 5 doesn't need to be done, since we're not actually storing a + // PromiseCapability in the executor; see the comments in + // GetCapabilitiesExecutor above. + + // Step 6 and step 7. + JS::Rooted<JS::Value> getCapabilities(aCx, + JS::ObjectValue(*getCapabilitiesObj)); + JS::Rooted<JSObject*> promiseObj(aCx); + if (!JS::Construct(aCx, aConstructor, + JS::HandleValueArray(getCapabilities), + &promiseObj)) { + aRv.NoteJSContextException(aCx); + return; + } + + // Step 8 plus copying over the value to the PromiseCapability. + JS::Rooted<JS::Value> v(aCx); + v = js::GetFunctionNativeReserved(getCapabilitiesObj, + GET_CAPABILITIES_EXECUTOR_RESOLVE_SLOT); + if (!v.isObject() || !JS::IsCallable(&v.toObject())) { + aRv.ThrowTypeError<MSG_PROMISE_RESOLVE_FUNCTION_NOT_CALLABLE>(); + return; + } + aCapability.mResolve = v; + + // Step 9 plus copying over the value to the PromiseCapability. + v = js::GetFunctionNativeReserved(getCapabilitiesObj, + GET_CAPABILITIES_EXECUTOR_REJECT_SLOT); + if (!v.isObject() || !JS::IsCallable(&v.toObject())) { + aRv.ThrowTypeError<MSG_PROMISE_REJECT_FUNCTION_NOT_CALLABLE>(); + return; + } + aCapability.mReject = v; + + // Step 10. + aCapability.mPromise = promiseObj; + + // Step 11 doesn't need anything, since the PromiseCapability was passed in. +} + +/* static */ void +Promise::Resolve(const GlobalObject& aGlobal, JS::Handle<JS::Value> aThisv, + JS::Handle<JS::Value> aValue, + JS::MutableHandle<JS::Value> aRetval, ErrorResult& aRv) +{ + // Implementation of + // http://www.ecma-international.org/ecma-262/6.0/#sec-promise.resolve + + JSContext* cx = aGlobal.Context(); + + nsCOMPtr<nsIGlobalObject> global = + do_QueryInterface(aGlobal.GetAsSupports()); + if (!global) { + aRv.Throw(NS_ERROR_UNEXPECTED); + return; + } + + // Steps 1 and 2. + if (!aThisv.isObject()) { + aRv.ThrowTypeError<MSG_ILLEGAL_PROMISE_CONSTRUCTOR>(); + return; + } + + // Step 3. If a Promise was passed and matches our constructor, just return it. + if (aValue.isObject()) { + JS::Rooted<JSObject*> valueObj(cx, &aValue.toObject()); + Promise* nextPromise; + nsresult rv = UNWRAP_OBJECT(Promise, valueObj, nextPromise); + + if (NS_SUCCEEDED(rv)) { + JS::Rooted<JS::Value> constructor(cx); + if (!JS_GetProperty(cx, valueObj, "constructor", &constructor)) { + aRv.NoteJSContextException(cx); + return; + } + + // Cheat instead of calling JS_SameValue, since we know one's an object. + if (aThisv == constructor) { + aRetval.setObject(*valueObj); + return; + } + } + } + + // Step 4. + PromiseCapability capability(cx); + NewPromiseCapability(cx, global, aThisv, false, capability, aRv); + // Step 5. + if (aRv.Failed()) { + return; + } + + // Step 6. + Promise* p = capability.mNativePromise; + if (p) { + p->MaybeResolveInternal(cx, aValue); + p->mFullfillmentStack = p->mAllocationStack; + } else { + JS::Rooted<JS::Value> value(cx, aValue); + JS::Rooted<JS::Value> ignored(cx); + if (!JS::Call(cx, JS::UndefinedHandleValue /* thisVal */, + capability.mResolve, JS::HandleValueArray(value), + &ignored)) { + // Step 7. + aRv.NoteJSContextException(cx); + return; + } + } + + // Step 8. + aRetval.set(capability.PromiseValue()); +} + +/* static */ already_AddRefed<Promise> +Promise::Resolve(nsIGlobalObject* aGlobal, JSContext* aCx, + JS::Handle<JS::Value> aValue, ErrorResult& aRv) +{ + RefPtr<Promise> promise = Create(aGlobal, aRv); + if (aRv.Failed()) { + return nullptr; + } + + promise->MaybeResolveInternal(aCx, aValue); + return promise.forget(); +} + +/* static */ void +Promise::Reject(const GlobalObject& aGlobal, JS::Handle<JS::Value> aThisv, + JS::Handle<JS::Value> aValue, + JS::MutableHandle<JS::Value> aRetval, ErrorResult& aRv) +{ + // Implementation of + // http://www.ecma-international.org/ecma-262/6.0/#sec-promise.reject + + JSContext* cx = aGlobal.Context(); + + nsCOMPtr<nsIGlobalObject> global = + do_QueryInterface(aGlobal.GetAsSupports()); + if (!global) { + aRv.Throw(NS_ERROR_UNEXPECTED); + return; + } + + // Steps 1 and 2. + if (!aThisv.isObject()) { + aRv.ThrowTypeError<MSG_ILLEGAL_PROMISE_CONSTRUCTOR>(); + return; + } + + // Step 3. + PromiseCapability capability(cx); + NewPromiseCapability(cx, global, aThisv, false, capability, aRv); + // Step 4. + if (aRv.Failed()) { + return; + } + + // Step 5. + Promise* p = capability.mNativePromise; + if (p) { + p->MaybeRejectInternal(cx, aValue); + p->mRejectionStack = p->mAllocationStack; + } else { + JS::Rooted<JS::Value> value(cx, aValue); + JS::Rooted<JS::Value> ignored(cx); + if (!JS::Call(cx, JS::UndefinedHandleValue /* thisVal */, + capability.mReject, JS::HandleValueArray(value), + &ignored)) { + // Step 6. + aRv.NoteJSContextException(cx); + return; + } + } + + // Step 7. + aRetval.set(capability.PromiseValue()); +} + +/* static */ already_AddRefed<Promise> +Promise::Reject(nsIGlobalObject* aGlobal, JSContext* aCx, + JS::Handle<JS::Value> aValue, ErrorResult& aRv) +{ + RefPtr<Promise> promise = Create(aGlobal, aRv); + if (aRv.Failed()) { + return nullptr; + } + + promise->MaybeRejectInternal(aCx, aValue); + return promise.forget(); +} + +namespace { +void +SpeciesConstructor(JSContext* aCx, + JS::Handle<JSObject*> promise, + JS::Handle<JS::Value> defaultCtor, + JS::MutableHandle<JS::Value> ctor, + ErrorResult& aRv) +{ + // Implements + // http://www.ecma-international.org/ecma-262/6.0/#sec-speciesconstructor + + // Step 1. + MOZ_ASSERT(promise); + + // Step 2. + JS::Rooted<JS::Value> constructorVal(aCx); + if (!JS_GetProperty(aCx, promise, "constructor", &constructorVal)) { + // Step 3. + aRv.NoteJSContextException(aCx); + return; + } + + // Step 4. + if (constructorVal.isUndefined()) { + ctor.set(defaultCtor); + return; + } + + // Step 5. + if (!constructorVal.isObject()) { + aRv.ThrowTypeError<MSG_ILLEGAL_PROMISE_CONSTRUCTOR>(); + return; + } + + // Step 6. + JS::Rooted<jsid> species(aCx, + SYMBOL_TO_JSID(JS::GetWellKnownSymbol(aCx, JS::SymbolCode::species))); + JS::Rooted<JS::Value> speciesVal(aCx); + JS::Rooted<JSObject*> constructorObj(aCx, &constructorVal.toObject()); + if (!JS_GetPropertyById(aCx, constructorObj, species, &speciesVal)) { + // Step 7. + aRv.NoteJSContextException(aCx); + return; + } + + // Step 8. + if (speciesVal.isNullOrUndefined()) { + ctor.set(defaultCtor); + return; + } + + // Step 9. + if (speciesVal.isObject() && JS::IsConstructor(&speciesVal.toObject())) { + ctor.set(speciesVal); + return; + } + + // Step 10. + aRv.ThrowTypeError<MSG_ILLEGAL_PROMISE_CONSTRUCTOR>(); +} +} // anonymous namespace + +void +Promise::Then(JSContext* aCx, JS::Handle<JSObject*> aCalleeGlobal, + AnyCallback* aResolveCallback, AnyCallback* aRejectCallback, + JS::MutableHandle<JS::Value> aRetval, ErrorResult& aRv) +{ + NS_ASSERT_OWNINGTHREAD(Promise); + + // Implements + // http://www.ecma-international.org/ecma-262/6.0/#sec-promise.prototype.then + + // Step 1. + JS::Rooted<JS::Value> promiseVal(aCx, JS::ObjectValue(*GetWrapper())); + if (!MaybeWrapObjectValue(aCx, &promiseVal)) { + aRv.NoteJSContextException(aCx); + return; + } + JS::Rooted<JSObject*> promiseObj(aCx, &promiseVal.toObject()); + MOZ_ASSERT(promiseObj); + + // Step 2 was done by the bindings. + + // Step 3. We want to use aCalleeGlobal here because it will do the + // right thing for us via Xrays (where we won't find @@species on + // our promise constructor for now). + JS::Rooted<JS::Value> defaultCtorVal(aCx); + { // Scope for JSAutoCompartment + JSAutoCompartment ac(aCx, aCalleeGlobal); + JSObject* defaultCtor = PromiseBinding::GetConstructorObject(aCx); + if (!defaultCtor) { + aRv.NoteJSContextException(aCx); + return; + } + defaultCtorVal.setObject(*defaultCtor); + } + if (!MaybeWrapObjectValue(aCx, &defaultCtorVal)) { + aRv.NoteJSContextException(aCx); + return; + } + + JS::Rooted<JS::Value> constructor(aCx); + SpeciesConstructor(aCx, promiseObj, defaultCtorVal, &constructor, aRv); + if (aRv.Failed()) { + // Step 4. + return; + } + + // Step 5. + GlobalObject globalObj(aCx, GetWrapper()); + if (globalObj.Failed()) { + aRv.NoteJSContextException(aCx); + return; + } + nsCOMPtr<nsIGlobalObject> globalObject = + do_QueryInterface(globalObj.GetAsSupports()); + if (!globalObject) { + aRv.Throw(NS_ERROR_UNEXPECTED); + return; + } + PromiseCapability capability(aCx); + NewPromiseCapability(aCx, globalObject, constructor, false, capability, aRv); + if (aRv.Failed()) { + // Step 6. + return; + } + + // Now step 7: start + // http://www.ecma-international.org/ecma-262/6.0/#sec-performpromisethen + + // Step 1 and step 2 are just assertions. + + // Step 3 and step 4 are kinda handled for us already; we use null + // to represent "Identity" and "Thrower". + + // Steps 5 and 6. These branch based on whether we know we have a + // vanilla Promise or not. + JS::Rooted<JSObject*> global(aCx, JS::CurrentGlobalOrNull(aCx)); + if (capability.mNativePromise) { + Promise* promise = capability.mNativePromise; + + RefPtr<PromiseCallback> resolveCb = + PromiseCallback::Factory(promise, global, aResolveCallback, + PromiseCallback::Resolve); + + RefPtr<PromiseCallback> rejectCb = + PromiseCallback::Factory(promise, global, aRejectCallback, + PromiseCallback::Reject); + + AppendCallbacks(resolveCb, rejectCb); + } else { + JS::Rooted<JSObject*> resolveObj(aCx, &capability.mResolve.toObject()); + RefPtr<AnyCallback> resolveFunc = + new AnyCallback(aCx, resolveObj, GetIncumbentGlobal()); + + JS::Rooted<JSObject*> rejectObj(aCx, &capability.mReject.toObject()); + RefPtr<AnyCallback> rejectFunc = + new AnyCallback(aCx, rejectObj, GetIncumbentGlobal()); + + if (!capability.mPromise) { + aRv.ThrowTypeError<MSG_ILLEGAL_PROMISE_CONSTRUCTOR>(); + return; + } + JS::Rooted<JSObject*> newPromiseObj(aCx, capability.mPromise); + // We want to store the reflector itself. + newPromiseObj = js::CheckedUnwrap(newPromiseObj); + if (!newPromiseObj) { + // Just throw something. + aRv.ThrowTypeError<MSG_ILLEGAL_PROMISE_CONSTRUCTOR>(); + return; + } + + RefPtr<PromiseCallback> resolveCb; + if (aResolveCallback) { + resolveCb = new WrapperPromiseCallback(global, aResolveCallback, + newPromiseObj, + resolveFunc, rejectFunc); + } else { + resolveCb = new InvokePromiseFuncCallback(global, newPromiseObj, + resolveFunc); + } + + RefPtr<PromiseCallback> rejectCb; + if (aRejectCallback) { + rejectCb = new WrapperPromiseCallback(global, aRejectCallback, + newPromiseObj, + resolveFunc, rejectFunc); + } else { + rejectCb = new InvokePromiseFuncCallback(global, newPromiseObj, + rejectFunc); + } + + AppendCallbacks(resolveCb, rejectCb); + } + + aRetval.set(capability.PromiseValue()); +} + +void +Promise::Catch(JSContext* aCx, AnyCallback* aRejectCallback, + JS::MutableHandle<JS::Value> aRetval, + ErrorResult& aRv) +{ + NS_ASSERT_OWNINGTHREAD(Promise); + + // Implements + // http://www.ecma-international.org/ecma-262/6.0/#sec-promise.prototype.catch + + // We can't call Promise::Then directly, because someone might have + // overridden Promise.prototype.then. + JS::Rooted<JS::Value> promiseVal(aCx, JS::ObjectValue(*GetWrapper())); + if (!MaybeWrapObjectValue(aCx, &promiseVal)) { + aRv.NoteJSContextException(aCx); + return; + } + JS::Rooted<JSObject*> promiseObj(aCx, &promiseVal.toObject()); + MOZ_ASSERT(promiseObj); + JS::AutoValueArray<2> callbacks(aCx); + callbacks[0].setUndefined(); + if (aRejectCallback) { + callbacks[1].setObject(*aRejectCallback->Callable()); + // It could be in any compartment, so put it in ours. + if (!MaybeWrapObjectValue(aCx, callbacks[1])) { + aRv.NoteJSContextException(aCx); + return; + } + } else { + callbacks[1].setNull(); + } + if (!JS_CallFunctionName(aCx, promiseObj, "then", callbacks, aRetval)) { + aRv.NoteJSContextException(aCx); + } +} + +/** + * The CountdownHolder class encapsulates Promise.all countdown functions and + * the countdown holder parts of the Promises spec. It maintains the result + * array and AllResolveElementFunctions use SetValue() to set the array indices. + */ +class CountdownHolder 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.Context(); + + // The only time aGlobal.Context() and aGlobal.Get() are not + // same-compartment is when we're called via Xrays, and in that situation we + // in fact want to create the array in the callee compartment + + JSAutoCompartment ac(cx, aGlobal.Get()); + mValues = JS_NewArrayObject(cx, aCountdown); + mozilla::HoldJSObjects(this); + } + +private: + ~CountdownHolder() + { + mozilla::DropJSObjects(this); + } + +public: + void SetValue(uint32_t index, const JS::Handle<JS::Value> aValue) + { + MOZ_ASSERT(mCountdown > 0); + + AutoJSAPI jsapi; + if (!jsapi.Init(mValues)) { + return; + } + JSContext* cx = jsapi.cx(); + + JS::Rooted<JS::Value> value(cx, aValue); + JS::Rooted<JSObject*> values(cx, mValues); + if (!JS_WrapValue(cx, &value) || + !JS_DefineElement(cx, values, index, value, JSPROP_ENUMERATE)) { + MOZ_ASSERT(JS_IsExceptionPending(cx)); + JS::Rooted<JS::Value> exn(cx); + if (!jsapi.StealException(&exn)) { + mPromise->MaybeReject(NS_ERROR_OUT_OF_MEMORY); + } else { + mPromise->MaybeReject(cx, exn); + } + } + + --mCountdown; + if (mCountdown == 0) { + JS::Rooted<JS::Value> result(cx, JS::ObjectValue(*mValues)); + mPromise->MaybeResolve(cx, result); + } + } + +private: + RefPtr<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 AllResolveElementFunction 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 AllResolveElementFunction final : public PromiseNativeHandler +{ +public: + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_CLASS(AllResolveElementFunction) + + AllResolveElementFunction(CountdownHolder* aHolder, uint32_t aIndex) + : mCountdownHolder(aHolder), mIndex(aIndex) + { + MOZ_ASSERT(aHolder); + } + + void + ResolvedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue) override + { + mCountdownHolder->SetValue(mIndex, aValue); + } + + void + RejectedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue) override + { + // Should never be attached to Promise as a reject handler. + MOZ_CRASH("AllResolveElementFunction should never be attached to a Promise's reject handler!"); + } + +private: + ~AllResolveElementFunction() + { + } + + RefPtr<CountdownHolder> mCountdownHolder; + uint32_t mIndex; +}; + +NS_IMPL_CYCLE_COLLECTING_ADDREF(AllResolveElementFunction) +NS_IMPL_CYCLE_COLLECTING_RELEASE(AllResolveElementFunction) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(AllResolveElementFunction) + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +NS_IMPL_CYCLE_COLLECTION(AllResolveElementFunction, mCountdownHolder) + +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) +{ + // 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); + + // 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; + } + + // 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(cx); + 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> +Promise::All(const GlobalObject& aGlobal, + const nsTArray<RefPtr<Promise>>& aPromiseList, ErrorResult& aRv) +{ + nsCOMPtr<nsIGlobalObject> global = + do_QueryInterface(aGlobal.GetAsSupports()); + if (!global) { + aRv.Throw(NS_ERROR_UNEXPECTED); + return nullptr; + } + + JSContext* cx = aGlobal.Context(); + + if (aPromiseList.IsEmpty()) { + JS::Rooted<JSObject*> empty(cx, JS_NewArrayObject(cx, 0)); + if (!empty) { + aRv.Throw(NS_ERROR_OUT_OF_MEMORY); + return nullptr; + } + JS::Rooted<JS::Value> value(cx, JS::ObjectValue(*empty)); + // We know "value" is not a promise, so call the Resolve function + // that doesn't have to check for that. + return Promise::Resolve(global, cx, value, aRv); + } + + RefPtr<Promise> promise = Create(global, aRv); + if (aRv.Failed()) { + return nullptr; + } + RefPtr<CountdownHolder> holder = + new CountdownHolder(aGlobal, promise, aPromiseList.Length()); + + JS::Rooted<JSObject*> obj(cx, JS::CurrentGlobalOrNull(cx)); + if (!obj) { + aRv.Throw(NS_ERROR_UNEXPECTED); + return nullptr; + } + + RefPtr<PromiseCallback> rejectCb = new RejectPromiseCallback(promise, obj); + + for (uint32_t i = 0; i < aPromiseList.Length(); ++i) { + RefPtr<PromiseNativeHandler> resolveHandler = + new AllResolveElementFunction(holder, i); + + RefPtr<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. + aPromiseList[i]->AppendCallbacks(resolveCb, rejectCb); + } + + return promise.forget(); +} + +/* static */ void +Promise::Race(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.race + 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 + PromiseCapability capability(cx); + + // Step 6. + 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(cx); + return; + } + + // The arguments we're going to be passing to "then" on each loop iteration. + JS::AutoValueArray<2> callbackFunctions(cx); + callbackFunctions[0].set(capability.mResolve); + 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.race"); + 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-performpromiserace + // and do its steps. + 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) { + // We're all set! + return; + } + + // Step h. 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 i. + capability.RejectWithException(cx, aRv); + return; + } + + // Step j. And now we don't know whether nextPromise has an overridden + // "then" method, so no shortcuts here either. + 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 k. + capability.RejectWithException(cx, aRv); + } + } +} + +/* static */ +bool +Promise::PromiseSpecies(JSContext* aCx, unsigned aArgc, JS::Value* aVp) +{ + JS::CallArgs args = CallArgsFromVp(aArgc, aVp); + args.rval().set(args.thisv()); + return true; +} + +void +Promise::AppendNativeHandler(PromiseNativeHandler* aRunnable) +{ + NS_ASSERT_OWNINGTHREAD(Promise); + + RefPtr<PromiseCallback> resolveCb = + new NativePromiseCallback(aRunnable, Resolved); + + RefPtr<PromiseCallback> rejectCb = + new NativePromiseCallback(aRunnable, Rejected); + + AppendCallbacks(resolveCb, rejectCb); +} + +#endif // SPIDERMONKEY_PROMISE + +JSObject* +Promise::GlobalJSObject() const +{ + return mGlobal->GetGlobalJSObject(); +} + +JSCompartment* +Promise::Compartment() const +{ + return js::GetObjectCompartment(GlobalJSObject()); +} + +#ifndef SPIDERMONKEY_PROMISE +void +Promise::AppendCallbacks(PromiseCallback* aResolveCallback, + PromiseCallback* aRejectCallback) +{ + if (!mGlobal || mGlobal->IsDying()) { + return; + } + + MOZ_ASSERT(aResolveCallback); + MOZ_ASSERT(aRejectCallback); + + if (mIsLastInChain && mState == PromiseState::Rejected) { + // This rejection is now consumed. + PromiseDebugging::AddConsumedRejection(*this); + // Note that we may not have had the opportunity to call + // RunResolveTask() yet, so we may never have called + // `PromiseDebugging:AddUncaughtRejection`. + } + mIsLastInChain = false; + +#if defined(DOM_PROMISE_DEPRECATED_REPORTING) + // Now that there is a callback, we don't need to report anymore. + mHadRejectCallback = true; + RemoveWorkerHolder(); +#endif // defined(DOM_PROMISE_DEPRECATED_REPORTING) + + mResolveCallbacks.AppendElement(aResolveCallback); + mRejectCallbacks.AppendElement(aRejectCallback); + + // If promise's state is fulfilled, queue a task to process our fulfill + // 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) { + TriggerPromiseReactions(); + } +} +#endif // SPIDERMONKEY_PROMISE + +#ifndef SPIDERMONKEY_PROMISE +#if defined(DOM_PROMISE_DEPRECATED_REPORTING) +void +Promise::MaybeReportRejected() +{ + NS_ASSERT_OWNINGTHREAD(Promise); + + if (mState != Rejected || mHadRejectCallback || mResult.isUndefined()) { + return; + } + + AutoJSAPI jsapi; + // We may not have a usable global by now (if it got unlinked + // already), so don't init with it. + jsapi.Init(); + JSContext* cx = jsapi.cx(); + JS::Rooted<JSObject*> obj(cx, GetWrapper()); + MOZ_ASSERT(obj); // We preserve our wrapper, so should always have one here. + JS::Rooted<JS::Value> val(cx, mResult); + + JSAutoCompartment ac(cx, obj); + if (!JS_WrapValue(cx, &val)) { + JS_ClearPendingException(cx); + return; + } + + js::ErrorReport report(cx); + RefPtr<Exception> exp; + bool isObject = val.isObject(); + if (!isObject || NS_FAILED(UNWRAP_OBJECT(Exception, &val.toObject(), exp))) { + if (!isObject || + NS_FAILED(UNWRAP_OBJECT(DOMException, &val.toObject(), exp))) { + if (!report.init(cx, val, js::ErrorReport::NoSideEffects)) { + NS_WARNING("Couldn't convert the unhandled rejected value to an exception."); + JS_ClearPendingException(cx); + return; + } + } + } + + RefPtr<xpc::ErrorReport> xpcReport = new xpc::ErrorReport(); + bool isMainThread = MOZ_LIKELY(NS_IsMainThread()); + bool isChrome = isMainThread ? nsContentUtils::IsSystemPrincipal(nsContentUtils::ObjectPrincipal(obj)) + : GetCurrentThreadWorkerPrivate()->IsChromeWorker(); + nsGlobalWindow* win = isMainThread ? xpc::WindowGlobalOrNull(obj) : nullptr; + uint64_t windowID = win ? win->AsInner()->WindowID() : 0; + if (exp) { + xpcReport->Init(cx, exp, isChrome, windowID); + } else { + xpcReport->Init(report.report(), report.toStringResult(), + isChrome, windowID); + } + + // Now post an event to do the real reporting async + // Since Promises preserve their wrapper, it is essential to RefPtr<> the + // AsyncErrorReporter, otherwise if the call to DispatchToMainThread fails, it + // will leak. See Bug 958684. So... don't use DispatchToMainThread() + nsCOMPtr<nsIThread> mainThread = do_GetMainThread(); + if (NS_WARN_IF(!mainThread)) { + // Would prefer NS_ASSERTION, but that causes failure in xpcshell tests + NS_WARNING("!!! Trying to report rejected Promise after MainThread shutdown"); + } + if (mainThread) { + RefPtr<AsyncErrorReporter> r = new AsyncErrorReporter(xpcReport); + mainThread->Dispatch(r.forget(), NS_DISPATCH_NORMAL); + } +} +#endif // defined(DOM_PROMISE_DEPRECATED_REPORTING) + +void +Promise::MaybeResolveInternal(JSContext* aCx, + JS::Handle<JS::Value> aValue) +{ + NS_ASSERT_OWNINGTHREAD(Promise); + + if (mResolvePending) { + return; + } + + ResolveInternal(aCx, aValue); +} + +void +Promise::MaybeRejectInternal(JSContext* aCx, + JS::Handle<JS::Value> aValue) +{ + NS_ASSERT_OWNINGTHREAD(Promise); + + if (mResolvePending) { + return; + } + + RejectInternal(aCx, aValue); +} + +void +Promise::HandleException(JSContext* aCx) +{ + NS_ASSERT_OWNINGTHREAD(Promise); + + JS::Rooted<JS::Value> exn(aCx); + if (JS_GetPendingException(aCx, &exn)) { + JS_ClearPendingException(aCx); + RejectInternal(aCx, exn); + } +} + +void +Promise::ResolveInternal(JSContext* aCx, + JS::Handle<JS::Value> aValue) +{ + NS_ASSERT_OWNINGTHREAD(Promise); + + CycleCollectedJSContext* context = CycleCollectedJSContext::Get(); + + mResolvePending = true; + + if (aValue.isObject()) { + 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::IsCallable(&then.toObject())) { + // This is the then() function of the thenable aValueObj. + JS::Rooted<JSObject*> thenObj(aCx, &then.toObject()); + + // We used to have a fast path here for the case when the following + // requirements held: + // + // 1) valueObj is a Promise. + // 2) thenObj is a JSFunction backed by our actual Promise::Then + // implementation. + // + // But now that we're doing subclassing in Promise.prototype.then we would + // also need the following requirements: + // + // 3) Getting valueObj.constructor has no side-effects. + // 4) Getting valueObj.constructor[@@species] has no side-effects. + // 5) valueObj.constructor[@@species] is a function and calling it has no + // side-effects (e.g. it's the canonical Promise constructor) and it + // provides some callback functions to call as arguments to its + // argument. + // + // Ensuring that stuff while not inside SpiderMonkey is painful, so let's + // drop the fast path for now. + + RefPtr<PromiseInit> thenCallback = + new PromiseInit(nullptr, thenObj, mozilla::dom::GetIncumbentGlobal()); + RefPtr<PromiseResolveThenableJob> task = + new PromiseResolveThenableJob(this, valueObj, thenCallback); + context->DispatchToMicroTask(task.forget()); + return; + } + } + + MaybeSettle(aValue, Resolved); +} + +void +Promise::RejectInternal(JSContext* aCx, + JS::Handle<JS::Value> aValue) +{ + NS_ASSERT_OWNINGTHREAD(Promise); + + mResolvePending = true; + + MaybeSettle(aValue, Rejected); +} + +void +Promise::Settle(JS::Handle<JS::Value> aValue, PromiseState aState) +{ + MOZ_ASSERT(mGlobal, + "We really should have a global here. Except we sometimes don't " + "in the wild for some odd reason"); + NS_ASSERT_OWNINGTHREAD(Promise); + + if (!mGlobal || mGlobal->IsDying()) { + return; + } + + mSettlementTimestamp = TimeStamp::Now(); + + AutoJSAPI jsapi; + jsapi.Init(); + JSContext* cx = jsapi.cx(); + JS::RootedObject wrapper(cx, GetWrapper()); + MOZ_ASSERT(wrapper); // We preserved it + JSAutoCompartment ac(cx, wrapper); + + JS::Rooted<JS::Value> value(cx, aValue); + + if (!JS_WrapValue(cx, &value)) { + JS_ClearPendingException(cx); + value = JS::UndefinedValue(); + } + SetResult(value); + SetState(aState); + + JS::dbg::onPromiseSettled(cx, wrapper); + + if (aState == PromiseState::Rejected && + mIsLastInChain) { + // The Promise has just been rejected, and it is last in chain. + // We need to inform PromiseDebugging. + // If the Promise is eventually not the last in chain anymore, + // we will need to inform PromiseDebugging again. + PromiseDebugging::AddUncaughtRejection(*this); + } + +#if defined(DOM_PROMISE_DEPRECATED_REPORTING) + // If the Promise was rejected, and there is no reject handler already setup, + // watch for thread shutdown. + if (aState == PromiseState::Rejected && + !mHadRejectCallback && + !NS_IsMainThread()) { + WorkerPrivate* worker = GetCurrentThreadWorkerPrivate(); + MOZ_ASSERT(worker); + worker->AssertIsOnWorkerThread(); + + mWorkerHolder = new PromiseReportRejectWorkerHolder(this); + if (NS_WARN_IF(!mWorkerHolder->HoldWorker(worker, Closing))) { + mWorkerHolder = nullptr; + // Worker is shutting down, report rejection immediately since it is + // unlikely that reject callbacks will be added after this point. + MaybeReportRejectedOnce(); + } + } +#endif // defined(DOM_PROMISE_DEPRECATED_REPORTING) + + TriggerPromiseReactions(); +} + +void +Promise::MaybeSettle(JS::Handle<JS::Value> aValue, + PromiseState aState) +{ + NS_ASSERT_OWNINGTHREAD(Promise); + + // 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; + } + + Settle(aValue, aState); +} + +void +Promise::TriggerPromiseReactions() +{ + NS_ASSERT_OWNINGTHREAD(Promise); + + CycleCollectedJSContext* runtime = CycleCollectedJSContext::Get(); + + nsTArray<RefPtr<PromiseCallback>> callbacks; + callbacks.SwapElements(mState == Resolved ? mResolveCallbacks + : mRejectCallbacks); + mResolveCallbacks.Clear(); + mRejectCallbacks.Clear(); + + for (uint32_t i = 0; i < callbacks.Length(); ++i) { + RefPtr<PromiseReactionJob> task = + new PromiseReactionJob(this, callbacks[i], mResult); + runtime->DispatchToMicroTask(task.forget()); + } +} + +#if defined(DOM_PROMISE_DEPRECATED_REPORTING) +void +Promise::RemoveWorkerHolder() +{ + NS_ASSERT_OWNINGTHREAD(Promise); + + // The DTOR of this WorkerHolder will release the worker for us. + mWorkerHolder = nullptr; +} + +bool +PromiseReportRejectWorkerHolder::Notify(Status aStatus) +{ + MOZ_ASSERT(aStatus > Running); + mPromise->MaybeReportRejectedOnce(); + // After this point, `this` has been deleted by RemoveWorkerHolder! + return true; +} +#endif // defined(DOM_PROMISE_DEPRECATED_REPORTING) + +bool +Promise::CaptureStack(JSContext* aCx, JS::Heap<JSObject*>& aTarget) +{ + NS_ASSERT_OWNINGTHREAD(Promise); + + JS::Rooted<JSObject*> stack(aCx); + if (!JS::CaptureCurrentStack(aCx, &stack)) { + return false; + } + aTarget = stack; + return true; +} + +void +Promise::GetDependentPromises(nsTArray<RefPtr<Promise>>& aPromises) +{ + NS_ASSERT_OWNINGTHREAD(Promise); + + // We want to return promises that correspond to then() calls, Promise.all() + // calls, and Promise.race() calls. + // + // For the then() case, we have both resolve and reject callbacks that know + // what the next promise is. + // + // For the race() case, likewise. + // + // For the all() case, our reject callback knows what the next promise is, but + // our resolve callback just knows it needs to notify some + // PromiseNativeHandler, which itself only has an indirect relationship to the + // next promise. + // + // So we walk over our _reject_ callbacks and ask each of them what promise + // its dependent promise is. + for (size_t i = 0; i < mRejectCallbacks.Length(); ++i) { + Promise* p = mRejectCallbacks[i]->GetDependentPromise(); + if (p) { + aPromises.AppendElement(p); + } + } +} + +#endif // SPIDERMONKEY_PROMISE + +// A WorkerRunnable to resolve/reject the Promise on the worker thread. +// Calling thread MUST hold PromiseWorkerProxy's mutex before creating this. +class PromiseWorkerProxyRunnable : public WorkerRunnable +{ +public: + PromiseWorkerProxyRunnable(PromiseWorkerProxy* aPromiseWorkerProxy, + PromiseWorkerProxy::RunCallbackFunc aFunc) + : WorkerRunnable(aPromiseWorkerProxy->GetWorkerPrivate(), + WorkerThreadUnchangedBusyCount) + , mPromiseWorkerProxy(aPromiseWorkerProxy) + , mFunc(aFunc) + { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(mPromiseWorkerProxy); + } + + virtual bool + WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) + { + MOZ_ASSERT(aWorkerPrivate); + aWorkerPrivate->AssertIsOnWorkerThread(); + MOZ_ASSERT(aWorkerPrivate == mWorkerPrivate); + + MOZ_ASSERT(mPromiseWorkerProxy); + RefPtr<Promise> workerPromise = mPromiseWorkerProxy->WorkerPromise(); + + // Here we convert the buffer to a JS::Value. + JS::Rooted<JS::Value> value(aCx); + if (!mPromiseWorkerProxy->Read(aCx, &value)) { + JS_ClearPendingException(aCx); + return false; + } + + (workerPromise->*mFunc)(aCx, value); + + // Release the Promise because it has been resolved/rejected for sure. + mPromiseWorkerProxy->CleanUp(); + return true; + } + +protected: + ~PromiseWorkerProxyRunnable() {} + +private: + RefPtr<PromiseWorkerProxy> mPromiseWorkerProxy; + + // Function pointer for calling Promise::{ResolveInternal,RejectInternal}. + PromiseWorkerProxy::RunCallbackFunc mFunc; +}; + +class PromiseWorkerHolder final : public WorkerHolder +{ + // RawPointer because this proxy keeps alive the holder. + PromiseWorkerProxy* mProxy; + +public: + explicit PromiseWorkerHolder(PromiseWorkerProxy* aProxy) + : mProxy(aProxy) + { + MOZ_ASSERT(aProxy); + } + + bool + Notify(Status aStatus) override + { + if (aStatus >= Canceling) { + mProxy->CleanUp(); + } + + return true; + } +}; + +/* static */ +already_AddRefed<PromiseWorkerProxy> +PromiseWorkerProxy::Create(WorkerPrivate* aWorkerPrivate, + Promise* aWorkerPromise, + const PromiseWorkerProxyStructuredCloneCallbacks* aCb) +{ + MOZ_ASSERT(aWorkerPrivate); + aWorkerPrivate->AssertIsOnWorkerThread(); + MOZ_ASSERT(aWorkerPromise); + MOZ_ASSERT_IF(aCb, !!aCb->Write && !!aCb->Read); + + RefPtr<PromiseWorkerProxy> proxy = + new PromiseWorkerProxy(aWorkerPrivate, aWorkerPromise, aCb); + + // We do this to make sure the worker thread won't shut down before the + // promise is resolved/rejected on the worker thread. + if (!proxy->AddRefObject()) { + // Probably the worker is terminating. We cannot complete the operation + // and we have to release all the resources. + proxy->CleanProperties(); + return nullptr; + } + + return proxy.forget(); +} + +NS_IMPL_ISUPPORTS0(PromiseWorkerProxy) + +PromiseWorkerProxy::PromiseWorkerProxy(WorkerPrivate* aWorkerPrivate, + Promise* aWorkerPromise, + const PromiseWorkerProxyStructuredCloneCallbacks* aCallbacks) + : mWorkerPrivate(aWorkerPrivate) + , mWorkerPromise(aWorkerPromise) + , mCleanedUp(false) + , mCallbacks(aCallbacks) + , mCleanUpLock("cleanUpLock") +{ +} + +PromiseWorkerProxy::~PromiseWorkerProxy() +{ + MOZ_ASSERT(mCleanedUp); + MOZ_ASSERT(!mWorkerHolder); + MOZ_ASSERT(!mWorkerPromise); + MOZ_ASSERT(!mWorkerPrivate); +} + +void +PromiseWorkerProxy::CleanProperties() +{ +#ifdef DEBUG + WorkerPrivate* worker = GetCurrentThreadWorkerPrivate(); + MOZ_ASSERT(worker); + worker->AssertIsOnWorkerThread(); +#endif + // Ok to do this unprotected from Create(). + // CleanUp() holds the lock before calling this. + mCleanedUp = true; + mWorkerPromise = nullptr; + mWorkerPrivate = nullptr; + + // Clear the StructuredCloneHolderBase class. + Clear(); +} + +bool +PromiseWorkerProxy::AddRefObject() +{ + MOZ_ASSERT(mWorkerPrivate); + mWorkerPrivate->AssertIsOnWorkerThread(); + + MOZ_ASSERT(!mWorkerHolder); + mWorkerHolder.reset(new PromiseWorkerHolder(this)); + if (NS_WARN_IF(!mWorkerHolder->HoldWorker(mWorkerPrivate, Canceling))) { + mWorkerHolder = nullptr; + return false; + } + + // Maintain a reference so that we have a valid object to clean up when + // removing the feature. + AddRef(); + return true; +} + +WorkerPrivate* +PromiseWorkerProxy::GetWorkerPrivate() const +{ +#ifdef DEBUG + if (NS_IsMainThread()) { + mCleanUpLock.AssertCurrentThreadOwns(); + } +#endif + // Safe to check this without a lock since we assert lock ownership on the + // main thread above. + MOZ_ASSERT(!mCleanedUp); + MOZ_ASSERT(mWorkerHolder); + + return mWorkerPrivate; +} + +Promise* +PromiseWorkerProxy::WorkerPromise() const +{ +#ifdef DEBUG + WorkerPrivate* worker = GetCurrentThreadWorkerPrivate(); + MOZ_ASSERT(worker); + worker->AssertIsOnWorkerThread(); +#endif + MOZ_ASSERT(mWorkerPromise); + return mWorkerPromise; +} + +void +PromiseWorkerProxy::RunCallback(JSContext* aCx, + JS::Handle<JS::Value> aValue, + RunCallbackFunc aFunc) +{ + MOZ_ASSERT(NS_IsMainThread()); + + MutexAutoLock lock(Lock()); + // If the worker thread's been cancelled we don't need to resolve the Promise. + if (CleanedUp()) { + return; + } + + // The |aValue| is written into the StructuredCloneHolderBase. + if (!Write(aCx, aValue)) { + JS_ClearPendingException(aCx); + MOZ_ASSERT(false, "cannot serialize the value with the StructuredCloneAlgorithm!"); + } + + RefPtr<PromiseWorkerProxyRunnable> runnable = + new PromiseWorkerProxyRunnable(this, aFunc); + + runnable->Dispatch(); +} + +void +PromiseWorkerProxy::ResolvedCallback(JSContext* aCx, + JS::Handle<JS::Value> aValue) +{ + RunCallback(aCx, aValue, &Promise::MaybeResolve); +} + +void +PromiseWorkerProxy::RejectedCallback(JSContext* aCx, + JS::Handle<JS::Value> aValue) +{ + RunCallback(aCx, aValue, &Promise::MaybeReject); +} + +void +PromiseWorkerProxy::CleanUp() +{ + // Can't release Mutex while it is still locked, so scope the lock. + { + MutexAutoLock lock(Lock()); + + // |mWorkerPrivate| is not safe to use anymore if we have already + // cleaned up and RemoveWorkerHolder(), so we need to check |mCleanedUp| + // first. + if (CleanedUp()) { + return; + } + + MOZ_ASSERT(mWorkerPrivate); + mWorkerPrivate->AssertIsOnWorkerThread(); + + // Release the Promise and remove the PromiseWorkerProxy from the holders of + // the worker thread since the Promise has been resolved/rejected or the + // worker thread has been cancelled. + MOZ_ASSERT(mWorkerHolder); + mWorkerHolder = nullptr; + + CleanProperties(); + } + Release(); +} + +JSObject* +PromiseWorkerProxy::CustomReadHandler(JSContext* aCx, + JSStructuredCloneReader* aReader, + uint32_t aTag, + uint32_t aIndex) +{ + if (NS_WARN_IF(!mCallbacks)) { + return nullptr; + } + + return mCallbacks->Read(aCx, aReader, this, aTag, aIndex); +} + +bool +PromiseWorkerProxy::CustomWriteHandler(JSContext* aCx, + JSStructuredCloneWriter* aWriter, + JS::Handle<JSObject*> aObj) +{ + if (NS_WARN_IF(!mCallbacks)) { + return false; + } + + return mCallbacks->Write(aCx, aWriter, this, aObj); +} + +// Specializations of MaybeRejectBrokenly we actually support. +template<> +void Promise::MaybeRejectBrokenly(const RefPtr<DOMError>& aArg) { + MaybeSomething(aArg, &Promise::MaybeReject); +} +template<> +void Promise::MaybeRejectBrokenly(const nsAString& aArg) { + MaybeSomething(aArg, &Promise::MaybeReject); +} + +#ifndef SPIDERMONKEY_PROMISE +uint64_t +Promise::GetID() { + if (mID != 0) { + return mID; + } + return mID = ++gIDGenerator; +} +#endif // SPIDERMONKEY_PROMISE + +#ifndef SPIDERMONKEY_PROMISE +Promise::PromiseState +Promise::State() const +{ + return mState; +} +#else // SPIDERMONKEY_PROMISE +Promise::PromiseState +Promise::State() const +{ + JS::Rooted<JSObject*> p(RootingCx(), PromiseObj()); + const JS::PromiseState state = JS::GetPromiseState(p); + + if (state == JS::PromiseState::Fulfilled) { + return PromiseState::Resolved; + } + + if (state == JS::PromiseState::Rejected) { + return PromiseState::Rejected; + } + + return PromiseState::Pending; +} +#endif // SPIDERMONKEY_PROMISE + +} // namespace dom +} // namespace mozilla diff --git a/dom/promise/Promise.h b/dom/promise/Promise.h new file mode 100644 index 000000000..f2ad3bd6c --- /dev/null +++ b/dom/promise/Promise.h @@ -0,0 +1,537 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=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_Promise_h +#define mozilla_dom_Promise_h + +#include "mozilla/Attributes.h" +#include "mozilla/ErrorResult.h" +#include "mozilla/TimeStamp.h" +#include "mozilla/TypeTraits.h" +#include "mozilla/dom/BindingDeclarations.h" +#include "nsCycleCollectionParticipant.h" +#include "mozilla/dom/PromiseBinding.h" +#include "mozilla/dom/ToJSValue.h" +#include "mozilla/WeakPtr.h" +#include "nsWrapperCache.h" +#include "nsAutoPtr.h" +#include "js/TypeDecls.h" +#include "jspubtd.h" + +// Bug 1083361 introduces a new mechanism for tracking uncaught +// rejections. This #define serves to track down the parts of code +// that need to be removed once clients have been put together +// to take advantage of the new mechanism. New code should not +// depend on code #ifdefed to this #define. +#define DOM_PROMISE_DEPRECATED_REPORTING !SPIDERMONKEY_PROMISE + +#if defined(DOM_PROMISE_DEPRECATED_REPORTING) +#include "mozilla/dom/workers/bindings/WorkerHolder.h" +#endif // defined(DOM_PROMISE_DEPRECATED_REPORTING) + +class nsIGlobalObject; + +namespace mozilla { +namespace dom { + +class AnyCallback; +class DOMError; +class MediaStreamError; +class PromiseCallback; +class PromiseInit; +class PromiseNativeHandler; +class PromiseDebugging; + +class Promise; + +#if defined(DOM_PROMISE_DEPRECATED_REPORTING) +class PromiseReportRejectWorkerHolder : public workers::WorkerHolder +{ + // PromiseReportRejectWorkerHolder is held by an nsAutoPtr on the Promise + // which means that this object will be destroyed before the Promise is + // destroyed. + Promise* MOZ_NON_OWNING_REF mPromise; + +public: + explicit PromiseReportRejectWorkerHolder(Promise* aPromise) + : mPromise(aPromise) + { + MOZ_ASSERT(mPromise); + } + + virtual bool + Notify(workers::Status aStatus) override; +}; +#endif // defined(DOM_PROMISE_DEPRECATED_REPORTING) + +#define NS_PROMISE_IID \ + { 0x1b8d6215, 0x3e67, 0x43ba, \ + { 0x8a, 0xf9, 0x31, 0x5e, 0x8f, 0xce, 0x75, 0x65 } } + +class Promise : public nsISupports, +#ifndef SPIDERMONKEY_PROMISE + // Only wrappercached when we're not using SpiderMonkey + // promises, because those don't have a useful object moved + // hook, which wrappercache needs. + public nsWrapperCache, +#endif // SPIDERMONKEY_PROMISE + public SupportsWeakPtr<Promise> +{ + friend class NativePromiseCallback; + friend class PromiseReactionJob; + friend class PromiseResolverTask; + friend class PromiseTask; +#if defined(DOM_PROMISE_DEPRECATED_REPORTING) + friend class PromiseReportRejectWorkerHolder; +#endif // defined(DOM_PROMISE_DEPRECATED_REPORTING) + friend class PromiseWorkerProxy; + friend class PromiseWorkerProxyRunnable; + friend class RejectPromiseCallback; + friend class ResolvePromiseCallback; + friend class PromiseResolveThenableJob; + friend class FastPromiseResolveThenableJob; + friend class WrapperPromiseCallback; + +public: + NS_DECLARE_STATIC_IID_ACCESSOR(NS_PROMISE_IID) + NS_DECL_CYCLE_COLLECTING_ISUPPORTS +#ifdef SPIDERMONKEY_PROMISE + // We're not skippable, since we're not owned from JS to start with. + NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(Promise) +#else // SPIDERMONKEY_PROMISE + NS_DECL_CYCLE_COLLECTION_SKIPPABLE_SCRIPT_HOLDER_CLASS(Promise) +#endif // SPIDERMONKEY_PROMISE + MOZ_DECLARE_WEAKREFERENCE_TYPENAME(Promise) + + // Promise creation tries to create a JS reflector for the Promise, so is + // fallible. Furthermore, we don't want to do JS-wrapping on a 0-refcount + // object, so we addref before doing that and return the addrefed pointer + // here. +#ifdef SPIDERMONKEY_PROMISE + static already_AddRefed<Promise> + Create(nsIGlobalObject* aGlobal, ErrorResult& aRv); + + // Reports a rejected Promise by sending an error report. + static void ReportRejectedPromise(JSContext* aCx, JS::HandleObject aPromise); +#else + static already_AddRefed<Promise> + Create(nsIGlobalObject* aGlobal, ErrorResult& aRv, + // Passing null for aDesiredProto will use Promise.prototype. + JS::Handle<JSObject*> aDesiredProto = nullptr); +#endif // SPIDERMONKEY_PROMISE + + typedef void (Promise::*MaybeFunc)(JSContext* aCx, + JS::Handle<JS::Value> aValue); + + void MaybeResolve(JSContext* aCx, + JS::Handle<JS::Value> aValue); + void MaybeReject(JSContext* aCx, + JS::Handle<JS::Value> aValue); + + // Helpers for using Promise from C++. + // Most DOM objects are handled already. To add a new type T, add a + // ToJSValue overload in ToJSValue.h. + // aArg is a const reference so we can pass rvalues like integer constants + template <typename T> + void MaybeResolve(const T& aArg) { + MaybeSomething(aArg, &Promise::MaybeResolve); + } + + void MaybeResolveWithUndefined(); + + inline void MaybeReject(nsresult aArg) { + MOZ_ASSERT(NS_FAILED(aArg)); + MaybeSomething(aArg, &Promise::MaybeReject); + } + + inline void MaybeReject(ErrorResult& aArg) { + MOZ_ASSERT(aArg.Failed()); + MaybeSomething(aArg, &Promise::MaybeReject); + } + + void MaybeReject(const RefPtr<MediaStreamError>& aArg); + + void MaybeRejectWithUndefined(); + + // DO NOT USE MaybeRejectBrokenly with in new code. Promises should be + // rejected with Error instances. + // Note: MaybeRejectBrokenly is a template so we can use it with DOMError + // without instantiating the DOMError specialization of MaybeSomething in + // every translation unit that includes this header, because that would + // require use to include DOMError.h either here or in all those translation + // units. + template<typename T> + void MaybeRejectBrokenly(const T& aArg); // Not implemented by default; see + // specializations in the .cpp for + // the T values we support. + + // Called by DOM to let us execute our callbacks. May be called recursively. + // Returns true if at least one microtask was processed. + static bool PerformMicroTaskCheckpoint(); + + static void PerformWorkerMicroTaskCheckpoint(); + + static void PerformWorkerDebuggerMicroTaskCheckpoint(); + + // WebIDL + + nsIGlobalObject* GetParentObject() const + { + return mGlobal; + } + +#ifdef SPIDERMONKEY_PROMISE + bool + WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto, + JS::MutableHandle<JSObject*> aWrapper); + + // Do the equivalent of Promise.resolve in the current compartment of aCx. + // Errorrs are reported on the ErrorResult; if aRv comes back !Failed(), this + // function MUST return a non-null value. + static already_AddRefed<Promise> + Resolve(nsIGlobalObject* aGlobal, JSContext* aCx, + JS::Handle<JS::Value> aValue, ErrorResult& aRv); + + // Do the equivalent of Promise.reject in the current compartment of aCx. + // Errorrs are reported on the ErrorResult; if aRv comes back !Failed(), this + // function MUST return a non-null value. + static already_AddRefed<Promise> + Reject(nsIGlobalObject* aGlobal, JSContext* aCx, + JS::Handle<JS::Value> aValue, ErrorResult& aRv); + + static already_AddRefed<Promise> + All(const GlobalObject& aGlobal, + const nsTArray<RefPtr<Promise>>& aPromiseList, ErrorResult& aRv); + + void + Then(JSContext* aCx, + // aCalleeGlobal may not be in the compartment of aCx, when called over + // Xrays. + JS::Handle<JSObject*> aCalleeGlobal, + AnyCallback* aResolveCallback, AnyCallback* aRejectCallback, + JS::MutableHandle<JS::Value> aRetval, + ErrorResult& aRv); + + JSObject* PromiseObj() const + { + return mPromiseObj; + } + +#else // SPIDERMONKEY_PROMISE + JSObject* PromiseObj() + { + return GetWrapper(); + } + + virtual JSObject* + WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override; + + static already_AddRefed<Promise> + Constructor(const GlobalObject& aGlobal, PromiseInit& aInit, + ErrorResult& aRv, JS::Handle<JSObject*> aDesiredProto); + + static void + Resolve(const GlobalObject& aGlobal, JS::Handle<JS::Value> aThisv, + JS::Handle<JS::Value> aValue, + JS::MutableHandle<JS::Value> aRetval, ErrorResult& aRv); + + static already_AddRefed<Promise> + Resolve(nsIGlobalObject* aGlobal, JSContext* aCx, + JS::Handle<JS::Value> aValue, ErrorResult& aRv); + + static void + Reject(const GlobalObject& aGlobal, JS::Handle<JS::Value> aThisv, + JS::Handle<JS::Value> aValue, + JS::MutableHandle<JS::Value> aRetval, ErrorResult& aRv); + + static already_AddRefed<Promise> + Reject(nsIGlobalObject* aGlobal, JSContext* aCx, + JS::Handle<JS::Value> aValue, ErrorResult& aRv); + + void + Then(JSContext* aCx, + // aCalleeGlobal may not be in the compartment of aCx, when called over + // Xrays. + JS::Handle<JSObject*> aCalleeGlobal, + AnyCallback* aResolveCallback, AnyCallback* aRejectCallback, + JS::MutableHandle<JS::Value> aRetval, + ErrorResult& aRv); + + void + Catch(JSContext* aCx, + AnyCallback* aRejectCallback, + JS::MutableHandle<JS::Value> aRetval, + ErrorResult& aRv); + + static void + All(const GlobalObject& aGlobal, JS::Handle<JS::Value> aThisv, + JS::Handle<JS::Value> aIterable, JS::MutableHandle<JS::Value> aRetval, + ErrorResult& aRv); + + static already_AddRefed<Promise> + All(const GlobalObject& aGlobal, + const nsTArray<RefPtr<Promise>>& aPromiseList, ErrorResult& aRv); + + static void + Race(const GlobalObject& aGlobal, JS::Handle<JS::Value> aThisv, + JS::Handle<JS::Value> aIterable, JS::MutableHandle<JS::Value> aRetval, + ErrorResult& aRv); + + static bool + PromiseSpecies(JSContext* aCx, unsigned aArgc, JS::Value* aVp); +#endif // SPIDERMONKEY_PROMISE + + void AppendNativeHandler(PromiseNativeHandler* aRunnable); + + JSObject* GlobalJSObject() const; + + JSCompartment* Compartment() const; + +#ifndef SPIDERMONKEY_PROMISE + // Return a unique-to-the-process identifier for this Promise. + uint64_t GetID(); +#endif // SPIDERMONKEY_PROMISE + +#ifndef SPIDERMONKEY_PROMISE + enum JSCallbackSlots { + SLOT_PROMISE = 0, + SLOT_DATA + }; +#endif // SPIDERMONKEY_PROMISE + +#ifdef SPIDERMONKEY_PROMISE + // Create a dom::Promise from a given SpiderMonkey Promise object. + // aPromiseObj MUST be in the compartment of aGlobal's global JS object. + static already_AddRefed<Promise> + CreateFromExisting(nsIGlobalObject* aGlobal, + JS::Handle<JSObject*> aPromiseObj); +#endif // SPIDERMONKEY_PROMISE + + enum class PromiseState { + Pending, + Resolved, + Rejected + }; + + PromiseState State() const; + +protected: + struct PromiseCapability; + + // Do NOT call this unless you're Promise::Create or + // Promise::CreateFromExisting. I wish we could enforce that from inside this + // class too, somehow. + explicit Promise(nsIGlobalObject* aGlobal); + + virtual ~Promise(); + + // Do JS-wrapping after Promise creation. Passing null for aDesiredProto will + // use the default prototype for the sort of Promise we have. + void CreateWrapper(JS::Handle<JSObject*> aDesiredProto, ErrorResult& aRv); + +#ifndef SPIDERMONKEY_PROMISE + // 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); + + // The NewPromiseCapability function from + // <http://www.ecma-international.org/ecma-262/6.0/#sec-newpromisecapability>. + // Errors are communicated via aRv. If aForceCallbackCreation is + // true, then this function will ensure that aCapability has a + // useful mResolve/mReject even if mNativePromise is non-null. + static void NewPromiseCapability(JSContext* aCx, nsIGlobalObject* aGlobal, + JS::Handle<JS::Value> aConstructor, + bool aForceCallbackCreation, + PromiseCapability& aCapability, + ErrorResult& aRv); + + bool IsPending() + { + return mResolvePending; + } + + void GetDependentPromises(nsTArray<RefPtr<Promise>>& aPromises); + + bool IsLastInChain() const + { + return mIsLastInChain; + } + + void SetNotifiedAsUncaught() + { + mWasNotifiedAsUncaught = true; + } + + bool WasNotifiedAsUncaught() const + { + return mWasNotifiedAsUncaught; + } +#endif // SPIDERMONKEY_PROMISE + +private: +#ifndef SPIDERMONKEY_PROMISE + friend class PromiseDebugging; + + void SetState(PromiseState aState) + { + MOZ_ASSERT(mState == Pending); + MOZ_ASSERT(aState != Pending); + mState = aState; + } + + void SetResult(JS::Handle<JS::Value> aValue) + { + mResult = aValue; + } + + // This method enqueues 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 + // appended by then() or catch(). + void TriggerPromiseReactions(); + + void Settle(JS::Handle<JS::Value> aValue, Promise::PromiseState aState); + void MaybeSettle(JS::Handle<JS::Value> aValue, Promise::PromiseState aState); + + void AppendCallbacks(PromiseCallback* aResolveCallback, + PromiseCallback* aRejectCallback); + +#if defined(DOM_PROMISE_DEPRECATED_REPORTING) + // If we have been rejected and our mResult is a JS exception, + // report it to the error console. + // Use MaybeReportRejectedOnce() for actual calls. + void MaybeReportRejected(); + + void MaybeReportRejectedOnce() { + MaybeReportRejected(); + RemoveWorkerHolder(); + mResult.setUndefined(); + } +#endif // defined(DOM_PROMISE_DEPRECATED_REPORTING) + + void MaybeResolveInternal(JSContext* aCx, + JS::Handle<JS::Value> aValue); + void MaybeRejectInternal(JSContext* aCx, + JS::Handle<JS::Value> aValue); + + void ResolveInternal(JSContext* aCx, + JS::Handle<JS::Value> aValue); + void RejectInternal(JSContext* aCx, + JS::Handle<JS::Value> aValue); +#endif // SPIDERMONKEY_PROMISE + + template <typename T> + void MaybeSomething(T& aArgument, MaybeFunc aFunc) { + MOZ_ASSERT(PromiseObj()); // It was preserved! + + AutoEntryScript aes(mGlobal, "Promise resolution or rejection"); + JSContext* cx = aes.cx(); + + JS::Rooted<JS::Value> val(cx); + if (!ToJSValue(cx, aArgument, &val)) { + HandleException(cx); + return; + } + + (this->*aFunc)(cx, val); + } + +#ifndef SPIDERMONKEY_PROMISE + // 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, Promise* aPromise, int32_t aTask); + + static JSObject* + CreateThenableFunction(JSContext* aCx, Promise* aPromise, uint32_t aTask); + +#if defined(DOM_PROMISE_DEPRECATED_REPORTING) + void RemoveWorkerHolder(); +#endif // defined(DOM_PROMISE_DEPRECATED_REPORTING) + + // Capture the current stack and store it in aTarget. If false is + // returned, an exception is presumably pending on aCx. + bool CaptureStack(JSContext* aCx, JS::Heap<JSObject*>& aTarget); +#endif // SPIDERMONKEY_PROMISE + + void HandleException(JSContext* aCx); + + RefPtr<nsIGlobalObject> mGlobal; + +#ifndef SPIDERMONKEY_PROMISE + nsTArray<RefPtr<PromiseCallback> > mResolveCallbacks; + nsTArray<RefPtr<PromiseCallback> > mRejectCallbacks; + + JS::Heap<JS::Value> mResult; + // A stack that shows where this promise was allocated, if there was + // JS running at the time. Otherwise null. + JS::Heap<JSObject*> mAllocationStack; + // mRejectionStack is only set when the promise is rejected directly from + // script, by calling Promise.reject() or the rejection callback we pass to + // the PromiseInit function. Promises that are rejected internally do not + // have a rejection stack. + JS::Heap<JSObject*> mRejectionStack; + // mFullfillmentStack is only set when the promise is fulfilled directly from + // script, by calling Promise.resolve() or the fulfillment callback we pass to + // the PromiseInit function. Promises that are fulfilled internally do not + // have a fulfillment stack. + JS::Heap<JSObject*> mFullfillmentStack; + PromiseState mState; + +#if defined(DOM_PROMISE_DEPRECATED_REPORTING) + bool mHadRejectCallback; + + // If a rejected promise on a worker has no reject callbacks attached, it + // needs to know when the worker is shutting down, to report the error on the + // console before the worker's context is deleted. This feature is used for + // that purpose. + nsAutoPtr<PromiseReportRejectWorkerHolder> mWorkerHolder; +#endif // defined(DOM_PROMISE_DEPRECATED_REPORTING) + + bool mTaskPending; + bool mResolvePending; + + // `true` if this Promise is the last in the chain, or `false` if + // another Promise has been created from this one by a call to + // `then`, `all`, `race`, etc. + bool mIsLastInChain; + + // `true` if PromiseDebugging has already notified at least one observer that + // this promise was left uncaught, `false` otherwise. + bool mWasNotifiedAsUncaught; + + // The time when this promise was created. + TimeStamp mCreationTimestamp; + + // The time when this promise transitioned out of the pending state. + TimeStamp mSettlementTimestamp; + + // Once `GetID()` has been called, a unique-to-the-process identifier for this + // promise. Until then, `0`. + uint64_t mID; +#else // SPIDERMONKEY_PROMISE + JS::Heap<JSObject*> mPromiseObj; +#endif // SPIDERMONKEY_PROMISE +}; + +NS_DEFINE_STATIC_IID_ACCESSOR(Promise, NS_PROMISE_IID) + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_Promise_h diff --git a/dom/promise/PromiseCallback.cpp b/dom/promise/PromiseCallback.cpp new file mode 100644 index 000000000..3f4a689ae --- /dev/null +++ b/dom/promise/PromiseCallback.cpp @@ -0,0 +1,575 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=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 "PromiseCallback.h" +#include "mozilla/dom/Promise.h" +#include "mozilla/dom/PromiseNativeHandler.h" + +#include "jsapi.h" +#include "jsfriendapi.h" +#include "jswrapper.h" + +namespace mozilla { +namespace dom { + +#ifndef SPIDERMONKEY_PROMISE + +NS_IMPL_CYCLE_COLLECTING_ADDREF(PromiseCallback) +NS_IMPL_CYCLE_COLLECTING_RELEASE(PromiseCallback) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(PromiseCallback) + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +NS_IMPL_CYCLE_COLLECTION_0(PromiseCallback) + +PromiseCallback::PromiseCallback() +{ +} + +PromiseCallback::~PromiseCallback() +{ +} + +// ResolvePromiseCallback + +NS_IMPL_CYCLE_COLLECTION_CLASS(ResolvePromiseCallback) + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(ResolvePromiseCallback, + PromiseCallback) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mPromise) + tmp->mGlobal = nullptr; +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(ResolvePromiseCallback, + PromiseCallback) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mPromise) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE_SCRIPT_OBJECTS +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN(ResolvePromiseCallback) + NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mGlobal) +NS_IMPL_CYCLE_COLLECTION_TRACE_END + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION_INHERITED(ResolvePromiseCallback) +NS_INTERFACE_MAP_END_INHERITING(PromiseCallback) + +NS_IMPL_ADDREF_INHERITED(ResolvePromiseCallback, PromiseCallback) +NS_IMPL_RELEASE_INHERITED(ResolvePromiseCallback, PromiseCallback) + +ResolvePromiseCallback::ResolvePromiseCallback(Promise* aPromise, + JS::Handle<JSObject*> aGlobal) + : mPromise(aPromise) + , mGlobal(aGlobal) +{ + MOZ_ASSERT(aPromise); + MOZ_ASSERT(aGlobal); + HoldJSObjects(this); +} + +ResolvePromiseCallback::~ResolvePromiseCallback() +{ + DropJSObjects(this); +} + +nsresult +ResolvePromiseCallback::Call(JSContext* aCx, + JS::Handle<JS::Value> aValue) +{ + // Run resolver's algorithm with value and the synchronous flag set. + + JS::ExposeValueToActiveJS(aValue); + + JSAutoCompartment ac(aCx, mGlobal); + JS::Rooted<JS::Value> value(aCx, aValue); + if (!JS_WrapValue(aCx, &value)) { + NS_WARNING("Failed to wrap value into the right compartment."); + return NS_ERROR_FAILURE; + } + + mPromise->ResolveInternal(aCx, value); + return NS_OK; +} + +// RejectPromiseCallback + +NS_IMPL_CYCLE_COLLECTION_CLASS(RejectPromiseCallback) + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(RejectPromiseCallback, + PromiseCallback) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mPromise) + tmp->mGlobal = nullptr; +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(RejectPromiseCallback, + PromiseCallback) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mPromise) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE_SCRIPT_OBJECTS +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION_INHERITED(RejectPromiseCallback) +NS_INTERFACE_MAP_END_INHERITING(PromiseCallback) + +NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN_INHERITED(RejectPromiseCallback, + PromiseCallback) + NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mGlobal) +NS_IMPL_CYCLE_COLLECTION_TRACE_END + +NS_IMPL_ADDREF_INHERITED(RejectPromiseCallback, PromiseCallback) +NS_IMPL_RELEASE_INHERITED(RejectPromiseCallback, PromiseCallback) + +RejectPromiseCallback::RejectPromiseCallback(Promise* aPromise, + JS::Handle<JSObject*> aGlobal) + : mPromise(aPromise) + , mGlobal(aGlobal) +{ + MOZ_ASSERT(aPromise); + MOZ_ASSERT(mGlobal); + HoldJSObjects(this); +} + +RejectPromiseCallback::~RejectPromiseCallback() +{ + DropJSObjects(this); +} + +nsresult +RejectPromiseCallback::Call(JSContext* aCx, + JS::Handle<JS::Value> aValue) +{ + // Run resolver's algorithm with value and the synchronous flag set. + + JS::ExposeValueToActiveJS(aValue); + + JSAutoCompartment ac(aCx, mGlobal); + JS::Rooted<JS::Value> value(aCx, aValue); + if (!JS_WrapValue(aCx, &value)) { + NS_WARNING("Failed to wrap value into the right compartment."); + return NS_ERROR_FAILURE; + } + + + mPromise->RejectInternal(aCx, value); + return NS_OK; +} + +// InvokePromiseFuncCallback + +NS_IMPL_CYCLE_COLLECTION_CLASS(InvokePromiseFuncCallback) + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(InvokePromiseFuncCallback, + PromiseCallback) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mPromiseFunc) + tmp->mGlobal = nullptr; + tmp->mNextPromiseObj = nullptr; +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(InvokePromiseFuncCallback, + PromiseCallback) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mPromiseFunc) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE_SCRIPT_OBJECTS +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN(InvokePromiseFuncCallback) + NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mGlobal) + NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mNextPromiseObj) +NS_IMPL_CYCLE_COLLECTION_TRACE_END + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION_INHERITED(InvokePromiseFuncCallback) +NS_INTERFACE_MAP_END_INHERITING(PromiseCallback) + +NS_IMPL_ADDREF_INHERITED(InvokePromiseFuncCallback, PromiseCallback) +NS_IMPL_RELEASE_INHERITED(InvokePromiseFuncCallback, PromiseCallback) + +InvokePromiseFuncCallback::InvokePromiseFuncCallback(JS::Handle<JSObject*> aGlobal, + JS::Handle<JSObject*> aNextPromiseObj, + AnyCallback* aPromiseFunc) + : mGlobal(aGlobal) + , mNextPromiseObj(aNextPromiseObj) + , mPromiseFunc(aPromiseFunc) +{ + MOZ_ASSERT(aGlobal); + MOZ_ASSERT(aNextPromiseObj); + MOZ_ASSERT(aPromiseFunc); + HoldJSObjects(this); +} + +InvokePromiseFuncCallback::~InvokePromiseFuncCallback() +{ + DropJSObjects(this); +} + +nsresult +InvokePromiseFuncCallback::Call(JSContext* aCx, + JS::Handle<JS::Value> aValue) +{ + // Run resolver's algorithm with value and the synchronous flag set. + + JS::ExposeValueToActiveJS(aValue); + + JSAutoCompartment ac(aCx, mGlobal); + JS::Rooted<JS::Value> value(aCx, aValue); + if (!JS_WrapValue(aCx, &value)) { + NS_WARNING("Failed to wrap value into the right compartment."); + return NS_ERROR_FAILURE; + } + + ErrorResult rv; + JS::Rooted<JS::Value> ignored(aCx); + mPromiseFunc->Call(value, &ignored, rv); + // Useful exceptions already got reported. + rv.SuppressException(); + return NS_OK; +} + +Promise* +InvokePromiseFuncCallback::GetDependentPromise() +{ + Promise* promise; + if (NS_SUCCEEDED(UNWRAP_OBJECT(Promise, mNextPromiseObj, promise))) { + return promise; + } + + // Oh, well. + return nullptr; +} + +// WrapperPromiseCallback +NS_IMPL_CYCLE_COLLECTION_CLASS(WrapperPromiseCallback) + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(WrapperPromiseCallback, + PromiseCallback) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mNextPromise) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mResolveFunc) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mRejectFunc) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mCallback) + tmp->mGlobal = nullptr; + tmp->mNextPromiseObj = nullptr; +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(WrapperPromiseCallback, + PromiseCallback) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mNextPromise) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mResolveFunc) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mRejectFunc) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mCallback) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE_SCRIPT_OBJECTS +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN(WrapperPromiseCallback) + NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mGlobal) + NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mNextPromiseObj) +NS_IMPL_CYCLE_COLLECTION_TRACE_END + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION_INHERITED(WrapperPromiseCallback) +NS_INTERFACE_MAP_END_INHERITING(PromiseCallback) + +NS_IMPL_ADDREF_INHERITED(WrapperPromiseCallback, PromiseCallback) +NS_IMPL_RELEASE_INHERITED(WrapperPromiseCallback, PromiseCallback) + +WrapperPromiseCallback::WrapperPromiseCallback(Promise* aNextPromise, + JS::Handle<JSObject*> aGlobal, + AnyCallback* aCallback) + : mNextPromise(aNextPromise) + , mGlobal(aGlobal) + , mCallback(aCallback) +{ + MOZ_ASSERT(aNextPromise); + MOZ_ASSERT(aGlobal); + HoldJSObjects(this); +} + +WrapperPromiseCallback::WrapperPromiseCallback(JS::Handle<JSObject*> aGlobal, + AnyCallback* aCallback, + JS::Handle<JSObject*> aNextPromiseObj, + AnyCallback* aResolveFunc, + AnyCallback* aRejectFunc) + : mNextPromiseObj(aNextPromiseObj) + , mResolveFunc(aResolveFunc) + , mRejectFunc(aRejectFunc) + , mGlobal(aGlobal) + , mCallback(aCallback) +{ + MOZ_ASSERT(mNextPromiseObj); + MOZ_ASSERT(aResolveFunc); + MOZ_ASSERT(aRejectFunc); + MOZ_ASSERT(aGlobal); + HoldJSObjects(this); +} + +WrapperPromiseCallback::~WrapperPromiseCallback() +{ + DropJSObjects(this); +} + +nsresult +WrapperPromiseCallback::Call(JSContext* aCx, + JS::Handle<JS::Value> aValue) +{ + JS::ExposeValueToActiveJS(aValue); + + JSAutoCompartment ac(aCx, mGlobal); + JS::Rooted<JS::Value> value(aCx, aValue); + if (!JS_WrapValue(aCx, &value)) { + NS_WARNING("Failed to wrap value into the right compartment."); + return NS_ERROR_FAILURE; + } + + ErrorResult rv; + + // PromiseReactionTask step 6 + JS::Rooted<JS::Value> retValue(aCx); + JSCompartment* compartment; + if (mNextPromise) { + compartment = mNextPromise->Compartment(); + } else { + MOZ_ASSERT(mNextPromiseObj); + compartment = js::GetObjectCompartment(mNextPromiseObj); + } + mCallback->Call(value, &retValue, rv, "promise callback", + CallbackObject::eRethrowExceptions, + compartment); + + rv.WouldReportJSException(); + + // PromiseReactionTask step 7 + if (rv.Failed()) { + if (rv.IsUncatchableException()) { + // We have nothing to resolve/reject the promise with. + return rv.StealNSResult(); + } + + JS::Rooted<JS::Value> value(aCx); + { // Scope for JSAutoCompartment + // Convert the ErrorResult to a JS exception object that we can reject + // ourselves with. This will be exactly the exception that would get + // thrown from a binding method whose ErrorResult ended up with whatever + // is on "rv" right now. Do this in the promise reflector compartment. + Maybe<JSAutoCompartment> ac; + if (mNextPromise) { + ac.emplace(aCx, mNextPromise->GlobalJSObject()); + } else { + ac.emplace(aCx, mNextPromiseObj); + } + DebugOnly<bool> conversionResult = ToJSValue(aCx, rv, &value); + MOZ_ASSERT(conversionResult); + } + + if (mNextPromise) { + mNextPromise->RejectInternal(aCx, value); + } else { + JS::Rooted<JS::Value> ignored(aCx); + ErrorResult rejectRv; + mRejectFunc->Call(value, &ignored, rejectRv); + // This reported any JS exceptions; we just have a pointless exception on + // there now. + rejectRv.SuppressException(); + } + return NS_OK; + } + + // If the return value is the same as the promise itself, throw TypeError. + if (retValue.isObject()) { + JS::Rooted<JSObject*> valueObj(aCx, &retValue.toObject()); + valueObj = js::CheckedUnwrap(valueObj); + JS::Rooted<JSObject*> nextPromiseObj(aCx); + if (mNextPromise) { + nextPromiseObj = mNextPromise->GetWrapper(); + } else { + MOZ_ASSERT(mNextPromiseObj); + nextPromiseObj = mNextPromiseObj; + } + // XXXbz shouldn't this check be over in ResolveInternal anyway? + if (valueObj == nextPromiseObj) { + const char* fileName = nullptr; + uint32_t lineNumber = 0; + + // Try to get some information about the callback to report a sane error, + // but don't try too hard (only deals with scripted functions). + JS::Rooted<JSObject*> unwrapped(aCx, + js::CheckedUnwrap(mCallback->Callback())); + + if (unwrapped) { + JSAutoCompartment ac(aCx, unwrapped); + if (JS_ObjectIsFunction(aCx, unwrapped)) { + JS::Rooted<JS::Value> asValue(aCx, JS::ObjectValue(*unwrapped)); + JS::Rooted<JSFunction*> func(aCx, JS_ValueToFunction(aCx, asValue)); + + MOZ_ASSERT(func); + JSScript* script = JS_GetFunctionScript(aCx, func); + if (script) { + fileName = JS_GetScriptFilename(script); + lineNumber = JS_GetScriptBaseLineNumber(aCx, script); + } + } + } + + // We're back in aValue's compartment here. + JS::Rooted<JSString*> fn(aCx, JS_NewStringCopyZ(aCx, fileName)); + if (!fn) { + // Out of memory. Promise will stay unresolved. + JS_ClearPendingException(aCx); + return NS_ERROR_OUT_OF_MEMORY; + } + + JS::Rooted<JSString*> message(aCx, + JS_NewStringCopyZ(aCx, + "then() cannot return same Promise that it resolves.")); + if (!message) { + // Out of memory. Promise will stay unresolved. + JS_ClearPendingException(aCx); + return NS_ERROR_OUT_OF_MEMORY; + } + + JS::Rooted<JS::Value> typeError(aCx); + if (!JS::CreateError(aCx, JSEXN_TYPEERR, nullptr, fn, lineNumber, 0, + nullptr, message, &typeError)) { + // Out of memory. Promise will stay unresolved. + JS_ClearPendingException(aCx); + return NS_ERROR_OUT_OF_MEMORY; + } + + if (mNextPromise) { + mNextPromise->RejectInternal(aCx, typeError); + } else { + JS::Rooted<JS::Value> ignored(aCx); + ErrorResult rejectRv; + mRejectFunc->Call(typeError, &ignored, rejectRv); + // This reported any JS exceptions; we just have a pointless exception + // on there now. + rejectRv.SuppressException(); + } + return NS_OK; + } + } + + // Otherwise, run resolver's resolve with value. + if (!JS_WrapValue(aCx, &retValue)) { + NS_WARNING("Failed to wrap value into the right compartment."); + return NS_ERROR_FAILURE; + } + + if (mNextPromise) { + mNextPromise->ResolveInternal(aCx, retValue); + } else { + JS::Rooted<JS::Value> ignored(aCx); + ErrorResult resolveRv; + mResolveFunc->Call(retValue, &ignored, resolveRv); + // This reported any JS exceptions; we just have a pointless exception + // on there now. + resolveRv.SuppressException(); + } + + return NS_OK; +} + +Promise* +WrapperPromiseCallback::GetDependentPromise() +{ + // Per spec, various algorithms like all() and race() are actually implemented + // in terms of calling then() but passing it the resolve/reject functions that + // are passed as arguments to function passed to the Promise constructor. + // That will cause the promise in question to hold on to a + // WrapperPromiseCallback, but the dependent promise should really be the one + // whose constructor those functions came from, not the about-to-be-ignored + // return value of "then". So try to determine whether we're in that case and + // if so go ahead and dig the dependent promise out of the function we have. + JSObject* callable = mCallback->Callable(); + // Unwrap it, in case it's a cross-compartment wrapper. Our caller here is + // system, so it's really ok to just go and unwrap. + callable = js::UncheckedUnwrap(callable); + if (JS_IsNativeFunction(callable, Promise::JSCallback)) { + JS::Value promiseVal = + js::GetFunctionNativeReserved(callable, Promise::SLOT_PROMISE); + Promise* promise; + UNWRAP_OBJECT(Promise, &promiseVal.toObject(), promise); + return promise; + } + + if (mNextPromise) { + return mNextPromise; + } + + Promise* promise; + if (NS_SUCCEEDED(UNWRAP_OBJECT(Promise, mNextPromiseObj, promise))) { + return promise; + } + + // Oh, well. + return nullptr; +} + +// NativePromiseCallback + +NS_IMPL_CYCLE_COLLECTION_INHERITED(NativePromiseCallback, + PromiseCallback, mHandler) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION_INHERITED(NativePromiseCallback) +NS_INTERFACE_MAP_END_INHERITING(PromiseCallback) + +NS_IMPL_ADDREF_INHERITED(NativePromiseCallback, PromiseCallback) +NS_IMPL_RELEASE_INHERITED(NativePromiseCallback, PromiseCallback) + +NativePromiseCallback::NativePromiseCallback(PromiseNativeHandler* aHandler, + Promise::PromiseState aState) + : mHandler(aHandler) + , mState(aState) +{ + MOZ_ASSERT(aHandler); +} + +NativePromiseCallback::~NativePromiseCallback() +{ +} + +nsresult +NativePromiseCallback::Call(JSContext* aCx, + JS::Handle<JS::Value> aValue) +{ + JS::ExposeValueToActiveJS(aValue); + + if (mState == Promise::Resolved) { + mHandler->ResolvedCallback(aCx, aValue); + return NS_OK; + } + + if (mState == Promise::Rejected) { + mHandler->RejectedCallback(aCx, aValue); + return NS_OK; + } + + NS_NOTREACHED("huh?"); + return NS_ERROR_FAILURE; +} + +/* static */ PromiseCallback* +PromiseCallback::Factory(Promise* aNextPromise, JS::Handle<JSObject*> aGlobal, + AnyCallback* aCallback, Task aTask) +{ + MOZ_ASSERT(aNextPromise); + + // If we have a callback and a next resolver, we have to exec the callback and + // then propagate the return value to the next resolver->resolve(). + if (aCallback) { + return new WrapperPromiseCallback(aNextPromise, aGlobal, aCallback); + } + + if (aTask == Resolve) { + return new ResolvePromiseCallback(aNextPromise, aGlobal); + } + + if (aTask == Reject) { + return new RejectPromiseCallback(aNextPromise, aGlobal); + } + + MOZ_ASSERT(false, "This should not happen"); + return nullptr; +} + +#endif // SPIDERMONKEY_PROMISE + +} // namespace dom +} // namespace mozilla diff --git a/dom/promise/PromiseCallback.h b/dom/promise/PromiseCallback.h new file mode 100644 index 000000000..9f55e03d0 --- /dev/null +++ b/dom/promise/PromiseCallback.h @@ -0,0 +1,203 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=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_PromiseCallback_h +#define mozilla_dom_PromiseCallback_h + +#include "mozilla/dom/Promise.h" +#include "nsCycleCollectionParticipant.h" + +namespace mozilla { +namespace dom { + +#ifndef SPIDERMONKEY_PROMISE +// This is the base class for any PromiseCallback. +// It's a logical step in the promise chain of callbacks. +class PromiseCallback : public nsISupports +{ +protected: + virtual ~PromiseCallback(); + +public: + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_CLASS(PromiseCallback) + + PromiseCallback(); + + virtual nsresult Call(JSContext* aCx, + JS::Handle<JS::Value> aValue) = 0; + + // Return the Promise that this callback will end up resolving or + // rejecting, if any. + virtual Promise* GetDependentPromise() = 0; + + enum Task { + Resolve, + Reject + }; + + // This factory returns a PromiseCallback object with refcount of 0. + static PromiseCallback* + Factory(Promise* aNextPromise, JS::Handle<JSObject*> aObject, + AnyCallback* aCallback, Task aTask); +}; + +// WrapperPromiseCallback execs a JS Callback with a value, and then the return +// value is sent to either: +// a) If aNextPromise is non-null, the aNextPromise->ResolveFunction() or to +// aNextPromise->RejectFunction() if the JS Callback throws. +// or +// b) If aNextPromise is null, in which case aResolveFunc and aRejectFunc must +// be non-null, then to aResolveFunc, unless aCallback threw, in which case +// aRejectFunc. +class WrapperPromiseCallback final : public PromiseCallback +{ +public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS_INHERITED(WrapperPromiseCallback, + PromiseCallback) + + nsresult Call(JSContext* aCx, + JS::Handle<JS::Value> aValue) override; + + Promise* GetDependentPromise() override; + + // Constructor for when we know we have a vanilla Promise. + WrapperPromiseCallback(Promise* aNextPromise, JS::Handle<JSObject*> aGlobal, + AnyCallback* aCallback); + + // Constructor for when all we have to work with are resolve/reject functions. + WrapperPromiseCallback(JS::Handle<JSObject*> aGlobal, + AnyCallback* aCallback, + JS::Handle<JSObject*> mNextPromiseObj, + AnyCallback* aResolveFunc, + AnyCallback* aRejectFunc); + +private: + ~WrapperPromiseCallback(); + + // Either mNextPromise is non-null or all three of mNextPromiseObj, + // mResolveFund and mRejectFunc must are non-null. + RefPtr<Promise> mNextPromise; + // mNextPromiseObj is the reflector itself; it may not be in the + // same compartment as anything else we have. + JS::Heap<JSObject*> mNextPromiseObj; + RefPtr<AnyCallback> mResolveFunc; + RefPtr<AnyCallback> mRejectFunc; + JS::Heap<JSObject*> mGlobal; + RefPtr<AnyCallback> mCallback; +}; + +// ResolvePromiseCallback calls aPromise->ResolveFunction() with the value +// received by Call(). +class ResolvePromiseCallback final : public PromiseCallback +{ +public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS_INHERITED(ResolvePromiseCallback, + PromiseCallback) + + nsresult Call(JSContext* aCx, + JS::Handle<JS::Value> aValue) override; + + Promise* GetDependentPromise() override + { + return mPromise; + } + + ResolvePromiseCallback(Promise* aPromise, JS::Handle<JSObject*> aGlobal); + +private: + ~ResolvePromiseCallback(); + + RefPtr<Promise> mPromise; + JS::Heap<JSObject*> mGlobal; +}; + +// RejectPromiseCallback calls aPromise->RejectFunction() with the value +// received by Call(). +class RejectPromiseCallback final : public PromiseCallback +{ +public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS_INHERITED(RejectPromiseCallback, + PromiseCallback) + + nsresult Call(JSContext* aCx, + JS::Handle<JS::Value> aValue) override; + + Promise* GetDependentPromise() override + { + return mPromise; + } + + RejectPromiseCallback(Promise* aPromise, JS::Handle<JSObject*> aGlobal); + +private: + ~RejectPromiseCallback(); + + RefPtr<Promise> mPromise; + JS::Heap<JSObject*> mGlobal; +}; + +// InvokePromiseFuncCallback calls the given function with the value +// received by Call(). +class InvokePromiseFuncCallback final : public PromiseCallback +{ +public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS_INHERITED(InvokePromiseFuncCallback, + PromiseCallback) + + nsresult Call(JSContext* aCx, + JS::Handle<JS::Value> aValue) override; + + Promise* GetDependentPromise() override; + + InvokePromiseFuncCallback(JS::Handle<JSObject*> aGlobal, + JS::Handle<JSObject*> aNextPromiseObj, + AnyCallback* aPromiseFunc); + +private: + ~InvokePromiseFuncCallback(); + + JS::Heap<JSObject*> mGlobal; + JS::Heap<JSObject*> mNextPromiseObj; + RefPtr<AnyCallback> mPromiseFunc; +}; + +// NativePromiseCallback wraps a PromiseNativeHandler. +class NativePromiseCallback final : public PromiseCallback +{ +public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(NativePromiseCallback, + PromiseCallback) + + nsresult Call(JSContext* aCx, + JS::Handle<JS::Value> aValue) override; + + Promise* GetDependentPromise() override + { + return nullptr; + } + + NativePromiseCallback(PromiseNativeHandler* aHandler, + Promise::PromiseState aState); + +private: + ~NativePromiseCallback(); + + RefPtr<PromiseNativeHandler> mHandler; + Promise::PromiseState mState; +}; + +#endif // SPIDERMONKEY_PROMISE + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_PromiseCallback_h diff --git a/dom/promise/PromiseDebugging.cpp b/dom/promise/PromiseDebugging.cpp new file mode 100644 index 000000000..fc0942ee4 --- /dev/null +++ b/dom/promise/PromiseDebugging.cpp @@ -0,0 +1,521 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=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 "js/Value.h" +#include "nsThreadUtils.h" + +#include "mozilla/CycleCollectedJSContext.h" +#include "mozilla/ThreadLocal.h" +#include "mozilla/TimeStamp.h" + +#include "mozilla/dom/BindingDeclarations.h" +#include "mozilla/dom/ContentChild.h" +#include "mozilla/dom/Promise.h" +#include "mozilla/dom/PromiseBinding.h" +#include "mozilla/dom/PromiseDebugging.h" +#include "mozilla/dom/PromiseDebuggingBinding.h" + +namespace mozilla { +namespace dom { + +class FlushRejections: public CancelableRunnable +{ +public: + static void Init() { + if (!sDispatched.init()) { + MOZ_CRASH("Could not initialize FlushRejections::sDispatched"); + } + sDispatched.set(false); + } + + static void DispatchNeeded() { + if (sDispatched.get()) { + // An instance of `FlushRejections` has already been dispatched + // and not run yet. No need to dispatch another one. + return; + } + sDispatched.set(true); + NS_DispatchToCurrentThread(new FlushRejections()); + } + + static void FlushSync() { + sDispatched.set(false); + + // Call the callbacks if necessary. + // Note that these callbacks may in turn cause Promise to turn + // uncaught or consumed. Since `sDispatched` is `false`, + // `FlushRejections` will be called once again, on an ulterior + // tick. + PromiseDebugging::FlushUncaughtRejectionsInternal(); + } + + NS_IMETHOD Run() override { + FlushSync(); + return NS_OK; + } + +private: + // `true` if an instance of `FlushRejections` is currently dispatched + // and has not been executed yet. + static MOZ_THREAD_LOCAL(bool) sDispatched; +}; + +/* static */ MOZ_THREAD_LOCAL(bool) +FlushRejections::sDispatched; + +#ifndef SPIDERMONKEY_PROMISE +static Promise* +UnwrapPromise(JS::Handle<JSObject*> aPromise, ErrorResult& aRv) +{ + Promise* promise; + if (NS_WARN_IF(NS_FAILED(UNWRAP_OBJECT(Promise, aPromise, promise)))) { + aRv.ThrowTypeError<MSG_IS_NOT_PROMISE>(NS_LITERAL_STRING("Argument")); + return nullptr; + } + return promise; +} +#endif // SPIDERMONKEY_PROMISE + +#ifdef SPIDERMONKEY_PROMISE +/* static */ void +PromiseDebugging::GetState(GlobalObject& aGlobal, JS::Handle<JSObject*> aPromise, + PromiseDebuggingStateHolder& aState, + ErrorResult& aRv) +{ + JSContext* cx = aGlobal.Context(); + JS::Rooted<JSObject*> obj(cx, js::CheckedUnwrap(aPromise)); + if (!obj || !JS::IsPromiseObject(obj)) { + aRv.ThrowTypeError<MSG_IS_NOT_PROMISE>(NS_LITERAL_STRING( + "Argument of PromiseDebugging.getState")); + return; + } + switch (JS::GetPromiseState(obj)) { + case JS::PromiseState::Pending: + aState.mState = PromiseDebuggingState::Pending; + break; + case JS::PromiseState::Fulfilled: + aState.mState = PromiseDebuggingState::Fulfilled; + aState.mValue = JS::GetPromiseResult(obj); + break; + case JS::PromiseState::Rejected: + aState.mState = PromiseDebuggingState::Rejected; + aState.mReason = JS::GetPromiseResult(obj); + break; + } +} + +/* static */ void +PromiseDebugging::GetPromiseID(GlobalObject& aGlobal, + JS::Handle<JSObject*> aPromise, + nsString& aID, + ErrorResult& aRv) +{ + JSContext* cx = aGlobal.Context(); + JS::Rooted<JSObject*> obj(cx, js::CheckedUnwrap(aPromise)); + if (!obj || !JS::IsPromiseObject(obj)) { + aRv.ThrowTypeError<MSG_IS_NOT_PROMISE>(NS_LITERAL_STRING( + "Argument of PromiseDebugging.getState")); + return; + } + uint64_t promiseID = JS::GetPromiseID(obj); + aID = sIDPrefix; + aID.AppendInt(promiseID); +} + +/* static */ void +PromiseDebugging::GetAllocationStack(GlobalObject& aGlobal, + JS::Handle<JSObject*> aPromise, + JS::MutableHandle<JSObject*> aStack, + ErrorResult& aRv) +{ + JSContext* cx = aGlobal.Context(); + JS::Rooted<JSObject*> obj(cx, js::CheckedUnwrap(aPromise)); + if (!obj || !JS::IsPromiseObject(obj)) { + aRv.ThrowTypeError<MSG_IS_NOT_PROMISE>(NS_LITERAL_STRING( + "Argument of PromiseDebugging.getAllocationStack")); + return; + } + aStack.set(JS::GetPromiseAllocationSite(obj)); +} + +/* static */ void +PromiseDebugging::GetRejectionStack(GlobalObject& aGlobal, + JS::Handle<JSObject*> aPromise, + JS::MutableHandle<JSObject*> aStack, + ErrorResult& aRv) +{ + JSContext* cx = aGlobal.Context(); + JS::Rooted<JSObject*> obj(cx, js::CheckedUnwrap(aPromise)); + if (!obj || !JS::IsPromiseObject(obj)) { + aRv.ThrowTypeError<MSG_IS_NOT_PROMISE>(NS_LITERAL_STRING( + "Argument of PromiseDebugging.getRejectionStack")); + return; + } + aStack.set(JS::GetPromiseResolutionSite(obj)); +} + +/* static */ void +PromiseDebugging::GetFullfillmentStack(GlobalObject& aGlobal, + JS::Handle<JSObject*> aPromise, + JS::MutableHandle<JSObject*> aStack, + ErrorResult& aRv) +{ + JSContext* cx = aGlobal.Context(); + JS::Rooted<JSObject*> obj(cx, js::CheckedUnwrap(aPromise)); + if (!obj || !JS::IsPromiseObject(obj)) { + aRv.ThrowTypeError<MSG_IS_NOT_PROMISE>(NS_LITERAL_STRING( + "Argument of PromiseDebugging.getFulfillmentStack")); + return; + } + aStack.set(JS::GetPromiseResolutionSite(obj)); +} + +#else + +/* static */ void +PromiseDebugging::GetState(GlobalObject&, JS::Handle<JSObject*> aPromise, + PromiseDebuggingStateHolder& aState, + ErrorResult& aRv) +{ + Promise* promise = UnwrapPromise(aPromise, aRv); + if (aRv.Failed()) { + return; + } + switch (promise->mState) { + case Promise::Pending: + aState.mState = PromiseDebuggingState::Pending; + break; + case Promise::Resolved: + aState.mState = PromiseDebuggingState::Fulfilled; + aState.mValue = promise->mResult; + break; + case Promise::Rejected: + aState.mState = PromiseDebuggingState::Rejected; + aState.mReason = promise->mResult; + break; + } +} + +#endif // SPIDERMONKEY_PROMISE + +/*static */ nsString +PromiseDebugging::sIDPrefix; + +/* static */ void +PromiseDebugging::Init() +{ + FlushRejections::Init(); + + // Generate a prefix for identifiers: "PromiseDebugging.$processid." + sIDPrefix = NS_LITERAL_STRING("PromiseDebugging."); + if (XRE_IsContentProcess()) { + sIDPrefix.AppendInt(ContentChild::GetSingleton()->GetID()); + sIDPrefix.Append('.'); + } else { + sIDPrefix.AppendLiteral("0."); + } +} + +/* static */ void +PromiseDebugging::Shutdown() +{ + sIDPrefix.SetIsVoid(true); +} + +/* static */ void +PromiseDebugging::FlushUncaughtRejections() +{ + MOZ_ASSERT(!NS_IsMainThread()); + FlushRejections::FlushSync(); +} + +#ifndef SPIDERMONKEY_PROMISE + +/* static */ void +PromiseDebugging::GetAllocationStack(GlobalObject&, JS::Handle<JSObject*> aPromise, + JS::MutableHandle<JSObject*> aStack, + ErrorResult& aRv) +{ + Promise* promise = UnwrapPromise(aPromise, aRv); + if (aRv.Failed()) { + return; + } + aStack.set(promise->mAllocationStack); +} + +/* static */ void +PromiseDebugging::GetRejectionStack(GlobalObject&, JS::Handle<JSObject*> aPromise, + JS::MutableHandle<JSObject*> aStack, + ErrorResult& aRv) +{ + Promise* promise = UnwrapPromise(aPromise, aRv); + if (aRv.Failed()) { + return; + } + aStack.set(promise->mRejectionStack); +} + +/* static */ void +PromiseDebugging::GetFullfillmentStack(GlobalObject&, JS::Handle<JSObject*> aPromise, + JS::MutableHandle<JSObject*> aStack, + ErrorResult& aRv) +{ + Promise* promise = UnwrapPromise(aPromise, aRv); + if (aRv.Failed()) { + return; + } + aStack.set(promise->mFullfillmentStack); +} + +/* static */ void +PromiseDebugging::GetDependentPromises(GlobalObject&, JS::Handle<JSObject*> aPromise, + nsTArray<RefPtr<Promise>>& aPromises, + ErrorResult& aRv) +{ + Promise* promise = UnwrapPromise(aPromise, aRv); + if (aRv.Failed()) { + return; + } + promise->GetDependentPromises(aPromises); +} + +/* static */ double +PromiseDebugging::GetPromiseLifetime(GlobalObject&, + JS::Handle<JSObject*> aPromise, + ErrorResult& aRv) +{ + Promise* promise = UnwrapPromise(aPromise, aRv); + if (aRv.Failed()) { + return 0; + } + return (TimeStamp::Now() - promise->mCreationTimestamp).ToMilliseconds(); +} + +/* static */ double +PromiseDebugging::GetTimeToSettle(GlobalObject&, JS::Handle<JSObject*> aPromise, + ErrorResult& aRv) +{ + Promise* promise = UnwrapPromise(aPromise, aRv); + if (aRv.Failed()) { + return 0; + } + if (promise->mState == Promise::Pending) { + aRv.Throw(NS_ERROR_UNEXPECTED); + return 0; + } + return (promise->mSettlementTimestamp - + promise->mCreationTimestamp).ToMilliseconds(); +} + +#endif // SPIDERMONKEY_PROMISE + +/* static */ void +PromiseDebugging::AddUncaughtRejectionObserver(GlobalObject&, + UncaughtRejectionObserver& aObserver) +{ + CycleCollectedJSContext* storage = CycleCollectedJSContext::Get(); + nsTArray<nsCOMPtr<nsISupports>>& observers = storage->mUncaughtRejectionObservers; + observers.AppendElement(&aObserver); +} + +/* static */ bool +PromiseDebugging::RemoveUncaughtRejectionObserver(GlobalObject&, + UncaughtRejectionObserver& aObserver) +{ + CycleCollectedJSContext* storage = CycleCollectedJSContext::Get(); + nsTArray<nsCOMPtr<nsISupports>>& observers = storage->mUncaughtRejectionObservers; + for (size_t i = 0; i < observers.Length(); ++i) { + UncaughtRejectionObserver* observer = static_cast<UncaughtRejectionObserver*>(observers[i].get()); + if (*observer == aObserver) { + observers.RemoveElementAt(i); + return true; + } + } + return false; +} + +#ifdef SPIDERMONKEY_PROMISE + +/* static */ void +PromiseDebugging::AddUncaughtRejection(JS::HandleObject aPromise) +{ + // This might OOM, but won't set a pending exception, so we'll just ignore it. + if (CycleCollectedJSContext::Get()->mUncaughtRejections.append(aPromise)) { + FlushRejections::DispatchNeeded(); + } +} + +/* void */ void +PromiseDebugging::AddConsumedRejection(JS::HandleObject aPromise) +{ + // If the promise is in our list of uncaught rejections, we haven't yet + // reported it as unhandled. In that case, just remove it from the list + // and don't add it to the list of consumed rejections. + auto& uncaughtRejections = CycleCollectedJSContext::Get()->mUncaughtRejections; + for (size_t i = 0; i < uncaughtRejections.length(); i++) { + if (uncaughtRejections[i] == aPromise) { + // To avoid large amounts of memmoves, we don't shrink the vector here. + // Instead, we filter out nullptrs when iterating over the vector later. + uncaughtRejections[i].set(nullptr); + return; + } + } + // This might OOM, but won't set a pending exception, so we'll just ignore it. + if (CycleCollectedJSContext::Get()->mConsumedRejections.append(aPromise)) { + FlushRejections::DispatchNeeded(); + } +} + +/* static */ void +PromiseDebugging::FlushUncaughtRejectionsInternal() +{ + CycleCollectedJSContext* storage = CycleCollectedJSContext::Get(); + + auto& uncaught = storage->mUncaughtRejections; + auto& consumed = storage->mConsumedRejections; + + AutoJSAPI jsapi; + jsapi.Init(); + JSContext* cx = jsapi.cx(); + + // Notify observers of uncaught Promise. + auto& observers = storage->mUncaughtRejectionObservers; + + for (size_t i = 0; i < uncaught.length(); i++) { + JS::RootedObject promise(cx, uncaught[i]); + // Filter out nullptrs which might've been added by + // PromiseDebugging::AddConsumedRejection. + if (!promise) { + continue; + } + + for (size_t j = 0; j < observers.Length(); ++j) { + RefPtr<UncaughtRejectionObserver> obs = + static_cast<UncaughtRejectionObserver*>(observers[j].get()); + + IgnoredErrorResult err; + obs->OnLeftUncaught(promise, err); + } + JSAutoCompartment ac(cx, promise); + Promise::ReportRejectedPromise(cx, promise); + } + storage->mUncaughtRejections.clear(); + + // Notify observers of consumed Promise. + + for (size_t i = 0; i < consumed.length(); i++) { + JS::RootedObject promise(cx, consumed[i]); + + for (size_t j = 0; j < observers.Length(); ++j) { + RefPtr<UncaughtRejectionObserver> obs = + static_cast<UncaughtRejectionObserver*>(observers[j].get()); + + IgnoredErrorResult err; + obs->OnConsumed(promise, err); + } + } + storage->mConsumedRejections.clear(); +} + +#else + +/* static */ void +PromiseDebugging::AddUncaughtRejection(Promise& aPromise) +{ + CycleCollectedJSContext::Get()->mUncaughtRejections.AppendElement(&aPromise); + FlushRejections::DispatchNeeded(); +} + +/* void */ void +PromiseDebugging::AddConsumedRejection(Promise& aPromise) +{ + CycleCollectedJSContext::Get()->mConsumedRejections.AppendElement(&aPromise); + FlushRejections::DispatchNeeded(); +} + +/* static */ void +PromiseDebugging::GetPromiseID(GlobalObject&, + JS::Handle<JSObject*> aPromise, + nsString& aID, + ErrorResult& aRv) +{ + Promise* promise = UnwrapPromise(aPromise, aRv); + if (aRv.Failed()) { + return; + } + uint64_t promiseID = promise->GetID(); + aID = sIDPrefix; + aID.AppendInt(promiseID); +} + +/* static */ void +PromiseDebugging::FlushUncaughtRejectionsInternal() +{ + CycleCollectedJSContext* storage = CycleCollectedJSContext::Get(); + + // The Promise that have been left uncaught (rejected and last in + // their chain) since the last call to this function. + nsTArray<nsCOMPtr<nsISupports>> uncaught; + storage->mUncaughtRejections.SwapElements(uncaught); + + // The Promise that have been left uncaught at some point, but that + // have eventually had their `then` method called. + nsTArray<nsCOMPtr<nsISupports>> consumed; + storage->mConsumedRejections.SwapElements(consumed); + + nsTArray<nsCOMPtr<nsISupports>>& observers = storage->mUncaughtRejectionObservers; + + nsresult rv; + // Notify observers of uncaught Promise. + + for (size_t i = 0; i < uncaught.Length(); ++i) { + nsCOMPtr<Promise> promise = do_QueryInterface(uncaught[i], &rv); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + + if (!promise->IsLastInChain()) { + // This promise is not the last in the chain anymore, + // so the error has been caught at some point. + continue; + } + + // For the moment, the Promise is still at the end of the + // chain. Let's inform observers, so that they may decide whether + // to report it. + for (size_t j = 0; j < observers.Length(); ++j) { + ErrorResult err; + RefPtr<UncaughtRejectionObserver> obs = + static_cast<UncaughtRejectionObserver*>(observers[j].get()); + + obs->OnLeftUncaught(*promise, err); // Ignore errors + } + + promise->SetNotifiedAsUncaught(); + } + + // Notify observers of consumed Promise. + + for (size_t i = 0; i < consumed.Length(); ++i) { + nsCOMPtr<Promise> promise = do_QueryInterface(consumed[i], &rv); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + + if (!promise->WasNotifiedAsUncaught()) { + continue; + } + + MOZ_ASSERT(!promise->IsLastInChain()); + for (size_t j = 0; j < observers.Length(); ++j) { + ErrorResult err; + RefPtr<UncaughtRejectionObserver> obs = + static_cast<UncaughtRejectionObserver*>(observers[j].get()); + + obs->OnConsumed(*promise, err); // Ignore errors + } + } +} +#endif // SPIDERMONKEY_PROMISE + +} // namespace dom +} // namespace mozilla diff --git a/dom/promise/PromiseDebugging.h b/dom/promise/PromiseDebugging.h new file mode 100644 index 000000000..218a64c2e --- /dev/null +++ b/dom/promise/PromiseDebugging.h @@ -0,0 +1,108 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=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_PromiseDebugging_h +#define mozilla_dom_PromiseDebugging_h + +#include "js/TypeDecls.h" +#include "nsTArray.h" +#include "mozilla/RefPtr.h" + +namespace mozilla { + +class ErrorResult; + +namespace dom { +namespace workers { +class WorkerPrivate; +} // namespace workers + +class Promise; +struct PromiseDebuggingStateHolder; +class GlobalObject; +class UncaughtRejectionObserver; +class FlushRejections; + +void TriggerFlushRejections(); + +class PromiseDebugging +{ +public: + static void Init(); + static void Shutdown(); + + static void GetState(GlobalObject&, JS::Handle<JSObject*> aPromise, + PromiseDebuggingStateHolder& aState, + ErrorResult& aRv); + + static void GetPromiseID(GlobalObject&, JS::Handle<JSObject*>, nsString&, + ErrorResult&); + + static void GetAllocationStack(GlobalObject&, JS::Handle<JSObject*> aPromise, + JS::MutableHandle<JSObject*> aStack, + ErrorResult& aRv); + static void GetRejectionStack(GlobalObject&, JS::Handle<JSObject*> aPromise, + JS::MutableHandle<JSObject*> aStack, + ErrorResult& aRv); + static void GetFullfillmentStack(GlobalObject&, + JS::Handle<JSObject*> aPromise, + JS::MutableHandle<JSObject*> aStack, + ErrorResult& aRv); + +#ifndef SPIDERMONKEY_PROMISE + static void GetDependentPromises(GlobalObject&, + JS::Handle<JSObject*> aPromise, + nsTArray<RefPtr<Promise>>& aPromises, + ErrorResult& aRv); + static double GetPromiseLifetime(GlobalObject&, + JS::Handle<JSObject*> aPromise, + ErrorResult& aRv); + static double GetTimeToSettle(GlobalObject&, JS::Handle<JSObject*> aPromise, + ErrorResult& aRv); +#endif // SPIDERMONKEY_PROMISE + + // Mechanism for watching uncaught instances of Promise. + static void AddUncaughtRejectionObserver(GlobalObject&, + UncaughtRejectionObserver& aObserver); + static bool RemoveUncaughtRejectionObserver(GlobalObject&, + UncaughtRejectionObserver& aObserver); + +#ifdef SPIDERMONKEY_PROMISE + // Mark a Promise as having been left uncaught at script completion. + static void AddUncaughtRejection(JS::HandleObject); + // Mark a Promise previously added with `AddUncaughtRejection` as + // eventually consumed. + static void AddConsumedRejection(JS::HandleObject); +#else + // Mark a Promise as having been left uncaught at script completion. + static void AddUncaughtRejection(Promise&); + // Mark a Promise previously added with `AddUncaughtRejection` as + // eventually consumed. + static void AddConsumedRejection(Promise&); +#endif // SPIDERMONKEY_PROMISE + // Propagate the informations from AddUncaughtRejection + // and AddConsumedRejection to observers. + static void FlushUncaughtRejections(); + +protected: + static void FlushUncaughtRejectionsInternal(); + friend class FlushRejections; + friend class mozilla::dom::workers::WorkerPrivate; +private: + // Identity of the process. + // This property is: + // - set during initialization of the layout module, + // prior to any Worker using it; + // - read by both the main thread and the Workers; + // - unset during shutdown of the layout module, + // after any Worker has been shutdown. + static nsString sIDPrefix; +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_PromiseDebugging_h diff --git a/dom/promise/PromiseNativeHandler.h b/dom/promise/PromiseNativeHandler.h new file mode 100644 index 000000000..6ba7142aa --- /dev/null +++ b/dom/promise/PromiseNativeHandler.h @@ -0,0 +1,38 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=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_PromiseNativeHandler_h +#define mozilla_dom_PromiseNativeHandler_h + +#include "nsISupports.h" +#include "js/TypeDecls.h" + +namespace mozilla { +namespace dom { + +/* + * PromiseNativeHandler allows C++ to react to a Promise being rejected/resolved. + * A PromiseNativeHandler can be appended to a Promise using + * Promise::AppendNativeHandler(). + */ +class PromiseNativeHandler : public nsISupports +{ +protected: + virtual ~PromiseNativeHandler() + { } + +public: + virtual void + ResolvedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue) = 0; + + virtual void + RejectedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue) = 0; +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_PromiseNativeHandler_h diff --git a/dom/promise/PromiseWorkerProxy.h b/dom/promise/PromiseWorkerProxy.h new file mode 100644 index 000000000..bcb44d38f --- /dev/null +++ b/dom/promise/PromiseWorkerProxy.h @@ -0,0 +1,226 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=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_PromiseWorkerProxy_h +#define mozilla_dom_PromiseWorkerProxy_h + +// Required for Promise::PromiseTaskSync. +#include "mozilla/dom/Promise.h" +#include "mozilla/dom/PromiseNativeHandler.h" +#include "mozilla/dom/StructuredCloneHolder.h" +#include "mozilla/dom/workers/bindings/WorkerHolder.h" +#include "nsProxyRelease.h" + +#include "WorkerRunnable.h" + +namespace mozilla { +namespace dom { + +class Promise; + +namespace workers { +class WorkerPrivate; +} // namespace workers + +// A proxy to (eventually) mirror a resolved/rejected Promise's result from the +// main thread to a Promise on the worker thread. +// +// How to use: +// +// 1. Create a Promise on the worker thread and return it to the content +// script: +// +// RefPtr<Promise> promise = Promise::Create(workerPrivate->GlobalScope(), aRv); +// if (aRv.Failed()) { +// return nullptr; +// } +// +// 2. Create a PromiseWorkerProxy wrapping the Promise. If this fails, the +// worker is shutting down and you should fail the original call. This is +// only likely to happen in (Gecko-specific) worker onclose handlers. +// +// RefPtr<PromiseWorkerProxy> proxy = +// PromiseWorkerProxy::Create(workerPrivate, promise); +// if (!proxy) { +// // You may also reject the Promise with an AbortError or similar. +// return nullptr; +// } +// +// 3. Dispatch a runnable to the main thread, with a reference to the proxy to +// perform the main thread operation. PromiseWorkerProxy is thread-safe +// refcounted. +// +// 4. Return the worker thread promise to the JS caller: +// +// return promise.forget(); +// +// 5. In your main thread runnable Run(), obtain a Promise on +// the main thread and call its AppendNativeHandler(PromiseNativeHandler*) +// to bind the PromiseWorkerProxy created at #2. +// +// 4. Then the Promise results returned by ResolvedCallback/RejectedCallback +// will be dispatched as a WorkerRunnable to the worker thread to +// resolve/reject the Promise created at #1. +// +// PromiseWorkerProxy can also be used in situations where there is no main +// thread Promise, or where special handling is required on the worker thread +// for promise resolution. Create a PromiseWorkerProxy as in steps 1 to 3 +// above. When the main thread is ready to resolve the worker thread promise: +// +// 1. Acquire the mutex before attempting to access the worker private. +// +// AssertIsOnMainThread(); +// MutexAutoLock lock(proxy->Lock()); +// if (proxy->CleanedUp()) { +// // Worker has already shut down, can't access worker private. +// return; +// } +// +// 2. Dispatch a runnable to the worker. Use GetWorkerPrivate() to acquire the +// worker. +// +// RefPtr<FinishTaskWorkerRunnable> runnable = +// new FinishTaskWorkerRunnable(proxy->GetWorkerPrivate(), proxy, result); +// if (!r->Dispatch()) { +// // Worker is alive but not Running any more, so the Promise can't +// // be resolved, give up. The proxy will get Release()d at some +// // point. +// +// // Usually do nothing, but you may want to log the fact. +// } +// +// 3. In the WorkerRunnable's WorkerRun() use WorkerPromise() to access the +// Promise and resolve/reject it. Then call CleanUp(). +// +// bool +// WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) override +// { +// aWorkerPrivate->AssertIsOnWorkerThread(); +// RefPtr<Promise> promise = mProxy->WorkerPromise(); +// promise->MaybeResolve(mResult); +// mProxy->CleanUp(); +// } +// +// Note: If a PromiseWorkerProxy is not cleaned up by a WorkerRunnable - this +// can happen if the main thread Promise is never fulfilled - it will +// stay alive till the worker reaches a Canceling state, even if all external +// references to it are dropped. + +class PromiseWorkerProxy : public PromiseNativeHandler + , public StructuredCloneHolderBase +{ + friend class PromiseWorkerProxyRunnable; + + NS_DECL_THREADSAFE_ISUPPORTS + +public: + typedef JSObject* (*ReadCallbackOp)(JSContext* aCx, + JSStructuredCloneReader* aReader, + const PromiseWorkerProxy* aProxy, + uint32_t aTag, + uint32_t aData); + typedef bool (*WriteCallbackOp)(JSContext* aCx, + JSStructuredCloneWriter* aWorker, + PromiseWorkerProxy* aProxy, + JS::HandleObject aObj); + + struct PromiseWorkerProxyStructuredCloneCallbacks + { + ReadCallbackOp Read; + WriteCallbackOp Write; + }; + + static already_AddRefed<PromiseWorkerProxy> + Create(workers::WorkerPrivate* aWorkerPrivate, + Promise* aWorkerPromise, + const PromiseWorkerProxyStructuredCloneCallbacks* aCallbacks = nullptr); + + // Main thread callers must hold Lock() and check CleanUp() before calling this. + // Worker thread callers, this will assert that the proxy has not been cleaned + // up. + workers::WorkerPrivate* GetWorkerPrivate() const; + + // This should only be used within WorkerRunnable::WorkerRun() running on the + // worker thread! Do not call this after calling CleanUp(). + Promise* WorkerPromise() const; + + // Worker thread only. Calling this invalidates several assumptions, so be + // sure this is the last thing you do. + // 1. WorkerPrivate() will no longer return a valid worker. + // 2. WorkerPromise() will crash! + void CleanUp(); + + Mutex& Lock() + { + return mCleanUpLock; + } + + bool CleanedUp() const + { + mCleanUpLock.AssertCurrentThreadOwns(); + return mCleanedUp; + } + + // StructuredCloneHolderBase + + JSObject* CustomReadHandler(JSContext* aCx, + JSStructuredCloneReader* aReader, + uint32_t aTag, + uint32_t aIndex) override; + + bool CustomWriteHandler(JSContext* aCx, + JSStructuredCloneWriter* aWriter, + JS::Handle<JSObject*> aObj) override; + +protected: + virtual void ResolvedCallback(JSContext* aCx, + JS::Handle<JS::Value> aValue) override; + + virtual void RejectedCallback(JSContext* aCx, + JS::Handle<JS::Value> aValue) override; + +private: + PromiseWorkerProxy(workers::WorkerPrivate* aWorkerPrivate, + Promise* aWorkerPromise, + const PromiseWorkerProxyStructuredCloneCallbacks* aCallbacks = nullptr); + + virtual ~PromiseWorkerProxy(); + + bool AddRefObject(); + + // If not called from Create(), be sure to hold Lock(). + void CleanProperties(); + + // Function pointer for calling Promise::{ResolveInternal,RejectInternal}. + typedef void (Promise::*RunCallbackFunc)(JSContext*, + JS::Handle<JS::Value>); + + void RunCallback(JSContext* aCx, + JS::Handle<JS::Value> aValue, + RunCallbackFunc aFunc); + + // Any thread with appropriate checks. + workers::WorkerPrivate* mWorkerPrivate; + + // Worker thread only. + RefPtr<Promise> mWorkerPromise; + + // Modified on the worker thread. + // It is ok to *read* this without a lock on the worker. + // Main thread must always acquire a lock. + bool mCleanedUp; // To specify if the cleanUp() has been done. + + const PromiseWorkerProxyStructuredCloneCallbacks* mCallbacks; + + // Ensure the worker and the main thread won't race to access |mCleanedUp|. + Mutex mCleanUpLock; + + UniquePtr<workers::WorkerHolder> mWorkerHolder; +}; +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_PromiseWorkerProxy_h diff --git a/dom/promise/moz.build b/dom/promise/moz.build new file mode 100644 index 000000000..11d2a7496 --- /dev/null +++ b/dom/promise/moz.build @@ -0,0 +1,37 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +EXPORTS.mozilla.dom += [ + 'Promise.h', + 'PromiseDebugging.h', + 'PromiseNativeHandler.h', + 'PromiseWorkerProxy.h', +] + +UNIFIED_SOURCES += [ + 'Promise.cpp', + 'PromiseCallback.cpp', + 'PromiseDebugging.cpp', +] + +LOCAL_INCLUDES += [ + '../base', + '../ipc', + '../workers', +] + +include('/ipc/chromium/chromium-config.mozbuild') + +FINAL_LIBRARY = 'xul' + +MOCHITEST_MANIFESTS += ['tests/mochitest.ini'] + +MOCHITEST_CHROME_MANIFESTS += ['tests/chrome.ini'] + +XPCSHELL_TESTS_MANIFESTS += ['tests/unit/xpcshell.ini'] + +if CONFIG['GNU_CXX']: + CXXFLAGS += ['-Wno-error=shadow'] diff --git a/dom/promise/tests/chrome.ini b/dom/promise/tests/chrome.ini new file mode 100644 index 000000000..c6dc855c4 --- /dev/null +++ b/dom/promise/tests/chrome.ini @@ -0,0 +1,7 @@ +[DEFAULT] + +[test_on_new_promise.html] +[test_on_promise_settled.html] +[test_on_promise_settled_duplicates.html] +[test_promise_xrays.html] +support-files = file_promise_xrays.html diff --git a/dom/promise/tests/file_promise_and_timeout_ordering.js b/dom/promise/tests/file_promise_and_timeout_ordering.js new file mode 100644 index 000000000..c83b5a6ac --- /dev/null +++ b/dom/promise/tests/file_promise_and_timeout_ordering.js @@ -0,0 +1,18 @@ +var log = []; +var resolvedPromise = Promise.resolve(null); +function schedulePromiseTask(f) { + resolvedPromise.then(f); +} + +setTimeout(function() { + log.push('t1start'); + schedulePromiseTask(function() { + log.push('promise'); + }); + log.push('t1end'); +}, 10); + +setTimeout(function() { + log.push('t2'); + postMessage(log.join(', ')); +}, 10); diff --git a/dom/promise/tests/file_promise_xrays.html b/dom/promise/tests/file_promise_xrays.html new file mode 100644 index 000000000..73f9bf7d7 --- /dev/null +++ b/dom/promise/tests/file_promise_xrays.html @@ -0,0 +1,32 @@ +<!DOCTYPE html> +<html> + <script> + function vendGetter(name) { + return function() { throw "Getting " + String(name) }; + } + function vendSetter(name) { + return function() { throw "Setting " + String(name) }; + } + var setupThrew = false; + try { + // Neuter everything we can think of on Promise. + for (var obj of [Promise, Promise.prototype]) { + propNames = Object.getOwnPropertyNames(obj); + propNames = propNames.concat(Object.getOwnPropertySymbols(obj)); + for (var propName of propNames) { + if ((propName == "prototype" || + propName == Symbol.hasInstance) && + obj == Promise) { + // They're not configurable. + continue; + } + Object.defineProperty(obj, propName, + { get: vendGetter(propName), set: vendSetter(propName) }); + } + } + } catch (e) { + // Something went wrong. Save that info so the test can check for it. + setupThrew = e; + } + </script> +</html> diff --git a/dom/promise/tests/mochitest.ini b/dom/promise/tests/mochitest.ini new file mode 100644 index 000000000..0e6466d16 --- /dev/null +++ b/dom/promise/tests/mochitest.ini @@ -0,0 +1,18 @@ +[DEFAULT] +support-files = + promise_uncatchable_exception.js + +[test_bug883683.html] +[test_promise.html] +[test_promise_uncatchable_exception.html] +skip-if = debug == false +[test_promise_utils.html] +[test_resolve.html] +[test_resolver_return_value.html] +[test_thenable_vs_promise_ordering.html] +[test_promise_and_timeout_ordering.html] +support-files = file_promise_and_timeout_ordering.js +[test_promise_and_timeout_ordering_workers.html] +support-files = file_promise_and_timeout_ordering.js +[test_species_getter.html] +[test_webassembly_compile.html] diff --git a/dom/promise/tests/promise_uncatchable_exception.js b/dom/promise/tests/promise_uncatchable_exception.js new file mode 100644 index 000000000..d062e21af --- /dev/null +++ b/dom/promise/tests/promise_uncatchable_exception.js @@ -0,0 +1,9 @@ +postMessage("Done", "*"); + +var p = new Promise(function(resolve, reject) { + TestFunctions.throwUncatchableException(); + ok(false, "Shouldn't get here!"); +}).catch(function(exception) { + ok(false, "Shouldn't get here!"); +}); +ok(false, "Shouldn't get here!"); diff --git a/dom/promise/tests/test_bug883683.html b/dom/promise/tests/test_bug883683.html new file mode 100644 index 000000000..ab23ca10d --- /dev/null +++ b/dom/promise/tests/test_bug883683.html @@ -0,0 +1,41 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>Promise - bug 883683</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"><!-- + +function runTest() { + [{}, {}, {}, {}, {}].reduce(Promise.reject.bind(Promise)); + ok(true, "No leaks with reject?"); + + [{}, {}, {}, {}, {}].reduce(Promise.resolve.bind(Promise)); + ok(true, "No leaks with resolve?"); + + [{}, {}, {}, {}, {}].reduce(function(a, b, c, d) { return new Promise(function(r1, r2) { throw a; }); }); + ok(true, "No leaks with exception?"); + + [{}, {}, {}, {}, {}].reduce(function(a, b, c, d) { return new Promise(function(r1, r2) { }); }); + ok(true, "No leaks with empty promise?"); + + SimpleTest.finish(); +} + +SimpleTest.waitForExplicitFinish(); +runTest(); +// --> +</script> +</pre> +</body> +</html> diff --git a/dom/promise/tests/test_on_new_promise.html b/dom/promise/tests/test_on_new_promise.html new file mode 100644 index 000000000..634dd7dda --- /dev/null +++ b/dom/promise/tests/test_on_new_promise.html @@ -0,0 +1,45 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> + +<!-- +Bug 1083210 - Sanity test for interaction between DOM promises and +Debugger.prototype.onNewPromise. +--> + +<html> +<head> + <title>Test for interaction with SpiderMonkey's Debugger.prototype.onNewPromise</title> + <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> + <script type="application/javascript"> + is(Object.prototype.toString.call(new Promise(function () {})), + "[object Promise]", + "We should have the native DOM promise implementation."); + + var Cu = Components.utils; + Cu.import("resource://gre/modules/jsdebugger.jsm"); + var dbgGlobal = new Cu.Sandbox(document.nodePrincipal); + addDebuggerToGlobal(dbgGlobal); + var dbg = new dbgGlobal.Debugger(this); + + var wrappedPromise; + dbg.onNewPromise = function (wp) { wrappedPromise = wp; }; + + var promise = new Promise(function () {}); + debugger; + ok(wrappedPromise); + is(wrappedPromise.unsafeDereference(), promise); + </script> +</pre> +</body> +</html> + diff --git a/dom/promise/tests/test_on_promise_settled.html b/dom/promise/tests/test_on_promise_settled.html new file mode 100644 index 000000000..4061d3997 --- /dev/null +++ b/dom/promise/tests/test_on_promise_settled.html @@ -0,0 +1,54 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> + +<!-- +Bug 1084065 - Sanity test for interaction between DOM promises and +Debugger.prototype.onPromiseResolved. +--> + +<html> +<head> + <title>Test for interaction with SpiderMonkey's Debugger.prototype.onNewPromise</title> + <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/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(); + + is(Object.prototype.toString.call(new Promise(function () {})), + "[object Promise]", + "We should have the native DOM promise implementation."); + + var Cu = Components.utils; + Cu.import("resource://gre/modules/jsdebugger.jsm"); + var dbgGlobal = new Cu.Sandbox(document.nodePrincipal); + addDebuggerToGlobal(dbgGlobal); + var dbg = new dbgGlobal.Debugger(this); + + var wrappedPromise; + dbg.onPromiseSettled = function (wp) { wrappedPromise = wp; }; + + var promise = Promise.resolve(); + promise + .then(function () { + ok(wrappedPromise); + is(wrappedPromise.unsafeDereference(), promise); + dbg.onPromiseSettled = undefined; + }) + .then(null, function (e) { + ok(false, "Got an unexpected error: " + e); + }) + .then(SimpleTest.finish); + </script> +</pre> +</body> +</html> + diff --git a/dom/promise/tests/test_on_promise_settled_duplicates.html b/dom/promise/tests/test_on_promise_settled_duplicates.html new file mode 100644 index 000000000..5a1eddb1e --- /dev/null +++ b/dom/promise/tests/test_on_promise_settled_duplicates.html @@ -0,0 +1,59 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> + +<!-- +Bug 1084065 - Test that Debugger.prototype.onPromiseResolved doesn't get dupes. +--> + +<html> +<head> + <title>Test for interaction with SpiderMonkey's Debugger.prototype.onNewPromise</title> + <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/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(); + + is(Object.prototype.toString.call(new Promise(function () {})), + "[object Promise]", + "We should have the native DOM promise implementation."); + + var Cu = Components.utils; + Cu.import("resource://gre/modules/jsdebugger.jsm"); + var dbgGlobal = new Cu.Sandbox(document.nodePrincipal); + addDebuggerToGlobal(dbgGlobal); + var dbg = new dbgGlobal.Debugger(this); + + var seen = new Set(); + dbg.onPromiseSettled = function (wp) { + is(seen.has(wp), false); + seen.add(wp); + }; + + var promise = new Promise(function (fulfill, reject) { + fulfill(1); + fulfill(2); + fulfill(3); + }); + + promise + .then(function () { + dbg.onPromiseSettled = undefined; + }) + .then(null, function (e) { + ok(false, "Got an unexpected error: " + e); + }) + .then(SimpleTest.finish); + </script> +</pre> +</body> +</html> + diff --git a/dom/promise/tests/test_promise.html b/dom/promise/tests/test_promise.html new file mode 100644 index 000000000..af185efcd --- /dev/null +++ b/dom/promise/tests/test_promise.html @@ -0,0 +1,831 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>Basic Promise 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"><!-- + +function promiseResolve() { + ok(Promise, "Promise object should exist"); + + var promise = new Promise(function(resolve, reject) { + ok(resolve, "Promise.resolve exists"); + ok(reject, "Promise.reject exists"); + + resolve(42); + }).then(function(what) { + ok(true, "Then - resolveCb has been called"); + is(what, 42, "ResolveCb received 42"); + runTest(); + }, function() { + ok(false, "Then - rejectCb has been called"); + runTest(); + }); +} + +function promiseResolveNoArg() { + var promise = new Promise(function(resolve, reject) { + ok(resolve, "Promise.resolve exists"); + ok(reject, "Promise.reject exists"); + + resolve(); + }).then(function(what) { + ok(true, "Then - resolveCb has been called"); + is(what, undefined, "ResolveCb received undefined"); + runTest(); + }, function() { + ok(false, "Then - rejectCb has been called"); + runTest(); + }); +} + +function promiseReject() { + var promise = new Promise(function(resolve, reject) { + reject(42); + }).then(function(what) { + ok(false, "Then - resolveCb has been called"); + runTest(); + }, function(what) { + ok(true, "Then - rejectCb has been called"); + is(what, 42, "RejectCb received 42"); + runTest(); + }); +} + +function promiseRejectNoHandler() { + // This test only checks that the code that reports unhandled errors in the + // Promises implementation does not crash or leak. + var promise = new Promise(function(res, rej) { + noSuchMethod(); + }); + runTest(); +} + +function promiseRejectNoArg() { + var promise = new Promise(function(resolve, reject) { + reject(); + }).then(function(what) { + ok(false, "Then - resolveCb has been called"); + runTest(); + }, function(what) { + ok(true, "Then - rejectCb has been called"); + is(what, undefined, "RejectCb received undefined"); + runTest(); + }); +} + +function promiseException() { + var promise = new Promise(function(resolve, reject) { + throw 42; + }).then(function(what) { + ok(false, "Then - resolveCb has been called"); + runTest(); + }, function(what) { + ok(true, "Then - rejectCb has been called"); + is(what, 42, "RejectCb received 42"); + runTest(); + }); +} + +function promiseGC() { + var resolve; + var promise = new Promise(function(r1, r2) { + resolve = r1; + }).then(function(what) { + ok(true, "Then - promise is still alive"); + runTest(); + }); + + promise = null; + + SpecialPowers.gc(); + SpecialPowers.forceGC(); + SpecialPowers.forceCC(); + + resolve(42); +} + +function promiseAsync_TimeoutResolveThen() { + var handlerExecuted = false; + + setTimeout(function() { + ok(handlerExecuted, "Handler should have been called before the timeout."); + + // Allow other assertions to run so the test could fail before the next one. + setTimeout(runTest, 0); + }, 0); + + Promise.resolve().then(function() { + handlerExecuted = true; + }); + + ok(!handlerExecuted, "Handlers are not called before 'then' returns."); +} + +function promiseAsync_ResolveTimeoutThen() { + var handlerExecuted = false; + + var promise = Promise.resolve(); + + setTimeout(function() { + ok(handlerExecuted, "Handler should have been called before the timeout."); + + // Allow other assertions to run so the test could fail before the next one. + setTimeout(runTest, 0); + }, 0); + + promise.then(function() { + handlerExecuted = true; + }); + + ok(!handlerExecuted, "Handlers are not called before 'then' returns."); +} + +function promiseAsync_ResolveThenTimeout() { + var handlerExecuted = false; + + Promise.resolve().then(function() { + handlerExecuted = true; + }); + + setTimeout(function() { + ok(handlerExecuted, "Handler should have been called before the timeout."); + + // Allow other assertions to run so the test could fail before the next one. + setTimeout(runTest, 0); + }, 0); + + ok(!handlerExecuted, "Handlers are not called before 'then' returns."); +} + +function promiseAsync_SyncXHR() +{ + var handlerExecuted = false; + + Promise.resolve().then(function() { + handlerExecuted = true; + + // Allow other assertions to run so the test could fail before the next one. + setTimeout(runTest, 0); + }); + + ok(!handlerExecuted, "Handlers are not called until the next microtask."); + + var xhr = new XMLHttpRequest(); + xhr.open("GET", "testXHR.txt", false); + xhr.send(null); + + todo(!handlerExecuted, "Sync XHR should not trigger microtask execution."); +} + +function promiseDoubleThen() { + var steps = 0; + var promise = new Promise(function(r1, r2) { + r1(42); + }); + + promise.then(function(what) { + ok(true, "Then.resolve has been called"); + is(what, 42, "Value == 42"); + steps++; + }, function(what) { + ok(false, "Then.reject has been called"); + }); + + promise.then(function(what) { + ok(true, "Then.resolve has been called"); + is(steps, 1, "Then.resolve - step == 1"); + is(what, 42, "Value == 42"); + runTest(); + }, function(what) { + ok(false, "Then.reject has been called"); + }); +} + +function promiseThenException() { + var promise = new Promise(function(resolve, reject) { + resolve(42); + }); + + promise.then(function(what) { + ok(true, "Then.resolve has been called"); + throw "booh"; + }).catch(function(e) { + ok(true, "window.onerror has been called!"); + runTest(); + }); +} + +function promiseThenCatchThen() { + var promise = new Promise(function(resolve, reject) { + resolve(42); + }); + + var promise2 = promise.then(function(what) { + ok(true, "Then.resolve has been called"); + is(what, 42, "Value == 42"); + return what + 1; + }, function(what) { + ok(false, "Then.reject has been called"); + }); + + isnot(promise, promise2, "These 2 promise objs are different"); + + promise2.then(function(what) { + ok(true, "Then.resolve has been called"); + is(what, 43, "Value == 43"); + return what + 1; + }, function(what) { + ok(false, "Then.reject has been called"); + }).catch(function() { + ok(false, "Catch has been called"); + }).then(function(what) { + ok(true, "Then.resolve has been called"); + is(what, 44, "Value == 44"); + runTest(); + }, function(what) { + ok(false, "Then.reject has been called"); + }); +} + +function promiseThenNoArg() { + var promise = new Promise(function(resolve, reject) { + resolve(42); + }); + + var clone = promise.then(); + isnot(promise, clone, "These 2 promise objs are different"); + promise.then(function(v) { + clone.then(function(cv) { + is(v, cv, "Both resolve to the same value"); + runTest(); + }); + }); +} + +function promiseThenUndefinedResolveFunction() { + var promise = new Promise(function(resolve, reject) { + reject(42); + }); + + try { + promise.then(undefined, function(v) { + is(v, 42, "Promise rejected with 42"); + runTest(); + }); + } catch (e) { + ok(false, "then should not throw on undefined resolve function"); + } +} + +function promiseThenNullResolveFunction() { + var promise = new Promise(function(resolve, reject) { + reject(42); + }); + + try { + promise.then(null, function(v) { + is(v, 42, "Promise rejected with 42"); + runTest(); + }); + } catch (e) { + ok(false, "then should not throw on null resolve function"); + } +} + +function promiseRejectThenCatchThen() { + var promise = new Promise(function(resolve, reject) { + reject(42); + }); + + var promise2 = promise.then(function(what) { + ok(false, "Then.resolve has been called"); + }, function(what) { + ok(true, "Then.reject has been called"); + is(what, 42, "Value == 42"); + return what + 1; + }); + + isnot(promise, promise2, "These 2 promise objs are different"); + + promise2.then(function(what) { + ok(true, "Then.resolve has been called"); + is(what, 43, "Value == 43"); + return what+1; + }).catch(function(what) { + ok(false, "Catch has been called"); + }).then(function(what) { + ok(true, "Then.resolve has been called"); + is(what, 44, "Value == 44"); + runTest(); + }); +} + +function promiseRejectThenCatchThen2() { + var promise = new Promise(function(resolve, reject) { + reject(42); + }); + + promise.then(function(what) { + ok(true, "Then.resolve has been called"); + is(what, 42, "Value == 42"); + return what+1; + }).catch(function(what) { + is(what, 42, "Value == 42"); + ok(true, "Catch has been called"); + return what+1; + }).then(function(what) { + ok(true, "Then.resolve has been called"); + is(what, 43, "Value == 43"); + runTest(); + }); +} + +function promiseRejectThenCatchExceptionThen() { + var promise = new Promise(function(resolve, reject) { + reject(42); + }); + + promise.then(function(what) { + ok(false, "Then.resolve has been called"); + }, function(what) { + ok(true, "Then.reject has been called"); + is(what, 42, "Value == 42"); + throw(what + 1); + }).catch(function(what) { + ok(true, "Catch has been called"); + is(what, 43, "Value == 43"); + return what + 1; + }).then(function(what) { + ok(true, "Then.resolve has been called"); + is(what, 44, "Value == 44"); + runTest(); + }); +} + +function promiseThenCatchOrderingResolve() { + var global = 0; + var f = new Promise(function(r1, r2) { + r1(42); + }); + + f.then(function() { + f.then(function() { + global++; + }); + f.catch(function() { + global++; + }); + f.then(function() { + global++; + }); + setTimeout(function() { + is(global, 2, "Many steps... should return 2"); + runTest(); + }, 0); + }); +} + +function promiseThenCatchOrderingReject() { + var global = 0; + var f = new Promise(function(r1, r2) { + r2(42); + }) + + f.then(function() {}, function() { + f.then(function() { + global++; + }); + f.catch(function() { + global++; + }); + f.then(function() {}, function() { + global++; + }); + setTimeout(function() { + is(global, 2, "Many steps... should return 2"); + runTest(); + }, 0); + }); +} + +function promiseCatchNoArg() { + var promise = new Promise(function(resolve, reject) { + reject(42); + }); + + var clone = promise.catch(); + isnot(promise, clone, "These 2 promise objs are different"); + promise.catch(function(v) { + clone.catch(function(cv) { + is(v, cv, "Both reject to the same value"); + runTest(); + }); + }); +} + +function promiseNestedPromise() { + new Promise(function(resolve, reject) { + resolve(new Promise(function(resolve, reject) { + ok(true, "Nested promise is executed"); + resolve(42); + })); + }).then(function(value) { + is(value, 42, "Nested promise is executed and then == 42"); + runTest(); + }); +} + +function promiseNestedNestedPromise() { + new Promise(function(resolve, reject) { + resolve(new Promise(function(resolve, reject) { + ok(true, "Nested promise is executed"); + resolve(42); + }).then(function(what) { return what+1; })); + }).then(function(value) { + is(value, 43, "Nested promise is executed and then == 43"); + runTest(); + }); +} + +function promiseWrongNestedPromise() { + new Promise(function(resolve, reject) { + resolve(new Promise(function(r, r2) { + ok(true, "Nested promise is executed"); + r(42); + })); + reject(42); + }).then(function(value) { + is(value, 42, "Nested promise is executed and then == 42"); + runTest(); + }, function(value) { + ok(false, "This is wrong"); + }); +} + +function promiseLoop() { + new Promise(function(resolve, reject) { + resolve(new Promise(function(r1, r2) { + ok(true, "Nested promise is executed"); + r1(new Promise(function(r1, r2) { + ok(true, "Nested nested promise is executed"); + r1(42); + })); + })); + }).then(function(value) { + is(value, 42, "Nested nested promise is executed and then == 42"); + runTest(); + }, function(value) { + ok(false, "This is wrong"); + }); +} + +function promiseStaticReject() { + var promise = Promise.reject(42).then(function(what) { + ok(false, "This should not be called"); + }, function(what) { + is(what, 42, "Value == 42"); + runTest(); + }); +} + +function promiseStaticResolve() { + var promise = Promise.resolve(42).then(function(what) { + is(what, 42, "Value == 42"); + runTest(); + }, function() { + ok(false, "This should not be called"); + }); +} + +function promiseResolveNestedPromise() { + var promise = Promise.resolve(new Promise(function(r, r2) { + ok(true, "Nested promise is executed"); + r(42); + }, function() { + ok(false, "This should not be called"); + })).then(function(what) { + is(what, 42, "Value == 42"); + runTest(); + }, function() { + ok(false, "This should not be called"); + }); +} + +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(); + }); +} + +function promiseStrictHandlers() { + var promise = Promise.resolve(5); + promise.then(function() { + "use strict"; + ok(this === undefined, "Strict mode callback should have this === undefined."); + runTest(); + }); +} + +function promiseStrictExecutorThisArg() { + var promise = new Promise(function(resolve, reject) { + "use strict"; + ok(this === undefined, "thisArg should be undefined."); + runTest(); + }); +} + +function promiseResolveArray() { + var p = Promise.resolve([1,2,3]); + ok(p instanceof Promise, "Should return a Promise."); + p.then(function(v) { + ok(Array.isArray(v), "Resolved value should be an Array"); + is(v.length, 3, "Length should match"); + is(v[0], 1, "Resolved value should match original"); + is(v[1], 2, "Resolved value should match original"); + is(v[2], 3, "Resolved value should match original"); + runTest(); + }); +} + +function promiseResolveThenable() { + var p = Promise.resolve({ then: function(onFulfill, onReject) { onFulfill(2); } }); + ok(p instanceof Promise, "Should cast to a Promise."); + p.then(function(v) { + is(v, 2, "Should resolve to 2."); + runTest(); + }, function(e) { + ok(false, "promiseResolveThenable should've resolved"); + runTest(); + }); +} + +function promiseResolvePromise() { + var original = Promise.resolve(true); + var cast = Promise.resolve(original); + + ok(cast instanceof Promise, "Should cast to a Promise."); + is(cast, original, "Should return original Promise."); + cast.then(function(v) { + is(v, true, "Should resolve to true."); + runTest(); + }); +} + +// Bug 1009569. +// Ensure that thenables are run on a clean stack asynchronously. +// Test case adopted from +// https://gist.github.com/getify/d64bb01751b50ed6b281#file-bug1-js. +function promiseResolveThenableCleanStack() { + function immed(s) { x++; s(); } + function incX(){ x++; } + + var x = 0; + var thenable = { then: immed }; + var results = []; + + var p = Promise.resolve(thenable).then(incX); + results.push(x); + + // check what happens after all "next cycle" steps + // have had a chance to complete + setTimeout(function(){ + // Result should be [0, 2] since `thenable` will be called async. + is(results[0], 0, "Expected thenable to be called asynchronously"); + // See Bug 1023547 comment 13 for why this check has to be gated on p. + p.then(function() { + results.push(x); + is(results[1], 2, "Expected thenable to be called asynchronously"); + runTest(); + }); + },1000); +} + +// Bug 1008467 - Promise fails with "too much recursion". +// The bug was that the callbacks passed to a thenable would resolve the +// promise synchronously when the fulfill handler returned a non-thenable. +// +// For example: +// var p = new Promise(function(resolve) { +// resolve(5); +// }); +// var m = Promise.resolve(p); +// +// At this point `m` is a Promise that is resolved with a thenable `p`, so it +// calls `p.then()` with two callbacks, both of which would synchronously resolve +// `m` when `p` invoked them (on account of itself being resolved, possibly +// synchronously. A chain of these 'Promise resolved by a Promise' would lead to +// stack overflow. +function promiseTestAsyncThenableResolution() +{ + var k = 3000; + Promise.resolve().then(function next() { + k--; + if (k > 0) return Promise.resolve().then(next); + }).then(function () { + ok(true, "Resolution of a chain of thenables should not be synchronous."); + runTest(); + }); +} + +// Bug 1062323 +function promiseWrapperAsyncResolution() +{ + var p = new Promise(function(resolve, reject){ + resolve(); + }); + + var results = []; + var q = p.then(function () { + results.push("1-1"); + }).then(function () { + results.push("1-2"); + }).then(function () { + results.push("1-3"); + }); + + var r = p.then(function () { + results.push("2-1"); + }).then(function () { + results.push("2-2"); + }).then(function () { + results.push("2-3"); + }); + + Promise.all([q, r]).then(function() { + var match = results[0] == "1-1" && + results[1] == "2-1" && + results[2] == "1-2" && + results[3] == "2-2" && + results[4] == "1-3" && + results[5] == "2-3"; + info(results); + ok(match, "Chained promises should resolve asynchronously."); + runTest(); + }, function() { + ok(false, "promiseWrapperAsyncResolution: One of the promises failed."); + runTest(); + }); +} + +var tests = [ promiseResolve, promiseReject, + promiseException, promiseGC, + promiseAsync_TimeoutResolveThen, + promiseAsync_ResolveTimeoutThen, + promiseAsync_ResolveThenTimeout, + promiseAsync_SyncXHR, + promiseDoubleThen, promiseThenException, + promiseThenCatchThen, promiseRejectThenCatchThen, + promiseRejectThenCatchThen2, + promiseRejectThenCatchExceptionThen, + promiseThenCatchOrderingResolve, + promiseThenCatchOrderingReject, + promiseNestedPromise, promiseNestedNestedPromise, + promiseWrongNestedPromise, promiseLoop, + promiseStaticReject, promiseStaticResolve, + promiseResolveNestedPromise, + promiseResolveNoArg, + promiseRejectNoArg, + promiseThenNoArg, + promiseThenUndefinedResolveFunction, + promiseThenNullResolveFunction, + promiseCatchNoArg, + promiseRejectNoHandler, + promiseSimpleThenableResolve, + promiseSimpleThenableReject, + promiseThenableThrowsBeforeCallback, + promiseThenableThrowsAfterCallback, + promiseThenableRejectThenResolve, + promiseWithThenReplaced, + promiseStrictHandlers, + promiseStrictExecutorThisArg, + promiseResolveArray, + promiseResolveThenable, + promiseResolvePromise, + promiseResolveThenableCleanStack, + promiseTestAsyncThenableResolution, + promiseWrapperAsyncResolution, + ]; + +function runTest() { + if (!tests.length) { + SimpleTest.finish(); + return; + } + + var test = tests.shift(); + test(); +} + +SimpleTest.waitForExplicitFinish(); +SimpleTest.requestFlakyTimeout("untriaged"); +runTest(); +// --> +</script> +</pre> +</body> +</html> + diff --git a/dom/promise/tests/test_promise_and_timeout_ordering.html b/dom/promise/tests/test_promise_and_timeout_ordering.html new file mode 100644 index 000000000..1dfa13bbb --- /dev/null +++ b/dom/promise/tests/test_promise_and_timeout_ordering.html @@ -0,0 +1,15 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<title>Test for promise and timeout ordering</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<div id="log"></div> +<script> +var t = async_test("Promise callbacks should run immediately after the setTimeout handler that enqueues them"); +var origPostMessage = window.postMessage; +window.postMessage = function(msg) { origPostMessage.call(window, msg, "*"); } +window.onmessage = t.step_func_done(function(e) { + assert_equals(e.data, "t1start, t1end, promise, t2"); +}); +</script> +<script src="file_promise_and_timeout_ordering.js"></script> diff --git a/dom/promise/tests/test_promise_and_timeout_ordering_workers.html b/dom/promise/tests/test_promise_and_timeout_ordering_workers.html new file mode 100644 index 000000000..122a794db --- /dev/null +++ b/dom/promise/tests/test_promise_and_timeout_ordering_workers.html @@ -0,0 +1,13 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<title>Test for promise and timeout ordering in workers</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<div id="log"></div> +<script> +var t = async_test("Promise callbacks in workers should run immediately after the setTimeout handler that enqueues them"); +var w = new Worker("file_promise_and_timeout_ordering.js"); +w.onmessage = t.step_func_done(function(e) { + assert_equals(e.data, "t1start, t1end, promise, t2"); +}); +</script> diff --git a/dom/promise/tests/test_promise_uncatchable_exception.html b/dom/promise/tests/test_promise_uncatchable_exception.html new file mode 100644 index 000000000..8f7167d56 --- /dev/null +++ b/dom/promise/tests/test_promise_uncatchable_exception.html @@ -0,0 +1,35 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>Promise - uncatchable exceptions</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"> + +onmessage = function(evt) { + ok(true, "finished"); + SimpleTest.finish(); +} + +function go() { + var script = document.createElement("script"); + script.src = "promise_uncatchable_exception.js"; + document.body.appendChild(script); +} + +SimpleTest.waitForExplicitFinish(); +SpecialPowers.pushPrefEnv({set: [['dom.expose_test_interfaces', true]]}, go); +</script> +</pre> +</body> +</html> diff --git a/dom/promise/tests/test_promise_utils.html b/dom/promise/tests/test_promise_utils.html new file mode 100644 index 000000000..316ea02a5 --- /dev/null +++ b/dom/promise/tests/test_promise_utils.html @@ -0,0 +1,320 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>Test for Promise.all, Promise.race</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"><!-- + +function promiseUtilitiesDefined() { + ok(Promise.all, "Promise.all must be defined when Promise is enabled."); + ok(Promise.race, "Promise.race must be defined when Promise is enabled."); + runTest(); +} + +function promiseAllEmptyArray() { + var p = Promise.all([]); + ok(p instanceof Promise, "Return value of Promise.all should be a Promise."); + p.then(function(values) { + ok(Array.isArray(values), "Resolved value should be an array."); + is(values.length, 0, "Resolved array length should match iterable's length."); + runTest(); + }, function() { + ok(false, "Promise.all shouldn't fail when iterable has no rejected Promises."); + runTest(); + }); +} + +function promiseAllArray() { + var p = Promise.all([1, new Date(), Promise.resolve("firefox")]); + ok(p instanceof Promise, "Return value of Promise.all should be a Promise."); + p.then(function(values) { + ok(Array.isArray(values), "Resolved value should be an array."); + is(values.length, 3, "Resolved array length should match iterable's length."); + is(values[0], 1, "Array values should match."); + ok(values[1] instanceof Date, "Array values should match."); + is(values[2], "firefox", "Array values should match."); + runTest(); + }, function() { + ok(false, "Promise.all shouldn't fail when iterable has no rejected Promises."); + runTest(); + }); +} + +function promiseAllIterable() { + function* promiseGen() { + var i = 3; + while (--i) { + yield Promise.resolve(i); + } + + yield new Promise(function(resolve) { + setTimeout(resolve, 10); + }); + } + + Promise.all(promiseGen()).then(function(values) { + is(values.length, 3, "Resolved array length should match iterable's length."); + is(values[0], 2, "Array values should match."); + is(values[1], 1, "Array values should match."); + is(values[2], undefined, "Array values should match."); + runTest(); + }, function(e) { + ok(false, "Promise.all shouldn't fail when an iterable is passed."); + runTest(); + }); +} + +function promiseAllWaitsForAllPromises() { + var arr = [ + new Promise(function(resolve) { + setTimeout(resolve.bind(undefined, 1), 50); + }), + new Promise(function(resolve) { + setTimeout(resolve.bind(undefined, 2), 10); + }), + new Promise(function(resolve) { + setTimeout(resolve.bind(undefined, new Promise(function(resolve2) { + resolve2(3); + })), 10); + }), + new Promise(function(resolve) { + setTimeout(resolve.bind(undefined, 4), 20); + }) + ]; + + var p = Promise.all(arr); + p.then(function(values) { + ok(Array.isArray(values), "Resolved value should be an array."); + is(values.length, 4, "Resolved array length should match iterable's length."); + is(values[0], 1, "Array values should match."); + is(values[1], 2, "Array values should match."); + is(values[2], 3, "Array values should match."); + is(values[3], 4, "Array values should match."); + runTest(); + }, function() { + ok(false, "Promise.all shouldn't fail when iterable has no rejected Promises."); + runTest(); + }); +} + +function promiseAllRejectFails() { + var arr = [ + new Promise(function(resolve) { + setTimeout(resolve.bind(undefined, 1), 50); + }), + new Promise(function(resolve, reject) { + setTimeout(reject.bind(undefined, 2), 10); + }), + new Promise(function(resolve) { + setTimeout(resolve.bind(undefined, 3), 10); + }), + new Promise(function(resolve) { + setTimeout(resolve.bind(undefined, 4), 20); + }) + ]; + + var p = Promise.all(arr); + p.then(function(values) { + ok(false, "Promise.all shouldn't resolve when iterable has rejected Promises."); + runTest(); + }, function(e) { + ok(true, "Promise.all should reject when iterable has rejected Promises."); + is(e, 2, "Rejection value should match."); + runTest(); + }); +} + +function promiseAllCastError() { + var p = Promise.all([Promise.resolve(2), { then: function() { foo(); } }]); + ok(p instanceof Promise, "Should cast to a Promise."); + p.then(function(v) { + ok(false, "promiseAllCastError: should've rejected."); + runTest(); + }, function(e) { + ok(e instanceof ReferenceError, "promiseCastThenableError"); + runTest(); + }); +} + +// Check that the resolved array is enumerable. +function promiseAllEnumerable() { + var p = Promise.all([1, new Date(), Promise.resolve("firefox")]); + p.then(function(v) { + var count = 0; + for (key in v) { + ++count; + ok(v[key] === 1 || v[key] instanceof Date || v[key] === "firefox", + "Enumerated properties don't match."); + } + is(count, 3, "Resolved array from Promise.all should be enumerable"); + runTest(); + }, function(e) { + ok(false, "promiseAllEnumerable: should've resolved."); + runTest(); + }); +} + +function promiseRaceEmpty() { + var p = Promise.race([]); + ok(p instanceof Promise, "Should return a Promise."); + p.then(function() { + ok(false, "Should not resolve"); + }, function() { + ok(false, "Should not reject"); + }); + // Per spec, An empty race never resolves or rejects. + setTimeout(function() { + ok(true); + runTest(); + }, 50); +} + +function promiseRaceValuesArray() { + var p = Promise.race([true, new Date(), 3]); + ok(p instanceof Promise, "Should return a Promise."); + p.then(function(winner) { + is(winner, true, "First value should win."); + runTest(); + }, function(err) { + ok(false, "Should not fail " + err + "."); + runTest(); + }); +} + +function promiseRacePromiseArray() { + function timeoutPromise(n) { + return new Promise(function(resolve) { + setTimeout(function() { + resolve(n); + }, n); + }); + } + + var arr = [ + new Promise(function(resolve) { + resolve("first"); + }), + Promise.resolve("second"), + new Promise(function() {}), + new Promise(function(resolve) { + setTimeout(function() { + setTimeout(function() { + resolve("fourth"); + }, 0); + }, 0); + }), + ]; + + var p = Promise.race(arr); + p.then(function(winner) { + is(winner, "first", "First queued resolution should win the race."); + runTest(); + }); +} + +function promiseRaceIterable() { + function* participants() { + yield new Promise(function(resolve) { + setTimeout(resolve, 10, 10); + }); + yield new Promise(function(resolve) { + setTimeout(resolve, 20, 20); + }); + } + + Promise.race(participants()).then(function(winner) { + is(winner, 10, "Winner should be the one that finished earlier."); + runTest(); + }, function(e) { + ok(false, "Promise.race shouldn't throw when an iterable is passed!"); + runTest(); + }); +} + +function promiseRaceReject() { + var p = Promise.race([ + Promise.reject(new Error("Fail bad!")), + new Promise(function(resolve) { + setTimeout(resolve, 0); + }) + ]); + + p.then(function() { + ok(false, "Should not resolve when winning Promise rejected."); + runTest(); + }, function(e) { + ok(true, "Should be rejected"); + ok(e instanceof Error, "Should reject with Error."); + ok(e.message == "Fail bad!", "Message should match."); + runTest(); + }); +} + +function promiseRaceThrow() { + var p = Promise.race([ + new Promise(function(resolve) { + nonExistent(); + }), + new Promise(function(resolve) { + setTimeout(resolve, 0); + }) + ]); + + p.then(function() { + ok(false, "Should not resolve when winning Promise had an error."); + runTest(); + }, function(e) { + ok(true, "Should be rejected"); + ok(e instanceof ReferenceError, "Should reject with ReferenceError for function nonExistent()."); + runTest(); + }); +} + +var tests = [ + promiseUtilitiesDefined, + promiseAllEmptyArray, + promiseAllArray, + promiseAllIterable, + promiseAllWaitsForAllPromises, + promiseAllRejectFails, + promiseAllCastError, + promiseAllEnumerable, + + promiseRaceEmpty, + promiseRaceValuesArray, + promiseRacePromiseArray, + promiseRaceIterable, + promiseRaceReject, + promiseRaceThrow, + ]; + +function runTest() { + if (!tests.length) { + SimpleTest.finish(); + return; + } + + var test = tests.shift(); + test(); +} + +SimpleTest.waitForExplicitFinish(); +SimpleTest.requestFlakyTimeout("untriaged"); +runTest(); +// --> +</script> +</pre> +</body> +</html> + diff --git a/dom/promise/tests/test_promise_xrays.html b/dom/promise/tests/test_promise_xrays.html new file mode 100644 index 000000000..55398167e --- /dev/null +++ b/dom/promise/tests/test_promise_xrays.html @@ -0,0 +1,365 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1170760 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 1170760</title> + <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://global/skin"/> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1170760">Mozilla Bug 1170760</a> +<p id="display"></p> +<div id="content" style="display: none"> +<iframe id="t" src="http://example.org/chrome/dom/promise/tests/file_promise_xrays.html"></iframe> +</div> + +<pre id="test"> +<script type="application/javascript"> + +var win = $("t").contentWindow; + +/** Test for Bug 1170760 **/ +SimpleTest.waitForExplicitFinish(); + +function testLoadComplete() { + is(win.location.href, $("t").src, "Should have loaded the right thing"); + nextTest(); +} + +function testHaveXray() { + is(typeof win.Promise.race, "function", "Should see a race() function"); + var exception; + try { + win.Promise.wrappedJSObject.race; + } catch (e) { + exception = e; + } + is(exception, "Getting race", "Should have thrown the right exception"); + is(win.wrappedJSObject.setupThrew, false, "Setup should not have thrown"); + nextTest(); +} + +function testConstructor1() { + var p = new win.Promise(function(resolve, reject) { resolve(win.Promise.resolve(5)); }); + p.then( + function(arg) { + is(arg, 5, "Content Promise constructor resolved with content promise should work"); + }, + function(e) { + ok(false, "Content Promise constructor resolved with content promise should not fail"); + } + ).then(nextTest); +} + +function testConstructor2() { + var p = new win.Promise(function(resolve, reject) { resolve(Promise.resolve(5)); }); + p.then( + function(arg) { + is(arg, 5, "Content Promise constructor resolved with chrome promise should work"); + }, + function(e) { + ok(false, "Content Promise constructor resolved with chrome promise should not fail"); + } + ).then(nextTest); +} + +function testRace1() { + var p = win.Promise.race(new win.Array(1, 2)); + p.then( + function(arg) { + ok(arg == 1 || arg == 2, + "Should get the right value when racing content-side array"); + }, + function(e) { + ok(false, "testRace1 threw exception: " + e); + } + ).then(nextTest); +} + +function testRace2() { + var p = win.Promise.race( + new Array(win.Promise.resolve(1), win.Promise.resolve(2))); + p.then( + function(arg) { + ok(arg == 1 || arg == 2, + "Should get the right value when racing content-side array of explicit Promises"); + }, + function(e) { + ok(false, "testRace2 threw exception: " + e); + } + ).then(nextTest); +} + +function testRace3() { + // This works with a chrome-side array because we do the iteration + // while still in the Xray compartment. + var p = win.Promise.race([1, 2]); + p.then( + function(arg) { + ok(arg == 1 || arg == 2, + "Should get the right value when racing chrome-side array"); + }, + function(e) { + ok(false, "testRace3 threw exception: " + e); + } + ).then(nextTest); +} + +function testRace4() { + // 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.race([Promise.resolve(1), win.Promise.resolve(2)]); + p.then( + function(arg) { + ok(arg == 1 || arg == 2, + "Should get the right value when racing chrome-side promises"); + }, + function(e) { + ok(false, "testRace4 threw exception: " + e); + } + ).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); +} + +function testResolve1() { + var p = win.Promise.resolve(5); + ok(p instanceof win.Promise, "Promise.resolve should return a promise"); + p.then( + function(arg) { + is(arg, 5, "Should get correct Promise.resolve value"); + }, + function(e) { + ok(false, "testAll5 threw exception: " + e); + } + ).then(nextTest); +} + +function testResolve2() { + var p = win.Promise.resolve(5); + var q = win.Promise.resolve(p); + is(q, p, "Promise.resolve should just pass through Promise values"); + nextTest(); +} + +function testResolve3() { + var p = win.Promise.resolve(Promise.resolve(5)); + p.then( + function(arg) { + is(arg, 5, "Promise.resolve with chrome Promise should work"); + }, + function(e) { + ok(false, "Promise.resolve with chrome Promise should not fail"); + } + ).then(nextTest); +} + +function testResolve4() { + var p = new win.Promise((res, rej) => {}); + Components.utils.getJSTestingFunctions().resolvePromise(p, 42); + p.then( + function(arg) { + is(arg, 42, "Resolving an Xray to a promise with TestingFunctions resolvePromise should work"); + }, + function(e) { + ok(false, "Resolving an Xray to a promise with TestingFunctions resolvePromise should not fail"); + } + ).then(nextTest); +} + +function testReject1() { + var p = win.Promise.reject(5); + ok(p instanceof win.Promise, "Promise.reject should return a promise"); + p.then( + function(arg) { + ok(false, "Promise should be rejected"); + }, + function(e) { + is(e, 5, "Should get correct Promise.reject value"); + } + ).then(nextTest); +} + +function testReject2() { + var p = new win.Promise((res, rej) => {}); + Components.utils.getJSTestingFunctions().rejectPromise(p, 42); + p.then( + function(arg) { + ok(false, "Rejecting an Xray to a promise with TestingFunctions rejectPromise should trigger catch handler"); + }, + function(e) { + is(e, 42, "Rejecting an Xray to a promise with TestingFunctions rejectPromise should work"); + } + ).then(nextTest); +} + +function testThen1() { + var p = win.Promise.resolve(5); + var q = p.then((x) => x*x); + ok(q instanceof win.Promise, + "Promise.then should return a promise from the right global"); + q.then( + function(arg) { + is(arg, 25, "Promise.then should work"); + }, + function(e) { + ok(false, "Promise.then should not fail"); + } + ).then(nextTest); +} + +function testThen2() { + var p = win.Promise.resolve(5); + var q = p.then((x) => Promise.resolve(x*x)); + ok(q instanceof win.Promise, + "Promise.then should return a promise from the right global"); + q.then( + function(arg) { + is(arg, 25, "Promise.then resolved with chrome promise should work"); + }, + function(e) { + ok(false, "Promise.then resolved with chrome promise should not fail"); + } + ).then(nextTest); +} + +function testCatch1() { + var p = win.Promise.reject(5); + ok(p instanceof win.Promise, "Promise.reject should return a promise"); + var q = p.catch((x) => x*x); + ok(q instanceof win.Promise, + "Promise.catch should return a promise from the right global"); + q.then( + function(arg) { + is(arg, 25, "Promise.catch should work"); + }, + function(e) { + ok(false, "Promise.catch should not fail"); + } + ).then(nextTest); +} + +function testToStringTag1() { + is(win.Promise.prototype[Symbol.toStringTag], "Promise", "@@toStringTag was incorrect"); + var p = win.Promise.resolve(); + is(String(p), "[object Promise]", "String() result was incorrect"); + is(p.toString(), "[object Promise]", "toString result was incorrect"); + is(Object.prototype.toString.call(p), "[object Promise]", "second toString result was incorrect"); + nextTest(); +} + +var tests = [ + testLoadComplete, + testHaveXray, + testConstructor1, + testConstructor2, + testRace1, + testRace2, + testRace3, + testRace4, + testAll1, + testAll2, + testAll3, + testAll4, + testAll5, + testResolve1, + testResolve2, + testResolve3, + testResolve4, + testReject1, + testReject2, + testThen1, + testThen2, + testCatch1, + testToStringTag1, +]; + +function nextTest() { + if (tests.length == 0) { + SimpleTest.finish(); + return; + } + tests.shift()(); +} + +addLoadEvent(nextTest); + +</script> +</pre> +</body> +</html> diff --git a/dom/promise/tests/test_resolve.html b/dom/promise/tests/test_resolve.html new file mode 100644 index 000000000..780eae6c6 --- /dev/null +++ b/dom/promise/tests/test_resolve.html @@ -0,0 +1,67 @@ +<!-- + 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"><!-- + +var tests = [ + null, + 42, + "hello world", + true, + false, + {}, + { a: 42 }, + [ 1, 2, 3, 4, null, true, "hello world" ], + function() {}, + window, + undefined, + document.createElement('input'), + new Date(), +]; + +function cbError() { + ok(false, "Nothing should arrive here!"); +} + +function runTest() { + if (!tests.length) { + SimpleTest.finish(); + return; + } + + var test = tests.pop(); + + new Promise(function(resolve, reject) { + resolve(test); + }).then(function(what) { + ok(test === what, "What is: " + what); + }, cbError).then(function() { + new Promise(function(resolve, reject) { + reject(test) + }).then(cbError, function(what) { + ok(test === what, "What is: " + what); + }).then(runTest, cbError); + }); +} + +SimpleTest.waitForExplicitFinish(); +runTest(); +// --> +</script> +</pre> +</body> +</html> + diff --git a/dom/promise/tests/test_resolver_return_value.html b/dom/promise/tests/test_resolver_return_value.html new file mode 100644 index 000000000..1fb9652ac --- /dev/null +++ b/dom/promise/tests/test_resolver_return_value.html @@ -0,0 +1,40 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1120235 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 1120235</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="application/javascript"> + + /** Test for Bug 1120235 **/ + var res, rej; + var p = new Promise(function(resolve, reject) { res = resolve; rej = reject; }); + is(res(1), undefined, "Resolve function should return undefined"); + is(rej(2), undefined, "Reject function should return undefined"); + + var thenable = { + then: function(resolve, reject) { + is(resolve(3), undefined, "Thenable resolve argument should return undefined"); + is(reject(4), undefined, "Thenable reject argument should return undefined"); + SimpleTest.finish(); + } + }; + + SimpleTest.waitForExplicitFinish(); + p = Promise.resolve(thenable); + </script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1120235">Mozilla Bug 1120235</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> +</body> +</html> diff --git a/dom/promise/tests/test_species_getter.html b/dom/promise/tests/test_species_getter.html new file mode 100644 index 000000000..04e590a13 --- /dev/null +++ b/dom/promise/tests/test_species_getter.html @@ -0,0 +1,24 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<title>Test for ...</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<div id="log"></div> +<script> +test(function() { + var desc = Object.getOwnPropertyDescriptor(Promise, Symbol.species); + assert_not_equals(desc, undefined, "Should have a property"); + + assert_equals(desc.configurable, true, "Property should be configurable"); + assert_equals(desc.enumerable, false, "Property should not be enumerable"); + assert_equals(desc.set, undefined, "Should not have a setter"); + var getter = desc.get; + + var things = [undefined, null, 5, "xyz", Promise, Object]; + for (var thing of things) { + assert_equals(getter.call(thing), thing, + "Getter should return its this value"); + } + +}, "Promise should have an @@species getter that works per spec"); +</script> diff --git a/dom/promise/tests/test_thenable_vs_promise_ordering.html b/dom/promise/tests/test_thenable_vs_promise_ordering.html new file mode 100644 index 000000000..161e95d75 --- /dev/null +++ b/dom/promise/tests/test_thenable_vs_promise_ordering.html @@ -0,0 +1,27 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<title>Test for promise resolution ordering with thenables and promises</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<div id="log"></div> +<script> +var t = async_test("A promise resolved first (with a thenable) should trigger its callbacks before a promise resolved second (with a promise)."); +t.step(function() { + var customThenCalled = false; + var p0 = Promise.resolve(); + p0.then = function(resolved, rejected) { + customThenCalled = true; + Promise.prototype.then.call(this, resolved, rejected); + } + var p1 = new Promise(function(r) { r(p0); }); + delete p0.then; + var p2 = new Promise(function(r) { r(p0); }); + var resolutionOrder = ""; + Promise.all([ p1.then(function() { resolutionOrder += "1"; }), + p2.then(function() { resolutionOrder += "2"; }) ]) + .then(t.step_func_done(function() { + assert_true(customThenCalled, "Should have called custom then"); + assert_equals(resolutionOrder, "12"); + })); +}); +</script> diff --git a/dom/promise/tests/test_webassembly_compile.html b/dom/promise/tests/test_webassembly_compile.html new file mode 100644 index 000000000..0243df49c --- /dev/null +++ b/dom/promise/tests/test_webassembly_compile.html @@ -0,0 +1,174 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>WebAssembly.compile 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> +<script> +const wasmTextToBinary = SpecialPowers.unwrap(SpecialPowers.Cu.getJSTestingFunctions().wasmTextToBinary); +const wasmIsSupported = SpecialPowers.Cu.getJSTestingFunctions().wasmIsSupported +const fooModuleCode = wasmTextToBinary(`(module + (func $foo (result i32) (i32.const 42)) + (export "foo" $foo) +)`, 'new-format'); + +function checkFooModule(m) { + ok(m instanceof WebAssembly.Module, "got a module"); + var i = new WebAssembly.Instance(m); + ok(i instanceof WebAssembly.Instance, "got an instance"); + ok(i.exports.foo() === 42, "got 42"); +} + +function checkFooInstance(i) { + ok(i instanceof WebAssembly.Instance, "got a module"); + ok(i.exports.foo() === 42, "got 42"); +} + +function propertiesExist() { + if (!wasmIsSupported()) { + ok(!this["WebAssembly"], "If the device doesn't support, there will be no WebAssembly object"); + SimpleTest.finish(); + return; + } + + ok(WebAssembly, "WebAssembly object should exist"); + ok(WebAssembly.compile, "WebAssembly.compile function should exist"); + runTest(); +} + +function compileFail() { + WebAssembly.compile().then( + () => { ok(false, "should have failed"); runTest() } + ).catch( + err => { ok(err instanceof TypeError, "empty compile failed"); runTest() } + ); +} + +function compileSuccess() { + WebAssembly.compile(fooModuleCode).then( + m => { checkFooModule(m); runTest() } + ).catch( + err => { ok(false, String(err)); runTest() } + ); +} + +function compileManySuccess() { + const N = 100; + + var arr = []; + for (var i = 0; i < N; i++) + arr.push(WebAssembly.compile(fooModuleCode)); + + SpecialPowers.gc(); + + Promise.all(arr).then (ms => { + ok(ms.length === N, "got the right number"); + for (var i = 0; i < N; i++) + checkFooModule(ms[i]); + runTest(); + }).catch( + err => { ok(false, String(err)); runTest() } + ); +} + +function compileInWorker() { + var w = new Worker(`data:text/plain, + onmessage = e => { + WebAssembly.compile(e.data).then(m => { + var i = new WebAssembly.Instance(m); + if (i.exports.foo() !== 42) + throw "bad i.exports.foo() result"; + postMessage("ok"); + close(); + }).catch(err => { throw err }); + } + `); + w.postMessage(fooModuleCode); + w.onmessage = e => { + ok(e.data === "ok", "worker test"); + runTest(); + } +} + +function terminateCompileInWorker() { + var w = new Worker(`data:text/plain, + var fooModuleCode; + function spawnWork() { + const N = 100; + var arr = []; + for (var i = 0; i < N; i++) + arr.push(WebAssembly.compile(fooModuleCode)); + Promise.all(arr).then(spawnWork); + } + onmessage = e => { + fooModuleCode = e.data; + spawnWork(); + postMessage("ok"); + } + `); + w.postMessage(fooModuleCode); + w.onmessage = e => { + ok(e.data === "ok", "worker finished first step"); + w.terminate(); + runTest(); + } +} + +function instantiateFail() { + WebAssembly.instantiate().then( + () => { ok(false, "should have failed"); runTest() } + ).catch( + err => { ok(err instanceof TypeError, "empty compile failed"); runTest() } + ); +} + +function instantiateSuccess() { + WebAssembly.instantiate(fooModuleCode).then( + r => { checkFooModule(r.module); checkFooInstance(r.instance); runTest() } + ).catch( + err => { ok(false, String(err)); runTest() } + ); +} + +function chainSuccess() { + WebAssembly.compile(fooModuleCode).then( + m => WebAssembly.instantiate(m) + ).then( + i => { checkFooInstance(i); runTest() } + ).catch( + err => { ok(false, String(err)); runTest() } + ); +} + +var tests = [ propertiesExist, + compileFail, + compileSuccess, + compileManySuccess, + compileInWorker, + terminateCompileInWorker, + instantiateFail, + instantiateSuccess, + chainSuccess + ]; + +function runTest() { + if (!tests.length) { + SimpleTest.finish(); + return; + } + + var test = tests.shift(); + test(); +} + +SimpleTest.waitForExplicitFinish(); +SpecialPowers.pushPrefEnv({"set": [["javascript.options.wasm", true]]}, runTest); +</script> +</body> +</html> + diff --git a/dom/promise/tests/unit/test_monitor_uncaught.js b/dom/promise/tests/unit/test_monitor_uncaught.js new file mode 100644 index 000000000..7dd80d212 --- /dev/null +++ b/dom/promise/tests/unit/test_monitor_uncaught.js @@ -0,0 +1,274 @@ +/* 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/. */ + +"use strict"; + +var { utils: Cu } = Components; + +Cu.import("resource://gre/modules/Timer.jsm", this); +Cu.import("resource://testing-common/PromiseTestUtils.jsm", this); + +// Prevent test failures due to the unhandled rejections in this test file. +PromiseTestUtils.disableUncaughtRejectionObserverForSelfTest(); + +add_task(function* test_globals() { + Assert.equal(Promise.defer || undefined, undefined, "We are testing DOM Promise."); + Assert.notEqual(PromiseDebugging, undefined, "PromiseDebugging is available."); +}); + +add_task(function* test_promiseID() { + let p1 = new Promise(resolve => {}); + let p2 = new Promise(resolve => {}); + let p3 = p2.then(null, null); + let promise = [p1, p2, p3]; + + let identifiers = promise.map(PromiseDebugging.getPromiseID); + do_print("Identifiers: " + JSON.stringify(identifiers)); + let idSet = new Set(identifiers); + Assert.equal(idSet.size, identifiers.length, + "PromiseDebugging.getPromiseID returns a distinct id per promise"); + + let identifiers2 = promise.map(PromiseDebugging.getPromiseID); + Assert.equal(JSON.stringify(identifiers), + JSON.stringify(identifiers2), + "Successive calls to PromiseDebugging.getPromiseID return the same id for the same promise"); +}); + +add_task(function* test_observe_uncaught() { + // The names of Promise instances + let names = new Map(); + + // The results for UncaughtPromiseObserver callbacks. + let CallbackResults = function(name) { + this.name = name; + this.expected = new Set(); + this.observed = new Set(); + this.blocker = new Promise(resolve => this.resolve = resolve); + }; + CallbackResults.prototype = { + observe: function(promise) { + do_print(this.name + " observing Promise " + names.get(promise)); + Assert.equal(PromiseDebugging.getState(promise).state, "rejected", + this.name + " observed a rejected Promise"); + if (!this.expected.has(promise)) { + Assert.ok(false, + this.name + " observed a Promise that it expected to observe, " + + names.get(promise) + + " (" + PromiseDebugging.getPromiseID(promise) + + ", " + PromiseDebugging.getAllocationStack(promise) + ")"); + + } + Assert.ok(this.expected.delete(promise), + this.name + " observed a Promise that it expected to observe, " + + names.get(promise) + " (" + PromiseDebugging.getPromiseID(promise) + ")"); + Assert.ok(!this.observed.has(promise), + this.name + " observed a Promise that it has not observed yet"); + this.observed.add(promise); + if (this.expected.size == 0) { + this.resolve(); + } else { + do_print(this.name + " is still waiting for " + this.expected.size + " observations:"); + do_print(JSON.stringify(Array.from(this.expected.values(), (x) => names.get(x)))); + } + }, + }; + + let onLeftUncaught = new CallbackResults("onLeftUncaught"); + let onConsumed = new CallbackResults("onConsumed"); + + let observer = { + onLeftUncaught: function(promise, data) { + onLeftUncaught.observe(promise); + }, + onConsumed: function(promise) { + onConsumed.observe(promise); + }, + }; + + let resolveLater = function(delay = 20) { + return new Promise((resolve, reject) => setTimeout(resolve, delay)); + }; + let rejectLater = function(delay = 20) { + return new Promise((resolve, reject) => setTimeout(reject, delay)); + }; + let makeSamples = function*() { + yield { + promise: Promise.resolve(0), + name: "Promise.resolve", + }; + yield { + promise: Promise.resolve(resolve => resolve(0)), + name: "Resolution callback", + }; + yield { + promise: Promise.resolve(0).then(null, null), + name: "`then(null, null)`" + }; + yield { + promise: Promise.reject(0).then(null, () => {}), + name: "Reject and catch immediately", + }; + yield { + promise: resolveLater(), + name: "Resolve later", + }; + yield { + promise: Promise.reject("Simple rejection"), + leftUncaught: true, + consumed: false, + name: "Promise.reject", + }; + + // Reject a promise now, consume it later. + let p = Promise.reject("Reject now, consume later"); + setTimeout(() => p.then(null, () => { + do_print("Consumed promise"); + }), 200); + yield { + promise: p, + leftUncaught: true, + consumed: true, + name: "Reject now, consume later", + }; + + yield { + promise: Promise.all([ + Promise.resolve("Promise.all"), + rejectLater() + ]), + leftUncaught: true, + name: "Rejecting through Promise.all" + }; + yield { + promise: Promise.race([ + resolveLater(500), + Promise.reject(), + ]), + leftUncaught: true, // The rejection wins the race. + name: "Rejecting through Promise.race", + }; + yield { + promise: Promise.race([ + Promise.resolve(), + rejectLater(500) + ]), + leftUncaught: false, // The resolution wins the race. + name: "Resolving through Promise.race", + }; + + let boom = new Error("`throw` in the constructor"); + yield { + promise: new Promise(() => { throw boom; }), + leftUncaught: true, + name: "Throwing in the constructor", + }; + + let rejection = Promise.reject("`reject` during resolution"); + yield { + promise: rejection, + leftUncaught: false, + consumed: false, // `rejection` is consumed immediately (see below) + name: "Promise.reject, again", + }; + + yield { + promise: new Promise(resolve => resolve(rejection)), + leftUncaught: true, + consumed: false, + name: "Resolving with a rejected promise", + }; + + yield { + promise: Promise.resolve(0).then(() => rejection), + leftUncaught: true, + consumed: false, + name: "Returning a rejected promise from success handler", + }; + + yield { + promise: Promise.resolve(0).then(() => { throw new Error(); }), + leftUncaught: true, + consumed: false, + name: "Throwing during the call to the success callback", + }; + }; + let samples = []; + for (let s of makeSamples()) { + samples.push(s); + do_print("Promise '" + s.name + "' has id " + PromiseDebugging.getPromiseID(s.promise)); + } + + PromiseDebugging.addUncaughtRejectionObserver(observer); + + for (let s of samples) { + names.set(s.promise, s.name); + if (s.leftUncaught || false) { + onLeftUncaught.expected.add(s.promise); + } + if (s.consumed || false) { + onConsumed.expected.add(s.promise); + } + } + + do_print("Test setup, waiting for callbacks."); + yield onLeftUncaught.blocker; + + do_print("All calls to onLeftUncaught are complete."); + if (onConsumed.expected.size != 0) { + do_print("onConsumed is still waiting for the following Promise:"); + do_print(JSON.stringify(Array.from(onConsumed.expected.values(), (x) => names.get(x)))); + yield onConsumed.blocker; + } + + do_print("All calls to onConsumed are complete."); + let removed = PromiseDebugging.removeUncaughtRejectionObserver(observer); + Assert.ok(removed, "removeUncaughtRejectionObserver succeeded"); + removed = PromiseDebugging.removeUncaughtRejectionObserver(observer); + Assert.ok(!removed, "second call to removeUncaughtRejectionObserver didn't remove anything"); +}); + + +add_task(function* test_uninstall_observer() { + let Observer = function() { + this.blocker = new Promise(resolve => this.resolve = resolve); + this.active = true; + }; + Observer.prototype = { + set active(x) { + this._active = x; + if (x) { + PromiseDebugging.addUncaughtRejectionObserver(this); + } else { + PromiseDebugging.removeUncaughtRejectionObserver(this); + } + }, + onLeftUncaught: function() { + Assert.ok(this._active, "This observer is active."); + this.resolve(); + }, + onConsumed: function() { + Assert.ok(false, "We should not consume any Promise."); + }, + }; + + do_print("Adding an observer."); + let deactivate = new Observer(); + Promise.reject("I am an uncaught rejection."); + yield deactivate.blocker; + Assert.ok(true, "The observer has observed an uncaught Promise."); + deactivate.active = false; + do_print("Removing the observer, it should not observe any further uncaught Promise."); + + do_print("Rejecting a Promise and waiting a little to give a chance to observers."); + let wait = new Observer(); + Promise.reject("I am another uncaught rejection."); + yield wait.blocker; + yield new Promise(resolve => setTimeout(resolve, 100)); + // Normally, `deactivate` should not be notified of the uncaught rejection. + wait.active = false; +}); + +function run_test() { + run_next_test(); +} diff --git a/dom/promise/tests/unit/xpcshell.ini b/dom/promise/tests/unit/xpcshell.ini new file mode 100644 index 000000000..73df2380b --- /dev/null +++ b/dom/promise/tests/unit/xpcshell.ini @@ -0,0 +1,5 @@ +[DEFAULT] +head = +tail = + +[test_monitor_uncaught.js] |