michael@0: /* -*- Mode: c++; c-basic-offset: 2; indent-tabs-mode: nil; tab-width: 40 -*- */ michael@0: /* vim: set ts=2 et sw=2 tw=80: */ michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this file, michael@0: * You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: #include "mozilla/dom/Promise.h" michael@0: michael@0: #include "jsfriendapi.h" michael@0: #include "mozilla/dom/OwningNonNull.h" michael@0: #include "mozilla/dom/PromiseBinding.h" michael@0: #include "mozilla/CycleCollectedJSRuntime.h" michael@0: #include "mozilla/Preferences.h" michael@0: #include "PromiseCallback.h" michael@0: #include "PromiseNativeHandler.h" michael@0: #include "nsContentUtils.h" michael@0: #include "WorkerPrivate.h" michael@0: #include "WorkerRunnable.h" michael@0: #include "nsJSPrincipals.h" michael@0: #include "nsJSUtils.h" michael@0: #include "nsPIDOMWindow.h" michael@0: #include "nsJSEnvironment.h" michael@0: michael@0: namespace mozilla { michael@0: namespace dom { michael@0: michael@0: using namespace workers; michael@0: michael@0: NS_IMPL_ISUPPORTS0(PromiseNativeHandler) michael@0: michael@0: // PromiseTask michael@0: michael@0: // This class processes the promise's callbacks with promise's result. michael@0: class PromiseTask MOZ_FINAL : public nsRunnable michael@0: { michael@0: public: michael@0: PromiseTask(Promise* aPromise) michael@0: : mPromise(aPromise) michael@0: { michael@0: MOZ_ASSERT(aPromise); michael@0: MOZ_COUNT_CTOR(PromiseTask); michael@0: } michael@0: michael@0: ~PromiseTask() michael@0: { michael@0: MOZ_COUNT_DTOR(PromiseTask); michael@0: } michael@0: michael@0: NS_IMETHOD Run() michael@0: { michael@0: mPromise->mTaskPending = false; michael@0: mPromise->RunTask(); michael@0: return NS_OK; michael@0: } michael@0: michael@0: private: michael@0: nsRefPtr mPromise; michael@0: }; michael@0: michael@0: class WorkerPromiseTask MOZ_FINAL : public WorkerSameThreadRunnable michael@0: { michael@0: public: michael@0: WorkerPromiseTask(WorkerPrivate* aWorkerPrivate, Promise* aPromise) michael@0: : WorkerSameThreadRunnable(aWorkerPrivate) michael@0: , mPromise(aPromise) michael@0: { michael@0: MOZ_ASSERT(aPromise); michael@0: MOZ_COUNT_CTOR(WorkerPromiseTask); michael@0: } michael@0: michael@0: ~WorkerPromiseTask() michael@0: { michael@0: MOZ_COUNT_DTOR(WorkerPromiseTask); michael@0: } michael@0: michael@0: bool michael@0: WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) michael@0: { michael@0: mPromise->mTaskPending = false; michael@0: mPromise->RunTask(); michael@0: return true; michael@0: } michael@0: michael@0: private: michael@0: nsRefPtr mPromise; michael@0: }; michael@0: michael@0: class PromiseResolverMixin michael@0: { michael@0: public: michael@0: PromiseResolverMixin(Promise* aPromise, michael@0: JS::Handle aValue, michael@0: Promise::PromiseState aState) michael@0: : mPromise(aPromise) michael@0: , mValue(CycleCollectedJSRuntime::Get()->Runtime(), aValue) michael@0: , mState(aState) michael@0: { michael@0: MOZ_ASSERT(aPromise); michael@0: MOZ_ASSERT(mState != Promise::Pending); michael@0: MOZ_COUNT_CTOR(PromiseResolverMixin); michael@0: } michael@0: michael@0: virtual ~PromiseResolverMixin() michael@0: { michael@0: NS_ASSERT_OWNINGTHREAD(PromiseResolverMixin); michael@0: MOZ_COUNT_DTOR(PromiseResolverMixin); michael@0: } michael@0: michael@0: protected: michael@0: void michael@0: RunInternal() michael@0: { michael@0: NS_ASSERT_OWNINGTHREAD(PromiseResolverMixin); michael@0: mPromise->RunResolveTask( michael@0: JS::Handle::fromMarkedLocation(mValue.address()), michael@0: mState, Promise::SyncTask); michael@0: } michael@0: michael@0: private: michael@0: nsRefPtr mPromise; michael@0: JS::PersistentRooted mValue; michael@0: Promise::PromiseState mState; michael@0: NS_DECL_OWNINGTHREAD; michael@0: }; michael@0: michael@0: // This class processes the promise's callbacks with promise's result. michael@0: class PromiseResolverTask MOZ_FINAL : public nsRunnable, michael@0: public PromiseResolverMixin michael@0: { michael@0: public: michael@0: PromiseResolverTask(Promise* aPromise, michael@0: JS::Handle aValue, michael@0: Promise::PromiseState aState) michael@0: : PromiseResolverMixin(aPromise, aValue, aState) michael@0: {} michael@0: michael@0: ~PromiseResolverTask() michael@0: {} michael@0: michael@0: NS_IMETHOD Run() michael@0: { michael@0: RunInternal(); michael@0: return NS_OK; michael@0: } michael@0: }; michael@0: michael@0: class WorkerPromiseResolverTask MOZ_FINAL : public WorkerSameThreadRunnable, michael@0: public PromiseResolverMixin michael@0: { michael@0: public: michael@0: WorkerPromiseResolverTask(WorkerPrivate* aWorkerPrivate, michael@0: Promise* aPromise, michael@0: JS::Handle aValue, michael@0: Promise::PromiseState aState) michael@0: : WorkerSameThreadRunnable(aWorkerPrivate), michael@0: PromiseResolverMixin(aPromise, aValue, aState) michael@0: {} michael@0: michael@0: ~WorkerPromiseResolverTask() michael@0: {} michael@0: michael@0: bool michael@0: WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) michael@0: { michael@0: RunInternal(); michael@0: return true; michael@0: } michael@0: }; michael@0: michael@0: // Promise michael@0: michael@0: NS_IMPL_CYCLE_COLLECTION_CLASS(Promise) michael@0: michael@0: NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(Promise) michael@0: tmp->MaybeReportRejectedOnce(); michael@0: NS_IMPL_CYCLE_COLLECTION_UNLINK(mGlobal) michael@0: NS_IMPL_CYCLE_COLLECTION_UNLINK(mResolveCallbacks) michael@0: NS_IMPL_CYCLE_COLLECTION_UNLINK(mRejectCallbacks) michael@0: NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER michael@0: NS_IMPL_CYCLE_COLLECTION_UNLINK_END michael@0: michael@0: NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(Promise) michael@0: NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mGlobal) michael@0: NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mResolveCallbacks) michael@0: NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mRejectCallbacks) michael@0: NS_IMPL_CYCLE_COLLECTION_TRAVERSE_SCRIPT_OBJECTS michael@0: NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END michael@0: michael@0: NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN(Promise) michael@0: NS_IMPL_CYCLE_COLLECTION_TRACE_JSVAL_MEMBER_CALLBACK(mResult) michael@0: NS_IMPL_CYCLE_COLLECTION_TRACE_PRESERVED_WRAPPER michael@0: NS_IMPL_CYCLE_COLLECTION_TRACE_END michael@0: michael@0: NS_IMPL_CYCLE_COLLECTING_ADDREF(Promise) michael@0: NS_IMPL_CYCLE_COLLECTING_RELEASE(Promise) michael@0: michael@0: NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(Promise) michael@0: NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY michael@0: NS_INTERFACE_MAP_ENTRY(nsISupports) michael@0: NS_INTERFACE_MAP_END michael@0: michael@0: Promise::Promise(nsIGlobalObject* aGlobal) michael@0: : mGlobal(aGlobal) michael@0: , mResult(JS::UndefinedValue()) michael@0: , mState(Pending) michael@0: , mTaskPending(false) michael@0: , mHadRejectCallback(false) michael@0: , mResolvePending(false) michael@0: { michael@0: MOZ_ASSERT(mGlobal); michael@0: michael@0: mozilla::HoldJSObjects(this); michael@0: SetIsDOMBinding(); michael@0: } michael@0: michael@0: Promise::~Promise() michael@0: { michael@0: MaybeReportRejectedOnce(); michael@0: mozilla::DropJSObjects(this); michael@0: } michael@0: michael@0: JSObject* michael@0: Promise::WrapObject(JSContext* aCx) michael@0: { michael@0: return PromiseBinding::Wrap(aCx, this); michael@0: } michael@0: michael@0: JSObject* michael@0: Promise::GetOrCreateWrapper(JSContext* aCx) michael@0: { michael@0: if (JSObject* wrapper = GetWrapper()) { michael@0: return wrapper; michael@0: } michael@0: michael@0: nsIGlobalObject* global = GetParentObject(); michael@0: MOZ_ASSERT(global); michael@0: michael@0: JS::Rooted scope(aCx, global->GetGlobalJSObject()); michael@0: if (!scope) { michael@0: JS_ReportError(aCx, "can't get scope"); michael@0: return nullptr; michael@0: } michael@0: michael@0: JSAutoCompartment ac(aCx, scope); michael@0: michael@0: JS::Rooted val(aCx); michael@0: if (!WrapNewBindingObject(aCx, this, &val)) { michael@0: MOZ_ASSERT(JS_IsExceptionPending(aCx)); michael@0: return nullptr; michael@0: } michael@0: michael@0: return GetWrapper(); michael@0: } michael@0: michael@0: void michael@0: Promise::MaybeResolve(JSContext* aCx, michael@0: JS::Handle aValue) michael@0: { michael@0: MaybeResolveInternal(aCx, aValue); michael@0: } michael@0: michael@0: void michael@0: Promise::MaybeReject(JSContext* aCx, michael@0: JS::Handle aValue) michael@0: { michael@0: MaybeRejectInternal(aCx, aValue); michael@0: } michael@0: michael@0: enum { michael@0: SLOT_PROMISE = 0, michael@0: SLOT_DATA michael@0: }; michael@0: michael@0: /* static */ bool michael@0: Promise::JSCallback(JSContext* aCx, unsigned aArgc, JS::Value* aVp) michael@0: { michael@0: JS::CallArgs args = CallArgsFromVp(aArgc, aVp); michael@0: michael@0: JS::Rooted v(aCx, michael@0: js::GetFunctionNativeReserved(&args.callee(), michael@0: SLOT_PROMISE)); michael@0: MOZ_ASSERT(v.isObject()); michael@0: michael@0: Promise* promise; michael@0: if (NS_FAILED(UNWRAP_OBJECT(Promise, &v.toObject(), promise))) { michael@0: return Throw(aCx, NS_ERROR_UNEXPECTED); michael@0: } michael@0: michael@0: v = js::GetFunctionNativeReserved(&args.callee(), SLOT_DATA); michael@0: PromiseCallback::Task task = static_cast(v.toInt32()); michael@0: michael@0: if (task == PromiseCallback::Resolve) { michael@0: promise->MaybeResolveInternal(aCx, args.get(0)); michael@0: } else { michael@0: promise->MaybeRejectInternal(aCx, args.get(0)); michael@0: } michael@0: michael@0: return true; michael@0: } michael@0: michael@0: /* michael@0: * Utilities for thenable callbacks. michael@0: * michael@0: * A thenable is a { then: function(resolve, reject) { } }. michael@0: * `then` is called with a resolve and reject callback pair. michael@0: * Since only one of these should be called at most once (first call wins), the michael@0: * two keep a reference to each other in SLOT_DATA. When either of them is michael@0: * called, the references are cleared. Further calls are ignored. michael@0: */ michael@0: namespace { michael@0: void michael@0: LinkThenableCallables(JSContext* aCx, JS::Handle aResolveFunc, michael@0: JS::Handle aRejectFunc) michael@0: { michael@0: js::SetFunctionNativeReserved(aResolveFunc, SLOT_DATA, michael@0: JS::ObjectValue(*aRejectFunc)); michael@0: js::SetFunctionNativeReserved(aRejectFunc, SLOT_DATA, michael@0: JS::ObjectValue(*aResolveFunc)); michael@0: } michael@0: michael@0: /* michael@0: * Returns false if callback was already called before, otherwise breaks the michael@0: * links and returns true. michael@0: */ michael@0: bool michael@0: MarkAsCalledIfNotCalledBefore(JSContext* aCx, JS::Handle aFunc) michael@0: { michael@0: JS::Value otherFuncVal = js::GetFunctionNativeReserved(aFunc, SLOT_DATA); michael@0: michael@0: if (!otherFuncVal.isObject()) { michael@0: return false; michael@0: } michael@0: michael@0: JSObject* otherFuncObj = &otherFuncVal.toObject(); michael@0: MOZ_ASSERT(js::GetFunctionNativeReserved(otherFuncObj, SLOT_DATA).isObject()); michael@0: michael@0: // Break both references. michael@0: js::SetFunctionNativeReserved(aFunc, SLOT_DATA, JS::UndefinedValue()); michael@0: js::SetFunctionNativeReserved(otherFuncObj, SLOT_DATA, JS::UndefinedValue()); michael@0: michael@0: return true; michael@0: } michael@0: michael@0: Promise* michael@0: GetPromise(JSContext* aCx, JS::Handle aFunc) michael@0: { michael@0: JS::Value promiseVal = js::GetFunctionNativeReserved(aFunc, SLOT_PROMISE); michael@0: michael@0: MOZ_ASSERT(promiseVal.isObject()); michael@0: michael@0: Promise* promise; michael@0: UNWRAP_OBJECT(Promise, &promiseVal.toObject(), promise); michael@0: return promise; michael@0: } michael@0: }; michael@0: michael@0: /* michael@0: * Common bits of (JSCallbackThenableResolver/JSCallbackThenableRejecter). michael@0: * Resolves/rejects the Promise if it is ok to do so, based on whether either of michael@0: * the callbacks have been called before or not. michael@0: */ michael@0: /* static */ bool michael@0: Promise::ThenableResolverCommon(JSContext* aCx, uint32_t aTask, michael@0: unsigned aArgc, JS::Value* aVp) michael@0: { michael@0: JS::CallArgs args = CallArgsFromVp(aArgc, aVp); michael@0: JS::Rooted thisFunc(aCx, &args.callee()); michael@0: if (!MarkAsCalledIfNotCalledBefore(aCx, thisFunc)) { michael@0: // A function from this pair has been called before. michael@0: return true; michael@0: } michael@0: michael@0: Promise* promise = GetPromise(aCx, thisFunc); michael@0: MOZ_ASSERT(promise); michael@0: michael@0: if (aTask == PromiseCallback::Resolve) { michael@0: promise->ResolveInternal(aCx, args.get(0), SyncTask); michael@0: } else { michael@0: promise->RejectInternal(aCx, args.get(0), SyncTask); michael@0: } michael@0: return true; michael@0: } michael@0: michael@0: /* static */ bool michael@0: Promise::JSCallbackThenableResolver(JSContext* aCx, michael@0: unsigned aArgc, JS::Value* aVp) michael@0: { michael@0: return ThenableResolverCommon(aCx, PromiseCallback::Resolve, aArgc, aVp); michael@0: } michael@0: michael@0: /* static */ bool michael@0: Promise::JSCallbackThenableRejecter(JSContext* aCx, michael@0: unsigned aArgc, JS::Value* aVp) michael@0: { michael@0: return ThenableResolverCommon(aCx, PromiseCallback::Reject, aArgc, aVp); michael@0: } michael@0: michael@0: /* static */ JSObject* michael@0: Promise::CreateFunction(JSContext* aCx, JSObject* aParent, Promise* aPromise, michael@0: int32_t aTask) michael@0: { michael@0: JSFunction* func = js::NewFunctionWithReserved(aCx, JSCallback, michael@0: 1 /* nargs */, 0 /* flags */, michael@0: aParent, nullptr); michael@0: if (!func) { michael@0: return nullptr; michael@0: } michael@0: michael@0: JS::Rooted obj(aCx, JS_GetFunctionObject(func)); michael@0: michael@0: JS::Rooted promiseObj(aCx); michael@0: if (!dom::WrapNewBindingObject(aCx, aPromise, &promiseObj)) { michael@0: return nullptr; michael@0: } michael@0: michael@0: js::SetFunctionNativeReserved(obj, SLOT_PROMISE, promiseObj); michael@0: js::SetFunctionNativeReserved(obj, SLOT_DATA, JS::Int32Value(aTask)); michael@0: michael@0: return obj; michael@0: } michael@0: michael@0: /* static */ JSObject* michael@0: Promise::CreateThenableFunction(JSContext* aCx, Promise* aPromise, uint32_t aTask) michael@0: { michael@0: JSNative whichFunc = michael@0: aTask == PromiseCallback::Resolve ? JSCallbackThenableResolver : michael@0: JSCallbackThenableRejecter ; michael@0: michael@0: JSFunction* func = js::NewFunctionWithReserved(aCx, whichFunc, michael@0: 1 /* nargs */, 0 /* flags */, michael@0: nullptr, nullptr); michael@0: if (!func) { michael@0: return nullptr; michael@0: } michael@0: michael@0: JS::Rooted obj(aCx, JS_GetFunctionObject(func)); michael@0: michael@0: JS::Rooted promiseObj(aCx); michael@0: if (!dom::WrapNewBindingObject(aCx, aPromise, &promiseObj)) { michael@0: return nullptr; michael@0: } michael@0: michael@0: js::SetFunctionNativeReserved(obj, SLOT_PROMISE, promiseObj); michael@0: michael@0: return obj; michael@0: } michael@0: michael@0: /* static */ already_AddRefed michael@0: Promise::Constructor(const GlobalObject& aGlobal, michael@0: PromiseInit& aInit, ErrorResult& aRv) michael@0: { michael@0: JSContext* cx = aGlobal.GetContext(); michael@0: michael@0: nsCOMPtr global; michael@0: global = do_QueryInterface(aGlobal.GetAsSupports()); michael@0: if (!global) { michael@0: aRv.Throw(NS_ERROR_UNEXPECTED); michael@0: return nullptr; michael@0: } michael@0: michael@0: nsRefPtr promise = new Promise(global); michael@0: michael@0: JS::Rooted resolveFunc(cx, michael@0: CreateFunction(cx, aGlobal.Get(), promise, michael@0: PromiseCallback::Resolve)); michael@0: if (!resolveFunc) { michael@0: aRv.Throw(NS_ERROR_UNEXPECTED); michael@0: return nullptr; michael@0: } michael@0: michael@0: JS::Rooted rejectFunc(cx, michael@0: CreateFunction(cx, aGlobal.Get(), promise, michael@0: PromiseCallback::Reject)); michael@0: if (!rejectFunc) { michael@0: aRv.Throw(NS_ERROR_UNEXPECTED); michael@0: return nullptr; michael@0: } michael@0: michael@0: aInit.Call(resolveFunc, rejectFunc, aRv, CallbackObject::eRethrowExceptions); michael@0: aRv.WouldReportJSException(); michael@0: michael@0: if (aRv.IsJSException()) { michael@0: JS::Rooted value(cx); michael@0: aRv.StealJSException(cx, &value); michael@0: michael@0: // we want the same behavior as this JS implementation: michael@0: // function Promise(arg) { try { arg(a, b); } catch (e) { this.reject(e); }} michael@0: if (!JS_WrapValue(cx, &value)) { michael@0: aRv.Throw(NS_ERROR_UNEXPECTED); michael@0: return nullptr; michael@0: } michael@0: michael@0: promise->MaybeRejectInternal(cx, value); michael@0: } michael@0: michael@0: return promise.forget(); michael@0: } michael@0: michael@0: /* static */ already_AddRefed michael@0: Promise::Resolve(const GlobalObject& aGlobal, JSContext* aCx, michael@0: JS::Handle aValue, ErrorResult& aRv) michael@0: { michael@0: // If a Promise was passed, just return it. michael@0: if (aValue.isObject()) { michael@0: JS::Rooted valueObj(aCx, &aValue.toObject()); michael@0: Promise* nextPromise; michael@0: nsresult rv = UNWRAP_OBJECT(Promise, valueObj, nextPromise); michael@0: michael@0: if (NS_SUCCEEDED(rv)) { michael@0: nsRefPtr addRefed = nextPromise; michael@0: return addRefed.forget(); michael@0: } michael@0: } michael@0: michael@0: nsCOMPtr global = michael@0: do_QueryInterface(aGlobal.GetAsSupports()); michael@0: if (!global) { michael@0: aRv.Throw(NS_ERROR_UNEXPECTED); michael@0: return nullptr; michael@0: } michael@0: michael@0: return Resolve(global, aCx, aValue, aRv); michael@0: } michael@0: michael@0: /* static */ already_AddRefed michael@0: Promise::Resolve(nsIGlobalObject* aGlobal, JSContext* aCx, michael@0: JS::Handle aValue, ErrorResult& aRv) michael@0: { michael@0: nsRefPtr promise = new Promise(aGlobal); michael@0: michael@0: promise->MaybeResolveInternal(aCx, aValue); michael@0: return promise.forget(); michael@0: } michael@0: michael@0: /* static */ already_AddRefed michael@0: Promise::Reject(const GlobalObject& aGlobal, JSContext* aCx, michael@0: JS::Handle aValue, ErrorResult& aRv) michael@0: { michael@0: nsCOMPtr global = michael@0: do_QueryInterface(aGlobal.GetAsSupports()); michael@0: if (!global) { michael@0: aRv.Throw(NS_ERROR_UNEXPECTED); michael@0: return nullptr; michael@0: } michael@0: michael@0: return Reject(global, aCx, aValue, aRv); michael@0: } michael@0: michael@0: /* static */ already_AddRefed michael@0: Promise::Reject(nsIGlobalObject* aGlobal, JSContext* aCx, michael@0: JS::Handle aValue, ErrorResult& aRv) michael@0: { michael@0: nsRefPtr promise = new Promise(aGlobal); michael@0: michael@0: promise->MaybeRejectInternal(aCx, aValue); michael@0: return promise.forget(); michael@0: } michael@0: michael@0: already_AddRefed michael@0: Promise::Then(JSContext* aCx, AnyCallback* aResolveCallback, michael@0: AnyCallback* aRejectCallback) michael@0: { michael@0: nsRefPtr promise = new Promise(GetParentObject()); michael@0: michael@0: JS::Rooted global(aCx, JS::CurrentGlobalOrNull(aCx)); michael@0: michael@0: nsRefPtr resolveCb = michael@0: PromiseCallback::Factory(promise, global, aResolveCallback, michael@0: PromiseCallback::Resolve); michael@0: michael@0: nsRefPtr rejectCb = michael@0: PromiseCallback::Factory(promise, global, aRejectCallback, michael@0: PromiseCallback::Reject); michael@0: michael@0: AppendCallbacks(resolveCb, rejectCb); michael@0: michael@0: return promise.forget(); michael@0: } michael@0: michael@0: already_AddRefed michael@0: Promise::Catch(JSContext* aCx, AnyCallback* aRejectCallback) michael@0: { michael@0: nsRefPtr resolveCb; michael@0: return Then(aCx, resolveCb, aRejectCallback); michael@0: } michael@0: michael@0: /** michael@0: * The CountdownHolder class encapsulates Promise.all countdown functions and michael@0: * the countdown holder parts of the Promises spec. It maintains the result michael@0: * array and AllResolveHandlers use SetValue() to set the array indices. michael@0: */ michael@0: class CountdownHolder MOZ_FINAL : public nsISupports michael@0: { michael@0: public: michael@0: NS_DECL_CYCLE_COLLECTING_ISUPPORTS michael@0: NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(CountdownHolder) michael@0: michael@0: CountdownHolder(const GlobalObject& aGlobal, Promise* aPromise, michael@0: uint32_t aCountdown) michael@0: : mPromise(aPromise), mCountdown(aCountdown) michael@0: { michael@0: MOZ_ASSERT(aCountdown != 0); michael@0: JSContext* cx = aGlobal.GetContext(); michael@0: michael@0: // The only time aGlobal.GetContext() and aGlobal.Get() are not michael@0: // same-compartment is when we're called via Xrays, and in that situation we michael@0: // in fact want to create the array in the callee compartment michael@0: michael@0: JSAutoCompartment ac(cx, aGlobal.Get()); michael@0: mValues = JS_NewArrayObject(cx, aCountdown); michael@0: mozilla::HoldJSObjects(this); michael@0: } michael@0: michael@0: ~CountdownHolder() michael@0: { michael@0: mozilla::DropJSObjects(this); michael@0: } michael@0: michael@0: void SetValue(uint32_t index, const JS::Handle aValue) michael@0: { michael@0: MOZ_ASSERT(mCountdown > 0); michael@0: michael@0: ThreadsafeAutoSafeJSContext cx; michael@0: JSAutoCompartment ac(cx, mValues); michael@0: { michael@0: michael@0: AutoDontReportUncaught silenceReporting(cx); michael@0: JS::Rooted value(cx, aValue); michael@0: if (!JS_WrapValue(cx, &value) || michael@0: !JS_DefineElement(cx, mValues, index, value, nullptr, nullptr, michael@0: JSPROP_ENUMERATE)) { michael@0: MOZ_ASSERT(JS_IsExceptionPending(cx)); michael@0: JS::Rooted exn(cx); michael@0: JS_GetPendingException(cx, &exn); michael@0: michael@0: mPromise->MaybeReject(cx, exn); michael@0: } michael@0: } michael@0: michael@0: --mCountdown; michael@0: if (mCountdown == 0) { michael@0: JS::Rooted result(cx, JS::ObjectValue(*mValues)); michael@0: mPromise->MaybeResolve(cx, result); michael@0: } michael@0: } michael@0: michael@0: private: michael@0: nsRefPtr mPromise; michael@0: uint32_t mCountdown; michael@0: JS::Heap mValues; michael@0: }; michael@0: michael@0: NS_IMPL_CYCLE_COLLECTING_ADDREF(CountdownHolder) michael@0: NS_IMPL_CYCLE_COLLECTING_RELEASE(CountdownHolder) michael@0: NS_IMPL_CYCLE_COLLECTION_CLASS(CountdownHolder) michael@0: michael@0: NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(CountdownHolder) michael@0: NS_INTERFACE_MAP_ENTRY(nsISupports) michael@0: NS_INTERFACE_MAP_END michael@0: michael@0: NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN(CountdownHolder) michael@0: NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mValues) michael@0: NS_IMPL_CYCLE_COLLECTION_TRACE_END michael@0: michael@0: NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(CountdownHolder) michael@0: NS_IMPL_CYCLE_COLLECTION_TRAVERSE_SCRIPT_OBJECTS michael@0: NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mPromise) michael@0: NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END michael@0: michael@0: NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(CountdownHolder) michael@0: NS_IMPL_CYCLE_COLLECTION_UNLINK(mPromise) michael@0: tmp->mValues = nullptr; michael@0: NS_IMPL_CYCLE_COLLECTION_UNLINK_END michael@0: michael@0: /** michael@0: * An AllResolveHandler is the per-promise part of the Promise.all() algorithm. michael@0: * Every Promise in the handler is handed an instance of this as a resolution michael@0: * handler and it sets the relevant index in the CountdownHolder. michael@0: */ michael@0: class AllResolveHandler MOZ_FINAL : public PromiseNativeHandler michael@0: { michael@0: public: michael@0: NS_DECL_CYCLE_COLLECTING_ISUPPORTS michael@0: NS_DECL_CYCLE_COLLECTION_CLASS(AllResolveHandler) michael@0: michael@0: AllResolveHandler(CountdownHolder* aHolder, uint32_t aIndex) michael@0: : mCountdownHolder(aHolder), mIndex(aIndex) michael@0: { michael@0: MOZ_ASSERT(aHolder); michael@0: } michael@0: michael@0: ~AllResolveHandler() michael@0: { michael@0: } michael@0: michael@0: void michael@0: ResolvedCallback(JS::Handle aValue) michael@0: { michael@0: mCountdownHolder->SetValue(mIndex, aValue); michael@0: } michael@0: michael@0: void michael@0: RejectedCallback(JS::Handle aValue) michael@0: { michael@0: // Should never be attached to Promise as a reject handler. michael@0: MOZ_ASSERT(false, "AllResolveHandler should never be attached to a Promise's reject handler!"); michael@0: } michael@0: michael@0: private: michael@0: nsRefPtr mCountdownHolder; michael@0: uint32_t mIndex; michael@0: }; michael@0: michael@0: NS_IMPL_CYCLE_COLLECTING_ADDREF(AllResolveHandler) michael@0: NS_IMPL_CYCLE_COLLECTING_RELEASE(AllResolveHandler) michael@0: michael@0: NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(AllResolveHandler) michael@0: NS_INTERFACE_MAP_END_INHERITING(PromiseNativeHandler) michael@0: michael@0: NS_IMPL_CYCLE_COLLECTION(AllResolveHandler, mCountdownHolder) michael@0: michael@0: /* static */ already_AddRefed michael@0: Promise::All(const GlobalObject& aGlobal, JSContext* aCx, michael@0: const Sequence& aIterable, ErrorResult& aRv) michael@0: { michael@0: nsCOMPtr global = michael@0: do_QueryInterface(aGlobal.GetAsSupports()); michael@0: if (!global) { michael@0: aRv.Throw(NS_ERROR_UNEXPECTED); michael@0: return nullptr; michael@0: } michael@0: michael@0: if (aIterable.Length() == 0) { michael@0: JS::Rooted empty(aCx, JS_NewArrayObject(aCx, 0)); michael@0: if (!empty) { michael@0: aRv.Throw(NS_ERROR_OUT_OF_MEMORY); michael@0: return nullptr; michael@0: } michael@0: JS::Rooted value(aCx, JS::ObjectValue(*empty)); michael@0: return Promise::Resolve(aGlobal, aCx, value, aRv); michael@0: } michael@0: michael@0: nsRefPtr promise = new Promise(global); michael@0: nsRefPtr holder = michael@0: new CountdownHolder(aGlobal, promise, aIterable.Length()); michael@0: michael@0: JS::Rooted obj(aCx, JS::CurrentGlobalOrNull(aCx)); michael@0: if (!obj) { michael@0: aRv.Throw(NS_ERROR_UNEXPECTED); michael@0: return nullptr; michael@0: } michael@0: michael@0: nsRefPtr rejectCb = new RejectPromiseCallback(promise, obj); michael@0: michael@0: for (uint32_t i = 0; i < aIterable.Length(); ++i) { michael@0: JS::Rooted value(aCx, aIterable.ElementAt(i)); michael@0: nsRefPtr nextPromise = Promise::Resolve(aGlobal, aCx, value, aRv); michael@0: michael@0: MOZ_ASSERT(!aRv.Failed()); michael@0: michael@0: nsRefPtr resolveHandler = michael@0: new AllResolveHandler(holder, i); michael@0: michael@0: nsRefPtr resolveCb = michael@0: new NativePromiseCallback(resolveHandler, Resolved); michael@0: // Every promise gets its own resolve callback, which will set the right michael@0: // index in the array to the resolution value. michael@0: nextPromise->AppendCallbacks(resolveCb, rejectCb); michael@0: } michael@0: michael@0: return promise.forget(); michael@0: } michael@0: michael@0: /* static */ already_AddRefed michael@0: Promise::Race(const GlobalObject& aGlobal, JSContext* aCx, michael@0: const Sequence& aIterable, ErrorResult& aRv) michael@0: { michael@0: nsCOMPtr global = michael@0: do_QueryInterface(aGlobal.GetAsSupports()); michael@0: if (!global) { michael@0: aRv.Throw(NS_ERROR_UNEXPECTED); michael@0: return nullptr; michael@0: } michael@0: michael@0: JS::Rooted obj(aCx, JS::CurrentGlobalOrNull(aCx)); michael@0: if (!obj) { michael@0: aRv.Throw(NS_ERROR_UNEXPECTED); michael@0: return nullptr; michael@0: } michael@0: michael@0: nsRefPtr promise = new Promise(global); michael@0: michael@0: nsRefPtr resolveCb = michael@0: new ResolvePromiseCallback(promise, obj); michael@0: michael@0: nsRefPtr rejectCb = new RejectPromiseCallback(promise, obj); michael@0: michael@0: for (uint32_t i = 0; i < aIterable.Length(); ++i) { michael@0: JS::Rooted value(aCx, aIterable.ElementAt(i)); michael@0: nsRefPtr nextPromise = Promise::Resolve(aGlobal, aCx, value, aRv); michael@0: // According to spec, Resolve can throw, but our implementation never does. michael@0: // Well it does when window isn't passed on the main thread, but that is an michael@0: // implementation detail which should never be reached since we are checking michael@0: // for window above. Remove this when subclassing is supported. michael@0: MOZ_ASSERT(!aRv.Failed()); michael@0: nextPromise->AppendCallbacks(resolveCb, rejectCb); michael@0: } michael@0: michael@0: return promise.forget(); michael@0: } michael@0: michael@0: void michael@0: Promise::AppendNativeHandler(PromiseNativeHandler* aRunnable) michael@0: { michael@0: nsRefPtr resolveCb = michael@0: new NativePromiseCallback(aRunnable, Resolved); michael@0: michael@0: nsRefPtr rejectCb = michael@0: new NativePromiseCallback(aRunnable, Rejected); michael@0: michael@0: AppendCallbacks(resolveCb, rejectCb); michael@0: } michael@0: michael@0: void michael@0: Promise::AppendCallbacks(PromiseCallback* aResolveCallback, michael@0: PromiseCallback* aRejectCallback) michael@0: { michael@0: if (aResolveCallback) { michael@0: mResolveCallbacks.AppendElement(aResolveCallback); michael@0: } michael@0: michael@0: if (aRejectCallback) { michael@0: mHadRejectCallback = true; michael@0: mRejectCallbacks.AppendElement(aRejectCallback); michael@0: michael@0: // Now that there is a callback, we don't need to report anymore. michael@0: RemoveFeature(); michael@0: } michael@0: michael@0: // If promise's state is resolved, queue a task to process our resolve michael@0: // callbacks with promise's result. If promise's state is rejected, queue a michael@0: // task to process our reject callbacks with promise's result. michael@0: if (mState != Pending && !mTaskPending) { michael@0: if (MOZ_LIKELY(NS_IsMainThread())) { michael@0: nsRefPtr task = new PromiseTask(this); michael@0: NS_DispatchToCurrentThread(task); michael@0: } else { michael@0: WorkerPrivate* worker = GetCurrentThreadWorkerPrivate(); michael@0: MOZ_ASSERT(worker); michael@0: nsRefPtr task = new WorkerPromiseTask(worker, this); michael@0: task->Dispatch(worker->GetJSContext()); michael@0: } michael@0: mTaskPending = true; michael@0: } michael@0: } michael@0: michael@0: void michael@0: Promise::RunTask() michael@0: { michael@0: MOZ_ASSERT(mState != Pending); michael@0: michael@0: nsTArray> callbacks; michael@0: callbacks.SwapElements(mState == Resolved ? mResolveCallbacks michael@0: : mRejectCallbacks); michael@0: mResolveCallbacks.Clear(); michael@0: mRejectCallbacks.Clear(); michael@0: michael@0: ThreadsafeAutoJSContext cx; // Just for rooting. michael@0: JS::Rooted value(cx, mResult); michael@0: michael@0: for (uint32_t i = 0; i < callbacks.Length(); ++i) { michael@0: callbacks[i]->Call(value); michael@0: } michael@0: } michael@0: michael@0: void michael@0: Promise::MaybeReportRejected() michael@0: { michael@0: if (mState != Rejected || mHadRejectCallback || mResult.isUndefined()) { michael@0: return; michael@0: } michael@0: michael@0: if (!mResult.isObject()) { michael@0: return; michael@0: } michael@0: ThreadsafeAutoJSContext cx; michael@0: JS::Rooted obj(cx, &mResult.toObject()); michael@0: JSAutoCompartment ac(cx, obj); michael@0: JSErrorReport* report = JS_ErrorFromException(cx, obj); michael@0: if (!report) { michael@0: return; michael@0: } michael@0: michael@0: // Remains null in case of worker. michael@0: nsCOMPtr win; michael@0: bool isChromeError = false; michael@0: michael@0: if (MOZ_LIKELY(NS_IsMainThread())) { michael@0: win = michael@0: do_QueryInterface(nsJSUtils::GetStaticScriptGlobal(obj)); michael@0: nsIPrincipal* principal = nsContentUtils::GetObjectPrincipal(obj); michael@0: isChromeError = nsContentUtils::IsSystemPrincipal(principal); michael@0: } else { michael@0: WorkerPrivate* worker = GetCurrentThreadWorkerPrivate(); michael@0: MOZ_ASSERT(worker); michael@0: isChromeError = worker->IsChromeWorker(); michael@0: } michael@0: michael@0: // Now post an event to do the real reporting async michael@0: // Since Promises preserve their wrapper, it is essential to nsRefPtr<> the michael@0: // AsyncErrorReporter, otherwise if the call to DispatchToMainThread fails, it michael@0: // will leak. See Bug 958684. michael@0: nsRefPtr r = michael@0: new AsyncErrorReporter(JS_GetObjectRuntime(obj), michael@0: report, michael@0: nullptr, michael@0: isChromeError, michael@0: win); michael@0: NS_DispatchToMainThread(r); michael@0: } michael@0: michael@0: void michael@0: Promise::MaybeResolveInternal(JSContext* aCx, michael@0: JS::Handle aValue, michael@0: PromiseTaskSync aAsynchronous) michael@0: { michael@0: if (mResolvePending) { michael@0: return; michael@0: } michael@0: michael@0: ResolveInternal(aCx, aValue, aAsynchronous); michael@0: } michael@0: michael@0: void michael@0: Promise::MaybeRejectInternal(JSContext* aCx, michael@0: JS::Handle aValue, michael@0: PromiseTaskSync aAsynchronous) michael@0: { michael@0: if (mResolvePending) { michael@0: return; michael@0: } michael@0: michael@0: RejectInternal(aCx, aValue, aAsynchronous); michael@0: } michael@0: michael@0: void michael@0: Promise::HandleException(JSContext* aCx) michael@0: { michael@0: JS::Rooted exn(aCx); michael@0: if (JS_GetPendingException(aCx, &exn)) { michael@0: JS_ClearPendingException(aCx); michael@0: RejectInternal(aCx, exn, SyncTask); michael@0: } michael@0: } michael@0: michael@0: void michael@0: Promise::ResolveInternal(JSContext* aCx, michael@0: JS::Handle aValue, michael@0: PromiseTaskSync aAsynchronous) michael@0: { michael@0: mResolvePending = true; michael@0: michael@0: if (aValue.isObject()) { michael@0: AutoDontReportUncaught silenceReporting(aCx); michael@0: JS::Rooted valueObj(aCx, &aValue.toObject()); michael@0: michael@0: // Thenables. michael@0: JS::Rooted then(aCx); michael@0: if (!JS_GetProperty(aCx, valueObj, "then", &then)) { michael@0: HandleException(aCx); michael@0: return; michael@0: } michael@0: michael@0: if (then.isObject() && JS_ObjectIsCallable(aCx, &then.toObject())) { michael@0: JS::Rooted resolveFunc(aCx, michael@0: CreateThenableFunction(aCx, this, PromiseCallback::Resolve)); michael@0: michael@0: if (!resolveFunc) { michael@0: HandleException(aCx); michael@0: return; michael@0: } michael@0: michael@0: JS::Rooted rejectFunc(aCx, michael@0: CreateThenableFunction(aCx, this, PromiseCallback::Reject)); michael@0: if (!rejectFunc) { michael@0: HandleException(aCx); michael@0: return; michael@0: } michael@0: michael@0: LinkThenableCallables(aCx, resolveFunc, rejectFunc); michael@0: michael@0: JS::Rooted thenObj(aCx, &then.toObject()); michael@0: nsRefPtr thenCallback = michael@0: new PromiseInit(thenObj, mozilla::dom::GetIncumbentGlobal()); michael@0: michael@0: ErrorResult rv; michael@0: thenCallback->Call(valueObj, resolveFunc, rejectFunc, michael@0: rv, CallbackObject::eRethrowExceptions); michael@0: rv.WouldReportJSException(); michael@0: michael@0: if (rv.IsJSException()) { michael@0: JS::Rooted exn(aCx); michael@0: rv.StealJSException(aCx, &exn); michael@0: michael@0: bool couldMarkAsCalled = MarkAsCalledIfNotCalledBefore(aCx, resolveFunc); michael@0: michael@0: // If we could mark as called, neither of the callbacks had been called michael@0: // when the exception was thrown. So we can reject the Promise. michael@0: if (couldMarkAsCalled) { michael@0: bool ok = JS_WrapValue(aCx, &exn); michael@0: MOZ_ASSERT(ok); michael@0: if (!ok) { michael@0: NS_WARNING("Failed to wrap value into the right compartment."); michael@0: } michael@0: michael@0: RejectInternal(aCx, exn, Promise::SyncTask); michael@0: } michael@0: // At least one of resolveFunc or rejectFunc have been called, so ignore michael@0: // the exception. FIXME(nsm): This should be reported to the error michael@0: // console though, for debugging. michael@0: } michael@0: michael@0: return; michael@0: } michael@0: } michael@0: michael@0: // If the synchronous flag is set, process our resolve callbacks with michael@0: // value. Otherwise, the synchronous flag is unset, queue a task to process michael@0: // own resolve callbacks with value. Otherwise, the synchronous flag is michael@0: // unset, queue a task to process our resolve callbacks with value. michael@0: RunResolveTask(aValue, Resolved, aAsynchronous); michael@0: } michael@0: michael@0: void michael@0: Promise::RejectInternal(JSContext* aCx, michael@0: JS::Handle aValue, michael@0: PromiseTaskSync aAsynchronous) michael@0: { michael@0: mResolvePending = true; michael@0: michael@0: // If the synchronous flag is set, process our reject callbacks with michael@0: // value. Otherwise, the synchronous flag is unset, queue a task to process michael@0: // promise's reject callbacks with value. michael@0: RunResolveTask(aValue, Rejected, aAsynchronous); michael@0: } michael@0: michael@0: void michael@0: Promise::RunResolveTask(JS::Handle aValue, michael@0: PromiseState aState, michael@0: PromiseTaskSync aAsynchronous) michael@0: { michael@0: // If the synchronous flag is unset, queue a task to process our michael@0: // accept callbacks with value. michael@0: if (aAsynchronous == AsyncTask) { michael@0: if (MOZ_LIKELY(NS_IsMainThread())) { michael@0: nsRefPtr task = michael@0: new PromiseResolverTask(this, aValue, aState); michael@0: NS_DispatchToCurrentThread(task); michael@0: } else { michael@0: WorkerPrivate* worker = GetCurrentThreadWorkerPrivate(); michael@0: MOZ_ASSERT(worker); michael@0: nsRefPtr task = michael@0: new WorkerPromiseResolverTask(worker, this, aValue, aState); michael@0: task->Dispatch(worker->GetJSContext()); michael@0: } michael@0: return; michael@0: } michael@0: michael@0: // Promise.all() or Promise.race() implementations will repeatedly call michael@0: // Resolve/RejectInternal rather than using the Maybe... forms. Stop SetState michael@0: // from asserting. michael@0: if (mState != Pending) { michael@0: return; michael@0: } michael@0: michael@0: SetResult(aValue); michael@0: SetState(aState); michael@0: michael@0: // If the Promise was rejected, and there is no reject handler already setup, michael@0: // watch for thread shutdown. michael@0: if (aState == PromiseState::Rejected && michael@0: !mHadRejectCallback && michael@0: !NS_IsMainThread()) { michael@0: WorkerPrivate* worker = GetCurrentThreadWorkerPrivate(); michael@0: MOZ_ASSERT(worker); michael@0: worker->AssertIsOnWorkerThread(); michael@0: michael@0: mFeature = new PromiseReportRejectFeature(this); michael@0: if (NS_WARN_IF(!worker->AddFeature(worker->GetJSContext(), mFeature))) { michael@0: // To avoid a false RemoveFeature(). michael@0: mFeature = nullptr; michael@0: // Worker is shutting down, report rejection immediately since it is michael@0: // unlikely that reject callbacks will be added after this point. michael@0: MaybeReportRejectedOnce(); michael@0: } michael@0: } michael@0: michael@0: RunTask(); michael@0: } michael@0: michael@0: void michael@0: Promise::RemoveFeature() michael@0: { michael@0: if (mFeature) { michael@0: WorkerPrivate* worker = GetCurrentThreadWorkerPrivate(); michael@0: MOZ_ASSERT(worker); michael@0: worker->RemoveFeature(worker->GetJSContext(), mFeature); michael@0: mFeature = nullptr; michael@0: } michael@0: } michael@0: michael@0: bool michael@0: PromiseReportRejectFeature::Notify(JSContext* aCx, workers::Status aStatus) michael@0: { michael@0: MOZ_ASSERT(aStatus > workers::Running); michael@0: mPromise->MaybeReportRejectedOnce(); michael@0: // After this point, `this` has been deleted by RemoveFeature! michael@0: return true; michael@0: } michael@0: michael@0: } // namespace dom michael@0: } // namespace mozilla