/* -*- 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/CycleCollectedJSRuntime.h" #include "mozilla/OwningNonNull.h" #include "mozilla/Preferences.h" #include "mozilla/dom/BindingUtils.h" #include "mozilla/dom/DOMError.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 "xpcpublic.h" #ifdef MOZ_CRASHREPORTER #include "nsExceptionHandler.h" #endif namespace mozilla { namespace dom { namespace { // Generator used by Promise::GetID. Atomic gIDGenerator(0); } // namespace using namespace workers; // This class processes the promise's callbacks with promise's result. class PromiseReactionJob final : public nsRunnable { public: PromiseReactionJob(Promise* aPromise, PromiseCallback* aCallback, const JS::Value& aValue) : mPromise(aPromise) , mCallback(aCallback) , mValue(CycleCollectedJSRuntime::Get()->Runtime(), 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); ThreadsafeAutoJSContext cx; JS::Rooted wrapper(cx, mPromise->GetWrapper()); MOZ_ASSERT(wrapper); // It was preserved! JSAutoCompartment ac(cx, wrapper); JS::Rooted 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 asyncStack(cx, mPromise->mAllocationStack); JS::Rooted asyncCause(cx, JS_NewStringCopyZ(cx, "Promise")); if (!asyncCause) { JS_ClearPendingException(cx); return NS_ERROR_OUT_OF_MEMORY; } { Maybe sas; if (asyncStack) { sas.emplace(cx, asyncStack, asyncCause); } mCallback->Call(cx, value); } return NS_OK; } private: RefPtr mPromise; RefPtr mCallback; JS::PersistentRooted mValue; NS_DECL_OWNINGTHREAD; }; enum { SLOT_PROMISE = 0, SLOT_DATA }; /* * 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 aResolveFunc, JS::Handle aRejectFunc) { js::SetFunctionNativeReserved(aResolveFunc, SLOT_DATA, JS::ObjectValue(*aRejectFunc)); js::SetFunctionNativeReserved(aRejectFunc, SLOT_DATA, JS::ObjectValue(*aResolveFunc)); } /* * Returns false if callback was already called before, otherwise breaks the * links and returns true. */ bool MarkAsCalledIfNotCalledBefore(JSContext* aCx, JS::Handle aFunc) { JS::Value otherFuncVal = js::GetFunctionNativeReserved(aFunc, SLOT_DATA); if (!otherFuncVal.isObject()) { return false; } JSObject* otherFuncObj = &otherFuncVal.toObject(); MOZ_ASSERT(js::GetFunctionNativeReserved(otherFuncObj, SLOT_DATA).isObject()); // Break both references. js::SetFunctionNativeReserved(aFunc, SLOT_DATA, JS::UndefinedValue()); js::SetFunctionNativeReserved(otherFuncObj, SLOT_DATA, JS::UndefinedValue()); return true; } Promise* GetPromise(JSContext* aCx, JS::Handle aFunc) { JS::Value promiseVal = js::GetFunctionNativeReserved(aFunc, 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 nsRunnable { public: PromiseResolveThenableJob(Promise* aPromise, JS::Handle aThenable, PromiseInit* aThen) : mPromise(aPromise) , mThenable(CycleCollectedJSRuntime::Get()->Runtime(), 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); ThreadsafeAutoJSContext cx; JS::Rooted wrapper(cx, mPromise->GetWrapper()); MOZ_ASSERT(wrapper); // It was preserved! // 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. JSAutoCompartment ac(cx, wrapper); JS::Rooted resolveFunc(cx, mPromise->CreateThenableFunction(cx, mPromise, PromiseCallback::Resolve)); if (!resolveFunc) { mPromise->HandleException(cx); return NS_OK; } JS::Rooted rejectFunc(cx, mPromise->CreateThenableFunction(cx, mPromise, PromiseCallback::Reject)); if (!rejectFunc) { mPromise->HandleException(cx); return NS_OK; } LinkThenableCallables(cx, resolveFunc, rejectFunc); ErrorResult rv; JS::Rooted rootedThenable(cx, mThenable); mThen->Call(rootedThenable, resolveFunc, rejectFunc, rv, "promise thenable", CallbackObject::eRethrowExceptions, mPromise->Compartment()); rv.WouldReportJSException(); if (rv.Failed()) { JS::Rooted 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 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 mPromise; JS::PersistentRooted mThenable; RefPtr mThen; NS_DECL_OWNINGTHREAD; }; // Fast version of PromiseResolveThenableJob for use in the cases when we know we're // calling the canonical Promise.prototype.then on an actual DOM Promise. In // that case we can just bypass the jumping into and out of JS and call // AppendCallbacks on that promise directly. class FastPromiseResolveThenableJob final : public nsRunnable { public: FastPromiseResolveThenableJob(PromiseCallback* aResolveCallback, PromiseCallback* aRejectCallback, Promise* aNextPromise) : mResolveCallback(aResolveCallback) , mRejectCallback(aRejectCallback) , mNextPromise(aNextPromise) { MOZ_ASSERT(aResolveCallback); MOZ_ASSERT(aRejectCallback); MOZ_ASSERT(aNextPromise); MOZ_COUNT_CTOR(FastPromiseResolveThenableJob); } virtual ~FastPromiseResolveThenableJob() { NS_ASSERT_OWNINGTHREAD(FastPromiseResolveThenableJob); MOZ_COUNT_DTOR(FastPromiseResolveThenableJob); } protected: NS_IMETHOD Run() override { NS_ASSERT_OWNINGTHREAD(FastPromiseResolveThenableJob); mNextPromise->AppendCallbacks(mResolveCallback, mRejectCallback); return NS_OK; } private: RefPtr mResolveCallback; RefPtr mRejectCallback; RefPtr mNextPromise; }; // A struct implementing // . // 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 mPromise; // [[Resolve]]. Value in the context compartment. JS::Rooted mResolve; // [[Reject]]. Value in the context compartment. JS::Rooted mReject; // If mNativePromise is non-null, we should use it, not mPromise. RefPtr 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.isUndefined(), "NewPromiseCapability didn't succeed"); JS::Rooted 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 ignored(aCx); if (!JS::Call(aCx, JS::UndefinedHandleValue, mReject, JS::HandleValueArray(exn), &ignored)) { aRv.NoteJSContextException(); } } JS::Value Promise::PromiseCapability::PromiseValue() const { MOZ_ASSERT(mNativePromise || !mPromise.isUndefined(), "NewPromiseCapability didn't succeed"); if (mNativePromise) { return JS::ObjectValue(*mNativePromise->GetWrapper()); } return mPromise; } // Promise NS_IMPL_CYCLE_COLLECTION_CLASS(Promise) NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(Promise) #if defined(DOM_PROMISE_DEPRECATED_REPORTING) tmp->MaybeReportRejectedOnce(); #else tmp->mResult = JS::UndefinedValue(); #endif // defined(DOM_PROMISE_DEPRECATED_REPORTING) NS_IMPL_CYCLE_COLLECTION_UNLINK(mGlobal) NS_IMPL_CYCLE_COLLECTION_UNLINK(mResolveCallbacks) NS_IMPL_CYCLE_COLLECTION_UNLINK(mRejectCallbacks) NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER NS_IMPL_CYCLE_COLLECTION_UNLINK_END NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(Promise) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mGlobal) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mResolveCallbacks) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mRejectCallbacks) NS_IMPL_CYCLE_COLLECTION_TRAVERSE_SCRIPT_OBJECTS NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN(Promise) NS_IMPL_CYCLE_COLLECTION_TRACE_JSVAL_MEMBER_CALLBACK(mResult) NS_IMPL_CYCLE_COLLECTION_TRACE_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 NS_IMPL_CYCLE_COLLECTION_TRACE_END NS_IMPL_CYCLE_COLLECTION_CAN_SKIP_BEGIN(Promise) if (tmp->IsBlack()) { JS::ExposeValueToActiveJS(tmp->mResult); if (tmp->mAllocationStack) { JS::ExposeObjectToActiveJS(tmp->mAllocationStack); } if (tmp->mRejectionStack) { JS::ExposeObjectToActiveJS(tmp->mRejectionStack); } if (tmp->mFullfillmentStack) { JS::ExposeObjectToActiveJS(tmp->mFullfillmentStack); } 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 NS_IMPL_CYCLE_COLLECTING_ADDREF(Promise) NS_IMPL_CYCLE_COLLECTING_RELEASE(Promise) NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(Promise) NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY NS_INTERFACE_MAP_ENTRY(nsISupports) NS_INTERFACE_MAP_ENTRY(Promise) NS_INTERFACE_MAP_END Promise::Promise(nsIGlobalObject* aGlobal) : mGlobal(aGlobal) , 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) { MOZ_ASSERT(mGlobal); mozilla::HoldJSObjects(this); mCreationTimestamp = TimeStamp::Now(); } Promise::~Promise() { #if defined(DOM_PROMISE_DEPRECATED_REPORTING) MaybeReportRejectedOnce(); #endif // defined(DOM_PROMISE_DEPRECATED_REPORTING) mozilla::DropJSObjects(this); } JSObject* Promise::WrapObject(JSContext* aCx, JS::Handle aGivenProto) { return PromiseBinding::Wrap(aCx, this, aGivenProto); } already_AddRefed Promise::Create(nsIGlobalObject* aGlobal, ErrorResult& aRv, JS::Handle aDesiredProto) { RefPtr p = new Promise(aGlobal); p->CreateWrapper(aDesiredProto, aRv); if (aRv.Failed()) { return nullptr; } return p.forget(); } void Promise::CreateWrapper(JS::Handle aDesiredProto, ErrorResult& aRv) { AutoJSAPI jsapi; if (!jsapi.Init(mGlobal)) { aRv.Throw(NS_ERROR_UNEXPECTED); return; } JSContext* cx = jsapi.cx(); JS::Rooted 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 aValue) { MaybeResolveInternal(aCx, aValue); } void Promise::MaybeReject(JSContext* aCx, JS::Handle aValue) { MaybeRejectInternal(aCx, aValue); } void Promise::MaybeReject(const RefPtr& aArg) { MaybeSomething(aArg, &Promise::MaybeReject); } bool Promise::PerformMicroTaskCheckpoint() { CycleCollectedJSRuntime* runtime = CycleCollectedJSRuntime::Get(); std::queue>& microtaskQueue = runtime->GetPromiseMicroTaskQueue(); if (microtaskQueue.empty()) { return false; } Maybe cx; if (NS_IsMainThread()) { cx.emplace(); } do { nsCOMPtr runnable = microtaskQueue.front(); 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; } if (cx.isSome()) { JS_CheckForInterrupt(cx.ref()); } runtime->AfterProcessMicrotask(); } while (!microtaskQueue.empty()); return true; } /* static */ bool Promise::JSCallback(JSContext* aCx, unsigned aArgc, JS::Value* aVp) { JS::CallArgs args = CallArgsFromVp(aArgc, aVp); JS::Rooted 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(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 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) { JSFunction* func = js::NewFunctionWithReserved(aCx, JSCallback, 1 /* nargs */, 0 /* flags */, nullptr); if (!func) { return nullptr; } JS::Rooted obj(aCx, JS_GetFunctionObject(func)); JS::Rooted 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 obj(aCx, JS_GetFunctionObject(func)); JS::Rooted 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::Constructor(const GlobalObject& aGlobal, PromiseInit& aInit, ErrorResult& aRv, JS::Handle aDesiredProto) { nsCOMPtr global; global = do_QueryInterface(aGlobal.GetAsSupports()); if (!global) { aRv.Throw(NS_ERROR_UNEXPECTED); return nullptr; } RefPtr 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 resolveFunc(cx, CreateFunction(cx, this, PromiseCallback::Resolve)); if (!resolveFunc) { aRv.Throw(NS_ERROR_UNEXPECTED); return; } JS::Rooted 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()) { // We want the same behavior as this JS implementation: // // function Promise(arg) { try { arg(a, b); } catch (e) { this.reject(e); }} // // In particular, that means not using MaybeReject(aRv) here, since that // would create the exception object in our reflector compartment, while we // want to create it in whatever the current compartment on cx is. JS::Rooted value(cx); DebugOnly conversionResult = ToJSValue(cx, aRv, &value); MOZ_ASSERT(conversionResult); MaybeRejectInternal(cx, value); } } /* static */ already_AddRefed Promise::Resolve(const GlobalObject& aGlobal, JS::Handle aThisv, JS::Handle aValue, ErrorResult& aRv) { // If a Promise was passed, just return it. if (aValue.isObject()) { JS::Rooted valueObj(aGlobal.Context(), &aValue.toObject()); Promise* nextPromise; nsresult rv = UNWRAP_OBJECT(Promise, valueObj, nextPromise); if (NS_SUCCEEDED(rv)) { RefPtr addRefed = nextPromise; return addRefed.forget(); } } nsCOMPtr global = do_QueryInterface(aGlobal.GetAsSupports()); if (!global) { aRv.Throw(NS_ERROR_UNEXPECTED); return nullptr; } RefPtr p = Resolve(global, aGlobal.Context(), aValue, aRv); if (p) { p->mFullfillmentStack = p->mAllocationStack; } return p.forget(); } /* static */ already_AddRefed Promise::Resolve(nsIGlobalObject* aGlobal, JSContext* aCx, JS::Handle aValue, ErrorResult& aRv) { RefPtr promise = Create(aGlobal, aRv); if (aRv.Failed()) { return nullptr; } promise->MaybeResolveInternal(aCx, aValue); return promise.forget(); } /* static */ already_AddRefed Promise::Reject(const GlobalObject& aGlobal, JS::Handle aThisv, JS::Handle aValue, ErrorResult& aRv) { nsCOMPtr global = do_QueryInterface(aGlobal.GetAsSupports()); if (!global) { aRv.Throw(NS_ERROR_UNEXPECTED); return nullptr; } RefPtr p = Reject(global, aGlobal.Context(), aValue, aRv); if (p) { p->mRejectionStack = p->mAllocationStack; } return p.forget(); } /* static */ already_AddRefed Promise::Reject(nsIGlobalObject* aGlobal, JSContext* aCx, JS::Handle aValue, ErrorResult& aRv) { RefPtr promise = Create(aGlobal, aRv); if (aRv.Failed()) { return nullptr; } promise->MaybeRejectInternal(aCx, aValue); return promise.forget(); } already_AddRefed Promise::Then(JSContext* aCx, AnyCallback* aResolveCallback, AnyCallback* aRejectCallback, ErrorResult& aRv) { RefPtr promise = Create(GetParentObject(), aRv); if (aRv.Failed()) { return nullptr; } JS::Rooted global(aCx, JS::CurrentGlobalOrNull(aCx)); RefPtr resolveCb = PromiseCallback::Factory(promise, global, aResolveCallback, PromiseCallback::Resolve); RefPtr rejectCb = PromiseCallback::Factory(promise, global, aRejectCallback, PromiseCallback::Reject); AppendCallbacks(resolveCb, rejectCb); return promise.forget(); } already_AddRefed Promise::Catch(JSContext* aCx, AnyCallback* aRejectCallback, ErrorResult& aRv) { RefPtr resolveCb; return Then(aCx, resolveCb, aRejectCallback, aRv); } /** * 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 aValue) { MOZ_ASSERT(mCountdown > 0); ThreadsafeAutoSafeJSContext cx; JSAutoCompartment ac(cx, mValues); { AutoDontReportUncaught silenceReporting(cx); JS::Rooted value(cx, aValue); JS::Rooted values(cx, mValues); if (!JS_WrapValue(cx, &value) || !JS_DefineElement(cx, values, index, value, JSPROP_ENUMERATE)) { MOZ_ASSERT(JS_IsExceptionPending(cx)); JS::Rooted exn(cx); JS_GetPendingException(cx, &exn); mPromise->MaybeReject(cx, exn); } } --mCountdown; if (mCountdown == 0) { JS::Rooted result(cx, JS::ObjectValue(*mValues)); mPromise->MaybeResolve(cx, result); } } private: RefPtr mPromise; uint32_t mCountdown; JS::Heap 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 aValue) override { mCountdownHolder->SetValue(mIndex, aValue); } void RejectedCallback(JSContext* aCx, JS::Handle 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 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 */ already_AddRefed Promise::All(const GlobalObject& aGlobal, JS::Handle aThisv, const Sequence& aIterable, ErrorResult& aRv) { JSContext* cx = aGlobal.Context(); nsTArray> promiseList; for (uint32_t i = 0; i < aIterable.Length(); ++i) { JS::Rooted value(cx, aIterable.ElementAt(i)); RefPtr nextPromise = Promise::Resolve(aGlobal, aThisv, value, aRv); MOZ_ASSERT(!aRv.Failed()); promiseList.AppendElement(Move(nextPromise)); } return Promise::All(aGlobal, promiseList, aRv); } /* static */ already_AddRefed Promise::All(const GlobalObject& aGlobal, const nsTArray>& aPromiseList, ErrorResult& aRv) { nsCOMPtr global = do_QueryInterface(aGlobal.GetAsSupports()); if (!global) { aRv.Throw(NS_ERROR_UNEXPECTED); return nullptr; } JSContext* cx = aGlobal.Context(); if (aPromiseList.IsEmpty()) { JS::Rooted empty(cx, JS_NewArrayObject(cx, 0)); if (!empty) { aRv.Throw(NS_ERROR_OUT_OF_MEMORY); return nullptr; } JS::Rooted 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 = Create(global, aRv); if (aRv.Failed()) { return nullptr; } RefPtr holder = new CountdownHolder(aGlobal, promise, aPromiseList.Length()); JS::Rooted obj(cx, JS::CurrentGlobalOrNull(cx)); if (!obj) { aRv.Throw(NS_ERROR_UNEXPECTED); return nullptr; } RefPtr rejectCb = new RejectPromiseCallback(promise, obj); for (uint32_t i = 0; i < aPromiseList.Length(); ++i) { RefPtr resolveHandler = new AllResolveElementFunction(holder, i); RefPtr 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 */ already_AddRefed Promise::Race(const GlobalObject& aGlobal, JS::Handle aThisv, const Sequence& aIterable, ErrorResult& aRv) { nsCOMPtr global = do_QueryInterface(aGlobal.GetAsSupports()); if (!global) { aRv.Throw(NS_ERROR_UNEXPECTED); return nullptr; } JSContext* cx = aGlobal.Context(); JS::Rooted obj(cx, JS::CurrentGlobalOrNull(cx)); if (!obj) { aRv.Throw(NS_ERROR_UNEXPECTED); return nullptr; } RefPtr promise = Create(global, aRv); if (aRv.Failed()) { return nullptr; } RefPtr resolveCb = new ResolvePromiseCallback(promise, obj); RefPtr rejectCb = new RejectPromiseCallback(promise, obj); for (uint32_t i = 0; i < aIterable.Length(); ++i) { JS::Rooted value(cx, aIterable.ElementAt(i)); RefPtr nextPromise = Promise::Resolve(aGlobal, aThisv, value, aRv); // According to spec, Resolve can throw, but our implementation never does. // Well it does when window isn't passed on the main thread, but that is an // implementation detail which should never be reached since we are checking // for window above. Remove this when subclassing is supported. MOZ_ASSERT(!aRv.Failed()); nextPromise->AppendCallbacks(resolveCb, rejectCb); } return promise.forget(); } void Promise::AppendNativeHandler(PromiseNativeHandler* aRunnable) { RefPtr resolveCb = new NativePromiseCallback(aRunnable, Resolved); RefPtr rejectCb = new NativePromiseCallback(aRunnable, Rejected); AppendCallbacks(resolveCb, rejectCb); } JSObject* Promise::GlobalJSObject() const { return mGlobal->GetGlobalJSObject(); } JSCompartment* Promise::Compartment() const { return js::GetObjectCompartment(GlobalJSObject()); } void Promise::AppendCallbacks(PromiseCallback* aResolveCallback, PromiseCallback* aRejectCallback) { if (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; RemoveFeature(); #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(); } } class WrappedWorkerRunnable final : public WorkerSameThreadRunnable { public: WrappedWorkerRunnable(workers::WorkerPrivate* aWorkerPrivate, nsIRunnable* aRunnable) : WorkerSameThreadRunnable(aWorkerPrivate) , mRunnable(aRunnable) { MOZ_ASSERT(aRunnable); MOZ_COUNT_CTOR(WrappedWorkerRunnable); } bool WorkerRun(JSContext* aCx, workers::WorkerPrivate* aWorkerPrivate) override { NS_ASSERT_OWNINGTHREAD(WrappedWorkerRunnable); mRunnable->Run(); return true; } private: virtual ~WrappedWorkerRunnable() { MOZ_COUNT_DTOR(WrappedWorkerRunnable); NS_ASSERT_OWNINGTHREAD(WrappedWorkerRunnable); } nsCOMPtr mRunnable; NS_DECL_OWNINGTHREAD }; /* static */ void Promise::DispatchToMicroTask(nsIRunnable* aRunnable) { MOZ_ASSERT(aRunnable); CycleCollectedJSRuntime* runtime = CycleCollectedJSRuntime::Get(); std::queue>& microtaskQueue = runtime->GetPromiseMicroTaskQueue(); microtaskQueue.push(aRunnable); } #if defined(DOM_PROMISE_DEPRECATED_REPORTING) void Promise::MaybeReportRejected() { 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 obj(cx, GetWrapper()); MOZ_ASSERT(obj); // We preserve our wrapper, so should always have one here. JS::Rooted val(cx, mResult); JS::ExposeValueToActiveJS(val); JSAutoCompartment ac(cx, obj); if (!JS_WrapValue(cx, &val)) { JS_ClearPendingException(cx); return; } js::ErrorReport report(cx); if (!report.init(cx, val)) { JS_ClearPendingException(cx); return; } RefPtr xpcReport = new xpc::ErrorReport(); bool isMainThread = MOZ_LIKELY(NS_IsMainThread()); bool isChrome = isMainThread ? nsContentUtils::IsSystemPrincipal(nsContentUtils::ObjectPrincipal(obj)) : GetCurrentThreadWorkerPrivate()->IsChromeWorker(); nsPIDOMWindow* win = isMainThread ? xpc::WindowGlobalOrNull(obj) : nullptr; xpcReport->Init(report.report(), report.message(), isChrome, win ? win->WindowID() : 0); // 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 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 r = new AsyncErrorReporter(xpcReport); mainThread->Dispatch(r.forget(), NS_DISPATCH_NORMAL); } } #endif // defined(DOM_PROMISE_DEPRECATED_REPORTING) void Promise::MaybeResolveInternal(JSContext* aCx, JS::Handle aValue) { if (mResolvePending) { return; } ResolveInternal(aCx, aValue); } void Promise::MaybeRejectInternal(JSContext* aCx, JS::Handle aValue) { if (mResolvePending) { return; } RejectInternal(aCx, aValue); } void Promise::HandleException(JSContext* aCx) { JS::Rooted exn(aCx); if (JS_GetPendingException(aCx, &exn)) { JS_ClearPendingException(aCx); RejectInternal(aCx, exn); } } void Promise::ResolveInternal(JSContext* aCx, JS::Handle aValue) { mResolvePending = true; if (aValue.isObject()) { AutoDontReportUncaught silenceReporting(aCx); JS::Rooted valueObj(aCx, &aValue.toObject()); // Thenables. JS::Rooted 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 thenObj(aCx, &then.toObject()); // Add a fast path for the case when we're resolved with an actual // Promise. This has two requirements: // // 1) valueObj is a Promise. // 2) thenObj is a JSFunction backed by our actual Promise::Then // implementation. // // If those requirements are satisfied, then we know exactly what // thenObj.call(valueObj) will do, so we can optimize a bit and avoid ever // entering JS for this stuff. Promise* nextPromise; if (PromiseBinding::IsThenMethod(thenObj) && NS_SUCCEEDED(UNWRAP_OBJECT(Promise, valueObj, nextPromise))) { // If we were taking the codepath that involves PromiseResolveThenableJob and // PromiseInit below, then eventually, in PromiseResolveThenableJob::Run, we // would create some JSFunctions in the compartment of // this->GetWrapper() and pass them to the PromiseInit. So by the time // we'd see the resolution value it would be wrapped into the // compartment of this->GetWrapper(). The global of that compartment is // this->GetGlobalJSObject(), so use that as the global for // ResolvePromiseCallback/RejectPromiseCallback. JS::Rooted glob(aCx, GlobalJSObject()); RefPtr resolveCb = new ResolvePromiseCallback(this, glob); RefPtr rejectCb = new RejectPromiseCallback(this, glob); RefPtr task = new FastPromiseResolveThenableJob(resolveCb, rejectCb, nextPromise); DispatchToMicroTask(task); return; } RefPtr thenCallback = new PromiseInit(nullptr, thenObj, mozilla::dom::GetIncumbentGlobal()); RefPtr task = new PromiseResolveThenableJob(this, valueObj, thenCallback); DispatchToMicroTask(task); return; } } MaybeSettle(aValue, Resolved); } void Promise::RejectInternal(JSContext* aCx, JS::Handle aValue) { mResolvePending = true; MaybeSettle(aValue, Rejected); } void Promise::Settle(JS::Handle 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"); if (!mGlobal || mGlobal->IsDying()) { return; } mSettlementTimestamp = TimeStamp::Now(); SetResult(aValue); SetState(aState); AutoJSAPI jsapi; jsapi.Init(); JSContext* cx = jsapi.cx(); JS::RootedObject wrapper(cx, GetWrapper()); MOZ_ASSERT(wrapper); // We preserved it JSAutoCompartment ac(cx, wrapper); 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()) { workers::WorkerPrivate* worker = GetCurrentThreadWorkerPrivate(); MOZ_ASSERT(worker); worker->AssertIsOnWorkerThread(); mFeature = new PromiseReportRejectFeature(this); if (NS_WARN_IF(!worker->AddFeature(worker->GetJSContext(), mFeature))) { // To avoid a false RemoveFeature(). mFeature = 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 aValue, PromiseState aState) { // 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() { nsTArray> callbacks; callbacks.SwapElements(mState == Resolved ? mResolveCallbacks : mRejectCallbacks); mResolveCallbacks.Clear(); mRejectCallbacks.Clear(); for (uint32_t i = 0; i < callbacks.Length(); ++i) { RefPtr task = new PromiseReactionJob(this, callbacks[i], mResult); DispatchToMicroTask(task); } } #if defined(DOM_PROMISE_DEPRECATED_REPORTING) void Promise::RemoveFeature() { if (mFeature) { workers::WorkerPrivate* worker = GetCurrentThreadWorkerPrivate(); MOZ_ASSERT(worker); worker->RemoveFeature(worker->GetJSContext(), mFeature); mFeature = nullptr; } } bool PromiseReportRejectFeature::Notify(JSContext* aCx, workers::Status aStatus) { MOZ_ASSERT(aStatus > workers::Running); mPromise->MaybeReportRejectedOnce(); // After this point, `this` has been deleted by RemoveFeature! return true; } #endif // defined(DOM_PROMISE_DEPRECATED_REPORTING) bool Promise::CaptureStack(JSContext* aCx, JS::Heap& aTarget) { JS::Rooted stack(aCx); if (!JS::CaptureCurrentStack(aCx, &stack)) { return false; } aTarget = stack; return true; } void Promise::GetDependentPromises(nsTArray>& aPromises) { // 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); } } } // A WorkerRunnable to resolve/reject the Promise on the worker thread. // Calling thread MUST hold PromiseWorkerProxy's mutex before creating this. class PromiseWorkerProxyRunnable : public workers::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, workers::WorkerPrivate* aWorkerPrivate) { MOZ_ASSERT(aWorkerPrivate); aWorkerPrivate->AssertIsOnWorkerThread(); MOZ_ASSERT(aWorkerPrivate == mWorkerPrivate); MOZ_ASSERT(mPromiseWorkerProxy); RefPtr workerPromise = mPromiseWorkerProxy->WorkerPromise(); // Here we convert the buffer to a JS::Value. JS::Rooted 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(aCx); return true; } protected: ~PromiseWorkerProxyRunnable() {} private: RefPtr mPromiseWorkerProxy; // Function pointer for calling Promise::{ResolveInternal,RejectInternal}. PromiseWorkerProxy::RunCallbackFunc mFunc; }; /* static */ already_AddRefed PromiseWorkerProxy::Create(workers::WorkerPrivate* aWorkerPrivate, Promise* aWorkerPromise, const PromiseWorkerProxyStructuredCloneCallbacks* aCb) { MOZ_ASSERT(aWorkerPrivate); aWorkerPrivate->AssertIsOnWorkerThread(); MOZ_ASSERT(aWorkerPromise); MOZ_ASSERT_IF(aCb, !!aCb->Write && !!aCb->Read); RefPtr 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(workers::WorkerPrivate* aWorkerPrivate, Promise* aWorkerPromise, const PromiseWorkerProxyStructuredCloneCallbacks* aCallbacks) : mWorkerPrivate(aWorkerPrivate) , mWorkerPromise(aWorkerPromise) , mCleanedUp(false) , mCallbacks(aCallbacks) , mCleanUpLock("cleanUpLock") , mFeatureAdded(false) { } PromiseWorkerProxy::~PromiseWorkerProxy() { MOZ_ASSERT(mCleanedUp); MOZ_ASSERT(!mFeatureAdded); MOZ_ASSERT(!mWorkerPromise); MOZ_ASSERT(!mWorkerPrivate); } void PromiseWorkerProxy::CleanProperties() { #ifdef DEBUG workers::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(!mFeatureAdded); if (!mWorkerPrivate->AddFeature(mWorkerPrivate->GetJSContext(), this)) { return false; } mFeatureAdded = true; // Maintain a reference so that we have a valid object to clean up when // removing the feature. AddRef(); return true; } workers::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(mFeatureAdded); return mWorkerPrivate; } Promise* PromiseWorkerProxy::WorkerPromise() const { #ifdef DEBUG workers::WorkerPrivate* worker = GetCurrentThreadWorkerPrivate(); MOZ_ASSERT(worker); worker->AssertIsOnWorkerThread(); #endif MOZ_ASSERT(mWorkerPromise); return mWorkerPromise; } void PromiseWorkerProxy::StoreISupports(nsISupports* aSupports) { MOZ_ASSERT(NS_IsMainThread()); nsMainThreadPtrHandle supports( new nsMainThreadPtrHolder(aSupports)); mSupportsArray.AppendElement(supports); } void PromiseWorkerProxy::RunCallback(JSContext* aCx, JS::Handle 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 runnable = new PromiseWorkerProxyRunnable(this, aFunc); runnable->Dispatch(aCx); } void PromiseWorkerProxy::ResolvedCallback(JSContext* aCx, JS::Handle aValue) { RunCallback(aCx, aValue, &Promise::ResolveInternal); } void PromiseWorkerProxy::RejectedCallback(JSContext* aCx, JS::Handle aValue) { RunCallback(aCx, aValue, &Promise::RejectInternal); } bool PromiseWorkerProxy::Notify(JSContext* aCx, Status aStatus) { if (aStatus >= Canceling) { CleanUp(aCx); } return true; } void PromiseWorkerProxy::CleanUp(JSContext* aCx) { // 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 RemoveFeature(), so we need to check |mCleanedUp| first. if (CleanedUp()) { return; } MOZ_ASSERT(mWorkerPrivate); mWorkerPrivate->AssertIsOnWorkerThread(); MOZ_ASSERT(mWorkerPrivate->GetJSContext() == aCx); // Release the Promise and remove the PromiseWorkerProxy from the features of // the worker thread since the Promise has been resolved/rejected or the // worker thread has been cancelled. MOZ_ASSERT(mFeatureAdded); mWorkerPrivate->RemoveFeature(mWorkerPrivate->GetJSContext(), this); mFeatureAdded = false; 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 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& aArg) { MaybeSomething(aArg, &Promise::MaybeReject); } template<> void Promise::MaybeRejectBrokenly(const nsAString& aArg) { MaybeSomething(aArg, &Promise::MaybeReject); } uint64_t Promise::GetID() { if (mID != 0) { return mID; } return mID = ++gIDGenerator; } } // namespace dom } // namespace mozilla