зеркало из 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:
Родитель
6347cf3474
Коммит
286a3c2333
|
@ -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));
|
||||||
|
}
|
||||||
|
});
|
Загрузка…
Ссылка в новой задаче