/* -*- Mode: c++; c-basic-offset: 2; indent-tabs-mode: nil; tab-width: 40 -*- */ /* vim: set ts=2 et sw=2 tw=80: */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this file, * You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "mozilla/dom/Promise.h" #include "jsfriendapi.h" #include "mozilla/dom/BindingUtils.h" #include "mozilla/dom/DOMError.h" #include "mozilla/dom/OwningNonNull.h" #include "mozilla/dom/PromiseBinding.h" #include "mozilla/dom/ScriptSettings.h" #include "mozilla/CycleCollectedJSRuntime.h" #include "mozilla/Preferences.h" #include "PromiseCallback.h" #include "PromiseNativeHandler.h" #include "PromiseWorkerProxy.h" #include "nsContentUtils.h" #include "WorkerPrivate.h" #include "WorkerRunnable.h" #include "nsJSPrincipals.h" #include "nsJSUtils.h" #include "nsPIDOMWindow.h" #include "nsJSEnvironment.h" #include "nsIScriptObjectPrincipal.h" #include "xpcpublic.h" #include "nsGlobalWindow.h" namespace mozilla { namespace dom { using namespace workers; NS_IMPL_ISUPPORTS0(PromiseNativeHandler) // PromiseTask // This class processes the promise's callbacks with promise's result. class PromiseTask MOZ_FINAL : public nsRunnable { public: PromiseTask(Promise* aPromise) : mPromise(aPromise) { MOZ_ASSERT(aPromise); MOZ_COUNT_CTOR(PromiseTask); } protected: ~PromiseTask() { MOZ_COUNT_DTOR(PromiseTask); } public: NS_IMETHOD Run() { mPromise->mTaskPending = false; mPromise->RunTask(); return NS_OK; } private: nsRefPtr mPromise; }; class WorkerPromiseTask MOZ_FINAL : public WorkerSameThreadRunnable { public: WorkerPromiseTask(WorkerPrivate* aWorkerPrivate, Promise* aPromise) : WorkerSameThreadRunnable(aWorkerPrivate) , mPromise(aPromise) { MOZ_ASSERT(aPromise); MOZ_COUNT_CTOR(WorkerPromiseTask); } protected: ~WorkerPromiseTask() { MOZ_COUNT_DTOR(WorkerPromiseTask); } public: bool WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) { mPromise->mTaskPending = false; mPromise->RunTask(); return true; } private: nsRefPtr mPromise; }; class PromiseResolverMixin { public: PromiseResolverMixin(Promise* aPromise, JS::Handle aValue, Promise::PromiseState aState) : mPromise(aPromise) , mValue(CycleCollectedJSRuntime::Get()->Runtime(), aValue) , mState(aState) { MOZ_ASSERT(aPromise); MOZ_ASSERT(mState != Promise::Pending); MOZ_COUNT_CTOR(PromiseResolverMixin); } virtual ~PromiseResolverMixin() { NS_ASSERT_OWNINGTHREAD(PromiseResolverMixin); MOZ_COUNT_DTOR(PromiseResolverMixin); } protected: void RunInternal() { NS_ASSERT_OWNINGTHREAD(PromiseResolverMixin); mPromise->RunResolveTask( JS::Handle::fromMarkedLocation(mValue.address()), mState, Promise::SyncTask); } private: nsRefPtr mPromise; JS::PersistentRooted mValue; Promise::PromiseState mState; NS_DECL_OWNINGTHREAD; }; // This class processes the promise's callbacks with promise's result. class PromiseResolverTask MOZ_FINAL : public nsRunnable, public PromiseResolverMixin { public: PromiseResolverTask(Promise* aPromise, JS::Handle aValue, Promise::PromiseState aState) : PromiseResolverMixin(aPromise, aValue, aState) {} ~PromiseResolverTask() {} NS_IMETHOD Run() { RunInternal(); return NS_OK; } }; class WorkerPromiseResolverTask MOZ_FINAL : public WorkerSameThreadRunnable, public PromiseResolverMixin { public: WorkerPromiseResolverTask(WorkerPrivate* aWorkerPrivate, Promise* aPromise, JS::Handle aValue, Promise::PromiseState aState) : WorkerSameThreadRunnable(aWorkerPrivate), PromiseResolverMixin(aPromise, aValue, aState) {} ~WorkerPromiseResolverTask() {} bool WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) { RunInternal(); return true; } }; 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; } }; // Equivalent to the specification's ResolvePromiseViaThenableTask. class ThenableResolverMixin { public: ThenableResolverMixin(Promise* aPromise, JS::Handle aThenable, PromiseInit* aThen) : mPromise(aPromise) , mThenable(CycleCollectedJSRuntime::Get()->Runtime(), aThenable) , mThen(aThen) { MOZ_ASSERT(aPromise); MOZ_COUNT_CTOR(ThenableResolverMixin); } virtual ~ThenableResolverMixin() { NS_ASSERT_OWNINGTHREAD(ThenableResolverMixin); MOZ_COUNT_DTOR(ThenableResolverMixin); } protected: void RunInternal() { NS_ASSERT_OWNINGTHREAD(ThenableResolverMixin); ThreadsafeAutoJSContext cx; JS::Rooted wrapper(cx, mPromise->GetWrapper()); MOZ_ASSERT(wrapper); // It was preserved! if (!wrapper) { return; } JSAutoCompartment ac(cx, wrapper); JS::Rooted resolveFunc(cx, mPromise->CreateThenableFunction(cx, mPromise, PromiseCallback::Resolve)); if (!resolveFunc) { mPromise->HandleException(cx); return; } JS::Rooted rejectFunc(cx, mPromise->CreateThenableFunction(cx, mPromise, PromiseCallback::Reject)); if (!rejectFunc) { mPromise->HandleException(cx); return; } LinkThenableCallables(cx, resolveFunc, rejectFunc); ErrorResult rv; JS::Rooted rootedThenable(cx, mThenable); mThen->Call(rootedThenable, resolveFunc, rejectFunc, rv, CallbackObject::eRethrowExceptions); rv.WouldReportJSException(); if (rv.IsJSException()) { JS::Rooted exn(cx); rv.StealJSException(cx, &exn); 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, Promise::SyncTask); } // At least one of resolveFunc or rejectFunc have been called, so ignore // the exception. FIXME(nsm): This should be reported to the error // console though, for debugging. } } private: nsRefPtr mPromise; JS::PersistentRooted mThenable; nsRefPtr mThen; NS_DECL_OWNINGTHREAD; }; // Main thread runnable to resolve thenables. class ThenableResolverTask MOZ_FINAL : public nsRunnable, public ThenableResolverMixin { public: ThenableResolverTask(Promise* aPromise, JS::Handle aThenable, PromiseInit* aThen) : ThenableResolverMixin(aPromise, aThenable, aThen) { MOZ_ASSERT(NS_IsMainThread()); } ~ThenableResolverTask() { MOZ_ASSERT(NS_IsMainThread()); } NS_IMETHOD Run() { MOZ_ASSERT(NS_IsMainThread()); RunInternal(); return NS_OK; } }; // Worker thread runnable to resolve thenables. class WorkerThenableResolverTask MOZ_FINAL : public WorkerSameThreadRunnable, public ThenableResolverMixin { public: WorkerThenableResolverTask(WorkerPrivate* aWorkerPrivate, Promise* aPromise, JS::Handle aThenable, PromiseInit* aThen) : WorkerSameThreadRunnable(aWorkerPrivate), ThenableResolverMixin(aPromise, aThenable, aThen) { MOZ_ASSERT(aWorkerPrivate); aWorkerPrivate->AssertIsOnWorkerThread(); } ~WorkerThenableResolverTask() {} bool WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) { aWorkerPrivate->AssertIsOnWorkerThread(); RunInternal(); return true; } }; // Promise NS_IMPL_CYCLE_COLLECTION_CLASS(Promise) NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(Promise) tmp->MaybeReportRejectedOnce(); 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_PRESERVED_WRAPPER NS_IMPL_CYCLE_COLLECTION_TRACE_END NS_IMPL_CYCLE_COLLECTING_ADDREF(Promise) NS_IMPL_CYCLE_COLLECTING_RELEASE(Promise) NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(Promise) NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY NS_INTERFACE_MAP_ENTRY(nsISupports) NS_INTERFACE_MAP_END Promise::Promise(nsIGlobalObject* aGlobal) : mGlobal(aGlobal) , mResult(JS::UndefinedValue()) , mState(Pending) , mTaskPending(false) , mHadRejectCallback(false) , mResolvePending(false) { MOZ_ASSERT(mGlobal); mozilla::HoldJSObjects(this); SetIsDOMBinding(); } Promise::~Promise() { MaybeReportRejectedOnce(); mozilla::DropJSObjects(this); } JSObject* Promise::WrapObject(JSContext* aCx) { return PromiseBinding::Wrap(aCx, this); } already_AddRefed Promise::Create(nsIGlobalObject* aGlobal, ErrorResult& aRv) { AutoJSAPI jsapi; if (!jsapi.Init(aGlobal)) { aRv.Throw(NS_ERROR_UNEXPECTED); return nullptr; } JSContext* cx = jsapi.cx(); nsRefPtr p = new Promise(aGlobal); JS::Rooted ignored(cx); if (!WrapNewBindingObject(cx, p, &ignored)) { JS_ClearPendingException(cx); aRv.Throw(NS_ERROR_OUT_OF_MEMORY); return nullptr; } // Need the .get() bit here to get template deduction working right dom::PreserveWrapper(p.get()); return p.forget(); } void Promise::MaybeResolve(JSContext* aCx, JS::Handle aValue) { MaybeResolveInternal(aCx, aValue); } void Promise::MaybeReject(JSContext* aCx, JS::Handle aValue) { MaybeRejectInternal(aCx, aValue); } /* 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) { promise->MaybeResolveInternal(aCx, args.get(0)); } else { promise->MaybeRejectInternal(aCx, args.get(0)); } 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. 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)); } return true; } /* static */ bool Promise::JSCallbackThenableResolver(JSContext* aCx, unsigned aArgc, JS::Value* aVp) { return ThenableResolverCommon(aCx, PromiseCallback::Resolve, aArgc, aVp); } /* static */ bool Promise::JSCallbackThenableRejecter(JSContext* aCx, unsigned aArgc, JS::Value* aVp) { return ThenableResolverCommon(aCx, PromiseCallback::Reject, aArgc, aVp); } /* static */ JSObject* Promise::CreateFunction(JSContext* aCx, JSObject* aParent, Promise* aPromise, int32_t aTask) { JSFunction* func = js::NewFunctionWithReserved(aCx, JSCallback, 1 /* nargs */, 0 /* flags */, aParent, nullptr); if (!func) { return nullptr; } JS::Rooted obj(aCx, JS_GetFunctionObject(func)); JS::Rooted promiseObj(aCx); if (!dom::WrapNewBindingObject(aCx, aPromise, &promiseObj)) { return nullptr; } js::SetFunctionNativeReserved(obj, SLOT_PROMISE, promiseObj); js::SetFunctionNativeReserved(obj, SLOT_DATA, JS::Int32Value(aTask)); return obj; } /* static */ JSObject* Promise::CreateThenableFunction(JSContext* aCx, Promise* aPromise, uint32_t aTask) { JSNative whichFunc = aTask == PromiseCallback::Resolve ? JSCallbackThenableResolver : JSCallbackThenableRejecter ; JSFunction* func = js::NewFunctionWithReserved(aCx, whichFunc, 1 /* nargs */, 0 /* flags */, nullptr, nullptr); if (!func) { return nullptr; } JS::Rooted obj(aCx, JS_GetFunctionObject(func)); JS::Rooted promiseObj(aCx); if (!dom::WrapNewBindingObject(aCx, aPromise, &promiseObj)) { return nullptr; } js::SetFunctionNativeReserved(obj, SLOT_PROMISE, promiseObj); return obj; } /* static */ already_AddRefed Promise::Constructor(const GlobalObject& aGlobal, PromiseInit& aInit, ErrorResult& aRv) { JSContext* cx = aGlobal.Context(); nsCOMPtr global; global = do_QueryInterface(aGlobal.GetAsSupports()); if (!global) { aRv.Throw(NS_ERROR_UNEXPECTED); return nullptr; } nsRefPtr promise = Create(global, aRv); if (aRv.Failed()) { return nullptr; } JS::Rooted resolveFunc(cx, CreateFunction(cx, aGlobal.Get(), promise, PromiseCallback::Resolve)); if (!resolveFunc) { aRv.Throw(NS_ERROR_UNEXPECTED); return nullptr; } JS::Rooted rejectFunc(cx, CreateFunction(cx, aGlobal.Get(), promise, PromiseCallback::Reject)); if (!rejectFunc) { aRv.Throw(NS_ERROR_UNEXPECTED); return nullptr; } aInit.Call(resolveFunc, rejectFunc, aRv, CallbackObject::eRethrowExceptions); aRv.WouldReportJSException(); if (aRv.IsJSException()) { JS::Rooted value(cx); aRv.StealJSException(cx, &value); // we want the same behavior as this JS implementation: // function Promise(arg) { try { arg(a, b); } catch (e) { this.reject(e); }} if (!JS_WrapValue(cx, &value)) { aRv.Throw(NS_ERROR_UNEXPECTED); return nullptr; } promise->MaybeRejectInternal(cx, value); } return promise.forget(); } /* static */ already_AddRefed Promise::Resolve(const GlobalObject& aGlobal, 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)) { nsRefPtr addRefed = nextPromise; return addRefed.forget(); } } nsCOMPtr global = do_QueryInterface(aGlobal.GetAsSupports()); if (!global) { aRv.Throw(NS_ERROR_UNEXPECTED); return nullptr; } return Resolve(global, aGlobal.Context(), aValue, aRv); } /* static */ already_AddRefed Promise::Resolve(nsIGlobalObject* aGlobal, JSContext* aCx, JS::Handle aValue, ErrorResult& aRv) { nsRefPtr 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 aValue, ErrorResult& aRv) { nsCOMPtr global = do_QueryInterface(aGlobal.GetAsSupports()); if (!global) { aRv.Throw(NS_ERROR_UNEXPECTED); return nullptr; } return Reject(global, aGlobal.Context(), aValue, aRv); } /* static */ already_AddRefed Promise::Reject(nsIGlobalObject* aGlobal, JSContext* aCx, JS::Handle aValue, ErrorResult& aRv) { nsRefPtr 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) { nsRefPtr promise = Create(GetParentObject(), aRv); if (aRv.Failed()) { return nullptr; } JS::Rooted global(aCx, JS::CurrentGlobalOrNull(aCx)); nsRefPtr resolveCb = PromiseCallback::Factory(promise, global, aResolveCallback, PromiseCallback::Resolve); nsRefPtr rejectCb = PromiseCallback::Factory(promise, global, aRejectCallback, PromiseCallback::Reject); AppendCallbacks(resolveCb, rejectCb); return promise.forget(); } already_AddRefed Promise::Catch(JSContext* aCx, AnyCallback* aRejectCallback, ErrorResult& aRv) { nsRefPtr 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 AllResolveHandlers use SetValue() to set the array indices. */ class CountdownHolder MOZ_FINAL : public nsISupports { public: NS_DECL_CYCLE_COLLECTING_ISUPPORTS NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(CountdownHolder) CountdownHolder(const GlobalObject& aGlobal, Promise* aPromise, uint32_t aCountdown) : mPromise(aPromise), mCountdown(aCountdown) { MOZ_ASSERT(aCountdown != 0); JSContext* cx = aGlobal.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: nsRefPtr 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 AllResolveHandler is the per-promise part of the Promise.all() algorithm. * Every Promise in the handler is handed an instance of this as a resolution * handler and it sets the relevant index in the CountdownHolder. */ class AllResolveHandler MOZ_FINAL : public PromiseNativeHandler { public: NS_DECL_CYCLE_COLLECTING_ISUPPORTS NS_DECL_CYCLE_COLLECTION_CLASS(AllResolveHandler) AllResolveHandler(CountdownHolder* aHolder, uint32_t aIndex) : mCountdownHolder(aHolder), mIndex(aIndex) { MOZ_ASSERT(aHolder); } void ResolvedCallback(JSContext* aCx, JS::Handle aValue) { mCountdownHolder->SetValue(mIndex, aValue); } void RejectedCallback(JSContext* aCx, JS::Handle aValue) { // Should never be attached to Promise as a reject handler. MOZ_ASSERT(false, "AllResolveHandler should never be attached to a Promise's reject handler!"); } private: ~AllResolveHandler() { } nsRefPtr mCountdownHolder; uint32_t mIndex; }; NS_IMPL_CYCLE_COLLECTING_ADDREF(AllResolveHandler) NS_IMPL_CYCLE_COLLECTING_RELEASE(AllResolveHandler) NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(AllResolveHandler) NS_INTERFACE_MAP_END_INHERITING(PromiseNativeHandler) NS_IMPL_CYCLE_COLLECTION(AllResolveHandler, mCountdownHolder) /* static */ already_AddRefed Promise::All(const GlobalObject& aGlobal, const Sequence& aIterable, ErrorResult& aRv) { nsCOMPtr global = do_QueryInterface(aGlobal.GetAsSupports()); if (!global) { aRv.Throw(NS_ERROR_UNEXPECTED); return nullptr; } JSContext* cx = aGlobal.Context(); if (aIterable.Length() == 0) { 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)); return Promise::Resolve(aGlobal, value, aRv); } nsRefPtr promise = Create(global, aRv); if (aRv.Failed()) { return nullptr; } nsRefPtr holder = new CountdownHolder(aGlobal, promise, aIterable.Length()); JS::Rooted obj(cx, JS::CurrentGlobalOrNull(cx)); if (!obj) { aRv.Throw(NS_ERROR_UNEXPECTED); return nullptr; } nsRefPtr rejectCb = new RejectPromiseCallback(promise, obj); for (uint32_t i = 0; i < aIterable.Length(); ++i) { JS::Rooted value(cx, aIterable.ElementAt(i)); nsRefPtr nextPromise = Promise::Resolve(aGlobal, value, aRv); MOZ_ASSERT(!aRv.Failed()); nsRefPtr resolveHandler = new AllResolveHandler(holder, i); nsRefPtr resolveCb = new NativePromiseCallback(resolveHandler, Resolved); // Every promise gets its own resolve callback, which will set the right // index in the array to the resolution value. nextPromise->AppendCallbacks(resolveCb, rejectCb); } return promise.forget(); } /* static */ already_AddRefed Promise::Race(const GlobalObject& aGlobal, 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; } nsRefPtr promise = Create(global, aRv); if (aRv.Failed()) { return nullptr; } nsRefPtr resolveCb = new ResolvePromiseCallback(promise, obj); nsRefPtr rejectCb = new RejectPromiseCallback(promise, obj); for (uint32_t i = 0; i < aIterable.Length(); ++i) { JS::Rooted value(cx, aIterable.ElementAt(i)); nsRefPtr nextPromise = Promise::Resolve(aGlobal, value, aRv); // According to spec, Resolve can throw, but our implementation never does. // Well it does when window isn't passed on the main thread, but that is an // implementation detail which should never be reached since we are checking // for window above. Remove this when subclassing is supported. MOZ_ASSERT(!aRv.Failed()); nextPromise->AppendCallbacks(resolveCb, rejectCb); } return promise.forget(); } void Promise::AppendNativeHandler(PromiseNativeHandler* aRunnable) { nsRefPtr resolveCb = new NativePromiseCallback(aRunnable, Resolved); nsRefPtr rejectCb = new NativePromiseCallback(aRunnable, Rejected); AppendCallbacks(resolveCb, rejectCb); } void Promise::AppendCallbacks(PromiseCallback* aResolveCallback, PromiseCallback* aRejectCallback) { if (aResolveCallback) { mResolveCallbacks.AppendElement(aResolveCallback); } if (aRejectCallback) { mHadRejectCallback = true; mRejectCallbacks.AppendElement(aRejectCallback); // Now that there is a callback, we don't need to report anymore. RemoveFeature(); } // If promise's state is resolved, queue a task to process our resolve // callbacks with promise's result. If promise's state is rejected, queue a // task to process our reject callbacks with promise's result. if (mState != Pending && !mTaskPending) { if (MOZ_LIKELY(NS_IsMainThread())) { nsRefPtr task = new PromiseTask(this); NS_DispatchToCurrentThread(task); } else { WorkerPrivate* worker = GetCurrentThreadWorkerPrivate(); MOZ_ASSERT(worker); nsRefPtr task = new WorkerPromiseTask(worker, this); task->Dispatch(worker->GetJSContext()); } mTaskPending = true; } } void Promise::RunTask() { MOZ_ASSERT(mState != Pending); nsTArray> callbacks; callbacks.SwapElements(mState == Resolved ? mResolveCallbacks : mRejectCallbacks); mResolveCallbacks.Clear(); mRejectCallbacks.Clear(); ThreadsafeAutoJSContext cx; JS::Rooted value(cx, mResult); JS::Rooted wrapper(cx, GetWrapper()); MOZ_ASSERT(wrapper); // We preserved it JSAutoCompartment ac(cx, wrapper); if (!MaybeWrapValue(cx, &value)) { return; } for (uint32_t i = 0; i < callbacks.Length(); ++i) { callbacks[i]->Call(cx, value); } } 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; } // Remains null in case of worker. nsCOMPtr win; bool isChromeError = false; if (MOZ_LIKELY(NS_IsMainThread())) { nsIPrincipal* principal; win = xpc::WindowGlobalOrNull(obj); principal = nsContentUtils::ObjectPrincipal(obj); isChromeError = nsContentUtils::IsSystemPrincipal(principal); } else { WorkerPrivate* worker = GetCurrentThreadWorkerPrivate(); MOZ_ASSERT(worker); isChromeError = worker->IsChromeWorker(); } // Now post an event to do the real reporting async // Since Promises preserve their wrapper, it is essential to nsRefPtr<> the // AsyncErrorReporter, otherwise if the call to DispatchToMainThread fails, it // will leak. See Bug 958684. nsRefPtr r = new AsyncErrorReporter(CycleCollectedJSRuntime::Get()->Runtime(), report.report(), report.message(), isChromeError, win); NS_DispatchToMainThread(r); } void Promise::MaybeResolveInternal(JSContext* aCx, JS::Handle aValue, PromiseTaskSync aAsynchronous) { if (mResolvePending) { return; } ResolveInternal(aCx, aValue, aAsynchronous); } void Promise::MaybeRejectInternal(JSContext* aCx, JS::Handle aValue, PromiseTaskSync aAsynchronous) { if (mResolvePending) { return; } RejectInternal(aCx, aValue, aAsynchronous); } void Promise::HandleException(JSContext* aCx) { JS::Rooted exn(aCx); if (JS_GetPendingException(aCx, &exn)) { JS_ClearPendingException(aCx); RejectInternal(aCx, exn, SyncTask); } } void Promise::ResolveInternal(JSContext* aCx, JS::Handle aValue, PromiseTaskSync aAsynchronous) { 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_ObjectIsCallable(aCx, &then.toObject())) { // This is the then() function of the thenable aValueObj. JS::Rooted thenObj(aCx, &then.toObject()); nsRefPtr thenCallback = new PromiseInit(thenObj, mozilla::dom::GetIncumbentGlobal()); if (NS_IsMainThread()) { nsRefPtr task = new ThenableResolverTask(this, valueObj, thenCallback); NS_DispatchToCurrentThread(task); } else { WorkerPrivate* worker = GetCurrentThreadWorkerPrivate(); MOZ_ASSERT(worker); nsRefPtr task = new WorkerThenableResolverTask(worker, this, valueObj, thenCallback); task->Dispatch(worker->GetJSContext()); } return; } } // If the synchronous flag is set, process our resolve callbacks with // value. Otherwise, the synchronous flag is unset, queue a task to process // own resolve callbacks with value. Otherwise, the synchronous flag is // unset, queue a task to process our resolve callbacks with value. RunResolveTask(aValue, Resolved, aAsynchronous); } void Promise::RejectInternal(JSContext* aCx, JS::Handle aValue, PromiseTaskSync aAsynchronous) { mResolvePending = true; // If the synchronous flag is set, process our reject callbacks with // value. Otherwise, the synchronous flag is unset, queue a task to process // promise's reject callbacks with value. RunResolveTask(aValue, Rejected, aAsynchronous); } void Promise::RunResolveTask(JS::Handle aValue, PromiseState aState, PromiseTaskSync aAsynchronous) { // If the synchronous flag is unset, queue a task to process our // accept callbacks with value. if (aAsynchronous == AsyncTask) { if (MOZ_LIKELY(NS_IsMainThread())) { nsRefPtr task = new PromiseResolverTask(this, aValue, aState); NS_DispatchToCurrentThread(task); } else { WorkerPrivate* worker = GetCurrentThreadWorkerPrivate(); MOZ_ASSERT(worker); nsRefPtr task = new WorkerPromiseResolverTask(worker, this, aValue, aState); task->Dispatch(worker->GetJSContext()); } return; } // Promise.all() or Promise.race() implementations will repeatedly call // Resolve/RejectInternal rather than using the Maybe... forms. Stop SetState // from asserting. if (mState != Pending) { return; } SetResult(aValue); SetState(aState); // 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(); 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(); } } RunTask(); } void Promise::RemoveFeature() { if (mFeature) { 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; } // A WorkerRunnable to resolve/reject the Promise on the worker thread. class PromiseWorkerProxyRunnable : public workers::WorkerRunnable { public: PromiseWorkerProxyRunnable(PromiseWorkerProxy* aPromiseWorkerProxy, JSStructuredCloneCallbacks* aCallbacks, JSAutoStructuredCloneBuffer&& aBuffer, PromiseWorkerProxy::RunCallbackFunc aFunc) : WorkerRunnable(aPromiseWorkerProxy->GetWorkerPrivate(), WorkerThreadUnchangedBusyCount) , mPromiseWorkerProxy(aPromiseWorkerProxy) , mCallbacks(aCallbacks) , mBuffer(Move(aBuffer)) , 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); nsRefPtr workerPromise = mPromiseWorkerProxy->GetWorkerPromise(); MOZ_ASSERT(workerPromise); // Here we convert the buffer to a JS::Value. JS::Rooted value(aCx); if (!mBuffer.read(aCx, &value, mCallbacks, mPromiseWorkerProxy)) { JS_ClearPendingException(aCx); return false; } // TODO Bug 975246 - nsRefPtr should support operator |nsRefPtr->*funcType|. (workerPromise.get()->*mFunc)(aCx, value, Promise::PromiseTaskSync::SyncTask); // Release the Promise because it has been resolved/rejected for sure. mPromiseWorkerProxy->CleanUp(aCx); return true; } protected: ~PromiseWorkerProxyRunnable() {} private: nsRefPtr mPromiseWorkerProxy; JSStructuredCloneCallbacks* mCallbacks; JSAutoStructuredCloneBuffer mBuffer; // Function pointer for calling Promise::{ResolveInternal,RejectInternal}. PromiseWorkerProxy::RunCallbackFunc mFunc; }; PromiseWorkerProxy::PromiseWorkerProxy(WorkerPrivate* aWorkerPrivate, Promise* aWorkerPromise, JSStructuredCloneCallbacks* aCallbacks) : mWorkerPrivate(aWorkerPrivate) , mWorkerPromise(aWorkerPromise) , mCleanedUp(false) , mCallbacks(aCallbacks) , mCleanUpLock("cleanUpLock") { MOZ_ASSERT(mWorkerPrivate); mWorkerPrivate->AssertIsOnWorkerThread(); MOZ_ASSERT(mWorkerPromise); // We do this to make sure the worker thread won't shut down before the // promise is resolved/rejected on the worker thread. if (!mWorkerPrivate->AddFeature(mWorkerPrivate->GetJSContext(), this)) { MOZ_ASSERT(false, "cannot add the worker feature!"); return; } } PromiseWorkerProxy::~PromiseWorkerProxy() { MOZ_ASSERT(mCleanedUp); MOZ_ASSERT(!mWorkerPromise); } WorkerPrivate* PromiseWorkerProxy::GetWorkerPrivate() const { // It's ok to race on |mCleanedUp|, because it will never cause us to fire // the assertion when we should not. MOZ_ASSERT(!mCleanedUp); return mWorkerPrivate; } Promise* PromiseWorkerProxy::GetWorkerPromise() const { 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(mCleanUpLock); // If the worker thread's been cancelled we don't need to resolve the Promise. if (mCleanedUp) { return; } // The |aValue| is written into the buffer. Note that we also pass |this| // into the structured-clone write in order to set its |mSupportsArray| to // keep objects alive until the structured-clone read/write is done. JSAutoStructuredCloneBuffer buffer; if (!buffer.write(aCx, aValue, mCallbacks, this)) { JS_ClearPendingException(aCx); MOZ_ASSERT(false, "cannot write the JSAutoStructuredCloneBuffer!"); } nsRefPtr runnable = new PromiseWorkerProxyRunnable(this, mCallbacks, Move(buffer), 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) { MOZ_ASSERT(mWorkerPrivate); mWorkerPrivate->AssertIsOnWorkerThread(); MOZ_ASSERT(mWorkerPrivate->GetJSContext() == aCx); if (aStatus >= Canceling) { CleanUp(aCx); } return true; } void PromiseWorkerProxy::CleanUp(JSContext* aCx) { MutexAutoLock lock(mCleanUpLock); // |mWorkerPrivate| might not be safe to use anymore if we have already // cleaned up and RemoveFeature(), so we need to check |mCleanedUp| first. if (mCleanedUp) { 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. mWorkerPromise = nullptr; mWorkerPrivate->RemoveFeature(aCx, this); mCleanedUp = true; } // Specializations of MaybeRejectBrokenly we actually support. template<> void Promise::MaybeRejectBrokenly(const nsRefPtr& aArg) { MaybeSomething(aArg, &Promise::MaybeReject); } template<> void Promise::MaybeRejectBrokenly(const nsAString& aArg) { MaybeSomething(aArg, &Promise::MaybeReject); } } // namespace dom } // namespace mozilla