Bug 1220740 - nsIServiceWorkerRegistrationInfo should emit an event when its scriptSpec property changes;r=amarchesini
authorEddy Bruel <ejpbruel@mozilla.com>
Fri, 13 Nov 2015 21:54:45 +0100
changeset 308479 b427b61512da85abec6ce9a9ad44541ac17656b5
parent 308478 5ba17b5c8ce704328e466bde5dc95c30b7d74601
child 308480 781a1dfd83e406e87f0ef38e85a7263a71614ecd
push id1040
push userraliiev@mozilla.com
push dateMon, 29 Feb 2016 17:11:22 +0000
treeherdermozilla-release@8c3167321162 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersamarchesini
bugs1220740
milestone45.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1220740 - nsIServiceWorkerRegistrationInfo should emit an event when its scriptSpec property changes;r=amarchesini
dom/interfaces/base/nsIServiceWorkerManager.idl
dom/workers/ServiceWorkerManager.cpp
dom/workers/ServiceWorkerManager.h
dom/workers/test/serviceworkers/chrome.ini
dom/workers/test/serviceworkers/chrome_helpers.js
dom/workers/test/serviceworkers/serviceworkermanager_iframe.html
dom/workers/test/serviceworkers/serviceworkerregistrationinfo_iframe.html
dom/workers/test/serviceworkers/test_serviceworkermanager.xul
dom/workers/test/serviceworkers/test_serviceworkerregistrationinfo.xul
--- a/dom/interfaces/base/nsIServiceWorkerManager.idl
+++ b/dom/interfaces/base/nsIServiceWorkerManager.idl
@@ -23,27 +23,37 @@ interface nsIServiceWorkerUnregisterCall
 
 [scriptable, builtinclass, uuid(1a1e71dd-0f78-4e2e-a2db-a946fe02cddf)]
 interface nsIServiceWorkerInfo : nsISupports
 {
   readonly attribute DOMString scriptSpec;
   readonly attribute DOMString cacheName;
 };
 
-[scriptable, builtinclass, uuid(e908bc07-0a4c-443f-a546-2109bf1f4A01)]
+[scriptable, uuid(87e63548-d440-4b8a-b158-65ad1de0211E)]
+interface nsIServiceWorkerRegistrationInfoListener : nsISupports
+{
+  void onChange();
+};
+
+[scriptable, builtinclass, uuid(72faba24-0a1b-4284-bad3-d44c044d6d95)]
 interface nsIServiceWorkerRegistrationInfo : nsISupports
 {
   readonly attribute nsIPrincipal principal;
 
   readonly attribute DOMString scope;
   readonly attribute DOMString scriptSpec;
 
   readonly attribute nsIServiceWorkerInfo installingWorker;
   readonly attribute nsIServiceWorkerInfo waitingWorker;
   readonly attribute nsIServiceWorkerInfo activeWorker;
+
+  void addListener(in nsIServiceWorkerRegistrationInfoListener listener);
+
+  void removeListener(in nsIServiceWorkerRegistrationInfoListener listener);
 };
 
 [scriptable, uuid(9e523e7c-ad6f-4df0-8077-c74aebbc679d)]
 interface nsIServiceWorkerManagerListener : nsISupports
 {
   void onRegister(in nsIServiceWorkerRegistrationInfo aInfo);
 
   void onUnregister(in nsIServiceWorkerRegistrationInfo aInfo);
--- a/dom/workers/ServiceWorkerManager.cpp
+++ b/dom/workers/ServiceWorkerManager.cpp
@@ -431,16 +431,46 @@ NS_IMETHODIMP
 ServiceWorkerRegistrationInfo::GetActiveWorker(nsIServiceWorkerInfo **aResult)
 {
   AssertIsOnMainThread();
   nsCOMPtr<nsIServiceWorkerInfo> info = do_QueryInterface(mActiveWorker);
   info.forget(aResult);
   return NS_OK;
 }
 
+NS_IMETHODIMP
+ServiceWorkerRegistrationInfo::AddListener(
+                            nsIServiceWorkerRegistrationInfoListener *aListener)
+{
+  AssertIsOnMainThread();
+
+  if (!aListener || mListeners.Contains(aListener)) {
+    return NS_ERROR_INVALID_ARG;
+  }
+
+  mListeners.AppendElement(aListener);
+
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+ServiceWorkerRegistrationInfo::RemoveListener(
+                            nsIServiceWorkerRegistrationInfoListener *aListener)
+{
+  AssertIsOnMainThread();
+
+  if (!aListener || !mListeners.Contains(aListener)) {
+    return NS_ERROR_INVALID_ARG;
+  }
+
+  mListeners.RemoveElement(aListener);
+
+  return NS_OK;
+}
+
 already_AddRefed<ServiceWorkerInfo>
 ServiceWorkerRegistrationInfo::GetServiceWorkerInfoById(uint64_t aId)
 {
   RefPtr<ServiceWorkerInfo> serviceWorker;
   if (mInstallingWorker && mInstallingWorker->ID() == aId) {
     serviceWorker = mInstallingWorker;
   } else if (mWaitingWorker && mWaitingWorker->ID() == aId) {
     serviceWorker = mWaitingWorker;
@@ -965,16 +995,17 @@ public:
 
           return;
         }
       } else {
         mRegistration = swm->CreateNewRegistration(mScope, mPrincipal);
       }
 
       mRegistration->mScriptSpec = mScriptSpec;
+      mRegistration->NotifyListenersOnChange();
       swm->StoreRegistration(mPrincipal, mRegistration);
     } else {
       MOZ_ASSERT(mJobType == UPDATE_JOB);
     }
 
     Update();
   }
 
@@ -2458,16 +2489,25 @@ ServiceWorkerRegistrationInfo::IsLastUpd
   if ((mLastUpdateCheckTime != 0) &&
       (now - mLastUpdateCheckTime > kSecondsPerDay)) {
     return true;
   }
   return false;
 }
 
 void
+ServiceWorkerRegistrationInfo::NotifyListenersOnChange()
+{
+  nsTArray<nsCOMPtr<nsIServiceWorkerRegistrationInfoListener>> listeners(mListeners);
+  for (size_t index = 0; index < listeners.Length(); ++index) {
+    listeners[index]->OnChange();
+  }
+}
+
+void
 ServiceWorkerManager::LoadRegistration(
                              const ServiceWorkerRegistrationData& aRegistration)
 {
   AssertIsOnMainThread();
 
   nsCOMPtr<nsIPrincipal> principal =
     PrincipalInfoToPrincipal(aRegistration.principal());
   if (!principal) {
@@ -3878,31 +3918,31 @@ ServiceWorkerManager::RemoveAllRegistrat
   }
 }
 
 NS_IMETHODIMP
 ServiceWorkerManager::AddListener(nsIServiceWorkerManagerListener* aListener)
 {
   AssertIsOnMainThread();
 
-  if (mListeners.Contains(aListener)) {
+  if (!aListener || mListeners.Contains(aListener)) {
     return NS_ERROR_INVALID_ARG;
   }
 
   mListeners.AppendElement(aListener);
 
   return NS_OK;
 }
 
 NS_IMETHODIMP
 ServiceWorkerManager::RemoveListener(nsIServiceWorkerManagerListener* aListener)
 {
   AssertIsOnMainThread();
 
-  if (!mListeners.Contains(aListener)) {
+  if (!aListener || !mListeners.Contains(aListener)) {
     return NS_ERROR_INVALID_ARG;
   }
 
   mListeners.RemoveElement(aListener);
 
   return NS_OK;
 }
 
--- a/dom/workers/ServiceWorkerManager.h
+++ b/dom/workers/ServiceWorkerManager.h
@@ -67,16 +67,18 @@ public:
   nsCString mScriptSpec;
 
   nsCOMPtr<nsIPrincipal> mPrincipal;
 
   RefPtr<ServiceWorkerInfo> mActiveWorker;
   RefPtr<ServiceWorkerInfo> mWaitingWorker;
   RefPtr<ServiceWorkerInfo> mInstallingWorker;
 
+  nsTArray<nsCOMPtr<nsIServiceWorkerRegistrationInfoListener>> mListeners;
+
   uint64_t mLastUpdateCheckTime;
 
   // When unregister() is called on a registration, it is not immediately
   // removed since documents may be controlled. It is marked as
   // pendingUninstall and when all controlling documents go away, removed.
   bool mPendingUninstall;
 
   ServiceWorkerRegistrationInfo(const nsACString& aScope,
@@ -133,16 +135,19 @@ public:
   void
   FinishActivate(bool aSuccess);
 
   void
   RefreshLastUpdateCheckTime();
 
   bool
   IsLastUpdateCheckTimeOverOneDay() const;
+
+  void
+  NotifyListenersOnChange();
 };
 
 class ServiceWorkerUpdateFinishCallback
 {
 protected:
   virtual ~ServiceWorkerUpdateFinishCallback()
   { }
 
--- a/dom/workers/test/serviceworkers/chrome.ini
+++ b/dom/workers/test/serviceworkers/chrome.ini
@@ -1,13 +1,17 @@
 [DEFAULT]
 skip-if = buildapp == 'b2g' || os == 'android'
 support-files =
   app/*
   app2/*
+  chrome_helpers.js
   serviceworkermanager_iframe.html
+  serviceworkerregistrationinfo_iframe.html
   worker.js
+  worker2.js
 
 [test_aboutserviceworkers.html]
 skip-if = true #bug 1193319
 [test_app_installation.html]
 [test_privateBrowsing.html]
 [test_serviceworkermanager.xul]
+[test_serviceworkerregistrationinfo.xul]
new file mode 100644
--- /dev/null
+++ b/dom/workers/test/serviceworkers/chrome_helpers.js
@@ -0,0 +1,59 @@
+let { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+
+Cu.import("resource://gre/modules/Task.jsm");
+
+let swm = Cc["@mozilla.org/serviceworkers/manager;1"].
+          getService(Ci.nsIServiceWorkerManager);
+
+let EXAMPLE_URL = "https://example.com/chrome/dom/workers/test/serviceworkers/";
+
+function waitForIframeLoad(iframe) {
+  return new Promise(function (resolve) {
+    iframe.onload = resolve;
+  });
+}
+
+function waitForRegister(scope, callback) {
+  return new Promise(function (resolve) {
+    let listener = {
+      onRegister: function (registration) {
+        if (registration.scope !== scope) {
+          return;
+        }
+        swm.removeListener(listener);
+        resolve(callback ? callback(registration) : registration);
+      }
+    };
+    swm.addListener(listener);
+  });
+}
+
+function waitForUnregister(scope) {
+  return new Promise(function (resolve) {
+    let listener = {
+      onUnregister: function (registration) {
+        if (registration.scope !== scope) {
+          return;
+        }
+        swm.removeListener(listener);
+        resolve(registration);
+      }
+    };
+    swm.addListener(listener);
+  });
+}
+
+function waitForServiceWorkerRegistrationChange(registration, callback) {
+  return new Promise(function (resolve) {
+    let listener = {
+      onChange: function () {
+        registration.removeListener(listener);
+        if (callback) {
+          callback();
+        }
+        resolve(callback ? callback() : undefined);
+      }
+    };
+    registration.addListener(listener);
+  });
+}
--- a/dom/workers/test/serviceworkers/serviceworkermanager_iframe.html
+++ b/dom/workers/test/serviceworkers/serviceworkermanager_iframe.html
@@ -4,23 +4,31 @@
     <meta charset="utf-8">
     <script>
       window.onmessage = function (event) {
         if (event.data !== "register") {
           return;
         }
         var promise = navigator.serviceWorker.register("worker.js");
         window.onmessage = function (event) {
-          if (event.data !== "unregister") {
+          if (event.data !== "register") {
             return;
           }
-          promise.then(function (registration) {
-            registration.unregister();
+          promise = promise.then(function (registration) {
+            return navigator.serviceWorker.register("worker2.js");
           });
-          window.onmessage = null;
+          window.onmessage = function (event) {
+            if (event.data !== "unregister") {
+              return;
+            }
+            promise.then(function (registration) {
+              registration.unregister();
+            });
+            window.onmessage = null;
+          };
         };
       };
     </script>
   </head>
   <body>
     This is a test page.
   </body>
 <html>
new file mode 100644
--- /dev/null
+++ b/dom/workers/test/serviceworkers/serviceworkerregistrationinfo_iframe.html
@@ -0,0 +1,25 @@
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="utf-8">
+    <script>
+      window.onmessage = function (event) {
+        if (event.data !== "register") {
+          return;
+        }
+        var promise = navigator.serviceWorker.register("worker.js");
+        window.onmessage = function (event) {
+          if (event.data !== "register") {
+            return;
+          }
+          promise.then(function (registration) {
+            return navigator.serviceWorker.register("worker2.js");
+          });
+        };
+      };
+    </script>
+  </head>
+  <body>
+    This is a test page.
+  </body>
+<html>
--- a/dom/workers/test/serviceworkers/test_serviceworkermanager.xul
+++ b/dom/workers/test/serviceworkers/test_serviceworkermanager.xul
@@ -3,73 +3,30 @@
   Any copyright is dedicated to the Public Domain.
   http://creativecommons.org/publicdomain/zero/1.0/
 -->
 <window title="Test for ServiceWorkerManager"
         xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
         onload="test();">
   <script type="application/javascript"
           src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+  <script type="application/javascript" src="chrome_helpers.js"/>
   <script type="application/javascript">
   <![CDATA[
 
-    let { classes: Cc, interfaces: Ci, utils: Cu } = Components;
-
-    Cu.import("resource://gre/modules/Task.jsm");
-
-    let swm = Cc["@mozilla.org/serviceworkers/manager;1"].
-              getService(Ci.nsIServiceWorkerManager);
-
-    function waitForIframeLoad(iframe) {
-      return new Promise(function (resolve) {
-        iframe.onload = resolve;
-      });
-    }
-
-    function waitForRegister(scope) {
-      return new Promise(function (resolve) {
-        let listener = {
-          onRegister: function (registration) {
-            if (registration.scope !== scope) {
-              return;
-            }
-            swm.removeListener(listener);
-            resolve(registration);
-          }
-        };
-        swm.addListener(listener);
-      });
-    }
-
-    function waitForUnregister(scope) {
-      return new Promise(function (resolve) {
-        let listener = {
-          onUnregister: function (registration) {
-            if (registration.scope !== scope) {
-              return;
-            }
-            swm.removeListener(listener);
-            resolve(registration);
-          }
-        };
-        swm.addListener(listener);
-      });
-    }
-
-    let EXAMPLE_URL = "https://example.com/chrome/dom/workers/test/serviceworkers/";
     let IFRAME_URL = EXAMPLE_URL + "serviceworkermanager_iframe.html";
 
     function test() {
       SimpleTest.waitForExplicitFinish();
 
       SpecialPowers.pushPrefEnv({'set': [
         ["dom.serviceWorkers.enabled", true],
         ["dom.serviceWorkers.testing.enabled", true],
       ]}, function () {
-        Task.spawn(function *() {
+        Task.spawn(function* () {
           let registrations = swm.getAllRegistrations();
           is(registrations.length, 0);
 
           let iframe = $("iframe");
           let promise = waitForIframeLoad(iframe);
           iframe.src = IFRAME_URL;
           yield promise;
 
@@ -79,21 +36,33 @@
           iframe.contentWindow.postMessage("register", "*");
           let registration = yield promise;
 
           registrations = swm.getAllRegistrations();
           is(registrations.length, 1);
           is(registrations.queryElementAt(0, Ci.nsIServiceWorkerRegistrationInfo),
              registration);
 
+          info("Check that the service worker manager does not notify its " +
+               "listeners when a service worker is registered with the same " +
+               "scope as an existing registration.");
+          let listener = {
+            onRegister: function () {
+              ok(false, "Listener should not have been notified.");
+            }
+          };
+          swm.addListener(listener);
+          iframe.contentWindow.postMessage("register", "*");
+
           info("Check that the service worker manager notifies its listeners " +
                "when a service worker is unregistered.");
           promise = waitForUnregister(EXAMPLE_URL);
           iframe.contentWindow.postMessage("unregister", "*");
           registration = yield promise;
+          swm.removeListener(listener);
 
           registrations = swm.getAllRegistrations();
           is(registrations.length, 0);
 
           SimpleTest.finish();
         });
       }); 
     }
new file mode 100644
--- /dev/null
+++ b/dom/workers/test/serviceworkers/test_serviceworkerregistrationinfo.xul
@@ -0,0 +1,77 @@
+<?xml version="1.0"?>
+<!--
+  Any copyright is dedicated to the Public Domain.
+  http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<window title="Test for ServiceWorkerRegistrationInfo"
+        xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+        onload="test();">
+  <script type="application/javascript"
+          src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+  <script type="application/javascript" src="chrome_helpers.js"/>
+  <script type="application/javascript">
+  <![CDATA[
+
+    let IFRAME_URL = EXAMPLE_URL + "serviceworkerregistrationinfo_iframe.html";
+
+    function test() {
+      SimpleTest.waitForExplicitFinish();
+
+      SpecialPowers.pushPrefEnv({'set': [
+        ["dom.serviceWorkers.enabled", true],
+        ["dom.serviceWorkers.testing.enabled", true],
+      ]}, function () {
+        Task.spawn(function* () {
+          let iframe = $("iframe");
+          let promise = waitForIframeLoad(iframe);
+          iframe.src = IFRAME_URL;
+          yield promise;
+
+          // The change handler is not guaranteed to be called within the same
+          // tick of the event loop as the one in which the change happened.
+          // Because of this, the exact state of the service worker registration
+          // is only known until the handler returns.
+          //
+          // Because then-handlers are resolved asynchronously, the following
+          // checks are done using callbacks, which are called synchronously
+          // when then handler is called. These callbacks can return a promise,
+          // which is used to resolve the promise returned by the function.
+
+          info("Check that a service worker registration notifies its " +
+               "listeners when its state changes.");
+          promise = waitForRegister(EXAMPLE_URL, function (registration) {
+            is(registration.scriptSpec, "");
+
+            return waitForServiceWorkerRegistrationChange(registration, function  () {
+              is(registration.scriptSpec, EXAMPLE_URL + "worker.js");
+
+              return registration;
+            });
+          });
+          iframe.contentWindow.postMessage("register", "*");
+          let registration = yield promise;
+
+          promise = waitForServiceWorkerRegistrationChange(registration, function () {
+            is(registration.scriptSpec, EXAMPLE_URL + "worker2.js");
+
+            return registration;
+          });
+          iframe.contentWindow.postMessage("register", "*");
+          yield promise;
+
+          SimpleTest.finish();
+        });
+      });
+    }
+
+  ]]>
+  </script>
+
+  <body xmlns="http://www.w3.org/1999/xhtml">
+    <p id="display"></p>
+    <div id="content" style="display:none;"></div>
+    <pre id="test"></pre>
+    <iframe id="iframe"></iframe>
+  </body>
+  <label id="test-result"/>
+</window>