Bug 1188981 - Show Push Service subscription endpoint URL for Service Workers in about:debugging. r=jryans
authorJan Keromnes <janx@linux.com>
Fri, 24 Jun 2016 18:06:34 +0000
changeset 302693 110c7ad7d399d16b9c77940f766a26df5d90f881
parent 302692 17a89f9b5d55ed83fa1c464cb339f7365385b521
child 302694 0e3f8401b804702c894eb5fdf7eae3cbdf618668
child 302748 bca07aa9ac51fbc5bd3f51c890e7beba3d619999
push id78832
push usercbook@mozilla.com
push dateMon, 27 Jun 2016 11:52:18 +0000
treeherdermozilla-inbound@fccaa8ee3ed9 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjryans
bugs1188981
milestone50.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 1188981 - Show Push Service subscription endpoint URL for Service Workers in about:debugging. r=jryans
devtools/client/aboutdebugging/aboutdebugging.css
devtools/client/aboutdebugging/components/target-list.js
devtools/client/aboutdebugging/components/workers/service-worker-target.js
devtools/client/aboutdebugging/test/browser.ini
devtools/client/aboutdebugging/test/browser_service_workers_push_service.js
devtools/client/aboutdebugging/test/service-workers/push-sw.html
devtools/client/locales/en-US/aboutdebugging.properties
devtools/server/actors/worker.js
devtools/shared/specs/worker.js
--- a/devtools/client/aboutdebugging/aboutdebugging.css
+++ b/devtools/client/aboutdebugging/aboutdebugging.css
@@ -3,17 +3,17 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 html, body {
   height: 100%;
   width: 100%;
 }
 
 h2, h3, h4 {
-  margin-bottom: 5px;
+  margin-bottom: 10px;
 }
 
 button {
   padding-left: 20px;
   padding-right: 20px;
   min-width: 100px;
 }
 
@@ -61,45 +61,61 @@ button {
   display: flex;
   flex-direction: row;
   align-items: baseline;
 }
 
 .target-icon {
   height: 24px;
   margin: 6px 5px 0 0;
-  /* override the icon alignment and center it using margin-top */
+  /* Override the icon alignment and center it using margin-top. */
   align-self: flex-start;
 }
 
 .target-icon:not([src]) {
   display: none;
 }
 
 .inverted-icons .target-icon {
   filter: invert(30%);
 }
 
 .target {
   flex: 1;
+  /* This is silly: https://bugzilla.mozilla.org/show_bug.cgi?id=1086218#c4. */
+  min-width: 0;
 }
 
 .target-details {
   margin: 0;
   padding: 0;
   list-style-type: none
 }
 
 .target-detail {
+  display: flex;
   font-size: 12px;
   margin-top: 7px;
+  margin-bottom: 7px;
 }
 
 .target-detail a {
   cursor: pointer;
+  white-space: nowrap;
+}
+
+.target-detail strong {
+  white-space: nowrap;
+}
+
+.target-detail span {
+  /* Truncate items that are too long (e.g. URLs that would break the UI). */
+  overflow: hidden;
+  white-space: nowrap;
+  text-overflow: ellipsis;
 }
 
 .target-detail > :not(:first-child) {
   margin-left: 8px;
 }
 
 .addons-controls {
   display: flex;
--- a/devtools/client/aboutdebugging/components/target-list.js
+++ b/devtools/client/aboutdebugging/components/target-list.js
@@ -19,17 +19,18 @@ module.exports = createClass({
   displayName: "TargetList",
 
   render() {
     let { client, debugDisabled, error, targetClass, targets, sort } = this.props;
     if (sort) {
       targets = targets.sort(LocaleCompare);
     }
     targets = targets.map(target => {
-      return targetClass({ client, target, debugDisabled });
+      let key = target.name || target.url || target.title;
+      return targetClass({ client, key, target, debugDisabled });
     });
 
     let content = "";
     if (error) {
       content = error;
     } else if (targets.length > 0) {
       content = dom.ul({ className: "target-list" }, targets);
     } else {
--- a/devtools/client/aboutdebugging/components/workers/service-worker-target.js
+++ b/devtools/client/aboutdebugging/components/workers/service-worker-target.js
@@ -12,16 +12,35 @@ const { debugWorker } = require("../../m
 const Services = require("Services");
 
 const Strings = Services.strings.createBundle(
   "chrome://devtools/locale/aboutdebugging.properties");
 
 module.exports = createClass({
   displayName: "ServiceWorkerTarget",
 
+  getInitialState() {
+    return {
+      pushSubscription: null
+    };
+  },
+
+  componentDidMount() {
+    let { client } = this.props;
+    client.addListener("push-subscription-modified",
+      this.onPushSubscriptionModified);
+    this.updatePushSubscription();
+  },
+
+  componentWillUnmount() {
+    let { client } = this.props;
+    client.removeListener("push-subscription-modified",
+      this.onPushSubscriptionModified);
+  },
+
   debug() {
     if (!this.isRunning()) {
       // If the worker is not running, we can't debug it.
       return;
     }
 
     let { client, target } = this.props;
     debugWorker(client, target.workerActor);
@@ -56,34 +75,59 @@ module.exports = createClass({
   unregister() {
     let { client, target } = this.props;
     client.request({
       to: target.registrationActor,
       type: "unregister"
     });
   },
 
+  onPushSubscriptionModified(type, data) {
+    let { target } = this.props;
+    if (data.from === target.registrationActor) {
+      this.updatePushSubscription();
+    }
+  },
+
+  updatePushSubscription() {
+    let { client, target } = this.props;
+    client.request({
+      to: target.registrationActor,
+      type: "getPushSubscription"
+    }, ({ subscription }) => {
+      this.setState({ pushSubscription: subscription });
+    });
+  },
+
   isRunning() {
     // We know the target is running if it has a worker actor.
     return !!this.props.target.workerActor;
   },
 
   render() {
     let { target, debugDisabled } = this.props;
+    let { pushSubscription } = this.state;
     let isRunning = this.isRunning();
 
     return dom.div({ className: "target-container" },
       dom.img({
         className: "target-icon",
         role: "presentation",
         src: target.icon
       }),
       dom.div({ className: "target" },
         dom.div({ className: "target-name" }, target.name),
         dom.ul({ className: "target-details" },
+          (pushSubscription ?
+            dom.li({ className: "target-detail" },
+              dom.strong(null, Strings.GetStringFromName("pushService")),
+              dom.span({ className: "service-worker-push-url" },
+                pushSubscription.endpoint)) :
+            null
+          ),
           dom.li({ className: "target-detail" },
             dom.strong(null, Strings.GetStringFromName("scope")),
             dom.span({ className: "service-worker-scope" }, target.scope),
             dom.a({
               onClick: this.unregister,
               className: "unregister-link"
             }, Strings.GetStringFromName("unregister"))
           )
--- a/devtools/client/aboutdebugging/test/browser.ini
+++ b/devtools/client/aboutdebugging/test/browser.ini
@@ -17,13 +17,14 @@ support-files =
 [browser_addons_debugging_initial_state.js]
 [browser_addons_install.js]
 [browser_addons_reload.js]
 [browser_addons_toggle_debug.js]
 [browser_page_not_found.js]
 [browser_service_workers.js]
 [browser_service_workers_not_compatible.js]
 [browser_service_workers_push.js]
+[browser_service_workers_push_service.js]
 [browser_service_workers_start.js]
 [browser_service_workers_timeout.js]
 skip-if = true # Bug 1232931
 [browser_service_workers_unregister.js]
 [browser_tabs.js]
new file mode 100644
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser_service_workers_push_service.js
@@ -0,0 +1,111 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that a Service Worker registration's Push Service subscription appears
+// in about:debugging if it exists, and disappears when unregistered.
+
+// Service workers can't be loaded from chrome://, but http:// is ok with
+// dom.serviceWorkers.testing.enabled turned on.
+const SERVICE_WORKER = URL_ROOT + "service-workers/push-sw.js";
+const TAB_URL = URL_ROOT + "service-workers/push-sw.html";
+
+const FAKE_ENDPOINT = "https://fake/endpoint";
+
+const PushService = Cc["@mozilla.org/push/Service;1"]
+  .getService(Ci.nsIPushService).wrappedJSObject;
+
+add_task(function* () {
+  info("Turn on workers via mochitest http.");
+  yield SpecialPowers.pushPrefEnv({
+    "set": [
+      // Accept workers from mochitest's http.
+      ["dom.serviceWorkers.testing.enabled", true],
+      // Enable the push service.
+      ["dom.push.connection.enabled", true],
+    ]
+  });
+
+  info("Mock the push service");
+  PushService.service = {
+    _registrations: new Map(),
+    _notify(scope) {
+      Services.obs.notifyObservers(
+        null,
+        PushService.subscriptionModifiedTopic,
+        scope);
+    },
+    init() {},
+    register(pageRecord) {
+      let registration = {
+        endpoint: FAKE_ENDPOINT
+      };
+      this._registrations.set(pageRecord.scope, registration);
+      this._notify(pageRecord.scope);
+      return Promise.resolve(registration);
+    },
+    registration(pageRecord) {
+      return Promise.resolve(this._registrations.get(pageRecord.scope));
+    },
+    unregister(pageRecord) {
+      let deleted = this._registrations.delete(pageRecord.scope);
+      if (deleted) {
+        this._notify(pageRecord.scope);
+      }
+      return Promise.resolve(deleted);
+    },
+  };
+
+  let { tab, document } = yield openAboutDebugging("workers");
+
+  // Listen for mutations in the service-workers list.
+  let serviceWorkersElement = document.getElementById("service-workers");
+  let onMutation = waitForMutation(serviceWorkersElement, { childList: true });
+
+  // Open a tab that registers a push service worker.
+  let swTab = yield addTab(TAB_URL);
+
+  // Wait for the service-workers list to update.
+  yield onMutation;
+
+  // Check that the service worker appears in the UI.
+  assertHasTarget(true, document, "service-workers", SERVICE_WORKER);
+
+  // Wait for the service worker details to update.
+  let names = [...document.querySelectorAll("#service-workers .target-name")];
+  let name = names.filter(element => element.textContent === SERVICE_WORKER)[0];
+  ok(name, "Found the service worker in the list");
+  let targetContainer = name.parentNode.parentNode;
+  let targetDetailsElement = targetContainer.querySelector(".target-details");
+  yield waitForMutation(targetDetailsElement, { childList: true });
+
+  // Retrieve the push subscription endpoint URL, and verify it looks good.
+  let pushURL = targetContainer.querySelector(".service-worker-push-url");
+  ok(pushURL, "Found the push service URL in the service worker details");
+  is(pushURL.textContent, FAKE_ENDPOINT, "The push service URL looks correct");
+
+  // Unsubscribe from the push service.
+  ContentTask.spawn(swTab.linkedBrowser, {}, function () {
+    let win = content.wrappedJSObject;
+    return win.sub.unsubscribe();
+  });
+
+  // Wait for the service worker details to update again.
+  yield waitForMutation(targetDetailsElement, { childList: true });
+  ok(!targetContainer.querySelector(".service-worker-push-url"),
+    "The push service URL should be removed");
+
+  // Finally, unregister the service worker itself.
+  yield unregisterServiceWorker(swTab).then(() => {
+    ok(true, "Service worker registration unregistered");
+  }).catch(function (e) {
+    ok(false, "Service worker not unregistered; " + e);
+  });
+
+  info("Unmock the push service");
+  PushService.service = null;
+
+  yield removeTab(swTab);
+  yield closeAboutDebugging(tab);
+});
--- a/devtools/client/aboutdebugging/test/service-workers/push-sw.html
+++ b/devtools/client/aboutdebugging/test/service-workers/push-sw.html
@@ -2,20 +2,31 @@
 <html>
 <head>
   <meta charset="UTF-8">
   <title>Service worker push test</title>
 </head>
 <body>
 <script type="text/javascript">
 "use strict";
+SpecialPowers.addPermission("desktop-notification", true, document);
 var sw = navigator.serviceWorker.register("push-sw.js");
+var sub = null;
 sw.then(
   function (registration) {
     dump("SW registered\n");
+    registration.pushManager.subscribe().then(
+      function (subscription) {
+        sub = subscription;
+        dump("SW subscribed to push: " + sub.endpoint + "\n");
+      },
+      function (error) {
+        dump("SW not subscribed to push: " + error + "\n");
+      }
+    );
   },
   function (error) {
     dump("SW not registered: " + error + "\n");
   }
 );
 </script>
 </body>
 </html>
--- a/devtools/client/locales/en-US/aboutdebugging.properties
+++ b/devtools/client/locales/en-US/aboutdebugging.properties
@@ -4,16 +4,18 @@
 
 debug = Debug
 push = Push
 start = Start
 
 scope = Scope
 unregister = unregister
 
+pushService = Push Service
+
 addons = Add-ons
 addonDebugging.label = Enable add-on debugging
 addonDebugging.tooltip = Turning this on will allow you to debug add-ons and various other parts of the browser chrome
 moreInfo = more info
 loadTemporaryAddon = Load Temporary Add-on
 extensions = Extensions
 selectAddonFromFile2 = Select Manifest File or Package (.xpi)
 reload = Reload
--- a/devtools/server/actors/worker.js
+++ b/devtools/server/actors/worker.js
@@ -7,33 +7,41 @@
 const { Ci } = require("chrome");
 const { DebuggerServer } = require("devtools/server/main");
 const Services = require("Services");
 const { XPCOMUtils } = require("resource://gre/modules/XPCOMUtils.jsm");
 const protocol = require("devtools/shared/protocol");
 const { Arg, method, RetVal } = protocol;
 const {
   workerSpec,
+  pushSubscriptionSpec,
   serviceWorkerRegistrationSpec,
 } = require("devtools/shared/specs/worker");
 
 loader.lazyRequireGetter(this, "ChromeUtils");
+loader.lazyRequireGetter(this, "events", "sdk/event/core");
 
 XPCOMUtils.defineLazyServiceGetter(
   this, "wdm",
   "@mozilla.org/dom/workers/workerdebuggermanager;1",
   "nsIWorkerDebuggerManager"
 );
 
 XPCOMUtils.defineLazyServiceGetter(
   this, "swm",
   "@mozilla.org/serviceworkers/manager;1",
   "nsIServiceWorkerManager"
 );
 
+XPCOMUtils.defineLazyServiceGetter(
+  this, "PushService",
+  "@mozilla.org/push/Service;1",
+  "nsIPushService"
+);
+
 function matchWorkerDebugger(dbg, options) {
   if ("type" in options && dbg.type !== options.type) {
     return false;
   }
   if ("window" in options) {
     let window = dbg.window;
     while (window !== null && window.parent !== window) {
       window = window.parent;
@@ -43,43 +51,42 @@ function matchWorkerDebugger(dbg, option
       return false;
     }
   }
 
   return true;
 }
 
 let WorkerActor = protocol.ActorClassWithSpec(workerSpec, {
-  initialize: function (conn, dbg) {
+  initialize(conn, dbg) {
     protocol.Actor.prototype.initialize.call(this, conn);
     this._dbg = dbg;
     this._attached = false;
     this._threadActor = null;
     this._transport = null;
-    this.manage(this);
   },
 
-  form: function (detail) {
+  form(detail) {
     if (detail === "actorid") {
       return this.actorID;
     }
     let form = {
       actor: this.actorID,
       consoleActor: this._consoleActor,
       url: this._dbg.url,
       type: this._dbg.type
     };
     if (this._dbg.type === Ci.nsIWorkerDebugger.TYPE_SERVICE) {
       let registration = this._getServiceWorkerRegistrationInfo();
       form.scope = registration.scope;
     }
     return form;
   },
 
-  attach: function () {
+  attach() {
     if (this._dbg.isClosed) {
       return { error: "closed" };
     }
 
     if (!this._attached) {
       // Automatically disable their internal timeout that shut them down
       // Should be refactored by having actors specific to service workers
       if (this._dbg.type == Ci.nsIWorkerDebugger.TYPE_SERVICE) {
@@ -93,27 +100,38 @@ let WorkerActor = protocol.ActorClassWit
     }
 
     return {
       type: "attached",
       url: this._dbg.url
     };
   },
 
-  detach: function () {
+  detach() {
     if (!this._attached) {
       return { error: "wrongState" };
     }
 
     this._detach();
 
     return { type: "detached" };
   },
 
-  connect: function (options) {
+  destroy() {
+    protocol.Actor.prototype.destroy.call(this);
+    if (this._attached) {
+      this._detach();
+    }
+  },
+
+  disconnect() {
+    this.destroy();
+  },
+
+  connect(options) {
     if (!this._attached) {
       return { error: "wrongState" };
     }
 
     if (this._threadActor !== null) {
       return {
         type: "connected",
         threadActor: this._threadActor
@@ -132,49 +150,49 @@ let WorkerActor = protocol.ActorClassWit
         threadActor: this._threadActor,
         consoleActor: this._consoleActor
       };
     }, (error) => {
       return { error: error.toString() };
     });
   },
 
-  push: function () {
+  push() {
     if (this._dbg.type !== Ci.nsIWorkerDebugger.TYPE_SERVICE) {
       return { error: "wrongType" };
     }
     let registration = this._getServiceWorkerRegistrationInfo();
     let originAttributes = ChromeUtils.originAttributesToSuffix(
       this._dbg.principal.originAttributes);
     swm.sendPushEvent(originAttributes, registration.scope);
     return { type: "pushed" };
   },
 
-  onClose: function () {
+  onClose() {
     if (this._attached) {
       this._detach();
     }
 
     this.conn.sendActorEvent(this.actorID, "close");
   },
 
-  onError: function (filename, lineno, message) {
+  onError(filename, lineno, message) {
     reportError("ERROR:" + filename + ":" + lineno + ":" + message + "\n");
   },
 
   _getServiceWorkerRegistrationInfo() {
     return swm.getRegistrationByPrincipal(this._dbg.principal, this._dbg.url);
   },
 
-  _getServiceWorkerInfo: function () {
+  _getServiceWorkerInfo() {
     let registration = this._getServiceWorkerRegistrationInfo();
     return registration.getWorkerByID(this._dbg.serviceWorkerID);
   },
 
-  _detach: function () {
+  _detach() {
     if (this._threadActor !== null) {
       this._transport.close();
       this._transport = null;
       this._threadActor = null;
     }
 
     // If the worker is already destroyed, nsIWorkerDebugger.type throws
     // (_dbg.closed appears to be false when it throws)
@@ -203,17 +221,17 @@ function WorkerActorList(conn, options) 
   this._actors = new Map();
   this._onListChanged = null;
   this._mustNotify = false;
   this.onRegister = this.onRegister.bind(this);
   this.onUnregister = this.onUnregister.bind(this);
 }
 
 WorkerActorList.prototype = {
-  getList: function () {
+  getList() {
     // Create a set of debuggers.
     let dbgs = new Set();
     let e = wdm.getWorkerDebuggerEnumerator();
     while (e.hasMoreElements()) {
       let dbg = e.getNext().QueryInterface(Ci.nsIWorkerDebugger);
       if (matchWorkerDebugger(dbg, this._options)) {
         dbgs.add(dbg);
       }
@@ -266,101 +284,194 @@ WorkerActorList.prototype = {
       }
       if (this._onListChanged !== null && onListChanged === null) {
         wdm.removeListener(this);
       }
     }
     this._onListChanged = onListChanged;
   },
 
-  _notifyListChanged: function () {
+  _notifyListChanged() {
     this._onListChanged();
 
     if (this._onListChanged !== null) {
       wdm.removeListener(this);
     }
     this._mustNotify = false;
   },
 
-  onRegister: function (dbg) {
+  onRegister(dbg) {
     if (matchWorkerDebugger(dbg, this._options)) {
       this._notifyListChanged();
     }
   },
 
-  onUnregister: function (dbg) {
+  onUnregister(dbg) {
     if (matchWorkerDebugger(dbg, this._options)) {
       this._notifyListChanged();
     }
   }
 };
 
 exports.WorkerActorList = WorkerActorList;
 
+let PushSubscriptionActor = protocol.ActorClassWithSpec(pushSubscriptionSpec, {
+  initialize(conn, subscription) {
+    protocol.Actor.prototype.initialize.call(this, conn);
+    this._subscription = subscription;
+  },
+
+  form(detail) {
+    if (detail === "actorid") {
+      return this.actorID;
+    }
+    let subscription = this._subscription;
+    return {
+      actor: this.actorID,
+      endpoint: subscription.endpoint,
+      pushCount: subscription.pushCount,
+      lastPush: subscription.lastPush,
+      quota: subscription.quota
+    };
+  },
+
+  destroy() {
+    protocol.Actor.prototype.destroy.call(this);
+    this._subscription = null;
+  },
+});
+
 // Lazily load the service-worker-child.js process script only once.
 let _serviceWorkerProcessScriptLoaded = false;
 
 let ServiceWorkerRegistrationActor =
 protocol.ActorClassWithSpec(serviceWorkerRegistrationSpec, {
-  initialize: function (conn, registration) {
+  /**
+   * Create the ServiceWorkerRegistrationActor
+   * @param DebuggerServerConnection conn
+   *   The server connection.
+   * @param ServiceWorkerRegistrationInfo registration
+   *   The registration's information.
+   */
+  initialize(conn, registration) {
     protocol.Actor.prototype.initialize.call(this, conn);
+    this._conn = conn;
     this._registration = registration;
-    this.manage(this);
+    this._pushSubscriptionActor = null;
+    Services.obs.addObserver(this, PushService.subscriptionModifiedTopic, false);
   },
 
-  form: function (detail) {
+  form(detail) {
     if (detail === "actorid") {
       return this.actorID;
     }
+    let registration = this._registration;
     return {
       actor: this.actorID,
-      scope: this._registration.scope,
-      url: this._registration.scriptSpec
+      scope: registration.scope,
+      url: registration.scriptSpec
     };
   },
 
-  start: function () {
+  destroy() {
+    protocol.Actor.prototype.destroy.call(this);
+    Services.obs.removeObserver(this, PushService.subscriptionModifiedTopic, false);
+    this._registration = null;
+    if (this._pushSubscriptionActor) {
+      this._pushSubscriptionActor.destroy();
+    }
+    this._pushSubscriptionActor = null;
+  },
+
+  disconnect() {
+    this.destroy();
+  },
+
+  /**
+   * Standard observer interface to listen to push messages and changes.
+   */
+  observe(subject, topic, data) {
+    let scope = this._registration.scope;
+    if (data !== scope) {
+      // This event doesn't concern us, pretend nothing happened.
+      return;
+    }
+    switch (topic) {
+      case PushService.subscriptionModifiedTopic:
+        if (this._pushSubscriptionActor) {
+          this._pushSubscriptionActor.destroy();
+          this._pushSubscriptionActor = null;
+        }
+        events.emit(this, "push-subscription-modified");
+        break;
+    }
+  },
+
+  start() {
     if (!_serviceWorkerProcessScriptLoaded) {
       Services.ppmm.loadProcessScript(
         "resource://devtools/server/service-worker-child.js", true);
       _serviceWorkerProcessScriptLoaded = true;
     }
     Services.ppmm.broadcastAsyncMessage("serviceWorkerRegistration:start", {
       scope: this._registration.scope
     });
     return { type: "started" };
   },
 
-  unregister: function () {
+  unregister() {
     let { principal, scope } = this._registration;
     let unregisterCallback = {
       unregisterSucceeded: function () {},
       unregisterFailed: function () {
         console.error("Failed to unregister the service worker for " + scope);
       },
       QueryInterface: XPCOMUtils.generateQI(
         [Ci.nsIServiceWorkerUnregisterCallback])
     };
     swm.propagateUnregister(principal, unregisterCallback, scope);
 
     return { type: "unregistered" };
   },
+
+  getPushSubscription() {
+    let registration = this._registration;
+    let pushSubscriptionActor = this._pushSubscriptionActor;
+    if (pushSubscriptionActor) {
+      return Promise.resolve(pushSubscriptionActor);
+    }
+    return new Promise((resolve, reject) => {
+      PushService.getSubscription(
+        registration.scope,
+        registration.principal,
+        (result, subscription) => {
+          if (!subscription) {
+            resolve(null);
+            return;
+          }
+          pushSubscriptionActor = new PushSubscriptionActor(this._conn, subscription);
+          this._pushSubscriptionActor = pushSubscriptionActor;
+          resolve(pushSubscriptionActor);
+        }
+      );
+    });
+  },
 });
 
 function ServiceWorkerRegistrationActorList(conn) {
   this._conn = conn;
   this._actors = new Map();
   this._onListChanged = null;
   this._mustNotify = false;
   this.onRegister = this.onRegister.bind(this);
   this.onUnregister = this.onUnregister.bind(this);
 }
 
 ServiceWorkerRegistrationActorList.prototype = {
-  getList: function () {
+  getList() {
     // Create a set of registrations.
     let registrations = new Set();
     let array = swm.getAllRegistrations();
     for (let index = 0; index < array.length; ++index) {
       registrations.add(
         array.queryElementAt(index, Ci.nsIServiceWorkerRegistrationInfo));
     }
 
@@ -409,27 +520,27 @@ ServiceWorkerRegistrationActorList.proto
       }
       if (this._onListChanged !== null && onListChanged === null) {
         swm.removeListener(this);
       }
     }
     this._onListChanged = onListChanged;
   },
 
-  _notifyListChanged: function () {
+  _notifyListChanged() {
     this._onListChanged();
 
     if (this._onListChanged !== null) {
       swm.removeListener(this);
     }
     this._mustNotify = false;
   },
 
-  onRegister: function (registration) {
+  onRegister(registration) {
     this._notifyListChanged();
   },
 
-  onUnregister: function (registration) {
+  onUnregister(registration) {
     this._notifyListChanged();
   }
 };
 
 exports.ServiceWorkerRegistrationActorList = ServiceWorkerRegistrationActorList;
--- a/devtools/shared/specs/worker.js
+++ b/devtools/shared/specs/worker.js
@@ -27,24 +27,42 @@ const workerSpec = generateActorSpec({
       request: {},
       response: RetVal("json")
     },
   },
 });
 
 exports.workerSpec = workerSpec;
 
+const pushSubscriptionSpec = generateActorSpec({
+  typeName: "pushSubscription",
+});
+
+exports.pushSubscriptionSpec = pushSubscriptionSpec;
+
 const serviceWorkerRegistrationSpec = generateActorSpec({
   typeName: "serviceWorkerRegistration",
 
+  events: {
+    "push-subscription-modified": {
+      type: "push-subscription-modified"
+    }
+  },
+
   methods: {
     start: {
       request: {},
       response: RetVal("json")
     },
     unregister: {
       request: {},
       response: RetVal("json")
     },
+    getPushSubscription: {
+      request: {},
+      response: {
+        subscription: RetVal("nullable:pushSubscription")
+      }
+    },
   },
 });
 
 exports.serviceWorkerRegistrationSpec = serviceWorkerRegistrationSpec;