AbortSignal.cpp (17048B)
1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ 2 /* vim: set ts=8 sts=2 et sw=2 tw=80: */ 3 /* This Source Code Form is subject to the terms of the Mozilla Public 4 * License, v. 2.0. If a copy of the MPL was not distributed with this 5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 6 7 #include "AbortSignal.h" 8 9 #include "mozilla/RefPtr.h" 10 #include "mozilla/dom/AbortSignalBinding.h" 11 #include "mozilla/dom/DOMException.h" 12 #include "mozilla/dom/Event.h" 13 #include "mozilla/dom/EventBinding.h" 14 #include "mozilla/dom/TimeoutHandler.h" 15 #include "mozilla/dom/TimeoutManager.h" 16 #include "mozilla/dom/ToJSValue.h" 17 #include "mozilla/dom/WorkerPrivate.h" 18 #include "nsCycleCollectionParticipant.h" 19 #include "nsGlobalWindowInner.h" 20 #include "nsPIDOMWindow.h" 21 22 namespace mozilla::dom { 23 24 // AbortSignalImpl 25 // ---------------------------------------------------------------------------- 26 27 AbortSignalImpl::AbortSignalImpl(SignalAborted aAborted, 28 JS::Handle<JS::Value> aReason) 29 : mReason(aReason), mAborted(aAborted) { 30 MOZ_ASSERT_IF(!mReason.isUndefined(), Aborted()); 31 } 32 33 bool AbortSignalImpl::Aborted() const { return mAborted == SignalAborted::Yes; } 34 35 void AbortSignalImpl::GetReason(JSContext* aCx, 36 JS::MutableHandle<JS::Value> aReason) { 37 if (!Aborted()) { 38 return; 39 } 40 MaybeAssignAbortError(aCx); 41 aReason.set(mReason); 42 } 43 44 JS::Value AbortSignalImpl::RawReason() const { return mReason.get(); } 45 46 // https://dom.spec.whatwg.org/#abortsignal-signal-abort 47 void AbortSignalImpl::SignalAbort(JS::Handle<JS::Value> aReason) { 48 // Step 1: If signal is aborted, then return. 49 if (Aborted()) { 50 return; 51 } 52 53 // Step 2: Set signal’s abort reason to reason if it is given; otherwise to a 54 // new "AbortError" DOMException. 55 // 56 // (But given AbortSignalImpl is supposed to run without JS context, the 57 // DOMException creation is deferred to the getter.) 58 SetAborted(aReason); 59 60 // Step 3 - 6 61 SignalAbortWithDependents(); 62 } 63 64 void AbortSignalImpl::SignalAbortWithDependents() { 65 // AbortSignalImpl cannot have dependents, so just run abort steps for itself. 66 RunAbortSteps(); 67 } 68 69 // https://dom.spec.whatwg.org/#run-the-abort-steps 70 // This skips event firing as AbortSignalImpl is not supposed to be exposed to 71 // JS. It's done instead in AbortSignal::RunAbortSteps. 72 void AbortSignalImpl::RunAbortSteps() { 73 // Step 1: For each algorithm of signal’s abort algorithms: run algorithm. 74 // 75 // When there are multiple followers, the follower removal algorithm 76 // https://dom.spec.whatwg.org/#abortsignal-remove could be invoked in an 77 // earlier algorithm to remove a later algorithm, so |mFollowers| must be a 78 // |nsTObserverArray| to defend against mutation. 79 for (RefPtr<AbortFollower>& follower : mFollowers.ForwardRange()) { 80 MOZ_ASSERT(follower->mFollowingSignal == this); 81 follower->RunAbortAlgorithm(); 82 } 83 84 // Step 2: Empty signal’s abort algorithms. 85 UnlinkFollowers(); 86 } 87 88 void AbortSignalImpl::SetAborted(JS::Handle<JS::Value> aReason) { 89 mAborted = SignalAborted::Yes; 90 mReason = aReason; 91 } 92 93 void AbortSignalImpl::Traverse(AbortSignalImpl* aSignal, 94 nsCycleCollectionTraversalCallback& cb) { 95 ImplCycleCollectionTraverse(cb, aSignal->mFollowers, "mFollowers", 0); 96 } 97 98 void AbortSignalImpl::Unlink(AbortSignalImpl* aSignal) { 99 aSignal->mReason.setUndefined(); 100 aSignal->UnlinkFollowers(); 101 } 102 103 void AbortSignalImpl::MaybeAssignAbortError(JSContext* aCx) { 104 MOZ_ASSERT(Aborted()); 105 if (!mReason.isUndefined()) { 106 return; 107 } 108 109 JS::Rooted<JS::Value> exception(aCx); 110 RefPtr<DOMException> dom = DOMException::Create(NS_ERROR_DOM_ABORT_ERR); 111 112 if (NS_WARN_IF(!ToJSValue(aCx, dom, &exception))) { 113 return; 114 } 115 116 mReason.set(exception); 117 } 118 119 void AbortSignalImpl::UnlinkFollowers() { 120 // Manually unlink all followers before destructing the array, or otherwise 121 // the array will be accessed by Unfollow() while being destructed. 122 for (RefPtr<AbortFollower>& follower : mFollowers.ForwardRange()) { 123 follower->mFollowingSignal = nullptr; 124 } 125 mFollowers.Clear(); 126 } 127 128 // AbortSignal 129 // ---------------------------------------------------------------------------- 130 131 NS_IMPL_CYCLE_COLLECTION_CLASS(AbortSignal) 132 133 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(AbortSignal, 134 DOMEventTargetHelper) 135 AbortSignalImpl::Traverse(static_cast<AbortSignalImpl*>(tmp), cb); 136 NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mDependentSignals) 137 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END 138 139 NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(AbortSignal, 140 DOMEventTargetHelper) 141 AbortSignalImpl::Unlink(static_cast<AbortSignalImpl*>(tmp)); 142 NS_IMPL_CYCLE_COLLECTION_UNLINK(mDependentSignals) 143 NS_IMPL_CYCLE_COLLECTION_UNLINK_END 144 145 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(AbortSignal) 146 NS_INTERFACE_MAP_END_INHERITING(DOMEventTargetHelper) 147 148 NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN_INHERITED(AbortSignal, 149 DOMEventTargetHelper) 150 NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mReason) 151 NS_IMPL_CYCLE_COLLECTION_TRACE_END 152 153 NS_IMPL_ADDREF_INHERITED(AbortSignal, DOMEventTargetHelper) 154 NS_IMPL_RELEASE_INHERITED(AbortSignal, DOMEventTargetHelper) 155 156 already_AddRefed<AbortSignal> AbortSignal::Create( 157 nsIGlobalObject* aGlobalObject, SignalAborted aAborted, 158 JS::Handle<JS::Value> aReason) { 159 RefPtr<AbortSignal> signal = 160 new AbortSignal(aGlobalObject, aAborted, aReason); 161 signal->Init(); 162 return signal.forget(); 163 } 164 165 void AbortSignal::Init() { 166 // Init is use to separate this HoldJSObjects call to avoid calling 167 // it in the constructor. 168 // 169 // We can't call HoldJSObjects in the constructor because it'll 170 // addref `this` before the vtable is set up properly, so the parent 171 // type gets stored in the CC participant table. This is problematic 172 // for classes that inherit AbortSignal. 173 mozilla::HoldJSObjects(this); 174 } 175 176 AbortSignal::AbortSignal(nsIGlobalObject* aGlobalObject, SignalAborted aAborted, 177 JS::Handle<JS::Value> aReason) 178 : DOMEventTargetHelper(aGlobalObject), 179 AbortSignalImpl(aAborted, aReason), 180 mDependent(false) {} 181 182 JSObject* AbortSignal::WrapObject(JSContext* aCx, 183 JS::Handle<JSObject*> aGivenProto) { 184 return AbortSignal_Binding::Wrap(aCx, this, aGivenProto); 185 } 186 187 already_AddRefed<AbortSignal> AbortSignal::Abort( 188 GlobalObject& aGlobal, JS::Handle<JS::Value> aReason) { 189 nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(aGlobal.GetAsSupports()); 190 191 RefPtr<AbortSignal> abortSignal = 192 AbortSignal::Create(global, SignalAborted::Yes, aReason); 193 return abortSignal.forget(); 194 } 195 196 class AbortSignalTimeoutHandler final : public TimeoutHandler { 197 public: 198 AbortSignalTimeoutHandler(JSContext* aCx, AbortSignal* aSignal) 199 : TimeoutHandler(aCx), mSignal(aSignal) {} 200 201 NS_DECL_CYCLE_COLLECTING_ISUPPORTS 202 NS_DECL_CYCLE_COLLECTION_CLASS(AbortSignalTimeoutHandler) 203 204 // https://dom.spec.whatwg.org/#dom-abortsignal-timeout 205 // Step 3 206 MOZ_CAN_RUN_SCRIPT bool Call(const char* /* unused */) override { 207 AutoJSAPI jsapi; 208 if (NS_WARN_IF(!jsapi.Init(mSignal->GetParentObject()))) { 209 // (false is only for setInterval, see 210 // nsGlobalWindowInner::RunTimeoutHandler) 211 return true; 212 } 213 214 // Step 1. Queue a global task on the timer task source given global to 215 // signal abort given signal and a new "TimeoutError" DOMException. 216 JS::Rooted<JS::Value> exception(jsapi.cx()); 217 RefPtr<DOMException> dom = DOMException::Create(NS_ERROR_DOM_TIMEOUT_ERR); 218 if (NS_WARN_IF(!ToJSValue(jsapi.cx(), dom, &exception))) { 219 return true; 220 } 221 222 mSignal->SignalAbort(exception); 223 return true; 224 } 225 226 private: 227 ~AbortSignalTimeoutHandler() override = default; 228 229 RefPtr<AbortSignal> mSignal; 230 }; 231 232 NS_IMPL_CYCLE_COLLECTION(AbortSignalTimeoutHandler, mSignal) 233 NS_IMPL_CYCLE_COLLECTING_ADDREF(AbortSignalTimeoutHandler) 234 NS_IMPL_CYCLE_COLLECTING_RELEASE(AbortSignalTimeoutHandler) 235 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(AbortSignalTimeoutHandler) 236 NS_INTERFACE_MAP_ENTRY(nsISupports) 237 NS_INTERFACE_MAP_END 238 239 static void SetTimeoutForGlobal(GlobalObject& aGlobal, TimeoutHandler& aHandler, 240 int32_t timeout, ErrorResult& aRv) { 241 if (NS_IsMainThread()) { 242 nsCOMPtr<nsPIDOMWindowInner> innerWindow = 243 do_QueryInterface(aGlobal.GetAsSupports()); 244 if (!innerWindow) { 245 aRv.ThrowInvalidStateError("Could not find window."); 246 return; 247 } 248 249 int32_t handle; 250 nsresult rv = 251 nsGlobalWindowInner::Cast(innerWindow) 252 ->GetTimeoutManager() 253 ->SetTimeout(&aHandler, timeout, /* aIsInterval */ false, 254 Timeout::Reason::eAbortSignalTimeout, &handle); 255 if (NS_FAILED(rv)) { 256 aRv.Throw(rv); 257 return; 258 } 259 } else { 260 WorkerPrivate* workerPrivate = 261 GetWorkerPrivateFromContext(aGlobal.Context()); 262 workerPrivate->SetTimeout(aGlobal.Context(), &aHandler, timeout, 263 /* aIsInterval */ false, 264 Timeout::Reason::eAbortSignalTimeout, aRv); 265 if (aRv.Failed()) { 266 return; 267 } 268 } 269 } 270 271 // https://dom.spec.whatwg.org/#dom-abortsignal-timeout 272 already_AddRefed<AbortSignal> AbortSignal::Timeout(GlobalObject& aGlobal, 273 uint64_t aMilliseconds, 274 ErrorResult& aRv) { 275 // Step 2. Let global be signal’s relevant global object. 276 nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(aGlobal.GetAsSupports()); 277 278 // Step 1. Let signal be a new AbortSignal object. 279 RefPtr<AbortSignal> signal = 280 AbortSignal::Create(global, SignalAborted::No, JS::UndefinedHandleValue); 281 282 // Step 3. Run steps after a timeout given global, "AbortSignal-timeout", 283 // milliseconds, and the following step: ... 284 RefPtr<TimeoutHandler> handler = 285 new AbortSignalTimeoutHandler(aGlobal.Context(), signal); 286 287 // Note: We only supports int32_t range intervals 288 int32_t timeout = 289 aMilliseconds > uint64_t(std::numeric_limits<int32_t>::max()) 290 ? std::numeric_limits<int32_t>::max() 291 : static_cast<int32_t>(aMilliseconds); 292 293 SetTimeoutForGlobal(aGlobal, *handler, timeout, aRv); 294 if (aRv.Failed()) { 295 return nullptr; 296 } 297 298 // Step 4. Return signal. 299 return signal.forget(); 300 } 301 302 // https://dom.spec.whatwg.org/#create-a-dependent-abort-signal 303 already_AddRefed<AbortSignal> AbortSignal::Any( 304 GlobalObject& aGlobal, 305 const Sequence<OwningNonNull<AbortSignal>>& aSignals) { 306 nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(aGlobal.GetAsSupports()); 307 return Any(global, aSignals, [](nsIGlobalObject* aGlobal) { 308 return AbortSignal::Create(aGlobal, SignalAborted::No, 309 JS::UndefinedHandleValue); 310 }); 311 } 312 313 already_AddRefed<AbortSignal> AbortSignal::Any( 314 nsIGlobalObject* aGlobal, 315 const Span<const OwningNonNull<AbortSignal>>& aSignals, 316 FunctionRef<already_AddRefed<AbortSignal>(nsIGlobalObject* aGlobal)> 317 aCreateResultSignal) { 318 // Step 1. Let resultSignal be a new object implementing AbortSignal using 319 // realm 320 RefPtr<AbortSignal> resultSignal = aCreateResultSignal(aGlobal); 321 322 if (!aSignals.IsEmpty()) { 323 // (Prepare for step 2 which uses the reason of this. Cannot use 324 // RawReason because that can cause constructing new DOMException for each 325 // dependent signal instead of sharing the single one.) 326 AutoJSAPI jsapi; 327 if (!jsapi.Init(aGlobal)) { 328 return nullptr; 329 } 330 JSContext* cx = jsapi.cx(); 331 332 // Step 2. For each signal of signals: if signal is aborted, then set 333 // resultSignal's abort reason to signal's abort reason and return 334 // resultSignal. 335 for (const auto& signal : aSignals) { 336 if (signal->Aborted()) { 337 JS::Rooted<JS::Value> reason(cx); 338 signal->GetReason(cx, &reason); 339 resultSignal->SetAborted(reason); 340 return resultSignal.forget(); 341 } 342 } 343 } 344 345 // Step 3. Set resultSignal's dependent to true 346 resultSignal->mDependent = true; 347 348 // Step 4. For each signal of signals 349 for (const auto& signal : aSignals) { 350 if (!signal->Dependent()) { 351 // Step 4.1. If signal is not dependent, make resultSignal dependent on it 352 resultSignal->MakeDependentOn(signal); 353 } else { 354 // Step 4.2. Otherwise, make resultSignal dependent on its source signals 355 for (const auto& sourceSignal : signal->mSourceSignals) { 356 if (!sourceSignal) { 357 // Bug 1908466, sourceSignal might have been garbage collected. 358 // As signal is not aborted, sourceSignal also wasn't. 359 // Thus do not depend on it, as it cannot be aborted anymore. 360 continue; 361 } 362 MOZ_ASSERT(!sourceSignal->Aborted() && !sourceSignal->Dependent()); 363 resultSignal->MakeDependentOn(sourceSignal); 364 } 365 } 366 } 367 368 // Step 5. Return resultSignal. 369 return resultSignal.forget(); 370 } 371 372 void AbortSignal::MakeDependentOn(AbortSignal* aSignal) { 373 MOZ_ASSERT(mDependent); 374 MOZ_ASSERT(aSignal); 375 // append only if not already contained in list 376 // https://infra.spec.whatwg.org/#set-append 377 if (!mSourceSignals.Contains(aSignal)) { 378 mSourceSignals.AppendElement(aSignal); 379 } 380 if (!aSignal->mDependentSignals.Contains(this)) { 381 aSignal->mDependentSignals.AppendElement(this); 382 } 383 } 384 385 // https://dom.spec.whatwg.org/#dom-abortsignal-throwifaborted 386 void AbortSignal::ThrowIfAborted(JSContext* aCx, ErrorResult& aRv) { 387 aRv.MightThrowJSException(); 388 389 if (Aborted()) { 390 JS::Rooted<JS::Value> reason(aCx); 391 GetReason(aCx, &reason); 392 aRv.ThrowJSException(aCx, reason); 393 } 394 } 395 396 // Step 3 - 6 of https://dom.spec.whatwg.org/#abortsignal-signal-abort 397 void AbortSignal::SignalAbortWithDependents() { 398 // Step 3: Let dependentSignalsToAbort be a new list. 399 nsTArray<RefPtr<AbortSignal>> dependentSignalsToAbort; 400 401 // mDependentSignals can go away after this function. 402 nsTArray<RefPtr<AbortSignal>> dependentSignals = std::move(mDependentSignals); 403 404 if (!dependentSignals.IsEmpty()) { 405 // (Prepare for step 4.1.1 which uses the reason of this. Cannot use 406 // RawReason because that can cause constructing new DOMException for each 407 // dependent signal instead of sharing the single one.) 408 AutoJSAPI jsapi; 409 if (!jsapi.Init(GetParentObject())) { 410 return; 411 } 412 JSContext* cx = jsapi.cx(); 413 JS::Rooted<JS::Value> reason(cx); 414 GetReason(cx, &reason); 415 416 // Step 4. For each dependentSignal of signal’s dependent signals: 417 for (const auto& dependentSignal : dependentSignals) { 418 MOZ_ASSERT(dependentSignal->mSourceSignals.Contains(this)); 419 // Step 4.1: If dependentSignal is not aborted, then: 420 if (!dependentSignal->Aborted()) { 421 // Step 4.1.1: Set dependentSignal’s abort reason to signal’s abort 422 // reason. 423 dependentSignal->SetAborted(reason); 424 // Step 4.1.2: Append dependentSignal to dependentSignalsToAbort. 425 dependentSignalsToAbort.AppendElement(dependentSignal); 426 } 427 } 428 } 429 430 // Step 5: Run the abort steps for signal. 431 RunAbortSteps(); 432 433 // Step 6: For each dependentSignal of dependentSignalsToAbort, run the abort 434 // steps for dependentSignal. 435 for (const auto& dependentSignal : dependentSignalsToAbort) { 436 dependentSignal->RunAbortSteps(); 437 } 438 } 439 440 // https://dom.spec.whatwg.org/#run-the-abort-steps 441 void AbortSignal::RunAbortSteps() { 442 // Step 1 - 2: 443 AbortSignalImpl::RunAbortSteps(); 444 445 // Step 3. Fire an event named abort at this signal. 446 EventInit init; 447 init.mBubbles = false; 448 init.mCancelable = false; 449 450 RefPtr<Event> event = Event::Constructor(this, u"abort"_ns, init); 451 event->SetTrusted(true); 452 453 DispatchEvent(*event); 454 } 455 456 bool AbortSignal::Dependent() const { return mDependent; } 457 458 AbortSignal::~AbortSignal() { mozilla::DropJSObjects(this); } 459 460 // AbortFollower 461 // ---------------------------------------------------------------------------- 462 463 AbortFollower::~AbortFollower() { Unfollow(); } 464 465 // https://dom.spec.whatwg.org/#abortsignal-add 466 void AbortFollower::Follow(AbortSignalImpl* aSignal) { 467 // Step 1. 468 if (aSignal->Aborted()) { 469 return; 470 } 471 472 MOZ_DIAGNOSTIC_ASSERT(aSignal); 473 474 Unfollow(); 475 476 // Step 2. 477 mFollowingSignal = aSignal; 478 MOZ_ASSERT(!aSignal->mFollowers.Contains(this)); 479 aSignal->mFollowers.AppendElement(this); 480 } 481 482 // https://dom.spec.whatwg.org/#abortsignal-remove 483 void AbortFollower::Unfollow() { 484 if (mFollowingSignal) { 485 // |Unfollow| is called by cycle-collection unlink code that runs in no 486 // guaranteed order. So we can't, symmetric with |Follow| above, assert 487 // that |this| will be found in |mFollowingSignal->mFollowers|. 488 mFollowingSignal->mFollowers.RemoveElement(this); 489 mFollowingSignal = nullptr; 490 } 491 } 492 493 bool AbortFollower::IsFollowing() const { return !!mFollowingSignal; } 494 495 } // namespace mozilla::dom