Bug 1263734: Implement ServiceWorkerContainer.startMessages() r=asuth,smaug

Differential Revision: https://phabricator.services.mozilla.com/D4237

--HG--
extra : moz-landing-system : lando
This commit is contained in:
Yaron Tausky 2018-10-10 13:55:23 +00:00
Родитель 6347cf3474
Коммит 286a3c2333
13 изменённых файлов: 603 добавлений и 122 удалений

Просмотреть файл

@ -67,6 +67,8 @@
#include "mozilla/dom/FeaturePolicy.h" #include "mozilla/dom/FeaturePolicy.h"
#include "mozilla/dom/FramingChecker.h" #include "mozilla/dom/FramingChecker.h"
#include "mozilla/dom/HTMLSharedElement.h" #include "mozilla/dom/HTMLSharedElement.h"
#include "mozilla/dom/Navigator.h"
#include "mozilla/dom/ServiceWorkerContainer.h"
#include "mozilla/dom/SVGUseElement.h" #include "mozilla/dom/SVGUseElement.h"
#include "nsGenericHTMLElement.h" #include "nsGenericHTMLElement.h"
#include "mozilla/dom/CDATASection.h" #include "mozilla/dom/CDATASection.h"
@ -5195,6 +5197,14 @@ nsIDocument::DispatchContentLoadedEvents()
NS_LITERAL_STRING("DOMContentLoaded"), NS_LITERAL_STRING("DOMContentLoaded"),
CanBubble::eYes, Cancelable::eNo); CanBubble::eYes, Cancelable::eNo);
if (auto* const window = GetInnerWindow()) {
const RefPtr<ServiceWorkerContainer> serviceWorker = window->Navigator()->ServiceWorker();
// This could cause queued messages from a service worker to get
// dispatched on serviceWorker.
serviceWorker->StartMessages();
}
if (MayStartLayout()) { if (MayStartLayout()) {
MaybeResolveReadyForIdle(); MaybeResolveReadyForIdle();
} }

Просмотреть файл

@ -605,125 +605,17 @@ RefPtr<ClientOpPromise>
ClientSource::PostMessage(const ClientPostMessageArgs& aArgs) ClientSource::PostMessage(const ClientPostMessageArgs& aArgs)
{ {
NS_ASSERT_OWNINGTHREAD(ClientSource); NS_ASSERT_OWNINGTHREAD(ClientSource);
RefPtr<ClientOpPromise> ref;
ServiceWorkerDescriptor source(aArgs.serviceWorker()); // TODO: Currently this function only supports clients whose global
const PrincipalInfo& principalInfo = source.PrincipalInfo(); // object is a Window; it should also support those whose global
// object is a WorkerGlobalScope.
StructuredCloneData clonedData; if (nsPIDOMWindowInner* const window = GetInnerWindow()) {
clonedData.BorrowFromClonedMessageDataForBackgroundChild(aArgs.clonedData()); const RefPtr<ServiceWorkerContainer> container = window->Navigator()->ServiceWorker();
container->ReceiveMessage(aArgs);
// Currently we only support firing these messages on window Clients. return ClientOpPromise::CreateAndResolve(NS_OK, __func__).forget();
// Once we expose ServiceWorkerContainer and the ServiceWorker on Worker
// threads then this will need to change. See bug 1113522.
if (mClientInfo.Type() != ClientType::Window) {
ref = ClientOpPromise::CreateAndReject(NS_ERROR_NOT_IMPLEMENTED, __func__);
return ref.forget();
} }
MOZ_ASSERT(NS_IsMainThread()); return ClientOpPromise::CreateAndReject(NS_ERROR_NOT_IMPLEMENTED, __func__).forget();
RefPtr<ServiceWorkerContainer> target;
nsCOMPtr<nsIGlobalObject> globalObject;
// We don't need to force the creation of the about:blank document
// here because there is no postMessage listener. If a listener
// was registered then the document will already be created.
nsPIDOMWindowInner* window = GetInnerWindow();
if (window) {
globalObject = do_QueryInterface(window);
target = window->Navigator()->ServiceWorker();
}
if (NS_WARN_IF(!target)) {
ref = ClientOpPromise::CreateAndReject(NS_ERROR_DOM_INVALID_STATE_ERR,
__func__);
return ref.forget();
}
// If AutoJSAPI::Init() fails then either global is nullptr or not
// in a usable state.
AutoJSAPI jsapi;
if (!jsapi.Init(globalObject)) {
ref = ClientOpPromise::CreateAndResolve(NS_OK, __func__);
return ref.forget();
}
JSContext* cx = jsapi.cx();
ErrorResult result;
JS::Rooted<JS::Value> messageData(cx);
clonedData.Read(cx, &messageData, result);
if (result.MaybeSetPendingException(cx)) {
// We reported the error in the current window context. Resolve
// promise instead of rejecting.
ref = ClientOpPromise::CreateAndResolve(NS_OK, __func__);
return ref.forget();
}
RootedDictionary<MessageEventInit> init(cx);
init.mData = messageData;
if (!clonedData.TakeTransferredPortsAsSequence(init.mPorts)) {
// Report the error in the current window context and resolve the
// promise instead of rejecting.
xpc::Throw(cx, NS_ERROR_OUT_OF_MEMORY);
ref = ClientOpPromise::CreateAndResolve(NS_OK, __func__);
return ref.forget();
}
nsresult rv = NS_OK;
nsCOMPtr<nsIPrincipal> principal =
PrincipalInfoToPrincipal(principalInfo, &rv);
if (NS_FAILED(rv) || !principal) {
ref = ClientOpPromise::CreateAndReject(NS_ERROR_FAILURE, __func__);
return ref.forget();
}
nsAutoCString origin;
rv = principal->GetOriginNoSuffix(origin);
if (NS_SUCCEEDED(rv)) {
CopyUTF8toUTF16(origin, init.mOrigin);
}
RefPtr<ServiceWorker> instance;
if (ServiceWorkerParentInterceptEnabled()) {
instance = globalObject->GetOrCreateServiceWorker(source);
} else {
// If we are in legacy child-side intercept mode then we need to verify
// this registration exists in the current process.
RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance();
if (!swm) {
// Shutting down. Just don't deliver this message.
ref = ClientOpPromise::CreateAndReject(NS_ERROR_FAILURE, __func__);
return ref.forget();
}
RefPtr<ServiceWorkerRegistrationInfo> reg =
swm->GetRegistration(principal, source.Scope());
if (reg) {
instance = globalObject->GetOrCreateServiceWorker(source);
}
}
if (instance) {
init.mSource.SetValue().SetAsServiceWorker() = instance;
}
RefPtr<MessageEvent> event =
MessageEvent::Constructor(target, NS_LITERAL_STRING("message"), init);
event->SetTrusted(true);
target->DispatchEvent(*event, result);
if (result.Failed()) {
result.SuppressException();
ref = ClientOpPromise::CreateAndReject(NS_ERROR_FAILURE, __func__);
return ref.forget();
}
ref = ClientOpPromise::CreateAndResolve(NS_OK, __func__);
return ref.forget();
} }
RefPtr<ClientOpPromise> RefPtr<ClientOpPromise>

Просмотреть файл

@ -12,6 +12,7 @@
#include "nsIDocument.h" #include "nsIDocument.h"
#include "nsIServiceWorkerManager.h" #include "nsIServiceWorkerManager.h"
#include "nsIScriptError.h" #include "nsIScriptError.h"
#include "nsThreadUtils.h"
#include "nsIURL.h" #include "nsIURL.h"
#include "nsNetUtil.h" #include "nsNetUtil.h"
#include "nsPIDOMWindow.h" #include "nsPIDOMWindow.h"
@ -22,11 +23,16 @@
#include "nsServiceManagerUtils.h" #include "nsServiceManagerUtils.h"
#include "mozilla/LoadInfo.h" #include "mozilla/LoadInfo.h"
#include "mozilla/dom/ClientIPCTypes.h"
#include "mozilla/dom/DOMMozPromiseRequestHolder.h" #include "mozilla/dom/DOMMozPromiseRequestHolder.h"
#include "mozilla/dom/MessageEvent.h"
#include "mozilla/dom/MessageEventBinding.h"
#include "mozilla/dom/Navigator.h" #include "mozilla/dom/Navigator.h"
#include "mozilla/dom/Promise.h" #include "mozilla/dom/Promise.h"
#include "mozilla/dom/ServiceWorker.h" #include "mozilla/dom/ServiceWorker.h"
#include "mozilla/dom/ServiceWorkerContainerBinding.h" #include "mozilla/dom/ServiceWorkerContainerBinding.h"
#include "mozilla/dom/ServiceWorkerManager.h"
#include "mozilla/dom/ipc/StructuredCloneData.h"
#include "RemoteServiceWorkerContainerImpl.h" #include "RemoteServiceWorkerContainerImpl.h"
#include "ServiceWorker.h" #include "ServiceWorker.h"
@ -34,6 +40,11 @@
#include "ServiceWorkerRegistration.h" #include "ServiceWorkerRegistration.h"
#include "ServiceWorkerUtils.h" #include "ServiceWorkerUtils.h"
// This is defined to something else on Windows
#ifdef DispatchMessage
#undef DispatchMessage
#endif
namespace mozilla { namespace mozilla {
namespace dom { namespace dom {
@ -152,6 +163,39 @@ ServiceWorkerContainer::ControllerChanged(ErrorResult& aRv)
aRv = DispatchTrustedEvent(NS_LITERAL_STRING("controllerchange")); aRv = DispatchTrustedEvent(NS_LITERAL_STRING("controllerchange"));
} }
using mozilla::dom::ipc::StructuredCloneData;
// A ReceivedMessage represents a message sent via
// Client.postMessage(). It is used as used both for queuing of
// incoming messages and as an interface to DispatchMessage().
struct MOZ_HEAP_CLASS ServiceWorkerContainer::ReceivedMessage
{
explicit ReceivedMessage(const ClientPostMessageArgs& aArgs)
: mServiceWorker(aArgs.serviceWorker())
{
mClonedData.CopyFromClonedMessageDataForBackgroundChild(aArgs.clonedData());
}
ServiceWorkerDescriptor mServiceWorker;
StructuredCloneData mClonedData;
NS_INLINE_DECL_REFCOUNTING(ReceivedMessage)
private:
~ReceivedMessage() = default;
};
void
ServiceWorkerContainer::ReceiveMessage(const ClientPostMessageArgs& aArgs)
{
RefPtr<ReceivedMessage> message = new ReceivedMessage(aArgs);
if (mMessagesStarted) {
EnqueueReceivedMessageDispatch(message.forget());
} else {
mPendingMessages.AppendElement(message.forget());
}
}
JSObject* JSObject*
ServiceWorkerContainer::WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) ServiceWorkerContainer::WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto)
{ {
@ -426,6 +470,16 @@ ServiceWorkerContainer::GetRegistrations(ErrorResult& aRv)
return outer.forget(); return outer.forget();
} }
void
ServiceWorkerContainer::StartMessages()
{
while (!mPendingMessages.IsEmpty()) {
EnqueueReceivedMessageDispatch(mPendingMessages.ElementAt(0));
mPendingMessages.RemoveElementAt(0);
}
mMessagesStarted = true;
}
already_AddRefed<Promise> already_AddRefed<Promise>
ServiceWorkerContainer::GetRegistration(const nsAString& aURL, ServiceWorkerContainer::GetRegistration(const nsAString& aURL,
ErrorResult& aRv) ErrorResult& aRv)
@ -618,5 +672,153 @@ ServiceWorkerContainer::GetGlobalIfValid(ErrorResult& aRv,
return window->AsGlobal(); return window->AsGlobal();
} }
void
ServiceWorkerContainer::EnqueueReceivedMessageDispatch(RefPtr<ReceivedMessage> aMessage) {
if (nsPIDOMWindowInner* const window = GetOwner()) {
if (auto* const target = window->EventTargetFor(TaskCategory::Other)) {
target->Dispatch(
NewRunnableMethod<RefPtr<ReceivedMessage>>(
"ServiceWorkerContainer::DispatchMessage",
this,
&ServiceWorkerContainer::DispatchMessage,
std::move(aMessage)
)
);
}
}
}
template <typename F>
void
ServiceWorkerContainer::RunWithJSContext(F&& aCallable)
{
nsCOMPtr<nsIGlobalObject> globalObject;
if (nsPIDOMWindowInner* const window = GetOwner()) {
globalObject = do_QueryInterface(window);
}
// If AutoJSAPI::Init() fails then either global is nullptr or not
// in a usable state.
AutoJSAPI jsapi;
if (!jsapi.Init(globalObject)) {
return;
}
aCallable(jsapi.cx(), globalObject);
}
void
ServiceWorkerContainer::DispatchMessage(RefPtr<ReceivedMessage> aMessage)
{
MOZ_ASSERT(NS_IsMainThread());
// When dispatching a message, either DOMContentLoaded has already
// been fired, or someone called startMessages() or set onmessage.
// Either way, a global object is supposed to be present. If it's
// not, we'd fail to initialize the JS API and exit.
RunWithJSContext([this, message = std::move(aMessage)](JSContext* const aCx,
nsIGlobalObject* const aGlobal) {
RootedDictionary<MessageEventInit> init(aCx);
if (!FillInMessageEventInit(aCx, aGlobal, *message, init)) {
// TODO: The spec requires us to fire a messageerror event here.
return;
}
RefPtr<MessageEvent> event =
MessageEvent::Constructor(this, NS_LITERAL_STRING("message"), init);
event->SetTrusted(true);
ErrorResult result;
DispatchEvent(*event, result);
if (result.Failed()) {
result.SuppressException();
}
});
}
namespace {
nsresult
FillInOriginNoSuffix(const ServiceWorkerDescriptor& aServiceWorker, nsString& aOrigin)
{
using mozilla::ipc::PrincipalInfoToPrincipal;
nsresult rv;
nsCOMPtr<nsIPrincipal> principal = PrincipalInfoToPrincipal(aServiceWorker.PrincipalInfo(), &rv);
if (NS_FAILED(rv) || !principal) {
return rv;
}
nsAutoCString originUTF8;
rv = principal->GetOriginNoSuffix(originUTF8);
if (NS_FAILED(rv)) {
return rv;
}
CopyUTF8toUTF16(originUTF8, aOrigin);
return NS_OK;
}
already_AddRefed<ServiceWorker>
GetOrCreateServiceWorkerWithoutWarnings(nsIGlobalObject* const aGlobal,
const ServiceWorkerDescriptor& aDescriptor)
{
// In child-intercept mode we have to verify that the registration
// exists in the current process. This exact check is also performed
// (indirectly) in nsIGlobalObject::GetOrCreateServiceWorker, but it
// also emits a warning when the registration is not present. To
// to avoid having too many warnings, we do a precheck here.
if (!ServiceWorkerParentInterceptEnabled()) {
const RefPtr<ServiceWorkerManager> serviceWorkerManager = ServiceWorkerManager::GetInstance();
if (!serviceWorkerManager) {
return nullptr;
}
const RefPtr<ServiceWorkerRegistrationInfo> registration =
serviceWorkerManager->GetRegistration(aDescriptor.PrincipalInfo(), aDescriptor.Scope());
if (!registration) {
return nullptr;
}
}
return aGlobal->GetOrCreateServiceWorker(aDescriptor).forget();
}
}
bool
ServiceWorkerContainer::FillInMessageEventInit(JSContext* const aCx,
nsIGlobalObject* const aGlobal,
ReceivedMessage& aMessage,
MessageEventInit& aInit)
{
ErrorResult result;
JS::Rooted<JS::Value> messageData(aCx);
aMessage.mClonedData.Read(aCx, &messageData, result);
if (result.Failed()) {
return false;
}
aInit.mData = messageData;
if (!aMessage.mClonedData.TakeTransferredPortsAsSequence(aInit.mPorts)) {
return false;
}
const nsresult rv = FillInOriginNoSuffix(aMessage.mServiceWorker, aInit.mOrigin);
if (NS_FAILED(rv)) {
return false;
}
const RefPtr<ServiceWorker> serviceWorkerInstance =
GetOrCreateServiceWorkerWithoutWarnings(aGlobal, aMessage.mServiceWorker);
if (serviceWorkerInstance) {
aInit.mSource.SetValue().SetAsServiceWorker() = serviceWorkerInstance;
}
return true;
}
} // namespace dom } // namespace dom
} // namespace mozilla } // namespace mozilla

Просмотреть файл

@ -15,6 +15,8 @@ class nsIGlobalWindow;
namespace mozilla { namespace mozilla {
namespace dom { namespace dom {
class ClientPostMessageArgs;
struct MessageEventInit;
class Promise; class Promise;
struct RegistrationOptions; struct RegistrationOptions;
class ServiceWorker; class ServiceWorker;
@ -64,7 +66,19 @@ public:
IMPL_EVENT_HANDLER(controllerchange) IMPL_EVENT_HANDLER(controllerchange)
IMPL_EVENT_HANDLER(error) IMPL_EVENT_HANDLER(error)
IMPL_EVENT_HANDLER(message)
// Almost a manual expansion of IMPL_EVENT_HANDLER(message), but
// with the additional StartMessages() when setting the handler, as
// required by the spec.
inline mozilla::dom::EventHandlerNonNull* GetOnmessage()
{
return GetEventHandler(nsGkAtoms::onmessage);
}
inline void SetOnmessage(mozilla::dom::EventHandlerNonNull* aCallback)
{
SetEventHandler(nsGkAtoms::onmessage, aCallback);
StartMessages();
}
static bool IsEnabled(JSContext* aCx, JSObject* aGlobal); static bool IsEnabled(JSContext* aCx, JSObject* aGlobal);
@ -89,6 +103,9 @@ public:
already_AddRefed<Promise> already_AddRefed<Promise>
GetRegistrations(ErrorResult& aRv); GetRegistrations(ErrorResult& aRv);
void
StartMessages();
Promise* Promise*
GetReady(ErrorResult& aRv); GetReady(ErrorResult& aRv);
@ -104,6 +121,9 @@ public:
void void
ControllerChanged(ErrorResult& aRv); ControllerChanged(ErrorResult& aRv);
void
ReceiveMessage(const ClientPostMessageArgs& aArgs);
private: private:
ServiceWorkerContainer(nsIGlobalObject* aGlobal, ServiceWorkerContainer(nsIGlobalObject* aGlobal,
already_AddRefed<ServiceWorkerContainer::Inner> aInner); already_AddRefed<ServiceWorkerContainer::Inner> aInner);
@ -121,6 +141,27 @@ private:
GetGlobalIfValid(ErrorResult& aRv, GetGlobalIfValid(ErrorResult& aRv,
const std::function<void(nsIDocument*)>&& aStorageFailureCB = nullptr) const; const std::function<void(nsIDocument*)>&& aStorageFailureCB = nullptr) const;
struct ReceivedMessage;
// Dispatch a Runnable that dispatches the given message on this
// object. When the owner of this object is a Window, the Runnable
// is dispatched on the corresponding TabGroup.
void
EnqueueReceivedMessageDispatch(RefPtr<ReceivedMessage> aMessage);
template <typename F>
void
RunWithJSContext(F&& aCallable);
void
DispatchMessage(RefPtr<ReceivedMessage> aMessage);
static bool
FillInMessageEventInit(JSContext* aCx,
nsIGlobalObject* aGlobal,
ReceivedMessage& aMessage,
MessageEventInit& aInit);
RefPtr<Inner> mInner; RefPtr<Inner> mInner;
// This only changes when a worker hijacks everything in its scope by calling // This only changes when a worker hijacks everything in its scope by calling
@ -129,6 +170,13 @@ private:
RefPtr<Promise> mReadyPromise; RefPtr<Promise> mReadyPromise;
MozPromiseRequestHolder<ServiceWorkerRegistrationPromise> mReadyPromiseHolder; MozPromiseRequestHolder<ServiceWorkerRegistrationPromise> mReadyPromiseHolder;
// Set after StartMessages() has been called.
bool mMessagesStarted = false;
// Queue holding messages posted from service worker as long as
// StartMessages() hasn't been called.
nsTArray<RefPtr<ReceivedMessage>> mPendingMessages;
}; };
} // namespace dom } // namespace dom

Просмотреть файл

@ -4,7 +4,7 @@
* You can obtain one at http://mozilla.org/MPL/2.0/. * You can obtain one at http://mozilla.org/MPL/2.0/.
* *
* The origin of this IDL file is * The origin of this IDL file is
* http://slightlyoff.github.io/ServiceWorker/spec/service_worker/index.html * https://w3c.github.io/ServiceWorker/#serviceworkercontainer
* *
*/ */
@ -29,6 +29,8 @@ interface ServiceWorkerContainer : EventTarget {
[NewObject] [NewObject]
Promise<sequence<ServiceWorkerRegistration>> getRegistrations(); Promise<sequence<ServiceWorkerRegistration>> getRegistrations();
void startMessages();
attribute EventHandler oncontrollerchange; attribute EventHandler oncontrollerchange;
attribute EventHandler onerror; attribute EventHandler onerror;
attribute EventHandler onmessage; attribute EventHandler onmessage;

Просмотреть файл

@ -21,7 +21,8 @@
} }
promise_test(async t => { promise_test(async t => {
const scope = SCOPE + "?q=aborted-not-intercepted"; const suffix = "?q=aborted-not-intercepted";
const scope = SCOPE + suffix;
await setupRegistration(t, scope); await setupRegistration(t, scope);
const iframe = await with_iframe(scope); const iframe = await with_iframe(scope);
add_completion_callback(_ => iframe.remove()); add_completion_callback(_ => iframe.remove());
@ -33,8 +34,13 @@
const nextData = new Promise(resolve => { const nextData = new Promise(resolve => {
w.navigator.serviceWorker.addEventListener('message', function once(event) { w.navigator.serviceWorker.addEventListener('message', function once(event) {
w.navigator.serviceWorker.removeEventListener('message', once); // The message triggered by the iframe's document's fetch
resolve(event.data); // request cannot get dispatched by the time we add the event
// listener, so we have to guard against it.
if (!event.data.endsWith(suffix)) {
w.navigator.serviceWorker.removeEventListener('message', once);
resolve(event.data);
}
}) })
}); });

Просмотреть файл

@ -1,5 +1,5 @@
self.addEventListener('fetch', function(event) { self.addEventListener('fetch', function(event) {
if (event.request.url.includes('dummy')) { if (event.request.url.includes('dummy.html?')) {
event.waitUntil(async function() { event.waitUntil(async function() {
let destination = new URL(event.request.url).searchParams.get("dest"); let destination = new URL(event.request.url).searchParams.get("dest");
var result = "FAIL"; var result = "FAIL";

Просмотреть файл

@ -51,4 +51,208 @@ promise_test(t => {
}) })
.then(e => { assert_equals(e.data, 'quit'); }); .then(e => { assert_equals(e.data, 'quit'); });
}, 'postMessage from ServiceWorker to Client.'); }, 'postMessage from ServiceWorker to Client.');
// This function creates a message listener that captures all messages
// sent to this window and matches them with corresponding requests.
// This frees test code from having to use clunky constructs just to
// avoid race conditions, since the relative order of message and
// request arrival doesn't matter.
function create_message_listener(t) {
const listener = {
messages: new Set(),
requests: new Set(),
waitFor: function(predicate) {
for (const event of this.messages) {
// If a message satisfying the predicate has already
// arrived, it gets matched to this request.
if (predicate(event)) {
this.messages.delete(event);
return Promise.resolve(event);
}
}
// If no match was found, the request is stored and a
// promise is returned.
const request = { predicate };
const promise = new Promise(resolve => request.resolve = resolve);
this.requests.add(request);
return promise;
}
};
window.onmessage = t.step_func(event => {
for (const request of listener.requests) {
// If the new message matches a stored request's
// predicate, the request's promise is resolved with this
// message.
if (request.predicate(event)) {
listener.requests.delete(request);
request.resolve(event);
return;
}
};
// No outstanding request for this message, store it in case
// it's requested later.
listener.messages.add(event);
});
return listener;
}
async function service_worker_register_and_activate(t, script, scope) {
const registration = await service_worker_unregister_and_register(t, script, scope);
t.add_cleanup(() => registration.unregister());
const worker = registration.installing;
await wait_for_state(t, worker, 'activated');
return worker;
}
// Add an iframe (parent) whose document contains a nested iframe
// (child), then set the child's src attribute to child_url and return
// its Window (without waiting for it to finish loading).
async function with_nested_iframes(t, child_url) {
const parent = await with_iframe('resources/nested-iframe-parent.html?role=parent');
t.add_cleanup(() => parent.remove());
const child = parent.contentWindow.document.getElementById('child');
child.setAttribute('src', child_url);
return child.contentWindow;
}
// Returns a predicate matching a fetch message with the specified
// key.
function fetch_message(key) {
return event => event.data.type === 'fetch' && event.data.key === key;
}
// Returns a predicate matching a ping message with the specified
// payload.
function ping_message(data) {
return event => event.data.type === 'ping' && event.data.data === data;
}
// A client message queue test is a testharness.js test with some
// additional setup:
// 1. A listener (see create_message_listener)
// 2. An active service worker
// 3. Two nested iframes
// 4. A state transition function that controls the order of events
// during the test
function client_message_queue_test(url, test_function, description) {
promise_test(async t => {
t.listener = create_message_listener(t);
const script = 'resources/stalling-service-worker.js';
const scope = 'resources/';
t.service_worker = await service_worker_register_and_activate(t, script, scope);
// We create two nested iframes such that both are controlled by
// the newly installed service worker.
const child_url = url + '?role=child';
t.frame = await with_nested_iframes(t, child_url);
t.state_transition = async function(from, to, scripts) {
// A state transition begins with the child's parser
// fetching a script due to a <script> tag. The request
// arrives at the service worker, which notifies the
// parent, which in turn notifies the test. Note that the
// event loop keeps spinning while the parser is waiting.
const request = await this.listener.waitFor(fetch_message(to));
// The test instructs the service worker to send two ping
// messages through the Client interface: first to the
// child, then to the parent.
this.service_worker.postMessage(from);
// When the parent receives the ping message, it forwards
// it to the test. Assuming that messages to both child
// and parent are mapped to the same task queue (this is
// not [yet] required by the spec), receiving this message
// guarantees that the child has already dispatched its
// message if it was allowed to do so.
await this.listener.waitFor(ping_message(from));
// Finally, reply to the service worker's fetch
// notification with the script it should use as the fetch
// request's response. This is a defensive mechanism that
// ensures the child's parser really is blocked until the
// test is ready to continue.
request.ports[0].postMessage([`state = '${to}';`].concat(scripts));
};
await test_function(t);
}, description);
}
function client_message_queue_enable_test(
install_script,
start_script,
earliest_dispatch,
description)
{
function later_state(state1, state2) {
const states = ['init', 'install', 'start', 'finish', 'loaded'];
const index1 = states.indexOf(state1);
const index2 = states.indexOf(state2);
const max_index = Math.max(index1, index2);
return states[max_index];
}
client_message_queue_test('enable-client-message-queue.html', async t => {
// While parsing the child's document, the child transitions
// from the 'init' state all the way to the 'finish' state.
// Once parsing is finished it would enter the final 'loaded'
// state. All but the last transition require assitance from
// the test.
await t.state_transition('init', 'install', [install_script]);
await t.state_transition('install', 'start', [start_script]);
await t.state_transition('start', 'finish', []);
// Wait for all messages to get dispatched on the child's
// ServiceWorkerContainer and then verify that each message
// was dispatched while the child was in the correct state.
const report = await t.frame.report;
['init', 'install', 'start'].forEach(state => {
const dispatch = later_state(state, earliest_dispatch);
assert_equals(report[state], dispatch,
`Message sent in state '${state}' dispatched in state '${dispatch}'`);
});
}, description);
}
const empty_script = ``;
const add_event_listener =
`navigator.serviceWorker.addEventListener('message', handle_message);`;
const set_onmessage = `navigator.serviceWorker.onmessage = handle_message;`;
const start_messages = `navigator.serviceWorker.startMessages();`;
client_message_queue_enable_test(add_event_listener, empty_script, 'loaded',
'Messages from ServiceWorker to Client only received after DOMContentLoaded event.');
client_message_queue_enable_test(add_event_listener, start_messages, 'start',
'Messages from ServiceWorker to Client only received after calling startMessages().');
client_message_queue_enable_test(set_onmessage, empty_script, 'install',
'Messages from ServiceWorker to Client only received after setting onmessage.');
const resolve_manual_promise = `resolve_manual_promise();`
async function test_microtasks_when_client_message_queue_enabled(t, scripts) {
await t.state_transition('init', 'start', scripts.concat([resolve_manual_promise]));
let result = await t.frame.result;
assert_equals(result[0], 'microtask', 'The microtask was executed first.');
assert_equals(result[1], 'message', 'The message was dispatched.');
}
client_message_queue_test('message-vs-microtask.html', t => {
return test_microtasks_when_client_message_queue_enabled(t, [
add_event_listener,
start_messages,
]);
}, 'Microtasks run before dispatching messages after calling startMessages().');
client_message_queue_test('message-vs-microtask.html', t => {
return test_microtasks_when_client_message_queue_enabled(t, [set_onmessage]);
}, 'Microtasks run before dispatching messages after setting onmessage.');
</script> </script>

Просмотреть файл

@ -41,6 +41,7 @@ if (win.location.href !== 'about:blank') {
}); });
} }
}); });
win.navigator.serviceWorker.startMessages();
} }
</script> </script>
</body> </body>

Просмотреть файл

@ -0,0 +1,39 @@
<!DOCTYPE html>
<script>
// The state variable is used by handle_message to record the time
// at which a message was handled. It's updated by the scripts
// loaded by the <script> tags at the bottom of the file as well as
// by the event listener added here.
var state = 'init';
addEventListener('DOMContentLoaded', () => state = 'loaded');
// We expect to get three ping messages from the service worker.
const expected = ['init', 'install', 'start'];
let promises = {};
let resolvers = {};
expected.forEach(name => {
promises[name] = new Promise(resolve => resolvers[name] = resolve);
});
// Once all messages have been dispatched, the state in which each
// of them was dispatched is recorded in the draft. At that point
// the draft becomes the final report.
var draft = {};
var report = Promise.all(Object.values(promises)).then(() => window.draft);
// This message handler is installed by the 'install' script.
function handle_message(event) {
const data = event.data.data;
draft[data] = state;
resolvers[data]();
}
</script>
<!--
The controlling service worker will delay the response to these
fetch requests until the test instructs it how to reply. Note that
the event loop keeps spinning while the parser is blocked.
-->
<script src="empty.js?key=install"></script>
<script src="empty.js?key=start"></script>
<script src="empty.js?key=finish"></script>

Просмотреть файл

@ -0,0 +1,18 @@
<!DOCTYPE html>
<script>
let draft = [];
var resolve_manual_promise;
let manual_promise =
new Promise(resolve => resolve_manual_promise = resolve).then(() => draft.push('microtask'));
let resolve_message_promise;
let message_promise = new Promise(resolve => resolve_message_promise = resolve);
function handle_message(event) {
draft.push('message');
resolve_message_promise();
}
var result = Promise.all([manual_promise, message_promise]).then(() => draft);
</script>
<script src="empty.js?key=start"></script>

Просмотреть файл

@ -0,0 +1,5 @@
<!DOCTYPE html>
<script>
navigator.serviceWorker.onmessage = event => parent.postMessage(event.data, '*', event.ports);
</script>
<iframe id='child'></iframe>

Просмотреть файл

@ -0,0 +1,54 @@
async function post_message_to_client(role, message, ports) {
(await clients.matchAll()).forEach(client => {
if (new URL(client.url).searchParams.get('role') === role) {
client.postMessage(message, ports);
}
});
}
async function post_message_to_child(message, ports) {
await post_message_to_client('child', message, ports);
}
function ping_message(data) {
return { type: 'ping', data };
}
self.onmessage = event => {
const message = ping_message(event.data);
post_message_to_child(message);
post_message_to_parent(message);
}
async function post_message_to_parent(message, ports) {
await post_message_to_client('parent', message, ports);
}
function fetch_message(key) {
return { type: 'fetch', key };
}
// Send a message to the parent along with a MessagePort to respond
// with.
function report_fetch_request(key) {
const channel = new MessageChannel();
const reply = new Promise(resolve => {
channel.port1.onmessage = resolve;
}).then(event => event.data);
return post_message_to_parent(fetch_message(key), [channel.port2]).then(() => reply);
}
function respond_with_script(script) {
return new Response(new Blob(script, { type: 'text/javascript' }));
}
// Whenever a controlled document requests a URL with a 'key' search
// parameter we report the request to the parent frame and wait for
// a response. The content of the response is then used to respond to
// the fetch request.
addEventListener('fetch', event => {
let key = new URL(event.request.url).searchParams.get('key');
if (key) {
event.respondWith(report_fetch_request(key).then(respond_with_script));
}
});