Bug 1510654 - Create fronts for ServiceWorkerRegistration, PushSubscription and ServiceWorker;r=ochameau,ladybenko
authorJulian Descottes <jdescottes@mozilla.com>
Thu, 10 Jan 2019 17:26:35 +0000
changeset 510409 a2289d2698b76d737be4c5b9c4c8fb89cbbd86d6
parent 510408 2ecae1ee43a045b49c908001604f4834ac5c0c11
child 510410 247e6818b26264099735ce8a0c972940d15940c9
push id10547
push userffxbld-merge
push dateMon, 21 Jan 2019 13:03:58 +0000
treeherdermozilla-beta@24ec1916bffe [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersochameau, ladybenko
bugs1510654
milestone66.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 1510654 - Create fronts for ServiceWorkerRegistration, PushSubscription and ServiceWorker;r=ochameau,ladybenko Depends on D15375 Differential Revision: https://phabricator.services.mozilla.com/D15376
devtools/client/aboutdebugging-new/src/actions/debug-targets.js
devtools/client/aboutdebugging-new/src/components/debugtarget/ServiceWorkerAction.js
devtools/client/aboutdebugging-new/src/middleware/debug-target-listener.js
devtools/client/aboutdebugging-new/src/middleware/worker-component-data.js
devtools/client/aboutdebugging-new/src/modules/client-wrapper.js
devtools/client/aboutdebugging-new/src/types/debug-target.js
devtools/client/aboutdebugging-new/test/browser/mocks/helper-client-wrapper-mock.js
devtools/client/aboutdebugging/components/workers/Panel.js
devtools/client/aboutdebugging/components/workers/ServiceWorkerTarget.js
devtools/client/application/initializer.js
devtools/client/application/src/components/Worker.js
devtools/client/application/test/head.js
devtools/shared/fronts/moz.build
devtools/shared/fronts/root.js
devtools/shared/fronts/worker/moz.build
devtools/shared/fronts/worker/service-worker.js
devtools/shared/specs/index.js
devtools/shared/specs/root.js
devtools/shared/specs/worker/service-worker.js
--- a/devtools/client/aboutdebugging-new/src/actions/debug-targets.js
+++ b/devtools/client/aboutdebugging-new/src/actions/debug-targets.js
@@ -198,47 +198,41 @@ function requestWorkers() {
     try {
       const {
         otherWorkers,
         serviceWorkers,
         sharedWorkers,
       } = await clientWrapper.listWorkers();
 
       for (const serviceWorker of serviceWorkers) {
-        const { registrationActor } = serviceWorker;
-        if (!registrationActor) {
+        const { registrationFront } = serviceWorker;
+        if (!registrationFront) {
           continue;
         }
 
-        const { subscription } = await clientWrapper.request({
-          to: registrationActor,
-          type: "getPushSubscription",
-        });
-
+        const subscription = await registrationFront.getPushSubscription();
         serviceWorker.subscription = subscription;
       }
 
       dispatch({
         type: REQUEST_WORKERS_SUCCESS,
         otherWorkers,
         serviceWorkers,
         sharedWorkers,
       });
     } catch (e) {
       dispatch({ type: REQUEST_WORKERS_FAILURE, error: e });
     }
   };
 }
 
-function startServiceWorker(actor) {
+function startServiceWorker(registrationFront) {
   return async (_, getState) => {
-    const clientWrapper = getCurrentClient(getState().runtimes);
-
     try {
-      await clientWrapper.request({ to: actor, type: "start" });
+      await registrationFront.start();
     } catch (e) {
       console.error(e);
     }
   };
 }
 
 module.exports = {
   inspectDebugTarget,
--- a/devtools/client/aboutdebugging-new/src/components/debugtarget/ServiceWorkerAction.js
+++ b/devtools/client/aboutdebugging-new/src/components/debugtarget/ServiceWorkerAction.js
@@ -35,17 +35,17 @@ class ServiceWorkerAction extends PureCo
 
   push() {
     const { dispatch, target } = this.props;
     dispatch(Actions.pushServiceWorker(target.id));
   }
 
   start() {
     const { dispatch, target } = this.props;
-    dispatch(Actions.startServiceWorker(target.details.registrationActor));
+    dispatch(Actions.startServiceWorker(target.details.registrationFront));
   }
 
   _renderAction() {
     const { dispatch, runtimeDetails, target } = this.props;
     const { isActive, isRunning } = target.details;
     const { isMultiE10s } = runtimeDetails;
 
     if (!isRunning) {
--- a/devtools/client/aboutdebugging-new/src/middleware/debug-target-listener.js
+++ b/devtools/client/aboutdebugging-new/src/middleware/debug-target-listener.js
@@ -40,21 +40,26 @@ function debugTargetListenerMiddleware(s
         }
 
         if (isSupportedDebugTarget(runtime.type, DEBUG_TARGETS.WORKER)) {
           clientWrapper.addListener("workerListChanged", onWorkersUpdated);
           clientWrapper.onFront("contentProcessTarget", front => {
             clientWrapper.contentProcessFronts.push(front);
             front.on("workerListChanged", onWorkersUpdated);
           });
+
+          clientWrapper.onFront("serviceWorkerRegistration", front => {
+            clientWrapper.serviceWorkerRegistrationFronts.push(front);
+            front.on("push-subscription-modified", onWorkersUpdated);
+            front.on("registration-changed", onWorkersUpdated);
+          });
+
           clientWrapper.addListener("serviceWorkerRegistrationListChanged",
             onWorkersUpdated);
           clientWrapper.addListener("processListChanged", onWorkersUpdated);
-          clientWrapper.addListener("registration-changed", onWorkersUpdated);
-          clientWrapper.addListener("push-subscription-modified", onWorkersUpdated);
         }
         break;
       }
       case UNWATCH_RUNTIME_START: {
         const { runtime } = action;
         const { clientWrapper } = runtime.runtimeDetails;
 
         if (isSupportedDebugTarget(runtime.type, DEBUG_TARGETS.TAB)) {
@@ -70,19 +75,23 @@ function debugTargetListenerMiddleware(s
           clientWrapper.removeListener("serviceWorkerRegistrationListChanged",
             onWorkersUpdated);
 
           for (const front of clientWrapper.contentProcessFronts) {
             front.off("workerListChanged", onWorkersUpdated);
           }
           clientWrapper.contentProcessFronts = [];
 
+          for (const front of clientWrapper.serviceWorkerRegistrationFronts) {
+            front.off("push-subscription-modified", onWorkersUpdated);
+            front.off("registration-changed", onWorkersUpdated);
+          }
+          clientWrapper.serviceWorkerRegistrationFronts = [];
+
           clientWrapper.removeListener("processListChanged", onWorkersUpdated);
-          clientWrapper.removeListener("registration-changed", onWorkersUpdated);
-          clientWrapper.removeListener("push-subscription-modified", onWorkersUpdated);
         }
         break;
       }
     }
 
     return next(action);
   };
 }
--- a/devtools/client/aboutdebugging-new/src/middleware/worker-component-data.js
+++ b/devtools/client/aboutdebugging-new/src/middleware/worker-component-data.js
@@ -43,26 +43,26 @@ function getServiceWorkerStatus(isActive
 function toComponentData(workers, isServiceWorker) {
   return workers.map(worker => {
     // Here `worker` is the worker object created by RootFront.listAllWorkers
     const type = DEBUG_TARGETS.WORKER;
     const icon = "chrome://devtools/skin/images/debugging-workers.svg";
     let { fetch } = worker;
     const {
       name,
-      registrationActor,
+      registrationFront,
       scope,
       subscription,
       workerTargetFront,
     } = worker;
 
     // For registering service workers, workerTargetFront will not be available.
     // The only valid identifier we can use at that point is the actorID for the
     // service worker registration.
-    const id = workerTargetFront ? workerTargetFront.actorID : registrationActor;
+    const id = workerTargetFront ? workerTargetFront.actorID : registrationFront.actorID;
 
     let isActive = false;
     let isRunning = false;
     let pushServiceEndpoint = null;
     let status = null;
 
     if (isServiceWorker) {
       fetch = fetch ? SERVICE_WORKER_FETCH_STATES.LISTENING
@@ -74,17 +74,17 @@ function toComponentData(workers, isServ
     }
 
     return {
       details: {
         fetch,
         isActive,
         isRunning,
         pushServiceEndpoint,
-        registrationActor,
+        registrationFront,
         scope,
         status,
       },
       icon,
       id,
       name,
       type,
     };
--- a/devtools/client/aboutdebugging-new/src/modules/client-wrapper.js
+++ b/devtools/client/aboutdebugging-new/src/modules/client-wrapper.js
@@ -28,16 +28,17 @@ const MAIN_ROOT_EVENTS = [
  * The ClientWrapper class is used to isolate aboutdebugging from the DevTools client API
  * The modules of about:debugging should never call DevTools client APIs directly.
  */
 class ClientWrapper {
   constructor(client) {
     this.client = client;
     // Array of contentProcessTarget fronts on which we will listen for worker events.
     this.contentProcessFronts = [];
+    this.serviceWorkerRegistrationFronts = [];
   }
 
   addOneTimeListener(evt, listener) {
     if (MAIN_ROOT_EVENTS.includes(evt)) {
       this.client.mainRoot.once(evt, listener);
     } else {
       this.client.addOneTimeListener(evt, listener);
     }
--- a/devtools/client/aboutdebugging-new/src/types/debug-target.js
+++ b/devtools/client/aboutdebugging-new/src/types/debug-target.js
@@ -24,18 +24,18 @@ const tabTargetDetails = {
 
 const workerTargetDetails = {
   // (service worker specific) one of "LISTENING", "NOT_LISTENING". undefined otherwise.
   fetch: PropTypes.string,
   // (service worker specific) true if they reached the activated state.
   isActive: PropTypes.bool,
   // (service worker specific) true if they are currently running.
   isRunning: PropTypes.bool,
-  // actor id for the ServiceWorkerRegistration related to this service worker.
-  registrationActor: PropTypes.string,
+  // front for the ServiceWorkerRegistration related to this service worker.
+  registrationFront: PropTypes.object,
   // (service worker specific) scope of the service worker registration.
   scope: PropTypes.string,
   // (service worker specific) one of "RUNNING", "REGISTERING", "STOPPED".
   status: PropTypes.string,
 };
 
 const debugTarget = {
   // details property will contain a type-specific object.
--- a/devtools/client/aboutdebugging-new/test/browser/mocks/helper-client-wrapper-mock.js
+++ b/devtools/client/aboutdebugging-new/test/browser/mocks/helper-client-wrapper-mock.js
@@ -22,16 +22,17 @@ function createClientMock() {
   EventEmitter.decorate(eventEmitter);
 
   return {
     // add a reference to the internal event emitter so that consumers can fire client
     // events.
     _eventEmitter: eventEmitter,
     _preferences: {},
     contentProcessFronts: [],
+    serviceWorkerRegistrationFronts: [],
     addOneTimeListener: (evt, listener) => {
       eventEmitter.once(evt, listener);
     },
     addListener: (evt, listener) => {
       eventEmitter.on(evt, listener);
     },
     removeListener: (evt, listener) => {
       eventEmitter.off(evt, listener);
--- a/devtools/client/aboutdebugging/components/workers/Panel.js
+++ b/devtools/client/aboutdebugging/components/workers/Panel.js
@@ -58,53 +58,61 @@ class WorkersPanel extends Component {
   componentDidMount() {
     const client = this.props.client;
     // When calling RootFront.listAllWorkers, ContentProcessTargetActor are created
     // for each content process, which sends `workerListChanged` events.
     client.mainRoot.onFront("contentProcessTarget", front => {
       front.on("workerListChanged", this.updateWorkers);
       this.state.contentProcessFronts.push(front);
     });
+    client.mainRoot.onFront("serviceWorkerRegistration", front => {
+      this.state.serviceWorkerRegistrationFronts.push(front);
+      front.on("push-subscription-modified", this.updateWorkers);
+      front.on("registration-changed", this.updateWorkers);
+    });
     client.mainRoot.on("workerListChanged", this.updateWorkers);
 
     client.mainRoot.on("serviceWorkerRegistrationListChanged", this.updateWorkers);
     client.mainRoot.on("processListChanged", this.updateWorkers);
-    client.addListener("registration-changed", this.updateWorkers);
 
     addMultiE10sListener(this.updateMultiE10S);
 
     this.updateMultiE10S();
     this.updateWorkers();
   }
 
   componentWillUnmount() {
     const client = this.props.client;
     client.mainRoot.off("processListChanged", this.updateWorkers);
     client.mainRoot.off("serviceWorkerRegistrationListChanged", this.updateWorkers);
     client.mainRoot.off("workerListChanged", this.updateWorkers);
     for (const front of this.state.contentProcessFronts) {
       front.off("workerListChanged", this.updateWorkers);
     }
-    client.removeListener("registration-changed", this.updateWorkers);
+    for (const front of this.state.serviceWorkerRegistrationFronts) {
+      front.off("push-subscription-modified", this.updateWorkers);
+      front.off("registration-changed", this.updateWorkers);
+    }
 
     removeMultiE10sListener(this.updateMultiE10S);
   }
 
   get initialState() {
     return {
       workers: {
         service: [],
         shared: [],
         other: [],
       },
       isMultiE10S: isMultiE10s(),
 
       // List of ContentProcessTargetFront registered from componentWillMount
       // from which we listen for worker list changes
       contentProcessFronts: [],
+      serviceWorkerRegistrationFronts: [],
     };
   }
 
   updateMultiE10S() {
     this.setState({ isMultiE10S: isMultiE10s() });
   }
 
   updateWorkers() {
--- a/devtools/client/aboutdebugging/components/workers/ServiceWorkerTarget.js
+++ b/devtools/client/aboutdebugging/components/workers/ServiceWorkerTarget.js
@@ -26,62 +26,50 @@ class ServiceWorkerTarget extends Compon
       debugDisabled: PropTypes.bool,
       target: PropTypes.shape({
         active: PropTypes.bool,
         fetch: PropTypes.bool.isRequired,
         icon: PropTypes.string,
         name: PropTypes.string.isRequired,
         url: PropTypes.string,
         scope: PropTypes.string.isRequired,
-        // registrationActor can be missing in e10s.
-        registrationActor: PropTypes.string,
+        // registrationFront can be missing in e10s.
+        registrationFront: PropTypes.object,
         workerTargetFront: PropTypes.object,
       }).isRequired,
     };
   }
 
   constructor(props) {
     super(props);
 
     this.state = {
       pushSubscription: null,
     };
 
     this.debug = this.debug.bind(this);
     this.push = this.push.bind(this);
     this.start = this.start.bind(this);
     this.unregister = this.unregister.bind(this);
-    this.onPushSubscriptionModified = this.onPushSubscriptionModified.bind(this);
     this.updatePushSubscription = this.updatePushSubscription.bind(this);
     this.isRunning = this.isRunning.bind(this);
     this.isActive = this.isActive.bind(this);
     this.getServiceWorkerStatus = this.getServiceWorkerStatus.bind(this);
     this.renderButtons = this.renderButtons.bind(this);
     this.renderUnregisterLink = this.renderUnregisterLink.bind(this);
   }
 
   componentDidMount() {
-    const { client } = this.props;
-    client.addListener("push-subscription-modified", this.onPushSubscriptionModified);
     this.updatePushSubscription();
   }
 
   componentDidUpdate(oldProps, oldState) {
-    const wasActive = oldProps.target.active;
-    if (!wasActive && this.isActive()) {
-      // While the service worker isn't active, any calls to `updatePushSubscription`
-      // won't succeed. If we just became active, make sure we didn't miss a push
-      // subscription change by updating it now.
-      this.updatePushSubscription();
-    }
-  }
-
-  componentWillUnmount() {
-    const { client } = this.props;
-    client.removeListener("push-subscription-modified", this.onPushSubscriptionModified);
+    // The parent component listens to "push-subscription-modified" events,
+    // so we should update the push subscription after each update.
+    this.updatePushSubscription();
   }
 
   debug() {
     if (!this.isRunning()) {
       // If the worker is not running, we can't debug it.
       return;
     }
 
@@ -102,51 +90,38 @@ class ServiceWorkerTarget extends Compon
   }
 
   start() {
     if (!this.isActive() || this.isRunning()) {
       // If the worker is not active or if it is already running, we can't start it.
       return;
     }
 
-    const { client, target } = this.props;
-    client.request({
-      to: target.registrationActor,
-      type: "start",
-    });
+    const { registrationFront } = this.props.target;
+    registrationFront.start();
   }
 
   unregister() {
-    const { client, target } = this.props;
-    client.request({
-      to: target.registrationActor,
-      type: "unregister",
-    });
+    const { registrationFront } = this.props.target;
+    registrationFront.unregister();
   }
 
-  onPushSubscriptionModified(type, data) {
-    const { target } = this.props;
-    if (data.from === target.registrationActor) {
-      this.updatePushSubscription();
-    }
-  }
-
-  updatePushSubscription() {
-    if (!this.props.target.registrationActor) {
-      // A valid registrationActor is needed to retrieve the push subscription.
+  async updatePushSubscription() {
+    const { registrationFront } = this.props.target;
+    if (!registrationFront) {
+      // A valid registrationFront is needed to retrieve the push subscription.
       return;
     }
 
-    const { client, target } = this.props;
-    client.request({
-      to: target.registrationActor,
-      type: "getPushSubscription",
-    }, ({ subscription }) => {
+    try {
+      const subscription = await registrationFront.getPushSubscription();
       this.setState({ pushSubscription: subscription });
-    });
+    } catch (e) {
+      // The registration might be destroyed before the request reaches the server.
+    }
   }
 
   isRunning() {
     // We know the target is running if it has a worker actor.
     return !!this.props.target.workerTargetFront;
   }
 
   isActive() {
@@ -191,17 +166,17 @@ class ServiceWorkerTarget extends Compon
       // Only debug button is available if the service worker is not active.
       return debugButton;
     }
     return startButton;
   }
 
   renderUnregisterLink() {
     if (!this.isActive()) {
-      // If not active, there might be no registrationActor available.
+      // If not active, there might be no registrationFront available.
       return null;
     }
 
     return dom.a({
       onClick: this.unregister,
       className: "unregister-link",
     }, Strings.GetStringFromName("unregister"));
   }
--- a/devtools/client/application/initializer.js
+++ b/devtools/client/application/initializer.js
@@ -32,26 +32,31 @@ window.Application = {
     this.updateDomain = this.updateDomain.bind(this);
 
     this.mount = document.querySelector("#mount");
     this.toolbox = toolbox;
     this.client = toolbox.target.client;
 
     this.store = configureStore();
     this.actions = bindActionCreators(actions, this.store.dispatch);
+    this.serviceWorkerRegistrationFronts = [];
 
     const serviceContainer = {
       selectTool(toolId) {
         return toolbox.selectTool(toolId);
       },
     };
     this.toolbox.target.activeTab.on("workerListChanged", this.updateWorkers);
     this.client.mainRoot.on("serviceWorkerRegistrationListChanged", this.updateWorkers);
-    this.client.addListener("registration-changed", this.updateWorkers);
     this.client.mainRoot.on("processListChanged", this.updateWorkers);
+    this.client.mainRoot.onFront("serviceWorkerRegistration", front => {
+      this.serviceWorkerRegistrationFronts.push(front);
+      front.on("push-subscription-modified", this.updateWorkers);
+      front.on("registration-changed", this.updateWorkers);
+    });
     this.toolbox.target.on("navigate", this.updateDomain);
 
     this.updateDomain();
     await this.updateWorkers();
 
     const fluentBundles = await this.createFluentBundles();
 
     // Render the root Application component.
@@ -78,27 +83,34 @@ window.Application = {
     return contexts;
   },
 
   async updateWorkers() {
     const { service } = await this.client.mainRoot.listAllWorkers();
     this.actions.updateWorkers(service);
   },
 
+  removeRegistrationFrontListeners() {
+    for (const front of this.serviceWorkerRegistrationFronts) {
+      front.off("push-subscription-modified", this.updateWorkers);
+      front.off("registration-changed", this.updateWorkers);
+    }
+    this.serviceWorkerRegistrationFronts = [];
+  },
+
   updateDomain() {
     this.actions.updateDomain(this.toolbox.target.url);
   },
 
   destroy() {
     this.toolbox.target.activeTab.off("workerListChanged", this.updateWorkers);
     this.client.mainRoot.off("serviceWorkerRegistrationListChanged",
       this.updateWorkers);
-    this.client.removeListener("registration-changed", this.updateWorkers);
     this.client.mainRoot.off("processListChanged", this.updateWorkers);
-
+    this.removeRegistrationFrontListeners();
     this.toolbox.target.off("navigate", this.updateDomain);
 
     unmountComponentAtNode(this.mount);
     this.mount = null;
     this.toolbox = null;
     this.client = null;
   },
 };
--- a/devtools/client/application/src/components/Worker.js
+++ b/devtools/client/application/src/components/Worker.js
@@ -28,18 +28,18 @@ class Worker extends Component {
   static get propTypes() {
     return {
       client: PropTypes.instanceOf(DebuggerClient).isRequired,
       debugDisabled: PropTypes.bool,
       worker: PropTypes.shape({
         active: PropTypes.bool,
         name: PropTypes.string.isRequired,
         scope: PropTypes.string.isRequired,
-        // registrationActor can be missing in e10s.
-        registrationActor: PropTypes.string,
+        // registrationFront can be missing in e10s.
+        registrationFront: PropTypes.object,
         workerTargetFront: PropTypes.object,
       }).isRequired,
     };
   }
 
   constructor(props) {
     super(props);
 
@@ -59,29 +59,23 @@ class Worker extends Component {
   }
 
   start() {
     if (!this.isActive() || this.isRunning()) {
       console.log("Running or inactive service workers cannot be started");
       return;
     }
 
-    const { client, worker } = this.props;
-    client.request({
-      to: worker.registrationActor,
-      type: "start",
-    });
+    const { registrationFront } = this.props.worker;
+    registrationFront.start();
   }
 
   unregister() {
-    const { client, worker } = this.props;
-    client.request({
-      to: worker.registrationActor,
-      type: "unregister",
-    });
+    const { registrationFront } = this.props.worker;
+    registrationFront.unregister();
   }
 
   isRunning() {
     // We know the worker is running if it has a worker actor.
     return !!this.props.worker.workerTargetFront;
   }
 
   isActive() {
--- a/devtools/client/application/test/head.js
+++ b/devtools/client/application/test/head.js
@@ -50,25 +50,22 @@ async function openNewTabAndApplicationP
   const target = await TargetFactory.forTab(tab);
 
   const toolbox = await gDevTools.showToolbox(target, "application");
   const panel = toolbox.getCurrentPanel();
   return { panel, tab, target, toolbox };
 }
 
 async function unregisterAllWorkers(client) {
-  info("Wait until all workers have a valid registrationActor");
+  info("Wait until all workers have a valid registrationFront");
   let workers;
   await asyncWaitUntil(async function() {
     workers = await client.mainRoot.listAllWorkers();
     const allWorkersRegistered =
-      workers.service.every(worker => !!worker.registrationActor);
+      workers.service.every(worker => !!worker.registrationFront);
     return allWorkersRegistered;
   });
 
   info("Unregister all service workers");
   for (const worker of workers.service) {
-    await client.request({
-      to: worker.registrationActor,
-      type: "unregister",
-    });
+    await worker.registrationFront.unregister();
   }
 }
--- a/devtools/shared/fronts/moz.build
+++ b/devtools/shared/fronts/moz.build
@@ -3,16 +3,17 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 DIRS += [
     'addon',
     'inspector',
     'targets',
+    'worker',
 ]
 
 DevToolsModules(
     'accessibility.js',
     'actor-registry.js',
     'animation.js',
     'canvas.js',
     'changes.js',
--- a/devtools/shared/fronts/root.js
+++ b/devtools/shared/fronts/root.js
@@ -77,25 +77,29 @@ class RootFront extends FrontClassWithSp
     }
 
     const result = {
       service: [],
       shared: [],
       other: [],
     };
 
-    registrations.forEach(form => {
+    registrations.forEach(front => {
+      // All the information is simply mirrored from the registration front.
+      // However since registering workers will fetch similar information from the worker
+      // target front and will not have a service worker registration front, consumers
+      // should not read meta data directly on the registration front instance.
       result.service.push({
-        name: form.url,
-        url: form.url,
-        scope: form.scope,
-        fetch: form.fetch,
-        registrationActor: form.actor,
-        active: form.active,
-        lastUpdateTime: form.lastUpdateTime,
+        active: front.active,
+        fetch: front.fetch,
+        lastUpdateTime: front.lastUpdateTime,
+        name: front.url,
+        registrationFront: front,
+        scope: front.scope,
+        url: front.url,
       });
     });
 
     workers.forEach(front => {
       const worker = {
         name: front.url,
         url: front.url,
         workerTargetFront: front,
new file mode 100644
--- /dev/null
+++ b/devtools/shared/fronts/worker/moz.build
@@ -0,0 +1,9 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+DevToolsModules(
+    'service-worker.js',
+)
new file mode 100644
--- /dev/null
+++ b/devtools/shared/fronts/worker/service-worker.js
@@ -0,0 +1,84 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+const {
+  pushSubscriptionSpec,
+  serviceWorkerRegistrationSpec,
+  serviceWorkerSpec,
+} = require("devtools/shared/specs/worker/service-worker");
+const { FrontClassWithSpec, registerFront } = require("devtools/shared/protocol");
+
+class PushSubscriptionFront extends FrontClassWithSpec(pushSubscriptionSpec) {
+  get endpoint() {
+    return this._form.endpoint;
+  }
+
+  get pushCount() {
+    return this._form.pushCount;
+  }
+
+  get lastPush() {
+    return this._form.lastPush;
+  }
+
+  get quota() {
+    return this._form.quota;
+  }
+
+  form(form, detail) {
+    if (detail === "actorid") {
+      this.actorID = form;
+      return;
+    }
+
+    this.actorID = form.actor;
+    this._form = form;
+  }
+}
+exports.PushSubscriptionFront = PushSubscriptionFront;
+registerFront(PushSubscriptionFront);
+
+class ServiceWorkerRegistrationFront extends
+  FrontClassWithSpec(serviceWorkerRegistrationSpec) {
+  get active() {
+    return this._form.active;
+  }
+
+  get fetch() {
+    return this._form.fetch;
+  }
+
+  get lastUpdateTime() {
+    return this._form.lastUpdateTime;
+  }
+
+  get scope() {
+    return this._form.scope;
+  }
+
+  get type() {
+    return this._form.type;
+  }
+
+  get url() {
+    return this._form.url;
+  }
+
+  form(form, detail) {
+    if (detail === "actorid") {
+      this.actorID = form;
+      return;
+    }
+
+    this.actorID = form.actor;
+    this._form = form;
+  }
+}
+exports.ServiceWorkerRegistrationFront = ServiceWorkerRegistrationFront;
+registerFront(ServiceWorkerRegistrationFront);
+
+class ServiceWorkerFront extends FrontClassWithSpec(serviceWorkerSpec) {}
+exports.ServiceWorkerFront = ServiceWorkerFront;
+registerFront(ServiceWorkerFront);
--- a/devtools/shared/specs/index.js
+++ b/devtools/shared/specs/index.js
@@ -287,17 +287,17 @@ const Types = exports.__TypesForTests = 
   {
     types: ["gl-shader", "gl-program", "webgl"],
     spec: "devtools/shared/specs/webgl",
     front: "devtools/shared/fronts/webgl",
   },
   {
     types: ["pushSubscription", "serviceWorkerRegistration", "serviceWorker"],
     spec: "devtools/shared/specs/worker/service-worker",
-    front: null,
+    front: "devtools/shared/fronts/worker/service-worker",
   },
 ];
 
 const lazySpecs = new Map();
 const lazyFronts = new Map();
 
 // Convert the human readable `Types` list into efficient maps
 Types.forEach(item => {
--- a/devtools/shared/specs/root.js
+++ b/devtools/shared/specs/root.js
@@ -4,17 +4,17 @@
 "use strict";
 
 const { types, generateActorSpec, RetVal, Arg, Option } = require("devtools/shared/protocol");
 
 types.addDictType("root.listWorkers", {
   workers: "array:workerTarget",
 });
 types.addDictType("root.listServiceWorkerRegistrations", {
-  registrations: "array:json",
+  registrations: "array:serviceWorkerRegistration",
 });
 types.addDictType("root.listProcesses", {
   processes: "array:json",
 });
 types.addDictType("root.listTabs", {
   tabs: "array:browsingContextTarget",
   selected: "number",
 });
--- a/devtools/shared/specs/worker/service-worker.js
+++ b/devtools/shared/specs/worker/service-worker.js
@@ -21,21 +21,19 @@ const serviceWorkerRegistrationSpec = ge
     "registration-changed": {
       type: "registration-changed",
     },
   },
 
   methods: {
     start: {
       request: {},
-      response: RetVal("json"),
     },
     unregister: {
       request: {},
-      response: RetVal("json"),
     },
     getPushSubscription: {
       request: {},
       response: {
         subscription: RetVal("nullable:pushSubscription"),
       },
     },
   },