Diff
Modified: trunk/LayoutTests/ChangeLog (250760 => 250761)
--- trunk/LayoutTests/ChangeLog 2019-10-05 01:16:58 UTC (rev 250760)
+++ trunk/LayoutTests/ChangeLog 2019-10-05 01:38:21 UTC (rev 250761)
@@ -1,5 +1,21 @@
2019-10-04 Chris Dumez <cdu...@apple.com>
+ Allow pages using EventSource to enter the back/forward cache
+ https://bugs.webkit.org/show_bug.cgi?id=202370
+ <rdar://problem/55853142>
+
+ Reviewed by Geoffrey Garen.
+
+ Add layout test coverage.
+
+ * http/tests/eventsource/eventsource-page-cache-connected-expected.txt: Added.
+ * http/tests/eventsource/eventsource-page-cache-connected.html: Added.
+ * http/tests/eventsource/eventsource-page-cache-connecting-expected.txt: Added.
+ * http/tests/eventsource/eventsource-page-cache-connecting.html: Added.
+ * http/tests/eventsource/resources/infinite-event-stream.php: Added.
+
+2019-10-04 Chris Dumez <cdu...@apple.com>
+
ServiceWorkerContainer should never prevent a page from entering the back/forward cache
https://bugs.webkit.org/show_bug.cgi?id=202603
Added: trunk/LayoutTests/http/tests/eventsource/eventsource-page-cache-connected-expected.txt (0 => 250761)
--- trunk/LayoutTests/http/tests/eventsource/eventsource-page-cache-connected-expected.txt (rev 0)
+++ trunk/LayoutTests/http/tests/eventsource/eventsource-page-cache-connected-expected.txt 2019-10-05 01:38:21 UTC (rev 250761)
@@ -0,0 +1,22 @@
+Tests that a page with an EventSource that is connected can enter the PageCache.
+
+On success, you will see a series of "PASS" messages, followed by "TEST COMPLETE".
+
+
+pageshow - not from cache
+PASS Received open event
+PASS Received message before entering page cache
+pagehide - entering cache
+pageshow - from cache
+PASS Page did enter and was restored from the page cache
+PASS Received error event
+PASS restoredFromPageCache is true
+PASS openEventCount is 1
+PASS Received open event
+PASS Received message after restoring from page cache
+PASS openEventCount is 2
+PASS errorEventCount is 1
+PASS successfullyParsed is true
+
+TEST COMPLETE
+
Added: trunk/LayoutTests/http/tests/eventsource/eventsource-page-cache-connected.html (0 => 250761)
--- trunk/LayoutTests/http/tests/eventsource/eventsource-page-cache-connected.html (rev 0)
+++ trunk/LayoutTests/http/tests/eventsource/eventsource-page-cache-connected.html 2019-10-05 01:38:21 UTC (rev 250761)
@@ -0,0 +1,72 @@
+<!-- webkit-test-runner [ enablePageCache=true ] -->
+<!DOCTYPE html>
+<html>
+<head>
+<script src=""
+</head>
+<body>
+<script>
+description("Tests that a page with an EventSource that is connected can enter the PageCache.");
+jsTestIsAsync = true;
+
+let restoredFromPageCache = false;
+let openEventCount = 0;
+let errorEventCount = 0;
+let shouldNavigateOnMessage = 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");
+ restoredFromPageCache = 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);
+
+_onload_ = () => {
+ setTimeout(() => {
+ eventSource = new EventSource("resources/infinite-event-stream.php");
+
+ eventSource._onerror_ = () => {
+ testPassed("Received error event");
+ errorEventCount++;
+ shouldBeTrue("restoredFromPageCache");
+ shouldBe("openEventCount", "1");
+ }
+
+ eventSource._onopen_ = () => {
+ testPassed("Received open event");
+ openEventCount++;
+ }
+
+ eventSource._onmessage_ = () => {
+ if (shouldNavigateOnMessage) {
+ shouldNavigateOnMessage = false;
+ testPassed("Received message before entering page cache");
+ setTimeout(() => {
+ window.location = "/navigation/resources/page-cache-helper.html";
+ }, 0);
+ return;
+ }
+ if (!restoredFromPageCache)
+ return;
+
+ testPassed("Received message after restoring from page cache");
+ shouldBe("openEventCount", "2");
+ shouldBe("errorEventCount", "1");
+ finishJSTest();
+ }
+ }, 0);
+}
+
+</script>
+</body>
+</html>
Added: trunk/LayoutTests/http/tests/eventsource/eventsource-page-cache-connecting-expected.txt (0 => 250761)
--- trunk/LayoutTests/http/tests/eventsource/eventsource-page-cache-connecting-expected.txt (rev 0)
+++ trunk/LayoutTests/http/tests/eventsource/eventsource-page-cache-connecting-expected.txt 2019-10-05 01:38:21 UTC (rev 250761)
@@ -0,0 +1,18 @@
+Tests that a page with an EventSource that is connecting can enter the PageCache.
+
+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 did enter and was restored from the page cache
+PASS Received open event
+PASS restoredFromPageCache is true
+PASS Received message
+PASS restoredFromPageCache is true
+PASS receivedOpenEvent is true
+PASS successfullyParsed is true
+
+TEST COMPLETE
+
Added: trunk/LayoutTests/http/tests/eventsource/eventsource-page-cache-connecting.html (0 => 250761)
--- trunk/LayoutTests/http/tests/eventsource/eventsource-page-cache-connecting.html (rev 0)
+++ trunk/LayoutTests/http/tests/eventsource/eventsource-page-cache-connecting.html 2019-10-05 01:38:21 UTC (rev 250761)
@@ -0,0 +1,59 @@
+<!-- webkit-test-runner [ enablePageCache=true ] -->
+<!DOCTYPE html>
+<html>
+<head>
+<script src=""
+</head>
+<body>
+<script>
+description("Tests that a page with an EventSource that is connecting can enter the PageCache.");
+jsTestIsAsync = true;
+let restoredFromPageCache = false;
+let receivedOpenEvent = false;
+
+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");
+ restoredFromPageCache = 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();
+ }
+
+ eventSource = new EventSource("resources/infinite-event-stream.php");
+
+ eventSource._onerror_ = () => {
+ testFailed("Received error event");
+ finishJSTest();
+ }
+
+ eventSource._onopen_ = () => {
+ testPassed("Received open event");
+ shouldBeTrue("restoredFromPageCache");
+ receivedOpenEvent = true;
+ }
+
+ eventSource._onmessage_ = () => {
+ testPassed("Received message");
+ shouldBeTrue("restoredFromPageCache");
+ shouldBeTrue("receivedOpenEvent");
+ finishJSTest();
+ }
+}, false);
+
+_onload_ = () => {
+ setTimeout(() => {
+ window.location = "/navigation/resources/page-cache-helper.html";
+ }, 0);
+}
+
+</script>
+</body>
+</html>
Added: trunk/LayoutTests/http/tests/eventsource/resources/infinite-event-stream.php (0 => 250761)
--- trunk/LayoutTests/http/tests/eventsource/resources/infinite-event-stream.php (rev 0)
+++ trunk/LayoutTests/http/tests/eventsource/resources/infinite-event-stream.php 2019-10-05 01:38:21 UTC (rev 250761)
@@ -0,0 +1,27 @@
+<?php
+date_default_timezone_set("America/New_York");
+header("Content-Type: text/event-stream");
+$counter = rand(1, 10); // a random counter
+while (1) {
+// 1 is always true, so repeat the while loop forever (aka event-loop)
+ $curDate = date(DATE_ISO8601);
+ echo "event: ping\n",
+ 'data: {"time": "' . $curDate . '"}', "\n\n";
+ // Send a simple message at random intervals.
+ $counter--;
+ if (!$counter) {
+ echo 'data: This is a message at time ' . $curDate, "\n\n";
+ $counter = rand(1, 10); // reset random counter
+ }
+ // flush the output buffer and send echoed messages to the browser
+ while (ob_get_level() > 0) {
+ ob_end_flush();
+ }
+ flush();
+ // break the loop if the client aborted the connection (closed the page)
+
+ if ( connection_aborted() ) break;
+ // sleep for 1 second before running the loop again
+
+ sleep(1);
+}
Modified: trunk/Source/WebCore/ChangeLog (250760 => 250761)
--- trunk/Source/WebCore/ChangeLog 2019-10-05 01:16:58 UTC (rev 250760)
+++ trunk/Source/WebCore/ChangeLog 2019-10-05 01:38:21 UTC (rev 250761)
@@ -1,5 +1,42 @@
2019-10-04 Chris Dumez <cdu...@apple.com>
+ Allow pages using EventSource to enter the back/forward cache
+ https://bugs.webkit.org/show_bug.cgi?id=202370
+ <rdar://problem/55853142>
+
+ Reviewed by Geoffrey Garen.
+
+ Allow pages using EventSource to enter the back/forward cache. If the EventSource is
+ connecting at the time it enters PageCache, it will resume connecting after restoring
+ from PageCache (by making m_connectTimer a SuspendableTimer, which is PageCache-aware).
+ If the EventSource is was open upon navigating, it gets closed upon navigating (since
+ all pending loads get cancelled on navigation). To help the page recover, we fire an
+ error event and attempt to reconnect automatically when restoring the page from page
+ cache. It is allowed by the specification to attempt reconnection in case of non-fatal
+ network errors.
+
+ Tests: http/tests/eventsource/eventsource-page-cache-connected.html
+ http/tests/eventsource/eventsource-page-cache-connecting.html
+
+ * page/EventSource.cpp:
+ (WebCore::EventSource::EventSource):
+ (WebCore::EventSource::scheduleReconnect):
+ (WebCore::EventSource::close):
+ (WebCore::EventSource::didReceiveResponse):
+ (WebCore::EventSource::dispatchErrorEvent):
+ (WebCore::EventSource::didReceiveData):
+ (WebCore::EventSource::didFinishLoading):
+ (WebCore::EventSource::didFail):
+ (WebCore::EventSource::abortConnectionAttempt):
+ (WebCore::EventSource::doExplicitLoadCancellation):
+ (WebCore::EventSource::canSuspendForDocumentSuspension const):
+ (WebCore::EventSource::suspend):
+ (WebCore::EventSource::resume):
+ (WebCore::EventSource::dispatchMessageEvent):
+ * page/EventSource.h:
+
+2019-10-04 Chris Dumez <cdu...@apple.com>
+
ServiceWorkerContainer should never prevent a page from entering the back/forward cache
https://bugs.webkit.org/show_bug.cgi?id=202603
Modified: trunk/Source/WebCore/page/EventSource.cpp (250760 => 250761)
--- trunk/Source/WebCore/page/EventSource.cpp 2019-10-05 01:16:58 UTC (rev 250760)
+++ trunk/Source/WebCore/page/EventSource.cpp 2019-10-05 01:38:21 UTC (rev 250761)
@@ -45,6 +45,7 @@
#include "TextResourceDecoder.h"
#include "ThreadableLoader.h"
#include <wtf/IsoMallocInlines.h>
+#include <wtf/SetForScope.h>
namespace WebCore {
@@ -57,8 +58,9 @@
, m_url(url)
, m_withCredentials(eventSourceInit.withCredentials)
, m_decoder(TextResourceDecoder::create("text/plain"_s, "UTF-8"))
- , m_connectTimer(*this, &EventSource::connect)
+ , m_connectTimer(&context, *this, &EventSource::connect)
{
+ m_connectTimer.suspendIfNeeded();
}
ExceptionOr<Ref<EventSource>> EventSource::create(ScriptExecutionContext& context, const String& url, const Init& eventSourceInit)
@@ -141,9 +143,10 @@
void EventSource::scheduleReconnect()
{
+ RELEASE_ASSERT_WITH_SECURITY_IMPLICATION(!m_isSuspendedForPageCache);
m_state = CONNECTING;
m_connectTimer.startOneShot(1_ms * m_reconnectDelay);
- dispatchEvent(Event::create(eventNames().errorEvent, Event::CanBubble::No, Event::IsCancelable::No));
+ dispatchErrorEvent();
}
void EventSource::close()
@@ -155,10 +158,10 @@
// Stop trying to connect/reconnect if EventSource was explicitly closed or if ActiveDOMObject::stop() was called.
if (m_connectTimer.isActive())
- m_connectTimer.stop();
+ m_connectTimer.cancel();
if (m_requestInFlight)
- m_loader->cancel();
+ doExplicitLoadCancellation();
else {
m_state = CLOSED;
unsetPendingActivity(*this);
@@ -196,10 +199,11 @@
{
ASSERT(m_state == CONNECTING);
ASSERT(m_requestInFlight);
+ RELEASE_ASSERT_WITH_SECURITY_IMPLICATION(!m_isSuspendedForPageCache);
if (!responseIsValid(response)) {
- m_loader->cancel();
- dispatchEvent(Event::create(eventNames().errorEvent, Event::CanBubble::No, Event::IsCancelable::No));
+ doExplicitLoadCancellation();
+ dispatchErrorEvent();
return;
}
@@ -208,10 +212,16 @@
dispatchEvent(Event::create(eventNames().openEvent, Event::CanBubble::No, Event::IsCancelable::No));
}
+void EventSource::dispatchErrorEvent()
+{
+ dispatchEvent(Event::create(eventNames().errorEvent, Event::CanBubble::No, Event::IsCancelable::No));
+}
+
void EventSource::didReceiveData(const char* data, int length)
{
ASSERT(m_state == OPEN);
ASSERT(m_requestInFlight);
+ RELEASE_ASSERT_WITH_SECURITY_IMPLICATION(!m_isSuspendedForPageCache);
append(m_receiveBuffer, m_decoder->decode(data, length));
parseEventStream();
@@ -221,6 +231,7 @@
{
ASSERT(m_state == OPEN);
ASSERT(m_requestInFlight);
+ RELEASE_ASSERT_WITH_SECURITY_IMPLICATION(!m_isSuspendedForPageCache);
append(m_receiveBuffer, m_decoder->flush());
parseEventStream();
@@ -247,6 +258,14 @@
ASSERT(m_requestInFlight);
+ // This is the case where the load gets cancelled on navigating away. We only fire an error event and attempt to reconnect
+ // if we end up getting resumed from page cache.
+ if (error.isCancellation() && !m_isDoingExplicitCancellation) {
+ m_shouldReconnectOnResume = true;
+ m_requestInFlight = false;
+ return;
+ }
+
if (error.isCancellation())
m_state = CLOSED;
@@ -258,9 +277,10 @@
void EventSource::abortConnectionAttempt()
{
ASSERT(m_state == CONNECTING);
+ RELEASE_ASSERT_WITH_SECURITY_IMPLICATION(!m_isSuspendedForPageCache);
if (m_requestInFlight)
- m_loader->cancel();
+ doExplicitLoadCancellation();
else {
m_state = CLOSED;
unsetPendingActivity(*this);
@@ -270,6 +290,13 @@
dispatchEvent(Event::create(eventNames().errorEvent, Event::CanBubble::No, Event::IsCancelable::No));
}
+void EventSource::doExplicitLoadCancellation()
+{
+ ASSERT(m_requestInFlight);
+ SetForScope<bool> explicitLoadCancellation(m_isDoingExplicitCancellation, true);
+ m_loader->cancel();
+}
+
void EventSource::parseEventStream()
{
unsigned position = 0;
@@ -378,12 +405,36 @@
bool EventSource::canSuspendForDocumentSuspension() const
{
- // FIXME: We should return true here when we can because this object is not actually currently active.
- return false;
+ return true;
}
+void EventSource::suspend(ReasonForSuspension reason)
+{
+ if (reason != ReasonForSuspension::PageCache)
+ return;
+
+ m_isSuspendedForPageCache = true;
+ RELEASE_ASSERT_WITH_MESSAGE(!m_requestInFlight, "Loads get cancelled before entering PageCache.");
+}
+
+void EventSource::resume()
+{
+ if (!m_isSuspendedForPageCache)
+ return;
+
+ m_isSuspendedForPageCache = false;
+ if (std::exchange(m_shouldReconnectOnResume, false)) {
+ scriptExecutionContext()->postTask([this, pendingActivity = makePendingActivity(*this)](ScriptExecutionContext&) {
+ if (!isContextStopped())
+ scheduleReconnect();
+ });
+ }
+}
+
void EventSource::dispatchMessageEvent()
{
+ RELEASE_ASSERT_WITH_SECURITY_IMPLICATION(!m_isSuspendedForPageCache);
+
if (!m_currentlyParsedEventId.isNull())
m_lastEventId = WTFMove(m_currentlyParsedEventId);
Modified: trunk/Source/WebCore/page/EventSource.h (250760 => 250761)
--- trunk/Source/WebCore/page/EventSource.h 2019-10-05 01:16:58 UTC (rev 250760)
+++ trunk/Source/WebCore/page/EventSource.h 2019-10-05 01:38:21 UTC (rev 250761)
@@ -34,9 +34,10 @@
#include "ActiveDOMObject.h"
#include "EventTarget.h"
#include "ExceptionOr.h"
-#include <wtf/URL.h>
+#include "SuspendableTimer.h"
#include "ThreadableLoaderClient.h"
#include "Timer.h"
+#include <wtf/URL.h>
#include <wtf/Vector.h>
namespace WebCore {
@@ -78,6 +79,9 @@
void refEventTarget() final { ref(); }
void derefEventTarget() final { deref(); }
+ void dispatchErrorEvent();
+ void doExplicitLoadCancellation();
+
// ThreadableLoaderClient
void didReceiveResponse(unsigned long, const ResourceResponse&) final;
void didReceiveData(const char*, int) final;
@@ -84,9 +88,12 @@
void didFinishLoading(unsigned long) final;
void didFail(const ResourceError&) final;
+ // ActiveDOMObject
void stop() final;
const char* activeDOMObjectName() const final;
bool canSuspendForDocumentSuspension() const final;
+ void suspend(ReasonForSuspension) final;
+ void resume() final;
void connect();
void networkRequestEnded();
@@ -107,10 +114,13 @@
Ref<TextResourceDecoder> m_decoder;
RefPtr<ThreadableLoader> m_loader;
- Timer m_connectTimer;
+ SuspendableTimer m_connectTimer;
Vector<UChar> m_receiveBuffer;
bool m_discardTrailingNewline { false };
bool m_requestInFlight { false };
+ bool m_isSuspendedForPageCache { false };
+ bool m_isDoingExplicitCancellation { false };
+ bool m_shouldReconnectOnResume { false };
AtomString m_eventName;
Vector<UChar> m_data;