ServiceWorkerContainer should never prevent a page from entering the back/forward cache
https://bugs.webkit.org/show_bug.cgi?id=202603

Reviewed by Geoff Garen.

Source/WebCore:

Make it so that ServiceWorkerContainer can suspend, even if they have pending promises.
We now queue all promise resolutions to a SuspendableTaskQueue to make sure that those
promises do not get resolved while in the page cache.

Test: http/tests/workers/service/page-cache-service-worker-pending-promise.https.html

* workers/service/ServiceWorkerContainer.cpp:
(WebCore::ServiceWorkerContainer::ServiceWorkerContainer):
(WebCore::ServiceWorkerContainer::ready):
(WebCore::ServiceWorkerContainer::getRegistration):
(WebCore::ServiceWorkerContainer::getRegistrations):
(WebCore::ServiceWorkerContainer::jobFailedWithException):
(WebCore::ServiceWorkerContainer::jobResolvedWithRegistration):
(WebCore::ServiceWorkerContainer::jobResolvedWithUnregistrationResult):
(WebCore::ServiceWorkerContainer::jobFailedLoadingScript):
(WebCore::ServiceWorkerContainer::canSuspendForDocumentSuspension const):
* workers/service/ServiceWorkerContainer.h:

LayoutTests:

Add layout test coverage.

* http/tests/workers/service/page-cache-service-worker-pending-promise.https-expected.txt: Added.
* http/tests/workers/service/page-cache-service-worker-pending-promise.https.html: Added.


git-svn-id: http://svn.webkit.org/repository/webkit/trunk@250758 268f45cc-cd09-0410-ab3c-d52691b4dbfc
diff --git a/LayoutTests/ChangeLog b/LayoutTests/ChangeLog
index 8a56cee..2fd44bb 100644
--- a/LayoutTests/ChangeLog
+++ b/LayoutTests/ChangeLog
@@ -1,5 +1,17 @@
 2019-10-04  Chris Dumez  <cdumez@apple.com>
 
+        ServiceWorkerContainer should never prevent a page from entering the back/forward cache
+        https://bugs.webkit.org/show_bug.cgi?id=202603
+
+        Reviewed by Geoff Garen.
+
+        Add layout test coverage.
+
+        * http/tests/workers/service/page-cache-service-worker-pending-promise.https-expected.txt: Added.
+        * http/tests/workers/service/page-cache-service-worker-pending-promise.https.html: Added.
+
+2019-10-04  Chris Dumez  <cdumez@apple.com>
+
         Allow pages using IDBIndex to enter the PageCache
         https://bugs.webkit.org/show_bug.cgi?id=202430
         <rdar://problem/55887918>
diff --git a/LayoutTests/http/tests/workers/service/page-cache-service-worker-pending-promise.https-expected.txt b/LayoutTests/http/tests/workers/service/page-cache-service-worker-pending-promise.https-expected.txt
new file mode 100644
index 0000000..a450b20
--- /dev/null
+++ b/LayoutTests/http/tests/workers/service/page-cache-service-worker-pending-promise.https-expected.txt
@@ -0,0 +1,15 @@
+Tests that a page with a pending service worker promise is able to enter the page cache.
+
+On success, you will see a series of "PASS" messages, followed by "TEST COMPLETE".
+
+
+pageshow - not from cache
+pagehide - entering cache
+pageshow - from cache
+PASS Page was restored from Page Cache
+PASS Unregistered successfuly
+PASS !!restoredFromPageCache is true
+PASS successfullyParsed is true
+
+TEST COMPLETE
+
diff --git a/LayoutTests/http/tests/workers/service/page-cache-service-worker-pending-promise.https.html b/LayoutTests/http/tests/workers/service/page-cache-service-worker-pending-promise.https.html
new file mode 100644
index 0000000..09f0568
--- /dev/null
+++ b/LayoutTests/http/tests/workers/service/page-cache-service-worker-pending-promise.https.html
@@ -0,0 +1,45 @@
+<!-- webkit-test-runner [ enablePageCache=true ] -->
+<!DOCTYPE html>
+<html>
+<body>
+<script src="/js-test-resources/js-test.js"></script>
+<script>
+description("Tests that a page with a pending service worker promise is able to enter the page cache.");
+jsTestIsAsync = true;
+
+let restoredFromPageCache = false;
+
+window.addEventListener("pageshow", function(event) {
+    debug("pageshow - " + (event.persisted ? "" : "not ") + "from cache");
+    if (event.persisted) {
+        testPassed("Page was restored from Page Cache");
+        restoredFromPageCache = true;
+    }
+});
+
+window.addEventListener("pagehide", function(event) {
+    debug("pagehide - " + (event.persisted ? "" : "not ") + "entering cache");
+    if (!event.persisted) {
+        testFailed("Page failed to enter the Page Cache");
+        finishJSTest();
+    }
+
+    registration.unregister().then(() => {
+        testPassed("Unregistered successfuly");
+        shouldBeTrue("!!restoredFromPageCache");
+        finishJSTest();
+    }, () => {
+        testFailed("Failed to unregister");
+        finishJSTest();
+    });
+});
+
+onload = () => {
+    navigator.serviceWorker.register("resources/basic-fetch-worker.js", { scope: "/workers/service/resources/" }).then((_registration) => {
+        registration = _registration;
+        window.location = "/navigation/resources/page-cache-helper.html";
+    });
+}
+</script>
+</body>
+</html>
diff --git a/LayoutTests/imported/w3c/web-platform-tests/service-workers/service-worker/skip-waiting-without-using-registration.https-expected.txt b/LayoutTests/imported/w3c/web-platform-tests/service-workers/service-worker/skip-waiting-without-using-registration.https-expected.txt
index 812adc2..0ea9d58 100644
--- a/LayoutTests/imported/w3c/web-platform-tests/service-workers/service-worker/skip-waiting-without-using-registration.https-expected.txt
+++ b/LayoutTests/imported/w3c/web-platform-tests/service-workers/service-worker/skip-waiting-without-using-registration.https-expected.txt
@@ -1,3 +1,4 @@
 
-FAIL Test skipWaiting while a client is not being controlled promise_test: Unhandled rejection with value: object "TypeError: null is not an object (evaluating 'document.body.appendChild')"
+PASS Test skipWaiting while a client is not being controlled 
+PASS skipWaiting 
 
diff --git a/Source/WebCore/ChangeLog b/Source/WebCore/ChangeLog
index 0f457911..3baba08 100644
--- a/Source/WebCore/ChangeLog
+++ b/Source/WebCore/ChangeLog
@@ -1,5 +1,30 @@
 2019-10-04  Chris Dumez  <cdumez@apple.com>
 
+        ServiceWorkerContainer should never prevent a page from entering the back/forward cache
+        https://bugs.webkit.org/show_bug.cgi?id=202603
+
+        Reviewed by Geoff Garen.
+
+        Make it so that ServiceWorkerContainer can suspend, even if they have pending promises.
+        We now queue all promise resolutions to a SuspendableTaskQueue to make sure that those
+        promises do not get resolved while in the page cache.
+
+        Test: http/tests/workers/service/page-cache-service-worker-pending-promise.https.html
+
+        * workers/service/ServiceWorkerContainer.cpp:
+        (WebCore::ServiceWorkerContainer::ServiceWorkerContainer):
+        (WebCore::ServiceWorkerContainer::ready):
+        (WebCore::ServiceWorkerContainer::getRegistration):
+        (WebCore::ServiceWorkerContainer::getRegistrations):
+        (WebCore::ServiceWorkerContainer::jobFailedWithException):
+        (WebCore::ServiceWorkerContainer::jobResolvedWithRegistration):
+        (WebCore::ServiceWorkerContainer::jobResolvedWithUnregistrationResult):
+        (WebCore::ServiceWorkerContainer::jobFailedLoadingScript):
+        (WebCore::ServiceWorkerContainer::canSuspendForDocumentSuspension const):
+        * workers/service/ServiceWorkerContainer.h:
+
+2019-10-04  Chris Dumez  <cdumez@apple.com>
+
         Allow pages using IDBIndex to enter the PageCache
         https://bugs.webkit.org/show_bug.cgi?id=202430
         <rdar://problem/55887918>
diff --git a/Source/WebCore/workers/service/ServiceWorkerContainer.cpp b/Source/WebCore/workers/service/ServiceWorkerContainer.cpp
index 310e40f..28d5dbe 100644
--- a/Source/WebCore/workers/service/ServiceWorkerContainer.cpp
+++ b/Source/WebCore/workers/service/ServiceWorkerContainer.cpp
@@ -72,6 +72,7 @@
     : ActiveDOMObject(context)
     , m_navigator(navigator)
     , m_messageQueue(GenericEventQueue::create(*this))
+    , m_taskQueue(SuspendableTaskQueue::create(context))
 {
     suspendIfNeeded();
     
@@ -107,11 +108,10 @@
 
         auto& context = *scriptExecutionContext();
         ensureSWClientConnection().whenRegistrationReady(context.topOrigin().data(), context.url(), [this, protectedThis = makeRef(*this)](auto&& registrationData) mutable {
-            if (m_isStopped)
-                return;
-
-            auto registration = ServiceWorkerRegistration::getOrCreate(*scriptExecutionContext(), *this, WTFMove(registrationData));
-            m_readyPromise->resolve(WTFMove(registration));
+            m_taskQueue->enqueueTask([this, registrationData = WTFMove(registrationData)]() mutable {
+                auto registration = ServiceWorkerRegistration::getOrCreate(*scriptExecutionContext(), *this, WTFMove(registrationData));
+                m_readyPromise->resolve(WTFMove(registration));
+            });
         });
     }
     return *m_readyPromise;
@@ -270,14 +270,13 @@
     }
 
     ensureSWClientConnection().matchRegistration(SecurityOriginData { context.topOrigin().data() }, parsedURL, [this, protectedThis = makeRef(*this), promise = WTFMove(promise)](auto&& result) mutable {
-        if (m_isStopped)
-            return;
-
-        if (!result) {
-            promise->resolve();
-            return;
-        }
-        promise->resolve<IDLInterface<ServiceWorkerRegistration>>(ServiceWorkerRegistration::getOrCreate(*scriptExecutionContext(), *this, WTFMove(result.value())));
+        m_taskQueue->enqueueTask([this, promise = WTFMove(promise), result = WTFMove(result)]() mutable {
+            if (!result) {
+                promise->resolve();
+                return;
+            }
+            promise->resolve<IDLInterface<ServiceWorkerRegistration>>(ServiceWorkerRegistration::getOrCreate(*scriptExecutionContext(), *this, WTFMove(result.value())));
+        });
     });
 }
 
@@ -303,13 +302,12 @@
 
     auto& context = *scriptExecutionContext();
     ensureSWClientConnection().getRegistrations(SecurityOriginData { context.topOrigin().data() }, context.url(), [this, protectedThis = makeRef(*this), promise = WTFMove(promise)] (auto&& registrationDatas) mutable {
-        if (m_isStopped)
-            return;
-
-        auto registrations = WTF::map(WTFMove(registrationDatas), [&](auto&& registrationData) {
-            return ServiceWorkerRegistration::getOrCreate(*scriptExecutionContext(), *this, WTFMove(registrationData));
+        m_taskQueue->enqueueTask([this, promise = WTFMove(promise), registrationDatas = WTFMove(registrationDatas)]() mutable {
+            auto registrations = WTF::map(WTFMove(registrationDatas), [&](auto&& registrationData) {
+                return ServiceWorkerRegistration::getOrCreate(*scriptExecutionContext(), *this, WTFMove(registrationData));
+            });
+            promise->resolve<IDLSequence<IDLInterface<ServiceWorkerRegistration>>>(WTFMove(registrations));
         });
-        promise->resolve<IDLSequence<IDLInterface<ServiceWorkerRegistration>>>(WTFMove(registrations));
     });
 }
 
@@ -336,11 +334,9 @@
     if (!promise)
         return;
 
-    if (auto* context = scriptExecutionContext()) {
-        context->postTask([promise = WTFMove(promise), exception](auto&) mutable {
-            promise->reject(exception);
-        });
-    }
+    m_taskQueue->enqueueTask([promise = WTFMove(promise), exception]() mutable {
+        promise->reject(exception);
+    });
 }
 
 void ServiceWorkerContainer::fireUpdateFoundEvent(ServiceWorkerRegistrationIdentifier identifier)
@@ -371,7 +367,7 @@
         destroyJob(job);
     });
 
-    auto notifyIfExitEarly = WTF::makeScopeExit([this, &data, &shouldNotifyWhenResolved] {
+    auto notifyIfExitEarly = WTF::makeScopeExit([this, protectedThis = makeRef(*this), &data, &shouldNotifyWhenResolved] {
         if (shouldNotifyWhenResolved == ShouldNotifyWhenResolved::Yes)
             notifyRegistrationIsSettled(data.key);
     });
@@ -383,22 +379,16 @@
     if (!promise)
         return;
 
-    notifyIfExitEarly.release();
+    m_taskQueue->enqueueTask([this, promise = WTFMove(promise), jobIdentifier = job.identifier(), data = WTFMove(data), shouldNotifyWhenResolved, notifyIfExitEarly = WTFMove(notifyIfExitEarly)]() mutable {
+        notifyIfExitEarly.release();
 
-    scriptExecutionContext()->postTask([this, protectedThis = RefPtr<ServiceWorkerContainer>(this), promise = WTFMove(promise), jobIdentifier = job.identifier(), data = WTFMove(data), shouldNotifyWhenResolved](ScriptExecutionContext& context) mutable {
-        if (isStopped()) {
-            if (shouldNotifyWhenResolved == ShouldNotifyWhenResolved::Yes)
-                notifyRegistrationIsSettled(data.key);
-            return;
-        }
-
-        auto registration = ServiceWorkerRegistration::getOrCreate(context, *this, WTFMove(data));
+        auto registration = ServiceWorkerRegistration::getOrCreate(*scriptExecutionContext(), *this, WTFMove(data));
 
         CONTAINER_RELEASE_LOG_IF_ALLOWED("jobResolvedWithRegistration: Resolving promise for job %" PRIu64 ". Registration ID: %" PRIu64, jobIdentifier.toUInt64(), registration->identifier().toUInt64());
 
         if (shouldNotifyWhenResolved == ShouldNotifyWhenResolved::Yes) {
             m_ongoingSettledRegistrations.add(++m_lastOngoingSettledRegistrationIdentifier, registration->data().key);
-            promise->whenSettled([this, protectedThis = WTFMove(protectedThis), identifier = m_lastOngoingSettledRegistrationIdentifier] {
+            promise->whenSettled([this, protectedThis = makeRef(*this), identifier = m_lastOngoingSettledRegistrationIdentifier] {
                 notifyRegistrationIsSettled(m_ongoingSettledRegistrations.take(identifier));
             });
         }
@@ -444,7 +434,7 @@
         return;
     }
 
-    context->postTask([promise = job.takePromise(), unregistrationResult](auto&) mutable {
+    m_taskQueue->enqueueTask([promise = job.takePromise(), unregistrationResult]() mutable {
         promise->resolve<IDLBoolean>(unregistrationResult);
     });
 }
@@ -490,8 +480,11 @@
 
     CONTAINER_RELEASE_LOG_ERROR_IF_ALLOWED("jobFinishedLoadingScript: Failed to fetch script for job %" PRIu64 ", error: %s", job.identifier().toUInt64(), error.localizedDescription().utf8().data());
 
-    if (auto promise = job.takePromise())
-        promise->reject(WTFMove(exception));
+    if (auto promise = job.takePromise()) {
+        m_taskQueue->enqueueTask([promise = WTFMove(promise), exception = WTFMove(exception)]() mutable {
+            promise->reject(WTFMove(exception));
+        });
+    }
 
     notifyFailedFetchingScript(job, error);
     destroyJob(job);
@@ -521,7 +514,7 @@
 
 bool ServiceWorkerContainer::canSuspendForDocumentSuspension() const
 {
-    return !hasPendingActivity();
+    return true;
 }
 
 SWClientConnection& ServiceWorkerContainer::ensureSWClientConnection()
diff --git a/Source/WebCore/workers/service/ServiceWorkerContainer.h b/Source/WebCore/workers/service/ServiceWorkerContainer.h
index 2e0d9d0..461e70e 100644
--- a/Source/WebCore/workers/service/ServiceWorkerContainer.h
+++ b/Source/WebCore/workers/service/ServiceWorkerContainer.h
@@ -36,6 +36,7 @@
 #include "ServiceWorkerJobClient.h"
 #include "ServiceWorkerRegistration.h"
 #include "ServiceWorkerRegistrationOptions.h"
+#include "SuspendableTaskQueue.h"
 #include "WorkerType.h"
 #include <wtf/Threading.h>
 
@@ -144,6 +145,7 @@
     uint64_t m_lastOngoingSettledRegistrationIdentifier { 0 };
     HashMap<uint64_t, ServiceWorkerRegistrationKey> m_ongoingSettledRegistrations;
     UniqueRef<GenericEventQueue> m_messageQueue;
+    UniqueRef<SuspendableTaskQueue> m_taskQueue;
 };
 
 } // namespace WebCore