DOMCacheStorage should not prevent pages from entering the back/forward cache
https://bugs.webkit.org/show_bug.cgi?id=202608

Reviewed by Youenn Fablet.

Source/WebCore:

Make DOMCacheStorage fully suspendable by queueing all its asynchronous promise resolutions
to a SuspendableTaskQueue. This makes sure that no promises are resolved while suspended
in the page cache.

Test: http/tests/navigation/page-cache-domcachestorage-pending-promise.html

* Modules/cache/DOMCacheStorage.cpp:
(WebCore::DOMCacheStorage::DOMCacheStorage):
(WebCore::DOMCacheStorage::doSequentialMatch):
(WebCore::DOMCacheStorage::match):
(WebCore::DOMCacheStorage::has):
(WebCore::DOMCacheStorage::open):
(WebCore::DOMCacheStorage::doOpen):
(WebCore::DOMCacheStorage::remove):
(WebCore::DOMCacheStorage::doRemove):
(WebCore::DOMCacheStorage::keys):
(WebCore::DOMCacheStorage::canSuspendForDocumentSuspension const):
* Modules/cache/DOMCacheStorage.h:

LayoutTests:

Add layout test coverage.

* http/tests/navigation/page-cache-domcachestorage-pending-promise-expected.txt: Added.
* http/tests/navigation/page-cache-domcachestorage-pending-promise.html: Added.


git-svn-id: http://svn.webkit.org/repository/webkit/trunk@250965 268f45cc-cd09-0410-ab3c-d52691b4dbfc
diff --git a/LayoutTests/ChangeLog b/LayoutTests/ChangeLog
index 24c6cd9..2aa8b3b 100644
--- a/LayoutTests/ChangeLog
+++ b/LayoutTests/ChangeLog
@@ -1,3 +1,15 @@
+2019-10-10  Chris Dumez  <cdumez@apple.com>
+
+        DOMCacheStorage should not prevent pages from entering the back/forward cache
+        https://bugs.webkit.org/show_bug.cgi?id=202608
+
+        Reviewed by Youenn Fablet.
+
+        Add layout test coverage.
+
+        * http/tests/navigation/page-cache-domcachestorage-pending-promise-expected.txt: Added.
+        * http/tests/navigation/page-cache-domcachestorage-pending-promise.html: Added.
+
 2019-10-10  Miguel Gomez  <magomez@igalia.com>
 
         Unreviewed GTK and WPE gardening after r250954.
diff --git a/LayoutTests/http/tests/navigation/page-cache-domcachestorage-pending-promise-expected.txt b/LayoutTests/http/tests/navigation/page-cache-domcachestorage-pending-promise-expected.txt
new file mode 100644
index 0000000..b7f120e
--- /dev/null
+++ b/LayoutTests/http/tests/navigation/page-cache-domcachestorage-pending-promise-expected.txt
@@ -0,0 +1,15 @@
+Tests that a page with pending DOMCacheStorage activity goes into 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 Opened the cache
+PASS !!restoredFromPageCache is true
+PASS successfullyParsed is true
+
+TEST COMPLETE
+
diff --git a/LayoutTests/http/tests/navigation/page-cache-domcachestorage-pending-promise.html b/LayoutTests/http/tests/navigation/page-cache-domcachestorage-pending-promise.html
new file mode 100644
index 0000000..d05e5e0
--- /dev/null
+++ b/LayoutTests/http/tests/navigation/page-cache-domcachestorage-pending-promise.html
@@ -0,0 +1,46 @@
+<!-- webkit-test-runner [ enablePageCache=true ] -->
+<!DOCTYPE html>
+<html>
+<head>
+<script src="/js-test-resources/js-test.js"></script>
+</head>
+<body>
+<script>
+description('Tests that a page with pending DOMCacheStorage activity goes into 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();
+    }
+
+    caches.open('test').then(() => {
+       testPassed("Opened the cache");
+       shouldBeTrue("!!restoredFromPageCache");
+       finishJSTest();
+    }, () => {
+       testFailed("Failed to open the cache");
+       finishJSTest();
+    });
+});
+
+onload = () => {
+    setTimeout(() => {
+        testLink.click();
+    }, 0);
+}
+</script>
+<a id="testLink" href="resources/page-cache-helper.html" style="display: none">Link</a>
+</body>
+</html>
diff --git a/LayoutTests/platform/ios-wk1/TestExpectations b/LayoutTests/platform/ios-wk1/TestExpectations
index 6f5ac1f..8a56f87 100644
--- a/LayoutTests/platform/ios-wk1/TestExpectations
+++ b/LayoutTests/platform/ios-wk1/TestExpectations
@@ -12,6 +12,7 @@
 http/wpt/cache-storage [ Skip ]
 http/tests/cache-storage [ Skip ]
 http/tests/navigation/page-cache-domcache-pending-promise.html [ Skip ]
+http/tests/navigation/page-cache-domcachestorage-pending-promise.html [ Skip ]
 imported/w3c/web-platform-tests/fetch/api/request/destination [ Skip ]
 imported/w3c/web-platform-tests/fetch/cross-origin-resource-policy [ Skip ]
 
diff --git a/LayoutTests/platform/mac-wk1/TestExpectations b/LayoutTests/platform/mac-wk1/TestExpectations
index 75ee166..ba1aa40 100644
--- a/LayoutTests/platform/mac-wk1/TestExpectations
+++ b/LayoutTests/platform/mac-wk1/TestExpectations
@@ -272,6 +272,7 @@
 http/tests/appcache/main-resource-redirect-with-sw.html [ Skip ]
 http/tests/cache-storage [ Skip ]
 http/tests/navigation/page-cache-domcache-pending-promise.html [ Skip ]
+http/tests/navigation/page-cache-domcachestorage-pending-promise.html [ Skip ]
 http/tests/cookies/same-site/fetch-in-cross-origin-service-worker.html [ Skip ]
 http/tests/cookies/same-site/fetch-in-same-origin-service-worker.html [ Skip ]
 http/wpt/cache-storage [ Skip ]
diff --git a/LayoutTests/platform/win/TestExpectations b/LayoutTests/platform/win/TestExpectations
index 0d9e217..383e35d 100644
--- a/LayoutTests/platform/win/TestExpectations
+++ b/LayoutTests/platform/win/TestExpectations
@@ -3800,6 +3800,7 @@
 http/tests/appcache/main-resource-redirect-with-sw.html [ Skip ]
 http/tests/cache-storage [ Skip ]
 http/tests/navigation/page-cache-domcache-pending-promise.html [ Skip ]
+http/tests/navigation/page-cache-domcachestorage-pending-promise.html [ Skip ]
 http/tests/inspector/network/resource-response-service-worker.html [ Skip ]
 http/tests/workers/service [ Skip ]
 http/wpt/cache-storage [ Skip ]
diff --git a/Source/WebCore/ChangeLog b/Source/WebCore/ChangeLog
index 074fefb..da8e8b3 100644
--- a/Source/WebCore/ChangeLog
+++ b/Source/WebCore/ChangeLog
@@ -1,3 +1,29 @@
+2019-10-10  Chris Dumez  <cdumez@apple.com>
+
+        DOMCacheStorage should not prevent pages from entering the back/forward cache
+        https://bugs.webkit.org/show_bug.cgi?id=202608
+
+        Reviewed by Youenn Fablet.
+
+        Make DOMCacheStorage fully suspendable by queueing all its asynchronous promise resolutions
+        to a SuspendableTaskQueue. This makes sure that no promises are resolved while suspended
+        in the page cache.
+
+        Test: http/tests/navigation/page-cache-domcachestorage-pending-promise.html
+
+        * Modules/cache/DOMCacheStorage.cpp:
+        (WebCore::DOMCacheStorage::DOMCacheStorage):
+        (WebCore::DOMCacheStorage::doSequentialMatch):
+        (WebCore::DOMCacheStorage::match):
+        (WebCore::DOMCacheStorage::has):
+        (WebCore::DOMCacheStorage::open):
+        (WebCore::DOMCacheStorage::doOpen):
+        (WebCore::DOMCacheStorage::remove):
+        (WebCore::DOMCacheStorage::doRemove):
+        (WebCore::DOMCacheStorage::keys):
+        (WebCore::DOMCacheStorage::canSuspendForDocumentSuspension const):
+        * Modules/cache/DOMCacheStorage.h:
+
 2019-10-10  youenn fablet  <youenn@apple.com>
 
         Add asserts to MediaStreamPrivate observer routines
diff --git a/Source/WebCore/Modules/cache/DOMCacheStorage.cpp b/Source/WebCore/Modules/cache/DOMCacheStorage.cpp
index edf169d..cc289a6 100644
--- a/Source/WebCore/Modules/cache/DOMCacheStorage.cpp
+++ b/Source/WebCore/Modules/cache/DOMCacheStorage.cpp
@@ -31,7 +31,7 @@
 #include "JSDOMCache.h"
 #include "JSFetchResponse.h"
 #include "ScriptExecutionContext.h"
-
+#include "SuspendableTaskQueue.h"
 
 namespace WebCore {
 using namespace WebCore::DOMCacheEngine;
@@ -39,10 +39,13 @@
 DOMCacheStorage::DOMCacheStorage(ScriptExecutionContext& context, Ref<CacheStorageConnection>&& connection)
     : ActiveDOMObject(&context)
     , m_connection(WTFMove(connection))
+    , m_taskQueue(SuspendableTaskQueue::create(&context))
 {
     suspendIfNeeded();
 }
 
+DOMCacheStorage::~DOMCacheStorage() = default;
+
 Optional<ClientOrigin> DOMCacheStorage::origin() const
 {
     auto* origin = scriptExecutionContext() ? scriptExecutionContext()->securityOrigin() : nullptr;
@@ -86,17 +89,17 @@
 void DOMCacheStorage::doSequentialMatch(DOMCache::RequestInfo&& info, CacheQueryOptions&& options, Ref<DeferredPromise>&& promise)
 {
     startSequentialMatch(WTF::map(m_caches, copyCache), WTFMove(info), WTFMove(options), [this, pendingActivity = makePendingActivity(*this), promise = WTFMove(promise)](auto&& result) mutable {
-        if (m_isStopped)
-            return;
-        if (result.hasException()) {
-            promise->reject(result.releaseException());
-            return;
-        }
-        if (!result.returnValue()) {
-            promise->resolve();
-            return;
-        }
-        promise->resolve<IDLInterface<FetchResponse>>(*result.returnValue());
+        m_taskQueue->enqueueTask([promise = WTFMove(promise), result = WTFMove(result)]() mutable {
+            if (result.hasException()) {
+                promise->reject(result.releaseException());
+                return;
+            }
+            if (!result.returnValue()) {
+                promise->resolve();
+                return;
+            }
+            promise->resolve<IDLInterface<FetchResponse>>(*result.returnValue());
+        });
     });
 }
 
@@ -104,7 +107,9 @@
 {
     retrieveCaches([this, info = WTFMove(info), options = WTFMove(options), promise = WTFMove(promise)](Optional<Exception>&& exception) mutable {
         if (exception) {
-            promise->reject(WTFMove(exception.value()));
+            m_taskQueue->enqueueTask([promise = WTFMove(promise), exception = WTFMove(exception.value())]() mutable {
+                promise->reject(WTFMove(exception));
+            });
             return;
         }
 
@@ -114,7 +119,9 @@
                 m_caches[position]->match(WTFMove(info), WTFMove(options), WTFMove(promise));
                 return;
             }
-            promise->resolve();
+            m_taskQueue->enqueueTask([promise = WTFMove(promise)]() mutable {
+                promise->resolve();
+            });
             return;
         }
 
@@ -125,11 +132,13 @@
 void DOMCacheStorage::has(const String& name, DOMPromiseDeferred<IDLBoolean>&& promise)
 {
     retrieveCaches([this, name, promise = WTFMove(promise)](Optional<Exception>&& exception) mutable {
-        if (exception) {
-            promise.reject(WTFMove(exception.value()));
-            return;
-        }
-        promise.resolve(m_caches.findMatching([&](auto& item) { return item->name() == name; }) != notFound);
+        m_taskQueue->enqueueTask([this, name, promise = WTFMove(promise), exception = WTFMove(exception)]() mutable {
+            if (exception) {
+                promise.reject(WTFMove(exception.value()));
+                return;
+            }
+            promise.resolve(m_caches.findMatching([&](auto& item) { return item->name() == name; }) != notFound);
+        });
     });
 }
 
@@ -180,7 +189,9 @@
 {
     retrieveCaches([this, name, promise = WTFMove(promise)](Optional<Exception>&& exception) mutable {
         if (exception) {
-            promise.reject(WTFMove(exception.value()));
+            m_taskQueue->enqueueTask([promise = WTFMove(promise), exception = WTFMove(exception.value())]() mutable {
+                promise.reject(WTFMove(exception));
+            });
             return;
         }
         doOpen(name, WTFMove(promise));
@@ -191,23 +202,26 @@
 {
     auto position = m_caches.findMatching([&](auto& item) { return item->name() == name; });
     if (position != notFound) {
-        auto& cache = m_caches[position];
-        promise.resolve(DOMCache::create(*scriptExecutionContext(), String { cache->name() }, cache->identifier(), m_connection.copyRef()));
+        m_taskQueue->enqueueTask([this, promise = WTFMove(promise), cache = m_caches[position].copyRef()]() mutable {
+            promise.resolve(DOMCache::create(*scriptExecutionContext(), String { cache->name() }, cache->identifier(), m_connection.copyRef()));
+        });
         return;
     }
 
     m_connection->open(*origin(), name, [this, name, promise = WTFMove(promise), pendingActivity = makePendingActivity(*this)](const CacheIdentifierOrError& result) mutable {
-        if (!m_isStopped) {
-            if (!result.has_value())
-                promise.reject(DOMCacheEngine::convertToExceptionAndLog(scriptExecutionContext(), result.error()));
-            else {
-                if (result.value().hadStorageError)
-                    logConsolePersistencyError(scriptExecutionContext(), name);
+        if (!result.has_value()) {
+            m_taskQueue->enqueueTask([this, promise = WTFMove(promise), error = result.error()]() mutable {
+                promise.reject(DOMCacheEngine::convertToExceptionAndLog(scriptExecutionContext(), error));
+            });
+        } else {
+            if (result.value().hadStorageError)
+                logConsolePersistencyError(scriptExecutionContext(), name);
 
-                auto cache = DOMCache::create(*scriptExecutionContext(), String { name }, result.value().identifier, m_connection.copyRef());
+            m_taskQueue->enqueueTask([this, name, promise = WTFMove(promise), identifier = result.value().identifier]() mutable {
+                auto cache = DOMCache::create(*scriptExecutionContext(), String { name }, identifier, m_connection.copyRef());
                 promise.resolve(cache);
                 m_caches.append(WTFMove(cache));
-            }
+            });
         }
     });
 }
@@ -216,7 +230,9 @@
 {
     retrieveCaches([this, name, promise = WTFMove(promise)](Optional<Exception>&& exception) mutable {
         if (exception) {
-            promise.reject(WTFMove(exception.value()));
+            m_taskQueue->enqueueTask([promise = WTFMove(promise), exception = WTFMove(exception.value())]() mutable {
+                promise.reject(WTFMove(exception));
+            });
             return;
         }
         doRemove(name, WTFMove(promise));
@@ -227,12 +243,14 @@
 {
     auto position = m_caches.findMatching([&](auto& item) { return item->name() == name; });
     if (position == notFound) {
-        promise.resolve(false);
+        m_taskQueue->enqueueTask([promise = WTFMove(promise)]() mutable {
+            promise.resolve(false);
+        });
         return;
     }
 
     m_connection->remove(m_caches[position]->identifier(), [this, name, promise = WTFMove(promise), pendingActivity = makePendingActivity(*this)](const CacheIdentifierOrError& result) mutable {
-        if (!m_isStopped) {
+        m_taskQueue->enqueueTask([this, name, promise = WTFMove(promise), result]() mutable {
             if (!result.has_value())
                 promise.reject(DOMCacheEngine::convertToExceptionAndLog(scriptExecutionContext(), result.error()));
             else {
@@ -240,21 +258,23 @@
                     logConsolePersistencyError(scriptExecutionContext(), name);
                 promise.resolve(!!result.value().identifier);
             }
-        }
+        });
     });
 }
 
 void DOMCacheStorage::keys(KeysPromise&& promise)
 {
     retrieveCaches([this, promise = WTFMove(promise)](Optional<Exception>&& exception) mutable {
-        if (exception) {
-            promise.reject(WTFMove(exception.value()));
-            return;
-        }
+        m_taskQueue->enqueueTask([this, promise = WTFMove(promise), exception = WTFMove(exception)]() mutable {
+            if (exception) {
+                promise.reject(WTFMove(exception.value()));
+                return;
+            }
 
-        promise.resolve(WTF::map(m_caches, [] (const auto& cache) {
-            return cache->name();
-        }));
+            promise.resolve(WTF::map(m_caches, [] (const auto& cache) {
+                return cache->name();
+            }));
+        });
     });
 }
 
@@ -270,7 +290,7 @@
 
 bool DOMCacheStorage::canSuspendForDocumentSuspension() const
 {
-    return !hasPendingActivity();
+    return true;
 }
 
 } // namespace WebCore
diff --git a/Source/WebCore/Modules/cache/DOMCacheStorage.h b/Source/WebCore/Modules/cache/DOMCacheStorage.h
index a0c4618..9b6d9b1 100644
--- a/Source/WebCore/Modules/cache/DOMCacheStorage.h
+++ b/Source/WebCore/Modules/cache/DOMCacheStorage.h
@@ -32,9 +32,12 @@
 
 namespace WebCore {
 
+class SuspendableTaskQueue;
+
 class DOMCacheStorage : public RefCounted<DOMCacheStorage>, public ActiveDOMObject {
 public:
     static Ref<DOMCacheStorage> create(ScriptExecutionContext& context, Ref<CacheStorageConnection>&& connection) { return adoptRef(*new DOMCacheStorage(context, WTFMove(connection))); }
+    ~DOMCacheStorage();
 
     using KeysPromise = DOMPromiseDeferred<IDLSequence<IDLDOMString>>;
 
@@ -62,6 +65,7 @@
     Vector<Ref<DOMCache>> m_caches;
     uint64_t m_updateCounter { 0 };
     Ref<CacheStorageConnection> m_connection;
+    UniqueRef<SuspendableTaskQueue> m_taskQueue;
     bool m_isStopped { false };
 };