Backed out 8 changesets (bug 1485660) for damp failures on front.hasRequests. CLOSED TREE
authorBrindusan Cristian <cbrindusan@mozilla.com>
Thu, 11 Oct 2018 22:33:35 +0300
changeset 489100 7a97fe41a34fabed6beeb3db2fc0a2c61623eeae
parent 489099 04fbac02c101394f6b344a695e81bdfb026151a1
child 489101 bb7c6b3e49c79adb61ab85785e408b0e012b63bf
push id246
push userfmarier@mozilla.com
push dateSat, 13 Oct 2018 00:15:40 +0000
bugs1485660
milestone64.0a1
backs out7a2f29bbe2a8b484893efffb8f032b53f1794766
26e82fa6244486b3516c3279c13b6778839fcc3e
9bb2f56f219dea3965b61216f770af18c89765a0
40a3c2dd1b38a33b31a99a6810b6df1f15dfb85d
cfb160882db9439a359b47e3aa68383833d4877a
3642f2d6677725156a16768d7109e86a0583af09
9026679753b2a8085089447b9a60584071ad5836
dccca441b9803ac8672d0135ea208826333fcdfc
Backed out 8 changesets (bug 1485660) for damp failures on front.hasRequests. CLOSED TREE Backed out changeset 7a2f29bbe2a8 (bug 1485660) Backed out changeset 26e82fa62444 (bug 1485660) Backed out changeset 9bb2f56f219d (bug 1485660) Backed out changeset 40a3c2dd1b38 (bug 1485660) Backed out changeset cfb160882db9 (bug 1485660) Backed out changeset 3642f2d66777 (bug 1485660) Backed out changeset 9026679753b2 (bug 1485660) Backed out changeset dccca441b980 (bug 1485660)
devtools/client/aboutdebugging/test/browser_addons_debug_webextension_popup.js
devtools/client/accessibility/test/browser/head.js
devtools/client/application/initializer.js
devtools/client/application/test/head.js
devtools/client/canvasdebugger/test/head.js
devtools/client/debugger/debugger-controller.js
devtools/client/debugger/new/src/client/firefox/commands.js
devtools/client/debugger/new/src/client/firefox/events.js
devtools/client/debugger/new/test/mochitest/browser_dbg-inline-cache.js
devtools/client/debugger/test/mochitest/head.js
devtools/client/framework/target.js
devtools/client/framework/test/browser_toolbox_target.js
devtools/client/framework/test/browser_toolbox_tool_remote_reopen.js
devtools/client/framework/test/helper_disable_cache.js
devtools/client/framework/toolbox-options.js
devtools/client/framework/toolbox.js
devtools/client/inspector/test/browser_inspector_startup.js
devtools/client/inspector/test/head.js
devtools/client/netmonitor/src/connector/firefox-connector.js
devtools/client/netmonitor/test/head.js
devtools/client/shadereditor/test/head.js
devtools/client/shared/test/browser_dbg_listtabs-03.js
devtools/client/shared/test/browser_dbg_navigation.js
devtools/client/webaudioeditor/test/head.js
devtools/client/webide/modules/tab-store.js
devtools/docs/backend/client-api.md
devtools/server/actors/targets/browsing-context.js
devtools/server/tests/browser/browser_navigateEvents.js
devtools/server/tests/browser/browser_webextension_inspected_window.js
devtools/server/tests/mochitest/test_webextension-addon-debugging-connect.html
devtools/server/tests/mochitest/webextension-helpers.js
devtools/server/tests/unit/test_sourcemaps-01.js
devtools/shared/client/constants.js
devtools/shared/client/debugger-client.js
devtools/shared/client/moz.build
devtools/shared/client/tab-client.js
devtools/shared/client/thread-client.js
devtools/shared/fronts/moz.build
devtools/shared/fronts/targets/browsing-context.js
devtools/shared/fronts/targets/moz.build
devtools/shared/protocol.js
devtools/shared/specs/targets/browsing-context.js
--- a/devtools/client/aboutdebugging/test/browser_addons_debug_webextension_popup.js
+++ b/devtools/client/aboutdebugging/test/browser_addons_debug_webextension_popup.js
@@ -86,27 +86,17 @@ add_task(async function testWebExtension
   // Be careful, this JS function is going to be executed in the addon toolbox,
   // which lives in another process. So do not try to use any scope variable!
   const env = Cc["@mozilla.org/process/environment;1"].getService(Ci.nsIEnvironment);
 
   const testScript = function() {
     /* eslint-disable no-undef */
 
     let jsterm;
-    const popupFramePromise = new Promise(resolve => {
-      const listener = data => {
-        if (data.frames.some(({url}) => url && url.endsWith("popup.html"))) {
-          toolbox.target.off("frame-update", listener);
-          resolve();
-        }
-      };
-      toolbox.target.on("frame-update", listener);
-    });
-
-    const waitForFrameListUpdate = toolbox.target.once("frame-update");
+    let popupFramePromise;
 
     toolbox.selectTool("webconsole")
       .then(async (console) => {
         const clickNoAutoHideMenu = () => {
           return new Promise(resolve => {
             toolbox.doc.getElementById("toolbox-meatball-menu-button").click();
             toolbox.doc.addEventListener("popupshown", () => {
               const menuItem =
@@ -116,16 +106,28 @@ add_task(async function testWebExtension
             }, { once: true });
           });
         };
 
         dump(`Clicking the menu button\n`);
         await clickNoAutoHideMenu();
         dump(`Clicked the menu button\n`);
 
+        popupFramePromise = new Promise(resolve => {
+          const listener = data => {
+            if (data.frames.some(({url}) => url && url.endsWith("popup.html"))) {
+              toolbox.target.off("frame-update", listener);
+              resolve();
+            }
+          };
+          toolbox.target.on("frame-update", listener);
+        });
+
+        const waitForFrameListUpdate = toolbox.target.once("frame-update");
+
         jsterm = console.hud.jsterm;
         jsterm.execute("myWebExtensionShowPopup()");
 
         await Promise.all([
           // Wait the initial frame update (which list the background page).
           waitForFrameListUpdate,
           // Wait the new frame update (once the extension popup has been opened).
           popupFramePromise,
--- a/devtools/client/accessibility/test/browser/head.js
+++ b/devtools/client/accessibility/test/browser/head.js
@@ -403,11 +403,11 @@ function reload(target, waitForTargetEve
 
 /**
  * Navigate to a new URL within the panel target.
  * @param  {Object} target             Panel target.
  * @param  {Srting} url                URL to navigate to.
  * @param  {String} waitForTargetEvent Event to wait for after reload.
  */
 function navigate(target, url, waitForTargetEvent = "navigate") {
-  executeSoon(() => target.activeTab.navigateTo({ url }));
+  executeSoon(() => target.activeTab.navigateTo(url));
   return once(target, waitForTargetEvent);
 }
--- a/devtools/client/application/initializer.js
+++ b/devtools/client/application/initializer.js
@@ -38,17 +38,18 @@ window.Application = {
     this.store = configureStore();
     this.actions = bindActionCreators(actions, this.store.dispatch);
 
     const serviceContainer = {
       selectTool(toolId) {
         return toolbox.selectTool(toolId);
       }
     };
-    this.toolbox.target.activeTab.on("workerListChanged", this.updateWorkers);
+
+    this.client.addListener("workerListChanged", this.updateWorkers);
     this.client.addListener("serviceWorkerRegistrationListChanged", this.updateWorkers);
     this.client.addListener("registration-changed", this.updateWorkers);
     this.client.addListener("processListChanged", this.updateWorkers);
     this.toolbox.target.on("navigate", this.updateDomain);
 
     this.updateDomain();
     await this.updateWorkers();
 
@@ -83,17 +84,17 @@ window.Application = {
     this.actions.updateWorkers(service);
   },
 
   updateDomain() {
     this.actions.updateDomain(this.toolbox.target.url);
   },
 
   destroy() {
-    this.toolbox.target.activeTab.off("workerListChanged", this.updateWorkers);
+    this.client.removeListener("workerListChanged", this.updateWorkers);
     this.client.removeListener("serviceWorkerRegistrationListChanged",
       this.updateWorkers);
     this.client.removeListener("registration-changed", this.updateWorkers);
     this.client.removeListener("processListChanged", this.updateWorkers);
 
     this.toolbox.target.off("navigate", this.updateDomain);
 
     unmountComponentAtNode(this.mount);
--- a/devtools/client/application/test/head.js
+++ b/devtools/client/application/test/head.js
@@ -36,17 +36,17 @@ async function enableApplicationPanel() 
   await pushPref("devtools.application.enabled", true);
 }
 
 function getWorkerContainers(doc) {
   return doc.querySelectorAll(".js-sw-container");
 }
 
 function navigate(target, url, waitForTargetEvent = "navigate") {
-  executeSoon(() => target.activeTab.navigateTo({ url }));
+  executeSoon(() => target.activeTab.navigateTo(url));
   return once(target, waitForTargetEvent);
 }
 
 async function openNewTabAndApplicationPanel(url) {
   const tab = await addTab(url);
   const target = await TargetFactory.forTab(tab);
 
   const toolbox = await gDevTools.showToolbox(target, "application");
--- a/devtools/client/canvasdebugger/test/head.js
+++ b/devtools/client/canvasdebugger/test/head.js
@@ -102,17 +102,17 @@ function isTestingSupported() {
 }
 
 function navigateInHistory(aTarget, aDirection, aWaitForTargetEvent = "navigate") {
   executeSoon(() => content.history[aDirection]());
   return once(aTarget, aWaitForTargetEvent);
 }
 
 function navigate(aTarget, aUrl, aWaitForTargetEvent = "navigate") {
-  executeSoon(() => aTarget.activeTab.navigateTo({ url: aUrl }));
+  executeSoon(() => aTarget.activeTab.navigateTo(aUrl));
   return once(aTarget, aWaitForTargetEvent);
 }
 
 function reload(aTarget, aWaitForTargetEvent = "navigate") {
   executeSoon(() => aTarget.activeTab.reload());
   return once(aTarget, aWaitForTargetEvent);
 }
 
--- a/devtools/client/debugger/debugger-controller.js
+++ b/devtools/client/debugger/debugger-controller.js
@@ -473,32 +473,21 @@ Workers.prototype = {
   },
 
   connect: function () {
     if (!Prefs.workersEnabled) {
       return;
     }
 
     this._updateWorkerList();
-
-    // `_tabClient` can be BrowsingContextTargetFront (protocol.js front) or
-    // WorkerClient/DebuggerClient (old fashion client)
-    if (typeof(this._tabClient.on) == "function") {
-      this._tabClient.on("workerListChanged", this._onWorkerListChanged);
-    } else {
-      this._tabClient.addListener("workerListChanged", this._onWorkerListChanged);
-    }
+    this._tabClient.addListener("workerListChanged", this._onWorkerListChanged);
   },
 
   disconnect: function () {
-    if (typeof(this._tabClient.on) == "function") {
-      this._tabClient.off("workerListChanged", this._onWorkerListChanged);
-    } else {
-      this._tabClient.removeListener("workerListChanged", this._onWorkerListChanged);
-    }
+    this._tabClient.removeListener("workerListChanged", this._onWorkerListChanged);
   },
 
   _updateWorkerList: function () {
     if (!this._tabClient.listWorkers) {
       return;
     }
 
     this._tabClient.listWorkers().then((response) => {
--- a/devtools/client/debugger/new/src/client/firefox/commands.js
+++ b/devtools/client/debugger/new/src/client/firefox/commands.js
@@ -242,17 +242,17 @@ function debuggeeCommand(script) {
   request.emit("json-reply", {});
 
   debuggerClient._activeRequests.delete(consoleActor);
 
   return Promise.resolve();
 }
 
 function navigate(url) {
-  return tabTarget.activeTab.navigateTo({ url });
+  return tabTarget.activeTab.navigateTo(url);
 }
 
 function reload() {
   return tabTarget.activeTab.reload();
 }
 
 function getProperties(grip) {
   const objClient = threadClient.pauseGrip(grip);
@@ -450,9 +450,9 @@ const clientCommands = {
   disablePrettyPrint,
   fetchSources,
   fetchWorkers,
   sendPacket,
   setPausePoints,
   setSkipPausing
 };
 exports.setupCommands = setupCommands;
-exports.clientCommands = clientCommands;
+exports.clientCommands = clientCommands;
\ No newline at end of file
--- a/devtools/client/debugger/new/src/client/firefox/events.js
+++ b/devtools/client/debugger/new/src/client/firefox/events.js
@@ -36,23 +36,17 @@ function setupEvents(dependencies) {
   });
 
   if (threadClient) {
     Object.keys(clientEvents).forEach(eventName => {
       threadClient.addListener(eventName, clientEvents[eventName]);
     });
 
     if (threadClient._parent) {
-      // Parent may be BrowsingContextTargetFront and be protocol.js.
-      // Or DebuggerClient/WorkerClient and still be old fashion actor.
-      if (threadClient._parent.on) {
-        threadClient._parent.on("workerListChanged", workerListChanged);
-      } else {
-        threadClient._parent.addListener("workerListChanged", workerListChanged);
-      }
+      threadClient._parent.addListener("workerListChanged", workerListChanged);
     }
   }
 }
 
 async function paused(_, packet) {
   // If paused by an explicit interrupt, which are generated by the
   // slow script dialog and internal events such as setting
   // breakpoints, ignore the event.
@@ -114,9 +108,9 @@ function workerListChanged() {
 }
 
 const clientEvents = {
   paused,
   resumed,
   newSource
 };
 exports.setupEvents = setupEvents;
-exports.clientEvents = clientEvents;
+exports.clientEvents = clientEvents;
\ No newline at end of file
--- a/devtools/client/debugger/new/test/mochitest/browser_dbg-inline-cache.js
+++ b/devtools/client/debugger/new/test/mochitest/browser_dbg-inline-cache.js
@@ -71,17 +71,17 @@ add_task(async function() {
   let dbgValue = await findSource(dbg, "inline-cache.html");
   info(`Debugger text: ${dbgValue.text}`);
   ok(
     dbgValue.text.includes(pageValue),
     "Debugger loads from cache, gets value 1 like page"
   );
 
   info("Disable HTTP cache for page");
-  await toolbox.target.activeTab.reconfigure({ options: { cacheDisabled: true } });
+  await toolbox.target.activeTab.reconfigure({ cacheDisabled: true });
   makeChanges();
 
   info("Reload inside debugger with toolbox caching disabled (attempt 1)");
   await reloadTabAndDebugger(tab, dbg);
   pageValue = await getPageValue(tab);
   is(pageValue, "let x = 2;", "Content loads from network, has doc value 2");
   await waitForLoadedSource(dbg, "inline-cache.html");
   dbgValue = await findSource(dbg, "inline-cache.html");
@@ -101,17 +101,17 @@ add_task(async function() {
   dbgValue = await findSource(dbg, "inline-cache.html");
   info(`Debugger text: ${dbgValue.text}`);
   ok(
     dbgValue.text.includes(pageValue),
     "Debugger loads from network, gets value 3 like page"
   );
 
   info("Enable HTTP cache for page");
-  await toolbox.target.activeTab.reconfigure({ options: { cacheDisabled: false } });
+  await toolbox.target.activeTab.reconfigure({ cacheDisabled: false });
   makeChanges();
 
   // Even though the HTTP cache is now enabled, Gecko sets the VALIDATE_ALWAYS flag when
   // reloading the page.  So, it will always make a request to the server for the main
   // document contents.
 
   info("Reload inside debugger with toolbox caching enabled (attempt 1)");
   await reloadTabAndDebugger(tab, dbg);
--- a/devtools/client/debugger/test/mochitest/head.js
+++ b/devtools/client/debugger/test/mochitest/head.js
@@ -175,18 +175,18 @@ function getAddonActorForId(aClient, aAd
     deferred.resolve(addonTargetActor);
   });
 
   return deferred.promise;
 }
 
 async function attachTargetActorForUrl(aClient, aUrl) {
   let grip = await getTargetActorForUrl(aClient, aUrl);
-  let [ response, front ] = await aClient.attachTarget(grip.actor);
-  return [grip, response, front];
+  let [ response ] = await aClient.attachTarget(grip.actor);
+  return [grip, response];
 }
 
 async function attachThreadActorForUrl(aClient, aUrl) {
   let [grip, response] = await attachTargetActorForUrl(aClient, aUrl);
   let [response2, threadClient] = await aClient.attachThread(response.threadActor);
   await threadClient.resume();
   return threadClient;
 }
@@ -445,17 +445,17 @@ function ensureThreadClientState(aPanel,
     return promise.resolve(null);
   } else {
     return waitForThreadEvents(aPanel, aState);
   }
 }
 
 function reload(aPanel, aUrl) {
   let activeTab = aPanel.panelWin.DebuggerController._target.activeTab;
-  aUrl ? activeTab.navigateTo({ url: aUrl }) : activeTab.reload();
+  aUrl ? activeTab.navigateTo(aUrl) : activeTab.reload();
 }
 
 function navigateActiveTabTo(aPanel, aUrl, aWaitForEventName, aEventRepeat) {
   let finished = waitForDebuggerEvents(aPanel, aWaitForEventName, aEventRepeat);
   reload(aPanel, aUrl);
   return finished;
 }
 
@@ -1104,19 +1104,24 @@ function findWorker(workers, url) {
   return null;
 }
 
 function attachWorker(tabClient, worker) {
   info("Attaching to worker with url '" + worker.url + "'.");
   return tabClient.attachWorker(worker.actor);
 }
 
-function waitForWorkerListChanged(targetFront) {
+function waitForWorkerListChanged(tabClient) {
   info("Waiting for worker list to change.");
-  return targetFront.once("workerListChanged");
+  return new Promise(function (resolve) {
+    tabClient.addListener("workerListChanged", function listener() {
+      tabClient.removeListener("workerListChanged", listener);
+      resolve();
+    });
+  });
 }
 
 function attachThread(workerClient, options) {
   info("Attaching to thread.");
   return workerClient.attachThread(options);
 }
 
 function waitForWorkerClose(workerClient) {
--- a/devtools/client/framework/target.js
+++ b/devtools/client/framework/target.js
@@ -189,17 +189,16 @@ const TargetFactory = exports.TargetFact
  *                  frame scripts...
  * @param {xul:tab} tab (optional)
  *                  If the target is a local Firefox tab, a reference to the firefox
  *                  frontend tab object.
  */
 function TabTarget({ form, client, chrome, tab = null }) {
   EventEmitter.decorate(this);
   this.destroy = this.destroy.bind(this);
-  this._onTabNavigated = this._onTabNavigated.bind(this);
   this.activeTab = this.activeConsole = null;
 
   this._form = form;
   this._url = form.url;
   this._title = form.title;
 
   this._client = client;
   this._chrome = chrome;
@@ -514,22 +513,16 @@ TabTarget.prototype = {
       return this._attach;
     }
 
     // Attach the target actor
     const attachTarget = async () => {
       const [response, tabClient] = await this._client.attachTarget(this._form.actor);
       this.activeTab = tabClient;
       this.threadActor = response.threadActor;
-
-      this.activeTab.on("tabNavigated", this._onTabNavigated);
-      this._onFrameUpdate = packet => {
-        this.emit("frame-update", packet);
-      };
-      this.activeTab.on("frameUpdate", this._onFrameUpdate);
     };
 
     // Attach the console actor
     const attachConsole = async () => {
       const [, consoleClient] = await this._client.attachConsole(
         this._form.consoleActor, []);
       this.activeConsole = consoleClient;
 
@@ -551,27 +544,25 @@ TabTarget.prototype = {
           to: this._form.actor, type: "connect",
         });
 
         this._form = form;
         this._url = form.url;
         this._title = form.title;
       }
 
+      this._setupRemoteListeners();
+
       // AddonActor and chrome debugging on RootActor don't inherit from
       // BrowsingContextTargetActor (i.e. this.isBrowsingContext=false) and don't need
       // to be attached.
       if (this.isBrowsingContext) {
         await attachTarget();
       }
 
-      // _setupRemoteListeners has to be called after the potential call to `attachTarget`
-      // as it depends on `activeTab` which is set by this method.
-      this._setupRemoteListeners();
-
       // But all target actor have a console actor to attach
       return attachConsole();
     })();
 
     return this._attach;
   },
 
   /**
@@ -590,104 +581,79 @@ TabTarget.prototype = {
     if (this._tab.ownerDocument.defaultView) {
       this._tab.ownerDocument.defaultView.removeEventListener("unload", this);
     }
     this._tab.removeEventListener("TabClose", this);
     this._tab.removeEventListener("TabRemotenessChange", this);
   },
 
   /**
-   * Event listener for tabNavigated packet sent by activeTab's front.
-   */
-  _onTabNavigated: function(packet) {
-    const event = Object.create(null);
-    event.url = packet.url;
-    event.title = packet.title;
-    event.nativeConsoleAPI = packet.nativeConsoleAPI;
-    event.isFrameSwitching = packet.isFrameSwitching;
-
-    // Keep the title unmodified when a developer toolbox switches frame
-    // for a tab (Bug 1261687), but always update the title when the target
-    // is a WebExtension (where the addon name is always included in the title
-    // and the url is supposed to be updated every time the selected frame changes).
-    if (!packet.isFrameSwitching || this.isWebExtension) {
-      this._url = packet.url;
-      this._title = packet.title;
-    }
-
-    // Send any stored event payload (DOMWindow or nsIRequest) for backwards
-    // compatibility with non-remotable tools.
-    if (packet.state == "start") {
-      event._navPayload = this._navRequest;
-      this.emit("will-navigate", event);
-      this._navRequest = null;
-    } else {
-      event._navPayload = this._navWindow;
-      this.emit("navigate", event);
-      this._navWindow = null;
-    }
-  },
-
-  /**
    * Setup listeners for remote debugging, updating existing ones as necessary.
    */
   _setupRemoteListeners: function() {
     this.client.addListener("closed", this.destroy);
 
-    // For now, only browsing-context inherited actors are using a front,
-    // for which events have to be listened on the front itself.
-    // For other actors (ContentProcessTargetActor and AddonTargetActor), events should
-    // still be listened directly on the client. This should be ultimately cleaned up to
-    // only listen from a front by bug 1465635.
-    if (this.activeTab) {
-      this.activeTab.on("tabDetached", this.destroy);
+    this._onTabDetached = (type, packet) => {
+      // We have to filter message to ensure that this detach is for this tab
+      if (packet.from == this._form.actor) {
+        this.destroy();
+      }
+    };
+    this.client.addListener("tabDetached", this._onTabDetached);
+
+    this._onTabNavigated = (type, packet) => {
+      const event = Object.create(null);
+      event.url = packet.url;
+      event.title = packet.title;
+      event.nativeConsoleAPI = packet.nativeConsoleAPI;
+      event.isFrameSwitching = packet.isFrameSwitching;
 
-      // These events should be ultimately listened from the thread client as
-      // they are coming from it and no longer go through the Target Actor/Front.
-      this._onSourceUpdated = packet => this.emit("source-updated", packet);
-      this.activeTab.on("newSource", this._onSourceUpdated);
-      this.activeTab.on("updatedSource", this._onSourceUpdated);
-    } else {
-      this._onTabDetached = (type, packet) => {
-        // We have to filter message to ensure that this detach is for this tab
-        if (packet.from == this._form.actor) {
-          this.destroy();
-        }
-      };
-      this.client.addListener("tabDetached", this._onTabDetached);
+      // Keep the title unmodified when a developer toolbox switches frame
+      // for a tab (Bug 1261687), but always update the title when the target
+      // is a WebExtension (where the addon name is always included in the title
+      // and the url is supposed to be updated every time the selected frame changes).
+      if (!packet.isFrameSwitching || this.isWebExtension) {
+        this._url = packet.url;
+        this._title = packet.title;
+      }
 
-      this._onSourceUpdated = (type, packet) => this.emit("source-updated", packet);
-      this.client.addListener("newSource", this._onSourceUpdated);
-      this.client.addListener("updatedSource", this._onSourceUpdated);
-    }
+      // Send any stored event payload (DOMWindow or nsIRequest) for backwards
+      // compatibility with non-remotable tools.
+      if (packet.state == "start") {
+        event._navPayload = this._navRequest;
+        this.emit("will-navigate", event);
+        this._navRequest = null;
+      } else {
+        event._navPayload = this._navWindow;
+        this.emit("navigate", event);
+        this._navWindow = null;
+      }
+    };
+    this.client.addListener("tabNavigated", this._onTabNavigated);
+
+    this._onFrameUpdate = (type, packet) => {
+      this.emit("frame-update", packet);
+    };
+    this.client.addListener("frameUpdate", this._onFrameUpdate);
+
+    this._onSourceUpdated = (event, packet) => this.emit("source-updated", packet);
+    this.client.addListener("newSource", this._onSourceUpdated);
+    this.client.addListener("updatedSource", this._onSourceUpdated);
   },
 
   /**
    * Teardown listeners for remote debugging.
    */
   _teardownRemoteListeners: function() {
-    // Remove listeners set in _setupRemoteListeners
     this.client.removeListener("closed", this.destroy);
-    if (this.activeTab) {
-      this.activeTab.off("tabDetached", this.destroy);
-      this.activeTab.off("newSource", this._onSourceUpdated);
-      this.activeTab.off("updatedSource", this._onSourceUpdated);
-    } else {
-      this.client.removeListener("tabDetached", this._onTabDetached);
-      this.client.removeListener("newSource", this._onSourceUpdated);
-      this.client.removeListener("updatedSource", this._onSourceUpdated);
-    }
-
-    // Remove listeners set in attachTarget
-    if (this.activeTab) {
-      this.activeTab.off("tabNavigated", this._onTabNavigated);
-      this.activeTab.off("frameUpdate", this._onFrameUpdate);
-    }
-
-    // Remove listeners set in attachConsole
+    this.client.removeListener("tabNavigated", this._onTabNavigated);
+    this.client.removeListener("tabDetached", this._onTabDetached);
+    this.client.removeListener("frameUpdate", this._onFrameUpdate);
+    this.client.removeListener("newSource", this._onSourceUpdated);
+    this.client.removeListener("updatedSource", this._onSourceUpdated);
     if (this.activeConsole && this._onInspectObject) {
       this.activeConsole.off("inspectObject", this._onInspectObject);
     }
   },
 
   /**
    * Handle tabs events.
    */
@@ -770,21 +736,17 @@ TabTarget.prototype = {
           // We started with a local tab and created the client ourselves, so we
           // should close it.
           this._client.close().then(cleanupAndResolve);
         } else if (this.activeTab) {
           // The client was handed to us, so we are not responsible for closing
           // it. We just need to detach from the tab, if already attached.
           // |detach| may fail if the connection is already dead, so proceed with
           // cleanup directly after this.
-          try {
-            await this.activeTab.detach();
-          } catch (e) {
-            console.warn(`Error while detaching target: ${e.message}`);
-          }
+          this.activeTab.detach();
           cleanupAndResolve();
         } else {
           cleanupAndResolve();
         }
       }
     });
 
     return this._destroyer;
@@ -823,32 +785,32 @@ TabTarget.prototype = {
    * @param {String} text
    *                 The text to log.
    * @param {String} category
    *                 The category of the message.  @see nsIScriptError.
    */
   logErrorInPage: function(text, category) {
     if (this.activeTab && this.activeTab.traits.logInPage) {
       const errorFlag = 0;
-      this.activeTab.logInPage({ text, category, flags: errorFlag });
+      this.activeTab.logInPage(text, category, errorFlag);
     }
   },
 
   /**
    * Log a warning of some kind to the tab's console.
    *
    * @param {String} text
    *                 The text to log.
    * @param {String} category
    *                 The category of the message.  @see nsIScriptError.
    */
   logWarningInPage: function(text, category) {
     if (this.activeTab && this.activeTab.traits.logInPage) {
       const warningFlag = 1;
-      this.activeTab.logInPage({ text, category, flags: warningFlag });
+      this.activeTab.logInPage(text, category, warningFlag);
     }
   },
 };
 
 function WorkerTarget(workerClient) {
   EventEmitter.decorate(this);
   this._workerClient = workerClient;
 }
--- a/devtools/client/framework/test/browser_toolbox_target.js
+++ b/devtools/client/framework/test/browser_toolbox_target.js
@@ -32,17 +32,17 @@ add_task(async function() {
   await onLoad;
 
   // Also wait for toolbox-ready, as toolbox document load isn't enough, there
   // is plenty of asynchronous steps during toolbox load
   info("Waiting for toolbox-ready");
   const toolbox = await onToolboxReady;
 
   const onToolboxDestroyed = gDevTools.once("toolbox-destroyed");
-  const onTabDetached = toolbox.target.activeTab.once("tabDetached");
+  const onTabDetached = once(toolbox.target.client, "tabDetached");
 
   info("Removing the iframes");
   toolboxIframe.remove();
 
   // And wait for toolbox-destroyed as toolbox unload is also full of
   // asynchronous operation that outlast unload event
   info("Waiting for toolbox-destroyed");
   await onToolboxDestroyed;
--- a/devtools/client/framework/test/browser_toolbox_tool_remote_reopen.js
+++ b/devtools/client/framework/test/browser_toolbox_tool_remote_reopen.js
@@ -110,16 +110,20 @@ function test() {
       for (const actor of pool.__poolMap.keys()) {
         // Bug 1056342: Profiler fails today because of framerate actor, but
         // this appears more complex to rework, so leave it for that bug to
         // resolve.
         if (actor.includes("framerateActor")) {
           todo(false, "Front for " + actor + " still held in pool!");
           continue;
         }
+        // gcliActor is for the commandline which is separate to the toolbox
+        if (actor.includes("gcliActor")) {
+          continue;
+        }
         ok(false, "Front for " + actor + " still held in pool!");
       }
     }
 
     gBrowser.removeCurrentTab();
     DebuggerServer.destroy();
     toggleAllTools(false);
     finish();
--- a/devtools/client/framework/test/helper_disable_cache.js
+++ b/devtools/client/framework/test/helper_disable_cache.js
@@ -80,22 +80,21 @@ async function checkCacheEnabled(tabX, e
 async function setDisableCacheCheckboxChecked(tabX, state) {
   gBrowser.selectedTab = tabX.tab;
 
   const panel = tabX.toolbox.getCurrentPanel();
   const cbx = panel.panelDoc.getElementById("devtools-disable-cache");
 
   if (cbx.checked !== state) {
     info("Setting disable cache checkbox to " + state + " for " + tabX.title);
-    const onReconfigured = tabX.toolbox.once("cache-reconfigured");
     cbx.click();
 
-    // We have to wait for the reconfigure request to be finished before reloading
-    // the page.
-    await onReconfigured;
+    // We need to wait for all checkboxes to be updated and the docshells to
+    // apply the new cache settings.
+    await waitForTick();
   }
 }
 
 function reloadTab(tabX) {
   const browser = gBrowser.selectedBrowser;
 
   const reloadTabPromise = BrowserTestUtils.browserLoaded(browser).then(function() {
     info("Reloaded tab " + tabX.title);
--- a/devtools/client/framework/toolbox-options.js
+++ b/devtools/client/framework/toolbox-options.js
@@ -455,17 +455,17 @@ OptionsPanel.prototype = {
       prefSelect.addEventListener("change", function(e) {
         const select = e.target;
         SetPref(select.getAttribute("data-pref"),
           select.options[select.selectedIndex].value);
       });
     }
 
     if (this.target.activeTab && !this.target.chrome) {
-      const response = await this.target.activeTab.attach();
+      const [ response ] = await this.target.client.attachTarget(this.target.activeTab._actor);
       this._origJavascriptEnabled = !response.javascriptEnabled;
       this.disableJSNode.checked = this._origJavascriptEnabled;
       this.disableJSNode.addEventListener("click", this._disableJSClicked);
     } else {
       // Hide the checkbox and label
       this.disableJSNode.parentNode.style.display = "none";
 
       const triggersPageRefreshLabel =
@@ -507,17 +507,17 @@ OptionsPanel.prototype = {
    */
   _disableJSClicked: function(event) {
     const checked = event.target.checked;
 
     const options = {
       "javascriptEnabled": !checked
     };
 
-    this.target.activeTab.reconfigure({ options });
+    this.target.activeTab.reconfigure(options);
   },
 
   destroy: function() {
     if (this.destroyed) {
       return;
     }
     this.destroyed = true;
 
--- a/devtools/client/framework/toolbox.js
+++ b/devtools/client/framework/toolbox.js
@@ -1334,48 +1334,37 @@ Toolbox.prototype = {
 
     return this.pickerButton;
   },
 
   /**
    * Apply the current cache setting from devtools.cache.disabled to this
    * toolbox's tab.
    */
-  _applyCacheSettings: async function() {
+  _applyCacheSettings: function() {
     const pref = "devtools.cache.disabled";
     const cacheDisabled = Services.prefs.getBoolPref(pref);
 
     if (this.target.activeTab) {
-      await this.target.activeTab.reconfigure({
-        options: {
-          "cacheDisabled": cacheDisabled
-        }
-      });
-
-      // This event is only emitted for tests in order to know when to reload
-      if (flags.testing) {
-        this.emit("cache-reconfigured");
-      }
+      this.target.activeTab.reconfigure({"cacheDisabled": cacheDisabled});
     }
   },
 
   /**
    * Apply the current service workers testing setting from
    * devtools.serviceWorkers.testing.enabled to this toolbox's tab.
    */
   _applyServiceWorkersTestingSettings: function() {
     const pref = "devtools.serviceWorkers.testing.enabled";
     const serviceWorkersTestingEnabled =
       Services.prefs.getBoolPref(pref) || false;
 
     if (this.target.activeTab) {
       this.target.activeTab.reconfigure({
-        options: {
-          "serviceWorkersTestingEnabled": serviceWorkersTestingEnabled
-        }
+        "serviceWorkersTestingEnabled": serviceWorkersTestingEnabled
       });
     }
   },
 
   /**
    * Update the visibility of the buttons.
    */
   updateToolboxButtonsVisibility() {
@@ -1411,21 +1400,17 @@ Toolbox.prototype = {
    */
   togglePaintFlashing: function() {
     if (this.isPaintFlashing) {
       this.telemetry.toolOpened("paintflashing", this.sessionId, this);
     } else {
       this.telemetry.toolClosed("paintflashing", this.sessionId, this);
     }
     this.isPaintFlashing = !this.isPaintFlashing;
-    return this.target.activeTab.reconfigure({
-      options: {
-        "paintFlashing": this.isPaintFlashing
-      }
-    });
+    return this.target.activeTab.reconfigure({"paintFlashing": this.isPaintFlashing});
   },
 
   /**
    * Visually update picker button.
    * This function is called on every "select" event. Newly selected panel can
    * update the visual state of the picker button such as disabled state,
    * additional CSS classes (className), and tooltip (description).
    */
@@ -2338,17 +2323,17 @@ Toolbox.prototype = {
   },
 
   /**
    * Select a frame by sending 'switchToFrame' packet to the backend.
    */
   onSelectFrame: function(frameId) {
     // Send packet to the backend to select specified frame and
     // wait for 'frameUpdate' event packet to update the UI.
-    this.target.activeTab.switchToFrame({ windowId: frameId });
+    this.target.activeTab.switchToFrame(frameId);
   },
 
   /**
    * Highlight a frame in the page
    */
   onHighlightFrame: async function(frameId) {
     // Need to initInspector to check presence of getNodeActorFromWindowID
     // and use the highlighter later
--- a/devtools/client/inspector/test/browser_inspector_startup.js
+++ b/devtools/client/inspector/test/browser_inspector_startup.js
@@ -37,17 +37,17 @@ add_task(async function() {
   const domContentLoaded = waitForLinkedBrowserEvent(tab, "DOMContentLoaded");
   const pageLoaded = waitForLinkedBrowserEvent(tab, "load");
 
   const markupLoaded = inspector.once("markuploaded");
   const onRequest = onPageResourceRequest();
 
   info("Navigate to the slow loading page");
   const activeTab = inspector.toolbox.target.activeTab;
-  await activeTab.navigateTo({ url: TEST_URL });
+  await activeTab.navigateTo(TEST_URL);
 
   info("Wait for request made to the image");
   const response = await onRequest;
 
   // The request made to the image shouldn't block the DOMContentLoaded event
   info("Wait for DOMContentLoaded");
   await domContentLoaded;
 
--- a/devtools/client/inspector/test/head.js
+++ b/devtools/client/inspector/test/head.js
@@ -43,17 +43,17 @@ registerCleanupFunction(function() {
 
 var navigateTo = async function(inspector, url) {
   const markuploaded = inspector.once("markuploaded");
   const onNewRoot = inspector.once("new-root");
   const onUpdated = inspector.once("inspector-updated");
 
   info("Navigating to: " + url);
   const activeTab = inspector.toolbox.target.activeTab;
-  await activeTab.navigateTo({ url });
+  await activeTab.navigateTo(url);
 
   info("Waiting for markup view to load after navigation.");
   await markuploaded;
 
   info("Waiting for new root.");
   await onNewRoot;
 
   info("Waiting for inspector to update after new-root event.");
--- a/devtools/client/netmonitor/src/connector/firefox-connector.js
+++ b/devtools/client/netmonitor/src/connector/firefox-connector.js
@@ -311,17 +311,17 @@ class FirefoxConnector {
             resolve();
           });
         });
       });
     };
 
     // Reconfigures the tab, optionally triggering a reload.
     const reconfigureTab = options => {
-      return this.tabTarget.activeTab.reconfigure({ options });
+      return this.tabTarget.activeTab.reconfigure(options);
     };
 
     // Reconfigures the tab and waits for the target to finish navigating.
     const reconfigureTabAndWaitForNavigation = (options) => {
       options.performReload = true;
       const navigationFinished = waitForNavigation();
       return reconfigureTab(options).then(() => navigationFinished);
     };
--- a/devtools/client/netmonitor/test/head.js
+++ b/devtools/client/netmonitor/test/head.js
@@ -139,17 +139,17 @@ function waitForNavigation(target) {
 
 function toggleCache(target, disabled) {
   const options = { cacheDisabled: disabled, performReload: true };
   const navigationFinished = waitForNavigation(target);
 
   // Disable the cache for any toolbox that it is opened from this point on.
   Services.prefs.setBoolPref("devtools.cache.disabled", disabled);
 
-  return target.activeTab.reconfigure({ options }).then(() => navigationFinished);
+  return target.activeTab.reconfigure(options).then(() => navigationFinished);
 }
 
 /**
  * Wait for 2 markers during document load.
  */
 function waitForTimelineMarkers(monitor) {
   return new Promise(resolve => {
     const markers = [];
--- a/devtools/client/shadereditor/test/head.js
+++ b/devtools/client/shadereditor/test/head.js
@@ -126,17 +126,17 @@ function navigateInHistory(aTarget, aDir
     mm.sendAsyncMessage("devtools:test:history", { direction: aDirection });
   } else {
     executeSoon(() => content.history[aDirection]());
   }
   return once(aTarget, aWaitForTargetEvent);
 }
 
 function navigate(aTarget, aUrl, aWaitForTargetEvent = "navigate") {
-  executeSoon(() => aTarget.activeTab.navigateTo({ url: aUrl }));
+  executeSoon(() => aTarget.activeTab.navigateTo(aUrl));
   return once(aTarget, aWaitForTargetEvent);
 }
 
 function reload(aTarget, aWaitForTargetEvent = "navigate") {
   executeSoon(() => aTarget.activeTab.reload());
   return once(aTarget, aWaitForTargetEvent);
 }
 
--- a/devtools/client/shared/test/browser_dbg_listtabs-03.js
+++ b/devtools/client/shared/test/browser_dbg_listtabs-03.js
@@ -6,46 +6,56 @@
 "use strict";
 
 /**
  * Make sure the listTabs request works as specified.
  */
 
 var { DebuggerServer } = require("devtools/server/main");
 var { DebuggerClient } = require("devtools/shared/client/debugger-client");
+var { Task } = require("devtools/shared/task");
 
 const TAB1_URL = EXAMPLE_URL + "doc_empty-tab-01.html";
 
-add_task(async function test() {
+var gClient;
+
+function test() {
   DebuggerServer.init();
   DebuggerServer.registerAllActors();
 
   const transport = DebuggerServer.connectPipe();
-  const client = new DebuggerClient(transport);
-  const [type] = await client.connect();
-  is(type, "browser", "Root actor should identify itself as a browser.");
-  const tab = await addTab(TAB1_URL);
+  gClient = new DebuggerClient(transport);
+  gClient.connect().then(Task.async(function* ([aType, aTraits]) {
+    is(aType, "browser",
+      "Root actor should identify itself as a browser.");
+    const tab = yield addTab(TAB1_URL);
 
-  let { tabs } = await client.listTabs();
-  is(tabs.length, 2, "Should be two tabs");
-  const tabGrip = tabs.filter(a => a.url == TAB1_URL).pop();
-  ok(tabGrip, "Should have an actor for the tab");
+    let { tabs } = yield gClient.listTabs();
+    is(tabs.length, 2, "Should be two tabs");
+    const tabGrip = tabs.filter(a => a.url == TAB1_URL).pop();
+    ok(tabGrip, "Should have an actor for the tab");
 
-  let [response, targetFront] = await client.attachTarget(tabGrip.actor);
-  is(response.type, "tabAttached", "Should have attached");
+    let response = yield gClient.request({ to: tabGrip.actor, type: "attach" });
+    is(response.type, "tabAttached", "Should have attached");
+
+    response = yield gClient.listTabs();
+    tabs = response.tabs;
 
-  response = await client.listTabs();
-  tabs = response.tabs;
+    response = yield gClient.request({ to: tabGrip.actor, type: "detach" });
+    is(response.type, "detached", "Should have detached");
 
-  response = await targetFront.detach();
-  is(response.type, "detached", "Should have detached");
-
-  const newGrip = tabs.filter(a => a.url == TAB1_URL).pop();
-  is(newGrip.actor, tabGrip.actor, "Should have the same actor for the same tab");
+    const newGrip = tabs.filter(a => a.url == TAB1_URL).pop();
+    is(newGrip.actor, tabGrip.actor, "Should have the same actor for the same tab");
 
-  [response, targetFront] = await client.attachTarget(tabGrip.actor);
-  is(response.type, "tabAttached", "Should have attached");
-  response = await targetFront.detach();
-  is(response.type, "detached", "Should have detached");
+    response = yield gClient.request({ to: tabGrip.actor, type: "attach" });
+    is(response.type, "tabAttached", "Should have attached");
+    response = yield gClient.request({ to: tabGrip.actor, type: "detach" });
+    is(response.type, "detached", "Should have detached");
 
-  await removeTab(tab);
-  await client.close();
+    yield removeTab(tab);
+    yield gClient.close();
+    finish();
+  }));
+}
+
+registerCleanupFunction(function() {
+  gClient = null;
 });
--- a/devtools/client/shared/test/browser_dbg_navigation.js
+++ b/devtools/client/shared/test/browser_dbg_navigation.js
@@ -34,62 +34,62 @@ function test() {
       .then(testDetach)
       .then(finish)
       .catch(error => {
         ok(false, "Got an error: " + error.message + "\n" + error.stack);
       });
   });
 }
 
-function testNavigate(targetFront) {
+function testNavigate([aGrip, aResponse]) {
   const outstanding = [promise.defer(), promise.defer()];
 
-  targetFront.on("tabNavigated", function onTabNavigated(packet) {
+  gClient.addListener("tabNavigated", function onTabNavigated(event, packet) {
     is(packet.url.split("/").pop(), TAB2_FILE,
       "Got a tab navigation notification.");
 
     info(JSON.stringify(packet));
     info(JSON.stringify(event));
 
     if (packet.state == "start") {
       ok(true, "Tab started to navigate.");
       outstanding[0].resolve();
     } else {
       ok(true, "Tab finished navigating.");
-      targetFront.off("tabNavigated", onTabNavigated);
+      gClient.removeListener("tabNavigated", onTabNavigated);
       outstanding[1].resolve();
     }
   });
 
   BrowserTestUtils.loadURI(gBrowser.selectedBrowser, TAB2_URL);
   return promise.all(outstanding.map(e => e.promise))
-                .then(() => targetFront);
+                .then(() => aGrip.actor);
 }
 
-async function testDetach(targetFront) {
-  const onDetached = targetFront.once("tabDetached");
+function testDetach(actor) {
+  const deferred = promise.defer();
+
+  gClient.addOneTimeListener("tabDetached", (type, packet) => {
+    ok(true, "Got a tab detach notification.");
+    is(packet.from, actor, "tab detach message comes from the expected actor");
+    deferred.resolve(gClient.close());
+  });
 
   removeTab(gBrowser.selectedTab);
-
-  const packet = await onDetached;
-  ok(true, "Got a tab detach notification.");
-  is(packet.from, targetFront.actorID,
-    "tab detach message comes from the expected actor");
-
-  return gClient.close();
+  return deferred.promise;
 }
 
 registerCleanupFunction(function() {
   gClient = null;
 });
 
 async function attachTargetActorForUrl(client, url) {
   const grip = await getTargetActorForUrl(client, url);
-  const [, targetFront] = await client.attachTarget(grip.actor);
-  return targetFront;
+  const [ response ] = await client.attachTarget(grip.actor);
+  return [grip, response];
 }
 
 function getTargetActorForUrl(client, url) {
   const deferred = promise.defer();
 
   client.listTabs().then(response => {
     const targetActor = response.tabs.filter(grip => grip.url == url).pop();
     deferred.resolve(targetActor);
--- a/devtools/client/webaudioeditor/test/head.js
+++ b/devtools/client/webaudioeditor/test/head.js
@@ -43,17 +43,17 @@ registerCleanupFunction(() => {
 });
 
 function reload(aTarget, aWaitForTargetEvent = "navigate") {
   aTarget.activeTab.reload();
   return once(aTarget, aWaitForTargetEvent);
 }
 
 function navigate(aTarget, aUrl, aWaitForTargetEvent = "navigate") {
-  executeSoon(() => aTarget.activeTab.navigateTo({ url: aUrl }));
+  executeSoon(() => aTarget.activeTab.navigateTo(aUrl));
   return once(aTarget, aWaitForTargetEvent);
 }
 
 /**
  * Adds a new tab, and instantiate a WebAudiFront object.
  * This requires calling removeTab before the test ends.
  */
 function initBackend(aUrl) {
--- a/devtools/client/webide/modules/tab-store.js
+++ b/devtools/client/webide/modules/tab-store.js
@@ -56,21 +56,25 @@ TabStore.prototype = {
     this._selectedTabTargetPromise = null;
   },
 
   _onStatusChanged: function() {
     if (this._connection.status == Connection.Status.CONNECTED) {
       // Watch for changes to remote browser tabs
       this._connection.client.addListener("tabListChanged",
                                           this._onTabListChanged);
+      this._connection.client.addListener("tabNavigated",
+                                          this._onTabNavigated);
       this.listTabs();
     } else {
       if (this._connection.client) {
         this._connection.client.removeListener("tabListChanged",
                                                this._onTabListChanged);
+        this._connection.client.removeListener("tabNavigated",
+                                               this._onTabNavigated);
       }
       this._resetStore();
     }
   },
 
   _onTabListChanged: function() {
     this.listTabs().then(() => this.emit("tab-list"))
                    .catch(console.error);
--- a/devtools/docs/backend/client-api.md
+++ b/devtools/docs/backend/client-api.md
@@ -16,16 +16,18 @@ function start() {
   DebuggerServer.registerAllActors();
 
   // Listen to an nsIPipe
   let transport = DebuggerServer.connectPipe();
 
   // Start the client.
   client = new DebuggerClient(transport);
 
+  // Attach listeners for client events.
+  client.addListener("tabNavigated", onTab);
   client.connect((type, traits) => {
     // Now the client is conected to the server.
     debugTab();
   });
 }
 ```
 
 If a TCP socket is required, the function should be split in two parts, a server-side and a client-side, like this:
@@ -44,16 +46,19 @@ function startServer() {
 }
 
 async function startClient() {
   let transport = await DebuggerClient.socketConnect({ host: "localhost", port: 2929 });
 
   // Start the client.
   client = new DebuggerClient(transport);
 
+  // Attach listeners for client events.
+  client.addListener("tabNavigated", onTab);
+
   client.connect((type, traits) => {
     // Now the client is conected to the server.
     debugTab();
   });
 }
 ```
 
 ## Shutting down
@@ -79,36 +84,33 @@ function attachToTab() {
 
     // Attach to the tab.
     client.attachTarget(tab.actor).then(([response, tabClient]) => {
       if (!tabClient) {
         return;
       }
 
       // Now the tabClient is ready and can be used.
-
-      // Attach listeners for client events.
-      tabClient.addListener("tabNavigated", onTab);
     });
   });
 }
 ```
 
 The debugger client will send event notifications for a number of events the application may be interested in. These events include state changes in the debugger, like pausing and resuming, stack frames or source scripts being ready for retrieval, etc.
 
 ## Handling location changes
 
 When the user navigates away from a page, a `tabNavigated` event will be fired. The proper way to handle this event is to detach from the previous thread and tab and attach to the new ones:
 
 ```javascript
 async function onTab() {
   // Detach from the previous thread.
   await client.activeThread.detach();
   // Detach from the previous tab.
-  await tabClient.activeTab.detach();
+  await client.activeTab.detach();
   // Start debugging the new tab.
   start();
 }
 ```
 
 ## Debugging JavaScript running in a browser tab
 
 Once the application is attached to a tab, it can attach to its thread in order to interact with the JavaScript debugger:
@@ -162,16 +164,18 @@ function startDebugger() {
   let transport = DebuggerServer.connectPipe();
   // For an nsIServerSocket we do this:
   // DebuggerServer.openListener(port);
   // ...and this at the client:
   // let transport = debuggerSocketConnect(host, port);
 
   // Start the client.
   client = new DebuggerClient(transport);
+  // Attach listeners for client events.
+  client.addListener("tabNavigated", onTab);
   client.connect((type, traits) => {
     // Now the client is conected to the server.
     debugTab();
   });
 }
 
 function shutdownDebugger() {
   client.close();
--- a/devtools/server/actors/targets/browsing-context.js
+++ b/devtools/server/actors/targets/browsing-context.js
@@ -961,17 +961,17 @@ const browsingContextTargetPrototype = {
   /**
    * Navigate this browsing context to a new location
    */
   navigateTo(request) {
     // Wait a tick so that the response packet can be dispatched before the
     // subsequent navigation event packet.
     Services.tm.dispatchToMainThread(DevToolsUtils.makeInfallible(() => {
       this.window.location = request.url;
-    }, "BrowsingContextTargetActor.prototype.navigateTo's delayed body:" + request.url));
+    }, "BrowsingContextTargetActor.prototype.navigateTo's delayed body"));
     return {};
   },
 
   /**
    * Reconfigure options.
    */
   reconfigure(request) {
     const options = request.options || {};
--- a/devtools/server/tests/browser/browser_navigateEvents.js
+++ b/devtools/server/tests/browser/browser_navigateEvents.js
@@ -97,22 +97,22 @@ function onMessage({ data }) {
 
 async function connectAndAttachTab() {
   // Ensure having a minimal server
   initDebuggerServer();
 
   // Connect to this tab
   const transport = DebuggerServer.connectPipe();
   const client = new DebuggerClient(transport);
+  client.addListener("tabNavigated", function(event, packet) {
+    assertEvent("tabNavigated", packet);
+  });
   const form = await connectDebuggerClient(client);
   const actorID = form.actor;
-  const [, targetFront ] = await client.attachTarget(actorID);
-  targetFront.on("tabNavigated", function(packet) {
-    assertEvent("tabNavigated", packet);
-  });
+  await client.attachTarget(actorID);
   return { client, actorID };
 }
 
 add_task(async function() {
   // Open a test tab
   const browser = await addTab(URL1);
 
   // Listen for alert() call being made in navigate-first during unload
--- a/devtools/server/tests/browser/browser_webextension_inspected_window.js
+++ b/devtools/server/tests/browser/browser_webextension_inspected_window.js
@@ -21,42 +21,42 @@ async function setup(pageUrl) {
     lineNumber: 1,
     addonId: extension.id,
   };
 
   const target = await addTabTarget(pageUrl);
 
   const { client, form } = target;
 
-  const [, targetFront] = await client.attachTarget(form.actor);
+  const [, tabClient] = await client.attachTarget(form.actor);
 
   const [, consoleClient] = await client.attachConsole(form.consoleActor, []);
 
   const inspectedWindowFront = target.getFront("webExtensionInspectedWindow");
 
   return {
     client, form,
-    targetFront, consoleClient,
+    tabClient, consoleClient,
     inspectedWindowFront,
     extension, fakeExtCallerInfo,
   };
 }
 
 async function teardown({client, extension}) {
   await client.close();
   DebuggerServer.destroy();
   gBrowser.removeCurrentTab();
   await extension.unload();
 }
 
-function waitForNextTabNavigated(targetFront) {
+function waitForNextTabNavigated(client) {
   return new Promise(resolve => {
-    targetFront.on("tabNavigated", function tabNavigatedListener(pkt) {
+    client.addListener("tabNavigated", function tabNavigatedListener(evt, pkt) {
       if (pkt.state == "stop" && !pkt.isFrameSwitching) {
-        targetFront.off("tabNavigated", tabNavigatedListener);
+        client.removeListener("tabNavigated", tabNavigatedListener);
         resolve();
       }
     });
   });
 }
 
 function consoleEvalJS(consoleClient, jsCode) {
   return new Promise(resolve => {
@@ -222,111 +222,111 @@ add_task(async function test_exception_i
      "Got the expected stack trace in the exception message");
 
   await teardown({client, extension});
 });
 
 add_task(async function test_exception_inspectedWindowReload() {
   const {
     client, consoleClient, inspectedWindowFront,
-    extension, fakeExtCallerInfo, targetFront,
+    extension, fakeExtCallerInfo,
   } = await setup(`${TEST_RELOAD_URL}?test=cache`);
 
   // Test reload with bypassCache=false.
 
-  const waitForNoBypassCacheReload = waitForNextTabNavigated(targetFront);
+  const waitForNoBypassCacheReload = waitForNextTabNavigated(client);
   const reloadResult = await inspectedWindowFront.reload(fakeExtCallerInfo,
                                                          {ignoreCache: false});
 
   ok(!reloadResult, "Got the expected undefined result from inspectedWindow reload");
 
   await waitForNoBypassCacheReload;
 
   const noBypassCacheEval = await consoleEvalJS(consoleClient,
                                                 "document.body.textContent");
 
   is(noBypassCacheEval.result, "empty cache headers",
      "Got the expected result with reload forceBypassCache=false");
 
   // Test reload with bypassCache=true.
 
-  const waitForForceBypassCacheReload = waitForNextTabNavigated(targetFront);
+  const waitForForceBypassCacheReload = waitForNextTabNavigated(client);
   await inspectedWindowFront.reload(fakeExtCallerInfo, {ignoreCache: true});
 
   await waitForForceBypassCacheReload;
 
   const forceBypassCacheEval = await consoleEvalJS(consoleClient,
                                                    "document.body.textContent");
 
   is(forceBypassCacheEval.result, "no-cache:no-cache",
      "Got the expected result with reload forceBypassCache=true");
 
   await teardown({client, extension});
 });
 
 add_task(async function test_exception_inspectedWindowReload_customUserAgent() {
   const {
     client, consoleClient, inspectedWindowFront,
-    extension, fakeExtCallerInfo, targetFront,
+    extension, fakeExtCallerInfo,
   } = await setup(`${TEST_RELOAD_URL}?test=user-agent`);
 
   // Test reload with custom userAgent.
 
-  const waitForCustomUserAgentReload = waitForNextTabNavigated(targetFront);
+  const waitForCustomUserAgentReload = waitForNextTabNavigated(client);
   await inspectedWindowFront.reload(fakeExtCallerInfo,
                                     {userAgent: "Customized User Agent"});
 
   await waitForCustomUserAgentReload;
 
   const customUserAgentEval = await consoleEvalJS(consoleClient,
                                                   "document.body.textContent");
 
   is(customUserAgentEval.result, "Customized User Agent",
      "Got the expected result on reload with a customized userAgent");
 
   // Test reload with no custom userAgent.
 
-  const waitForNoCustomUserAgentReload = waitForNextTabNavigated(targetFront);
+  const waitForNoCustomUserAgentReload = waitForNextTabNavigated(client);
   await inspectedWindowFront.reload(fakeExtCallerInfo, {});
 
   await waitForNoCustomUserAgentReload;
 
   const noCustomUserAgentEval = await consoleEvalJS(consoleClient,
                                                     "document.body.textContent");
 
   is(noCustomUserAgentEval.result, window.navigator.userAgent,
      "Got the expected result with reload without a customized userAgent");
 
   await teardown({client, extension});
 });
 
 add_task(async function test_exception_inspectedWindowReload_injectedScript() {
   const {
     client, consoleClient, inspectedWindowFront,
-    extension, fakeExtCallerInfo, targetFront,
+    extension, fakeExtCallerInfo,
   } = await setup(`${TEST_RELOAD_URL}?test=injected-script&frames=3`);
 
   // Test reload with an injectedScript.
 
-  const waitForInjectedScriptReload = waitForNextTabNavigated(targetFront);
+  const waitForInjectedScriptReload = waitForNextTabNavigated(client);
   await inspectedWindowFront.reload(fakeExtCallerInfo,
                                     {injectedScript: `new ${injectedScript}`});
   await waitForInjectedScriptReload;
 
   const injectedScriptEval = await consoleEvalJS(consoleClient,
                                                  `(${collectEvalResults})()`);
 
   const expectedResult = (new Array(5)).fill("injected script executed first");
 
   SimpleTest.isDeeply(JSON.parse(injectedScriptEval.result), expectedResult,
      "Got the expected result on reload with an injected script");
 
   // Test reload without an injectedScript.
 
-  const waitForNoInjectedScriptReload = waitForNextTabNavigated(targetFront);
+  const waitForNoInjectedScriptReload = waitForNextTabNavigated(client);
   await inspectedWindowFront.reload(fakeExtCallerInfo, {});
   await waitForNoInjectedScriptReload;
 
   const noInjectedScriptEval = await consoleEvalJS(consoleClient,
                                                    `(${collectEvalResults})()`);
 
   const newExpectedResult = (new Array(5)).fill("injected script NOT executed");
 
@@ -334,68 +334,68 @@ add_task(async function test_exception_i
                       "Got the expected result on reload with no injected script");
 
   await teardown({client, extension});
 });
 
 add_task(async function test_exception_inspectedWindowReload_multiple_calls() {
   const {
     client, consoleClient, inspectedWindowFront,
-    extension, fakeExtCallerInfo, targetFront,
+    extension, fakeExtCallerInfo,
   } = await setup(`${TEST_RELOAD_URL}?test=user-agent`);
 
   // Test reload with custom userAgent three times (and then
   // check that only the first one has affected the page reload.
 
-  const waitForCustomUserAgentReload = waitForNextTabNavigated(targetFront);
+  const waitForCustomUserAgentReload = waitForNextTabNavigated(client);
 
   inspectedWindowFront.reload(fakeExtCallerInfo, {userAgent: "Customized User Agent 1"});
   inspectedWindowFront.reload(fakeExtCallerInfo, {userAgent: "Customized User Agent 2"});
 
   await waitForCustomUserAgentReload;
 
   const customUserAgentEval = await consoleEvalJS(consoleClient,
                                                   "document.body.textContent");
 
   is(customUserAgentEval.result, "Customized User Agent 1",
      "Got the expected result on reload with a customized userAgent");
 
   // Test reload with no custom userAgent.
 
-  const waitForNoCustomUserAgentReload = waitForNextTabNavigated(targetFront);
+  const waitForNoCustomUserAgentReload = waitForNextTabNavigated(client);
   await inspectedWindowFront.reload(fakeExtCallerInfo, {});
 
   await waitForNoCustomUserAgentReload;
 
   const noCustomUserAgentEval = await consoleEvalJS(consoleClient,
                                                     "document.body.textContent");
 
   is(noCustomUserAgentEval.result, window.navigator.userAgent,
      "Got the expected result with reload without a customized userAgent");
 
   await teardown({client, extension});
 });
 
 add_task(async function test_exception_inspectedWindowReload_stopped() {
   const {
     client, consoleClient, inspectedWindowFront,
-    extension, fakeExtCallerInfo, targetFront,
+    extension, fakeExtCallerInfo,
   } = await setup(`${TEST_RELOAD_URL}?test=injected-script&frames=3`);
 
   // Test reload on a page that calls window.stop() immediately during the page loading
 
-  const waitForPageLoad = waitForNextTabNavigated(targetFront);
+  const waitForPageLoad = waitForNextTabNavigated(client);
   await inspectedWindowFront.eval(fakeExtCallerInfo,
                                   "window.location += '&stop=windowStop'");
 
   info("Load a webpage that calls 'window.stop()' while is still loading");
   await waitForPageLoad;
 
   info("Starting a reload with an injectedScript");
-  const waitForInjectedScriptReload = waitForNextTabNavigated(targetFront);
+  const waitForInjectedScriptReload = waitForNextTabNavigated(client);
   await inspectedWindowFront.reload(fakeExtCallerInfo,
                                     {injectedScript: `new ${injectedScript}`});
   await waitForInjectedScriptReload;
 
   const injectedScriptEval = await consoleEvalJS(consoleClient,
                                                  `(${collectEvalResults})()`);
 
   // The page should have stopped during the reload and only one injected script
@@ -403,17 +403,17 @@ add_task(async function test_exception_i
   const expectedResult = (new Array(1)).fill("injected script executed first");
 
   SimpleTest.isDeeply(JSON.parse(injectedScriptEval.result), expectedResult,
      "The injected script has been executed on the 'stopped' page reload");
 
   // Reload again with no options.
 
   info("Reload the tab again without any reload options");
-  const waitForNoInjectedScriptReload = waitForNextTabNavigated(targetFront);
+  const waitForNoInjectedScriptReload = waitForNextTabNavigated(client);
   await inspectedWindowFront.reload(fakeExtCallerInfo, {});
   await waitForNoInjectedScriptReload;
 
   const noInjectedScriptEval = await consoleEvalJS(consoleClient,
                                                    `(${collectEvalResults})()`);
 
   // The page should have stopped during the reload and no injected script should
   // have been executed during this second reload (or it would mean that the previous
--- a/devtools/server/tests/mochitest/test_webextension-addon-debugging-connect.html
+++ b/devtools/server/tests/mochitest/test_webextension-addon-debugging-connect.html
@@ -37,24 +37,25 @@ async function test_connect_addon(oopMod
   await client.connect();
 
   // List addons and assertions on the expected addon actor.
   const {addons} = await client.mainRoot.listAddons();
   const addonTargetActor = addons.filter(actor => actor.id === extension.id).pop();
   ok(addonTargetActor, "The expected webextension addon actor has been found");
 
   // Connect to the target addon actor and wait for the updated list of frames.
+  const waitFramesUpdated = waitForFramesUpdated({client});
   const addonTarget = await TargetFactory.forRemoteTab({
     form: addonTargetActor,
     client,
     chrome: true,
   });
   is(addonTarget.form.isOOP, oopMode,
      "Got the expected oop mode in the webextension actor form");
-  const frames = await waitForFramesUpdated(addonTarget);
+  const frames = await waitFramesUpdated;
   const backgroundPageFrame = frames.filter((frame) => {
     return frame.url && frame.url.endsWith("/_generated_background_page.html");
   }).pop();
   is(backgroundPageFrame.addonID, extension.id, "Got an extension frame");
   ok(addonTarget.activeTab, "The addon target has an activeTab");
 
   // When running in oop mode we can explicitly attach the thread without locking
   // the main process.
--- a/devtools/server/tests/mochitest/webextension-helpers.js
+++ b/devtools/server/tests/mochitest/webextension-helpers.js
@@ -46,29 +46,29 @@ SimpleTest.registerCleanupFunction(funct
 function setWebExtensionOOPMode(oopMode) {
   return SpecialPowers.pushPrefEnv({
     "set": [
       ["extensions.webextensions.remote", oopMode],
     ]
   });
 }
 
-function waitForFramesUpdated(target, matchFn) {
+function waitForFramesUpdated({client}, matchFn) {
   return new Promise(resolve => {
-    const listener = data => {
+    const listener = (evt, data) => {
       if (typeof matchFn === "function" && !matchFn(data)) {
         return;
       } else if (!data.frames) {
         return;
       }
 
-      target.activeTab.off("frameUpdate", listener);
+      client.removeListener("frameUpdate", listener);
       resolve(data.frames);
     };
-    target.activeTab.on("frameUpdate", listener);
+    client.addListener("frameUpdate", listener);
   });
 }
 
 function collectFrameUpdates({client}, matchFn) {
   const collected = [];
 
   const listener = (evt, data) => {
     if (matchFn(data)) {
--- a/devtools/server/tests/unit/test_sourcemaps-01.js
+++ b/devtools/server/tests/unit/test_sourcemaps-01.js
@@ -39,17 +39,17 @@ function test_simple_source_map() {
     Assert.equal(packet.type, "newSource");
     Assert.ok(!!packet.source);
 
     Assert.ok(expectedSources.has(packet.source.url),
               "The source url should be one of our original sources.");
     expectedSources.delete(packet.source.url);
 
     if (expectedSources.size === 0) {
-      gThreadClient.removeListener("newSource", _onNewSource);
+      gClient.removeListener("newSource", _onNewSource);
       finishClient(gClient);
     }
   });
 
   let { code, map } = (new SourceNode(null, null, null, [
     new SourceNode(1, 0, "a.js", "function a() { return 'a'; }\n"),
     new SourceNode(1, 0, "b.js", "function b() { return 'b'; }\n"),
     new SourceNode(1, 0, "c.js", "function c() { return 'c'; }\n"),
--- a/devtools/shared/client/constants.js
+++ b/devtools/shared/client/constants.js
@@ -28,26 +28,23 @@ const UnsolicitedNotifications = {
   "networkEventUpdate": "networkEventUpdate",
   "documentEvent": "documentEvent",
   "tabDetached": "tabDetached",
   "tabListChanged": "tabListChanged",
   "reflowActivity": "reflowActivity",
   "addonListChanged": "addonListChanged",
   "workerListChanged": "workerListChanged",
   "serviceWorkerRegistrationListChanged": "serviceWorkerRegistrationList",
+  "tabNavigated": "tabNavigated",
+  "frameUpdate": "frameUpdate",
   "pageError": "pageError",
   "evaluationResult": "evaluationResult",
+  "newSource": "newSource",
   "updatedSource": "updatedSource",
-  "inspectObject": "inspectObject",
-
-  // newSource is still emitted on the ThreadActor, in addition to the
-  // BrowsingContextActor we have to keep it here until ThreadClient is converted to
-  // ThreadFront and/or we stop emitting this duplicated events.
-  // See ThreadActor.onNewSourceEvent.
-  "newSource": "newSource",
+  "inspectObject": "inspectObject"
 };
 
 /**
  * Set of pause types that are sent by the server and not as an immediate
  * response to a client request.
  */
 const UnsolicitedPauses = {
   "resumeLimit": "resumeLimit",
--- a/devtools/shared/client/debugger-client.js
+++ b/devtools/shared/client/debugger-client.js
@@ -19,21 +19,20 @@ const {
 
 loader.lazyRequireGetter(this, "Authentication", "devtools/shared/security/auth");
 loader.lazyRequireGetter(this, "DebuggerSocket", "devtools/shared/security/socket", true);
 loader.lazyRequireGetter(this, "EventEmitter", "devtools/shared/event-emitter");
 
 loader.lazyRequireGetter(this, "WebConsoleClient", "devtools/shared/webconsole/client", true);
 loader.lazyRequireGetter(this, "AddonClient", "devtools/shared/client/addon-client");
 loader.lazyRequireGetter(this, "RootClient", "devtools/shared/client/root-client");
-loader.lazyRequireGetter(this, "BrowsingContextFront", "devtools/shared/fronts/targets/browsing-context", true);
+loader.lazyRequireGetter(this, "TabClient", "devtools/shared/client/tab-client");
 loader.lazyRequireGetter(this, "ThreadClient", "devtools/shared/client/thread-client");
 loader.lazyRequireGetter(this, "WorkerClient", "devtools/shared/client/worker-client");
 loader.lazyRequireGetter(this, "ObjectClient", "devtools/shared/client/object-client");
-loader.lazyRequireGetter(this, "Pool", "devtools/shared/protocol", true);
 
 // Retrieve the major platform version, i.e. if we are on Firefox 64.0a1, it will be 64.
 const PLATFORM_MAJOR_VERSION = AppConstants.MOZ_APP_VERSION.match(/\d+/)[0];
 
 // Define the minimum officially supported version of Firefox when connecting to a remote
 // runtime. (Use ".0a1" to support the very first nightly version)
 // This matches the release channel's version when we are on nightly,
 // or 2 versions before when we are on other channels.
@@ -46,29 +45,18 @@ const MS_PER_DAY = 86400000;
  * provides the means to communicate with the server and exchange the messages
  * required by the protocol in a traditional JavaScript API.
  */
 function DebuggerClient(transport) {
   this._transport = transport;
   this._transport.hooks = this;
 
   // Map actor ID to client instance for each actor type.
-  // To be removed once all clients are refactored to protocol.js
   this._clients = new Map();
 
-  // Pool of fronts instanciated by this class.
-  // This is useful for actors that have already been transitioned to protocol.js
-  // Once RootClient becomes a protocol.js actor, these actors can be attached to it
-  // instead of this pool.
-  // This Pool will automatically be added to this._pools via addActorPool once the first
-  // Front will be added to it (in attachTarget, attachWorker,...).
-  // And it does not need to destroyed explicitly as all Pools are destroyed on client
-  // closing.
-  this._frontPool = new Pool(this);
-
   this._pendingRequests = new Map();
   this._activeRequests = new Map();
   this._eventsEnabled = true;
 
   this.traits = {};
 
   this.request = this.request.bind(this);
   this.localTransport = this._transport.onOutputStreamReady === undefined;
@@ -291,19 +279,17 @@ DebuggerClient.prototype = {
       deferred.promise.then(onClosed);
     }
 
     // Disable detach event notifications, because event handlers will be in a
     // cleared scope by the time they run.
     this._eventsEnabled = false;
 
     const cleanup = () => {
-      if (this._transport) {
-        this._transport.close();
-      }
+      this._transport.close();
       this._transport = null;
     };
 
     // If the connection is already closed,
     // there is no need to detach client
     // as we won't be able to send any message.
     if (this._closed) {
       cleanup();
@@ -363,25 +349,39 @@ DebuggerClient.prototype = {
    *  - start watching for inner iframe updates (emits `frameUpdate` messages)
    *  - retrieve the thread actor:
    *    Instantiates a new ThreadActor that can be later attached to in order to
    *    debug JS sources in the document.
    *
    * @param string targetActor
    *        The target actor ID for the tab to attach.
    */
-  attachTarget: async function(targetActor) {
-    let front = this._frontPool.actor(targetActor);
-    if (!front) {
-      front = new BrowsingContextFront(this, { actor: targetActor });
-      this._frontPool.manage(front);
+  attachTarget: function(targetActor) {
+    if (this._clients.has(targetActor)) {
+      const cachedTarget = this._clients.get(targetActor);
+      const cachedResponse = {
+        cacheDisabled: cachedTarget.cacheDisabled,
+        javascriptEnabled: cachedTarget.javascriptEnabled,
+        traits: cachedTarget.traits,
+      };
+      return promise.resolve([cachedResponse, cachedTarget]);
     }
 
-    const response = await front.attach();
-    return [response, front];
+    const packet = {
+      to: targetActor,
+      type: "attach"
+    };
+    return this.request(packet).then(response => {
+      // TabClient can actually represent targets other than a tab.
+      // It is planned to be renamed while being converted to a front
+      // in bug 1485660.
+      const targetClient = new TabClient(this, response);
+      this.registerClient(targetClient);
+      return [response, targetClient];
+    });
   },
 
   attachWorker: function(workerTargetActor) {
     let workerClient = this._clients.get(workerTargetActor);
     if (workerClient !== undefined) {
       const response = {
         from: workerClient.actor,
         type: "attached",
--- a/devtools/shared/client/moz.build
+++ b/devtools/shared/client/moz.build
@@ -14,11 +14,12 @@ DevToolsModules(
     'environment-client.js',
     'event-source.js',
     'long-string-client.js',
     'object-client.js',
     'property-iterator-client.js',
     'root-client.js',
     'source-client.js',
     'symbol-iterator-client.js',
+    'tab-client.js',
     'thread-client.js',
     'worker-client.js',
 )
new file mode 100644
--- /dev/null
+++ b/devtools/shared/client/tab-client.js
@@ -0,0 +1,163 @@
+/* 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 promise = require("devtools/shared/deprecated-sync-thenables");
+
+const eventSource = require("devtools/shared/client/event-source");
+const {arg, DebuggerClient} = require("devtools/shared/client/debugger-client");
+loader.lazyRequireGetter(this, "ThreadClient", "devtools/shared/client/thread-client");
+
+/**
+ * Creates a tab client for the remote debugging protocol server. This client is a front
+ * to the target actor for a tab created in the server side, hiding the protocol details
+ * in a traditional JavaScript API.
+ *
+ * @param client DebuggerClient
+ *        The debugger client parent.
+ * @param form object
+ *        The protocol form for this tab.
+ */
+function TabClient(client, form) {
+  this.client = client;
+  this._actor = form.from;
+  this._threadActor = form.threadActor;
+  this.javascriptEnabled = form.javascriptEnabled;
+  this.cacheDisabled = form.cacheDisabled;
+  this.thread = null;
+  this.request = this.client.request;
+  this.traits = form.traits || {};
+  this.events = ["workerListChanged"];
+}
+
+TabClient.prototype = {
+  get actor() {
+    return this._actor;
+  },
+  get _transport() {
+    return this.client._transport;
+  },
+
+  /**
+   * Attach to a thread actor.
+   *
+   * @param object options
+   *        Configuration options.
+   *        - useSourceMaps: whether to use source maps or not.
+   */
+  attachThread: function(options = {}) {
+    if (this.thread) {
+      return promise.resolve([{}, this.thread]);
+    }
+
+    const packet = {
+      to: this._threadActor,
+      type: "attach",
+      options,
+    };
+    return this.request(packet).then(response => {
+      this.thread = new ThreadClient(this, this._threadActor);
+      this.client.registerClient(this.thread);
+      return [response, this.thread];
+    });
+  },
+
+  /**
+   * Detach the client from the target actor.
+   */
+  detach: DebuggerClient.requester({
+    type: "detach"
+  }, {
+    before: function(packet) {
+      if (this.thread) {
+        this.thread.detach();
+      }
+      return packet;
+    },
+    after: function(response) {
+      this.client.unregisterClient(this);
+      return response;
+    },
+  }),
+
+  /**
+   * Bring the window to the front.
+   */
+  focus: DebuggerClient.requester({
+    type: "focus"
+  }, {}),
+
+  /**
+   * Ensure relevant pages have error reporting enabled.
+   */
+  ensureCSSErrorReportingEnabled: DebuggerClient.requester({
+    type: "ensureCSSErrorReportingEnabled",
+  }, {}),
+
+  /**
+   * Reload the page in this tab.
+   *
+   * @param [optional] object options
+   *        An object with a `force` property indicating whether or not
+   *        this reload should skip the cache
+   */
+  reload: function(options = { force: false }) {
+    return this._reload(options);
+  },
+  _reload: DebuggerClient.requester({
+    type: "reload",
+    options: arg(0)
+  }),
+
+  /**
+   * Navigate to another URL.
+   *
+   * @param string url
+   *        The URL to navigate to.
+   */
+  navigateTo: DebuggerClient.requester({
+    type: "navigateTo",
+    url: arg(0)
+  }),
+
+  /**
+   * Reconfigure the target actor.
+   *
+   * @param object options
+   *        A dictionary object of the new options to use in the target actor.
+   */
+  reconfigure: DebuggerClient.requester({
+    type: "reconfigure",
+    options: arg(0)
+  }),
+
+  listWorkers: DebuggerClient.requester({
+    type: "listWorkers"
+  }),
+
+  attachWorker: function(workerTargetActor) {
+    return this.client.attachWorker(workerTargetActor);
+  },
+
+  logInPage: DebuggerClient.requester({
+    type: "logInPage",
+    text: arg(0),
+    category: arg(1),
+    flags: arg(2),
+  }),
+
+  listFrames: DebuggerClient.requester({
+    type: "listFrames",
+  }),
+
+  switchToFrame: DebuggerClient.requester({
+    type: "switchToFrame",
+    windowId: arg(0),
+  }),
+};
+
+eventSource(TabClient.prototype);
+
+module.exports = TabClient;
--- a/devtools/shared/client/thread-client.js
+++ b/devtools/shared/client/thread-client.js
@@ -20,17 +20,17 @@ loader.lazyRequireGetter(this, "SourceCl
 
 const noop = () => {};
 
 /**
  * Creates a thread client for the remote debugging protocol server. This client
  * is a front to the thread actor created in the server side, hiding the
  * protocol details in a traditional JavaScript API.
  *
- * @param client DebuggerClient, WorkerClient or BrowsingContextFront
+ * @param client DebuggerClient|TabClient
  *        The parent of the thread (tab for target-scoped debuggers,
  *        DebuggerClient for chrome debuggers).
  * @param actor string
  *        The actor ID for this thread.
  */
 function ThreadClient(client, actor) {
   this._parent = client;
   this.client = client instanceof DebuggerClient ? client : client.client;
--- a/devtools/shared/fronts/moz.build
+++ b/devtools/shared/fronts/moz.build
@@ -1,17 +1,16 @@
 # -*- 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/.
 
 DIRS += [
     'addon',
-    'targets',
 ]
 
 DevToolsModules(
     'accessibility.js',
     'actor-registry.js',
     'animation.js',
     'canvas.js',
     'css-properties.js',
deleted file mode 100644
--- a/devtools/shared/fronts/targets/browsing-context.js
+++ /dev/null
@@ -1,88 +0,0 @@
-/* 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 {browsingContextTargetSpec} = require("devtools/shared/specs/targets/browsing-context");
-const protocol = require("devtools/shared/protocol");
-const {custom} = protocol;
-
-loader.lazyRequireGetter(this, "ThreadClient", "devtools/shared/client/thread-client");
-
-const BrowsingContextFront = protocol.FrontClassWithSpec(browsingContextTargetSpec, {
-  initialize: function(client, form) {
-    protocol.Front.prototype.initialize.call(this, client, form);
-
-    this.thread = null;
-
-    // TODO: remove once ThreadClient becomes a front
-    this.client = client;
-  },
-
-  /**
-   * Attach to a thread actor.
-   *
-   * @param object options
-   *        Configuration options.
-   *        - useSourceMaps: whether to use source maps or not.
-   */
-  attachThread: function(options = {}) {
-    if (this.thread) {
-      return Promise.resolve([{}, this.thread]);
-    }
-
-    const packet = {
-      to: this._threadActor,
-      type: "attach",
-      options,
-    };
-    return this.client.request(packet).then(response => {
-      this.thread = new ThreadClient(this, this._threadActor);
-      this.client.registerClient(this.thread);
-      return [response, this.thread];
-    });
-  },
-
-  attach: custom(async function() {
-    const response = await this._attach();
-
-    this._threadActor = response.threadActor;
-    this.javascriptEnabled = response.javascriptEnabled;
-    this.cacheDisabled = response.cacheDisabled;
-    this.traits = response.traits || {};
-
-    return response;
-  }, {
-    impl: "_attach"
-  }),
-
-  detach: custom(async function() {
-    let response;
-    try {
-      response = await this._detach();
-    } catch (e) {
-      console.warn(
-        `Error while detaching the browsing context target front: ${e.message}`);
-    }
-
-    if (this.thread) {
-      try {
-        await this.thread.detach();
-      } catch (e) {
-        console.warn(`Error while detaching the thread front: ${e.message}`);
-      }
-    }
-
-    this.destroy();
-
-    return response;
-  }, {
-    impl: "_detach"
-  }),
-
-  attachWorker: function(workerTargetActor) {
-    return this.client.attachWorker(workerTargetActor);
-  },
-});
-
-exports.BrowsingContextFront = BrowsingContextFront;
deleted file mode 100644
--- a/devtools/shared/fronts/targets/moz.build
+++ /dev/null
@@ -1,9 +0,0 @@
-# -*- 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(
-    'browsing-context.js',
-)
--- a/devtools/shared/protocol.js
+++ b/devtools/shared/protocol.js
@@ -1347,20 +1347,17 @@ Front.prototype = extend(Pool.prototype,
    * Send a packet on the connection.
    */
   send: function(packet) {
     if (packet.to) {
       this.conn._transport.send(packet);
     } else {
       this.actor().then(actorID => {
         packet.to = actorID;
-        // The connection might be closed during the promise resolution
-        if (this.conn._transport) {
-          this.conn._transport.send(packet);
-        }
+        this.conn._transport.send(packet);
       }).catch(console.error);
     }
   },
 
   /**
    * Send a two-way request on the connection.
    */
   request: function(packet) {
--- a/devtools/shared/specs/targets/browsing-context.js
+++ b/devtools/shared/specs/targets/browsing-context.js
@@ -122,27 +122,20 @@ const browsingContextTargetSpecPrototype
     },
     frameUpdate: {
       type: "frameUpdate",
       frames: Option(0, "nullable:array:browsingContextTarget.window"),
       selected: Option(0, "nullable:number"),
       destroyAll: Option(0, "nullable:boolean")
     },
     tabDetached: {
-      type: "tabDetached",
-      // This is to make browser_dbg_navigation.js to work as it expect to
-      // see a packet object when listening for tabDetached
-      from: Option(0, "string"),
+      type: "tabDetached"
     },
     workerListChanged: {
       type: "workerListChanged"
-    },
-    newSource: {
-      type: "newSource",
-      source: Option(0, "json")
     }
   }
 };
 
 const browsingContextTargetSpec = generateActorSpec(browsingContextTargetSpecPrototype);
 
 exports.browsingContextTargetSpecPrototype = browsingContextTargetSpecPrototype;
 exports.browsingContextTargetSpec = browsingContextTargetSpec;