summaryrefslogtreecommitdiffstats
path: root/dom/abort/AbortSignal.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'dom/abort/AbortSignal.cpp')
-rw-r--r--dom/abort/AbortSignal.cpp401
1 files changed, 401 insertions, 0 deletions
diff --git a/dom/abort/AbortSignal.cpp b/dom/abort/AbortSignal.cpp
new file mode 100644
index 0000000000..de5f24c080
--- /dev/null
+++ b/dom/abort/AbortSignal.cpp
@@ -0,0 +1,401 @@
+/* -*- 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 "AbortSignal.h"
+
+#include "mozilla/dom/AbortSignalBinding.h"
+#include "mozilla/dom/DOMException.h"
+#include "mozilla/dom/Event.h"
+#include "mozilla/dom/EventBinding.h"
+#include "mozilla/dom/TimeoutHandler.h"
+#include "mozilla/dom/TimeoutManager.h"
+#include "mozilla/dom/ToJSValue.h"
+#include "mozilla/dom/WorkerPrivate.h"
+#include "mozilla/RefPtr.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsPIDOMWindow.h"
+
+namespace mozilla::dom {
+
+// AbortSignalImpl
+// ----------------------------------------------------------------------------
+
+AbortSignalImpl::AbortSignalImpl(bool aAborted, JS::Handle<JS::Value> aReason)
+ : mReason(aReason), mAborted(aAborted) {
+ MOZ_ASSERT_IF(!mReason.isUndefined(), mAborted);
+}
+
+bool AbortSignalImpl::Aborted() const { return mAborted; }
+
+void AbortSignalImpl::GetReason(JSContext* aCx,
+ JS::MutableHandle<JS::Value> aReason) {
+ if (!mAborted) {
+ return;
+ }
+ MaybeAssignAbortError(aCx);
+ aReason.set(mReason);
+}
+
+JS::Value AbortSignalImpl::RawReason() const { return mReason.get(); }
+
+// https://dom.spec.whatwg.org/#abortsignal-signal-abort steps 1-4
+void AbortSignalImpl::SignalAbort(JS::Handle<JS::Value> aReason) {
+ // Step 1.
+ if (mAborted) {
+ return;
+ }
+
+ // Step 2.
+ SetAborted(aReason);
+
+ // Step 3.
+ // When there are multiple followers, the follower removal algorithm
+ // https://dom.spec.whatwg.org/#abortsignal-remove could be invoked in an
+ // earlier algorithm to remove a later algorithm, so |mFollowers| must be a
+ // |nsTObserverArray| to defend against mutation.
+ for (RefPtr<AbortFollower>& follower : mFollowers.ForwardRange()) {
+ MOZ_ASSERT(follower->mFollowingSignal == this);
+ follower->RunAbortAlgorithm();
+ }
+
+ // Step 4.
+ UnlinkFollowers();
+}
+
+void AbortSignalImpl::SetAborted(JS::Handle<JS::Value> aReason) {
+ mAborted = true;
+ mReason = aReason;
+}
+
+void AbortSignalImpl::Traverse(AbortSignalImpl* aSignal,
+ nsCycleCollectionTraversalCallback& cb) {
+ ImplCycleCollectionTraverse(cb, aSignal->mFollowers, "mFollowers", 0);
+}
+
+void AbortSignalImpl::Unlink(AbortSignalImpl* aSignal) {
+ aSignal->mReason.setUndefined();
+ aSignal->UnlinkFollowers();
+}
+
+void AbortSignalImpl::MaybeAssignAbortError(JSContext* aCx) {
+ MOZ_ASSERT(mAborted);
+ if (!mReason.isUndefined()) {
+ return;
+ }
+
+ JS::Rooted<JS::Value> exception(aCx);
+ RefPtr<DOMException> dom = DOMException::Create(NS_ERROR_DOM_ABORT_ERR);
+
+ if (NS_WARN_IF(!ToJSValue(aCx, dom, &exception))) {
+ return;
+ }
+
+ mReason.set(exception);
+}
+
+void AbortSignalImpl::UnlinkFollowers() {
+ // Manually unlink all followers before destructing the array, or otherwise
+ // the array will be accessed by Unfollow() while being destructed.
+ for (RefPtr<AbortFollower>& follower : mFollowers.ForwardRange()) {
+ follower->mFollowingSignal = nullptr;
+ }
+ mFollowers.Clear();
+}
+
+// AbortSignal
+// ----------------------------------------------------------------------------
+
+NS_IMPL_CYCLE_COLLECTION_CLASS(AbortSignal)
+
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(AbortSignal,
+ DOMEventTargetHelper)
+ AbortSignalImpl::Traverse(static_cast<AbortSignalImpl*>(tmp), cb);
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mDependentSignals)
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
+
+NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(AbortSignal,
+ DOMEventTargetHelper)
+ AbortSignalImpl::Unlink(static_cast<AbortSignalImpl*>(tmp));
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mDependentSignals)
+NS_IMPL_CYCLE_COLLECTION_UNLINK_END
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(AbortSignal)
+NS_INTERFACE_MAP_END_INHERITING(DOMEventTargetHelper)
+
+NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN_INHERITED(AbortSignal,
+ DOMEventTargetHelper)
+ NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mReason)
+NS_IMPL_CYCLE_COLLECTION_TRACE_END
+
+NS_IMPL_ADDREF_INHERITED(AbortSignal, DOMEventTargetHelper)
+NS_IMPL_RELEASE_INHERITED(AbortSignal, DOMEventTargetHelper)
+
+AbortSignal::AbortSignal(nsIGlobalObject* aGlobalObject, bool aAborted,
+ JS::Handle<JS::Value> aReason)
+ : DOMEventTargetHelper(aGlobalObject),
+ AbortSignalImpl(aAborted, aReason),
+ mDependent(false) {
+ mozilla::HoldJSObjects(this);
+}
+
+JSObject* AbortSignal::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return AbortSignal_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+already_AddRefed<AbortSignal> AbortSignal::Abort(
+ GlobalObject& aGlobal, JS::Handle<JS::Value> aReason) {
+ nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(aGlobal.GetAsSupports());
+
+ RefPtr<AbortSignal> abortSignal = new AbortSignal(global, true, aReason);
+ return abortSignal.forget();
+}
+
+class AbortSignalTimeoutHandler final : public TimeoutHandler {
+ public:
+ AbortSignalTimeoutHandler(JSContext* aCx, AbortSignal* aSignal)
+ : TimeoutHandler(aCx), mSignal(aSignal) {}
+
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_CYCLE_COLLECTION_CLASS(AbortSignalTimeoutHandler)
+
+ // https://dom.spec.whatwg.org/#dom-abortsignal-timeout
+ // Step 3
+ MOZ_CAN_RUN_SCRIPT bool Call(const char* /* unused */) override {
+ AutoJSAPI jsapi;
+ if (NS_WARN_IF(!jsapi.Init(mSignal->GetParentObject()))) {
+ // (false is only for setInterval, see
+ // nsGlobalWindowInner::RunTimeoutHandler)
+ return true;
+ }
+
+ // Step 1. Queue a global task on the timer task source given global to
+ // signal abort given signal and a new "TimeoutError" DOMException.
+ JS::Rooted<JS::Value> exception(jsapi.cx());
+ RefPtr<DOMException> dom = DOMException::Create(NS_ERROR_DOM_TIMEOUT_ERR);
+ if (NS_WARN_IF(!ToJSValue(jsapi.cx(), dom, &exception))) {
+ return true;
+ }
+
+ mSignal->SignalAbort(exception);
+ return true;
+ }
+
+ private:
+ ~AbortSignalTimeoutHandler() override = default;
+
+ RefPtr<AbortSignal> mSignal;
+};
+
+NS_IMPL_CYCLE_COLLECTION(AbortSignalTimeoutHandler, mSignal)
+NS_IMPL_CYCLE_COLLECTING_ADDREF(AbortSignalTimeoutHandler)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(AbortSignalTimeoutHandler)
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(AbortSignalTimeoutHandler)
+ NS_INTERFACE_MAP_ENTRY(nsISupports)
+NS_INTERFACE_MAP_END
+
+static void SetTimeoutForGlobal(GlobalObject& aGlobal, TimeoutHandler& aHandler,
+ int32_t timeout, ErrorResult& aRv) {
+ if (NS_IsMainThread()) {
+ nsCOMPtr<nsPIDOMWindowInner> innerWindow =
+ do_QueryInterface(aGlobal.GetAsSupports());
+ if (!innerWindow) {
+ aRv.ThrowInvalidStateError("Could not find window.");
+ return;
+ }
+
+ int32_t handle;
+ nsresult rv = innerWindow->TimeoutManager().SetTimeout(
+ &aHandler, timeout, /* aIsInterval */ false,
+ Timeout::Reason::eAbortSignalTimeout, &handle);
+ if (NS_FAILED(rv)) {
+ aRv.Throw(rv);
+ return;
+ }
+ } else {
+ WorkerPrivate* workerPrivate =
+ GetWorkerPrivateFromContext(aGlobal.Context());
+ workerPrivate->SetTimeout(aGlobal.Context(), &aHandler, timeout,
+ /* aIsInterval */ false,
+ Timeout::Reason::eAbortSignalTimeout, aRv);
+ if (aRv.Failed()) {
+ return;
+ }
+ }
+}
+
+// https://dom.spec.whatwg.org/#dom-abortsignal-timeout
+already_AddRefed<AbortSignal> AbortSignal::Timeout(GlobalObject& aGlobal,
+ uint64_t aMilliseconds,
+ ErrorResult& aRv) {
+ // Step 2. Let global be signal’s relevant global object.
+ nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(aGlobal.GetAsSupports());
+
+ // Step 1. Let signal be a new AbortSignal object.
+ RefPtr<AbortSignal> signal =
+ new AbortSignal(global, false, JS::UndefinedHandleValue);
+
+ // Step 3. Run steps after a timeout given global, "AbortSignal-timeout",
+ // milliseconds, and the following step: ...
+ RefPtr<TimeoutHandler> handler =
+ new AbortSignalTimeoutHandler(aGlobal.Context(), signal);
+
+ // Note: We only supports int32_t range intervals
+ int32_t timeout =
+ aMilliseconds > uint64_t(std::numeric_limits<int32_t>::max())
+ ? std::numeric_limits<int32_t>::max()
+ : static_cast<int32_t>(aMilliseconds);
+
+ SetTimeoutForGlobal(aGlobal, *handler, timeout, aRv);
+ if (aRv.Failed()) {
+ return nullptr;
+ }
+
+ // Step 4. Return signal.
+ return signal.forget();
+}
+
+// https://dom.spec.whatwg.org/#create-a-dependent-abort-signal
+already_AddRefed<AbortSignal> AbortSignal::Any(
+ GlobalObject& aGlobal,
+ const Sequence<OwningNonNull<AbortSignal>>& aSignals) {
+ nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(aGlobal.GetAsSupports());
+
+ // Step 1. Let resultSignal be a new object implementing AbortSignal using
+ // realm
+ RefPtr<AbortSignal> resultSignal =
+ new AbortSignal(global, false, JS::UndefinedHandleValue);
+
+ // Step 2. For each signal of signals: if signal is aborted, then set
+ // resultSignal's abort reason to signal's abort reason and return
+ // resultSignal.
+ for (const auto& signal : aSignals) {
+ if (signal->Aborted()) {
+ JS::Rooted<JS::Value> reason(RootingCx(), signal->RawReason());
+ resultSignal->SetAborted(reason);
+ return resultSignal.forget();
+ }
+ }
+
+ // Step 3. Set resultSignal's dependent to true
+ resultSignal->mDependent = true;
+
+ // Step 4. For each signal of signals
+ for (const auto& signal : aSignals) {
+ if (!signal->Dependent()) {
+ // Step 4.1. If signal is not dependent, make resultSignal dependent on it
+ resultSignal->MakeDependentOn(signal);
+ } else {
+ // Step 4.2. Otherwise, make resultSignal dependent on its source signals
+ for (const auto& sourceSignal : signal->mSourceSignals) {
+ MOZ_ASSERT(!sourceSignal->Aborted() && !sourceSignal->Dependent());
+ resultSignal->MakeDependentOn(sourceSignal);
+ }
+ }
+ }
+
+ // Step 5. Return resultSignal.
+ return resultSignal.forget();
+}
+
+void AbortSignal::MakeDependentOn(AbortSignal* aSignal) {
+ MOZ_ASSERT(mDependent);
+ MOZ_ASSERT(aSignal);
+ // append only if not already contained in list
+ // https://infra.spec.whatwg.org/#set-append
+ if (!mSourceSignals.Contains(aSignal)) {
+ mSourceSignals.AppendElement(aSignal);
+ }
+ if (!aSignal->mDependentSignals.Contains(this)) {
+ aSignal->mDependentSignals.AppendElement(this);
+ }
+}
+
+// https://dom.spec.whatwg.org/#dom-abortsignal-throwifaborted
+void AbortSignal::ThrowIfAborted(JSContext* aCx, ErrorResult& aRv) {
+ aRv.MightThrowJSException();
+
+ if (Aborted()) {
+ JS::Rooted<JS::Value> reason(aCx);
+ GetReason(aCx, &reason);
+ aRv.ThrowJSException(aCx, reason);
+ }
+}
+
+// https://dom.spec.whatwg.org/#abortsignal-signal-abort
+void AbortSignal::SignalAbort(JS::Handle<JS::Value> aReason) {
+ // Step 1, in case "signal abort" algorithm is called directly
+ if (Aborted()) {
+ return;
+ }
+
+ // Steps 1-4.
+ AbortSignalImpl::SignalAbort(aReason);
+
+ // Step 5. Fire an event named abort at this signal
+ EventInit init;
+ init.mBubbles = false;
+ init.mCancelable = false;
+
+ RefPtr<Event> event = Event::Constructor(this, u"abort"_ns, init);
+ event->SetTrusted(true);
+
+ DispatchEvent(*event);
+
+ // Step 6. Abort dependentSignals of this signal
+ for (const auto& dependant : mDependentSignals) {
+ MOZ_ASSERT(dependant->mSourceSignals.Contains(this));
+ dependant->SignalAbort(aReason);
+ }
+ // clear dependent signals so that they might be garbage collected
+ mDependentSignals.Clear();
+}
+
+void AbortSignal::RunAbortAlgorithm() {
+ JS::Rooted<JS::Value> reason(RootingCx(), Signal()->RawReason());
+ SignalAbort(reason);
+}
+
+bool AbortSignal::Dependent() const { return mDependent; }
+
+AbortSignal::~AbortSignal() { mozilla::DropJSObjects(this); }
+
+// AbortFollower
+// ----------------------------------------------------------------------------
+
+AbortFollower::~AbortFollower() { Unfollow(); }
+
+// https://dom.spec.whatwg.org/#abortsignal-add
+void AbortFollower::Follow(AbortSignalImpl* aSignal) {
+ // Step 1.
+ if (aSignal->mAborted) {
+ return;
+ }
+
+ MOZ_DIAGNOSTIC_ASSERT(aSignal);
+
+ Unfollow();
+
+ // Step 2.
+ mFollowingSignal = aSignal;
+ MOZ_ASSERT(!aSignal->mFollowers.Contains(this));
+ aSignal->mFollowers.AppendElement(this);
+}
+
+// https://dom.spec.whatwg.org/#abortsignal-remove
+void AbortFollower::Unfollow() {
+ if (mFollowingSignal) {
+ // |Unfollow| is called by cycle-collection unlink code that runs in no
+ // guaranteed order. So we can't, symmetric with |Follow| above, assert
+ // that |this| will be found in |mFollowingSignal->mFollowers|.
+ mFollowingSignal->mFollowers.RemoveElement(this);
+ mFollowingSignal = nullptr;
+ }
+}
+
+bool AbortFollower::IsFollowing() const { return !!mFollowingSignal; }
+
+} // namespace mozilla::dom