зеркало из https://github.com/mozilla/gecko-dev.git
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:
Родитель
87fbb29107
Коммит
f6f1bde06f
|
@ -67,6 +67,8 @@
|
|||
#include "mozilla/dom/FeaturePolicy.h"
|
||||
#include "mozilla/dom/FramingChecker.h"
|
||||
#include "mozilla/dom/HTMLSharedElement.h"
|
||||
#include "mozilla/dom/Navigator.h"
|
||||
#include "mozilla/dom/ServiceWorkerContainer.h"
|
||||
#include "mozilla/dom/SVGUseElement.h"
|
||||
#include "nsGenericHTMLElement.h"
|
||||
#include "mozilla/dom/CDATASection.h"
|
||||
|
@ -5195,6 +5197,14 @@ nsIDocument::DispatchContentLoadedEvents()
|
|||
NS_LITERAL_STRING("DOMContentLoaded"),
|
||||
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()) {
|
||||
MaybeResolveReadyForIdle();
|
||||
}
|
||||
|
|
|
@ -605,125 +605,17 @@ RefPtr<ClientOpPromise>
|
|||
ClientSource::PostMessage(const ClientPostMessageArgs& aArgs)
|
||||
{
|
||||
NS_ASSERT_OWNINGTHREAD(ClientSource);
|
||||
RefPtr<ClientOpPromise> ref;
|
||||
|
||||
ServiceWorkerDescriptor source(aArgs.serviceWorker());
|
||||
const PrincipalInfo& principalInfo = source.PrincipalInfo();
|
||||
|
||||
StructuredCloneData clonedData;
|
||||
clonedData.BorrowFromClonedMessageDataForBackgroundChild(aArgs.clonedData());
|
||||
|
||||
// Currently we only support firing these messages on window Clients.
|
||||
// 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();
|
||||
// TODO: Currently this function only supports clients whose global
|
||||
// object is a Window; it should also support those whose global
|
||||
// object is a WorkerGlobalScope.
|
||||
if (nsPIDOMWindowInner* const window = GetInnerWindow()) {
|
||||
const RefPtr<ServiceWorkerContainer> container = window->Navigator()->ServiceWorker();
|
||||
container->ReceiveMessage(aArgs);
|
||||
return ClientOpPromise::CreateAndResolve(NS_OK, __func__).forget();
|
||||
}
|
||||
|
||||
MOZ_ASSERT(NS_IsMainThread());
|
||||
|
||||
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();
|
||||
return ClientOpPromise::CreateAndReject(NS_ERROR_NOT_IMPLEMENTED, __func__).forget();
|
||||
}
|
||||
|
||||
RefPtr<ClientOpPromise>
|
||||
|
|
|
@ -12,6 +12,7 @@
|
|||
#include "nsIDocument.h"
|
||||
#include "nsIServiceWorkerManager.h"
|
||||
#include "nsIScriptError.h"
|
||||
#include "nsThreadUtils.h"
|
||||
#include "nsIURL.h"
|
||||
#include "nsNetUtil.h"
|
||||
#include "nsPIDOMWindow.h"
|
||||
|
@ -22,11 +23,16 @@
|
|||
#include "nsServiceManagerUtils.h"
|
||||
|
||||
#include "mozilla/LoadInfo.h"
|
||||
#include "mozilla/dom/ClientIPCTypes.h"
|
||||
#include "mozilla/dom/DOMMozPromiseRequestHolder.h"
|
||||
#include "mozilla/dom/MessageEvent.h"
|
||||
#include "mozilla/dom/MessageEventBinding.h"
|
||||
#include "mozilla/dom/Navigator.h"
|
||||
#include "mozilla/dom/Promise.h"
|
||||
#include "mozilla/dom/ServiceWorker.h"
|
||||
#include "mozilla/dom/ServiceWorkerContainerBinding.h"
|
||||
#include "mozilla/dom/ServiceWorkerManager.h"
|
||||
#include "mozilla/dom/ipc/StructuredCloneData.h"
|
||||
|
||||
#include "RemoteServiceWorkerContainerImpl.h"
|
||||
#include "ServiceWorker.h"
|
||||
|
@ -34,6 +40,11 @@
|
|||
#include "ServiceWorkerRegistration.h"
|
||||
#include "ServiceWorkerUtils.h"
|
||||
|
||||
// This is defined to something else on Windows
|
||||
#ifdef DispatchMessage
|
||||
#undef DispatchMessage
|
||||
#endif
|
||||
|
||||
namespace mozilla {
|
||||
namespace dom {
|
||||
|
||||
|
@ -152,6 +163,39 @@ ServiceWorkerContainer::ControllerChanged(ErrorResult& aRv)
|
|||
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*
|
||||
ServiceWorkerContainer::WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto)
|
||||
{
|
||||
|
@ -426,6 +470,16 @@ ServiceWorkerContainer::GetRegistrations(ErrorResult& aRv)
|
|||
return outer.forget();
|
||||
}
|
||||
|
||||
void
|
||||
ServiceWorkerContainer::StartMessages()
|
||||
{
|
||||
while (!mPendingMessages.IsEmpty()) {
|
||||
EnqueueReceivedMessageDispatch(mPendingMessages.ElementAt(0));
|
||||
mPendingMessages.RemoveElementAt(0);
|
||||
}
|
||||
mMessagesStarted = true;
|
||||
}
|
||||
|
||||
already_AddRefed<Promise>
|
||||
ServiceWorkerContainer::GetRegistration(const nsAString& aURL,
|
||||
ErrorResult& aRv)
|
||||
|
@ -618,5 +672,153 @@ ServiceWorkerContainer::GetGlobalIfValid(ErrorResult& aRv,
|
|||
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 mozilla
|
||||
|
|
|
@ -15,6 +15,8 @@ class nsIGlobalWindow;
|
|||
namespace mozilla {
|
||||
namespace dom {
|
||||
|
||||
class ClientPostMessageArgs;
|
||||
struct MessageEventInit;
|
||||
class Promise;
|
||||
struct RegistrationOptions;
|
||||
class ServiceWorker;
|
||||
|
@ -64,7 +66,19 @@ public:
|
|||
|
||||
IMPL_EVENT_HANDLER(controllerchange)
|
||||
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);
|
||||
|
||||
|
@ -89,6 +103,9 @@ public:
|
|||
already_AddRefed<Promise>
|
||||
GetRegistrations(ErrorResult& aRv);
|
||||
|
||||
void
|
||||
StartMessages();
|
||||
|
||||
Promise*
|
||||
GetReady(ErrorResult& aRv);
|
||||
|
||||
|
@ -104,6 +121,9 @@ public:
|
|||
void
|
||||
ControllerChanged(ErrorResult& aRv);
|
||||
|
||||
void
|
||||
ReceiveMessage(const ClientPostMessageArgs& aArgs);
|
||||
|
||||
private:
|
||||
ServiceWorkerContainer(nsIGlobalObject* aGlobal,
|
||||
already_AddRefed<ServiceWorkerContainer::Inner> aInner);
|
||||
|
@ -121,6 +141,27 @@ private:
|
|||
GetGlobalIfValid(ErrorResult& aRv,
|
||||
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;
|
||||
|
||||
// This only changes when a worker hijacks everything in its scope by calling
|
||||
|
@ -129,6 +170,13 @@ private:
|
|||
|
||||
RefPtr<Promise> mReadyPromise;
|
||||
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
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
* You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
*
|
||||
* 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]
|
||||
Promise<sequence<ServiceWorkerRegistration>> getRegistrations();
|
||||
|
||||
void startMessages();
|
||||
|
||||
attribute EventHandler oncontrollerchange;
|
||||
attribute EventHandler onerror;
|
||||
attribute EventHandler onmessage;
|
||||
|
|
|
@ -21,7 +21,8 @@
|
|||
}
|
||||
|
||||
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);
|
||||
const iframe = await with_iframe(scope);
|
||||
add_completion_callback(_ => iframe.remove());
|
||||
|
@ -33,8 +34,13 @@
|
|||
|
||||
const nextData = new Promise(resolve => {
|
||||
w.navigator.serviceWorker.addEventListener('message', function once(event) {
|
||||
w.navigator.serviceWorker.removeEventListener('message', once);
|
||||
resolve(event.data);
|
||||
// The message triggered by the iframe's document's fetch
|
||||
// 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,7 +1,8 @@
|
|||
self.addEventListener('fetch', function(event) {
|
||||
if (event.request.url.includes('dummy')) {
|
||||
const url = event.request.url;
|
||||
if (url.includes('dummy') && url.includes('?')) {
|
||||
event.waitUntil(async function() {
|
||||
let destination = new URL(event.request.url).searchParams.get("dest");
|
||||
let destination = new URL(url).searchParams.get("dest");
|
||||
var result = "FAIL";
|
||||
if (event.request.destination == destination) {
|
||||
result = "PASS";
|
||||
|
|
|
@ -51,4 +51,208 @@ promise_test(t => {
|
|||
})
|
||||
.then(e => { assert_equals(e.data, 'quit'); });
|
||||
}, '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>
|
||||
|
|
|
@ -41,6 +41,7 @@ if (win.location.href !== 'about:blank') {
|
|||
});
|
||||
}
|
||||
});
|
||||
win.navigator.serviceWorker.startMessages();
|
||||
}
|
||||
</script>
|
||||
</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));
|
||||
}
|
||||
});
|
Загрузка…
Ссылка в новой задаче