Backed out 8 changesets (bug 1541631) for permafailing in debugger/cold-open.js CLOSED TREE
authorNoemi Erli <nerli@mozilla.com>
Fri, 16 Aug 2019 00:41:23 +0300
changeset 488356 db9aaec44445e5ccfef53a25cd6812107c3df802
parent 488355 4473a7402f25c2f632ce4aa06dee18328c96d59b
child 488357 476c9068a66937becf5950a0eb4829e981d6a723
push id36440
push userncsoregi@mozilla.com
push dateFri, 16 Aug 2019 03:57:48 +0000
treeherdermozilla-central@a58b7dc85887 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
bugs1541631
milestone70.0a1
backs outbc99fb7a9d125b00d33e54d002facbef7e8e3f2b
696e24030686ebe58588d5415b932077a6af0c11
6db731f26958d5f6ab2f6b3166a3d77e4cfeacaa
9f928da98d32a29631d35fd6787c6c36bb038bef
53b83b8e37e0986e048db3c40ddbf6d59a515660
1d7a76a1fac8fc0ed137859162f7b9965447b934
4f94c700f97773ecfe6cc9531c8b027df5dca5ce
41f6c078e950a7d4231745274047133b7372ebf4
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
Backed out 8 changesets (bug 1541631) for permafailing in debugger/cold-open.js CLOSED TREE Backed out changeset bc99fb7a9d12 (bug 1541631) Backed out changeset 696e24030686 (bug 1541631) Backed out changeset 6db731f26958 (bug 1541631) Backed out changeset 9f928da98d32 (bug 1541631) Backed out changeset 53b83b8e37e0 (bug 1541631) Backed out changeset 1d7a76a1fac8 (bug 1541631) Backed out changeset 4f94c700f977 (bug 1541631) Backed out changeset 41f6c078e950 (bug 1541631)
devtools/client/debugger/src/actions/ast/setInScopeLines.js
devtools/client/debugger/src/actions/ast/tests/setInScopeLines.spec.js
devtools/client/debugger/src/actions/breakpoints/breakpointPositions.js
devtools/client/debugger/src/actions/breakpoints/tests/breakpointPositions.spec.js
devtools/client/debugger/src/actions/breakpoints/tests/breakpoints.spec.js
devtools/client/debugger/src/actions/file-search.js
devtools/client/debugger/src/actions/pause/tests/pause.spec.js
devtools/client/debugger/src/actions/source-actors.js
devtools/client/debugger/src/actions/sources/breakableLines.js
devtools/client/debugger/src/actions/sources/loadSourceText.js
devtools/client/debugger/src/actions/sources/prettyPrint.js
devtools/client/debugger/src/actions/sources/symbols.js
devtools/client/debugger/src/actions/sources/tests/blackbox.spec.js
devtools/client/debugger/src/actions/sources/tests/loadSource.spec.js
devtools/client/debugger/src/actions/sources/tests/newSources.spec.js
devtools/client/debugger/src/actions/sources/tests/prettyPrint.spec.js
devtools/client/debugger/src/actions/sources/tests/querystrings.spec.js
devtools/client/debugger/src/actions/sources/tests/select.spec.js
devtools/client/debugger/src/actions/tests/ast.spec.js
devtools/client/debugger/src/actions/tests/expressions.spec.js
devtools/client/debugger/src/actions/tests/helpers/mockCommandClient.js
devtools/client/debugger/src/actions/tests/helpers/threadFront.js
devtools/client/debugger/src/actions/tests/navigation.spec.js
devtools/client/debugger/src/actions/tests/pending-breakpoints.spec.js
devtools/client/debugger/src/actions/tests/preview.spec.js
devtools/client/debugger/src/actions/tests/project-text-search.spec.js
devtools/client/debugger/src/actions/tests/setProjectDirectoryRoot.spec.js
devtools/client/debugger/src/actions/tests/tabs.spec.js
devtools/client/debugger/src/actions/types/SourceAction.js
devtools/client/debugger/src/actions/types/SourceActorAction.js
devtools/client/debugger/src/actions/utils/middleware/promise.js
devtools/client/debugger/src/client/firefox/commands.js
devtools/client/debugger/src/components/Editor/EditorMenu.js
devtools/client/debugger/src/components/Editor/Footer.js
devtools/client/debugger/src/components/Editor/HighlightLine.js
devtools/client/debugger/src/components/Editor/index.js
devtools/client/debugger/src/components/Editor/menus/editor.js
devtools/client/debugger/src/components/Editor/tests/DebugLine.spec.js
devtools/client/debugger/src/components/Editor/tests/Editor.spec.js
devtools/client/debugger/src/components/Editor/tests/Footer.spec.js
devtools/client/debugger/src/components/PrimaryPanes/Outline.js
devtools/client/debugger/src/components/SecondaryPanes/Frames/tests/Frames.spec.js
devtools/client/debugger/src/reducers/source-actors.js
devtools/client/debugger/src/reducers/sources.js
devtools/client/debugger/src/reducers/tests/sources.spec.js
devtools/client/debugger/src/selectors/visibleColumnBreakpoints.js
devtools/client/debugger/src/types.js
devtools/client/debugger/src/utils/async-value.js
devtools/client/debugger/src/utils/breakpoint/tests/astBreakpointLocation.spec.js
devtools/client/debugger/src/utils/editor/tests/editor.spec.js
devtools/client/debugger/src/utils/function.js
devtools/client/debugger/src/utils/isMinified.js
devtools/client/debugger/src/utils/memoizableAction.js
devtools/client/debugger/src/utils/test-mockup.js
devtools/client/debugger/src/utils/tests/ast.spec.js
devtools/client/debugger/src/utils/tests/function.spec.js
devtools/client/debugger/src/utils/tests/source.spec.js
devtools/client/debugger/src/utils/tests/wasm.spec.js
devtools/client/debugger/src/workers/parser/tests/findOutOfScopeLocations.spec.js
devtools/client/debugger/src/workers/parser/tests/framework.spec.js
devtools/client/debugger/src/workers/parser/tests/getScopes.spec.js
devtools/client/debugger/src/workers/parser/tests/getSymbols.spec.js
devtools/client/debugger/src/workers/parser/tests/helpers/index.js
devtools/client/debugger/src/workers/parser/tests/steps.spec.js
devtools/client/debugger/src/workers/parser/utils/tests/ast.spec.js
devtools/client/debugger/test/mochitest/helpers.js
--- a/devtools/client/debugger/src/actions/ast/setInScopeLines.js
+++ b/devtools/client/debugger/src/actions/ast/setInScopeLines.js
@@ -27,31 +27,32 @@ function getOutOfScopeLines(outOfScopeLo
   return uniq(
     flatMap(outOfScopeLocations, location =>
       range(location.start.line, location.end.line)
     )
   );
 }
 
 async function getInScopeLines(cx, location, { dispatch, getState, parser }) {
-  const source = getSourceWithContent(getState(), location.sourceId);
+  const { source, content } = getSourceWithContent(
+    getState(),
+    location.sourceId
+  );
 
   let locations = null;
   if (location.line && source && !source.isWasm) {
     locations = await parser.findOutOfScopeLocations(
       source.id,
       ((location: any): parser.AstPosition)
     );
   }
 
   const linesOutOfScope = getOutOfScopeLines(locations);
   const sourceNumLines =
-    !source.content || !isFulfilled(source.content)
-      ? 0
-      : getSourceLineCount(source.content.value);
+    !content || !isFulfilled(content) ? 0 : getSourceLineCount(content.value);
 
   const sourceLines = range(1, sourceNumLines + 1);
 
   return !linesOutOfScope
     ? sourceLines
     : without(sourceLines, ...linesOutOfScope);
 }
 
--- a/devtools/client/debugger/src/actions/ast/tests/setInScopeLines.spec.js
+++ b/devtools/client/debugger/src/actions/ast/tests/setInScopeLines.spec.js
@@ -16,30 +16,30 @@ import {
 } from "../../../utils/test-head";
 
 const { getInScopeLines } = selectors;
 
 const sourceTexts = {
   "scopes.js": readFixture("scopes.js"),
 };
 
-const mockCommandClient = {
+const threadFront = {
   sourceContents: async ({ source }) => ({
     source: sourceTexts[source],
     contentType: "text/javascript",
   }),
   evaluateExpressions: async () => {},
   getFrameScopes: async () => {},
-  getSourceActorBreakpointPositions: async () => ({}),
-  getSourceActorBreakableLines: async () => [],
+  getBreakpointPositions: async () => ({}),
+  getBreakableLines: async () => [],
 };
 
 describe("getInScopeLine", () => {
   it("with selected line", async () => {
-    const store = createStore(mockCommandClient);
+    const store = createStore(threadFront);
     const { dispatch, getState } = store;
     const source = makeMockSource("scopes.js", "scopes.js");
 
     await dispatch(actions.newGeneratedSource(makeSource("scopes.js")));
 
     await dispatch(
       actions.selectLocation(selectors.getContext(getState()), {
         sourceId: "scopes.js",
--- a/devtools/client/debugger/src/actions/breakpoints/breakpointPositions.js
+++ b/devtools/client/debugger/src/actions/breakpoints/breakpointPositions.js
@@ -9,16 +9,18 @@ import {
   isGeneratedId,
   originalToGeneratedId,
 } from "devtools-source-map";
 import { uniqBy, zip } from "lodash";
 
 import {
   getSource,
   getSourceFromId,
+  hasBreakpointPositions,
+  hasBreakpointPositionsForLine,
   getBreakpointPositionsForSource,
   getSourceActorsForSource,
 } from "../../selectors";
 
 import type {
   MappedLocation,
   Range,
   SourceLocation,
@@ -26,19 +28,17 @@ import type {
   Context,
 } from "../../types";
 
 import { makeBreakpointId } from "../../utils/breakpoint";
 import {
   memoizeableAction,
   type MemoizedAction,
 } from "../../utils/memoizableAction";
-import { fulfilled } from "../../utils/async-value";
 import type { ThunkArgs } from "../../actions/types";
-import { loadSourceActorBreakpointColumns } from "../source-actors";
 
 async function mapLocations(
   generatedLocations: SourceLocation[],
   { sourceMaps }: ThunkArgs
 ) {
   if (generatedLocations.length == 0) {
     return [];
   }
@@ -109,17 +109,17 @@ function groupByLine(results, sourceId, 
 
 async function _setBreakpointPositions(cx, sourceId, line, thunkArgs) {
   const { client, dispatch, getState, sourceMaps } = thunkArgs;
   let generatedSource = getSource(getState(), sourceId);
   if (!generatedSource) {
     return;
   }
 
-  const results = {};
+  let results = {};
   if (isOriginalId(sourceId)) {
     // Explicitly typing ranges is required to work around the following issue
     // https://github.com/facebook/flow/issues/5294
     const ranges: Range[] = await sourceMaps.getGeneratedRangesForOriginal(
       sourceId,
       generatedSource.url,
       true
     );
@@ -135,48 +135,33 @@ async function _setBreakpointPositions(c
       // in this case.
       if (range.end.column === Infinity) {
         range.end = {
           line: range.end.line + 1,
           column: 0,
         };
       }
 
-      const actorBps = await Promise.all(
-        getSourceActorsForSource(getState(), generatedSource.id).map(actor =>
-          client.getSourceActorBreakpointPositions(actor, range)
-        )
+      const bps = await client.getBreakpointPositions(
+        getSourceActorsForSource(getState(), generatedSource.id),
+        range
       );
-
-      for (const actorPositions of actorBps) {
-        for (const rangeLine of Object.keys(actorPositions)) {
-          let columns = actorPositions[parseInt(rangeLine, 10)];
-          const existing = results[rangeLine];
-          if (existing) {
-            columns = [...new Set([...existing, ...columns])];
-          }
-
-          results[rangeLine] = columns;
-        }
+      for (const bpLine in bps) {
+        results[bpLine] = (results[bpLine] || []).concat(bps[bpLine]);
       }
     }
   } else {
     if (typeof line !== "number") {
       throw new Error("Line is required for generated sources");
     }
 
-    const actorColumns = await Promise.all(
-      getSourceActorsForSource(getState(), generatedSource.id).map(actor =>
-        dispatch(loadSourceActorBreakpointColumns({ id: actor.id, line }))
-      )
+    results = await client.getBreakpointPositions(
+      getSourceActorsForSource(getState(), generatedSource.id),
+      { start: { line, column: 0 }, end: { line: line + 1, column: 0 } }
     );
-
-    for (const columns of actorColumns) {
-      results[line] = (results[line] || []).concat(columns);
-    }
   }
 
   let positions = convertToList(results, generatedSource);
   positions = await mapLocations(positions, thunkArgs);
 
   positions = filterBySource(positions, sourceId);
   positions = filterByUniqLocation(positions);
   positions = groupByLine(positions, sourceId, line);
@@ -188,16 +173,18 @@ async function _setBreakpointPositions(c
   }
 
   dispatch({
     type: "ADD_BREAKPOINT_POSITIONS",
     cx,
     source: source,
     positions,
   });
+
+  return positions;
 }
 
 function generatedSourceActorKey(state, sourceId) {
   const generatedSource = getSource(
     state,
     isOriginalId(sourceId) ? originalToGeneratedId(sourceId) : sourceId
   );
   const actors = generatedSource
@@ -207,29 +194,21 @@ function generatedSourceActorKey(state, 
     : [];
   return [sourceId, ...actors].join(":");
 }
 
 export const setBreakpointPositions: MemoizedAction<
   { cx: Context, sourceId: string, line?: number },
   ?BreakpointPositions
 > = memoizeableAction("setBreakpointPositions", {
-  getValue: ({ sourceId, line }, { getState }) => {
-    const positions = getBreakpointPositionsForSource(getState(), sourceId);
-    if (!positions) {
-      return null;
-    }
-
-    if (isGeneratedId(sourceId) && line && !positions[line]) {
-      // We always return the full position dataset, but if a given line is
-      // not available, we treat the whole set as loading.
-      return null;
-    }
-
-    return fulfilled(positions);
-  },
+  hasValue: ({ sourceId, line }, { getState }) =>
+    isGeneratedId(sourceId) && line
+      ? hasBreakpointPositionsForLine(getState(), sourceId, line)
+      : hasBreakpointPositions(getState(), sourceId),
+  getValue: ({ sourceId, line }, { getState }) =>
+    getBreakpointPositionsForSource(getState(), sourceId),
   createKey({ sourceId, line }, { getState }) {
     const key = generatedSourceActorKey(getState(), sourceId);
     return isGeneratedId(sourceId) && line ? `${key}-${line}` : key;
   },
   action: async ({ cx, sourceId, line }, thunkArgs) =>
     _setBreakpointPositions(cx, sourceId, line, thunkArgs),
 });
--- a/devtools/client/debugger/src/actions/breakpoints/tests/breakpointPositions.spec.js
+++ b/devtools/client/debugger/src/actions/breakpoints/tests/breakpointPositions.spec.js
@@ -6,25 +6,25 @@
 
 import {
   actions,
   selectors,
   createStore,
   makeSource,
   waitForState,
 } from "../../../utils/test-head";
-import { createSource } from "../../tests/helpers/mockCommandClient";
+import { createSource } from "../../tests/helpers/threadFront";
 
 describe("breakpointPositions", () => {
   it("fetches positions", async () => {
     const fooContent = createSource("foo", "");
 
     const store = createStore({
-      getSourceActorBreakpointPositions: async () => ({ "9": [1] }),
-      getSourceActorBreakableLines: async () => [],
+      getBreakpointPositions: async () => ({ "9": [1] }),
+      getBreakableLines: async () => [],
       sourceContents: async () => fooContent,
     });
 
     const { dispatch, getState, cx } = store;
     const source = await dispatch(
       actions.newGeneratedSource(makeSource("foo"))
     );
     await dispatch(actions.loadSourceById(cx, source.id));
@@ -58,22 +58,22 @@ describe("breakpointPositions", () => {
   });
 
   it("doesn't re-fetch positions", async () => {
     const fooContent = createSource("foo", "");
 
     let resolve = _ => {};
     let count = 0;
     const store = createStore({
-      getSourceActorBreakpointPositions: () =>
+      getBreakpointPositions: () =>
         new Promise(r => {
           count++;
           resolve = r;
         }),
-      getSourceActorBreakableLines: async () => [],
+      getBreakableLines: async () => [],
       sourceContents: async () => fooContent,
     });
 
     const { dispatch, getState, cx } = store;
     const source = await dispatch(
       actions.newGeneratedSource(makeSource("foo"))
     );
     await dispatch(actions.loadSourceById(cx, source.id));
--- a/devtools/client/debugger/src/actions/breakpoints/tests/breakpoints.spec.js
+++ b/devtools/client/debugger/src/actions/breakpoints/tests/breakpoints.spec.js
@@ -7,23 +7,23 @@
 import {
   createStore,
   selectors,
   actions,
   makeSource,
   getTelemetryEvents,
 } from "../../../utils/test-head";
 
-import { mockCommandClient } from "../../tests/helpers/mockCommandClient";
+import { simpleMockThreadFront } from "../../tests/helpers/threadFront.js";
 
 function mockClient(positionsResponse = {}) {
   return {
-    ...mockCommandClient,
-    getSourceActorBreakpointPositions: async () => positionsResponse,
-    getSourceActorBreakableLines: async () => [],
+    ...simpleMockThreadFront,
+    getBreakpointPositions: async () => positionsResponse,
+    getBreakableLines: async () => [],
   };
 }
 
 describe("breakpoints", () => {
   it("should add a breakpoint", async () => {
     const { dispatch, getState, cx } = createStore(mockClient({ "2": [1] }));
     const loc1 = {
       sourceId: "a",
--- a/devtools/client/debugger/src/actions/file-search.js
+++ b/devtools/client/debugger/src/actions/file-search.js
@@ -30,35 +30,35 @@ import {
   setActiveSearch,
 } from "./ui";
 import { isFulfilled } from "../utils/async-value";
 type Editor = Object;
 type Match = Object;
 
 export function doSearch(cx: Context, query: string, editor: Editor) {
   return ({ getState, dispatch }: ThunkArgs) => {
-    const selectedSource = getSelectedSourceWithContent(getState());
-    if (!selectedSource || !selectedSource.content) {
+    const selectedSourceWithContent = getSelectedSourceWithContent(getState());
+    if (!selectedSourceWithContent || !selectedSourceWithContent.content) {
       return;
     }
 
     dispatch(setFileSearchQuery(cx, query));
     dispatch(searchContents(cx, query, editor));
   };
 }
 
 export function doSearchForHighlight(
   query: string,
   editor: Editor,
   line: number,
   ch: number
 ) {
   return async ({ getState, dispatch }: ThunkArgs) => {
-    const selectedSource = getSelectedSourceWithContent(getState());
-    if (!selectedSource || !selectedSource.content) {
+    const selectedSourceWithContent = getSelectedSourceWithContent(getState());
+    if (!selectedSourceWithContent || !selectedSourceWithContent.content) {
       return;
     }
     dispatch(searchContentsForHighlight(query, editor, line, ch));
   };
 }
 
 export function setFileSearchQuery(cx: Context, query: string): Action {
   return {
@@ -100,28 +100,29 @@ export function updateSearchResults(
 export function searchContents(
   cx: Context,
   query: string,
   editor: Object,
   focusFirstResult?: boolean = true
 ) {
   return async ({ getState, dispatch }: ThunkArgs) => {
     const modifiers = getFileSearchModifiers(getState());
-    const selectedSource = getSelectedSourceWithContent(getState());
+    const selectedSourceWithContent = getSelectedSourceWithContent(getState());
 
     if (
       !editor ||
-      !selectedSource ||
-      !selectedSource.content ||
-      !isFulfilled(selectedSource.content) ||
+      !selectedSourceWithContent ||
+      !selectedSourceWithContent.content ||
+      !isFulfilled(selectedSourceWithContent.content) ||
       !modifiers
     ) {
       return;
     }
-    const selectedContent = selectedSource.content.value;
+    const selectedSource = selectedSourceWithContent.source;
+    const selectedContent = selectedSourceWithContent.content.value;
 
     const ctx = { ed: editor, cm: editor.codeMirror };
 
     if (!query) {
       clearSearch(ctx.cm, query);
       return;
     }
 
--- a/devtools/client/debugger/src/actions/pause/tests/pause.spec.js
+++ b/devtools/client/debugger/src/actions/pause/tests/pause.spec.js
@@ -17,17 +17,17 @@ import {
 import { makeWhyNormal } from "../../../utils/test-mockup";
 
 import { parserWorker } from "../../../test/tests-setup";
 import { features } from "../../../utils/prefs";
 
 const { isStepping } = selectors;
 
 let stepInResolve = null;
-const mockCommandClient = {
+const mockThreadFront = {
   stepIn: () =>
     new Promise(_resolve => {
       stepInResolve = _resolve;
     }),
   stepOver: () => new Promise(_resolve => _resolve),
   evaluate: async () => {},
   evaluateInFrame: async () => {},
   evaluateExpressions: async () => [],
@@ -66,18 +66,18 @@ const mockCommandClient = {
         case "foo-wasm/originalSource":
           return resolve({
             source: "fn fooBar() {}\nfn barZoo() { fooBar() }",
             contentType: "text/rust",
           });
       }
     });
   },
-  getSourceActorBreakpointPositions: async () => ({}),
-  getSourceActorBreakableLines: async () => [],
+  getBreakpointPositions: async () => ({}),
+  getBreakableLines: async () => [],
   actorID: "threadActorID",
 };
 
 const mockFrameId = "1";
 
 function createPauseInfo(
   frameLocation = { sourceId: "foo1", line: 2 },
   frameOpts = {}
@@ -99,17 +99,17 @@ function createPauseInfo(
     loadedObjects: [],
     why: makeWhyNormal(),
   };
 }
 
 describe("pause", () => {
   describe("stepping", () => {
     it("should set and clear the command", async () => {
-      const { dispatch, getState } = createStore(mockCommandClient);
+      const { dispatch, getState } = createStore(mockThreadFront);
       const mockPauseInfo = createPauseInfo();
 
       await dispatch(actions.newGeneratedSource(makeSource("foo1")));
       await dispatch(actions.paused(mockPauseInfo));
       const cx = selectors.getThreadContext(getState());
       const stepped = dispatch(actions.stepIn(cx));
       expect(isStepping(getState(), "FakeThread")).toBeTruthy();
       if (!stepInResolve) {
@@ -124,43 +124,43 @@ describe("pause", () => {
       const client = { stepIn: jest.fn() };
       const { dispatch, cx } = createStore(client);
 
       dispatch(actions.stepIn(cx));
       expect(client.stepIn.mock.calls).toHaveLength(0);
     });
 
     it("should step when paused", async () => {
-      const { dispatch, getState } = createStore(mockCommandClient);
+      const { dispatch, getState } = createStore(mockThreadFront);
       const mockPauseInfo = createPauseInfo();
 
       await dispatch(actions.newGeneratedSource(makeSource("foo1")));
       await dispatch(actions.paused(mockPauseInfo));
       const cx = selectors.getThreadContext(getState());
       dispatch(actions.stepIn(cx));
       expect(isStepping(getState(), "FakeThread")).toBeTruthy();
     });
 
     it("should step over when paused", async () => {
-      const store = createStore(mockCommandClient);
+      const store = createStore(mockThreadFront);
       const { dispatch, getState } = store;
       const mockPauseInfo = createPauseInfo();
 
       await dispatch(actions.newGeneratedSource(makeSource("foo1")));
       await dispatch(actions.paused(mockPauseInfo));
       const cx = selectors.getThreadContext(getState());
       const getNextStepSpy = jest.spyOn(parserWorker, "getNextStep");
       dispatch(actions.stepOver(cx));
       expect(getNextStepSpy).not.toHaveBeenCalled();
       expect(isStepping(getState(), "FakeThread")).toBeTruthy();
     });
 
     it("should step over when paused before an await", async () => {
       features.asyncStepping = true;
-      const store = createStore(mockCommandClient);
+      const store = createStore(mockThreadFront);
       const { dispatch, getState } = store;
       const mockPauseInfo = createPauseInfo({
         sourceId: "await",
         line: 2,
         column: 0,
       });
 
       await dispatch(actions.newGeneratedSource(makeSource("await")));
@@ -170,18 +170,18 @@ describe("pause", () => {
       const getNextStepSpy = jest.spyOn(parserWorker, "getNextStep");
       dispatch(actions.stepOver(cx));
       expect(getNextStepSpy).toHaveBeenCalled();
       getNextStepSpy.mockRestore();
     });
 
     it("should step over when paused after an await", async () => {
       const store = createStore({
-        ...mockCommandClient,
-        getSourceActorBreakpointPositions: async () => ({ [2]: [1] }),
+        ...mockThreadFront,
+        getBreakpointPositions: async () => ({ [2]: [1] }),
       });
       const { dispatch, getState } = store;
       const mockPauseInfo = createPauseInfo({
         sourceId: "await",
         line: 2,
         column: 6,
       });
 
@@ -197,17 +197,17 @@ describe("pause", () => {
 
     it("getting frame scopes with bindings", async () => {
       const generatedLocation = {
         sourceId: "foo",
         line: 1,
         column: 0,
       };
 
-      const store = createStore(mockCommandClient, {});
+      const store = createStore(mockThreadFront, {});
       const { dispatch, getState } = store;
       const mockPauseInfo = createPauseInfo(generatedLocation, {
         scope: {
           bindings: {
             variables: { b: { value: {} } },
             arguments: [{ a: { value: {} } }],
           },
         },
@@ -274,17 +274,17 @@ describe("pause", () => {
         getOriginalLocations: async items => items,
         getOriginalSourceText: async () => ({
           text: "\n\nfunction fooOriginal() {\n  return -5;\n}",
           contentType: "text/javascript",
         }),
         getGeneratedLocation: async location => location,
       };
 
-      const store = createStore(mockCommandClient, {}, sourceMapsMock);
+      const store = createStore(mockThreadFront, {}, sourceMapsMock);
       const { dispatch, getState } = store;
       const mockPauseInfo = createPauseInfo(generatedLocation);
 
       await dispatch(actions.newGeneratedSource(makeSource("foo")));
       await dispatch(actions.newGeneratedSource(makeSource("foo-original")));
 
       await dispatch(actions.paused(mockPauseInfo));
       expect(selectors.getFrames(getState(), "FakeThread")).toEqual([
@@ -335,17 +335,17 @@ describe("pause", () => {
         getOriginalLocations: async items => items,
         getOriginalSourceText: async () => ({
           text: "fn fooBar() {}\nfn barZoo() { fooBar() }",
           contentType: "text/rust",
         }),
         getGeneratedRangesForOriginal: async () => [],
       };
 
-      const store = createStore(mockCommandClient, {}, sourceMapsMock);
+      const store = createStore(mockThreadFront, {}, sourceMapsMock);
       const { dispatch, getState } = store;
       const mockPauseInfo = createPauseInfo(generatedLocation);
 
       const source = await dispatch(
         actions.newGeneratedSource(
           makeSource("foo-wasm", { introductionType: "wasm" })
         )
       );
@@ -378,41 +378,41 @@ describe("pause", () => {
           thread: "FakeThread",
         },
       ]);
     });
   });
 
   describe("resumed", () => {
     it("should not evaluate expression while stepping", async () => {
-      const client = { ...mockCommandClient, evaluateExpressions: jest.fn() };
+      const client = { ...mockThreadFront, evaluateExpressions: jest.fn() };
       const { dispatch, getState } = createStore(client);
       const mockPauseInfo = createPauseInfo();
 
       await dispatch(actions.newGeneratedSource(makeSource("foo1")));
       await dispatch(actions.paused(mockPauseInfo));
 
       const cx = selectors.getThreadContext(getState());
       dispatch(actions.stepIn(cx));
-      await dispatch(actions.resumed(mockCommandClient.actorID));
+      await dispatch(actions.resumed(mockThreadFront.actorID));
       expect(client.evaluateExpressions.mock.calls).toHaveLength(1);
     });
 
     it("resuming - will re-evaluate watch expressions", async () => {
-      const client = { ...mockCommandClient, evaluateExpressions: jest.fn() };
+      const client = { ...mockThreadFront, evaluateExpressions: jest.fn() };
       const store = createStore(client);
       const { dispatch, getState, cx } = store;
       const mockPauseInfo = createPauseInfo();
 
       await dispatch(actions.newGeneratedSource(makeSource("foo1")));
       await dispatch(actions.newGeneratedSource(makeSource("foo")));
       await dispatch(actions.addExpression(cx, "foo"));
       await waitForState(store, state => selectors.getExpression(state, "foo"));
 
       client.evaluateExpressions.mockReturnValue(Promise.resolve(["YAY"]));
       await dispatch(actions.paused(mockPauseInfo));
 
-      await dispatch(actions.resumed(mockCommandClient.actorID));
+      await dispatch(actions.resumed(mockThreadFront.actorID));
       const expression = selectors.getExpression(getState(), "foo");
       expect(expression && expression.value).toEqual("YAY");
     });
   });
 });
--- a/devtools/client/debugger/src/actions/source-actors.js
+++ b/devtools/client/debugger/src/actions/source-actors.js
@@ -1,27 +1,16 @@
 /* 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/>. */
 
 // @flow
 
 import type { ThunkArgs } from "./types";
-import {
-  getSourceActor,
-  getSourceActorBreakableLines,
-  getSourceActorBreakpointColumns,
-  type SourceActorId,
-  type SourceActor,
-} from "../reducers/source-actors";
-import {
-  memoizeableAction,
-  type MemoizedAction,
-} from "../utils/memoizableAction";
-import { PROMISE } from "./utils/middleware/promise";
+import type { SourceActor } from "../reducers/source-actors";
 
 export function insertSourceActor(item: SourceActor) {
   return insertSourceActors([item]);
 }
 export function insertSourceActors(items: Array<SourceActor>) {
   return function({ dispatch }: ThunkArgs) {
     dispatch({
       type: "INSERT_SOURCE_ACTORS",
@@ -36,53 +25,8 @@ export function removeSourceActor(item: 
 export function removeSourceActors(items: Array<SourceActor>) {
   return function({ dispatch }: ThunkArgs) {
     dispatch({
       type: "REMOVE_SOURCE_ACTORS",
       items,
     });
   };
 }
-
-export const loadSourceActorBreakpointColumns: MemoizedAction<
-  { id: SourceActorId, line: number },
-  Array<number>
-> = memoizeableAction("loadSourceActorBreakpointColumns", {
-  createKey: ({ id, line }) => `${id}:${line}`,
-  getValue: ({ id, line }, { getState }) =>
-    getSourceActorBreakpointColumns(getState(), id, line),
-  action: async ({ id, line }, { dispatch, getState, client }) => {
-    await dispatch({
-      type: "SET_SOURCE_ACTOR_BREAKPOINT_COLUMNS",
-      sourceId: id,
-      line,
-      [PROMISE]: (async () => {
-        const positions = await client.getSourceActorBreakpointPositions(
-          getSourceActor(getState(), id),
-          {
-            start: { line, column: 0 },
-            end: { line: line + 1, column: 0 },
-          }
-        );
-
-        return positions[line] || [];
-      })(),
-    });
-  },
-});
-
-export const loadSourceActorBreakableLines: MemoizedAction<
-  { id: SourceActorId },
-  Array<number>
-> = memoizeableAction("loadSourceActorBreakableLines", {
-  createKey: args => args.id,
-  getValue: ({ id }, { getState }) =>
-    getSourceActorBreakableLines(getState(), id),
-  action: async ({ id }, { dispatch, getState, client }) => {
-    await dispatch({
-      type: "SET_SOURCE_ACTOR_BREAKABLE_LINES",
-      sourceId: id,
-      [PROMISE]: client.getSourceActorBreakableLines(
-        getSourceActor(getState(), id)
-      ),
-    });
-  },
-});
--- a/devtools/client/debugger/src/actions/sources/breakableLines.js
+++ b/devtools/client/debugger/src/actions/sources/breakableLines.js
@@ -5,17 +5,16 @@
 // @flow
 
 import { isOriginalId } from "devtools-source-map";
 import { getSourceActorsForSource, getBreakableLines } from "../../selectors";
 import { setBreakpointPositions } from "../breakpoints/breakpointPositions";
 import { union } from "lodash";
 import type { Context } from "../../types";
 import type { ThunkArgs } from "../../actions/types";
-import { loadSourceActorBreakableLines } from "../source-actors";
 
 function calculateBreakableLines(positions) {
   const lines = [];
   for (const line in positions) {
     if (positions[line].length > 0) {
       lines.push(Number(line));
     }
   }
@@ -26,31 +25,27 @@ function calculateBreakableLines(positio
 export function setBreakableLines(cx: Context, sourceId: string) {
   return async ({ getState, dispatch, client }: ThunkArgs) => {
     let breakableLines;
     if (isOriginalId(sourceId)) {
       const positions = await dispatch(
         setBreakpointPositions({ cx, sourceId })
       );
       breakableLines = calculateBreakableLines(positions);
-
-      const existingBreakableLines = getBreakableLines(getState(), sourceId);
-      if (existingBreakableLines) {
-        breakableLines = union(existingBreakableLines, breakableLines);
-      }
-
-      dispatch({
-        type: "SET_ORIGINAL_BREAKABLE_LINES",
-        cx,
-        sourceId,
-        breakableLines,
-      });
     } else {
-      const actors = getSourceActorsForSource(getState(), sourceId);
-
-      await Promise.all(
-        actors.map(actor =>
-          dispatch(loadSourceActorBreakableLines({ id: actor.id }))
-        )
+      breakableLines = await client.getBreakableLines(
+        getSourceActorsForSource(getState(), sourceId)
       );
     }
+
+    const existingBreakableLines = getBreakableLines(getState(), sourceId);
+    if (existingBreakableLines) {
+      breakableLines = union(existingBreakableLines, breakableLines);
+    }
+
+    dispatch({
+      type: "SET_BREAKABLE_LINES",
+      cx,
+      sourceId,
+      breakableLines,
+    });
   };
 }
--- a/devtools/client/debugger/src/actions/sources/loadSourceText.js
+++ b/devtools/client/debugger/src/actions/sources/loadSourceText.js
@@ -14,17 +14,17 @@ import {
   getSourcesEpoch,
   getBreakpointsForSource,
   getSourceActorsForSource,
 } from "../../selectors";
 import { addBreakpoint } from "../breakpoints";
 
 import { prettyPrintSource } from "./prettyPrint";
 import { setBreakableLines } from "./breakableLines";
-import { isFulfilled, fulfilled } from "../../utils/async-value";
+import { isFulfilled } from "../../utils/async-value";
 
 import { isOriginal, isPretty } from "../../utils/source";
 import {
   memoizeableAction,
   type MemoizedAction,
 } from "../../utils/memoizableAction";
 
 import { Telemetry } from "devtools-modules";
@@ -119,44 +119,38 @@ async function loadSourceTextPromise(
 
     await dispatch(setBreakableLines(cx, source.id));
     // Update the text in any breakpoints for this source by re-adding them.
     const breakpoints = getBreakpointsForSource(getState(), source.id);
     for (const { location, options, disabled } of breakpoints) {
       await dispatch(addBreakpoint(cx, location, options, disabled));
     }
   }
+
+  return newSource;
 }
 
 export function loadSourceById(cx: Context, sourceId: string) {
   return ({ getState, dispatch }: ThunkArgs) => {
     const source = getSourceFromId(getState(), sourceId);
     return dispatch(loadSourceText({ cx, source }));
   };
 }
 
 export const loadSourceText: MemoizedAction<
   { cx: Context, source: Source },
   ?Source
 > = memoizeableAction("loadSourceText", {
-  getValue: ({ source }, { getState }) => {
-    source = source ? getSource(getState(), source.id) : null;
-    if (!source) {
-      return null;
-    }
-
-    const { content } = getSourceWithContent(getState(), source.id);
-    if (!content || content.state === "pending") {
-      return content;
-    }
-
-    // This currently swallows source-load-failure since we return fulfilled
-    // here when content.state === "rejected". In an ideal world we should
-    // propagate that error upward.
-    return fulfilled(source);
+  exitEarly: ({ source }) => !source,
+  hasValue: ({ source }, { getState }) => {
+    return !!(
+      getSource(getState(), source.id) &&
+      getSourceWithContent(getState(), source.id).content
+    );
   },
+  getValue: ({ source }, { getState }) => getSource(getState(), source.id),
   createKey: ({ source }, { getState }) => {
     const epoch = getSourcesEpoch(getState());
     return `${epoch}:${source.id}`;
   },
   action: ({ cx, source }, thunkArgs) =>
     loadSourceTextPromise(cx, source, thunkArgs),
 });
--- a/devtools/client/debugger/src/actions/sources/prettyPrint.js
+++ b/devtools/client/debugger/src/actions/sources/prettyPrint.js
@@ -61,17 +61,17 @@ export async function prettyPrintSource(
 }
 
 export function createPrettySource(cx: Context, sourceId: string) {
   return async ({ dispatch, getState, sourceMaps }: ThunkArgs) => {
     const source = getSourceFromId(getState(), sourceId);
     const url = getPrettySourceURL(source.url);
     const id = generatedToOriginalId(sourceId, url);
 
-    const prettySource = {
+    const prettySource: Source = {
       id,
       url,
       relativeUrl: url,
       isBlackBoxed: false,
       isPrettyPrinted: true,
       isWasm: false,
       introductionUrl: null,
       introductionType: undefined,
--- a/devtools/client/debugger/src/actions/sources/symbols.js
+++ b/devtools/client/debugger/src/actions/sources/symbols.js
@@ -1,25 +1,24 @@
 /* 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/>. */
 
 // @flow
 
-import { getSymbols } from "../../selectors";
+import { hasSymbols, getSymbols } from "../../selectors";
 
 import { PROMISE } from "../utils/middleware/promise";
 import { updateTab } from "../tabs";
 import { loadSourceText } from "./loadSourceText";
 
 import {
   memoizeableAction,
   type MemoizedAction,
 } from "../../utils/memoizableAction";
-import { fulfilled } from "../../utils/async-value";
 
 import type { Source, Context } from "../../types";
 import type { Symbols } from "../../reducers/types";
 
 async function doSetSymbols(cx, source, { dispatch, getState, parser }) {
   const sourceId = source.id;
 
   await dispatch(loadSourceText({ cx, source }));
@@ -30,31 +29,24 @@ async function doSetSymbols(cx, source, 
     sourceId,
     [PROMISE]: parser.getSymbols(sourceId),
   });
 
   const symbols = getSymbols(getState(), source);
   if (symbols && symbols.framework) {
     dispatch(updateTab(source, symbols.framework));
   }
+
+  return symbols;
 }
 
 type Args = { cx: Context, source: Source };
 
 export const setSymbols: MemoizedAction<Args, ?Symbols> = memoizeableAction(
   "setSymbols",
   {
-    getValue: ({ source }, { getState }) => {
-      if (source.isWasm) {
-        return fulfilled(null);
-      }
-
-      const symbols = getSymbols(getState(), source);
-      if (!symbols || symbols.loading) {
-        return null;
-      }
-
-      return fulfilled(symbols);
-    },
+    exitEarly: ({ source }) => source.isWasm,
+    hasValue: ({ source }, { getState }) => hasSymbols(getState(), source),
+    getValue: ({ source }, { getState }) => getSymbols(getState(), source),
     createKey: ({ source }) => source.id,
     action: ({ cx, source }, thunkArgs) => doSetSymbols(cx, source, thunkArgs),
   }
 );
--- a/devtools/client/debugger/src/actions/sources/tests/blackbox.spec.js
+++ b/devtools/client/debugger/src/actions/sources/tests/blackbox.spec.js
@@ -10,17 +10,17 @@ import {
   createStore,
   makeSource,
 } from "../../../utils/test-head";
 
 describe("blackbox", () => {
   it("should blackbox a source", async () => {
     const store = createStore({
       blackBox: async () => true,
-      getSourceActorBreakableLines: async () => [],
+      getBreakableLines: async () => [],
     });
     const { dispatch, getState, cx } = store;
 
     const foo1Source = await dispatch(
       actions.newGeneratedSource(makeSource("foo1"))
     );
     await dispatch(actions.toggleBlackBox(cx, foo1Source));
 
--- a/devtools/client/debugger/src/actions/sources/tests/loadSource.spec.js
+++ b/devtools/client/debugger/src/actions/sources/tests/loadSource.spec.js
@@ -9,24 +9,24 @@ import {
   selectors,
   watchForState,
   createStore,
   makeOriginalSource,
   makeSource,
 } from "../../../utils/test-head";
 import {
   createSource,
-  mockCommandClient,
-} from "../../tests/helpers/mockCommandClient";
+  sourceThreadFront,
+} from "../../tests/helpers/threadFront.js";
 import { getBreakpointsList } from "../../../selectors";
 import { isFulfilled, isRejected } from "../../../utils/async-value";
 
 describe("loadSourceText", () => {
   it("should load source text", async () => {
-    const store = createStore(mockCommandClient);
+    const store = createStore(sourceThreadFront);
     const { dispatch, getState, cx } = store;
 
     const foo1Source = await dispatch(
       actions.newGeneratedSource(makeSource("foo1"))
     );
     await dispatch(actions.loadSourceText({ cx, source: foo1Source }));
 
     const foo1Content = selectors.getSourceContent(getState(), foo1Source.id);
@@ -54,20 +54,20 @@ describe("loadSourceText", () => {
   });
 
   it("should update breakpoint text when a source loads", async () => {
     const fooOrigContent = createSource("fooOrig", "var fooOrig = 42;");
     const fooGenContent = createSource("fooGen", "var fooGen = 42;");
 
     const store = createStore(
       {
-        ...mockCommandClient,
+        ...sourceThreadFront,
         sourceContents: async () => fooGenContent,
-        getSourceActorBreakpointPositions: async () => ({ "1": [0] }),
-        getSourceActorBreakableLines: async () => [],
+        getBreakpointPositions: async () => ({ "1": [0] }),
+        getBreakableLines: async () => [],
       },
       {},
       {
         getGeneratedRangesForOriginal: async () => [
           { start: { line: 1, column: 0 }, end: { line: 1, column: 1 } },
         ],
         getOriginalLocations: async (sourceId, items) =>
           items.map(item => ({
@@ -151,18 +151,18 @@ describe("loadSourceText", () => {
     let resolve;
     let count = 0;
     const { dispatch, getState, cx } = createStore({
       sourceContents: () =>
         new Promise(r => {
           count++;
           resolve = r;
         }),
-      getSourceActorBreakpointPositions: async () => ({}),
-      getSourceActorBreakableLines: async () => [],
+      getBreakpointPositions: async () => ({}),
+      getBreakableLines: async () => [],
     });
     const id = "foo";
 
     await dispatch(actions.newGeneratedSource(makeSource(id)));
 
     let source = selectors.getSourceFromId(getState(), id);
     dispatch(actions.loadSourceText({ cx, source }));
 
@@ -189,18 +189,18 @@ describe("loadSourceText", () => {
     let resolve;
     let count = 0;
     const { dispatch, getState, cx } = createStore({
       sourceContents: () =>
         new Promise(r => {
           count++;
           resolve = r;
         }),
-      getSourceActorBreakpointPositions: async () => ({}),
-      getSourceActorBreakableLines: async () => [],
+      getBreakpointPositions: async () => ({}),
+      getBreakableLines: async () => [],
     });
     const id = "foo";
 
     await dispatch(actions.newGeneratedSource(makeSource(id)));
     let source = selectors.getSourceFromId(getState(), id);
     const loading = dispatch(actions.loadSourceText({ cx, source }));
 
     if (!resolve) {
@@ -218,49 +218,49 @@ describe("loadSourceText", () => {
       content &&
         isFulfilled(content) &&
         content.value.type === "text" &&
         content.value.value
     ).toEqual("yay");
   });
 
   it("should cache subsequent source text loads", async () => {
-    const { dispatch, getState, cx } = createStore(mockCommandClient);
+    const { dispatch, getState, cx } = createStore(sourceThreadFront);
 
     const source = await dispatch(
       actions.newGeneratedSource(makeSource("foo1"))
     );
     await dispatch(actions.loadSourceText({ cx, source }));
     const prevSource = selectors.getSourceFromId(getState(), "foo1");
 
     await dispatch(actions.loadSourceText({ cx, source: prevSource }));
     const curSource = selectors.getSource(getState(), "foo1");
 
     expect(prevSource === curSource).toBeTruthy();
   });
 
   it("should indicate a loading source", async () => {
-    const store = createStore(mockCommandClient);
+    const store = createStore(sourceThreadFront);
     const { dispatch, cx } = store;
 
     const source = await dispatch(
       actions.newGeneratedSource(makeSource("foo2"))
     );
 
     const wasLoading = watchForState(store, state => {
       return !selectors.getSourceContent(state, "foo2");
     });
 
     await dispatch(actions.loadSourceText({ cx, source }));
 
     expect(wasLoading()).toBe(true);
   });
 
   it("should indicate an errored source text", async () => {
-    const { dispatch, getState, cx } = createStore(mockCommandClient);
+    const { dispatch, getState, cx } = createStore(sourceThreadFront);
 
     const source = await dispatch(
       actions.newGeneratedSource(makeSource("bad-id"))
     );
     await dispatch(actions.loadSourceText({ cx, source }));
     const badSource = selectors.getSource(getState(), "bad-id");
 
     const content = badSource
--- a/devtools/client/debugger/src/actions/sources/tests/newSources.spec.js
+++ b/devtools/client/debugger/src/actions/sources/tests/newSources.spec.js
@@ -15,57 +15,58 @@ import {
 const {
   getSource,
   getSourceCount,
   getSelectedSource,
   getSourceByURL,
 } = selectors;
 import sourceQueue from "../../../utils/source-queue";
 
-import { mockCommandClient } from "../../tests/helpers/mockCommandClient";
+// eslint-disable-next-line max-len
+import { sourceThreadFront as threadFront } from "../../tests/helpers/threadFront.js";
 
 describe("sources - new sources", () => {
   it("should add sources to state", async () => {
-    const { dispatch, getState } = createStore(mockCommandClient);
+    const { dispatch, getState } = createStore(threadFront);
     await dispatch(actions.newGeneratedSource(makeSource("base.js")));
     await dispatch(actions.newGeneratedSource(makeSource("jquery.js")));
 
     expect(getSourceCount(getState())).toEqual(2);
     const base = getSource(getState(), "base.js");
     const jquery = getSource(getState(), "jquery.js");
     expect(base && base.id).toEqual("base.js");
     expect(jquery && jquery.id).toEqual("jquery.js");
   });
 
   it("should not add multiple identical sources", async () => {
-    const { dispatch, getState } = createStore(mockCommandClient);
+    const { dispatch, getState } = createStore(threadFront);
 
     await dispatch(actions.newGeneratedSource(makeSource("base.js")));
     await dispatch(actions.newGeneratedSource(makeSource("base.js")));
 
     expect(getSourceCount(getState())).toEqual(1);
   });
 
   it("should automatically select a pending source", async () => {
-    const { dispatch, getState, cx } = createStore(mockCommandClient);
+    const { dispatch, getState, cx } = createStore(threadFront);
     const baseSourceURL = makeSourceURL("base.js");
     await dispatch(actions.selectSourceURL(cx, baseSourceURL));
 
     expect(getSelectedSource(getState())).toBe(undefined);
     const baseSource = await dispatch(
       actions.newGeneratedSource(makeSource("base.js"))
     );
 
     const selected = getSelectedSource(getState());
     expect(selected && selected.url).toBe(baseSource.url);
   });
 
   it("should add original sources", async () => {
     const { dispatch, getState } = createStore(
-      mockCommandClient,
+      threadFront,
       {},
       {
         getOriginalURLs: async () => ["magic.js"],
         getOriginalLocations: async items => items,
       }
     );
 
     await dispatch(
@@ -76,32 +77,32 @@ describe("sources - new sources", () => 
     const magic = getSourceByURL(getState(), "magic.js");
     expect(magic && magic.url).toEqual("magic.js");
   });
 
   // eslint-disable-next-line
   it("should not attempt to fetch original sources if it's missing a source map url", async () => {
     const getOriginalURLs = jest.fn();
     const { dispatch } = createStore(
-      mockCommandClient,
+      threadFront,
       {},
       {
         getOriginalURLs,
         getOriginalLocations: async items => items,
       }
     );
 
     await dispatch(actions.newGeneratedSource(makeSource("base.js")));
     expect(getOriginalURLs).not.toHaveBeenCalled();
   });
 
   // eslint-disable-next-line
   it("should process new sources immediately, without waiting for source maps to be fetched first", async () => {
     const { dispatch, getState } = createStore(
-      mockCommandClient,
+      threadFront,
       {},
       {
         getOriginalURLs: async () => new Promise(_ => {}),
         getOriginalLocations: async items => items,
       }
     );
     await dispatch(
       actions.newGeneratedSource(
@@ -111,17 +112,17 @@ describe("sources - new sources", () => 
     expect(getSourceCount(getState())).toEqual(1);
     const base = getSource(getState(), "base.js");
     expect(base && base.id).toEqual("base.js");
   });
 
   // eslint-disable-next-line
   it("shouldn't let one slow loading source map delay all the other source maps", async () => {
     const dbg = createStore(
-      mockCommandClient,
+      threadFront,
       {},
       {
         getOriginalURLs: async source => {
           if (source.id == "foo.js") {
             // simulate a hang loading foo.js.map
             return new Promise(_ => {});
           }
 
@@ -147,17 +148,17 @@ describe("sources - new sources", () => 
     const bazzCljs = getSourceByURL(getState(), "bazz.cljs");
     expect(bazzCljs && bazzCljs.url).toEqual("bazz.cljs");
   });
 
   describe("sources - sources with querystrings", () => {
     it(`should find two sources when same source with
       querystring`, async () => {
       const { getSourcesUrlsInSources } = selectors;
-      const { dispatch, getState } = createStore(mockCommandClient);
+      const { dispatch, getState } = createStore(threadFront);
       await dispatch(actions.newGeneratedSource(makeSource("base.js?v=1")));
       await dispatch(actions.newGeneratedSource(makeSource("base.js?v=2")));
       await dispatch(actions.newGeneratedSource(makeSource("diff.js?v=1")));
 
       const base1 = "http://localhost:8000/examples/base.js?v=1";
       const diff1 = "http://localhost:8000/examples/diff.js?v=1";
       const diff2 = "http://localhost:8000/examples/diff.js?v=1";
 
--- a/devtools/client/debugger/src/actions/sources/tests/prettyPrint.spec.js
+++ b/devtools/client/debugger/src/actions/sources/tests/prettyPrint.spec.js
@@ -6,22 +6,22 @@
 
 import {
   actions,
   selectors,
   createStore,
   makeSource,
 } from "../../../utils/test-head";
 import { createPrettySource } from "../prettyPrint";
-import { mockCommandClient } from "../../tests/helpers/mockCommandClient";
+import { sourceThreadFront } from "../../tests/helpers/threadFront.js";
 import { isFulfilled } from "../../../utils/async-value";
 
 describe("sources - pretty print", () => {
   it("returns a pretty source for a minified file", async () => {
-    const { dispatch, getState, cx } = createStore(mockCommandClient);
+    const { dispatch, getState, cx } = createStore(sourceThreadFront);
 
     const url = "base.js";
     const source = await dispatch(actions.newGeneratedSource(makeSource(url)));
     await dispatch(actions.loadSourceText({ cx, source }));
 
     await dispatch(createPrettySource(cx, source.id));
 
     const prettyURL = `${source.url}:formatted`;
@@ -37,29 +37,29 @@ describe("sources - pretty print", () =>
         isFulfilled(content) &&
         content.value.type === "text" &&
         content.value.contentType
     ).toEqual("text/javascript");
     expect(content).toMatchSnapshot();
   });
 
   it("should create a source when first toggling pretty print", async () => {
-    const { dispatch, getState, cx } = createStore(mockCommandClient);
+    const { dispatch, getState, cx } = createStore(sourceThreadFront);
 
     const source = await dispatch(
       actions.newGeneratedSource(makeSource("foobar.js"))
     );
     await dispatch(actions.loadSourceText({ cx, source }));
 
     await dispatch(actions.togglePrettyPrint(cx, source.id));
     expect(selectors.getSourceCount(getState())).toEqual(2);
   });
 
   it("should not make a second source when toggling pretty print", async () => {
-    const { dispatch, getState, cx } = createStore(mockCommandClient);
+    const { dispatch, getState, cx } = createStore(sourceThreadFront);
 
     const source = await dispatch(
       actions.newGeneratedSource(makeSource("foobar.js"))
     );
     await dispatch(actions.loadSourceText({ cx, source }));
 
     await dispatch(actions.togglePrettyPrint(cx, source.id));
     expect(selectors.getSourceCount(getState())).toEqual(2);
--- a/devtools/client/debugger/src/actions/sources/tests/querystrings.spec.js
+++ b/devtools/client/debugger/src/actions/sources/tests/querystrings.spec.js
@@ -8,21 +8,21 @@ import {
   actions,
   selectors,
   createStore,
   makeSource,
 } from "../../../utils/test-head";
 const { getSourcesUrlsInSources } = selectors;
 
 // eslint-disable-next-line max-len
-import { mockCommandClient } from "../../tests/helpers/mockCommandClient";
+import { sourceThreadFront as threadFront } from "../../tests/helpers/threadFront.js";
 
 describe("sources - sources with querystrings", () => {
   it("should find two sources when same source with querystring", async () => {
-    const { dispatch, getState } = createStore(mockCommandClient);
+    const { dispatch, getState } = createStore(threadFront);
     await dispatch(actions.newGeneratedSource(makeSource("base.js?v=1")));
     await dispatch(actions.newGeneratedSource(makeSource("base.js?v=2")));
     await dispatch(actions.newGeneratedSource(makeSource("diff.js?v=1")));
 
     expect(
       getSourcesUrlsInSources(
         getState(),
         "http://localhost:8000/examples/base.js?v=1"
--- a/devtools/client/debugger/src/actions/sources/tests/select.spec.js
+++ b/devtools/client/debugger/src/actions/sources/tests/select.spec.js
@@ -18,29 +18,29 @@ import {
 const {
   getSource,
   getSourceCount,
   getSelectedSource,
   getSourceTabs,
   getSelectedLocation,
 } = selectors;
 
-import { mockCommandClient } from "../../tests/helpers/mockCommandClient";
+import { sourceThreadFront } from "../../tests/helpers/threadFront.js";
 
 process.on("unhandledRejection", (reason, p) => {});
 
 function initialLocation(sourceId) {
   return { sourceId, line: 1 };
 }
 
 describe("sources", () => {
   it("should select a source", async () => {
     // Note that we pass an empty client in because the action checks
     // if it exists.
-    const store = createStore(mockCommandClient);
+    const store = createStore(sourceThreadFront);
     const { dispatch, getState } = store;
 
     const frame = makeFrame({ id: "1", sourceId: "foo1" });
 
     await dispatch(actions.newGeneratedSource(makeSource("foo1")));
     await dispatch(
       actions.paused({
         thread: "FakeThread",
@@ -64,17 +64,17 @@ describe("sources", () => {
     const source = getSource(getState(), selectedSource.id);
     if (!source) {
       throw new Error("bad source");
     }
     expect(source.id).toEqual("foo1");
   });
 
   it("should select next tab on tab closed if no previous tab", async () => {
-    const { dispatch, getState, cx } = createStore(mockCommandClient);
+    const { dispatch, getState, cx } = createStore(sourceThreadFront);
 
     const fooSource = await dispatch(
       actions.newGeneratedSource(makeSource("foo.js"))
     );
     await dispatch(actions.newGeneratedSource(makeSource("bar.js")));
     await dispatch(actions.newGeneratedSource(makeSource("baz.js")));
 
     // 3rd tab
@@ -93,27 +93,27 @@ describe("sources", () => {
     await dispatch(actions.closeTab(cx, fooSource));
 
     const selected = getSelectedSource(getState());
     expect(selected && selected.id).toBe("bar.js");
     expect(getSourceTabs(getState())).toHaveLength(2);
   });
 
   it("should open a tab for the source", async () => {
-    const { dispatch, getState, cx } = createStore(mockCommandClient);
+    const { dispatch, getState, cx } = createStore(sourceThreadFront);
     await dispatch(actions.newGeneratedSource(makeSource("foo.js")));
     dispatch(actions.selectLocation(cx, initialLocation("foo.js")));
 
     const tabs = getSourceTabs(getState());
     expect(tabs).toHaveLength(1);
     expect(tabs[0].url).toEqual("http://localhost:8000/examples/foo.js");
   });
 
   it("should select previous tab on tab closed", async () => {
-    const { dispatch, getState, cx } = createStore(mockCommandClient);
+    const { dispatch, getState, cx } = createStore(sourceThreadFront);
     await dispatch(actions.newGeneratedSource(makeSource("foo.js")));
     await dispatch(actions.newGeneratedSource(makeSource("bar.js")));
 
     const bazSource = await dispatch(
       actions.newGeneratedSource(makeSource("baz.js"))
     );
 
     await dispatch(actions.selectLocation(cx, initialLocation("foo.js")));
@@ -122,17 +122,17 @@ describe("sources", () => {
     await dispatch(actions.closeTab(cx, bazSource));
 
     const selected = getSelectedSource(getState());
     expect(selected && selected.id).toBe("bar.js");
     expect(getSourceTabs(getState())).toHaveLength(2);
   });
 
   it("should keep the selected source when other tab closed", async () => {
-    const { dispatch, getState, cx } = createStore(mockCommandClient);
+    const { dispatch, getState, cx } = createStore(sourceThreadFront);
 
     await dispatch(actions.newGeneratedSource(makeSource("foo.js")));
     await dispatch(actions.newGeneratedSource(makeSource("bar.js")));
     const bazSource = await dispatch(
       actions.newGeneratedSource(makeSource("baz.js"))
     );
 
     // 3rd tab
@@ -149,32 +149,32 @@ describe("sources", () => {
     await dispatch(actions.closeTab(cx, bazSource));
 
     const selected = getSelectedSource(getState());
     expect(selected && selected.id).toBe("foo.js");
     expect(getSourceTabs(getState())).toHaveLength(2);
   });
 
   it("should not select new sources that lack a URL", async () => {
-    const { dispatch, getState } = createStore(mockCommandClient);
+    const { dispatch, getState } = createStore(sourceThreadFront);
 
     await dispatch(
       actions.newGeneratedSource({
         ...makeSource("foo"),
         url: "",
       })
     );
 
     expect(getSourceCount(getState())).toEqual(1);
     const selectedLocation = getSelectedLocation(getState());
     expect(selectedLocation).toEqual(undefined);
   });
 
   it("sets and clears selected location correctly", async () => {
-    const { dispatch, getState, cx } = createStore(mockCommandClient);
+    const { dispatch, getState, cx } = createStore(sourceThreadFront);
     const source = await dispatch(
       actions.newGeneratedSource(makeSource("testSource"))
     );
     const location = ({ test: "testLocation" }: any);
 
     // set value
     dispatch(actions.setSelectedLocation(cx, source, location));
     expect(getSelectedLocation(getState())).toEqual({
@@ -183,17 +183,17 @@ describe("sources", () => {
     });
 
     // clear value
     dispatch(actions.clearSelectedLocation(cx));
     expect(getSelectedLocation(getState())).toEqual(null);
   });
 
   it("sets and clears pending selected location correctly", () => {
-    const { dispatch, getState, cx } = createStore(mockCommandClient);
+    const { dispatch, getState, cx } = createStore(sourceThreadFront);
     const url = "testURL";
     const options = { location: { line: "testLine" } };
 
     // set value
     dispatch(actions.setPendingSelectedLocation(cx, url, options));
     const setResult = getState().sources.pendingSelectedLocation;
     expect(setResult).toEqual({
       url,
@@ -202,34 +202,34 @@ describe("sources", () => {
 
     // clear value
     dispatch(actions.clearSelectedLocation(cx));
     const clearResult = getState().sources.pendingSelectedLocation;
     expect(clearResult).toEqual({ url: "" });
   });
 
   it("should keep the generated the viewing context", async () => {
-    const store = createStore(mockCommandClient);
+    const store = createStore(sourceThreadFront);
     const { dispatch, getState, cx } = store;
     const baseSource = await dispatch(
       actions.newGeneratedSource(makeSource("base.js"))
     );
 
     await dispatch(
       actions.selectLocation(cx, { sourceId: baseSource.id, line: 1 })
     );
 
     const selected = getSelectedSource(getState());
     expect(selected && selected.id).toBe(baseSource.id);
     await waitForState(store, state => getSymbols(state, baseSource));
   });
 
   it("should keep the original the viewing context", async () => {
     const { dispatch, getState, cx } = createStore(
-      mockCommandClient,
+      sourceThreadFront,
       {},
       {
         getOriginalLocation: async location => ({ ...location, line: 12 }),
         getOriginalLocations: async items => items,
         getGeneratedLocation: async location => ({ ...location, line: 12 }),
         getOriginalSourceText: async () => ({ text: "" }),
         getGeneratedRangesForOriginal: async () => [],
       }
@@ -253,17 +253,17 @@ describe("sources", () => {
     );
 
     const selected = getSelectedLocation(getState());
     expect(selected && selected.line).toBe(12);
   });
 
   it("should change the original the viewing context", async () => {
     const { dispatch, getState, cx } = createStore(
-      mockCommandClient,
+      sourceThreadFront,
       {},
       {
         getOriginalLocation: async location => ({ ...location, line: 12 }),
         getOriginalLocations: async items => items,
         getGeneratedRangesForOriginal: async () => [],
         getOriginalSourceText: async () => ({ text: "" }),
       }
     );
@@ -285,17 +285,17 @@ describe("sources", () => {
     );
 
     const selected = getSelectedLocation(getState());
     expect(selected && selected.line).toBe(1);
   });
 
   describe("selectSourceURL", () => {
     it("should automatically select a pending source", async () => {
-      const { dispatch, getState, cx } = createStore(mockCommandClient);
+      const { dispatch, getState, cx } = createStore(sourceThreadFront);
       const baseSourceURL = makeSourceURL("base.js");
       await dispatch(actions.selectSourceURL(cx, baseSourceURL));
 
       expect(getSelectedSource(getState())).toBe(undefined);
       const baseSource = await dispatch(
         actions.newGeneratedSource(makeSource("base.js"))
       );
 
--- a/devtools/client/debugger/src/actions/tests/ast.spec.js
+++ b/devtools/client/debugger/src/actions/tests/ast.spec.js
@@ -12,27 +12,27 @@ import {
   makeSource,
   makeOriginalSource,
   waitForState,
 } from "../../utils/test-head";
 
 import readFixture from "./helpers/readFixture";
 const { getSymbols, isSymbolsLoading, getFramework } = selectors;
 
-const mockCommandClient = {
+const threadFront = {
   sourceContents: async ({ source }) => ({
     source: sourceTexts[source],
     contentType: "text/javascript",
   }),
   getFrameScopes: async () => {},
   evaluate: async expression => ({ result: evaluationResult[expression] }),
   evaluateExpressions: async expressions =>
     expressions.map(expression => ({ result: evaluationResult[expression] })),
-  getSourceActorBreakpointPositions: async () => ({}),
-  getSourceActorBreakableLines: async () => [],
+  getBreakpointPositions: async () => ({}),
+  getBreakableLines: async () => [],
 };
 
 const sourceMaps = {
   getOriginalSourceText: async ({ id }) => ({
     id,
     text: sourceTexts[id],
     contentType: "text/javascript",
   }),
@@ -51,17 +51,17 @@ const evaluationResult = {
   "this.bazz": { actor: "bazz", preview: {} },
   this: { actor: "this", preview: {} },
 };
 
 describe("ast", () => {
   describe("setSymbols", () => {
     describe("when the source is loaded", () => {
       it("should be able to set symbols", async () => {
-        const store = createStore(mockCommandClient);
+        const store = createStore(threadFront);
         const { dispatch, getState, cx } = store;
         const base = await dispatch(
           actions.newGeneratedSource(makeSource("base.js"))
         );
         await dispatch(actions.loadSourceText({ cx, source: base }));
 
         const loadedSource = selectors.getSourceFromId(getState(), base.id);
         await dispatch(actions.setSymbols({ cx, source: loadedSource }));
@@ -69,37 +69,37 @@ describe("ast", () => {
 
         const baseSymbols = getSymbols(getState(), base);
         expect(baseSymbols).toMatchSnapshot();
       });
     });
 
     describe("when the source is not loaded", () => {
       it("should return null", async () => {
-        const { getState, dispatch } = createStore(mockCommandClient);
+        const { getState, dispatch } = createStore(threadFront);
         const base = await dispatch(
           actions.newGeneratedSource(makeSource("base.js"))
         );
 
         const baseSymbols = getSymbols(getState(), base);
         expect(baseSymbols).toEqual(null);
       });
     });
 
     describe("when there is no source", () => {
       it("should return null", async () => {
-        const { getState } = createStore(mockCommandClient);
+        const { getState } = createStore(threadFront);
         const baseSymbols = getSymbols(getState());
         expect(baseSymbols).toEqual(null);
       });
     });
 
     describe("frameworks", () => {
       it("should detect react components", async () => {
-        const store = createStore(mockCommandClient, {}, sourceMaps);
+        const store = createStore(threadFront, {}, sourceMaps);
         const { cx, dispatch, getState } = store;
 
         const genSource = await dispatch(
           actions.newGeneratedSource(makeSource("reactComponent.js"))
         );
 
         const source = await dispatch(
           actions.newOriginalSource(makeOriginalSource(genSource))
@@ -108,17 +108,17 @@ describe("ast", () => {
         await dispatch(actions.loadSourceText({ cx, source }));
         const loadedSource = selectors.getSourceFromId(getState(), source.id);
         await dispatch(actions.setSymbols({ cx, source: loadedSource }));
 
         expect(getFramework(getState(), source)).toBe("React");
       });
 
       it("should not give false positive on non react components", async () => {
-        const store = createStore(mockCommandClient);
+        const store = createStore(threadFront);
         const { cx, dispatch, getState } = store;
         const base = await dispatch(
           actions.newGeneratedSource(makeSource("base.js"))
         );
         await dispatch(actions.loadSourceText({ cx, source: base }));
         await dispatch(actions.setSymbols({ cx, source: base }));
 
         expect(getFramework(getState(), base)).toBe(undefined);
--- a/devtools/client/debugger/src/actions/tests/expressions.spec.js
+++ b/devtools/client/debugger/src/actions/tests/expressions.spec.js
@@ -32,18 +32,18 @@ const mockThreadFront = {
             } else {
               resolve("boo");
             }
           })
       )
     ),
   getFrameScopes: async () => {},
   sourceContents: () => ({ source: "", contentType: "text/javascript" }),
-  getSourceActorBreakpointPositions: async () => ({}),
-  getSourceActorBreakableLines: async () => [],
+  getBreakpointPositions: async () => ({}),
+  getBreakableLines: async () => [],
   autocomplete: () => {
     return new Promise(resolve => {
       resolve({
         from: "foo",
         matches: ["toLocaleString", "toSource", "toString", "toolbar", "top"],
         matchProp: "to",
       });
     });
rename from devtools/client/debugger/src/actions/tests/helpers/mockCommandClient.js
rename to devtools/client/debugger/src/actions/tests/helpers/threadFront.js
--- a/devtools/client/debugger/src/actions/tests/helpers/mockCommandClient.js
+++ b/devtools/client/debugger/src/actions/tests/helpers/threadFront.js
@@ -1,15 +1,19 @@
 /* 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/>. */
 
 // @flow
 
-import type { SourceActor } from "../../../types";
+import type {
+  SourceActor,
+  SourceActorLocation,
+  BreakpointOptions,
+} from "../../../types";
 
 export function createSource(name: string, code?: string) {
   name = name.replace(/\..*$/, "");
   return {
     source: code || `function ${name}() {\n  return ${name} \n}`,
     contentType: "text/javascript",
   };
 }
@@ -27,28 +31,53 @@ const sources = [
   "barfoo.js",
   "foo.js",
   "bar.js",
   "base.js",
   "bazz.js",
   "jquery.js",
 ];
 
-export const mockCommandClient = {
+export const simpleMockThreadFront = {
+  getBreakpointByLocation: (jest.fn(): any),
+  setBreakpoint: (location: SourceActorLocation, _condition: string) =>
+    Promise.resolve({ id: "hi", actualLocation: location }),
+
+  removeBreakpoint: (_id: string) => Promise.resolve(),
+
+  setBreakpointOptions: (
+    _id: string,
+    _location: SourceActorLocation,
+    _options: BreakpointOptions,
+    _noSliding: boolean
+  ) => Promise.resolve({ sourceId: "a", line: 5 }),
+  sourceContents: ({
+    source,
+  }: SourceActor): Promise<{| source: any, contentType: ?string |}> =>
+    new Promise((resolve, reject) => {
+      if (sources.includes(source)) {
+        resolve(createSource(source));
+      }
+
+      reject(`unknown source: ${source}`);
+    }),
+};
+
+// sources and tabs
+export const sourceThreadFront = {
   sourceContents: function({
     source,
   }: SourceActor): Promise<{| source: any, contentType: ?string |}> {
     return new Promise((resolve, reject) => {
       if (sources.includes(source)) {
         resolve(createSource(source));
       }
 
       reject(`unknown source: ${source}`);
     });
   },
   setBreakpoint: async () => {},
-  removeBreakpoint: (_id: string) => Promise.resolve(),
   threadFront: async () => {},
   getFrameScopes: async () => {},
   evaluateExpressions: async () => {},
-  getSourceActorBreakpointPositions: async () => ({}),
-  getSourceActorBreakableLines: async () => [],
+  getBreakpointPositions: async () => ({}),
+  getBreakableLines: async () => [],
 };
--- a/devtools/client/debugger/src/actions/tests/navigation.spec.js
+++ b/devtools/client/debugger/src/actions/tests/navigation.spec.js
@@ -22,18 +22,18 @@ const {
   getFileSearchResults,
 } = selectors;
 
 const threadFront = {
   sourceContents: async () => ({
     source: "function foo1() {\n  const foo = 5; return foo;\n}",
     contentType: "text/javascript",
   }),
-  getSourceActorBreakpointPositions: async () => ({}),
-  getSourceActorBreakableLines: async () => [],
+  getBreakpointPositions: async () => ({}),
+  getBreakableLines: async () => [],
   detachWorkers: () => {},
 };
 
 describe("navigation", () => {
   it("connect sets the debuggeeUrl", async () => {
     const { dispatch, getState } = createStore({
       fetchWorkers: () => Promise.resolve([]),
       getMainThread: () => "FakeThread",
--- a/devtools/client/debugger/src/actions/tests/pending-breakpoints.spec.js
+++ b/devtools/client/debugger/src/actions/tests/pending-breakpoints.spec.js
@@ -5,17 +5,17 @@
 // @flow
 
 // TODO: we would like to mock this in the local tests
 import {
   generateBreakpoint,
   mockPendingBreakpoint,
 } from "./helpers/breakpoints.js";
 
-import { mockCommandClient } from "./helpers/mockCommandClient";
+import { simpleMockThreadFront } from "./helpers/threadFront.js";
 
 import { asyncStore } from "../../utils/prefs";
 
 function loadInitialState(opts = {}) {
   const mockedPendingBreakpoint = mockPendingBreakpoint({ ...opts, column: 2 });
   const id = makePendingLocationId(mockedPendingBreakpoint.location);
   asyncStore.pendingBreakpoints = { [id]: mockedPendingBreakpoint };
 
@@ -42,20 +42,20 @@ import {
   waitForState,
 } from "../../utils/test-head";
 
 import sourceMaps from "devtools-source-map";
 
 import { makePendingLocationId } from "../../utils/breakpoint";
 function mockClient(bpPos = {}) {
   return {
-    ...mockCommandClient,
+    ...simpleMockThreadFront,
 
-    getSourceActorBreakpointPositions: async () => bpPos,
-    getSourceActorBreakableLines: async () => [],
+    getBreakpointPositions: async () => bpPos,
+    getBreakableLines: async () => [],
   };
 }
 
 function mockSourceMaps() {
   return {
     ...sourceMaps,
     getOriginalSourceText: async source => ({
       id: source.id,
--- a/devtools/client/debugger/src/actions/tests/preview.spec.js
+++ b/devtools/client/debugger/src/actions/tests/preview.spec.js
@@ -26,18 +26,18 @@ function waitForPreview(store, expressio
 function mockThreadFront(overrides) {
   return {
     evaluateInFrame: async () => ({ result: {} }),
     getFrameScopes: async () => {},
     sourceContents: async () => ({
       source: "",
       contentType: "text/javascript",
     }),
-    getSourceActorBreakpointPositions: async () => ({}),
-    getSourceActorBreakableLines: async () => [],
+    getBreakpointPositions: async () => ({}),
+    getBreakableLines: async () => [],
     evaluateExpressions: async () => [],
     loadObjectProperties: async () => ({}),
     ...overrides,
   };
 }
 
 function dispatchSetPreview(dispatch, context, expression, target) {
   return dispatch(
--- a/devtools/client/debugger/src/actions/tests/project-text-search.spec.js
+++ b/devtools/client/debugger/src/actions/tests/project-text-search.spec.js
@@ -34,18 +34,18 @@ const sources = {
   "bar:formatted": {
     source: "function bla(x, y) {\n const bar = 4; return 2;\n}",
     contentType: "text/javascript",
   },
 };
 
 const threadFront = {
   sourceContents: async ({ source }) => sources[source],
-  getSourceActorBreakpointPositions: async () => ({}),
-  getSourceActorBreakableLines: async () => [],
+  getBreakpointPositions: async () => ({}),
+  getBreakableLines: async () => [],
 };
 
 describe("project text search", () => {
   it("should add a project text search query", () => {
     const { dispatch, getState, cx } = createStore();
     const mockQuery = "foo";
 
     dispatch(actions.addSearchQuery(cx, mockQuery));
--- a/devtools/client/debugger/src/actions/tests/setProjectDirectoryRoot.spec.js
+++ b/devtools/client/debugger/src/actions/tests/setProjectDirectoryRoot.spec.js
@@ -38,17 +38,17 @@ describe("setProjectDirectoryRoot", () =
     dispatch(actions.setProjectDirectoryRoot(cx, "/example.com/foo"));
     dispatch(actions.clearProjectDirectoryRoot(cx));
     dispatch(actions.setProjectDirectoryRoot(cx, "/example.com/bar"));
     expect(getProjectDirectoryRoot(getState())).toBe("/example.com/bar");
   });
 
   it("should filter sources", async () => {
     const store = createStore({
-      getSourceActorBreakableLines: async () => [],
+      getBreakableLines: async () => [],
     });
     const { dispatch, getState, cx } = store;
     await dispatch(actions.newGeneratedSource(makeSource("js/scopes.js")));
     await dispatch(actions.newGeneratedSource(makeSource("lib/vendor.js")));
 
     dispatch(actions.setProjectDirectoryRoot(cx, "localhost:8000/examples/js"));
 
     const filteredSourcesByThread = getDisplayedSources(getState());
@@ -60,24 +60,24 @@ describe("setProjectDirectoryRoot", () =
       "http://localhost:8000/examples/js/scopes.js"
     );
 
     expect(filteredSources.relativeUrl).toEqual("scopes.js");
   });
 
   it("should update the child directory ", () => {
     const { dispatch, getState, cx } = createStore({
-      getSourceActorBreakableLines: async () => [],
+      getBreakableLines: async () => [],
     });
     dispatch(actions.setProjectDirectoryRoot(cx, "example.com"));
     dispatch(actions.setProjectDirectoryRoot(cx, "example.com/foo/bar"));
     expect(getProjectDirectoryRoot(getState())).toBe("example.com/foo/bar");
   });
 
   it("should update the child directory when domain name is Webpack://", () => {
     const { dispatch, getState, cx } = createStore({
-      getSourceActorBreakableLines: async () => [],
+      getBreakableLines: async () => [],
     });
     dispatch(actions.setProjectDirectoryRoot(cx, "webpack://"));
     dispatch(actions.setProjectDirectoryRoot(cx, "webpack:///app"));
     expect(getProjectDirectoryRoot(getState())).toBe("webpack:///app");
   });
 });
--- a/devtools/client/debugger/src/actions/tests/tabs.spec.js
+++ b/devtools/client/debugger/src/actions/tests/tabs.spec.js
@@ -7,79 +7,79 @@
 import {
   actions,
   selectors,
   createStore,
   makeSource,
 } from "../../utils/test-head";
 const { getSelectedSource, getSourceTabs } = selectors;
 
-import { mockCommandClient } from "./helpers/mockCommandClient";
+import { sourceThreadFront as threadFront } from "./helpers/threadFront.js";
 
 describe("closing tabs", () => {
   it("closing a tab", async () => {
-    const { dispatch, getState, cx } = createStore(mockCommandClient);
+    const { dispatch, getState, cx } = createStore(threadFront);
 
     const fooSource = await dispatch(
       actions.newGeneratedSource(makeSource("foo.js"))
     );
     await dispatch(actions.selectLocation(cx, { sourceId: "foo.js", line: 1 }));
     dispatch(actions.closeTab(cx, fooSource));
 
     expect(getSelectedSource(getState())).toBe(undefined);
     expect(getSourceTabs(getState())).toHaveLength(0);
   });
 
   it("closing the inactive tab", async () => {
-    const { dispatch, getState, cx } = createStore(mockCommandClient);
+    const { dispatch, getState, cx } = createStore(threadFront);
 
     const fooSource = await dispatch(
       actions.newGeneratedSource(makeSource("foo.js"))
     );
     await dispatch(actions.newGeneratedSource(makeSource("bar.js")));
     await dispatch(actions.selectLocation(cx, { sourceId: "foo.js", line: 1 }));
     await dispatch(actions.selectLocation(cx, { sourceId: "bar.js", line: 1 }));
     dispatch(actions.closeTab(cx, fooSource));
 
     const selected = getSelectedSource(getState());
     expect(selected && selected.id).toBe("bar.js");
     expect(getSourceTabs(getState())).toHaveLength(1);
   });
 
   it("closing the only tab", async () => {
-    const { dispatch, getState, cx } = createStore(mockCommandClient);
+    const { dispatch, getState, cx } = createStore(threadFront);
 
     const fooSource = await dispatch(
       actions.newGeneratedSource(makeSource("foo.js"))
     );
     await dispatch(actions.selectLocation(cx, { sourceId: "foo.js", line: 1 }));
     dispatch(actions.closeTab(cx, fooSource));
 
     expect(getSelectedSource(getState())).toBe(undefined);
     expect(getSourceTabs(getState())).toHaveLength(0);
   });
 
   it("closing the active tab", async () => {
-    const { dispatch, getState, cx } = createStore(mockCommandClient);
+    const { dispatch, getState, cx } = createStore(threadFront);
 
     await dispatch(actions.newGeneratedSource(makeSource("foo.js")));
     const barSource = await dispatch(
       actions.newGeneratedSource(makeSource("bar.js"))
     );
     await dispatch(actions.selectLocation(cx, { sourceId: "foo.js", line: 1 }));
     await dispatch(actions.selectLocation(cx, { sourceId: "bar.js", line: 1 }));
     await dispatch(actions.closeTab(cx, barSource));
 
     const selected = getSelectedSource(getState());
     expect(selected && selected.id).toBe("foo.js");
     expect(getSourceTabs(getState())).toHaveLength(1);
   });
 
   it("closing many inactive tabs", async () => {
-    const { dispatch, getState, cx } = createStore(mockCommandClient);
+    const { dispatch, getState, cx } = createStore(threadFront);
 
     await dispatch(actions.newGeneratedSource(makeSource("foo.js")));
     await dispatch(actions.newGeneratedSource(makeSource("bar.js")));
     await dispatch(actions.newGeneratedSource(makeSource("bazz.js")));
     await dispatch(actions.selectLocation(cx, { sourceId: "foo.js", line: 1 }));
     await dispatch(actions.selectLocation(cx, { sourceId: "bar.js", line: 1 }));
     await dispatch(
       actions.selectLocation(cx, { sourceId: "bazz.js", line: 1 })
@@ -92,17 +92,17 @@ describe("closing tabs", () => {
     dispatch(actions.closeTabs(cx, tabs));
 
     const selected = getSelectedSource(getState());
     expect(selected && selected.id).toBe("bazz.js");
     expect(getSourceTabs(getState())).toHaveLength(1);
   });
 
   it("closing many tabs including the active tab", async () => {
-    const { dispatch, getState, cx } = createStore(mockCommandClient);
+    const { dispatch, getState, cx } = createStore(threadFront);
 
     await dispatch(actions.newGeneratedSource(makeSource("foo.js")));
     await dispatch(actions.newGeneratedSource(makeSource("bar.js")));
     await dispatch(actions.newGeneratedSource(makeSource("bazz.js")));
     await dispatch(actions.selectLocation(cx, { sourceId: "foo.js", line: 1 }));
     await dispatch(actions.selectLocation(cx, { sourceId: "bar.js", line: 1 }));
     await dispatch(
       actions.selectLocation(cx, { sourceId: "bazz.js", line: 1 })
@@ -114,17 +114,17 @@ describe("closing tabs", () => {
     await dispatch(actions.closeTabs(cx, tabs));
 
     const selected = getSelectedSource(getState());
     expect(selected && selected.id).toBe("foo.js");
     expect(getSourceTabs(getState())).toHaveLength(1);
   });
 
   it("closing all the tabs", async () => {
-    const { dispatch, getState, cx } = createStore(mockCommandClient);
+    const { dispatch, getState, cx } = createStore(threadFront);
 
     await dispatch(actions.newGeneratedSource(makeSource("foo.js")));
     await dispatch(actions.newGeneratedSource(makeSource("bar.js")));
     await dispatch(actions.selectLocation(cx, { sourceId: "foo.js", line: 1 }));
     await dispatch(actions.selectLocation(cx, { sourceId: "bar.js", line: 1 }));
     await dispatch(
       actions.closeTabs(cx, [
         "http://localhost:8000/examples/foo.js",
--- a/devtools/client/debugger/src/actions/types/SourceAction.js
+++ b/devtools/client/debugger/src/actions/types/SourceAction.js
@@ -1,17 +1,16 @@
 /* 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/>. */
 
 // @flow
 
 import type { SourceId, Source, SourceLocation, Context } from "../../types";
 import type { PromiseAction } from "../utils/middleware/promise";
-import type { SourceBase } from "../../reducers/sources";
 
 export type LoadSourceAction = PromiseAction<
   {|
     +type: "LOAD_SOURCE_TEXT",
     +cx: Context,
     +sourceId: string,
     +epoch: number,
   |},
@@ -20,22 +19,22 @@ export type LoadSourceAction = PromiseAc
     contentType: string | void,
   }
 >;
 export type SourceAction =
   | LoadSourceAction
   | {|
       +type: "ADD_SOURCE",
       +cx: Context,
-      +source: SourceBase,
+      +source: Source,
     |}
   | {|
       +type: "ADD_SOURCES",
       +cx: Context,
-      +sources: Array<SourceBase>,
+      +sources: Array<Source>,
     |}
   | {|
       +type: "CLEAR_SOURCE_MAP_URL",
       +cx: Context,
       +sourceId: SourceId,
     |}
   | {|
       +type: "SET_SELECTED_LOCATION",
@@ -72,13 +71,13 @@ export type SourceAction =
       +tabs: any,
     |}
   | {|
       +type: "CLOSE_TABS",
       +sources: Array<Source>,
       +tabs: any,
     |}
   | {|
-      type: "SET_ORIGINAL_BREAKABLE_LINES",
+      type: "SET_BREAKABLE_LINES",
       +cx: Context,
       breakableLines: number[],
       sourceId: string,
     |};
--- a/devtools/client/debugger/src/actions/types/SourceActorAction.js
+++ b/devtools/client/debugger/src/actions/types/SourceActorAction.js
@@ -1,43 +1,20 @@
 /* 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/>. */
 
 // @flow
 
-import { type PromiseAction } from "../utils/middleware/promise";
-import type {
-  SourceActorId,
-  SourceActor,
-} from "../../reducers/source-actors.js";
+import type { SourceActor } from "../../reducers/source-actors.js";
 
 export type SourceActorsInsertAction = {|
   type: "INSERT_SOURCE_ACTORS",
   items: Array<SourceActor>,
 |};
 export type SourceActorsRemoveAction = {|
   type: "REMOVE_SOURCE_ACTORS",
   items: Array<SourceActor>,
 |};
 
-export type SourceActorBreakpointColumnsAction = PromiseAction<
-  {|
-    type: "SET_SOURCE_ACTOR_BREAKPOINT_COLUMNS",
-    sourceId: SourceActorId,
-    line: number,
-  |},
-  Array<number>
->;
-
-export type SourceActorBreakableLinesAction = PromiseAction<
-  {|
-    type: "SET_SOURCE_ACTOR_BREAKABLE_LINES",
-    sourceId: SourceActorId,
-  |},
-  Array<number>
->;
-
 export type SourceActorAction =
   | SourceActorsInsertAction
-  | SourceActorsRemoveAction
-  | SourceActorBreakpointColumnsAction
-  | SourceActorBreakableLinesAction;
+  | SourceActorsRemoveAction;
--- a/devtools/client/debugger/src/actions/utils/middleware/promise.js
+++ b/devtools/client/debugger/src/actions/utils/middleware/promise.js
@@ -1,16 +1,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/>. */
 
 // @flow
 
 import { fromPairs, toPairs } from "lodash";
 import { executeSoon } from "../../../utils/DevToolsUtils";
+
 import type { ThunkArgs } from "../../types";
 
 type BasePromiseAction = {|
   +"@@dispatch/promise": Promise<mixed>,
 |};
 
 export type StartPromiseAction = {|
   ...BasePromiseAction,
@@ -24,35 +25,17 @@ export type DonePromiseAction = {|
 |};
 
 export type ErrorPromiseAction = {|
   ...BasePromiseAction,
   +status: "error",
   +error: any,
 |};
 
-import {
-  pending,
-  rejected,
-  fulfilled,
-  type AsyncValue,
-} from "../../../utils/async-value";
-export function asyncActionAsValue<T>(
-  action: PromiseAction<mixed, T>
-): AsyncValue<T> {
-  if (action.status === "start") {
-    return pending();
-  }
-  if (action.status === "error") {
-    return rejected(action.error);
-  }
-  return fulfilled(action.value);
-}
-
-export type PromiseAction<+Action, Value = any> =
+export type PromiseAction<Action, Value = any> =
   // | {| ...Action, "@@dispatch/promise": Promise<Object> |}
   | {|
       ...BasePromiseAction,
       ...Action,
       +status: "start",
       value: void,
     |}
   | {|
--- a/devtools/client/debugger/src/client/firefox/commands.js
+++ b/devtools/client/debugger/src/client/firefox/commands.js
@@ -433,48 +433,65 @@ async function fetchWorkers(): Promise<W
   targets = newTargets;
   return Object.keys(targets).map(id => createTarget(id, targets[id]));
 }
 
 function getMainThread() {
   return currentThreadFront.actor;
 }
 
-async function getSourceActorBreakpointPositions(
-  { thread, actor }: SourceActor,
-  range: Range
-): Promise<{ [number]: number[] }> {
-  const sourceThreadFront = lookupThreadFront(thread);
-  const sourceFront = sourceThreadFront.source({ actor });
-  return sourceFront.getBreakpointPositionsCompressed(range);
+async function getBreakpointPositions(
+  actors: Array<SourceActor>,
+  range: ?Range
+): Promise<{ [string]: number[] }> {
+  const sourcePositions = {};
+
+  for (const { thread, actor } of actors) {
+    const sourceThreadFront = lookupThreadFront(thread);
+    const sourceFront = sourceThreadFront.source({ actor });
+    const positions = await sourceFront.getBreakpointPositionsCompressed(range);
+
+    for (const line of Object.keys(positions)) {
+      let columns = positions[line];
+      const existing = sourcePositions[line];
+      if (existing) {
+        columns = [...new Set([...existing, ...columns])];
+      }
+
+      sourcePositions[line] = columns;
+    }
+  }
+  return sourcePositions;
 }
 
-async function getSourceActorBreakableLines({
-  thread,
-  actor,
-}: SourceActor): Promise<Array<number>> {
-  const sourceThreadFront = lookupThreadFront(thread);
-  const sourceFront = sourceThreadFront.source({ actor });
-  let actorLines = [];
-  try {
-    actorLines = await sourceFront.getBreakableLines();
-  } catch (e) {
-    // Handle backward compatibility
-    if (
-      e.message &&
-      e.message.match(/does not recognize the packet type getBreakableLines/)
-    ) {
-      const pos = await sourceFront.getBreakpointPositionsCompressed();
-      actorLines = Object.keys(pos).map(line => Number(line));
-    } else if (!e.message || !e.message.match(/Connection closed/)) {
-      throw e;
+async function getBreakableLines(actors: Array<SourceActor>) {
+  let lines = [];
+  for (const { thread, actor } of actors) {
+    const sourceThreadFront = lookupThreadFront(thread);
+    const sourceFront = sourceThreadFront.source({ actor });
+    let actorLines = [];
+    try {
+      actorLines = await sourceFront.getBreakableLines();
+    } catch (e) {
+      // Handle backward compatibility
+      if (
+        e.message &&
+        e.message.match(/does not recognize the packet type getBreakableLines/)
+      ) {
+        const pos = await sourceFront.getBreakpointPositionsCompressed();
+        actorLines = Object.keys(pos).map(line => Number(line));
+      } else if (!e.message || !e.message.match(/Connection closed/)) {
+        throw e;
+      }
     }
+
+    lines = [...new Set([...lines, ...actorLines])];
   }
 
-  return actorLines;
+  return lines;
 }
 
 const clientCommands = {
   autocomplete,
   blackBox,
   createObjectClient,
   loadObjectProperties,
   releaseActor,
@@ -484,18 +501,18 @@ const clientCommands = {
   stepIn,
   stepOut,
   stepOver,
   rewind,
   reverseStepOver,
   breakOnNext,
   sourceContents,
   getSourceForActor,
-  getSourceActorBreakpointPositions,
-  getSourceActorBreakableLines,
+  getBreakpointPositions,
+  getBreakableLines,
   hasBreakpoint,
   setBreakpoint,
   setXHRBreakpoint,
   removeXHRBreakpoint,
   removeBreakpoint,
   evaluate,
   evaluateInFrame,
   evaluateExpressions,
--- a/devtools/client/debugger/src/components/Editor/EditorMenu.js
+++ b/devtools/client/debugger/src/components/Editor/EditorMenu.js
@@ -25,53 +25,53 @@ import type SourceEditor from "../../uti
 type Props = {
   cx: ThreadContext,
   contextMenu: ?MouseEvent,
   editorActions: EditorItemActions,
   clearContextMenu: () => void,
   editor: SourceEditor,
   hasPrettySource: boolean,
   isPaused: boolean,
-  selectedSource: SourceWithContent,
+  selectedSourceWithContent: SourceWithContent,
 };
 
 class EditorMenu extends Component<Props> {
   props: Props;
 
   componentWillUpdate(nextProps: Props) {
     this.props.clearContextMenu();
     if (nextProps.contextMenu) {
       this.showMenu(nextProps);
     }
   }
 
   showMenu(props) {
     const {
       cx,
       editor,
-      selectedSource,
+      selectedSourceWithContent,
       editorActions,
       hasPrettySource,
       isPaused,
       contextMenu: event,
     } = props;
 
     const location = getSourceLocationFromMouseEvent(
       editor,
-      selectedSource,
+      selectedSourceWithContent.source,
       // Use a coercion, as contextMenu is optional
       (event: any)
     );
 
     showMenu(
       event,
       editorMenuItems({
         cx,
         editorActions,
-        selectedSource,
+        selectedSourceWithContent,
         hasPrettySource,
         location,
         isPaused,
         selectionText: editor.codeMirror.getSelection().trim(),
         isTextSelected: editor.codeMirror.somethingSelected(),
       })
     );
   }
@@ -79,17 +79,20 @@ class EditorMenu extends Component<Props
   render() {
     return null;
   }
 }
 
 const mapStateToProps = (state, props) => ({
   cx: getThreadContext(state),
   isPaused: getIsPaused(state, getCurrentThread(state)),
-  hasPrettySource: !!getPrettySource(state, props.selectedSource.id),
+  hasPrettySource: !!getPrettySource(
+    state,
+    props.selectedSourceWithContent.source.id
+  ),
 });
 
 const mapDispatchToProps = dispatch => ({
   editorActions: editorItemActions(dispatch),
 });
 
 export default connect(
   mapStateToProps,
--- a/devtools/client/debugger/src/components/Editor/Footer.js
+++ b/devtools/client/debugger/src/components/Editor/Footer.js
@@ -33,17 +33,17 @@ import "./Footer.css";
 
 type CursorPosition = {
   line: number,
   column: number,
 };
 
 type Props = {
   cx: Context,
-  selectedSource: ?SourceWithContent,
+  selectedSourceWithContent: ?SourceWithContent,
   mappedSource: Source,
   endPanelCollapsed: boolean,
   horizontal: boolean,
   togglePrettyPrint: typeof actions.togglePrettyPrint,
   toggleBlackBox: typeof actions.toggleBlackBox,
   jumpToMappedLocation: typeof actions.jumpToMappedLocation,
   togglePaneCollapse: typeof actions.togglePaneCollapse,
 };
@@ -79,86 +79,93 @@ class SourceFooter extends PureComponent
     if (toggle === true) {
       eventDoc.CodeMirror.on("cursorActivity", this.onCursorChange);
     } else {
       eventDoc.CodeMirror.off("cursorActivity", this.onCursorChange);
     }
   }
 
   prettyPrintButton() {
-    const { cx, selectedSource, togglePrettyPrint } = this.props;
+    const { cx, selectedSourceWithContent, togglePrettyPrint } = this.props;
 
-    if (!selectedSource) {
+    if (!selectedSourceWithContent) {
       return;
     }
 
-    if (!selectedSource.content && selectedSource.isPrettyPrinted) {
+    if (
+      !selectedSourceWithContent.content &&
+      selectedSourceWithContent.source.isPrettyPrinted
+    ) {
       return (
         <div className="loader" key="pretty-loader">
           <AccessibleImage className="loader" />
         </div>
       );
     }
 
     const sourceContent =
-      selectedSource.content && isFulfilled(selectedSource.content)
-        ? selectedSource.content.value
+      selectedSourceWithContent.content &&
+      isFulfilled(selectedSourceWithContent.content)
+        ? selectedSourceWithContent.content.value
         : null;
     if (
       !shouldShowPrettyPrint(
-        selectedSource,
+        selectedSourceWithContent.source,
         sourceContent || { type: "text", value: "", contentType: undefined }
       )
     ) {
       return;
     }
 
     const tooltip = L10N.getStr("sourceTabs.prettyPrint");
-    const sourceLoaded = !!selectedSource.content;
+    const sourceLoaded = !!selectedSourceWithContent.content;
 
     const type = "prettyPrint";
     return (
       <button
-        onClick={() => togglePrettyPrint(cx, selectedSource.id)}
+        onClick={() =>
+          togglePrettyPrint(cx, selectedSourceWithContent.source.id)
+        }
         className={classnames("action", type, {
           active: sourceLoaded,
-          pretty: isPretty(selectedSource),
+          pretty: isPretty(selectedSourceWithContent.source),
         })}
         key={type}
         title={tooltip}
         aria-label={tooltip}
       >
         <AccessibleImage className={type} />
       </button>
     );
   }
 
   blackBoxButton() {
-    const { cx, selectedSource, toggleBlackBox } = this.props;
-    const sourceLoaded = selectedSource && selectedSource.content;
+    const { cx, selectedSourceWithContent, toggleBlackBox } = this.props;
+    const sourceLoaded =
+      selectedSourceWithContent && selectedSourceWithContent.content;
 
-    if (!selectedSource) {
+    if (!selectedSourceWithContent) {
       return;
     }
 
-    if (!shouldBlackbox(selectedSource)) {
+    if (!shouldBlackbox(selectedSourceWithContent.source)) {
       return;
     }
 
-    const blackboxed = selectedSource.isBlackBoxed;
+    const blackboxed = selectedSourceWithContent.source.isBlackBoxed;
 
     const tooltip = blackboxed
       ? L10N.getStr("sourceFooter.unblackbox")
       : L10N.getStr("sourceFooter.blackbox");
 
     const type = "black-box";
 
     return (
       <button
-        onClick={() => toggleBlackBox(cx, selectedSource)}
+        onClick={() => toggleBlackBox(cx, selectedSourceWithContent.source)}
         className={classnames("action", type, {
           active: sourceLoaded,
           blackboxed: blackboxed,
         })}
         key={type}
         title={tooltip}
         aria-label={tooltip}
       >
@@ -191,31 +198,35 @@ class SourceFooter extends PureComponent
     return commands.length ? <div className="commands">{commands}</div> : null;
   }
 
   renderSourceSummary() {
     const {
       cx,
       mappedSource,
       jumpToMappedLocation,
-      selectedSource,
+      selectedSourceWithContent,
     } = this.props;
 
-    if (!mappedSource || !selectedSource || !isOriginal(selectedSource)) {
+    if (
+      !mappedSource ||
+      !selectedSourceWithContent ||
+      !isOriginal(selectedSourceWithContent.source)
+    ) {
       return null;
     }
 
     const filename = getFilename(mappedSource);
     const tooltip = L10N.getFormatStr(
       "sourceFooter.mappedSourceTooltip",
       filename
     );
     const title = L10N.getFormatStr("sourceFooter.mappedSource", filename);
     const mappedSourceLocation = {
-      sourceId: selectedSource.id,
+      sourceId: selectedSourceWithContent.source.id,
       line: 1,
       column: 1,
     };
     return (
       <button
         className="mapped-source"
         onClick={() => jumpToMappedLocation(cx, mappedSourceLocation)}
         title={tooltip}
@@ -226,17 +237,17 @@ class SourceFooter extends PureComponent
   }
 
   onCursorChange = event => {
     const { line, ch } = event.doc.getCursor();
     this.setState({ cursorPosition: { line, column: ch } });
   };
 
   renderCursorPosition() {
-    if (!this.props.selectedSource) {
+    if (!this.props.selectedSourceWithContent) {
       return null;
     }
 
     const { line, column } = this.state.cursorPosition;
 
     const text = L10N.getFormatStr(
       "sourceFooter.currentCursorPosition",
       line + 1,
@@ -264,25 +275,28 @@ class SourceFooter extends PureComponent
           {this.renderToggleButton()}
         </div>
       </div>
     );
   }
 }
 
 const mapStateToProps = state => {
-  const selectedSource = getSelectedSourceWithContent(state);
+  const selectedSourceWithContent = getSelectedSourceWithContent(state);
 
   return {
     cx: getContext(state),
-    selectedSource,
-    mappedSource: getGeneratedSource(state, selectedSource),
+    selectedSourceWithContent,
+    mappedSource: getGeneratedSource(
+      state,
+      selectedSourceWithContent && selectedSourceWithContent.source
+    ),
     prettySource: getPrettySource(
       state,
-      selectedSource ? selectedSource.id : null
+      selectedSourceWithContent ? selectedSourceWithContent.source.id : null
     ),
     endPanelCollapsed: getPaneCollapse(state, "end"),
   };
 };
 
 export default connect(
   mapStateToProps,
   {
--- a/devtools/client/debugger/src/components/Editor/HighlightLine.js
+++ b/devtools/client/debugger/src/components/Editor/HighlightLine.js
@@ -23,103 +23,115 @@ import type {
   SourceDocuments,
 } from "../../types";
 import type { Command } from "../../reducers/types";
 
 type Props = {
   pauseCommand: Command,
   selectedFrame: Frame,
   selectedLocation: SourceLocation,
-  selectedSource: ?SourceWithContent,
+  selectedSourceWithContent: ?SourceWithContent,
 };
 
 function isDebugLine(selectedFrame: Frame, selectedLocation: SourceLocation) {
   if (!selectedFrame) {
     return;
   }
 
   return (
     selectedFrame.location.sourceId == selectedLocation.sourceId &&
     selectedFrame.location.line == selectedLocation.line
   );
 }
 
-function isDocumentReady(selectedSource: ?SourceWithContent, selectedLocation) {
+function isDocumentReady(
+  selectedSourceWithContent: ?SourceWithContent,
+  selectedLocation
+) {
   return (
     selectedLocation &&
-    selectedSource &&
-    selectedSource.content &&
+    selectedSourceWithContent &&
+    selectedSourceWithContent.content &&
     hasDocument(selectedLocation.sourceId)
   );
 }
 
 export class HighlightLine extends Component<Props> {
   isStepping: boolean = false;
   previousEditorLine: ?number = null;
 
   shouldComponentUpdate(nextProps: Props) {
-    const { selectedLocation, selectedSource } = nextProps;
-    return this.shouldSetHighlightLine(selectedLocation, selectedSource);
+    const { selectedLocation, selectedSourceWithContent } = nextProps;
+    return this.shouldSetHighlightLine(
+      selectedLocation,
+      selectedSourceWithContent
+    );
   }
 
   componentDidUpdate(prevProps: Props) {
     this.completeHighlightLine(prevProps);
   }
 
   componentDidMount() {
     this.completeHighlightLine(null);
   }
 
   shouldSetHighlightLine(
     selectedLocation: SourceLocation,
-    selectedSource: ?SourceWithContent
+    selectedSourceWithContent: ?SourceWithContent
   ) {
     const { sourceId, line } = selectedLocation;
     const editorLine = toEditorLine(sourceId, line);
 
-    if (!isDocumentReady(selectedSource, selectedLocation)) {
+    if (!isDocumentReady(selectedSourceWithContent, selectedLocation)) {
       return false;
     }
 
     if (this.isStepping && editorLine === this.previousEditorLine) {
       return false;
     }
 
     return true;
   }
 
   completeHighlightLine(prevProps: Props | null) {
     const {
       pauseCommand,
       selectedLocation,
       selectedFrame,
-      selectedSource,
+      selectedSourceWithContent,
     } = this.props;
     if (pauseCommand) {
       this.isStepping = true;
     }
 
     startOperation();
     if (prevProps) {
       this.clearHighlightLine(
         prevProps.selectedLocation,
-        prevProps.selectedSource
+        prevProps.selectedSourceWithContent
       );
     }
-    this.setHighlightLine(selectedLocation, selectedFrame, selectedSource);
+    this.setHighlightLine(
+      selectedLocation,
+      selectedFrame,
+      selectedSourceWithContent
+    );
     endOperation();
   }
 
   setHighlightLine(
     selectedLocation: SourceLocation,
     selectedFrame: Frame,
-    selectedSource: ?SourceWithContent
+    selectedSourceWithContent: ?SourceWithContent
   ) {
     const { sourceId, line } = selectedLocation;
-    if (!this.shouldSetHighlightLine(selectedLocation, selectedSource)) {
+    if (
+      !this.shouldSetHighlightLine(selectedLocation, selectedSourceWithContent)
+    ) {
       return;
     }
 
     this.isStepping = false;
     const editorLine = toEditorLine(sourceId, line);
     this.previousEditorLine = editorLine;
 
     if (!line || isDebugLine(selectedFrame, selectedLocation)) {
@@ -149,19 +161,19 @@ export class HighlightLine extends Compo
     setTimeout(
       () => doc && doc.removeLineClass(editorLine, "line", "highlight-line"),
       duration
     );
   }
 
   clearHighlightLine(
     selectedLocation: SourceLocation,
-    selectedSource: ?SourceWithContent
+    selectedSourceWithContent: ?SourceWithContent
   ) {
-    if (!isDocumentReady(selectedSource, selectedLocation)) {
+    if (!isDocumentReady(selectedSourceWithContent, selectedLocation)) {
       return;
     }
 
     const { line, sourceId } = selectedLocation;
     const editorLine = toEditorLine(sourceId, line);
     const doc = getDocument(sourceId);
     doc.removeLineClass(editorLine, "line", "highlight-line");
   }
@@ -170,10 +182,10 @@ export class HighlightLine extends Compo
     return null;
   }
 }
 
 export default connect(state => ({
   pauseCommand: getPauseCommand(state, getCurrentThread(state)),
   selectedFrame: getVisibleSelectedFrame(state),
   selectedLocation: getSelectedLocation(state),
-  selectedSource: getSelectedSourceWithContent(state),
+  selectedSourceWithContent: getSelectedSourceWithContent(state),
 }))(HighlightLine);
--- a/devtools/client/debugger/src/components/Editor/index.js
+++ b/devtools/client/debugger/src/components/Editor/index.js
@@ -90,17 +90,17 @@ import type {
 
 const cssVars = {
   searchbarHeight: "var(--editor-searchbar-height)",
 };
 
 export type Props = {
   cx: ThreadContext,
   selectedLocation: ?SourceLocation,
-  selectedSource: ?SourceWithContent,
+  selectedSourceWithContent: ?SourceWithContent,
   searchOn: boolean,
   startPanelSize: number,
   endPanelSize: number,
   conditionalPanelLocation: SourceLocation,
   symbols: SymbolDeclarations,
   isPaused: boolean,
   skipPausing: boolean,
 
@@ -134,27 +134,30 @@ class Editor extends PureComponent<Props
       editor: (null: any),
       contextMenu: null,
     };
   }
 
   componentWillReceiveProps(nextProps: Props) {
     let editor = this.state.editor;
 
-    if (!this.state.editor && nextProps.selectedSource) {
+    if (!this.state.editor && nextProps.selectedSourceWithContent) {
       editor = this.setupEditor();
     }
 
     startOperation();
     this.setText(nextProps, editor);
     this.setSize(nextProps, editor);
     this.scrollToLocation(nextProps, editor);
     endOperation();
 
-    if (this.props.selectedSource != nextProps.selectedSource) {
+    if (
+      this.props.selectedSourceWithContent !=
+      nextProps.selectedSourceWithContent
+    ) {
       this.props.updateViewport();
       resizeBreakpointGutter(editor.codeMirror);
       resizeToggleButton(editor.codeMirror);
     }
   }
 
   setupEditor() {
     const editor = getEditor();
@@ -222,21 +225,21 @@ class Editor extends PureComponent<Props
       L10N.getStr("toggleCondPanel.logPoint.key"),
       this.onToggleConditionalPanel
     );
     shortcuts.on(L10N.getStr("sourceTabs.closeTab.key"), this.onClosePress);
     shortcuts.on("Esc", this.onEscape);
   }
 
   onClosePress = (key, e: KeyboardEvent) => {
-    const { cx, selectedSource } = this.props;
-    if (selectedSource) {
+    const { cx, selectedSourceWithContent } = this.props;
+    if (selectedSourceWithContent) {
       e.preventDefault();
       e.stopPropagation();
-      this.props.closeTab(cx, selectedSource);
+      this.props.closeTab(cx, selectedSourceWithContent.source);
     }
   };
 
   componentWillUnmount() {
     if (this.state.editor) {
       this.state.editor.destroy();
       this.state.editor.codeMirror.off("scroll", this.onEditorScroll);
       this.setState({ editor: (null: any) });
@@ -246,23 +249,23 @@ class Editor extends PureComponent<Props
     shortcuts.off(L10N.getStr("sourceTabs.closeTab.key"));
     shortcuts.off(L10N.getStr("toggleBreakpoint.key"));
     shortcuts.off(L10N.getStr("toggleCondPanel.breakpoint.key"));
     shortcuts.off(L10N.getStr("toggleCondPanel.logPoint.key"));
   }
 
   getCurrentLine() {
     const { codeMirror } = this.state.editor;
-    const { selectedSource } = this.props;
-    if (!selectedSource) {
+    const { selectedSourceWithContent } = this.props;
+    if (!selectedSourceWithContent) {
       return;
     }
 
     const line = getCursorLine(codeMirror);
-    return toSourceLine(selectedSource.id, line);
+    return toSourceLine(selectedSourceWithContent.source.id, line);
   }
 
   onToggleBreakpoint = (key, e: KeyboardEvent) => {
     e.preventDefault();
     e.stopPropagation();
 
     const line = this.getCurrentLine();
     if (typeof line !== "number") {
@@ -323,35 +326,35 @@ class Editor extends PureComponent<Props
   };
 
   openMenu(event: MouseEvent) {
     event.stopPropagation();
     event.preventDefault();
 
     const {
       cx,
-      selectedSource,
+      selectedSourceWithContent,
       breakpointActions,
       editorActions,
       isPaused,
       conditionalPanelLocation,
       closeConditionalPanel,
     } = this.props;
     const { editor } = this.state;
-    if (!selectedSource || !editor) {
+    if (!selectedSourceWithContent || !editor) {
       return;
     }
 
     // only allow one conditionalPanel location.
     if (conditionalPanelLocation) {
       closeConditionalPanel();
     }
 
     const target: Element = (event.target: any);
-    const { id: sourceId } = selectedSource;
+    const { id: sourceId } = selectedSourceWithContent.source;
     const line = lineAtHeight(editor, sourceId, event);
 
     if (typeof line != "number") {
       return;
     }
 
     const location = { line, column: undefined, sourceId };
 
@@ -377,126 +380,136 @@ class Editor extends PureComponent<Props
   onGutterClick = (
     cm: Object,
     line: number,
     gutter: string,
     ev: MouseEvent
   ) => {
     const {
       cx,
-      selectedSource,
+      selectedSourceWithContent,
       conditionalPanelLocation,
       closeConditionalPanel,
       addBreakpointAtLine,
       continueToHere,
       toggleBlackBox,
     } = this.props;
 
     // ignore right clicks in the gutter
-    if ((ev.ctrlKey && ev.button === 0) || ev.button === 2 || !selectedSource) {
+    if (
+      (ev.ctrlKey && ev.button === 0) ||
+      ev.button === 2 ||
+      !selectedSourceWithContent
+    ) {
       return;
     }
 
     // if user clicks gutter to set breakpoint on blackboxed source, un-blackbox the source.
-    if (selectedSource && selectedSource.isBlackBoxed) {
-      toggleBlackBox(cx, selectedSource);
+    if (
+      selectedSourceWithContent &&
+      selectedSourceWithContent.source.isBlackBoxed
+    ) {
+      toggleBlackBox(cx, selectedSourceWithContent.source);
     }
 
     if (conditionalPanelLocation) {
       return closeConditionalPanel();
     }
 
     if (gutter === "CodeMirror-foldgutter") {
       return;
     }
 
-    const sourceLine = toSourceLine(selectedSource.id, line);
+    const sourceLine = toSourceLine(selectedSourceWithContent.source.id, line);
     if (typeof sourceLine !== "number") {
       return;
     }
 
     if (ev.metaKey) {
       return continueToHere(cx, sourceLine);
     }
 
     return addBreakpointAtLine(cx, sourceLine, ev.altKey, ev.shiftKey);
   };
 
   onGutterContextMenu = (event: MouseEvent) => {
     return this.openMenu(event);
   };
 
   onClick(e: MouseEvent) {
-    const { cx, selectedSource, jumpToMappedLocation } = this.props;
+    const { cx, selectedSourceWithContent, jumpToMappedLocation } = this.props;
 
-    if (selectedSource && e.metaKey && e.altKey) {
+    if (selectedSourceWithContent && e.metaKey && e.altKey) {
       const sourceLocation = getSourceLocationFromMouseEvent(
         this.state.editor,
-        selectedSource,
+        selectedSourceWithContent.source,
         e
       );
       jumpToMappedLocation(cx, sourceLocation);
     }
   }
 
   toggleConditionalPanel = (line, log: boolean = false) => {
     const {
       conditionalPanelLocation,
       closeConditionalPanel,
       openConditionalPanel,
-      selectedSource,
+      selectedSourceWithContent,
     } = this.props;
 
     if (conditionalPanelLocation) {
       return closeConditionalPanel();
     }
 
-    if (!selectedSource) {
+    if (!selectedSourceWithContent) {
       return;
     }
 
     return openConditionalPanel(
       {
         line: line,
-        sourceId: selectedSource.id,
-        sourceUrl: selectedSource.url,
+        sourceId: selectedSourceWithContent.source.id,
+        sourceUrl: selectedSourceWithContent.source.url,
       },
       log
     );
   };
 
   shouldScrollToLocation(nextProps, editor) {
-    const { selectedLocation, selectedSource } = this.props;
+    const { selectedLocation, selectedSourceWithContent } = this.props;
     if (
       !editor ||
-      !nextProps.selectedSource ||
+      !nextProps.selectedSourceWithContent ||
       !nextProps.selectedLocation ||
       !nextProps.selectedLocation.line ||
-      !nextProps.selectedSource.content
+      !nextProps.selectedSourceWithContent.content
     ) {
       return false;
     }
 
     const isFirstLoad =
-      (!selectedSource || !selectedSource.content) &&
-      nextProps.selectedSource.content;
+      (!selectedSourceWithContent || !selectedSourceWithContent.content) &&
+      nextProps.selectedSourceWithContent.content;
     const locationChanged = selectedLocation !== nextProps.selectedLocation;
     const symbolsChanged = nextProps.symbols != this.props.symbols;
 
     return isFirstLoad || locationChanged || symbolsChanged;
   }
 
   scrollToLocation(nextProps, editor) {
-    const { selectedLocation, selectedSource } = nextProps;
+    const { selectedLocation, selectedSourceWithContent } = nextProps;
 
     if (selectedLocation && this.shouldScrollToLocation(nextProps, editor)) {
       let { line, column } = toEditorPosition(selectedLocation);
 
-      if (selectedSource && hasDocument(selectedSource.id)) {
-        const doc = getDocument(selectedSource.id);
+      if (
+        selectedSourceWithContent &&
+        hasDocument(selectedSourceWithContent.source.id)
+      ) {
+        const doc = getDocument(selectedSourceWithContent.source.id);
         const lineText: ?string = doc.getLine(line);
         column = Math.max(column, getIndentation(lineText));
       }
 
       scrollToColumn(editor.codeMirror, line, column);
     }
   }
 
@@ -509,44 +522,44 @@ class Editor extends PureComponent<Props
       nextProps.startPanelSize !== this.props.startPanelSize ||
       nextProps.endPanelSize !== this.props.endPanelSize
     ) {
       editor.codeMirror.setSize();
     }
   }
 
   setText(props, editor) {
-    const { selectedSource, symbols } = props;
+    const { selectedSourceWithContent, symbols } = props;
 
     if (!editor) {
       return;
     }
 
     // check if we previously had a selected source
-    if (!selectedSource) {
+    if (!selectedSourceWithContent) {
       return this.clearEditor();
     }
 
-    if (!selectedSource.content) {
+    if (!selectedSourceWithContent.content) {
       return showLoading(editor);
     }
 
-    if (selectedSource.content.state === "rejected") {
-      let { value } = selectedSource.content;
+    if (selectedSourceWithContent.content.state === "rejected") {
+      let { value } = selectedSourceWithContent.content;
       if (typeof value !== "string") {
         value = "Unexpected source error";
       }
 
       return this.showErrorMessage(value);
     }
 
     return showSourceText(
       editor,
-      selectedSource,
-      selectedSource.content.value,
+      selectedSourceWithContent.source,
+      selectedSourceWithContent.content.value,
       symbols
     );
   }
 
   clearEditor() {
     const { editor } = this.state;
     if (!editor) {
       return;
@@ -576,69 +589,78 @@ class Editor extends PureComponent<Props
     return {
       height: "100%",
     };
   }
 
   renderItems() {
     const {
       cx,
-      selectedSource,
+      selectedSourceWithContent,
       conditionalPanelLocation,
       isPaused,
     } = this.props;
     const { editor, contextMenu } = this.state;
 
-    if (!selectedSource || !editor || !getDocument(selectedSource.id)) {
+    if (
+      !selectedSourceWithContent ||
+      !editor ||
+      !getDocument(selectedSourceWithContent.source.id)
+    ) {
       return null;
     }
 
     return (
       <div>
         <DebugLine editor={editor} />
         <HighlightLine />
         <EmptyLines editor={editor} />
         <Breakpoints editor={editor} cx={cx} />
         <Preview editor={editor} editorRef={this.$editorWrapper} />
         <HighlightLines editor={editor} />
         {
           <EditorMenu
             editor={editor}
             contextMenu={contextMenu}
             clearContextMenu={this.clearContextMenu}
-            selectedSource={selectedSource}
+            selectedSourceWithContent={selectedSourceWithContent}
           />
         }
         {conditionalPanelLocation ? <ConditionalPanel editor={editor} /> : null}
         {features.columnBreakpoints ? (
           <ColumnBreakpoints editor={editor} />
         ) : null}
         {isPaused && features.inlinePreview ? (
-          <InlinePreviews editor={editor} selectedSource={selectedSource} />
+          <InlinePreviews
+            editor={editor}
+            selectedSource={selectedSourceWithContent.source}
+          />
         ) : null}
       </div>
     );
   }
 
   renderSearchBar() {
     const { editor } = this.state;
 
-    if (!this.props.selectedSource) {
+    if (!this.props.selectedSourceWithContent) {
       return null;
     }
 
     return <SearchBar editor={editor} />;
   }
 
   render() {
-    const { selectedSource, skipPausing } = this.props;
+    const { selectedSourceWithContent, skipPausing } = this.props;
     return (
       <div
         className={classnames("editor-wrapper", {
-          blackboxed: selectedSource && selectedSource.isBlackBoxed,
+          blackboxed:
+            selectedSourceWithContent &&
+            selectedSourceWithContent.source.isBlackBoxed,
           "skip-pausing": skipPausing,
         })}
         ref={c => (this.$editorWrapper = c)}
       >
         <div
           className="editor-mount devtools-monospace"
           style={this.getInlineEditorStyles()}
         />
@@ -649,25 +671,28 @@ class Editor extends PureComponent<Props
   }
 }
 
 Editor.contextTypes = {
   shortcuts: PropTypes.object,
 };
 
 const mapStateToProps = state => {
-  const selectedSource = getSelectedSourceWithContent(state);
+  const selectedSourceWithContent = getSelectedSourceWithContent(state);
 
   return {
     cx: getThreadContext(state),
     selectedLocation: getSelectedLocation(state),
-    selectedSource,
+    selectedSourceWithContent,
     searchOn: getActiveSearch(state) === "file",
     conditionalPanelLocation: getConditionalPanelLocation(state),
-    symbols: getSymbols(state, selectedSource),
+    symbols: getSymbols(
+      state,
+      selectedSourceWithContent ? selectedSourceWithContent.source : null
+    ),
     isPaused: getIsPaused(state, getCurrentThread(state)),
     skipPausing: getSkipPausing(state),
   };
 };
 
 const mapDispatchToProps = dispatch => ({
   ...bindActionCreators(
     {
--- a/devtools/client/debugger/src/components/Editor/menus/editor.js
+++ b/devtools/client/debugger/src/components/Editor/menus/editor.js
@@ -163,58 +163,56 @@ const downloadFileItem = (
   label: L10N.getStr("downloadFile.label"),
   accesskey: L10N.getStr("downloadFile.accesskey"),
   click: () => downloadFile(selectedContent, getFilename(selectedSource)),
 });
 
 export function editorMenuItems({
   cx,
   editorActions,
-  selectedSource,
+  selectedSourceWithContent,
   location,
   selectionText,
   hasPrettySource,
   isTextSelected,
   isPaused,
 }: {
   cx: ThreadContext,
   editorActions: EditorItemActions,
-  selectedSource: SourceWithContent,
+  selectedSourceWithContent: SourceWithContent,
   location: SourceLocation,
   selectionText: string,
   hasPrettySource: boolean,
   isTextSelected: boolean,
   isPaused: boolean,
 }) {
   const items = [];
-
-  const content =
-    selectedSource.content && isFulfilled(selectedSource.content)
-      ? selectedSource.content.value
-      : null;
+  const { source: selectedSource, content } = selectedSourceWithContent;
 
   items.push(
     jumpToMappedLocationItem(
       cx,
       selectedSource,
       location,
       hasPrettySource,
       editorActions
     ),
     continueToHereItem(cx, location, isPaused, editorActions),
     { type: "separator" },
-    ...(content ? [copyToClipboardItem(content, editorActions)] : []),
+    ...(content && isFulfilled(content)
+      ? [copyToClipboardItem(content.value, editorActions)]
+      : []),
     ...(!selectedSource.isWasm
       ? [
           copySourceItem(selectedSource, selectionText, editorActions),
           copySourceUri2Item(selectedSource, editorActions),
         ]
       : []),
-    ...(content
-      ? [downloadFileItem(selectedSource, content, editorActions)]
+    ...(content && isFulfilled(content)
+      ? [downloadFileItem(selectedSource, content.value, editorActions)]
       : []),
     { type: "separator" },
     showSourceMenuItem(cx, selectedSource, editorActions),
     blackBoxMenuItem(cx, selectedSource, editorActions)
   );
 
   if (isTextSelected) {
     items.push(
--- a/devtools/client/debugger/src/components/Editor/tests/DebugLine.spec.js
+++ b/devtools/client/debugger/src/components/Editor/tests/DebugLine.spec.js
@@ -29,17 +29,17 @@ function createMockDocument(clear) {
 function generateDefaults(editor, overrides) {
   return {
     editor,
     pauseInfo: {
       why: { type: "breakpoint" },
     },
     frame: null,
     source: ({
-      ...createSourceObject("foo"),
+      source: createSourceObject("foo"),
       content: null,
     }: SourceWithContent),
     ...overrides,
   };
 }
 
 function createFrame(line) {
   return {
@@ -52,31 +52,31 @@ function createFrame(line) {
 }
 
 function render(overrides = {}) {
   const clear = jest.fn();
   const editor = { codeMirror: {} };
   const props = generateDefaults(editor, overrides);
 
   const doc = createMockDocument(clear);
-  setDocument(props.source.id, doc);
+  setDocument(props.source.source.id, doc);
 
   // $FlowIgnore
   const component = shallow(<DebugLine.WrappedComponent {...props} />, {
     lifecycleExperimental: true,
   });
   return { component, props, clear, editor, doc };
 }
 
 describe("DebugLine Component", () => {
   describe("pausing at the first location", () => {
     it("should show a new debug line", async () => {
       const { component, props, doc } = render({
         source: {
-          ...createSourceObject("foo"),
+          source: createSourceObject("foo"),
           content: asyncValue.fulfilled({
             type: "text",
             value: "",
             contentType: undefined,
           }),
         },
       });
       const line = 2;
@@ -89,17 +89,17 @@ describe("DebugLine Component", () => {
         [toEditorLine("foo", line), "line", "new-debug-line"],
       ]);
     });
 
     describe("pausing at a new location", () => {
       it("should replace the first debug line", async () => {
         const { props, component, clear, doc } = render({
           source: {
-            ...createSourceObject("foo"),
+            source: createSourceObject("foo"),
             content: asyncValue.fulfilled({
               type: "text",
               value: "",
               contentType: undefined,
             }),
           },
         });
 
--- a/devtools/client/debugger/src/components/Editor/tests/Editor.spec.js
+++ b/devtools/client/debugger/src/components/Editor/tests/Editor.spec.js
@@ -2,17 +2,17 @@
  * 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/>. */
 
 // @flow
 
 import React from "react";
 import { shallow } from "enzyme";
 import Editor from "../index";
-import type { Source, SourceWithContent, SourceBase } from "../../../types";
+import type { Source, SourceWithContent } from "../../../types";
 import { getDocument } from "../../../utils/editor/source-documents";
 import * as asyncValue from "../../../utils/async-value";
 
 function generateDefaults(overrides) {
   return {
     toggleBreakpoint: jest.fn(),
     updateViewport: jest.fn(),
     toggleDisabledBreakpoint: jest.fn(),
@@ -69,17 +69,17 @@ function createMockSourceWithContent(
   const {
     loadedState = "loaded",
     text = "the text",
     contentType = undefined,
     error = undefined,
     ...otherOverrides
   } = overrides;
 
-  const source: SourceBase = ({
+  const source: Source = ({
     id: "foo",
     url: "foo",
     ...otherOverrides,
   }: any);
   let content = null;
   if (loadedState === "loaded") {
     if (typeof text !== "string") {
       throw new Error("Cannot create a non-text source");
@@ -90,17 +90,17 @@ function createMockSourceWithContent(
       : asyncValue.fulfilled({
           type: "text",
           value: text,
           contentType: contentType || undefined,
         });
   }
 
   return {
-    ...source,
+    source,
     content,
   };
 }
 
 function render(overrides = {}) {
   const props = generateDefaults(overrides);
   const mockEditor = createMockEditor();
 
@@ -123,17 +123,17 @@ describe("Editor", () => {
     });
   });
 
   describe("When loading initial source", () => {
     it("should show a loading message", async () => {
       const { component, mockEditor } = render();
       await component.setState({ editor: mockEditor });
       component.setProps({
-        selectedSource: {
+        selectedSourceWithContent: {
           source: { loadedState: "loading" },
           content: null,
         },
       });
 
       expect(mockEditor.replaceDocument.mock.calls[0][0].getValue()).toBe(
         "Loading…"
       );
@@ -143,17 +143,17 @@ describe("Editor", () => {
 
   describe("When loaded", () => {
     it("should show text", async () => {
       const { component, mockEditor, props } = render({});
 
       await component.setState({ editor: mockEditor });
       await component.setProps({
         ...props,
-        selectedSource: createMockSourceWithContent({
+        selectedSourceWithContent: createMockSourceWithContent({
           loadedState: "loaded",
         }),
         selectedLocation: { sourceId: "foo", line: 3, column: 1 },
       });
 
       expect(mockEditor.setText.mock.calls).toEqual([["the text"]]);
       expect(mockEditor.codeMirror.scrollTo.mock.calls).toEqual([[1, 2]]);
     });
@@ -161,17 +161,17 @@ describe("Editor", () => {
 
   describe("When error", () => {
     it("should show error text", async () => {
       const { component, mockEditor, props } = render({});
 
       await component.setState({ editor: mockEditor });
       await component.setProps({
         ...props,
-        selectedSource: createMockSourceWithContent({
+        selectedSourceWithContent: createMockSourceWithContent({
           loadedState: "loaded",
           text: undefined,
           error: "error text",
         }),
         selectedLocation: { sourceId: "bad-foo", line: 3, column: 1 },
       });
 
       expect(mockEditor.setText.mock.calls).toEqual([
@@ -180,17 +180,17 @@ describe("Editor", () => {
     });
 
     it("should show wasm error", async () => {
       const { component, mockEditor, props } = render({});
 
       await component.setState({ editor: mockEditor });
       await component.setProps({
         ...props,
-        selectedSource: createMockSourceWithContent({
+        selectedSourceWithContent: createMockSourceWithContent({
           loadedState: "loaded",
           isWasm: true,
           text: undefined,
           error: "blah WebAssembly binary source is not available blah",
         }),
         selectedLocation: { sourceId: "bad-foo", line: 3, column: 1 },
       });
 
@@ -202,26 +202,26 @@ describe("Editor", () => {
 
   describe("When navigating to a loading source", () => {
     it("should show loading message and not scroll", async () => {
       const { component, mockEditor, props } = render({});
 
       await component.setState({ editor: mockEditor });
       await component.setProps({
         ...props,
-        selectedSource: createMockSourceWithContent({
+        selectedSourceWithContent: createMockSourceWithContent({
           loadedState: "loaded",
         }),
         selectedLocation: { sourceId: "foo", line: 3, column: 1 },
       });
 
       // navigate to a new source that is still loading
       await component.setProps({
         ...props,
-        selectedSource: createMockSourceWithContent({
+        selectedSourceWithContent: createMockSourceWithContent({
           id: "bar",
           loadedState: "loading",
         }),
         selectedLocation: { sourceId: "bar", line: 1, column: 1 },
       });
 
       expect(mockEditor.replaceDocument.mock.calls[1][0].getValue()).toBe(
         "Loading…"
@@ -232,64 +232,66 @@ describe("Editor", () => {
       expect(mockEditor.codeMirror.scrollTo.mock.calls).toEqual([[1, 2]]);
     });
 
     it("should set the mode when symbols load", async () => {
       const { component, mockEditor, props } = render({});
 
       await component.setState({ editor: mockEditor });
 
-      const selectedSource = createMockSourceWithContent({
+      const selectedSourceWithContent = createMockSourceWithContent({
         loadedState: "loaded",
         contentType: "javascript",
       });
 
-      await component.setProps({ ...props, selectedSource });
+      await component.setProps({ ...props, selectedSourceWithContent });
 
       const symbols = { hasJsx: true };
       await component.setProps({
         ...props,
-        selectedSource,
+        selectedSourceWithContent,
         symbols,
       });
 
       expect(mockEditor.setMode.mock.calls).toEqual([
         [{ name: "javascript" }],
         [{ name: "jsx" }],
       ]);
     });
 
     it("should not re-set the mode when the location changes", async () => {
       const { component, mockEditor, props } = render({});
 
       await component.setState({ editor: mockEditor });
 
-      const selectedSource = createMockSourceWithContent({
+      const selectedSourceWithContent = createMockSourceWithContent({
         loadedState: "loaded",
         contentType: "javascript",
       });
 
-      await component.setProps({ ...props, selectedSource });
+      await component.setProps({ ...props, selectedSourceWithContent });
 
       // symbols are parsed
       const symbols = { hasJsx: true };
       await component.setProps({
         ...props,
-        selectedSource,
+        selectedSourceWithContent,
         symbols,
       });
 
       // selectedLocation changes e.g. pausing/stepping
-      mockEditor.codeMirror.doc = getDocument(selectedSource.id);
+      mockEditor.codeMirror.doc = getDocument(
+        selectedSourceWithContent.source.id
+      );
       mockEditor.codeMirror.getOption = () => ({ name: "jsx" });
       const selectedLocation = { sourceId: "foo", line: 4, column: 1 };
 
       await component.setProps({
         ...props,
-        selectedSource,
+        selectedSourceWithContent,
         symbols,
         selectedLocation,
       });
 
       expect(mockEditor.setMode.mock.calls).toEqual([
         [{ name: "javascript" }],
         [{ name: "jsx" }],
       ]);
@@ -298,26 +300,26 @@ describe("Editor", () => {
 
   describe("When navigating to a loaded source", () => {
     it("should show text and then scroll", async () => {
       const { component, mockEditor, props } = render({});
 
       await component.setState({ editor: mockEditor });
       await component.setProps({
         ...props,
-        selectedSource: createMockSourceWithContent({
+        selectedSourceWithContent: createMockSourceWithContent({
           loadedState: "loading",
         }),
         selectedLocation: { sourceId: "foo", line: 1, column: 1 },
       });
 
       // navigate to a new source that is still loading
       await component.setProps({
         ...props,
-        selectedSource: createMockSourceWithContent({
+        selectedSourceWithContent: createMockSourceWithContent({
           loadedState: "loaded",
         }),
         selectedLocation: { sourceId: "foo", line: 1, column: 1 },
       });
 
       expect(mockEditor.replaceDocument.mock.calls[0][0].getValue()).toBe(
         "Loading…"
       );
--- a/devtools/client/debugger/src/components/Editor/tests/Footer.spec.js
+++ b/devtools/client/debugger/src/components/Editor/tests/Footer.spec.js
@@ -24,30 +24,30 @@ function generateDefaults(overrides) {
     editor: {
       codeMirror: {
         doc: {},
         cursorActivity: jest.fn(),
         on: jest.fn(),
       },
     },
     endPanelCollapsed: false,
-    selectedSource: {
-      ...createSourceObject("foo"),
+    selectedSourceWithContent: {
+      source: createSourceObject("foo"),
       content: null,
     },
     ...overrides,
   };
 }
 
 function render(overrides = {}, position = { line: 0, column: 0 }) {
   const clear = jest.fn();
   const props = generateDefaults(overrides);
 
   const doc = createMockDocument(clear, position);
-  setDocument(props.selectedSource.id, doc);
+  setDocument(props.selectedSourceWithContent.source.id, doc);
 
   // $FlowIgnore
   const component = shallow(<SourceFooter.WrappedComponent {...props} />, {
     lifecycleExperimental: true,
   });
   return { component, props, clear, doc };
 }
 
--- a/devtools/client/debugger/src/components/PrimaryPanes/Outline.js
+++ b/devtools/client/debugger/src/components/PrimaryPanes/Outline.js
@@ -258,22 +258,24 @@ export class Outline extends Component<P
         </div>
       </div>
     );
   }
 }
 
 const mapStateToProps = state => {
   const selectedSource = getSelectedSourceWithContent(state);
-  const symbols = selectedSource ? getSymbols(state, selectedSource) : null;
+  const symbols = selectedSource
+    ? getSymbols(state, selectedSource.source)
+    : null;
 
   return {
     cx: getContext(state),
     symbols,
-    selectedSource: (selectedSource: ?Source),
+    selectedSource: selectedSource && selectedSource.source,
     selectedLocation: getSelectedLocation(state),
     getFunctionText: line => {
       if (selectedSource) {
         return findFunctionText(line, selectedSource, symbols);
       }
 
       return null;
     },
--- a/devtools/client/debugger/src/components/SecondaryPanes/Frames/tests/Frames.spec.js
+++ b/devtools/client/debugger/src/components/SecondaryPanes/Frames/tests/Frames.spec.js
@@ -201,18 +201,18 @@ describe("Frames", () => {
       const frames = [
         makeMockFrame("1", source1),
         makeMockFrame("2", source2),
         makeMockFrame("3", source1),
         makeMockFrame("8", source2),
       ];
 
       const sources: SourceResourceState = insertResources(createInitial(), [
-        { ...source1, content: null },
-        { ...source2, content: null },
+        source1,
+        source2,
       ]);
 
       const processedFrames = formatCallStackFrames(frames, sources, source1);
       const selectedFrame = frames[0];
 
       const component = render({
         frames: processedFrames,
         frameworkGroupingOn: false,
--- a/devtools/client/debugger/src/reducers/source-actors.js
+++ b/devtools/client/debugger/src/reducers/source-actors.js
@@ -2,44 +2,29 @@
  * 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/>. */
 
 // @flow
 
 import type { Action } from "../actions/types";
 import type { SourceId, ThreadId } from "../types";
 import {
-  asSettled,
-  type AsyncValue,
-  type SettledValue,
-} from "../utils/async-value";
-import {
   createInitial,
   insertResources,
-  updateResources,
   removeResources,
   hasResource,
   getResource,
-  getMappedResource,
-  makeWeakQuery,
   makeIdQuery,
   makeReduceAllQuery,
   type Resource,
   type ResourceState,
-  type WeakQuery,
   type IdQuery,
   type ReduceAllQuery,
 } from "../utils/resource";
 
-import { asyncActionAsValue } from "../actions/utils/middleware/promise";
-import type {
-  SourceActorBreakpointColumnsAction,
-  SourceActorBreakableLinesAction,
-} from "../actions/types/SourceActorAction";
-
 export opaque type SourceActorId: string = string;
 export type SourceActor = {|
   +id: SourceActorId,
   +actor: string,
   +thread: ThreadId,
   +source: SourceId,
 
   +isBlackBoxed: boolean,
@@ -57,107 +42,49 @@ export type SourceActor = {|
 
   // The debugger's Debugger.Source API provides type information for the
   // cause of this source's creation.
   +introductionType: string | null,
 |};
 
 type SourceActorResource = Resource<{
   ...SourceActor,
-
-  // The list of breakpoint positions on each line of the file.
-  breakpointPositions: Map<number, AsyncValue<Array<number>>>,
-
-  // The list of lines that contain breakpoints.
-  breakableLines: AsyncValue<Array<number>> | null,
 }>;
 export type SourceActorsState = ResourceState<SourceActorResource>;
 export type SourceActorOuterState = { sourceActors: SourceActorsState };
 
 const initial: SourceActorsState = createInitial();
 
 export default function update(
   state: SourceActorsState = initial,
   action: Action
 ): SourceActorsState {
   switch (action.type) {
     case "INSERT_SOURCE_ACTORS": {
       const { items } = action;
-      state = insertResources(
-        state,
-        items.map(item => ({
-          ...item,
-          breakpointPositions: new Map(),
-          breakableLines: null,
-        }))
-      );
+      state = insertResources(state, items);
       break;
     }
     case "REMOVE_SOURCE_ACTORS": {
       const { items } = action;
       state = removeResources(state, items);
       break;
     }
 
     case "NAVIGATE": {
       state = initial;
       break;
     }
-
-    case "SET_SOURCE_ACTOR_BREAKPOINT_COLUMNS":
-      state = updateBreakpointColumns(state, action);
-      break;
-
-    case "SET_SOURCE_ACTOR_BREAKABLE_LINES":
-      state = updateBreakableLines(state, action);
-      break;
   }
 
   return state;
 }
 
-function updateBreakpointColumns(
-  state: SourceActorsState,
-  action: SourceActorBreakpointColumnsAction
-): SourceActorsState {
-  const { sourceId, line } = action;
-  const value = asyncActionAsValue(action);
-
-  if (!hasResource(state, sourceId)) {
-    return state;
-  }
-
-  const breakpointPositions = new Map(
-    getResource(state, sourceId).breakpointPositions
-  );
-  breakpointPositions.set(line, value);
-
-  return updateResources(state, [{ id: sourceId, breakpointPositions }]);
-}
-
-function updateBreakableLines(
-  state: SourceActorsState,
-  action: SourceActorBreakableLinesAction
-): SourceActorsState {
-  const value = asyncActionAsValue(action);
-  const { sourceId } = action;
-
-  if (!hasResource(state, sourceId)) {
-    return state;
-  }
-
-  return updateResources(state, [{ id: sourceId, breakableLines: value }]);
-}
-
-export function resourceAsSourceActor({
-  breakpointPositions,
-  breakableLines,
-  ...sourceActor
-}: SourceActorResource): SourceActor {
-  return sourceActor;
+export function resourceAsSourceActor(r: SourceActorResource): SourceActor {
+  return r;
 }
 
 // Because we are using an opaque type for our source actor IDs, these
 // functions are required to convert back and forth in order to get a string
 // version of the IDs. That should be super rarely used, but it means that
 // we can very easily see where we're relying on the string version of IDs.
 export function stringToSourceActorId(s: string): SourceActorId {
   return s;
@@ -169,17 +96,17 @@ export function hasSourceActor(
 ): boolean {
   return hasResource(state.sourceActors, id);
 }
 
 export function getSourceActor(
   state: SourceActorOuterState,
   id: SourceActorId
 ): SourceActor {
-  return getMappedResource(state.sourceActors, id, resourceAsSourceActor);
+  return getResource(state.sourceActors, id);
 }
 
 /**
  * Get all of the source actors for a set of IDs. Caches based on the identity
  * of "ids" when possible.
  */
 const querySourceActorsById: IdQuery<
   SourceActorResource,
@@ -234,45 +161,8 @@ const queryThreadsBySourceObject: Reduce
     }, {})
 );
 
 export function getThreadsBySource(
   state: SourceActorOuterState
 ): { [SourceId]: Array<ThreadId> } {
   return queryThreadsBySourceObject(state.sourceActors);
 }
-
-export function getSourceActorBreakableLines(
-  state: SourceActorOuterState,
-  id: SourceActorId
-): SettledValue<Array<number>> | null {
-  const { breakableLines } = getResource(state.sourceActors, id);
-
-  return asSettled(breakableLines);
-}
-
-export function getSourceActorBreakpointColumns(
-  state: SourceActorOuterState,
-  id: SourceActorId,
-  line: number
-): SettledValue<Array<number>> | null {
-  const { breakpointPositions } = getResource(state.sourceActors, id);
-
-  return asSettled(breakpointPositions.get(line) || null);
-}
-
-export const getBreakableLinesForSourceActors: WeakQuery<
-  SourceActorResource,
-  Array<SourceActorId>,
-  Array<number>
-> = makeWeakQuery({
-  filter: (state, ids) => ids,
-  map: ({ breakableLines }) => breakableLines,
-  reduce: items =>
-    Array.from(
-      items.reduce((acc, item) => {
-        if (item && item.state === "fulfilled") {
-          acc = acc.concat(item.value);
-        }
-        return acc;
-      }, [])
-    ),
-});
--- a/devtools/client/debugger/src/reducers/sources.js
+++ b/devtools/client/debugger/src/reducers/sources.js
@@ -19,19 +19,17 @@ import {
   getPlainUrl,
 } from "../utils/source";
 import {
   createInitial,
   insertResources,
   updateResources,
   hasResource,
   getResource,
-  getMappedResource,
   getResourceIds,
-  memoizeResourceShallow,
   makeReduceQuery,
   makeReduceAllQuery,
   makeMapWithArgs,
   type Resource,
   type ResourceState,
   type ReduceQuery,
   type ReduceAllQuery,
 } from "../utils/resource";
@@ -42,17 +40,16 @@ import type { AsyncValue, SettledValue }
 import { originalToGeneratedId } from "devtools-source-map";
 import { prefs } from "../utils/prefs";
 
 import {
   hasSourceActor,
   getSourceActor,
   getSourceActors,
   getThreadsBySource,
-  getBreakableLinesForSourceActors,
   type SourceActorId,
   type SourceActorOuterState,
 } from "./source-actors";
 import type {
   Source,
   SourceId,
   SourceActor,
   SourceLocation,
@@ -64,50 +61,40 @@ import type {
 } from "../types";
 import type { PendingSelectedLocation, Selector } from "./types";
 import type { Action, DonePromiseAction, FocusItem } from "../actions/types";
 import type { LoadSourceAction } from "../actions/types/SourceAction";
 import type { DebuggeeState } from "./debuggee";
 import { uniq } from "lodash";
 
 export type SourcesMap = { [SourceId]: Source };
+type SourcesContentMap = {
+  [SourceId]: AsyncValue<SourceContent> | null,
+};
 export type SourcesMapByThread = { [ThreadId]: SourcesMap };
 
 export type BreakpointPositionsMap = { [SourceId]: BreakpointPositions };
 type SourceActorMap = { [SourceId]: Array<SourceActorId> };
 
 type UrlsMap = { [string]: SourceId[] };
 type PlainUrlsMap = { [string]: string[] };
 
-export type SourceBase = {|
-  +id: SourceId,
-  +url: string,
-  +sourceMapURL?: string,
-  +isBlackBoxed: boolean,
-  +isPrettyPrinted: boolean,
-  +relativeUrl: string,
-  +introductionUrl: ?string,
-  +introductionType: ?string,
-  +extensionName: ?string,
-  +isExtension: boolean,
-  +isWasm: boolean,
-|};
-
 type SourceResource = Resource<{
-  ...SourceBase,
-  content: AsyncValue<SourceContent> | null,
+  ...Source,
 }>;
 export type SourceResourceState = ResourceState<SourceResource>;
 
 export type SourcesState = {
   epoch: number,
 
   // All known sources.
   sources: SourceResourceState,
 
+  content: SourcesContentMap,
+
   breakpointPositions: BreakpointPositionsMap,
   breakableLines: { [SourceId]: Array<number> },
 
   // A link between each source object and the source actor they wrap over.
   actors: SourceActorMap,
 
   // All sources associated with a given URL. When using source maps, multiple
   // sources can have the same URL.
@@ -212,17 +199,17 @@ function update(
         updateBlackBoxList(url, isBlackBoxed);
         return updateBlackboxFlag(state, id, isBlackBoxed);
       }
       break;
 
     case "SET_PROJECT_DIRECTORY_ROOT":
       return updateProjectDirectoryRoot(state, action.url);
 
-    case "SET_ORIGINAL_BREAKABLE_LINES": {
+    case "SET_BREAKABLE_LINES": {
       const { breakableLines, sourceId } = action;
       return {
         ...state,
         breakableLines: {
           ...state.breakableLines,
           [sourceId]: breakableLines,
         },
       };
@@ -248,55 +235,46 @@ function update(
 
     case "SET_FOCUSED_SOURCE_ITEM":
       return { ...state, focusedItem: action.item };
   }
 
   return state;
 }
 
-const resourceAsSourceBase = memoizeResourceShallow(
-  ({ content, ...source }: SourceResource): SourceBase => source
-);
-
-const resourceAsSourceWithContent = memoizeResourceShallow(
-  ({ content, ...source }: SourceResource): SourceWithContent => ({
-    ...source,
-    content: asyncValue.asSettled(content),
-  })
-);
+function resourceAsSource(r: SourceResource): Source {
+  return r;
+}
 
 /*
  * Add sources to the sources store
  * - Add the source to the sources store
  * - Add the source URL to the urls map
  */
-function addSources(state: SourcesState, sources: SourceBase[]): SourcesState {
+function addSources(state: SourcesState, sources: Source[]): SourcesState {
   state = {
     ...state,
+    content: { ...state.content },
     urls: { ...state.urls },
     plainUrls: { ...state.plainUrls },
   };
 
-  state.sources = insertResources(
-    state.sources,
-    sources.map(source => ({
-      ...source,
-      content: null,
-    }))
-  );
+  state.sources = insertResources(state.sources, sources);
 
   for (const source of sources) {
-    // 1. Update the source url map
+    // 1. Add the source to the sources map
+    state.content[source.id] = null;
+
+    // 2. Update the source url map
     const existing = state.urls[source.url] || [];
     if (!existing.includes(source.id)) {
       state.urls[source.url] = [...existing, source.id];
     }
 
-    // 2. Update the plain url map
+    // 3. Update the plain url map
     if (source.url) {
       const plainUrl = getPlainUrl(source.url);
       const existingPlainUrls = state.plainUrls[plainUrl] || [];
       if (!existingPlainUrls.includes(source.url)) {
         state.plainUrls[plainUrl] = [...existingPlainUrls, source.url];
       }
     }
   }
@@ -370,17 +348,17 @@ function updateProjectDirectoryRoot(stat
   return updateRootRelativeValues({
     ...state,
     projectDirectoryRoot: root,
   });
 }
 
 function updateRootRelativeValues(
   state: SourcesState,
-  sources?: $ReadOnlyArray<Source>
+  sources?: Array<Source>
 ) {
   const ids = sources
     ? sources.map(source => source.id)
     : getResourceIds(state.sources);
 
   state = {
     ...state,
   };
@@ -406,17 +384,17 @@ function updateRootRelativeValues(
 function updateLoadedState(
   state: SourcesState,
   action: LoadSourceAction
 ): SourcesState {
   const { sourceId } = action;
 
   // If there was a navigation between the time the action was started and
   // completed, we don't want to update the store.
-  if (action.epoch !== state.epoch || !hasResource(state.sources, sourceId)) {
+  if (action.epoch !== state.epoch || !(sourceId in state.content)) {
     return state;
   }
 
   let content;
   if (action.status === "start") {
     content = asyncValue.pending();
   } else if (action.status === "error") {
     content = asyncValue.rejected(action.error);
@@ -430,22 +408,20 @@ function updateLoadedState(
     content = asyncValue.fulfilled({
       type: "wasm",
       value: action.value.text,
     });
   }
 
   return {
     ...state,
-    sources: updateResources(state.sources, [
-      {
-        id: sourceId,
-        content,
-      },
-    ]),
+    content: {
+      ...state.content,
+      [sourceId]: content,
+    },
   };
 }
 
 function clearSourceMaps(
   state: SourcesState,
   sourceId: SourceId
 ): SourcesState {
   if (!hasResource(state.sources, sourceId)) {
@@ -533,19 +509,17 @@ export function getSourceThreads(
     )
   );
 }
 
 export function getSourceInSources(
   sources: SourceResourceState,
   id: string
 ): ?Source {
-  return hasResource(sources, id)
-    ? getMappedResource(sources, id, resourceAsSourceBase)
-    : null;
+  return hasResource(sources, id) ? getResource(sources, id) : null;
 }
 
 export function getSource(state: OuterState, id: SourceId): ?Source {
   return getSourceInSources(getSources(state), id);
 }
 
 export function getSourceFromId(state: OuterState, id: string): Source {
   const source = getSource(state, id);
@@ -569,19 +543,17 @@ export function getSourceByActorId(
 export function getSourcesByURLInSources(
   sources: SourceResourceState,
   urls: UrlsMap,
   url: string
 ): Source[] {
   if (!url || !urls[url]) {
     return [];
   }
-  return urls[url].map(id =>
-    getMappedResource(sources, id, resourceAsSourceBase)
-  );
+  return urls[url].map(id => getResource(sources, id));
 }
 
 export function getSourcesByURL(state: OuterState, url: string): Source[] {
   return getSourcesByURLInSources(getSources(state), getUrls(state), url);
 }
 
 export function getSourceByURL(state: OuterState, url: string): ?Source {
   const foundSources = getSourcesByURL(state, url);
@@ -690,17 +662,17 @@ export function getHasSiblingOfSameName(
   }
 
   return getSourcesUrlsInSources(state, source.url).length > 1;
 }
 
 const querySourceList: ReduceAllQuery<
   SourceResource,
   Array<Source>
-> = makeReduceAllQuery(resourceAsSourceBase, sources => sources.slice());
+> = makeReduceAllQuery(resourceAsSource, sources => sources.slice());
 
 export function getSources(state: OuterState): SourceResourceState {
   return state.sources.sources;
 }
 
 export function getSourcesEpoch(state: OuterState) {
   return state.sources.epoch;
 }
@@ -757,44 +729,77 @@ export const getSelectedSource: Selector
     return getSourceInSources(sources, selectedLocation.sourceId);
   }
 );
 
 type GSSWC = Selector<?SourceWithContent>;
 export const getSelectedSourceWithContent: GSSWC = createSelector(
   getSelectedLocation,
   getSources,
+  state => state.sources.content,
   (
     selectedLocation: ?SourceLocation,
-    sources: SourceResourceState
+    sources: SourceResourceState,
+    content: SourcesContentMap
   ): SourceWithContent | null => {
     const source =
       selectedLocation &&
       getSourceInSources(sources, selectedLocation.sourceId);
     return source
-      ? getMappedResource(sources, source.id, resourceAsSourceWithContent)
+      ? getSourceWithContentInner(sources, content, source.id)
       : null;
   }
 );
 export function getSourceWithContent(
   state: OuterState,
   id: SourceId
 ): SourceWithContent {
-  return getMappedResource(
+  return getSourceWithContentInner(
     state.sources.sources,
-    id,
-    resourceAsSourceWithContent
+    state.sources.content,
+    id
   );
 }
 export function getSourceContent(
   state: OuterState,
   id: SourceId
 ): SettledValue<SourceContent> | null {
-  const { content } = getResource(state.sources.sources, id);
-  return asyncValue.asSettled(content);
+  // Assert the resource exists.
+  getResource(state.sources.sources, id);
+  const content = state.sources.content[id];
+
+  if (!content || content.state === "pending") {
+    return null;
+  }
+
+  return content;
+}
+
+const contentLookup: WeakMap<Source, SourceWithContent> = new WeakMap();
+function getSourceWithContentInner(
+  sources: SourceResourceState,
+  content: SourcesContentMap,
+  id: SourceId
+): SourceWithContent {
+  const source = getResource(sources, id);
+  let contentValue = content[source.id];
+
+  let result = contentLookup.get(source);
+  if (!result || result.content !== contentValue) {
+    if (contentValue && contentValue.state === "pending") {
+      contentValue = null;
+    }
+    result = {
+      source,
+      content: contentValue,
+    };
+    contentLookup.set(source, result);
+  }
+
+  return result;
 }
 
 export function getSelectedSourceId(state: OuterState) {
   const source = getSelectedSource((state: any));
   return source && source.id;
 }
 
 export function getProjectDirectoryRoot(state: OuterState): string {
@@ -960,46 +965,30 @@ export function getBreakpointPositionsFo
   state: OuterState,
   location: SourceLocation
 ): ?MappedLocation {
   const { sourceId } = location;
   const positions = getBreakpointPositionsForSource(state, sourceId);
   return findPosition(positions, location);
 }
 
-export function getBreakableLines(
-  state: OuterState & SourceActorOuterState,
-  sourceId: string
-): ?Array<number> {
+export function getBreakableLines(state: OuterState, sourceId: string) {
   if (!sourceId) {
     return null;
   }
-  const source = getSource(state, sourceId);
-  if (!source) {
-    return null;
-  }
 
-  if (isOriginalSource(source)) {
-    return state.sources.breakableLines[sourceId];
-  }
-
-  // We pull generated file breakable lines directly from the source actors
-  // so that breakable lines can be added as new source actors on HTML loads.
-  return getBreakableLinesForSourceActors(
-    state.sourceActors,
-    state.sources.actors[sourceId]
-  );
+  return state.sources.breakableLines[sourceId];
 }
 
 export const getSelectedBreakableLines: Selector<Set<number>> = createSelector(
   state => {
     const sourceId = getSelectedSourceId(state);
-    return sourceId && getBreakableLines(state, sourceId);
+    return sourceId && state.sources.breakableLines[sourceId];
   },
   breakableLines => new Set(breakableLines || [])
 );
 
 export function isSourceLoadingOrLoaded(state: OuterState, sourceId: string) {
-  const { content } = getResource(state.sources.sources, sourceId);
+  const content = state.sources.content[sourceId];
   return content !== null;
 }
 
 export default update;
--- a/devtools/client/debugger/src/reducers/tests/sources.spec.js
+++ b/devtools/client/debugger/src/reducers/tests/sources.spec.js
@@ -5,17 +5,17 @@
 // @flow
 declare var describe: (name: string, func: () => void) => void;
 declare var it: (desc: string, func: () => void) => void;
 declare var expect: (value: any) => any;
 
 import update, { initialSourcesState, getDisplayedSources } from "../sources";
 import { initialDebuggeeState } from "../debuggee";
 import updateSourceActors from "../source-actors";
-import type { SourceActor } from "../../types";
+import type { Source, SourceActor } from "../../types";
 import { prefs } from "../../utils/prefs";
 import { makeMockSource, mockcx } from "../../utils/test-mockup";
 import { getResourceIds } from "../../utils/resource";
 
 const extensionSource = {
   ...makeMockSource(),
   id: "extensionId",
   url: "http://example.com/script.js",
@@ -77,17 +77,17 @@ describe("sources reducer", () => {
 describe("sources selectors", () => {
   it("should return all extensions when chrome preference enabled", () => {
     prefs.chromeAndExtenstionsEnabled = true;
     let state = initialSourcesState();
     state = {
       sources: update(state, {
         type: "ADD_SOURCES",
         cx: mockcx,
-        sources: mockedSources,
+        sources: ((mockedSources: any): Source[]),
       }),
       sourceActors: undefined,
     };
     const insertAction = {
       type: "INSERT_SOURCE_ACTORS",
       items: mockSourceActors,
     };
     state = {
@@ -101,17 +101,17 @@ describe("sources selectors", () => {
 
   it("should omit all extensions when chrome preference enabled", () => {
     prefs.chromeAndExtenstionsEnabled = false;
     let state = initialSourcesState();
     state = {
       sources: update(state, {
         type: "ADD_SOURCES",
         cx: mockcx,
-        sources: mockedSources,
+        sources: ((mockedSources: any): Source[]),
       }),
       sourceActors: undefined,
     };
 
     const insertAction = {
       type: "INSERT_SOURCE_ACTORS",
       items: mockSourceActors,
     };
--- a/devtools/client/debugger/src/selectors/visibleColumnBreakpoints.js
+++ b/devtools/client/debugger/src/selectors/visibleColumnBreakpoints.js
@@ -143,31 +143,36 @@ function convertToList(
 ): BreakpointPosition[] {
   return ([].concat(...Object.values(breakpointPositions)): any);
 }
 
 export function getColumnBreakpoints(
   positions: BreakpointPosition[],
   breakpoints: ?(Breakpoint[]),
   viewport: ?Range,
-  selectedSource: ?SourceWithContent
+  selectedSourceWithContent: ?SourceWithContent
 ) {
-  if (!positions || !selectedSource) {
+  if (!positions || !selectedSourceWithContent) {
     return [];
   }
 
+  const {
+    source: selectedSource,
+    content: selectedContent,
+  } = selectedSourceWithContent;
+
   // We only want to show a column breakpoint if several conditions are matched
   // - it is the first breakpoint to appear at an the original location
   // - the position is in the current viewport
   // - there is atleast one other breakpoint on that line
   // - there is a breakpoint on that line
   const breakpointMap = groupBreakpoints(breakpoints, selectedSource);
   positions = filterByLineCount(positions, selectedSource);
   positions = filterVisible(positions, selectedSource, viewport);
-  positions = filterInLine(positions, selectedSource, selectedSource.content);
+  positions = filterInLine(positions, selectedSource, selectedContent);
   positions = filterByBreakpoints(positions, selectedSource, breakpointMap);
 
   return formatPositions(positions, selectedSource, breakpointMap);
 }
 
 const getVisibleBreakpointPositions = createSelector(
   getSelectedSource,
   getBreakpointPositions,
--- a/devtools/client/debugger/src/types.js
+++ b/devtools/client/debugger/src/types.js
@@ -2,19 +2,18 @@
  * 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/>. */
 
 // @flow
 
 import type { SettledValue, FulfilledValue } from "./utils/async-value";
 import type { SourcePayload } from "./client/firefox/types";
 import type { SourceActorId, SourceActor } from "./reducers/source-actors";
-import type { SourceBase } from "./reducers/sources";
 
-export type { SourceActorId, SourceActor, SourceBase };
+export type { SourceActorId, SourceActor };
 
 export type SearchModifiers = {
   caseSensitive: boolean,
   wholeWord: boolean,
   regexMatch: boolean,
 };
 
 export type Mode =
@@ -377,45 +376,45 @@ export type TextSourceContent = {|
   contentType: string | void,
 |};
 export type WasmSourceContent = {|
   type: "wasm",
   value: {| binary: Object |},
 |};
 export type SourceContent = TextSourceContent | WasmSourceContent;
 
-export type SourceWithContent = $ReadOnly<{
-  ...SourceBase,
+export type SourceWithContent = {|
+  source: Source,
   +content: SettledValue<SourceContent> | null,
-}>;
-export type SourceWithContentAndType<+Content: SourceContent> = $ReadOnly<{
-  ...SourceBase,
+|};
+export type SourceWithContentAndType<+Content: SourceContent> = {|
+  source: Source,
   +content: FulfilledValue<Content>,
-}>;
+|};
 
 /**
  * Source
  *
  * @memberof types
  * @static
  */
 
-export type Source = {
+export type Source = {|
   +id: SourceId,
   +url: string,
   +sourceMapURL?: string,
   +isBlackBoxed: boolean,
   +isPrettyPrinted: boolean,
   +relativeUrl: string,
   +introductionUrl: ?string,
   +introductionType: ?string,
   +extensionName: ?string,
   +isExtension: boolean,
   +isWasm: boolean,
-};
+|};
 
 /**
  * Script
  * This describes scripts which are sent to the debug server to be eval'd
  * @memberof types
  * @static
  * FIXME: This needs a real type definition
  */
--- a/devtools/client/debugger/src/utils/async-value.js
+++ b/devtools/client/debugger/src/utils/async-value.js
@@ -23,22 +23,16 @@ export function pending(): PendingValue 
 }
 export function fulfilled<+T>(value: T): FulfilledValue<T> {
   return { state: "fulfilled", value };
 }
 export function rejected(value: mixed): RejectedValue {
   return { state: "rejected", value };
 }
 
-export function asSettled<T>(
-  value: AsyncValue<T> | null
-): SettledValue<T> | null {
-  return value && value.state !== "pending" ? value : null;
-}
-
 export function isPending(value: AsyncValue<mixed>): boolean %checks {
   return value.state === "pending";
 }
 export function isFulfilled(value: AsyncValue<mixed>): boolean %checks {
   return value.state === "fulfilled";
 }
 export function isRejected(value: AsyncValue<mixed>): boolean %checks {
   return value.state === "rejected";
--- a/devtools/client/debugger/src/utils/breakpoint/tests/astBreakpointLocation.spec.js
+++ b/devtools/client/debugger/src/utils/breakpoint/tests/astBreakpointLocation.spec.js
@@ -8,17 +8,19 @@ import { getASTLocation } from "../astBr
 import {
   populateSource,
   populateOriginalSource,
 } from "../../../workers/parser/tests/helpers";
 import { getSymbols } from "../../../workers/parser/getSymbols";
 import cases from "jest-in-case";
 
 async function setup({ file, location, functionName, original }) {
-  const source = original ? populateOriginalSource(file) : populateSource(file);
+  const { source } = original
+    ? populateOriginalSource(file)
+    : populateSource(file);
 
   const symbols = getSymbols(source.id);
 
   const astLocation = getASTLocation(source, symbols, location);
   expect(astLocation.name).toBe(functionName);
   expect(astLocation).toMatchSnapshot();
 }
 
--- a/devtools/client/debugger/src/utils/editor/tests/editor.spec.js
+++ b/devtools/client/debugger/src/utils/editor/tests/editor.spec.js
@@ -21,17 +21,17 @@ import {
   getTextForLine,
   getCursorLine,
 } from "../index";
 
 import { makeMockSource, makeMockSourceAndContent } from "../../test-mockup";
 
 describe("shouldShowPrettyPrint", () => {
   it("shows pretty print for a source", () => {
-    const { content, ...source } = makeMockSourceAndContent(
+    const { source, content } = makeMockSourceAndContent(
       "http://example.com/index.js",
       "test-id-123",
       "text/javascript",
       "some text here"
     );
     expect(shouldShowPrettyPrint(source, content)).toEqual(true);
   });
 });
--- a/devtools/client/debugger/src/utils/function.js
+++ b/devtools/client/debugger/src/utils/function.js
@@ -6,39 +6,39 @@
 import { isFulfilled } from "./async-value";
 import { findClosestFunction } from "./ast";
 import { correctIndentation } from "./indentation";
 import type { SourceWithContent } from "../types";
 import type { Symbols } from "../reducers/ast";
 
 export function findFunctionText(
   line: number,
-  source: SourceWithContent,
+  { source, content }: SourceWithContent,
   symbols: ?Symbols
 ): ?string {
   const func = findClosestFunction(symbols, {
     sourceId: source.id,
     line,
     column: Infinity,
   });
 
   if (
     source.isWasm ||
     !func ||
-    !source.content ||
-    !isFulfilled(source.content) ||
-    source.content.value.type !== "text"
+    !content ||
+    !isFulfilled(content) ||
+    content.value.type !== "text"
   ) {
     return null;
   }
 
   const {
     location: { start, end },
   } = func;
-  const lines = source.content.value.value.split("\n");
+  const lines = content.value.value.split("\n");
   const firstLine = lines[start.line - 1].slice(start.column);
   const lastLine = lines[end.line - 1].slice(0, end.column);
   const middle = lines.slice(start.line, end.line - 1);
   const functionText = [firstLine, ...middle, lastLine].join("\n");
   const indentedFunctionText = correctIndentation(functionText);
 
   return indentedFunctionText;
 }
--- a/devtools/client/debugger/src/utils/isMinified.js
+++ b/devtools/client/debugger/src/utils/isMinified.js
@@ -8,30 +8,26 @@ import type { SourceWithContent } from "
 import { isFulfilled } from "./async-value";
 
 // Used to detect minification for automatic pretty printing
 const SAMPLE_SIZE = 50;
 const INDENT_COUNT_THRESHOLD = 5;
 const CHARACTER_LIMIT = 250;
 const _minifiedCache = new Map();
 
-export function isMinified(source: SourceWithContent) {
+export function isMinified({ source, content }: SourceWithContent) {
   if (_minifiedCache.has(source.id)) {
     return _minifiedCache.get(source.id);
   }
 
-  if (
-    !source.content ||
-    !isFulfilled(source.content) ||
-    source.content.value.type !== "text"
-  ) {
+  if (!content || !isFulfilled(content) || content.value.type !== "text") {
     return false;
   }
 
-  let text = source.content.value.value;
+  let text = content.value.value;
 
   let lineEndIndex = 0;
   let lineStartIndex = 0;
   let lines = 0;
   let indentCount = 0;
   let overCharLimit = false;
 
   // Strip comments.
--- a/devtools/client/debugger/src/utils/memoizableAction.js
+++ b/devtools/client/debugger/src/utils/memoizableAction.js
@@ -1,31 +1,34 @@
 /* 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/>. */
 
 // @flow
 
 import type { ThunkArgs } from "../actions/types";
-import { asSettled, type AsyncValue } from "./async-value";
 
 export type MemoizedAction<
   Args,
   Result
-> = Args => ThunkArgs => Promise<Result | null>;
+> = Args => ThunkArgs => Promise<?Result>;
 type MemoizableActionParams<Args, Result> = {
-  getValue: (args: Args, thunkArgs: ThunkArgs) => AsyncValue<Result> | null,
+  exitEarly?: (args: Args, thunkArgs: ThunkArgs) => boolean,
+  hasValue: (args: Args, thunkArgs: ThunkArgs) => boolean,
+  getValue: (args: Args, thunkArgs: ThunkArgs) => Result,
   createKey: (args: Args, thunkArgs: ThunkArgs) => string,
-  action: (args: Args, thunkArgs: ThunkArgs) => Promise<mixed>,
+  action: (args: Args, thunkArgs: ThunkArgs) => Promise<Result>,
 };
 
 /*
  * memoizableActon is a utility for actions that should only be performed
  * once per key. It is useful for loading sources, parsing symbols ...
  *
+ * @exitEarly - if true, do not attempt to perform the action
+ * @hasValue - checks to see if the result is in the redux store
  * @getValue - gets the result from the redux store
  * @createKey - creates a key for the requests map
  * @action - kicks off the async work for the action
  *
  *
  * For Example
  *
  * export const setItem = memoizeableAction(
@@ -36,52 +39,46 @@ type MemoizableActionParams<Args, Result
  *     createKey: ({ a }) => a,
  *     action: ({ a }, thunkArgs) => doSetItem(a, thunkArgs)
  *   }
  * );
  *
  */
 export function memoizeableAction<Args, Result>(
   name: string,
-  { getValue, createKey, action }: MemoizableActionParams<Args, Result>
+  {
+    hasValue,
+    getValue,
+    createKey,
+    action,
+    exitEarly,
+  }: MemoizableActionParams<Args, Result>
 ): MemoizedAction<Args, Result> {
   const requests = new Map();
-  return args => async thunkArgs => {
-    let result = asSettled(getValue(args, thunkArgs));
-    if (!result) {
-      const key = createKey(args, thunkArgs);
-      if (!requests.has(key)) {
-        requests.set(
-          key,
-          (async () => {
-            try {
-              await action(args, thunkArgs);
-            } catch (e) {
-              console.warn(`Action ${name} had an exception:`, e);
-            } finally {
-              requests.delete(key);
-            }
-          })()
-        );
-      }
+  return args => async (thunkArgs: ThunkArgs) => {
+    if (exitEarly && exitEarly(args, thunkArgs)) {
+      return;
+    }
 
-      await requests.get(key);
-
-      result = asSettled(getValue(args, thunkArgs));
-
-      if (!result) {
-        // Returning null here is not ideal. This means that the action
-        // resolved but 'getValue' didn't return a loaded value, for instance
-        // if the data the action was meant to store was deleted. In a perfect
-        // world we'd throw a ContextError here or handle cancellation somehow.
-        // Throwing will also allow us to change the return type on the action
-        // to always return a promise for the getValue AsyncValue type, but
-        // for now we have to add an additional '| null' for this case.
-        return null;
-      }
+    if (hasValue(args, thunkArgs)) {
+      return getValue(args, thunkArgs);
     }
 
-    if (result.state === "rejected") {
-      throw result.value;
+    const key = createKey(args, thunkArgs);
+    if (!requests.has(key)) {
+      requests.set(
+        key,
+        (async () => {
+          try {
+            await action(args, thunkArgs);
+          } catch (e) {
+            console.warn(`Action ${name} had an exception:`, e);
+          } finally {
+            requests.delete(key);
+          }
+        })()
+      );
     }
-    return result.value;
+
+    await requests.get(key);
+    return getValue(args, thunkArgs);
   };
 }
--- a/devtools/client/debugger/src/utils/test-mockup.js
+++ b/devtools/client/debugger/src/utils/test-mockup.js
@@ -22,22 +22,18 @@ import type {
   SourceId,
   SourceWithContentAndType,
   SourceWithContent,
   TextSourceContent,
   WasmSourceContent,
   Why,
 } from "../types";
 import * as asyncValue from "./async-value";
-import type { SourceBase } from "../reducers/sources";
 
-function makeMockSource(
-  url: string = "url",
-  id: SourceId = "source"
-): SourceBase {
+function makeMockSource(url: string = "url", id: SourceId = "source"): Source {
   return {
     id,
     url,
     isBlackBoxed: false,
     isPrettyPrinted: false,
     relativeUrl: url,
     introductionUrl: null,
     introductionType: undefined,
@@ -51,46 +47,46 @@ function makeMockSourceWithContent(
   url?: string,
   id?: SourceId,
   contentType?: string = "text/javascript",
   text?: string = ""
 ): SourceWithContent {
   const source = makeMockSource(url, id);
 
   return {
-    ...source,
+    source,
     content: text
       ? asyncValue.fulfilled({
           type: "text",
           value: text,
           contentType,
         })
       : null,
   };
 }
 
 function makeMockSourceAndContent(
   url?: string,
   id?: SourceId,
   contentType?: string = "text/javascript",
   text: string = ""
-): { ...SourceBase, content: TextSourceContent } {
+): { source: Source, content: TextSourceContent } {
   const source = makeMockSource(url, id);
 
   return {
-    ...source,
+    source,
     content: {
       type: "text",
       value: text,
       contentType,
     },
   };
 }
 
-function makeMockWasmSource(): SourceBase {
+function makeMockWasmSource(): Source {
   return {
     id: "wasm-source-id",
     url: "url",
     isBlackBoxed: false,
     isPrettyPrinted: false,
     relativeUrl: "url",
     introductionUrl: null,
     introductionType: undefined,
@@ -101,17 +97,17 @@ function makeMockWasmSource(): SourceBas
 }
 
 function makeMockWasmSourceWithContent(text: {|
   binary: Object,
 |}): SourceWithContentAndType<WasmSourceContent> {
   const source = makeMockWasmSource();
 
   return {
-    ...source,
+    source,
     content: asyncValue.fulfilled({
       type: "wasm",
       value: text,
     }),
   };
 }
 
 function makeMockScope(
--- a/devtools/client/debugger/src/utils/tests/ast.spec.js
+++ b/devtools/client/debugger/src/utils/tests/ast.spec.js
@@ -5,17 +5,17 @@
 // @flow
 
 import { findBestMatchExpression } from "../ast";
 
 import { getSymbols } from "../../workers/parser/getSymbols";
 import { populateSource } from "../../workers/parser/tests/helpers";
 
 describe("find the best expression for the token", () => {
-  const source = populateSource("computed-props");
+  const { source } = populateSource("computed-props");
   const symbols = getSymbols(source.id);
 
   it("should find the identifier", () => {
     const expression = findBestMatchExpression(symbols, {
       line: 1,
       column: 13,
     });
     expect(expression).toMatchSnapshot();
--- a/devtools/client/debugger/src/utils/tests/function.spec.js
+++ b/devtools/client/debugger/src/utils/tests/function.spec.js
@@ -8,56 +8,56 @@ import { findFunctionText } from "../fun
 
 import { getSymbols } from "../../workers/parser/getSymbols";
 import { populateOriginalSource } from "../../workers/parser/tests/helpers";
 
 describe("function", () => {
   describe("findFunctionText", () => {
     it("finds function", () => {
       const source = populateOriginalSource("func");
-      const symbols = getSymbols(source.id);
+      const symbols = getSymbols(source.source.id);
       const text = findFunctionText(14, source, symbols);
       expect(text).toMatchSnapshot();
     });
 
     it("finds function signature", () => {
       const source = populateOriginalSource("func");
-      const symbols = getSymbols(source.id);
+      const symbols = getSymbols(source.source.id);
 
       const text = findFunctionText(13, source, symbols);
       expect(text).toMatchSnapshot();
     });
 
     it("misses function closing brace", () => {
       const source = populateOriginalSource("func");
-      const symbols = getSymbols(source.id);
+      const symbols = getSymbols(source.source.id);
 
       const text = findFunctionText(15, source, symbols);
 
       // TODO: we should try and match the closing bracket.
       expect(text).toEqual(null);
     });
 
     it("finds property function", () => {
       const source = populateOriginalSource("func");
-      const symbols = getSymbols(source.id);
+      const symbols = getSymbols(source.source.id);
 
       const text = findFunctionText(29, source, symbols);
       expect(text).toMatchSnapshot();
     });
 
     it("finds class function", () => {
       const source = populateOriginalSource("func");
-      const symbols = getSymbols(source.id);
+      const symbols = getSymbols(source.source.id);
 
       const text = findFunctionText(33, source, symbols);
       expect(text).toMatchSnapshot();
     });
 
     it("cant find function", () => {
       const source = populateOriginalSource("func");
-      const symbols = getSymbols(source.id);
+      const symbols = getSymbols(source.source.id);
 
       const text = findFunctionText(20, source, symbols);
       expect(text).toEqual(null);
     });
   });
 });
--- a/devtools/client/debugger/src/utils/tests/source.spec.js
+++ b/devtools/client/debugger/src/utils/tests/source.spec.js
@@ -232,53 +232,57 @@ describe("sources", () => {
         getFileURL(makeMockSource(`http://${encodedUnicode.repeat(39)}.html`))
       ).toBe(`…ttp://${unicode.repeat(39)}.html`);
     });
   });
 
   describe("isJavaScript", () => {
     it("is not JavaScript", () => {
       {
-        const source = makeMockSourceAndContent("foo.html", undefined, "");
-        expect(isJavaScript(source, source.content)).toBe(false);
+        const { source, content } = makeMockSourceAndContent(
+          "foo.html",
+          undefined,
+          ""
+        );
+        expect(isJavaScript(source, content)).toBe(false);
       }
       {
-        const source = makeMockSourceAndContent(
+        const { source, content } = makeMockSourceAndContent(
           undefined,
           undefined,
           "text/html"
         );
-        expect(isJavaScript(source, source.content)).toBe(false);
+        expect(isJavaScript(source, content)).toBe(false);
       }
     });
 
     it("is JavaScript", () => {
       {
-        const source = makeMockSourceAndContent("foo.js");
-        expect(isJavaScript(source, source.content)).toBe(true);
+        const { source, content } = makeMockSourceAndContent("foo.js");
+        expect(isJavaScript(source, content)).toBe(true);
       }
       {
-        const source = makeMockSourceAndContent("bar.jsm");
-        expect(isJavaScript(source, source.content)).toBe(true);
+        const { source, content } = makeMockSourceAndContent("bar.jsm");
+        expect(isJavaScript(source, content)).toBe(true);
       }
       {
-        const source = makeMockSourceAndContent(
+        const { source, content } = makeMockSourceAndContent(
           undefined,
           undefined,
           "text/javascript"
         );
-        expect(isJavaScript(source, source.content)).toBe(true);
+        expect(isJavaScript(source, content)).toBe(true);
       }
       {
-        const source = makeMockSourceAndContent(
+        const { source, content } = makeMockSourceAndContent(
           undefined,
           undefined,
           "application/javascript"
         );
-        expect(isJavaScript(source, source.content)).toBe(true);
+        expect(isJavaScript(source, content)).toBe(true);
       }
     });
   });
 
   describe("isThirdParty", () => {
     it("node_modules", () => {
       expect(isThirdParty(makeMockSource("/node_modules/foo.js"))).toBe(true);
     });
@@ -291,189 +295,186 @@ describe("sources", () => {
 
     it("not third party", () => {
       expect(isThirdParty(makeMockSource("/bar/foo.js"))).toBe(false);
     });
   });
 
   describe("getMode", () => {
     it("//@flow", () => {
-      const source = makeMockSourceAndContent(
+      const { source, content } = makeMockSourceAndContent(
         undefined,
         undefined,
         "text/javascript",
         "// @flow"
       );
-      expect(getMode(source, source.content)).toEqual({
+      expect(getMode(source, content)).toEqual({
         name: "javascript",
         typescript: true,
       });
     });
 
     it("/* @flow */", () => {
-      const source = makeMockSourceAndContent(
+      const { source, content } = makeMockSourceAndContent(
         undefined,
         undefined,
         "text/javascript",
         "   /* @flow */"
       );
-      expect(getMode(source, source.content)).toEqual({
+      expect(getMode(source, content)).toEqual({
         name: "javascript",
         typescript: true,
       });
     });
 
     it("mixed html", () => {
-      const source = makeMockSourceAndContent(
+      const { source, content } = makeMockSourceAndContent(
         undefined,
         undefined,
         "",
         " <html"
       );
-      expect(getMode(source, source.content)).toEqual({ name: "htmlmixed" });
+      expect(getMode(source, content)).toEqual({ name: "htmlmixed" });
     });
 
     it("elm", () => {
-      const source = makeMockSourceAndContent(
+      const { source, content } = makeMockSourceAndContent(
         undefined,
         undefined,
         "text/x-elm",
         'main = text "Hello, World!"'
       );
-      expect(getMode(source, source.content)).toEqual({ name: "elm" });
+      expect(getMode(source, content)).toEqual({ name: "elm" });
     });
 
     it("returns jsx if contentType jsx is given", () => {
-      const source = makeMockSourceAndContent(
+      const { source, content } = makeMockSourceAndContent(
         undefined,
         undefined,
         "text/jsx",
         "<h1></h1>"
       );
-      expect(getMode(source, source.content)).toEqual({ name: "jsx" });
+      expect(getMode(source, content)).toEqual({ name: "jsx" });
     });
 
     it("returns jsx if sourceMetaData says it's a react component", () => {
-      const source = makeMockSourceAndContent(
+      const { source, content } = makeMockSourceAndContent(
         undefined,
         undefined,
         "",
         "<h1></h1>"
       );
       expect(
-        getMode(source, source.content, {
-          ...defaultSymbolDeclarations,
-          hasJsx: true,
-        })
+        getMode(source, content, { ...defaultSymbolDeclarations, hasJsx: true })
       ).toEqual({ name: "jsx" });
     });
 
     it("returns jsx if the fileExtension is .jsx", () => {
-      const source = makeMockSourceAndContent(
+      const { source, content } = makeMockSourceAndContent(
         "myComponent.jsx",
         undefined,
         "",
         "<h1></h1>"
       );
-      expect(getMode(source, source.content)).toEqual({ name: "jsx" });
+      expect(getMode(source, content)).toEqual({ name: "jsx" });
     });
 
     it("returns text/x-haxe if the file extension is .hx", () => {
-      const source = makeMockSourceAndContent(
+      const { source, content } = makeMockSourceAndContent(
         "myComponent.hx",
         undefined,
         "",
         "function foo(){}"
       );
-      expect(getMode(source, source.content)).toEqual({ name: "text/x-haxe" });
+      expect(getMode(source, content)).toEqual({ name: "text/x-haxe" });
     });
 
     it("typescript", () => {
-      const source = makeMockSourceAndContent(
+      const { source, content } = makeMockSourceAndContent(
         undefined,
         undefined,
         "text/typescript",
         "function foo(){}"
       );
-      expect(getMode(source, source.content)).toEqual({
+      expect(getMode(source, content)).toEqual({
         name: "javascript",
         typescript: true,
       });
     });
 
     it("typescript-jsx", () => {
-      const source = makeMockSourceAndContent(
+      const { source, content } = makeMockSourceAndContent(
         undefined,
         undefined,
         "text/typescript-jsx",
         "<h1></h1>"
       );
-      expect(getMode(source, source.content).base).toEqual({
+      expect(getMode(source, content).base).toEqual({
         name: "javascript",
         typescript: true,
       });
     });
 
     it("cross-platform clojure(script) with reader conditionals", () => {
-      const source = makeMockSourceAndContent(
+      const { source, content } = makeMockSourceAndContent(
         "my-clojurescript-source-with-reader-conditionals.cljc",
         undefined,
         "text/x-clojure",
         "(defn str->int [s] " +
           "  #?(:clj  (java.lang.Integer/parseInt s) " +
           "     :cljs (js/parseInt s)))"
       );
-      expect(getMode(source, source.content)).toEqual({ name: "clojure" });
+      expect(getMode(source, content)).toEqual({ name: "clojure" });
     });
 
     it("clojurescript", () => {
-      const source = makeMockSourceAndContent(
+      const { source, content } = makeMockSourceAndContent(
         "my-clojurescript-source.cljs",
         undefined,
         "text/x-clojurescript",
         "(+ 1 2 3)"
       );
-      expect(getMode(source, source.content)).toEqual({ name: "clojure" });
+      expect(getMode(source, content)).toEqual({ name: "clojure" });
     });
 
     it("coffeescript", () => {
-      const source = makeMockSourceAndContent(
+      const { source, content } = makeMockSourceAndContent(
         undefined,
         undefined,
         "text/coffeescript",
         "x = (a) -> 3"
       );
-      expect(getMode(source, source.content)).toEqual({ name: "coffeescript" });
+      expect(getMode(source, content)).toEqual({ name: "coffeescript" });
     });
 
     it("wasm", () => {
-      const source = makeMockWasmSourceWithContent({
+      const { source, content } = makeMockWasmSourceWithContent({
         binary: "\x00asm\x01\x00\x00\x00",
       });
-      expect(getMode(source, source.content.value)).toEqual({ name: "text" });
+      expect(getMode(source, content.value)).toEqual({ name: "text" });
     });
 
     it("marko", () => {
-      const source = makeMockSourceAndContent(
+      const { source, content } = makeMockSourceAndContent(
         "http://localhost.com:7999/increment/sometestfile.marko",
         undefined,
         "does not matter",
         "function foo(){}"
       );
-      expect(getMode(source, source.content)).toEqual({ name: "javascript" });
+      expect(getMode(source, content)).toEqual({ name: "javascript" });
     });
 
     it("es6", () => {
-      const source = makeMockSourceAndContent(
+      const { source, content } = makeMockSourceAndContent(
         "http://localhost.com:7999/increment/sometestfile.es6",
         undefined,
         "does not matter",
         "function foo(){}"
       );
-      expect(getMode(source, source.content)).toEqual({ name: "javascript" });
+      expect(getMode(source, content)).toEqual({ name: "javascript" });
     });
   });
 
   describe("getSourceLineCount", () => {
     it("should give us the amount bytes for wasm source", () => {
       const { content } = makeMockWasmSourceWithContent({
         binary: "\x00asm\x01\x00\x00\x00",
       });
--- a/devtools/client/debugger/src/utils/tests/wasm.spec.js
+++ b/devtools/client/debugger/src/utils/tests/wasm.spec.js
@@ -32,49 +32,49 @@ describe("wasm", () => {
   const SIMPLE_WASM_NOP_OFFSET = 46;
 
   describe("isWasm", () => {
     it("should give us the false when wasm text was not registered", () => {
       const sourceId = "source.0";
       expect(isWasm(sourceId)).toEqual(false);
     });
     it("should give us the true when wasm text was registered", () => {
-      const source = makeMockWasmSourceWithContent(SIMPLE_WASM);
-      renderWasmText(source.id, source.content.value);
+      const { source, content } = makeMockWasmSourceWithContent(SIMPLE_WASM);
+      renderWasmText(source.id, content.value);
       expect(isWasm(source.id)).toEqual(true);
       // clear shall remove
       clearWasmStates();
       expect(isWasm(source.id)).toEqual(false);
     });
   });
 
   describe("renderWasmText", () => {
     it("render simple wasm", () => {
-      const source = makeMockWasmSourceWithContent(SIMPLE_WASM);
-      const lines = renderWasmText(source.id, source.content.value);
+      const { source, content } = makeMockWasmSourceWithContent(SIMPLE_WASM);
+      const lines = renderWasmText(source.id, content.value);
       expect(lines.join("\n")).toEqual(SIMPLE_WASM_TEXT);
       clearWasmStates();
     });
   });
 
   describe("lineToWasmOffset", () => {
     // Test data sanity check: checking if 'nop' is found in the SIMPLE_WASM.
     expect(SIMPLE_WASM.binary[SIMPLE_WASM_NOP_OFFSET]).toEqual("\x01");
 
     it("get simple wasm nop offset", () => {
-      const source = makeMockWasmSourceWithContent(SIMPLE_WASM);
-      renderWasmText(source.id, source.content.value);
+      const { source, content } = makeMockWasmSourceWithContent(SIMPLE_WASM);
+      renderWasmText(source.id, content.value);
       const offset = lineToWasmOffset(source.id, SIMPLE_WASM_NOP_TEXT_LINE);
       expect(offset).toEqual(SIMPLE_WASM_NOP_OFFSET);
       clearWasmStates();
     });
   });
 
   describe("wasmOffsetToLine", () => {
     it("get simple wasm nop line", () => {
-      const source = makeMockWasmSourceWithContent(SIMPLE_WASM);
-      renderWasmText(source.id, source.content.value);
+      const { source, content } = makeMockWasmSourceWithContent(SIMPLE_WASM);
+      renderWasmText(source.id, content.value);
       const line = wasmOffsetToLine(source.id, SIMPLE_WASM_NOP_OFFSET);
       expect(line).toEqual(SIMPLE_WASM_NOP_TEXT_LINE);
       clearWasmStates();
     });
   });
 });
--- a/devtools/client/debugger/src/workers/parser/tests/findOutOfScopeLocations.spec.js
+++ b/devtools/client/debugger/src/workers/parser/tests/findOutOfScopeLocations.spec.js
@@ -15,58 +15,58 @@ function formatLines(actual) {
       ({ start, end }) =>
         `(${start.line}, ${start.column}) -> (${end.line}, ${end.column})`
     )
     .join("\n");
 }
 
 describe("Parser.findOutOfScopeLocations", () => {
   it("should exclude non-enclosing function blocks", () => {
-    const source = populateSource("outOfScope");
+    const { source } = populateSource("outOfScope");
     const actual = findOutOfScopeLocations(source.id, {
       line: 5,
       column: 5,
     });
 
     expect(formatLines(actual)).toMatchSnapshot();
   });
 
   it("should roll up function blocks", () => {
-    const source = populateSource("outOfScope");
+    const { source } = populateSource("outOfScope");
     const actual = findOutOfScopeLocations(source.id, {
       line: 24,
       column: 0,
     });
 
     expect(formatLines(actual)).toMatchSnapshot();
   });
 
   it("should exclude function for locations on declaration", () => {
-    const source = populateSource("outOfScope");
+    const { source } = populateSource("outOfScope");
     const actual = findOutOfScopeLocations(source.id, {
       line: 3,
       column: 12,
     });
 
     expect(formatLines(actual)).toMatchSnapshot();
   });
 
   it("should treat comments as out of scope", () => {
-    const source = populateSource("outOfScopeComment");
+    const { source } = populateSource("outOfScopeComment");
     const actual = findOutOfScopeLocations(source.id, {
       line: 3,
       column: 2,
     });
 
     expect(actual).toEqual([
       { end: { column: 15, line: 1 }, start: { column: 0, line: 1 } },
     ]);
   });
 
   it("should not exclude in-scope inner locations", () => {
-    const source = populateSource("outOfScope");
+    const { source } = populateSource("outOfScope");
     const actual = findOutOfScopeLocations(source.id, {
       line: 61,
       column: 0,
     });
     expect(formatLines(actual)).toMatchSnapshot();
   });
 });
--- a/devtools/client/debugger/src/workers/parser/tests/framework.spec.js
+++ b/devtools/client/debugger/src/workers/parser/tests/framework.spec.js
@@ -6,17 +6,17 @@
 
 import { getSymbols } from "../getSymbols";
 import { populateOriginalSource } from "./helpers";
 import cases from "jest-in-case";
 
 cases(
   "Parser.getFramework",
   ({ name, file, value }) => {
-    const source = populateOriginalSource("frameworks/plainJavascript");
+    const { source } = populateOriginalSource("frameworks/plainJavascript");
     const symbols = getSymbols(source.id);
     expect(symbols.framework).toBeUndefined();
   },
   [
     {
       name: "is undefined when no framework",
       file: "frameworks/plainJavascript",
       value: undefined,
--- a/devtools/client/debugger/src/workers/parser/tests/getScopes.spec.js
+++ b/devtools/client/debugger/src/workers/parser/tests/getScopes.spec.js
@@ -7,17 +7,17 @@
 
 import getScopes from "../getScopes";
 import { populateOriginalSource } from "./helpers";
 import cases from "jest-in-case";
 
 cases(
   "Parser.getScopes",
   ({ name, file, type, locations }) => {
-    const source = populateOriginalSource(file, type);
+    const { source } = populateOriginalSource(file, type);
 
     locations.forEach(([line, column]) => {
       const scopes = getScopes({
         sourceId: source.id,
         line,
         column,
       });
 
--- a/devtools/client/debugger/src/workers/parser/tests/getSymbols.spec.js
+++ b/devtools/client/debugger/src/workers/parser/tests/getSymbols.spec.js
@@ -7,17 +7,17 @@
 
 import { formatSymbols } from "../utils/formatSymbols";
 import { populateSource, populateOriginalSource } from "./helpers";
 import cases from "jest-in-case";
 
 cases(
   "Parser.getSymbols",
   ({ name, file, original, type }) => {
-    const source = original
+    const { source } = original
       ? populateOriginalSource(file, type)
       : populateSource(file, type);
 
     expect(formatSymbols(source.id)).toMatchSnapshot();
   },
   [
     { name: "es6", file: "es6", original: true },
     { name: "func", file: "func", original: true },
--- a/devtools/client/debugger/src/workers/parser/tests/helpers/index.js
+++ b/devtools/client/debugger/src/workers/parser/tests/helpers/index.js
@@ -5,17 +5,16 @@
 // @flow
 
 import fs from "fs";
 import path from "path";
 
 import type {
   Source,
   TextSourceContent,
-  SourceBase,
   SourceWithContent,
 } from "../../../../types";
 import { makeMockSourceAndContent } from "../../../../utils/test-mockup";
 import { setSource } from "../../sources";
 import * as asyncValue from "../../../../utils/async-value";
 
 export function getFixture(name: string, type: string = "js") {
   return fs.readFileSync(
@@ -43,68 +42,68 @@ function getSourceContent(
   return {
     type: "text",
     value: text,
     contentType,
   };
 }
 
 export function getSource(name: string, type?: string): Source {
-  return getSourceWithContent(name, type);
+  return getSourceWithContent(name, type).source;
 }
 
 export function getSourceWithContent(
   name: string,
   type?: string
-): { ...SourceBase, content: TextSourceContent } {
+): { source: Source, content: TextSourceContent } {
   const { value: text, contentType } = getSourceContent(name, type);
 
   return makeMockSourceAndContent(undefined, name, contentType, text);
 }
 
 export function populateSource(name: string, type?: string): SourceWithContent {
-  const { content, ...source } = getSourceWithContent(name, type);
+  const { source, content } = getSourceWithContent(name, type);
   setSource({
     id: source.id,
     text: content.value,
     contentType: content.contentType,
     isWasm: false,
   });
   return {
-    ...source,
+    source,
     content: asyncValue.fulfilled(content),
   };
 }
 
 export function getOriginalSource(name: string, type?: string): Source {
-  return getOriginalSourceWithContent(name, type);
+  return getOriginalSourceWithContent(name, type).source;
 }
 
 export function getOriginalSourceWithContent(
   name: string,
   type?: string
-): { ...SourceBase, content: TextSourceContent } {
+): { source: Source, content: TextSourceContent } {
   const { value: text, contentType } = getSourceContent(name, type);
 
   return makeMockSourceAndContent(
     undefined,
     `${name}/originalSource-1`,
     contentType,
     text
   );
 }
 
 export function populateOriginalSource(
   name: string,
   type?: string
 ): SourceWithContent {
-  const { content, ...source } = getOriginalSourceWithContent(name, type);
+  const { source, content } = getOriginalSourceWithContent(name, type);
   setSource({
     id: source.id,
     text: content.value,
     contentType: content.contentType,
     isWasm: false,
   });
   return {
-    ...source,
+    source,
     content: asyncValue.fulfilled(content),
   };
 }
--- a/devtools/client/debugger/src/workers/parser/tests/steps.spec.js
+++ b/devtools/client/debugger/src/workers/parser/tests/steps.spec.js
@@ -5,56 +5,56 @@
 // @flow
 
 import { getNextStep } from "../steps";
 import { populateSource } from "./helpers";
 
 describe("getNextStep", () => {
   describe("await", () => {
     it("first await call", () => {
-      const source = populateSource("async");
+      const { source } = populateSource("async");
       const pausePosition = { line: 8, column: 2, sourceId: source.id };
       expect(getNextStep(source.id, pausePosition)).toEqual({
         ...pausePosition,
         line: 9,
       });
     });
 
     it("first await call expression", () => {
-      const source = populateSource("async");
+      const { source } = populateSource("async");
       const pausePosition = { line: 8, column: 9, sourceId: source.id };
       expect(getNextStep(source.id, pausePosition)).toEqual({
         ...pausePosition,
         line: 9,
         column: 2,
       });
     });
 
     it("second await call", () => {
-      const source = populateSource("async");
+      const { source } = populateSource("async");
       const pausePosition = { line: 9, column: 2, sourceId: source.id };
       expect(getNextStep(source.id, pausePosition)).toEqual(null);
     });
 
     it("second call expression", () => {
-      const source = populateSource("async");
+      const { source } = populateSource("async");
       const pausePosition = { line: 9, column: 9, sourceId: source.id };
       expect(getNextStep(source.id, pausePosition)).toEqual(null);
     });
   });
 
   describe("yield", () => {
     it("first yield call", () => {
-      const source = populateSource("generators");
+      const { source } = populateSource("generators");
       const pausePosition = { line: 2, column: 2, sourceId: source.id };
       expect(getNextStep(source.id, pausePosition)).toEqual({
         ...pausePosition,
         line: 3,
       });
     });
 
     it("second yield call", () => {
-      const source = populateSource("generators");
+      const { source } = populateSource("generators");
       const pausePosition = { line: 3, column: 2, sourceId: source.id };
       expect(getNextStep(source.id, pausePosition)).toEqual(null);
     });
   });
 });
--- a/devtools/client/debugger/src/workers/parser/utils/tests/ast.spec.js
+++ b/devtools/client/debugger/src/workers/parser/utils/tests/ast.spec.js
@@ -18,21 +18,26 @@ const astKeys = [
   "program",
   "comments",
   "tokens",
 ];
 
 cases(
   "ast.getAst",
   ({ name }) => {
-    const source = makeMockSourceAndContent(undefined, "foo", name, "2");
+    const { source, content } = makeMockSourceAndContent(
+      undefined,
+      "foo",
+      name,
+      "2"
+    );
     setSource({
       id: source.id,
-      text: source.content.value || "",
-      contentType: source.content.contentType,
+      text: content.value || "",
+      contentType: content.contentType,
       isWasm: false,
     });
     const ast = getAst("foo");
     expect(ast && Object.keys(ast)).toEqual(astKeys);
   },
   [
     { name: "text/javascript" },
     { name: "application/javascript" },
--- a/devtools/client/debugger/test/mochitest/helpers.js
+++ b/devtools/client/debugger/test/mochitest/helpers.js
@@ -236,18 +236,18 @@ function waitForSelectedSource(dbg, url)
     getSelectedSourceWithContent,
     hasSymbols,
     getBreakableLines,
   } = dbg.selectors;
 
   return waitForState(
     dbg,
     state => {
-      const source = getSelectedSourceWithContent() || {};
-      if (!source.content) {
+      const { source, content } = getSelectedSourceWithContent() || {};
+      if (!content) {
         return false;
       }
 
       if (!url) {
         return true;
       }
 
       const newSource = findSource(dbg, url, { silent: true });
@@ -313,18 +313,19 @@ function assertPausedLocation(dbg) {
   assertDebugLine(dbg, pauseLine);
 
   ok(isVisibleInEditor(dbg, getCM(dbg).display.gutters), "gutter is visible");
 }
 
 function assertDebugLine(dbg, line) {
   // Check the debug line
   const lineInfo = getCM(dbg).lineInfo(line - 1);
-  const source = dbg.selectors.getSelectedSourceWithContent() || {};
-  if (source && !source.content) {
+  const { source, content } =
+    dbg.selectors.getSelectedSourceWithContent() || {};
+  if (source && !content) {
     const url = source.url;
     ok(
       false,
       `Looks like the source ${url} is still loading. Try adding waitForLoadedSource in the test.`
     );
     return;
   }
 
@@ -509,19 +510,24 @@ function waitForTime(ms) {
 }
 
 function isSelectedFrameSelected(dbg, state) {
   const frame = dbg.selectors.getVisibleSelectedFrame();
 
   // Make sure the source text is completely loaded for the
   // source we are paused in.
   const sourceId = frame.location.sourceId;
-  const source = dbg.selectors.getSelectedSourceWithContent() || {};
+  const { source, content } =
+    dbg.selectors.getSelectedSourceWithContent() || {};
 
-  if (!source || !source.content) {
+  if (!source) {
+    return false;
+  }
+
+  if (!content) {
     return false;
   }
 
   return source.id == sourceId;
 }
 
 /**
  * Clear all the debugger related preferences.