diff --git a/js/src/jsapi.cpp b/js/src/jsapi.cpp index 1be4e39113..bb143a4998 100644 --- a/js/src/jsapi.cpp +++ b/js/src/jsapi.cpp @@ -4949,6 +4949,13 @@ JS::GetPromiseResult(JS::HandleObject promiseObj) return promise->state() == JS::PromiseState::Fulfilled ? promise->value() : promise->reason(); } +JS_PUBLIC_API(bool) +JS::GetPromiseIsHandled(JS::HandleObject promise) +{ + PromiseObject* promiseObj = &promise->as(); + return !promiseObj->isUnhandled(); +} + JS_PUBLIC_API(JSObject*) JS::GetPromiseAllocationSite(JS::HandleObject promise) { diff --git a/js/src/jsapi.h b/js/src/jsapi.h index 444b9e2ed2..97c085b330 100644 --- a/js/src/jsapi.h +++ b/js/src/jsapi.h @@ -4611,6 +4611,15 @@ GetPromiseID(JS::HandleObject promise); extern JS_PUBLIC_API(JS::Value) GetPromiseResult(JS::HandleObject promise); +/** + * Returns whether the given promise's rejection is already handled or not. + * + * The caller must check the given promise is rejected before checking it's + * handled or not. + */ +extern JS_PUBLIC_API(bool) +GetPromiseIsHandled(JS::HandleObject promise); + /** * Returns a js::SavedFrame linked list of the stack that lead to the given * Promise's allocation. diff --git a/xpcom/base/CycleCollectedJSContext.cpp b/xpcom/base/CycleCollectedJSContext.cpp index bbf8bca623..9d033c33b8 100644 --- a/xpcom/base/CycleCollectedJSContext.cpp +++ b/xpcom/base/CycleCollectedJSContext.cpp @@ -56,6 +56,7 @@ #include "mozilla/CycleCollectedJSContext.h" #include #include "mozilla/ArrayUtils.h" +#include "mozilla/AsyncEventDispatcher.h" #include "mozilla/AutoRestore.h" #include "mozilla/Move.h" #include "mozilla/MemoryReporting.h" @@ -69,6 +70,8 @@ #include "mozilla/dom/Promise.h" #include "mozilla/dom/PromiseBinding.h" #include "mozilla/dom/PromiseDebugging.h" +#include "mozilla/dom/PromiseRejectionEvent.h" +#include "mozilla/dom/PromiseRejectionEventBinding.h" #include "mozilla/dom/ScriptSettings.h" #include "jsprf.h" #include "js/Debug.h" @@ -994,16 +997,53 @@ CycleCollectedJSContext::PromiseRejectionTrackerCallback(JSContext* aCx, PromiseRejectionHandlingState state, void* aData) { -#ifdef DEBUG CycleCollectedJSContext* self = static_cast(aData); -#endif // DEBUG + MOZ_ASSERT(aCx == self->Context()); MOZ_ASSERT(Get() == self); + // TODO: Bug 1549351 - Promise rejection event should not be sent for + // cross-origin scripts + + PromiseArray& aboutToBeNotified = self->mAboutToBeNotifiedRejectedPromises; + PromiseHashtable& unhandled = self->mPendingUnhandledRejections; + uint64_t promiseID = JS::GetPromiseID(aPromise); + if (state == PromiseRejectionHandlingState::Unhandled) { PromiseDebugging::AddUncaughtRejection(aPromise); + RefPtr promise = Promise::CreateFromExisting(xpc::NativeGlobal(aPromise), aPromise); + aboutToBeNotified.AppendElement(promise); + unhandled.Put(promiseID, promise); } else { PromiseDebugging::AddConsumedRejection(aPromise); + for (size_t i = 0; i < aboutToBeNotified.Length(); i++) { + if (aboutToBeNotified[i] && + aboutToBeNotified[i]->PromiseObj() == aPromise) { + // To avoid large amounts of memmoves, we don't shrink the vector + // here. Instead, we filter out nullptrs when iterating over the + // vector later. + aboutToBeNotified[i] = nullptr; + MOZ_ASSERT(unhandled.Get(promiseID, nullptr)); + unhandled.Remove(promiseID); + return; + } + } + RefPtr promise; + unhandled.Remove(promiseID, getter_AddRefs(promise)); + if (!promise) { + nsIGlobalObject* global = xpc::NativeGlobal(aPromise); + if (nsCOMPtr owner = do_QueryInterface(global)) { + PromiseRejectionEventInit init; + init.mPromise = Promise::CreateFromExisting(global, aPromise); + init.mReason = JS::GetPromiseResult(aPromise); + + RefPtr event = PromiseRejectionEvent::Constructor(owner, + NS_LITERAL_STRING("rejectionhandled"), init); + + RefPtr asyncDispatcher = new AsyncEventDispatcher(owner, event); + asyncDispatcher->PostDOMEvent(); + } + } } } @@ -1415,6 +1455,13 @@ void CycleCollectedJSContext::AfterProcessMicrotasks() { MOZ_ASSERT(mJSContext); + // Notify unhandled promise rejections: + // https://html.spec.whatwg.org/multipage/webappapis.html#notify-about-rejected-promises + if (mAboutToBeNotifiedRejectedPromises.Length()) { + RefPtr runnable = new NotifyUnhandledRejections( + this, std::move(mAboutToBeNotifiedRejectedPromises)); + NS_DispatchToCurrentThread(runnable); + } // Cleanup Indexed Database transactions: // https://html.spec.whatwg.org/multipage/webappapis.html#perform-a-microtask-checkpoint CleanupIDBTransactions(RecursionDepth()); @@ -1783,6 +1830,64 @@ CycleCollectedJSContext::PerformDebuggerMicroTaskCheckpoint() AfterProcessMicrotasks(); } +NS_IMETHODIMP +CycleCollectedJSContext::NotifyUnhandledRejections::Run() +{ + for (size_t i = 0; i < mUnhandledRejections.Length(); ++i) { + RefPtr& promise = mUnhandledRejections[i]; + if (!promise) + continue; + + JS::RootedObject promiseObj(mCx->RootingCx(), promise->PromiseObj()); + MOZ_ASSERT(JS::IsPromiseObject(promiseObj)); + + // Only fire unhandledrejection if the promise is still not handled; + uint64_t promiseID = JS::GetPromiseID(promiseObj); + if (!JS::GetPromiseIsHandled(promiseObj)) { + if (nsCOMPtr target = + do_QueryInterface(promise->GetParentObject())) { + PromiseRejectionEventInit init; + init.mPromise = promise; + init.mReason = JS::GetPromiseResult(promiseObj); + init.mCancelable = true; + + RefPtr event = + PromiseRejectionEvent::Constructor( + target, NS_LITERAL_STRING("unhandledrejection"), init); + // We don't use the result of dispatching event here to check whether to + // report the Promise to console. + bool dummy = true; + target->DispatchEvent(event, &dummy); + } + } + + if (!JS::GetPromiseIsHandled(promiseObj)) { + MOZ_ASSERT(mCx->mPendingUnhandledRejections.Get(promiseID, nullptr)); + mCx->mPendingUnhandledRejections.Remove(promiseID); + } + + // If a rejected promise is being handled in "unhandledrejection" event + // handler, it should be removed from the table in + // PromiseRejectionTrackerCallback. + MOZ_ASSERT(!mCx->mPendingUnhandledRejections.Get(promiseID, nullptr)); + } + return NS_OK; +} + +nsresult +CycleCollectedJSContext::NotifyUnhandledRejections::Cancel() +{ + for (size_t i = 0; i < mUnhandledRejections.Length(); ++i) { + RefPtr& promise = mUnhandledRejections[i]; + if (!promise) + continue; + + JS::RootedObject promiseObj(mCx->RootingCx(), promise->PromiseObj()); + mCx->mPendingUnhandledRejections.Remove(JS::GetPromiseID(promiseObj)); + } + return NS_OK; +} + void CycleCollectedJSContext::EnvironmentPreparer::invoke(JS::HandleObject scope, js::ScriptEnvironmentPreparer::Closure& closure) diff --git a/xpcom/base/CycleCollectedJSContext.h b/xpcom/base/CycleCollectedJSContext.h index d106d54ae9..63669bffa7 100644 --- a/xpcom/base/CycleCollectedJSContext.h +++ b/xpcom/base/CycleCollectedJSContext.h @@ -12,6 +12,7 @@ #include "mozilla/mozalloc.h" #include "mozilla/MemoryReporting.h" #include "mozilla/SegmentedVector.h" +#include "mozilla/dom/Promise.h" #include "jsapi.h" #include "jsfriendapi.h" @@ -474,6 +475,47 @@ private: uint32_t mMicroTaskRecursionDepth; + // This implements about-to-be-notified rejected promises list in the spec. + // https://html.spec.whatwg.org/multipage/webappapis.html#about-to-be-notified-rejected-promises-list + typedef nsTArray> PromiseArray; + PromiseArray mAboutToBeNotifiedRejectedPromises; + + // This is for the "outstanding rejected promises weak set" in the spec, + // https://html.spec.whatwg.org/multipage/webappapis.html#outstanding-rejected-promises-weak-set + // We use different data structure and opposite logic here to achieve the same + // effect. Basically this is used for tracking the rejected promise that does + // NOT need firing a rejectionhandled event. We will check the table to see if + // firing rejectionhandled event is required when a rejected promise is being + // handled. + // + // The rejected promise will be stored in the table if + // - it is unhandled, and + // - The unhandledrejection is not yet fired. + // + // And be removed when + // - it is handled, or + // - A unhandledrejection is fired and it isn't being handled in event + // handler. + typedef nsRefPtrHashtable PromiseHashtable; + PromiseHashtable mPendingUnhandledRejections; + + class NotifyUnhandledRejections final : public CancelableRunnable { + public: + NotifyUnhandledRejections(CycleCollectedJSContext* aCx, + PromiseArray&& aPromises) + : CancelableRunnable(), + mCx(aCx), + mUnhandledRejections(std::move(aPromises)) {} + + NS_IMETHOD Run() final; + + nsresult Cancel() final; + + private: + CycleCollectedJSContext* mCx; + PromiseArray mUnhandledRejections; + }; + OOMState mOutOfMemoryState; OOMState mLargeAllocationFailureState;