summaryrefslogtreecommitdiffstats
path: root/dom/abort
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
commit26a029d407be480d791972afb5975cf62c9360a6 (patch)
treef435a8308119effd964b339f76abb83a57c29483 /dom/abort
parentInitial commit. (diff)
downloadfirefox-upstream/124.0.1.tar.xz
firefox-upstream/124.0.1.zip
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'dom/abort')
-rw-r--r--dom/abort/AbortController.cpp80
-rw-r--r--dom/abort/AbortController.h56
-rw-r--r--dom/abort/AbortFollower.h97
-rw-r--r--dom/abort/AbortSignal.cpp401
-rw-r--r--dom/abort/AbortSignal.h80
-rw-r--r--dom/abort/moz.build25
-rw-r--r--dom/abort/tests/mochitest.toml12
-rw-r--r--dom/abort/tests/moz.build9
-rw-r--r--dom/abort/tests/slow.sjs13
-rw-r--r--dom/abort/tests/test_abort_controller.html73
-rw-r--r--dom/abort/tests/test_abort_controller_fetch.html80
-rw-r--r--dom/abort/tests/test_event_listener_leaks.html43
-rw-r--r--dom/abort/tests/unit/test_abort.js8
-rw-r--r--dom/abort/tests/unit/xpcshell.toml5
-rw-r--r--dom/abort/tests/worker_abort_controller_fetch.js33
15 files changed, 1015 insertions, 0 deletions
diff --git a/dom/abort/AbortController.cpp b/dom/abort/AbortController.cpp
new file mode 100644
index 0000000000..9bd8202559
--- /dev/null
+++ b/dom/abort/AbortController.cpp
@@ -0,0 +1,80 @@
+/* -*- 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 "AbortController.h"
+#include "AbortSignal.h"
+#include "js/Value.h"
+#include "mozilla/dom/AbortControllerBinding.h"
+#include "mozilla/dom/BindingUtils.h"
+#include "mozilla/dom/DOMException.h"
+#include "mozilla/dom/WorkerPrivate.h"
+#include "mozilla/HoldDropJSObjects.h"
+
+namespace mozilla::dom {
+
+NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE_WITH_JS_MEMBERS(AbortController,
+ (mGlobal, mSignal),
+ (mReason))
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(AbortController)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(AbortController)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(AbortController)
+ NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
+ NS_INTERFACE_MAP_ENTRY(nsISupports)
+NS_INTERFACE_MAP_END
+
+/* static */
+already_AddRefed<AbortController> AbortController::Constructor(
+ const GlobalObject& aGlobal, ErrorResult& aRv) {
+ nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(aGlobal.GetAsSupports());
+ if (!global) {
+ aRv.Throw(NS_ERROR_FAILURE);
+ return nullptr;
+ }
+
+ RefPtr<AbortController> abortController = new AbortController(global);
+ return abortController.forget();
+}
+
+AbortController::AbortController(nsIGlobalObject* aGlobal)
+ : mGlobal(aGlobal), mAborted(false), mReason(JS::UndefinedHandleValue) {
+ mozilla::HoldJSObjects(this);
+}
+
+JSObject* AbortController::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return AbortController_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+nsIGlobalObject* AbortController::GetParentObject() const { return mGlobal; }
+
+AbortSignal* AbortController::Signal() {
+ if (!mSignal) {
+ JS::Rooted<JS::Value> reason(RootingCx(), mReason);
+ mSignal = new AbortSignal(mGlobal, mAborted, reason);
+ }
+
+ return mSignal;
+}
+
+void AbortController::Abort(JSContext* aCx, JS::Handle<JS::Value> aReason) {
+ if (mAborted) {
+ return;
+ }
+
+ mAborted = true;
+
+ if (mSignal) {
+ mSignal->SignalAbort(aReason);
+ } else {
+ mReason = aReason;
+ }
+}
+
+AbortController::~AbortController() { mozilla::DropJSObjects(this); }
+
+} // namespace mozilla::dom
diff --git a/dom/abort/AbortController.h b/dom/abort/AbortController.h
new file mode 100644
index 0000000000..0af33453cf
--- /dev/null
+++ b/dom/abort/AbortController.h
@@ -0,0 +1,56 @@
+/* -*- 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/. */
+
+#ifndef mozilla_dom_AbortController_h
+#define mozilla_dom_AbortController_h
+
+#include "mozilla/dom/BindingDeclarations.h"
+#include "nsCOMPtr.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsWrapperCache.h"
+
+class nsIGlobalObject;
+
+namespace mozilla {
+class ErrorResult;
+
+namespace dom {
+
+class AbortSignal;
+
+class AbortController : public nsISupports, public nsWrapperCache {
+ public:
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(AbortController)
+
+ static already_AddRefed<AbortController> Constructor(
+ const GlobalObject& aGlobal, ErrorResult& aRv);
+
+ explicit AbortController(nsIGlobalObject* aGlobal);
+
+ JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ nsIGlobalObject* GetParentObject() const;
+
+ AbortSignal* Signal();
+
+ void Abort(JSContext* aCx, JS::Handle<JS::Value> aReason);
+
+ protected:
+ virtual ~AbortController();
+
+ nsCOMPtr<nsIGlobalObject> mGlobal;
+ RefPtr<AbortSignal> mSignal;
+
+ bool mAborted;
+ JS::Heap<JS::Value> mReason;
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif // mozilla_dom_AbortController_h
diff --git a/dom/abort/AbortFollower.h b/dom/abort/AbortFollower.h
new file mode 100644
index 0000000000..8c2509781a
--- /dev/null
+++ b/dom/abort/AbortFollower.h
@@ -0,0 +1,97 @@
+/* -*- 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/. */
+
+#ifndef mozilla_dom_AbortFollower_h
+#define mozilla_dom_AbortFollower_h
+
+#include "jsapi.h"
+#include "nsISupportsImpl.h"
+#include "nsTObserverArray.h"
+#include "mozilla/WeakPtr.h"
+
+namespace mozilla::dom {
+
+class AbortSignal;
+class AbortSignalImpl;
+
+// This class must be implemented by objects who want to follow an
+// AbortSignalImpl.
+class AbortFollower : public nsISupports {
+ public:
+ virtual void RunAbortAlgorithm() = 0;
+
+ // This adds strong reference to this follower on the signal, which means
+ // you'll need to call Unfollow() to prevent your object from living
+ // needlessly longer.
+ void Follow(AbortSignalImpl* aSignal);
+
+ // Explicitly call this to let garbage collection happen sooner when the
+ // follower finished its work and cannot be aborted anymore.
+ void Unfollow();
+
+ bool IsFollowing() const;
+
+ AbortSignalImpl* Signal() const { return mFollowingSignal; }
+
+ protected:
+ virtual ~AbortFollower();
+
+ friend class AbortSignalImpl;
+
+ WeakPtr<AbortSignalImpl> mFollowingSignal;
+};
+
+/*
+ * AbortSignalImpl is a minimal implementation without an associated global
+ * and without event dispatching, those are added in AbortSignal.
+ * See Bug 1478101
+ */
+class AbortSignalImpl : public nsISupports, public SupportsWeakPtr {
+ public:
+ explicit AbortSignalImpl(bool aAborted, JS::Handle<JS::Value> aReason);
+
+ bool Aborted() const;
+
+ // Web IDL Layer
+ void GetReason(JSContext* aCx, JS::MutableHandle<JS::Value> aReason);
+ // Helper for other DOM code
+ JS::Value RawReason() const;
+
+ virtual void SignalAbort(JS::Handle<JS::Value> aReason);
+
+ protected:
+ // Subclasses of this class must call these Traverse and Unlink functions
+ // during corresponding cycle collection operations.
+ static void Traverse(AbortSignalImpl* aSignal,
+ nsCycleCollectionTraversalCallback& cb);
+
+ static void Unlink(AbortSignalImpl* aSignal);
+
+ virtual ~AbortSignalImpl() { UnlinkFollowers(); }
+
+ void SetAborted(JS::Handle<JS::Value> aReason);
+
+ JS::Heap<JS::Value> mReason;
+
+ private:
+ friend class AbortFollower;
+
+ void MaybeAssignAbortError(JSContext* aCx);
+
+ void UnlinkFollowers();
+
+ // Raw pointers. |AbortFollower::Follow| adds to this array, and
+ // |AbortFollower::Unfollow| (also called by the destructor) will remove
+ // from this array. Finally, calling |SignalAbort()| will (after running all
+ // abort algorithms) empty this and make all contained followers |Unfollow()|.
+ nsTObserverArray<RefPtr<AbortFollower>> mFollowers;
+
+ bool mAborted;
+};
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_AbortFollower_h
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
diff --git a/dom/abort/AbortSignal.h b/dom/abort/AbortSignal.h
new file mode 100644
index 0000000000..b5732cb022
--- /dev/null
+++ b/dom/abort/AbortSignal.h
@@ -0,0 +1,80 @@
+/* -*- 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/. */
+
+#ifndef mozilla_dom_AbortSignal_h
+#define mozilla_dom_AbortSignal_h
+
+#include "mozilla/RefPtr.h"
+#include "mozilla/dom/AbortFollower.h"
+#include "mozilla/DOMEventTargetHelper.h"
+
+namespace mozilla::dom {
+
+// AbortSignal the spec concept includes the concept of a child signal
+// "following" a parent signal -- internally, adding abort steps to the parent
+// signal that will then signal abort on the child signal -- to propagate
+// signaling abort from one signal to another. See
+// <https://dom.spec.whatwg.org/#abortsignal-follow>.
+//
+// This requires that AbortSignal also inherit from AbortFollower.
+//
+// This ability to follow isn't directly exposed in the DOM; as of this writing
+// it appears only to be used internally in the Fetch API. It might be a good
+// idea to split AbortSignal into an implementation that can follow, and an
+// implementation that can't, to provide this complexity only when it's needed.
+class AbortSignal : public DOMEventTargetHelper,
+ public AbortSignalImpl,
+ public AbortFollower {
+ public:
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS_INHERITED(AbortSignal,
+ DOMEventTargetHelper)
+
+ AbortSignal(nsIGlobalObject* aGlobalObject, bool aAborted,
+ JS::Handle<JS::Value> aReason);
+
+ JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ IMPL_EVENT_HANDLER(abort);
+
+ static already_AddRefed<AbortSignal> Abort(GlobalObject& aGlobal,
+ JS::Handle<JS::Value> aReason);
+
+ static already_AddRefed<AbortSignal> Timeout(GlobalObject& aGlobal,
+ uint64_t aMilliseconds,
+ ErrorResult& aRv);
+
+ static already_AddRefed<AbortSignal> Any(
+ GlobalObject& aGlobal,
+ const Sequence<OwningNonNull<AbortSignal>>& aSignals);
+
+ void ThrowIfAborted(JSContext* aCx, ErrorResult& aRv);
+
+ // AbortSignalImpl
+ void SignalAbort(JS::Handle<JS::Value> aReason) override;
+
+ // AbortFollower
+ void RunAbortAlgorithm() override;
+
+ virtual bool IsTaskSignal() const { return false; }
+
+ bool Dependent() const;
+
+ protected:
+ ~AbortSignal();
+
+ void MakeDependentOn(AbortSignal* aSignal);
+
+ nsTArray<WeakPtr<AbortSignal>> mSourceSignals;
+ nsTArray<RefPtr<AbortSignal>> mDependentSignals;
+
+ bool mDependent;
+};
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_AbortSignal_h
diff --git a/dom/abort/moz.build b/dom/abort/moz.build
new file mode 100644
index 0000000000..eb5b25584a
--- /dev/null
+++ b/dom/abort/moz.build
@@ -0,0 +1,25 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+with Files("**"):
+ BUG_COMPONENT = ("Core", "DOM: Core & HTML")
+
+TEST_DIRS += ["tests"]
+
+EXPORTS.mozilla.dom += [
+ "AbortController.h",
+ "AbortFollower.h",
+ "AbortSignal.h",
+]
+
+UNIFIED_SOURCES += [
+ "AbortController.cpp",
+ "AbortSignal.cpp",
+]
+
+include("/ipc/chromium/chromium-config.mozbuild")
+
+FINAL_LIBRARY = "xul"
diff --git a/dom/abort/tests/mochitest.toml b/dom/abort/tests/mochitest.toml
new file mode 100644
index 0000000000..9faf48e655
--- /dev/null
+++ b/dom/abort/tests/mochitest.toml
@@ -0,0 +1,12 @@
+[DEFAULT]
+support-files = [
+ "worker_abort_controller_fetch.js",
+ "slow.sjs",
+ "!/dom/events/test/event_leak_utils.js",
+]
+
+["test_abort_controller.html"]
+
+["test_abort_controller_fetch.html"]
+
+["test_event_listener_leaks.html"]
diff --git a/dom/abort/tests/moz.build b/dom/abort/tests/moz.build
new file mode 100644
index 0000000000..e69f90fccf
--- /dev/null
+++ b/dom/abort/tests/moz.build
@@ -0,0 +1,9 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+MOCHITEST_MANIFESTS += ["mochitest.toml"]
+
+XPCSHELL_TESTS_MANIFESTS += ["unit/xpcshell.toml"]
diff --git a/dom/abort/tests/slow.sjs b/dom/abort/tests/slow.sjs
new file mode 100644
index 0000000000..33a9c76b81
--- /dev/null
+++ b/dom/abort/tests/slow.sjs
@@ -0,0 +1,13 @@
+function handleRequest(request, response) {
+ response.processAsync();
+
+ let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+ timer.init(
+ function () {
+ response.write("Here the content. But slowly.");
+ response.finish();
+ },
+ 1000,
+ Ci.nsITimer.TYPE_ONE_SHOT
+ );
+}
diff --git a/dom/abort/tests/test_abort_controller.html b/dom/abort/tests/test_abort_controller.html
new file mode 100644
index 0000000000..a7181711d5
--- /dev/null
+++ b/dom/abort/tests/test_abort_controller.html
@@ -0,0 +1,73 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test AbortController</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<script class="testbody" type="text/javascript">
+
+function testWebIDL() {
+ ok("AbortController" in self, "We have a AbortController prototype");
+ ok("AbortSignal" in self, "We have a AbortSignal prototype");
+
+ var ac = new AbortController();
+ ok(!!ac, "AbortController can be created");
+ ok(ac instanceof AbortController, "AbortController is a AbortController");
+
+ ok(!!ac.signal, "AbortController has a signal");
+ ok(ac.signal instanceof AbortSignal, "abortSignal is a AbortSignal");
+ is(ac.signal.aborted, false, "By default AbortSignal.aborted is false");
+ next();
+}
+
+function testUpdateData() {
+ var ac = new AbortController();
+
+ is(ac.signal.aborted, false, "By default AbortSignal.aborted is false");
+
+ ac.abort();
+ is(ac.signal.aborted, true, "Signal is aborted");
+
+ next();
+}
+
+function testAbortEvent() {
+ var ac = new AbortController();
+ ac.signal.onabort = function(e) {
+ is(e.type, "abort", "Abort received");
+ next();
+ };
+ ac.abort();
+}
+
+var steps = [
+ // Simple stuff
+ testWebIDL,
+ testUpdateData,
+
+ // Event propagation
+ testAbortEvent,
+];
+
+function next() {
+ if (!steps.length) {
+ SimpleTest.finish();
+ return;
+ }
+
+ var step = steps.shift();
+ step();
+}
+
+SimpleTest.waitForExplicitFinish();
+next();
+
+</script>
+</body>
+</html>
diff --git a/dom/abort/tests/test_abort_controller_fetch.html b/dom/abort/tests/test_abort_controller_fetch.html
new file mode 100644
index 0000000000..2342ecda89
--- /dev/null
+++ b/dom/abort/tests/test_abort_controller_fetch.html
@@ -0,0 +1,80 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test AbortController in Fetch API</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<script class="testbody" type="text/javascript">
+
+function testAbortedFetch() {
+ var ac = new AbortController();
+ ac.abort();
+
+ fetch("slow.sjs", { signal: ac.signal }).then(() => {
+ ok(false, "Fetch should not return a resolved promise");
+ }, e => {
+ is(e.name, "AbortError", "We have an abort error");
+ }).then(next);
+}
+
+function testFetchAndAbort() {
+ var ac = new AbortController();
+
+ var p = fetch("slow.sjs", { signal: ac.signal });
+ ac.abort();
+
+ p.then(() => {
+ ok(false, "Fetch should not return a resolved promise");
+ }, e => {
+ is(e.name, "AbortError", "We have an abort error");
+ }).then(next);
+}
+
+function testWorkerAbortedFetch() {
+ var w = new Worker("worker_abort_controller_fetch.js");
+ w.onmessage = function(e) {
+ ok(e.data, "Abort + Fetch works in workers");
+ next();
+ };
+ w.postMessage("testWorkerAbortedFetch");
+}
+
+function testWorkerFetchAndAbort() {
+ var w = new Worker("worker_abort_controller_fetch.js");
+ w.onmessage = function(e) {
+ ok(e.data, "Abort + Fetch works in workers");
+ next();
+ };
+ w.postMessage("testWorkerFetchAndAbort");
+}
+
+var steps = [
+ // fetch + signaling
+ testAbortedFetch,
+ testFetchAndAbort,
+ testWorkerAbortedFetch,
+ testWorkerFetchAndAbort,
+];
+
+function next() {
+ if (!steps.length) {
+ SimpleTest.finish();
+ return;
+ }
+
+ var step = steps.shift();
+ step();
+}
+
+SimpleTest.waitForExplicitFinish();
+next();
+
+</script>
+</body>
+</html>
diff --git a/dom/abort/tests/test_event_listener_leaks.html b/dom/abort/tests/test_event_listener_leaks.html
new file mode 100644
index 0000000000..f9684e2309
--- /dev/null
+++ b/dom/abort/tests/test_event_listener_leaks.html
@@ -0,0 +1,43 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 1450271 - Test AbortSignal event listener leak conditions</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/dom/events/test/event_leak_utils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<script class="testbody" type="text/javascript">
+
+// Manipulate AbortSignal. Its important here that we create a
+// listener callback from the DOM objects back to the frame's global
+// in order to exercise the leak condition.
+async function useAbortSignal(contentWindow) {
+ let controller = new contentWindow.AbortController();
+ let signal = controller.signal;
+ signal.onabort = _ => {
+ contentWindow.abortCount += 1;
+ };
+}
+
+async function runTest() {
+ try {
+ await checkForEventListenerLeaks("AbortSignal", useAbortSignal);
+ } catch (e) {
+ ok(false, e);
+ } finally {
+ SimpleTest.finish();
+ }
+}
+
+SimpleTest.waitForExplicitFinish();
+addEventListener("load", runTest, { once: true });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/abort/tests/unit/test_abort.js b/dom/abort/tests/unit/test_abort.js
new file mode 100644
index 0000000000..c1586443cb
--- /dev/null
+++ b/dom/abort/tests/unit/test_abort.js
@@ -0,0 +1,8 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function run_test() {
+ let ac = new AbortController();
+ Assert.ok(ac instanceof AbortController);
+ Assert.ok(ac.signal instanceof AbortSignal);
+}
diff --git a/dom/abort/tests/unit/xpcshell.toml b/dom/abort/tests/unit/xpcshell.toml
new file mode 100644
index 0000000000..2285dc8092
--- /dev/null
+++ b/dom/abort/tests/unit/xpcshell.toml
@@ -0,0 +1,5 @@
+[DEFAULT]
+head = ""
+support-files = ""
+
+["test_abort.js"]
diff --git a/dom/abort/tests/worker_abort_controller_fetch.js b/dom/abort/tests/worker_abort_controller_fetch.js
new file mode 100644
index 0000000000..571d9ffc7e
--- /dev/null
+++ b/dom/abort/tests/worker_abort_controller_fetch.js
@@ -0,0 +1,33 @@
+function testWorkerAbortedFetch() {
+ var ac = new AbortController();
+ ac.abort();
+
+ fetch("slow.sjs", { signal: ac.signal }).then(
+ () => {
+ postMessage(false);
+ },
+ e => {
+ postMessage(e.name == "AbortError");
+ }
+ );
+}
+
+function testWorkerFetchAndAbort() {
+ var ac = new AbortController();
+
+ var p = fetch("slow.sjs", { signal: ac.signal });
+ ac.abort();
+
+ p.then(
+ () => {
+ postMessage(false);
+ },
+ e => {
+ postMessage(e.name == "AbortError");
+ }
+ );
+}
+
+self.onmessage = function (e) {
+ self[e.data]();
+};