XMLHttpRequest should not prevent entering the back/forward cache
https://bugs.webkit.org/show_bug.cgi?id=203107
<rdar://problem/56438647>

Reviewed by Youenn Fablet.

LayoutTests/imported/w3c:

Rebaseline a new WPT tests that are passing now that we properly check that the
Document is fully active in open().

* web-platform-tests/xhr/open-url-multi-window-2-expected.txt:
* web-platform-tests/xhr/open-url-multi-window-5-expected.txt:
* web-platform-tests/xhr/open-url-multi-window-6-expected.txt:

Source/WebCore:

Improve XMLHttpRequest for back/forward cache suspension:
1. We no longer cancel pending loads in the suspend() method as this may
   fire events.
2. Simplify XMLHttpRequestProgressEventThrottle to use SuspendableTimers
   to dispatch events that are deferred by suspension or throttling.

Test: http/tests/navigation/page-cache-xhr-in-loading-iframe.html

* xml/XMLHttpRequest.cpp:
(WebCore::XMLHttpRequest::XMLHttpRequest):
(WebCore::XMLHttpRequest::open):
Add check to throw a InvalidStateError if the associated document is not fully active,
as per https://xhr.spec.whatwg.org/#dom-xmlhttprequest-open (Step 2). This avoids
dispatching events after ActiveDOMObject::stop() has been called and brings a few more
passes on WPT tests.

(WebCore::XMLHttpRequest::dispatchEvent):
(WebCore::XMLHttpRequest::suspend):
(WebCore::XMLHttpRequest::resume):
(WebCore::XMLHttpRequest::shouldPreventEnteringBackForwardCache_DEPRECATED const): Deleted.
(WebCore::XMLHttpRequest::resumeTimerFired): Deleted.
* xml/XMLHttpRequest.h:
* xml/XMLHttpRequestProgressEventThrottle.cpp:
(WebCore::XMLHttpRequestProgressEventThrottle::XMLHttpRequestProgressEventThrottle):
(WebCore::XMLHttpRequestProgressEventThrottle::dispatchThrottledProgressEvent):
(WebCore::XMLHttpRequestProgressEventThrottle::dispatchReadyStateChangeEvent):
(WebCore::XMLHttpRequestProgressEventThrottle::dispatchEventWhenPossible):
(WebCore::XMLHttpRequestProgressEventThrottle::dispatchProgressEvent):
(WebCore::XMLHttpRequestProgressEventThrottle::flushProgressEvent):
(WebCore::XMLHttpRequestProgressEventThrottle::dispatchDeferredEventsAfterResuming):
(WebCore::XMLHttpRequestProgressEventThrottle::dispatchThrottledProgressEventTimerFired):
(WebCore::XMLHttpRequestProgressEventThrottle::suspend):
(WebCore::XMLHttpRequestProgressEventThrottle::resume):
(WebCore::XMLHttpRequestProgressEventThrottle::dispatchEvent): Deleted.
(WebCore::XMLHttpRequestProgressEventThrottle::dispatchDeferredEvents): Deleted.
(WebCore::XMLHttpRequestProgressEventThrottle::fired): Deleted.
(WebCore::XMLHttpRequestProgressEventThrottle::hasEventToDispatch const): Deleted.
* xml/XMLHttpRequestProgressEventThrottle.h:

LayoutTests:

Add more test coverage.

* TestExpectations:
* fast/dom/xmlhttprequest-constructor-in-detached-document-expected.txt:
* fast/xmlhttprequest/xmlhttprequest-open-after-iframe-onload-remove-self.html:
* http/tests/navigation/page-cache-xhr-in-loading-iframe-expected.txt: Added.
* http/tests/navigation/page-cache-xhr-in-loading-iframe.html: Added.
* http/tests/navigation/resources/page-cache-xhr-in-loading-iframe.html: Added.

git-svn-id: http://svn.webkit.org/repository/webkit/trunk@251366 268f45cc-cd09-0410-ab3c-d52691b4dbfc
diff --git a/LayoutTests/ChangeLog b/LayoutTests/ChangeLog
index 5a6369e..e56bbb2 100644
--- a/LayoutTests/ChangeLog
+++ b/LayoutTests/ChangeLog
@@ -1,3 +1,20 @@
+2019-10-21  Chris Dumez  <cdumez@apple.com>
+
+        XMLHttpRequest should not prevent entering the back/forward cache
+        https://bugs.webkit.org/show_bug.cgi?id=203107
+        <rdar://problem/56438647>
+
+        Reviewed by Youenn Fablet.
+
+        Add more test coverage.
+
+        * TestExpectations:
+        * fast/dom/xmlhttprequest-constructor-in-detached-document-expected.txt:
+        * fast/xmlhttprequest/xmlhttprequest-open-after-iframe-onload-remove-self.html:
+        * http/tests/navigation/page-cache-xhr-in-loading-iframe-expected.txt: Added.
+        * http/tests/navigation/page-cache-xhr-in-loading-iframe.html: Added.
+        * http/tests/navigation/resources/page-cache-xhr-in-loading-iframe.html: Added.
+
 2019-10-21  Alicia Boya García  <aboya@igalia.com>
 
         [MSE][GStreamer] Revert WebKitMediaSrc rework temporarily
diff --git a/LayoutTests/TestExpectations b/LayoutTests/TestExpectations
index 33bba44..d56d319 100644
--- a/LayoutTests/TestExpectations
+++ b/LayoutTests/TestExpectations
@@ -268,6 +268,7 @@
 imported/w3c/web-platform-tests/service-workers/service-worker/fetch-cors-xhr.https.html [ DumpJSConsoleLogInStdErr ]
 fast/files/file-reader-back-forward-cache.html [ DumpJSConsoleLogInStdErr ]
 fast/history/page-cache-createImageBitmap.html [ DumpJSConsoleLogInStdErr ]
+http/tests/navigation/page-cache-xhr-in-loading-iframe.html [ DumpJSConsoleLogInStdErr ]
 
 webkit.org/b/202495 imported/w3c/web-platform-tests/shadow-dom/directionality-002.tentative.html [ ImageOnlyFailure ]
 
diff --git a/LayoutTests/fast/dom/xmlhttprequest-constructor-in-detached-document-expected.txt b/LayoutTests/fast/dom/xmlhttprequest-constructor-in-detached-document-expected.txt
index 6fc31c2..68b5713 100644
--- a/LayoutTests/fast/dom/xmlhttprequest-constructor-in-detached-document-expected.txt
+++ b/LayoutTests/fast/dom/xmlhttprequest-constructor-in-detached-document-expected.txt
@@ -1,3 +1,4 @@
+CONSOLE MESSAGE: line 14: InvalidStateError: Document is not fully active
 Text for bug 25290: Crash when constructing XMLHttpRequest in a detached document.
 
 PASS
diff --git a/LayoutTests/fast/xmlhttprequest/xmlhttprequest-open-after-iframe-onload-remove-self.html b/LayoutTests/fast/xmlhttprequest/xmlhttprequest-open-after-iframe-onload-remove-self.html
index d02ff26..dd3688e 100644
--- a/LayoutTests/fast/xmlhttprequest/xmlhttprequest-open-after-iframe-onload-remove-self.html
+++ b/LayoutTests/fast/xmlhttprequest/xmlhttprequest-open-after-iframe-onload-remove-self.html
@@ -8,7 +8,9 @@
 function onFrameLoad(frame) {
   var client = frame.contentWindow.client();
   frame.parentNode.removeChild(frame);
-  client.open("GET", "DoesNotExist.txt");
+  try {
+      client.open("GET", "DoesNotExist.txt");
+  } catch(e) { }
   if (window.testRunner)
     testRunner.notifyDone();
 }
diff --git a/LayoutTests/http/tests/navigation/page-cache-xhr-in-loading-iframe-expected.txt b/LayoutTests/http/tests/navigation/page-cache-xhr-in-loading-iframe-expected.txt
new file mode 100644
index 0000000..9b7a68d
--- /dev/null
+++ b/LayoutTests/http/tests/navigation/page-cache-xhr-in-loading-iframe-expected.txt
@@ -0,0 +1,15 @@
+Tests that a page with a loading iframe that has a pending XHR is able to enter the back/forward cache.
+
+On success, you will see a series of "PASS" messages, followed by "TEST COMPLETE".
+
+
+pageshow - not from cache
+* iframe starting XHR
+pagehide - entering cache
+pageshow - from cache
+PASS Page did enter and was restored from the page cache
+PASS XHR finished after restoring from the cache
+PASS successfullyParsed is true
+
+TEST COMPLETE
+
diff --git a/LayoutTests/http/tests/navigation/page-cache-xhr-in-loading-iframe.html b/LayoutTests/http/tests/navigation/page-cache-xhr-in-loading-iframe.html
new file mode 100644
index 0000000..48847ac
--- /dev/null
+++ b/LayoutTests/http/tests/navigation/page-cache-xhr-in-loading-iframe.html
@@ -0,0 +1,42 @@
+<!-- webkit-test-runner [ enableBackForwardCache=true ] -->
+<!DOCTYPE html>
+<html>
+<body>
+<script src="/js-test-resources/js-test.js"></script>
+<script>
+description('Tests that a page with a loading iframe that has a pending XHR is able to enter the back/forward cache.');
+window.jsTestIsAsync = true;
+
+window.addEventListener("pageshow", function(event) {
+    debug("pageshow - " + (event.persisted ? "" : "not ") + "from cache");
+
+    if (event.persisted) {
+        testPassed("Page did enter and was restored from the page cache");
+        iframe.contentWindow.shouldFinishJSOnXHRLoad = true;
+    }
+}, false);
+
+window.addEventListener("pagehide", function(event) {
+    debug("pagehide - " + (event.persisted ? "" : "not ") + "entering cache");
+    if (!event.persisted) {
+        testFailed("Page did not enter the page cache.");
+        finishJSTest();
+    }
+}, false);
+
+function navigate()
+{
+    window.location.href = "/navigation/resources/page-cache-helper.html";
+}
+
+window.addEventListener('load', function() {
+    setTimeout(function() {
+        iframe = document.createElement("iframe");
+        iframe.src = "/navigation/resources/page-cache-xhr-in-loading-iframe.html";
+        document.body.appendChild(iframe);
+    }, 0);
+}, false);
+
+</script>
+</body>
+</html>
diff --git a/LayoutTests/http/tests/navigation/resources/page-cache-xhr-in-loading-iframe.html b/LayoutTests/http/tests/navigation/resources/page-cache-xhr-in-loading-iframe.html
new file mode 100644
index 0000000..6a97d67
--- /dev/null
+++ b/LayoutTests/http/tests/navigation/resources/page-cache-xhr-in-loading-iframe.html
@@ -0,0 +1,31 @@
+<script>
+
+shouldFinishJSOnXHRLoad = false;
+
+function doXHR()
+{
+    xhr = new XMLHttpRequest();
+    xhr.open("GET", "/navigation/resources/slow-resource.pl?delay=1000");
+    xhr.addEventListener("load", () => {
+        if (shouldFinishJSOnXHRLoad) {
+            parent.testPassed("XHR finished after restoring from the cache");
+            parent.restoredFromCache = false;
+            parent.finishJSTest();
+        } else
+            doXHR();
+    });
+    xhr.addEventListener("error", () => {
+        doXHR();
+    });
+    xhr.addEventListener("progress", () => {
+    });
+
+    xhr.send();
+}
+
+parent.debug("* iframe starting XHR");
+doXHR();
+
+parent.navigate();
+
+</script>
diff --git a/LayoutTests/imported/w3c/ChangeLog b/LayoutTests/imported/w3c/ChangeLog
index 9175da5..b43dc4d 100644
--- a/LayoutTests/imported/w3c/ChangeLog
+++ b/LayoutTests/imported/w3c/ChangeLog
@@ -1,3 +1,18 @@
+2019-10-21  Chris Dumez  <cdumez@apple.com>
+
+        XMLHttpRequest should not prevent entering the back/forward cache
+        https://bugs.webkit.org/show_bug.cgi?id=203107
+        <rdar://problem/56438647>
+
+        Reviewed by Youenn Fablet.
+
+        Rebaseline a new WPT tests that are passing now that we properly check that the
+        Document is fully active in open().
+
+        * web-platform-tests/xhr/open-url-multi-window-2-expected.txt:
+        * web-platform-tests/xhr/open-url-multi-window-5-expected.txt:
+        * web-platform-tests/xhr/open-url-multi-window-6-expected.txt:
+
 2019-10-18  Said Abou-Hallawa  <sabouhallawa@apple.com>
 
         [SVG2]: Remove the SVGExternalResourcesRequired interface
diff --git a/LayoutTests/imported/w3c/web-platform-tests/xhr/open-url-multi-window-2-expected.txt b/LayoutTests/imported/w3c/web-platform-tests/xhr/open-url-multi-window-2-expected.txt
index e525698..f107bb0 100644
--- a/LayoutTests/imported/w3c/web-platform-tests/xhr/open-url-multi-window-2-expected.txt
+++ b/LayoutTests/imported/w3c/web-platform-tests/xhr/open-url-multi-window-2-expected.txt
@@ -1,5 +1,3 @@
 
-FAIL XMLHttpRequest: open() resolving URLs (multi-Window; 2; evil) assert_throws: open() when associated document's IFRAME is removed function "function () {
-            client.open("GET", "folder.txt")
-          }" did not throw
+PASS XMLHttpRequest: open() resolving URLs (multi-Window; 2; evil) 
 
diff --git a/LayoutTests/imported/w3c/web-platform-tests/xhr/open-url-multi-window-5-expected.txt b/LayoutTests/imported/w3c/web-platform-tests/xhr/open-url-multi-window-5-expected.txt
index 6b36111..fa78a2e 100644
--- a/LayoutTests/imported/w3c/web-platform-tests/xhr/open-url-multi-window-5-expected.txt
+++ b/LayoutTests/imported/w3c/web-platform-tests/xhr/open-url-multi-window-5-expected.txt
@@ -1,3 +1,3 @@
 
-FAIL XMLHttpRequest: open() resolving URLs (multi-Window; 5) assert_throws: function "function () { client.open("GET", "...") }" did not throw
+PASS XMLHttpRequest: open() resolving URLs (multi-Window; 5) 
 
diff --git a/LayoutTests/imported/w3c/web-platform-tests/xhr/open-url-multi-window-6-expected.txt b/LayoutTests/imported/w3c/web-platform-tests/xhr/open-url-multi-window-6-expected.txt
index ac86536..5a45792 100644
--- a/LayoutTests/imported/w3c/web-platform-tests/xhr/open-url-multi-window-6-expected.txt
+++ b/LayoutTests/imported/w3c/web-platform-tests/xhr/open-url-multi-window-6-expected.txt
@@ -1,3 +1,3 @@
 
-FAIL XMLHttpRequest: open() in document that is not fully active (but may be active) should throw assert_throws: function "function () { client.open("GET", "...") }" threw object "SyntaxError: The string did not match the expected pattern." that is not a DOMException InvalidStateError: property "code" is equal to 12, expected 11
+PASS XMLHttpRequest: open() in document that is not fully active (but may be active) should throw 
 
diff --git a/Source/WebCore/ChangeLog b/Source/WebCore/ChangeLog
index 5ff2b6e..c5e6f65 100644
--- a/Source/WebCore/ChangeLog
+++ b/Source/WebCore/ChangeLog
@@ -1,3 +1,50 @@
+2019-10-21  Chris Dumez  <cdumez@apple.com>
+
+        XMLHttpRequest should not prevent entering the back/forward cache
+        https://bugs.webkit.org/show_bug.cgi?id=203107
+        <rdar://problem/56438647>
+
+        Reviewed by Youenn Fablet.
+
+        Improve XMLHttpRequest for back/forward cache suspension:
+        1. We no longer cancel pending loads in the suspend() method as this may
+           fire events.
+        2. Simplify XMLHttpRequestProgressEventThrottle to use SuspendableTimers
+           to dispatch events that are deferred by suspension or throttling.
+
+        Test: http/tests/navigation/page-cache-xhr-in-loading-iframe.html
+
+        * xml/XMLHttpRequest.cpp:
+        (WebCore::XMLHttpRequest::XMLHttpRequest):
+        (WebCore::XMLHttpRequest::open):
+        Add check to throw a InvalidStateError if the associated document is not fully active,
+        as per https://xhr.spec.whatwg.org/#dom-xmlhttprequest-open (Step 2). This avoids
+        dispatching events after ActiveDOMObject::stop() has been called and brings a few more
+        passes on WPT tests.
+
+        (WebCore::XMLHttpRequest::dispatchEvent):
+        (WebCore::XMLHttpRequest::suspend):
+        (WebCore::XMLHttpRequest::resume):
+        (WebCore::XMLHttpRequest::shouldPreventEnteringBackForwardCache_DEPRECATED const): Deleted.
+        (WebCore::XMLHttpRequest::resumeTimerFired): Deleted.
+        * xml/XMLHttpRequest.h:
+        * xml/XMLHttpRequestProgressEventThrottle.cpp:
+        (WebCore::XMLHttpRequestProgressEventThrottle::XMLHttpRequestProgressEventThrottle):
+        (WebCore::XMLHttpRequestProgressEventThrottle::dispatchThrottledProgressEvent):
+        (WebCore::XMLHttpRequestProgressEventThrottle::dispatchReadyStateChangeEvent):
+        (WebCore::XMLHttpRequestProgressEventThrottle::dispatchEventWhenPossible):
+        (WebCore::XMLHttpRequestProgressEventThrottle::dispatchProgressEvent):
+        (WebCore::XMLHttpRequestProgressEventThrottle::flushProgressEvent):
+        (WebCore::XMLHttpRequestProgressEventThrottle::dispatchDeferredEventsAfterResuming):
+        (WebCore::XMLHttpRequestProgressEventThrottle::dispatchThrottledProgressEventTimerFired):
+        (WebCore::XMLHttpRequestProgressEventThrottle::suspend):
+        (WebCore::XMLHttpRequestProgressEventThrottle::resume):
+        (WebCore::XMLHttpRequestProgressEventThrottle::dispatchEvent): Deleted.
+        (WebCore::XMLHttpRequestProgressEventThrottle::dispatchDeferredEvents): Deleted.
+        (WebCore::XMLHttpRequestProgressEventThrottle::fired): Deleted.
+        (WebCore::XMLHttpRequestProgressEventThrottle::hasEventToDispatch const): Deleted.
+        * xml/XMLHttpRequestProgressEventThrottle.h:
+
 2019-10-21  Alicia Boya García  <aboya@igalia.com>
 
         [MSE][GStreamer] Revert WebKitMediaSrc rework temporarily
diff --git a/Source/WebCore/xml/XMLHttpRequest.cpp b/Source/WebCore/xml/XMLHttpRequest.cpp
index 67ad293..152274f 100644
--- a/Source/WebCore/xml/XMLHttpRequest.cpp
+++ b/Source/WebCore/xml/XMLHttpRequest.cpp
@@ -117,11 +117,9 @@
     , m_uploadComplete(false)
     , m_wasAbortedByClient(false)
     , m_responseCacheIsValid(false)
-    , m_dispatchErrorOnResuming(false)
     , m_readyState(static_cast<unsigned>(UNSENT))
     , m_responseType(static_cast<unsigned>(ResponseType::EmptyString))
-    , m_progressEventThrottle(this)
-    , m_resumeTimer(*this, &XMLHttpRequest::resumeTimerFired)
+    , m_progressEventThrottle(*this)
     , m_networkErrorTimer(*this, &XMLHttpRequest::networkErrorTimerFired)
     , m_timeoutTimer(*this, &XMLHttpRequest::didReachTimeout)
     , m_maximumIntervalForUserGestureForwarding(maximumIntervalForUserGestureForwarding)
@@ -337,6 +335,11 @@
 
 ExceptionOr<void> XMLHttpRequest::open(const String& method, const URL& url, bool async)
 {
+    auto* context = scriptExecutionContext();
+    bool contextIsDocument = is<Document>(*context);
+    if (contextIsDocument && !downcast<Document>(*context).isFullyActive())
+        return Exception { InvalidStateError, "Document is not fully active"_s };
+
     if (!isValidHTTPToken(method))
         return Exception { SyntaxError };
 
@@ -346,19 +349,19 @@
     if (!url.isValid())
         return Exception { SyntaxError };
 
-    if (!async && scriptExecutionContext()->isDocument()) {
+    if (!async && contextIsDocument) {
         // Newer functionality is not available to synchronous requests in window contexts, as a spec-mandated
         // attempt to discourage synchronous XHR use. responseType is one such piece of functionality.
         // We'll only disable this functionality for HTTP(S) requests since sync requests for local protocols
         // such as file: and data: still make sense to allow.
         if (url.protocolIsInHTTPFamily() && responseType() != ResponseType::EmptyString) {
-            logConsoleError(scriptExecutionContext(), "Synchronous HTTP(S) requests made from the window context cannot have XMLHttpRequest.responseType set.");
+            logConsoleError(context, "Synchronous HTTP(S) requests made from the window context cannot have XMLHttpRequest.responseType set.");
             return Exception { InvalidAccessError };
         }
 
         // Similarly, timeouts are disabled for synchronous requests as well.
         if (m_timeoutMilliseconds > 0) {
-            logConsoleError(scriptExecutionContext(), "Synchronous XMLHttpRequests must not have a timeout value set.");
+            logConsoleError(context, "Synchronous XMLHttpRequests must not have a timeout value set.");
             return Exception { InvalidAccessError };
         }
     }
@@ -378,7 +381,7 @@
     clearRequest();
 
     m_url = url;
-    scriptExecutionContext()->contentSecurityPolicy()->upgradeInsecureRequestIfNeeded(m_url, ContentSecurityPolicy::InsecureRequestType::Load);
+    context->contentSecurityPolicy()->upgradeInsecureRequestIfNeeded(m_url, ContentSecurityPolicy::InsecureRequestType::Load);
 
     m_async = async;
 
@@ -1091,6 +1094,8 @@
 
 void XMLHttpRequest::dispatchEvent(Event& event)
 {
+    RELEASE_ASSERT(!scriptExecutionContext()->activeDOMObjectsAreSuspended());
+
     if (m_userGestureToken && m_userGestureToken->hasExpired(m_maximumIntervalForUserGestureForwarding))
         m_userGestureToken = nullptr;
 
@@ -1141,55 +1146,19 @@
     dispatchErrorEvents(eventNames().timeoutEvent);
 }
 
-// FIXME: This should never prevent entering the back/forward cache.
-bool XMLHttpRequest::shouldPreventEnteringBackForwardCache_DEPRECATED() const
-{
-    // If the load event has not fired yet, cancelling the load in suspend() may cause
-    // the load event to be fired and arbitrary JS execution, which would be unsafe.
-    // Therefore, we prevent suspending in this case.
-    return m_loader && !document()->loadEventFinished();
-}
-
 const char* XMLHttpRequest::activeDOMObjectName() const
 {
     return "XMLHttpRequest";
 }
 
-void XMLHttpRequest::suspend(ReasonForSuspension reason)
+void XMLHttpRequest::suspend(ReasonForSuspension)
 {
     m_progressEventThrottle.suspend();
-
-    if (m_resumeTimer.isActive()) {
-        m_resumeTimer.stop();
-        m_dispatchErrorOnResuming = true;
-    }
-
-    if (reason == ReasonForSuspension::BackForwardCache && m_loader) {
-        // Going into the BackForwardCache, abort the request and dispatch a network error on resuming.
-        genericError();
-        m_dispatchErrorOnResuming = true;
-        bool aborted = internalAbort();
-        // It should not be possible to restart the load when aborting in suspend() because
-        // we are not allowed to execute in JS in suspend().
-        ASSERT_UNUSED(aborted, aborted);
-    }
 }
 
 void XMLHttpRequest::resume()
 {
     m_progressEventThrottle.resume();
-
-    // We are not allowed to execute arbitrary JS in resume() so dispatch
-    // the error event in a timer.
-    if (m_dispatchErrorOnResuming && !m_resumeTimer.isActive())
-        m_resumeTimer.startOneShot(0_s);
-}
-
-void XMLHttpRequest::resumeTimerFired()
-{
-    ASSERT(m_dispatchErrorOnResuming);
-    m_dispatchErrorOnResuming = false;
-    dispatchErrorEvents(eventNames().errorEvent);
 }
 
 void XMLHttpRequest::stop()
diff --git a/Source/WebCore/xml/XMLHttpRequest.h b/Source/WebCore/xml/XMLHttpRequest.h
index 52008c7..df32ab0 100644
--- a/Source/WebCore/xml/XMLHttpRequest.h
+++ b/Source/WebCore/xml/XMLHttpRequest.h
@@ -137,7 +137,6 @@
 
     // ActiveDOMObject
     void contextDestroyed() override;
-    bool shouldPreventEnteringBackForwardCache_DEPRECATED() const override;
     void suspend(ReasonForSuspension) override;
     void resume() override;
     void stop() override;
@@ -189,7 +188,6 @@
     using EventTarget::dispatchEvent;
     void dispatchEvent(Event&) override;
 
-    void resumeTimerFired();
     Ref<TextResourceDecoder> createDecoder() const;
 
     void networkErrorTimerFired();
@@ -203,7 +201,6 @@
     unsigned m_uploadComplete : 1;
     unsigned m_wasAbortedByClient : 1;
     unsigned m_responseCacheIsValid : 1;
-    unsigned m_dispatchErrorOnResuming : 1;
     unsigned m_readyState : 3; // State
     unsigned m_responseType : 3; // ResponseType
 
@@ -238,7 +235,6 @@
 
     mutable String m_allResponseHeaders;
 
-    Timer m_resumeTimer;
     Timer m_networkErrorTimer;
     Timer m_timeoutTimer;
 
diff --git a/Source/WebCore/xml/XMLHttpRequestProgressEventThrottle.cpp b/Source/WebCore/xml/XMLHttpRequestProgressEventThrottle.cpp
index f8f9f5e..7745f4f 100644
--- a/Source/WebCore/xml/XMLHttpRequestProgressEventThrottle.cpp
+++ b/Source/WebCore/xml/XMLHttpRequestProgressEventThrottle.cpp
@@ -35,11 +35,13 @@
 
 const Seconds XMLHttpRequestProgressEventThrottle::minimumProgressEventDispatchingInterval { 50_ms }; // 50 ms per specification.
 
-XMLHttpRequestProgressEventThrottle::XMLHttpRequestProgressEventThrottle(EventTarget* target)
+XMLHttpRequestProgressEventThrottle::XMLHttpRequestProgressEventThrottle(EventTarget& target)
     : m_target(target)
-    , m_dispatchDeferredEventsTimer(*this, &XMLHttpRequestProgressEventThrottle::dispatchDeferredEvents)
+    , m_dispatchThrottledProgressEventTimer(target.scriptExecutionContext(), *this, &XMLHttpRequestProgressEventThrottle::dispatchThrottledProgressEventTimerFired)
+    , m_dispatchDeferredEventsAfterResumingTimer(target.scriptExecutionContext(), *this, &XMLHttpRequestProgressEventThrottle::dispatchDeferredEventsAfterResuming)
 {
-    ASSERT(target);
+    m_dispatchThrottledProgressEventTimer.suspendIfNeeded();
+    m_dispatchDeferredEventsAfterResumingTimer.suspendIfNeeded();
 }
 
 XMLHttpRequestProgressEventThrottle::~XMLHttpRequestProgressEventThrottle() = default;
@@ -50,29 +52,23 @@
     m_loaded = loaded;
     m_total = total;
 
-    if (!m_target->hasEventListeners(eventNames().progressEvent))
+    if (!m_target.hasEventListeners(eventNames().progressEvent))
         return;
-    
-    if (m_deferEvents) {
-        // Only store the latest progress event while suspended.
-        m_deferredProgressEvent = XMLHttpRequestProgressEvent::create(eventNames().progressEvent, lengthComputable, loaded, total);
-        return;
-    }
 
-    if (!isActive()) {
+    if (!m_shouldDeferEventsDueToSuspension && !m_dispatchThrottledProgressEventTimer.isActive()) {
         // The timer is not active so the least frequent event for now is every byte. Just dispatch the event.
 
         // We should not have any throttled progress event.
-        ASSERT(!m_hasThrottledProgressEvent);
+        ASSERT(!m_hasPendingThrottledProgressEvent);
 
-        dispatchEvent(XMLHttpRequestProgressEvent::create(eventNames().progressEvent, lengthComputable, loaded, total));
-        startRepeating(minimumProgressEventDispatchingInterval);
-        m_hasThrottledProgressEvent = false;
+        dispatchEventWhenPossible(XMLHttpRequestProgressEvent::create(eventNames().progressEvent, lengthComputable, loaded, total));
+        m_dispatchThrottledProgressEventTimer.startRepeating(minimumProgressEventDispatchingInterval);
+        m_hasPendingThrottledProgressEvent = false;
         return;
     }
 
     // The timer is already active so minimumProgressEventDispatchingInterval is the least frequent event.
-    m_hasThrottledProgressEvent = true;
+    m_hasPendingThrottledProgressEvent = true;
 }
 
 void XMLHttpRequestProgressEventThrottle::dispatchReadyStateChangeEvent(Event& event, ProgressEventAction progressEventAction)
@@ -80,19 +76,19 @@
     if (progressEventAction == FlushProgressEvent)
         flushProgressEvent();
 
-    dispatchEvent(event);
+    dispatchEventWhenPossible(event);
 }
 
-void XMLHttpRequestProgressEventThrottle::dispatchEvent(Event& event)
+void XMLHttpRequestProgressEventThrottle::dispatchEventWhenPossible(Event& event)
 {
-    if (m_deferEvents) {
-        if (m_deferredEvents.size() > 1 && event.type() == eventNames().readystatechangeEvent && event.type() == m_deferredEvents.last()->type()) {
+    if (m_shouldDeferEventsDueToSuspension) {
+        if (m_eventsDeferredDueToSuspension.size() > 1 && event.type() == eventNames().readystatechangeEvent && event.type() == m_eventsDeferredDueToSuspension.last()->type()) {
             // Readystatechange events are state-less so avoid repeating two identical events in a row on resume.
             return;
         }
-        m_deferredEvents.append(event);
+        m_eventsDeferredDueToSuspension.append(event);
     } else
-        m_target->dispatchEvent(event);
+        m_target.dispatchEvent(event);
 }
 
 void XMLHttpRequestProgressEventThrottle::dispatchProgressEvent(const AtomString& type)
@@ -105,103 +101,62 @@
         m_total = 0;
     }
 
-    if (m_target->hasEventListeners(type))
-        dispatchEvent(XMLHttpRequestProgressEvent::create(type, m_lengthComputable, m_loaded, m_total));
+    if (m_target.hasEventListeners(type))
+        dispatchEventWhenPossible(XMLHttpRequestProgressEvent::create(type, m_lengthComputable, m_loaded, m_total));
 }
 
 void XMLHttpRequestProgressEventThrottle::flushProgressEvent()
 {
-    if (m_deferEvents && m_deferredProgressEvent) {
-        // Move the progress event to the queue, to get it in the right order on resume.
-        m_deferredEvents.append(m_deferredProgressEvent.releaseNonNull());
+    if (!m_hasPendingThrottledProgressEvent)
         return;
-    }
 
-    if (!hasEventToDispatch())
-        return;
-    Ref<Event> event = XMLHttpRequestProgressEvent::create(eventNames().progressEvent, m_lengthComputable, m_loaded, m_total);
-    m_hasThrottledProgressEvent = false;
-
+    m_hasPendingThrottledProgressEvent = false;
     // We stop the timer as this is called when no more events are supposed to occur.
-    stop();
+    m_dispatchThrottledProgressEventTimer.cancel();
 
-    dispatchEvent(WTFMove(event));
+    dispatchEventWhenPossible(XMLHttpRequestProgressEvent::create(eventNames().progressEvent, m_lengthComputable, m_loaded, m_total));
 }
 
-void XMLHttpRequestProgressEventThrottle::dispatchDeferredEvents()
+void XMLHttpRequestProgressEventThrottle::dispatchDeferredEventsAfterResuming()
 {
-    ASSERT(m_deferEvents);
-    m_deferEvents = false;
+    ASSERT(m_shouldDeferEventsDueToSuspension);
+    m_shouldDeferEventsDueToSuspension = false;
 
     // Take over the deferred events before dispatching them which can potentially add more.
-    auto deferredEvents = WTFMove(m_deferredEvents);
+    auto eventsDeferredDueToSuspension = WTFMove(m_eventsDeferredDueToSuspension);
 
-    RefPtr<Event> deferredProgressEvent = WTFMove(m_deferredProgressEvent);
+    flushProgressEvent();
 
-    for (auto& deferredEvent : deferredEvents)
-        dispatchEvent(deferredEvent);
-
-    // The progress event will be in the m_deferredEvents vector if the load was finished while suspended.
-    // If not, just send the most up-to-date progress on resume.
-    if (deferredProgressEvent)
-        dispatchEvent(*deferredProgressEvent);
+    for (auto& deferredEvent : eventsDeferredDueToSuspension)
+        dispatchEventWhenPossible(deferredEvent);
 }
 
-void XMLHttpRequestProgressEventThrottle::fired()
+void XMLHttpRequestProgressEventThrottle::dispatchThrottledProgressEventTimerFired()
 {
-    ASSERT(isActive());
-    if (!hasEventToDispatch()) {
+    ASSERT(m_dispatchThrottledProgressEventTimer.isActive());
+    if (!m_hasPendingThrottledProgressEvent) {
         // No progress event was queued since the previous dispatch, we can safely stop the timer.
-        stop();
+        m_dispatchThrottledProgressEventTimer.cancel();
         return;
     }
 
-    dispatchEvent(XMLHttpRequestProgressEvent::create(eventNames().progressEvent, m_lengthComputable, m_loaded, m_total));
-    m_hasThrottledProgressEvent = false;
-}
-
-bool XMLHttpRequestProgressEventThrottle::hasEventToDispatch() const
-{
-    return m_hasThrottledProgressEvent && isActive();
+    dispatchEventWhenPossible(XMLHttpRequestProgressEvent::create(eventNames().progressEvent, m_lengthComputable, m_loaded, m_total));
+    m_hasPendingThrottledProgressEvent = false;
 }
 
 void XMLHttpRequestProgressEventThrottle::suspend()
 {
-    // If re-suspended before deferred events have been dispatched, just stop the dispatch
-    // and continue the last suspend.
-    if (m_dispatchDeferredEventsTimer.isActive()) {
-        ASSERT(m_deferEvents);
-        m_dispatchDeferredEventsTimer.stop();
-        return;
-    }
-    ASSERT(!m_deferredProgressEvent);
-    ASSERT(m_deferredEvents.isEmpty());
-    ASSERT(!m_deferEvents);
-
-    m_deferEvents = true;
-    // If we have a progress event waiting to be dispatched,
-    // just defer it.
-    if (hasEventToDispatch()) {
-        m_deferredProgressEvent = XMLHttpRequestProgressEvent::create(eventNames().progressEvent, m_lengthComputable, m_loaded, m_total);
-        m_hasThrottledProgressEvent = false;
-    }
-    stop();
+    m_shouldDeferEventsDueToSuspension = true;
 }
 
 void XMLHttpRequestProgressEventThrottle::resume()
 {
-    ASSERT(!m_hasThrottledProgressEvent);
-
-    if (m_deferredEvents.isEmpty() && !m_deferredProgressEvent) {
-        m_deferEvents = false;
+    if (m_eventsDeferredDueToSuspension.isEmpty() && !m_hasPendingThrottledProgressEvent) {
+        m_shouldDeferEventsDueToSuspension = false;
         return;
     }
 
-    // Do not dispatch events inline here, since ScriptExecutionContext is iterating over
-    // the list of active DOM objects to resume them, and any activated JS event-handler
-    // could insert new active DOM objects to the list.
-    // m_deferEvents is kept true until all deferred events have been dispatched.
-    m_dispatchDeferredEventsTimer.startOneShot(0_s);
+    m_dispatchDeferredEventsAfterResumingTimer.startOneShot(0_s);
 }
 
 } // namespace WebCore
diff --git a/Source/WebCore/xml/XMLHttpRequestProgressEventThrottle.h b/Source/WebCore/xml/XMLHttpRequestProgressEventThrottle.h
index 195316c..a83cf3d 100644
--- a/Source/WebCore/xml/XMLHttpRequestProgressEventThrottle.h
+++ b/Source/WebCore/xml/XMLHttpRequestProgressEventThrottle.h
@@ -26,7 +26,7 @@
 
 #pragma once
 
-#include "Timer.h"
+#include "SuspendableTimer.h"
 #include <wtf/Forward.h>
 #include <wtf/Vector.h>
 
@@ -42,9 +42,9 @@
 
 // This implements the XHR2 progress event dispatching: "dispatch a progress event called progress
 // about every 50ms or for every byte received, whichever is least frequent".
-class XMLHttpRequestProgressEventThrottle : public TimerBase {
+class XMLHttpRequestProgressEventThrottle {
 public:
-    explicit XMLHttpRequestProgressEventThrottle(EventTarget*);
+    explicit XMLHttpRequestProgressEventThrottle(EventTarget&);
     virtual ~XMLHttpRequestProgressEventThrottle();
 
     void dispatchThrottledProgressEvent(bool lengthComputable, unsigned long long loaded, unsigned long long total);
@@ -57,26 +57,25 @@
 private:
     static const Seconds minimumProgressEventDispatchingInterval;
 
-    void fired() override;
-    void dispatchDeferredEvents();
+    void dispatchThrottledProgressEventTimerFired();
+    void dispatchDeferredEventsAfterResuming();
     void flushProgressEvent();
-    void dispatchEvent(Event&);
-
-    bool hasEventToDispatch() const;
+    void dispatchEventWhenPossible(Event&);
 
     // Weak pointer to our XMLHttpRequest object as it is the one holding us.
-    EventTarget* m_target;
+    EventTarget& m_target;
 
     unsigned long long m_loaded { 0 };
     unsigned long long m_total { 0 };
 
     RefPtr<Event> m_deferredProgressEvent;
-    Vector<Ref<Event>> m_deferredEvents;
-    Timer m_dispatchDeferredEventsTimer;
+    Vector<Ref<Event>> m_eventsDeferredDueToSuspension;
+    SuspendableTimer m_dispatchThrottledProgressEventTimer;
+    SuspendableTimer m_dispatchDeferredEventsAfterResumingTimer;
 
-    bool m_hasThrottledProgressEvent { false };
+    bool m_hasPendingThrottledProgressEvent { false };
     bool m_lengthComputable { false };
-    bool m_deferEvents { false };
+    bool m_shouldDeferEventsDueToSuspension { false };
 };
 
 } // namespace WebCore