Bug 1546202 - Part 5: Add a new selector to handle both Source and SourceContent datatypes. r=jlast
authorLogan Smyth <loganfsmyth@gmail.com>
Tue, 23 Apr 2019 18:00:32 +0000
changeset 470528 55ac8a74ae588ba1ea7760c53512ad4fc42b51d0
parent 470527 ed79e89eea8d82a8a7d4c3e3a673f036d9532f81
child 470529 f81c7ca6d292fdd90c810576ea888132c3a11803
push id35906
push useraciure@mozilla.com
push dateTue, 23 Apr 2019 22:14:56 +0000
treeherdermozilla-central@0ce3633f8b80 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjlast
bugs1546202
milestone68.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1546202 - Part 5: Add a new selector to handle both Source and SourceContent datatypes. r=jlast Depends on D28411 Differential Revision: https://phabricator.services.mozilla.com/D28412
devtools/client/debugger/bin/module-manifest.json
devtools/client/debugger/dist/search-worker.js
devtools/client/debugger/src/actions/ast.js
devtools/client/debugger/src/actions/ast/setInScopeLines.js
devtools/client/debugger/src/actions/breakpoints/modify.js
devtools/client/debugger/src/actions/file-search.js
devtools/client/debugger/src/actions/pause/commands.js
devtools/client/debugger/src/actions/pause/mapScopes.js
devtools/client/debugger/src/actions/pause/tests/pause.spec.js
devtools/client/debugger/src/actions/project-text-search.js
devtools/client/debugger/src/actions/sources/loadSourceText.js
devtools/client/debugger/src/actions/sources/prettyPrint.js
devtools/client/debugger/src/actions/sources/select.js
devtools/client/debugger/src/actions/sources/symbols.js
devtools/client/debugger/src/actions/sources/tests/__snapshots__/prettyPrint.spec.js.snap
devtools/client/debugger/src/actions/sources/tests/loadSource.spec.js
devtools/client/debugger/src/actions/sources/tests/prettyPrint.spec.js
devtools/client/debugger/src/actions/sources/tests/select.spec.js
devtools/client/debugger/src/actions/types/SourceAction.js
devtools/client/debugger/src/components/Editor/DebugLine.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/SearchBar.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/Editor/tests/SearchBar.spec.js
devtools/client/debugger/src/components/PrimaryPanes/Outline.js
devtools/client/debugger/src/components/QuickOpenModal.js
devtools/client/debugger/src/components/test/QuickOpenModal.spec.js
devtools/client/debugger/src/reducers/sources.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/index.js
devtools/client/debugger/src/utils/editor/source-documents.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/moz.build
devtools/client/debugger/src/utils/pause/mapScopes/index.js
devtools/client/debugger/src/utils/source.js
devtools/client/debugger/src/utils/sources-tree/tests/__snapshots__/updateTree.spec.js.snap
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/isMinified.spec.js
devtools/client/debugger/src/utils/tests/source.spec.js
devtools/client/debugger/src/utils/tests/wasm.spec.js
devtools/client/debugger/src/utils/utils.js
devtools/client/debugger/src/utils/wasm.js
devtools/client/debugger/src/workers/parser/index.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/src/workers/pretty-print/index.js
devtools/client/debugger/src/workers/search/project-search.js
devtools/client/debugger/src/workers/search/tests/project-search.spec.js
devtools/client/debugger/test/mochitest/.eslintrc
devtools/client/debugger/test/mochitest/browser_dbg-editor-highlight.js
devtools/client/debugger/test/mochitest/browser_dbg-inline-cache.js
devtools/client/debugger/test/mochitest/helpers.js
testing/talos/talos/tests/devtools/addon/content/tests/debugger/debugger-helpers.js
--- a/devtools/client/debugger/bin/module-manifest.json
+++ b/devtools/client/debugger/bin/module-manifest.json
@@ -10,17 +10,17 @@
           "0": 0,
           "1": 1
         }
       },
       "chunks": {
         "byName": {},
         "byBlocks": {},
         "usedIds": {
-          "1": 1
+          "0": 0
         }
       }
     }
   ],
   "extract-text-webpack-plugin ../../extract-text-webpack-plugin/dist ../../css-loader/index.js??ref--3-1!../../postcss-loader/lib/index.js!../../react-aria-components/src/tabs/tab.css": [
     {
       "modules": {
         "byIdentifier": {
@@ -31,17 +31,17 @@
           "0": 0,
           "1": 1
         }
       },
       "chunks": {
         "byName": {},
         "byBlocks": {},
         "usedIds": {
-          "1": 1
+          "0": 0
         }
       }
     }
   ],
   "extract-text-webpack-plugin ../../extract-text-webpack-plugin/dist ../../css-loader/index.js??ref--3-1!../../postcss-loader/lib/index.js!../../react-aria-components/src/tabs/tab-list.css": [
     {
       "modules": {
         "byIdentifier": {
@@ -52,17 +52,17 @@
           "0": 0,
           "1": 1
         }
       },
       "chunks": {
         "byName": {},
         "byBlocks": {},
         "usedIds": {
-          "1": 1
+          "0": 0
         }
       }
     }
   ],
   "extract-text-webpack-plugin ../../extract-text-webpack-plugin/dist ../../css-loader/index.js??ref--3-1!../../postcss-loader/lib/index.js!../../devtools-contextmenu/menu.css": [
     {
       "modules": {
         "byIdentifier": {
@@ -73,17 +73,17 @@
           "0": 0,
           "1": 1
         }
       },
       "chunks": {
         "byName": {},
         "byBlocks": {},
         "usedIds": {
-          "1": 1
+          "0": 0
         }
       }
     }
   ],
   "extract-text-webpack-plugin ../../extract-text-webpack-plugin/dist ../../css-loader/index.js??ref--3-1!../../postcss-loader/lib/index.js!../../../packages/devtools-components/src/tree.css": [
     {
       "modules": {
         "byIdentifier": {
@@ -94,17 +94,17 @@
           "0": 0,
           "1": 1
         }
       },
       "chunks": {
         "byName": {},
         "byBlocks": {},
         "usedIds": {
-          "1": 1
+          "0": 0
         }
       }
     }
   ],
   "extract-text-webpack-plugin ../../extract-text-webpack-plugin/dist ../../css-loader/index.js??ref--3-1!../../postcss-loader/lib/index.js!../../../packages/devtools-reps/src/object-inspector/components/ObjectInspector.css": [
     {
       "modules": {
         "byIdentifier": {
@@ -115,17 +115,17 @@
           "0": 0,
           "1": 1
         }
       },
       "chunks": {
         "byName": {},
         "byBlocks": {},
         "usedIds": {
-          "1": 1
+          "0": 0
         }
       }
     }
   ],
   "extract-text-webpack-plugin ../../extract-text-webpack-plugin/dist ../../css-loader/index.js??ref--3-1!../../postcss-loader/lib/index.js!../../../packages/devtools-reps/src/reps/reps.css": [
     {
       "modules": {
         "byIdentifier": {
@@ -136,17 +136,17 @@
           "0": 0,
           "1": 1
         }
       },
       "chunks": {
         "byName": {},
         "byBlocks": {},
         "usedIds": {
-          "1": 1
+          "0": 0
         }
       }
     }
   ],
   "modules": {
     "byIdentifier": {
       "external \"devtools/client/shared/vendor/react-prop-types\"": 0,
       "external \"devtools/client/shared/vendor/react-dom-factories\"": 1,
--- a/devtools/client/debugger/dist/search-worker.js
+++ b/devtools/client/debugger/dist/search-worker.js
@@ -924,34 +924,34 @@ Object.defineProperty(exports, "__esModu
 exports.findSourceMatches = findSourceMatches;
 
 var _getMatches = __webpack_require__(170);
 
 var _getMatches2 = _interopRequireDefault(_getMatches);
 
 function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
 
-function findSourceMatches(source, queryText) {
-  const { id, loadedState, text } = source;
-  if (loadedState != "loaded" || typeof text != "string" || queryText == "") {
+function findSourceMatches(sourceId, content, queryText) {
+  if (queryText == "") {
     return [];
   }
 
   const modifiers = {
     caseSensitive: false,
     regexMatch: false,
     wholeWord: false
   };
 
+  const text = content.value;
   const lines = text.split("\n");
 
   return (0, _getMatches2.default)(queryText, text, modifiers).map(({ line, ch }) => {
     const { value, matchIndex } = truncateLine(lines[line], ch);
     return {
-      sourceId: id,
+      sourceId,
       line: line + 1,
       column: ch,
       matchIndex,
       match: queryText,
       value
     };
   });
 }
--- a/devtools/client/debugger/src/actions/ast.js
+++ b/devtools/client/debugger/src/actions/ast.js
@@ -1,35 +1,36 @@
 /* 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 { getSourceFromId, getSelectedLocation } from "../selectors";
+import { getSourceWithContent, getSelectedLocation } from "../selectors";
 
 import { setInScopeLines } from "./ast/setInScopeLines";
 
 import * as parser from "../workers/parser";
 
-import { isLoaded } from "../utils/source";
-
 import type { Context } from "../types";
 import type { ThunkArgs, Action } from "./types";
 
 export function setOutOfScopeLocations(cx: Context) {
   return async ({ dispatch, getState }: ThunkArgs) => {
     const location = getSelectedLocation(getState());
     if (!location) {
       return;
     }
 
-    const source = getSourceFromId(getState(), location.sourceId);
+    const { source, content } = getSourceWithContent(
+      getState(),
+      location.sourceId
+    );
 
-    if (!isLoaded(source)) {
+    if (!content) {
       return;
     }
 
     let locations = null;
     if (location.line && source && !source.isWasm) {
       locations = await parser.findOutOfScopeLocations(
         source.id,
         ((location: any): parser.AstPosition)
--- a/devtools/client/debugger/src/actions/ast/setInScopeLines.js
+++ b/devtools/client/debugger/src/actions/ast/setInScopeLines.js
@@ -1,18 +1,22 @@
 /* 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 { getOutOfScopeLocations, getSelectedSource } from "../../selectors";
+import {
+  getOutOfScopeLocations,
+  getSelectedSourceWithContent
+} from "../../selectors";
 import { getSourceLineCount } from "../../utils/source";
 
 import { range, flatMap, uniq, without } from "lodash";
+import { isFulfilled } from "../../utils/async-value";
 
 import type { AstLocation } from "../../workers/parser";
 import type { ThunkArgs } from "../types";
 import type { Context } from "../../types";
 
 function getOutOfScopeLines(outOfScopeLocations: ?(AstLocation[])) {
   if (!outOfScopeLocations) {
     return null;
@@ -22,26 +26,29 @@ function getOutOfScopeLines(outOfScopeLo
     flatMap(outOfScopeLocations, location =>
       range(location.start.line, location.end.line)
     )
   );
 }
 
 export function setInScopeLines(cx: Context) {
   return ({ dispatch, getState }: ThunkArgs) => {
-    const source = getSelectedSource(getState());
+    const sourceWithContent = getSelectedSourceWithContent(getState());
     const outOfScopeLocations = getOutOfScopeLocations(getState());
 
-    if (!source || !source.text) {
+    if (!sourceWithContent || !sourceWithContent.content) {
       return;
     }
+    const content = sourceWithContent.content;
 
     const linesOutOfScope = getOutOfScopeLines(outOfScopeLocations);
 
-    const sourceNumLines = getSourceLineCount(source);
+    const sourceNumLines = isFulfilled(content)
+      ? getSourceLineCount(content.value)
+      : 0;
     const sourceLines = range(1, sourceNumLines + 1);
 
     const inScopeLines = !linesOutOfScope
       ? sourceLines
       : without(sourceLines, ...linesOutOfScope);
 
     dispatch({
       type: "IN_SCOPE_LINES",
--- a/devtools/client/debugger/src/actions/breakpoints/modify.js
+++ b/devtools/client/debugger/src/actions/breakpoints/modify.js
@@ -11,16 +11,17 @@ import {
 } from "../../utils/breakpoint";
 
 import {
   getBreakpoint,
   getBreakpointPositionsForLocation,
   getFirstBreakpointPosition,
   getSymbols,
   getSource,
+  getSourceContent,
   getBreakpointsList,
   getPendingBreakpointList
 } from "../../selectors";
 
 import { setBreakpointPositions } from "./breakpointPositions";
 
 import { recordEvent } from "../../utils/telemetry";
 import { comparePosition } from "../../utils/location";
@@ -125,18 +126,29 @@ export function addBreakpoint(
 
     if (!source || !generatedSource) {
       return;
     }
 
     const symbols = getSymbols(getState(), source);
     const astLocation = getASTLocation(source, symbols, location);
 
-    const originalText = getTextAtPosition(source, location);
-    const text = getTextAtPosition(generatedSource, generatedLocation);
+    const originalContent = getSourceContent(getState(), source.id);
+    const originalText = getTextAtPosition(
+      source.id,
+      originalContent,
+      location
+    );
+
+    const content = getSourceContent(getState(), generatedSource.id);
+    const text = getTextAtPosition(
+      generatedSource.id,
+      content,
+      generatedLocation
+    );
 
     const id = makeBreakpointId(location);
     const breakpoint = {
       id,
       disabled,
       options,
       location,
       astLocation,
--- a/devtools/client/debugger/src/actions/file-search.js
+++ b/devtools/client/debugger/src/actions/file-search.js
@@ -7,57 +7,58 @@
 import {
   clearSearch,
   find,
   findNext,
   findPrev,
   removeOverlay,
   searchSourceForHighlight
 } from "../utils/editor";
-import { isWasm, renderWasmText } from "../utils/wasm";
+import { renderWasmText } from "../utils/wasm";
 import { getMatches } from "../workers/search";
 import type { Action, FileTextSearchModifier, ThunkArgs } from "./types";
-import type { WasmSource, Context } from "../types";
+import type { Context } from "../types";
 
 import {
-  getSelectedSource,
+  getSelectedSourceWithContent,
   getFileSearchModifiers,
   getFileSearchQuery,
   getFileSearchResults
 } from "../selectors";
 
 import {
   closeActiveSearch,
   clearHighlightLineRange,
   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 = getSelectedSource(getState());
-    if (!selectedSource || !selectedSource.text) {
+    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 = getSelectedSource(getState());
-    if (!selectedSource || !selectedSource.text) {
+    const selectedSourceWithContent = getSelectedSourceWithContent(getState());
+    if (!selectedSourceWithContent || !selectedSourceWithContent.content) {
       return;
     }
     dispatch(searchContentsForHighlight(query, editor, line, ch));
   };
 }
 
 export function setFileSearchQuery(cx: Context, query: string): Action {
   return {
@@ -94,34 +95,43 @@ export function updateSearchResults(
       index: characterIndex
     }
   };
 }
 
 export function searchContents(cx: Context, query: string, editor: Object) {
   return async ({ getState, dispatch }: ThunkArgs) => {
     const modifiers = getFileSearchModifiers(getState());
-    const selectedSource = getSelectedSource(getState());
+    const selectedSourceWithContent = getSelectedSourceWithContent(getState());
 
-    if (!editor || !selectedSource || !selectedSource.text || !modifiers) {
+    if (
+      !editor ||
+      !selectedSourceWithContent ||
+      !selectedSourceWithContent.content ||
+      !isFulfilled(selectedSourceWithContent.content) ||
+      !modifiers
+    ) {
       return;
     }
+    const selectedSource = selectedSourceWithContent.source;
+    const selectedContent = selectedSourceWithContent.content.value;
 
     const ctx = { ed: editor, cm: editor.codeMirror };
 
     if (!query) {
       clearSearch(ctx.cm, query);
       return;
     }
 
     const _modifiers = modifiers.toJS();
-    let text = selectedSource.text;
-
-    if (isWasm(selectedSource.id)) {
-      text = renderWasmText(((selectedSource: any): WasmSource)).join("\n");
+    let text;
+    if (selectedContent.type === "wasm") {
+      text = renderWasmText(selectedSource.id, selectedContent).join("\n");
+    } else {
+      text = selectedContent.value;
     }
 
     const matches = await getMatches(query, text, _modifiers);
 
     const res = find(ctx, query, true, _modifiers);
     if (!res) {
       return;
     }
@@ -135,23 +145,23 @@ export function searchContents(cx: Conte
 export function searchContentsForHighlight(
   query: string,
   editor: Object,
   line: number,
   ch: number
 ) {
   return async ({ getState, dispatch }: ThunkArgs) => {
     const modifiers = getFileSearchModifiers(getState());
-    const selectedSource = getSelectedSource(getState());
+    const selectedSource = getSelectedSourceWithContent(getState());
 
     if (
       !query ||
       !editor ||
       !selectedSource ||
-      !selectedSource.text ||
+      !selectedSource.content ||
       !modifiers
     ) {
       return;
     }
 
     const ctx = { ed: editor, cm: editor.codeMirror };
     const _modifiers = modifiers.toJS();
 
--- a/devtools/client/debugger/src/actions/pause/commands.js
+++ b/devtools/client/debugger/src/actions/pause/commands.js
@@ -2,31 +2,38 @@
 /* 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 {
   getSource,
+  getSourceContent,
   getTopFrame,
   getSelectedFrame,
   getThreadContext
 } from "../../selectors";
 import { PROMISE } from "../utils/middleware/promise";
 import { getNextStep } from "../../workers/parser";
 import { addHiddenBreakpoint } from "../breakpoints";
 import { evaluateExpressions } from "../expressions";
 import { selectLocation } from "../sources";
 import { fetchScopes } from "./fetchScopes";
 import { features } from "../../utils/prefs";
 import { recordEvent } from "../../utils/telemetry";
 import assert from "../../utils/assert";
+import { isFulfilled, type AsyncValue } from "../../utils/async-value";
 
-import type { Source, ThreadId, Context, ThreadContext } from "../../types";
+import type {
+  SourceContent,
+  ThreadId,
+  Context,
+  ThreadContext
+} from "../../types";
 import type { ThunkArgs } from "../types";
 import type { Command } from "../../reducers/types";
 
 export function selectThread(cx: Context, thread: ThreadId) {
   return async ({ dispatch, getState, client }: ThunkArgs) => {
     await dispatch({ cx, type: "SELECT_THREAD", thread });
 
     // Get a new context now that the current thread has changed.
@@ -177,23 +184,23 @@ export function reverseStepOut(cx: Threa
   };
 }
 
 /*
  * Checks for await or yield calls on the paused line
  * This avoids potentially expensive parser calls when we are likely
  * not at an async expression.
  */
-function hasAwait(source: Source, pauseLocation) {
+function hasAwait(content: AsyncValue<SourceContent> | null, pauseLocation) {
   const { line, column } = pauseLocation;
-  if (source.isWasm || !source.text) {
+  if (!content || !isFulfilled(content) || content.value.type !== "text") {
     return false;
   }
 
-  const lineText = source.text.split("\n")[line - 1];
+  const lineText = content.value.value.split("\n")[line - 1];
 
   if (!lineText) {
     return false;
   }
 
   const snippet = lineText.slice(column - 50, column + 50);
 
   return !!snippet.match(/(yield|await)/);
@@ -210,18 +217,19 @@ export function astCommand(cx: ThreadCon
     if (!features.asyncStepping) {
       return dispatch(command(cx, stepType));
     }
 
     if (stepType == "stepOver") {
       // This type definition is ambiguous:
       const frame: any = getTopFrame(getState(), cx.thread);
       const source = getSource(getState(), frame.location.sourceId);
+      const content = source ? getSourceContent(getState(), source.id) : null;
 
-      if (source && hasAwait(source, frame.location)) {
+      if (source && hasAwait(content, frame.location)) {
         const nextLocation = await getNextStep(source.id, frame.location);
         if (nextLocation) {
           await dispatch(addHiddenBreakpoint(cx, nextLocation));
           return dispatch(command(cx, "resume"));
         }
       }
     }
 
--- a/devtools/client/debugger/src/actions/pause/mapScopes.js
+++ b/devtools/client/debugger/src/actions/pause/mapScopes.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 {
   getSource,
+  getSourceContent,
   isMapScopesEnabled,
   getSelectedFrame,
   getSelectedGeneratedScope,
   getSelectedOriginalScope,
   getThreadContext
 } from "../../selectors";
 import { loadSourceText } from "../sources/loadSourceText";
 import { PROMISE } from "../utils/middleware/promise";
@@ -18,16 +19,17 @@ import assert from "../../utils/assert";
 
 import { log } from "../../utils/log";
 import { isGenerated, isOriginal } from "../../utils/source";
 import type { Frame, Scope, ThreadContext } from "../../types";
 
 import type { ThunkArgs } from "../types";
 
 import { buildMappedScopes } from "../../utils/pause/mapScopes";
+import { isFulfilled } from "../../utils/async-value";
 
 export function toggleMapScopes() {
   return async function({ dispatch, getState, client, sourceMaps }: ThunkArgs) {
     if (isMapScopesEnabled(getState())) {
       return dispatch({ type: "TOGGLE_MAP_SCOPES", mapScopes: false });
     }
 
     dispatch({ type: "TOGGLE_MAP_SCOPES", mapScopes: true });
@@ -81,18 +83,25 @@ export function mapScopes(
         }
 
         await dispatch(loadSourceText({ cx, source }));
         if (isOriginal(source)) {
           await dispatch(loadSourceText({ cx, source: generatedSource }));
         }
 
         try {
+          const content =
+            getSource(getState(), source.id) &&
+            getSourceContent(getState(), source.id);
+
           return await buildMappedScopes(
             source,
+            content && isFulfilled(content)
+              ? content.value
+              : { type: "text", value: "", contentType: undefined },
             frame,
             await scopes,
             sourceMaps,
             client
           );
         } catch (e) {
           log(e);
           return null;
--- a/devtools/client/debugger/src/actions/pause/tests/pause.spec.js
+++ b/devtools/client/debugger/src/actions/pause/tests/pause.spec.js
@@ -254,17 +254,17 @@ describe("pause", () => {
         line: 3,
         column: 0
       };
 
       const sourceMapsMock = {
         getOriginalLocation: () => Promise.resolve(originalLocation),
         getOriginalLocations: async items => items,
         getOriginalSourceText: async () => ({
-          source: "\n\nfunction fooOriginal() {\n  return -5;\n}",
+          text: "\n\nfunction fooOriginal() {\n  return -5;\n}",
           contentType: "text/javascript"
         }),
         getGeneratedLocation: async location => location
       };
 
       const store = createStore(mockThreadClient, {}, sourceMapsMock);
       const { dispatch, getState } = store;
       const mockPauseInfo = createPauseInfo(generatedLocation);
@@ -315,17 +315,17 @@ describe("pause", () => {
         }
       ];
 
       const sourceMapsMock = {
         getOriginalStackFrames: loc => Promise.resolve(originStackFrames),
         getOriginalLocation: () => Promise.resolve(originalLocation),
         getOriginalLocations: async items => items,
         getOriginalSourceText: async () => ({
-          source: "fn fooBar() {}\nfn barZoo() { fooBar() }",
+          text: "fn fooBar() {}\nfn barZoo() { fooBar() }",
           contentType: "text/rust"
         }),
         getGeneratedRangesForOriginal: async () => []
       };
 
       const store = createStore(mockThreadClient, {}, sourceMapsMock);
       const { dispatch, getState } = store;
       const mockPauseInfo = createPauseInfo(generatedLocation);
--- a/devtools/client/debugger/src/actions/project-text-search.js
+++ b/devtools/client/debugger/src/actions/project-text-search.js
@@ -4,18 +4,24 @@
 
 // @flow
 
 /**
  * Redux actions for the search state
  * @module actions/search
  */
 
+import { isFulfilled } from "../utils/async-value";
 import { findSourceMatches } from "../workers/search";
-import { getSource, hasPrettySource, getSourceList } from "../selectors";
+import {
+  getSource,
+  hasPrettySource,
+  getSourceList,
+  getSourceContent
+} from "../selectors";
 import { isThirdParty } from "../utils/source";
 import { loadSourceText } from "./sources/loadSourceText";
 import {
   statusType,
   getTextSearchOperation,
   getTextSearchStatus
 } from "../reducers/project-text-search";
 
@@ -109,15 +115,19 @@ export function searchSources(cx: Contex
 
 export function searchSource(cx: Context, sourceId: string, query: string) {
   return async ({ dispatch, getState }: ThunkArgs) => {
     const source = getSource(getState(), sourceId);
     if (!source) {
       return;
     }
 
-    const matches = await findSourceMatches(source, query);
+    const content = getSourceContent(getState(), source.id);
+    let matches = [];
+    if (content && isFulfilled(content) && content.value.type === "text") {
+      matches = await findSourceMatches(source.id, content.value, query);
+    }
     if (!matches.length) {
       return;
     }
     dispatch(addSearchResult(cx, source.id, source.url, matches));
   };
 }
--- a/devtools/client/debugger/src/actions/sources/loadSourceText.js
+++ b/devtools/client/debugger/src/actions/sources/loadSourceText.js
@@ -3,36 +3,38 @@
  * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
 
 // @flow
 
 import { PROMISE } from "../utils/middleware/promise";
 import {
   getSource,
   getSourceFromId,
+  getSourceWithContent,
+  getSourceContent,
   getGeneratedSource,
   getSourcesEpoch,
   getBreakpointsForSource,
   getSourceActorsForSource
 } from "../../selectors";
 import { setBreakpointPositions, addBreakpoint } from "../breakpoints";
 
 import { prettyPrintSource } from "./prettyPrint";
+import { isFulfilled } from "../../utils/async-value";
 
 import * as parser from "../../workers/parser";
-import { isLoaded, isOriginal, isPretty } from "../../utils/source";
+import { isOriginal, isPretty } from "../../utils/source";
 import {
   memoizeableAction,
   type MemoizedAction
 } from "../../utils/memoizableAction";
 
 import { Telemetry } from "devtools-modules";
 
 import type { ThunkArgs } from "../types";
-
 import type { Source, Context } from "../../types";
 
 // Measures the time it takes for a source to load
 const loadSourceHistogram = "DEVTOOLS_DEBUGGER_LOAD_SOURCE_MS";
 const telemetry = new Telemetry();
 
 async function loadSource(
   state,
@@ -42,21 +44,25 @@ async function loadSource(
   text: string,
   contentType: string
 }> {
   if (isPretty(source) && isOriginal(source)) {
     const generatedSource = getGeneratedSource(state, source);
     if (!generatedSource) {
       throw new Error("Unable to find minified original.");
     }
+    const content = getSourceContent(state, generatedSource.id);
+    if (!content || !isFulfilled(content)) {
+      throw new Error("Cannot pretty-print a file that has not loaded");
+    }
 
     return prettyPrintSource(
       sourceMaps,
-      source,
       generatedSource,
+      content.value,
       getSourceActorsForSource(state, generatedSource.id)
     );
   }
 
   if (isOriginal(source)) {
     const result = await sourceMaps.getOriginalSourceText(source);
     if (!result) {
       // The way we currently try to load and select a pending
@@ -96,19 +102,25 @@ async function loadSourceTextPromise(
     [PROMISE]: loadSource(getState(), source, { sourceMaps, client, getState })
   });
 
   const newSource = getSource(getState(), source.id);
 
   if (!newSource) {
     return;
   }
+  const content = getSourceContent(getState(), newSource.id);
 
-  if (!newSource.isWasm && isLoaded(newSource)) {
-    parser.setSource(newSource);
+  if (!newSource.isWasm && content) {
+    parser.setSource(
+      newSource.id,
+      isFulfilled(content)
+        ? content.value
+        : { type: "text", value: "", contentType: undefined }
+    );
     dispatch(setBreakpointPositions({ cx, sourceId: newSource.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));
     }
   }
@@ -123,17 +135,22 @@ export function loadSourceById(cx: Conte
   };
 }
 
 export const loadSourceText: MemoizedAction<
   { cx: Context, source: Source },
   ?Source
 > = memoizeableAction("loadSourceText", {
   exitEarly: ({ source }) => !source,
-  hasValue: ({ source }, { getState }) => isLoaded(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
@@ -6,68 +6,81 @@
 import SourceMaps, { generatedToOriginalId } from "devtools-source-map";
 
 import assert from "../../utils/assert";
 import { recordEvent } from "../../utils/telemetry";
 import { remapBreakpoints } from "../breakpoints";
 
 import { setSymbols } from "./symbols";
 import { prettyPrint } from "../../workers/pretty-print";
-import { getPrettySourceURL, isLoaded, isGenerated } from "../../utils/source";
+import {
+  getPrettySourceURL,
+  isGenerated,
+  isJavaScript
+} from "../../utils/source";
 import { loadSourceText } from "./loadSourceText";
 import { mapFrames } from "../pause";
 import { selectSpecificLocation } from "../sources";
 
 import {
   getSource,
   getSourceFromId,
   getSourceByURL,
   getSelectedLocation,
   getThreadContext
 } from "../../selectors";
 
 import type { Action, ThunkArgs } from "../types";
 import { selectSource } from "./select";
-import type { JsSource, Source, SourceActor, Context } from "../../types";
+import type {
+  JsSource,
+  Source,
+  SourceContent,
+  SourceActor,
+  Context
+} from "../../types";
 
 export async function prettyPrintSource(
   sourceMaps: typeof SourceMaps,
-  prettySource: Source,
   generatedSource: Source,
+  content: SourceContent,
   actors: Array<SourceActor>
 ) {
+  if (!isJavaScript(generatedSource, content) || content.type !== "text") {
+    throw new Error("Can't prettify non-javascript files.");
+  }
+
   const url = getPrettySourceURL(generatedSource.url);
   const { code, mappings } = await prettyPrint({
-    source: generatedSource,
+    text: content.value,
     url: url
   });
   await sourceMaps.applySourceMap(generatedSource.id, url, code, mappings);
 
   // The source map URL service used by other devtools listens to changes to
   // sources based on their actor IDs, so apply the mapping there too.
   for (const { actor } of actors) {
     await sourceMaps.applySourceMap(actor, url, code, mappings);
   }
   return {
-    id: prettySource.id,
     text: code,
     contentType: "text/javascript"
   };
 }
 
 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: JsSource = {
+      id,
       url,
       relativeUrl: url,
-      id,
       isBlackBoxed: false,
       isPrettyPrinted: true,
       isWasm: false,
       contentType: "text/javascript",
       loadedState: "loading",
       introductionUrl: null,
       introductionType: undefined,
       isExtension: false
@@ -113,19 +126,17 @@ export function togglePrettyPrint(cx: Co
     if (!source) {
       return {};
     }
 
     if (!source.isPrettyPrinted) {
       recordEvent("pretty_print");
     }
 
-    if (!isLoaded(source)) {
-      await dispatch(loadSourceText({ cx, source }));
-    }
+    await dispatch(loadSourceText({ cx, source }));
 
     assert(
       isGenerated(source),
       "Pretty-printing only allowed on generated sources"
     );
 
     const url = getPrettySourceURL(source.url);
     const prettySource = getSourceByURL(getState(), url);
--- a/devtools/client/debugger/src/actions/sources/select.js
+++ b/devtools/client/debugger/src/actions/sources/select.js
@@ -6,22 +6,22 @@
 
 /**
  * Redux actions for the sources state
  * @module actions/sources
  */
 
 import { isOriginalId } from "devtools-source-map";
 
-import { getSourceFromId } from "../../reducers/sources";
+import { getSourceFromId, getSourceWithContent } from "../../reducers/sources";
 import { getSourcesForTabs } from "../../reducers/tabs";
 import { setOutOfScopeLocations } from "../ast";
 import { setSymbols } from "./symbols";
 import { closeActiveSearch, updateActiveFileSearch } from "../ui";
-
+import { isFulfilled } from "../../utils/async-value";
 import { togglePrettyPrint } from "./prettyPrint";
 import { addTab, closeTab } from "../tabs";
 import { loadSourceText } from "./loadSourceText";
 
 import { prefs } from "../../utils/prefs";
 import { shouldPrettyPrint, isMinified } from "../../utils/source";
 import { createLocation } from "../../utils/location";
 import { mapLocation } from "../../utils/source-maps";
@@ -163,23 +163,31 @@ export function selectLocation(
 
     await dispatch(loadSourceText({ cx, source }));
     const loadedSource = getSource(getState(), source.id);
 
     if (!loadedSource) {
       // If there was a navigation while we were loading the loadedSource
       return;
     }
+    const sourceWithContent = getSourceWithContent(getState(), source.id);
+    const sourceContent =
+      sourceWithContent.content && isFulfilled(sourceWithContent.content)
+        ? sourceWithContent.content.value
+        : null;
 
     if (
       keepContext &&
       prefs.autoPrettyPrint &&
       !getPrettySource(getState(), loadedSource.id) &&
-      shouldPrettyPrint(loadedSource) &&
-      isMinified(loadedSource)
+      shouldPrettyPrint(
+        loadedSource,
+        sourceContent || { type: "text", value: "", contentType: undefined }
+      ) &&
+      isMinified(sourceWithContent)
     ) {
       await dispatch(togglePrettyPrint(cx, loadedSource.id));
       dispatch(closeTab(cx, loadedSource));
     }
 
     dispatch(setSymbols({ cx, source: loadedSource }));
     dispatch(setOutOfScopeLocations(cx));
 
--- a/devtools/client/debugger/src/actions/sources/symbols.js
+++ b/devtools/client/debugger/src/actions/sources/symbols.js
@@ -7,31 +7,28 @@
 import { hasSymbols, getSymbols } from "../../selectors";
 
 import { PROMISE } from "../utils/middleware/promise";
 import { updateTab } from "../tabs";
 import { loadSourceText } from "./loadSourceText";
 
 import * as parser from "../../workers/parser";
 
-import { isLoaded } from "../../utils/source";
 import {
   memoizeableAction,
   type MemoizedAction
 } from "../../utils/memoizableAction";
 
 import type { Source, Context } from "../../types";
 import type { Symbols } from "../../reducers/types";
 
 async function doSetSymbols(cx, source, { dispatch, getState }) {
   const sourceId = source.id;
 
-  if (!isLoaded(source)) {
-    await dispatch(loadSourceText({ cx, source }));
-  }
+  await dispatch(loadSourceText({ cx, source }));
 
   await dispatch({
     type: "SET_SYMBOLS",
     cx,
     sourceId,
     [PROMISE]: parser.getSymbols(sourceId)
   });
 
--- a/devtools/client/debugger/src/actions/sources/tests/__snapshots__/prettyPrint.spec.js.snap
+++ b/devtools/client/debugger/src/actions/sources/tests/__snapshots__/prettyPrint.spec.js.snap
@@ -7,13 +7,29 @@ Object {
   "introductionType": undefined,
   "introductionUrl": null,
   "isBlackBoxed": false,
   "isExtension": false,
   "isPrettyPrinted": true,
   "isWasm": false,
   "loadedState": "loaded",
   "relativeUrl": "http://localhost:8000/examples/base.js:formatted",
-  "text": "undefined
+  "text": "function base() {
+  return base
+}
 ",
   "url": "http://localhost:8000/examples/base.js:formatted",
 }
 `;
+
+exports[`sources - pretty print returns a pretty source for a minified file 2`] = `
+Object {
+  "state": "fulfilled",
+  "value": Object {
+    "contentType": "text/javascript",
+    "type": "text",
+    "value": "function base() {
+  return base
+}
+",
+  },
+}
+`;
--- a/devtools/client/debugger/src/actions/sources/tests/loadSource.spec.js
+++ b/devtools/client/debugger/src/actions/sources/tests/loadSource.spec.js
@@ -12,43 +12,50 @@ import {
   makeOriginalSource,
   makeSource
 } from "../../../utils/test-head";
 import {
   createSource,
   sourceThreadClient
 } from "../../tests/helpers/threadClient.js";
 import { getBreakpointsList } from "../../../selectors";
+import { isFulfilled, isRejected } from "../../../utils/async-value";
 
 describe("loadSourceText", () => {
   it("should load source text", async () => {
     const store = createStore(sourceThreadClient);
     const { dispatch, getState, cx } = store;
 
     const foo1Source = await dispatch(
       actions.newGeneratedSource(makeSource("foo1"))
     );
     await dispatch(actions.loadSourceText({ cx, source: foo1Source }));
-    const fooSource = selectors.getSource(getState(), "foo1");
 
-    if (!fooSource || typeof fooSource.text != "string") {
-      throw new Error("bad fooSource");
-    }
-    expect(fooSource.text.indexOf("return foo1")).not.toBe(-1);
+    const foo1Content = selectors.getSourceContent(getState(), foo1Source.id);
+    expect(
+      foo1Content &&
+      isFulfilled(foo1Content) &&
+      foo1Content.value.type === "text"
+        ? foo1Content.value.value.indexOf("return foo1")
+        : -1
+    ).not.toBe(-1);
 
-    const baseFoo2Source = await dispatch(
+    const foo2Source = await dispatch(
       actions.newGeneratedSource(makeSource("foo2"))
     );
-    await dispatch(actions.loadSourceText({ cx, source: baseFoo2Source }));
-    const foo2Source = selectors.getSource(getState(), "foo2");
+    await dispatch(actions.loadSourceText({ cx, source: foo2Source }));
 
-    if (!foo2Source || typeof foo2Source.text != "string") {
-      throw new Error("bad fooSource");
-    }
-    expect(foo2Source.text.indexOf("return foo2")).not.toBe(-1);
+    const foo2Content = selectors.getSourceContent(getState(), foo2Source.id);
+    expect(
+      foo2Content &&
+      isFulfilled(foo2Content) &&
+      foo2Content.value.type === "text"
+        ? foo2Content.value.value.indexOf("return foo2")
+        : -1
+    ).not.toBe(-1);
   });
 
   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(
       {
@@ -147,68 +154,74 @@ describe("loadSourceText", () => {
         new Promise(r => {
           count++;
           resolve = r;
         }),
       getBreakpointPositions: async () => ({})
     });
     const id = "foo";
 
-    await dispatch(
-      actions.newGeneratedSource(makeSource(id, { loadedState: "unloaded" }))
-    );
+    await dispatch(actions.newGeneratedSource(makeSource(id)));
 
     let source = selectors.getSourceFromId(getState(), id);
     dispatch(actions.loadSourceText({ cx, source }));
 
     source = selectors.getSourceFromId(getState(), id);
     const loading = dispatch(actions.loadSourceText({ cx, source }));
 
     if (!resolve) {
       throw new Error("no resolve");
     }
     resolve({ source: "yay", contentType: "text/javascript" });
     await loading;
     expect(count).toEqual(1);
 
-    source = selectors.getSource(getState(), id);
-    expect(source && source.text).toEqual("yay");
+    const content = selectors.getSourceContent(getState(), id);
+    expect(
+      content &&
+        isFulfilled(content) &&
+        content.value.type === "text" &&
+        content.value.value
+    ).toEqual("yay");
   });
 
   it("doesn't re-load loaded sources", async () => {
     let resolve;
     let count = 0;
     const { dispatch, getState, cx } = createStore({
       sourceContents: () =>
         new Promise(r => {
           count++;
           resolve = r;
         }),
       getBreakpointPositions: async () => ({})
     });
     const id = "foo";
 
-    await dispatch(
-      actions.newGeneratedSource(makeSource(id, { loadedState: "unloaded" }))
-    );
+    await dispatch(actions.newGeneratedSource(makeSource(id)));
     let source = selectors.getSourceFromId(getState(), id);
     const loading = dispatch(actions.loadSourceText({ cx, source }));
 
     if (!resolve) {
       throw new Error("no resolve");
     }
     resolve({ source: "yay", contentType: "text/javascript" });
     await loading;
 
     source = selectors.getSourceFromId(getState(), id);
     await dispatch(actions.loadSourceText({ cx, source }));
     expect(count).toEqual(1);
 
-    source = selectors.getSource(getState(), id);
-    expect(source && source.text).toEqual("yay");
+    const content = selectors.getSourceContent(getState(), id);
+    expect(
+      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(sourceThreadClient);
 
     const source = await dispatch(
       actions.newGeneratedSource(makeSource("foo1"))
     );
@@ -225,32 +238,35 @@ describe("loadSourceText", () => {
     const store = createStore(sourceThreadClient);
     const { dispatch, cx } = store;
 
     const source = await dispatch(
       actions.newGeneratedSource(makeSource("foo2"))
     );
 
     const wasLoading = watchForState(store, state => {
-      const fooSource = selectors.getSource(state, "foo2");
-      return fooSource && fooSource.loadedState === "loading";
+      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(sourceThreadClient);
 
     const source = await dispatch(
       actions.newGeneratedSource(makeSource("bad-id"))
     );
     await dispatch(actions.loadSourceText({ cx, source }));
     const badSource = selectors.getSource(getState(), "bad-id");
 
-    if (!badSource || !badSource.error) {
-      throw new Error("bad badSource");
-    }
-    expect(badSource.error.indexOf("unknown source")).not.toBe(-1);
+    const content = badSource
+      ? selectors.getSourceContent(getState(), badSource.id)
+      : null;
+    expect(
+      content && isRejected(content) && typeof content.value === "string"
+        ? content.value.indexOf("unknown source")
+        : -1
+    ).not.toBe(-1);
   });
 });
--- a/devtools/client/debugger/src/actions/sources/tests/prettyPrint.spec.js
+++ b/devtools/client/debugger/src/actions/sources/tests/prettyPrint.spec.js
@@ -7,30 +7,43 @@
 import {
   actions,
   selectors,
   createStore,
   makeSource
 } from "../../../utils/test-head";
 import { createPrettySource } from "../prettyPrint";
 import { sourceThreadClient } from "../../tests/helpers/threadClient.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(sourceThreadClient);
 
     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`;
     const pretty = selectors.getSourceByURL(getState(), prettyURL);
-    expect(pretty && pretty.contentType).toEqual("text/javascript");
+    const content = pretty
+      ? selectors.getSourceContent(getState(), pretty.id)
+      : null;
     expect(pretty && pretty.url.includes(prettyURL)).toEqual(true);
     expect(pretty).toMatchSnapshot();
+
+    expect(
+      content &&
+        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(sourceThreadClient);
 
     const source = await dispatch(
       actions.newGeneratedSource(makeSource("foobar.js"))
     );
--- a/devtools/client/debugger/src/actions/sources/tests/select.spec.js
+++ b/devtools/client/debugger/src/actions/sources/tests/select.spec.js
@@ -230,17 +230,17 @@ describe("sources", () => {
   it("should keep the original the viewing context", async () => {
     const { dispatch, getState, cx } = createStore(
       sourceThreadClient,
       {},
       {
         getOriginalLocation: async location => ({ ...location, line: 12 }),
         getOriginalLocations: async items => items,
         getGeneratedLocation: async location => ({ ...location, line: 12 }),
-        getOriginalSourceText: async () => ({ source: "" }),
+        getOriginalSourceText: async () => ({ text: "" }),
         getGeneratedRangesForOriginal: async () => []
       }
     );
 
     const baseSource = await dispatch(
       actions.newGeneratedSource(makeSource("base.js"))
     );
 
@@ -264,17 +264,17 @@ describe("sources", () => {
   it("should change the original the viewing context", async () => {
     const { dispatch, getState, cx } = createStore(
       sourceThreadClient,
       {},
       {
         getOriginalLocation: async location => ({ ...location, line: 12 }),
         getOriginalLocations: async items => items,
         getGeneratedRangesForOriginal: async () => [],
-        getOriginalSourceText: async () => ({ source: "" })
+        getOriginalSourceText: async () => ({ text: "" })
       }
     );
 
     const baseGenSource = await dispatch(
       actions.newGeneratedSource(makeSource("base.js"))
     );
 
     const baseSource = await dispatch(
--- a/devtools/client/debugger/src/actions/types/SourceAction.js
+++ b/devtools/client/debugger/src/actions/types/SourceAction.js
@@ -9,17 +9,20 @@ import type { PromiseAction } from "../u
 
 export type LoadSourceAction = PromiseAction<
   {|
     +type: "LOAD_SOURCE_TEXT",
     +cx: Context,
     +sourceId: string,
     +epoch: number
   |},
-  Source
+  {
+    text: string | {| binary: Object |},
+    contentType: string | void
+  }
 >;
 export type SourceAction =
   | LoadSourceAction
   | {|
       +type: "ADD_SOURCE",
       +cx: Context,
       +source: Source
     |}
--- a/devtools/client/debugger/src/components/Editor/DebugLine.js
+++ b/devtools/client/debugger/src/components/Editor/DebugLine.js
@@ -6,42 +6,43 @@
 import { PureComponent } from "react";
 import {
   toEditorPosition,
   getDocument,
   hasDocument,
   startOperation,
   endOperation
 } from "../../utils/editor";
-import { isLoaded } from "../../utils/source";
 import { isException } from "../../utils/pause";
 import { getIndentation } from "../../utils/indentation";
 import { connect } from "../../utils/connect";
 import {
   getVisibleSelectedFrame,
   getPauseReason,
-  getSourceFromId,
+  getSourceWithContent,
   getCurrentThread
 } from "../../selectors";
 
-import type { Frame, Why, Source } from "../../types";
+import type { Frame, Why, SourceWithContent } from "../../types";
 
 type Props = {
   frame: Frame,
   why: Why,
-  source: Source
+  source: ?SourceWithContent
 };
 
 type TextClasses = {
   markTextClass: string,
   lineClass: string
 };
 
-function isDocumentReady(source, frame) {
-  return frame && isLoaded(source) && hasDocument(frame.location.sourceId);
+function isDocumentReady(source: ?SourceWithContent, frame) {
+  return (
+    frame && source && source.content && hasDocument(frame.location.sourceId)
+  );
 }
 
 export class DebugLine extends PureComponent<Props> {
   debugExpression: null;
 
   componentDidMount() {
     const { why, frame, source } = this.props;
     this.setDebugLine(why, frame, source);
@@ -56,17 +57,17 @@ export class DebugLine extends PureCompo
     const { why, frame, source } = this.props;
 
     startOperation();
     this.clearDebugLine(prevProps.why, prevProps.frame, prevProps.source);
     this.setDebugLine(why, frame, source);
     endOperation();
   }
 
-  setDebugLine(why: Why, frame: Frame, source: Source) {
+  setDebugLine(why: Why, frame: Frame, source: ?SourceWithContent) {
     if (!isDocumentReady(source, frame)) {
       return;
     }
     const sourceId = frame.location.sourceId;
     const doc = getDocument(sourceId);
 
     let { line, column } = toEditorPosition(frame.location);
     const { markTextClass, lineClass } = this.getTextClasses(why);
@@ -77,17 +78,17 @@ export class DebugLine extends PureCompo
 
     this.debugExpression = doc.markText(
       { ch: column, line },
       { ch: null, line },
       { className: markTextClass }
     );
   }
 
-  clearDebugLine(why: Why, frame: Frame, source: Source) {
+  clearDebugLine(why: Why, frame: Frame, source: ?SourceWithContent) {
     if (!isDocumentReady(source, frame)) {
       return;
     }
 
     if (this.debugExpression) {
       this.debugExpression.clear();
     }
 
@@ -113,14 +114,14 @@ export class DebugLine extends PureCompo
     return null;
   }
 }
 
 const mapStateToProps = state => {
   const frame = getVisibleSelectedFrame(state);
   return {
     frame,
-    source: frame && getSourceFromId(state, frame.location.sourceId),
+    source: frame && getSourceWithContent(state, frame.location.sourceId),
     why: getPauseReason(state, getCurrentThread(state))
   };
 };
 
 export default connect(mapStateToProps)(DebugLine);
--- a/devtools/client/debugger/src/components/Editor/EditorMenu.js
+++ b/devtools/client/debugger/src/components/Editor/EditorMenu.js
@@ -13,65 +13,65 @@ import {
   getPrettySource,
   getIsPaused,
   getCurrentThread,
   getThreadContext
 } from "../../selectors";
 
 import { editorMenuItems, editorItemActions } from "./menus/editor";
 
-import type { Source, ThreadContext } from "../../types";
+import type { SourceWithContent, ThreadContext } from "../../types";
 import type { EditorItemActions } from "./menus/editor";
 import type SourceEditor from "../../utils/editor/source-editor";
 
 type Props = {
   cx: ThreadContext,
   contextMenu: ?MouseEvent,
   editorActions: EditorItemActions,
   clearContextMenu: () => void,
   editor: SourceEditor,
   hasPrettySource: boolean,
   isPaused: boolean,
-  selectedSource: Source
+  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
@@ -3,47 +3,47 @@
  * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
 
 // @flow
 import React, { PureComponent } from "react";
 import { connect } from "../../utils/connect";
 import classnames from "classnames";
 import actions from "../../actions";
 import {
-  getSelectedSource,
+  getSelectedSourceWithContent,
   getPrettySource,
   getPaneCollapse,
   getContext
 } from "../../selectors";
 
+import { isFulfilled } from "../../utils/async-value";
 import {
   isPretty,
-  isLoaded,
   getFilename,
   isOriginal,
   shouldBlackbox
 } from "../../utils/source";
 import { getGeneratedSource } from "../../reducers/sources";
 import { shouldShowPrettyPrint } from "../../utils/editor";
 
 import { PaneToggleButton } from "../shared/Button";
 import AccessibleImage from "../shared/AccessibleImage";
 
-import type { Source, Context } from "../../types";
+import type { SourceWithContent, Source, Context } from "../../types";
 
 import "./Footer.css";
 
 type CursorPosition = {
   line: number,
   column: number
 };
 
 type Props = {
   cx: Context,
-  selectedSource: Source,
+  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,77 +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 (!isLoaded(selectedSource) && selectedSource.isPrettyPrinted) {
+    if (
+      !selectedSourceWithContent.content &&
+      selectedSourceWithContent.source.isPrettyPrinted
+    ) {
       return (
         <div className="loader" key="pretty-loader">
           <AccessibleImage className="loader" />
         </div>
       );
     }
 
-    if (!shouldShowPrettyPrint(selectedSource)) {
+    const sourceContent =
+      selectedSourceWithContent.content &&
+      isFulfilled(selectedSourceWithContent.content)
+        ? selectedSourceWithContent.content.value
+        : null;
+    if (
+      !shouldShowPrettyPrint(
+        selectedSourceWithContent.source,
+        sourceContent || { type: "text", value: "", contentType: undefined }
+      )
+    ) {
       return;
     }
 
     const tooltip = L10N.getStr("sourceTabs.prettyPrint");
-    const sourceLoaded = selectedSource && isLoaded(selectedSource);
+    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 && isLoaded(selectedSource);
+    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}
       >
@@ -182,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 || !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}
@@ -217,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,
@@ -255,25 +275,28 @@ class SourceFooter extends PureComponent
           {this.renderToggleButton()}
         </div>
       </div>
     );
   }
 }
 
 const mapStateToProps = state => {
-  const selectedSource = getSelectedSource(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
@@ -1,125 +1,137 @@
 /* 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 { Component } from "react";
 import { toEditorLine, endOperation, startOperation } from "../../utils/editor";
 import { getDocument, hasDocument } from "../../utils/editor/source-documents";
-import { isLoaded } from "../../utils/source";
 
 import { connect } from "../../utils/connect";
 import {
   getVisibleSelectedFrame,
   getSelectedLocation,
-  getSelectedSource,
+  getSelectedSourceWithContent,
   getPauseCommand,
   getCurrentThread
 } from "../../selectors";
 
 import type {
   Frame,
   SourceLocation,
-  Source,
+  SourceWithContent,
   SourceDocuments
 } from "../../types";
 import type { Command } from "../../reducers/types";
 
 type Props = {
   pauseCommand: Command,
   selectedFrame: Frame,
   selectedLocation: SourceLocation,
-  selectedSource: Source
+  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, selectedLocation) {
+function isDocumentReady(
+  selectedSourceWithContent: ?SourceWithContent,
+  selectedLocation
+) {
   return (
     selectedLocation &&
-    isLoaded(selectedSource) &&
+    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: Source
+    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: Source
+    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)) {
@@ -147,18 +159,21 @@ export class HighlightLine extends Compo
     duration = duration.length ? Number(duration[0]) : 0;
 
     setTimeout(
       () => doc && doc.removeLineClass(editorLine, "line", "highlight-line"),
       duration
     );
   }
 
-  clearHighlightLine(selectedLocation: SourceLocation, selectedSource: Source) {
-    if (!isDocumentReady(selectedSource, selectedLocation)) {
+  clearHighlightLine(
+    selectedLocation: SourceLocation,
+    selectedSourceWithContent: ?SourceWithContent
+  ) {
+    if (!isDocumentReady(selectedSourceWithContent, selectedLocation)) {
       return;
     }
 
     const { line, sourceId } = selectedLocation;
     const editorLine = toEditorLine(sourceId, line);
     const doc = getDocument(sourceId);
     doc.removeLineClass(editorLine, "line", "highlight-line");
   }
@@ -167,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: getSelectedSource(state)
+  selectedSourceWithContent: getSelectedSourceWithContent(state)
 }))(HighlightLine);
--- a/devtools/client/debugger/src/components/Editor/SearchBar.js
+++ b/devtools/client/debugger/src/components/Editor/SearchBar.js
@@ -8,16 +8,17 @@ import PropTypes from "prop-types";
 import React, { Component } from "react";
 import { connect } from "../../utils/connect";
 import { CloseButton } from "../shared/Button";
 import AccessibleImage from "../shared/AccessibleImage";
 import actions from "../../actions";
 import {
   getActiveSearch,
   getSelectedSource,
+  getSourceContent,
   getSelectedLocation,
   getFileSearchQuery,
   getFileSearchModifiers,
   getFileSearchResults,
   getHighlightedLineRange,
   getContext
 } from "../../selectors";
 
@@ -54,16 +55,17 @@ type State = {
   index: number,
   inputFocused: boolean
 };
 
 type Props = {
   cx: Context,
   editor: SourceEditor,
   selectedSource?: Source,
+  selectedContentLoaded: boolean,
   searchOn: boolean,
   searchResults: SearchResults,
   modifiers: Modifiers,
   query: string,
   showClose?: boolean,
   size?: string,
   toggleFileSearchModifier: typeof actions.toggleFileSearchModifier,
   setFileSearchQuery: typeof actions.setFileSearchQuery,
@@ -166,18 +168,18 @@ class SearchBar extends Component<Props,
         this.doSearch(query);
       } else {
         this.setState({ query: "", inputFocused: true });
       }
     }
   };
 
   doSearch = (query: string) => {
-    const { cx, selectedSource } = this.props;
-    if (!selectedSource || !selectedSource.text) {
+    const { cx, selectedSource, selectedContentLoaded } = this.props;
+    if (!selectedSource || !selectedContentLoaded) {
       return;
     }
 
     this.props.doSearch(cx, query, this.props.editor);
   };
 
   traverseResults = (e: SyntheticEvent<HTMLElement>, rev: boolean) => {
     e.stopPropagation();
@@ -353,26 +355,33 @@ class SearchBar extends Component<Props,
     );
   }
 }
 
 SearchBar.contextTypes = {
   shortcuts: PropTypes.object
 };
 
-const mapStateToProps = state => ({
-  cx: getContext(state),
-  searchOn: getActiveSearch(state) === "file",
-  selectedSource: getSelectedSource(state),
-  selectedLocation: getSelectedLocation(state),
-  query: getFileSearchQuery(state),
-  modifiers: getFileSearchModifiers(state),
-  highlightedLineRange: getHighlightedLineRange(state),
-  searchResults: getFileSearchResults(state)
-});
+const mapStateToProps = state => {
+  const selectedSource = getSelectedSource(state);
+
+  return {
+    cx: getContext(state),
+    searchOn: getActiveSearch(state) === "file",
+    selectedSource,
+    selectedContentLoaded: selectedSource
+      ? !!getSourceContent(state, selectedSource.id)
+      : null,
+    selectedLocation: getSelectedLocation(state),
+    query: getFileSearchQuery(state),
+    modifiers: getFileSearchModifiers(state),
+    highlightedLineRange: getHighlightedLineRange(state),
+    searchResults: getFileSearchResults(state)
+  };
+};
 
 export default connect(
   mapStateToProps,
   {
     toggleFileSearchModifier: actions.toggleFileSearchModifier,
     setFileSearchQuery: actions.setFileSearchQuery,
     setActiveSearch: actions.setActiveSearch,
     closeFileSearch: actions.closeFileSearch,
--- a/devtools/client/debugger/src/components/Editor/index.js
+++ b/devtools/client/debugger/src/components/Editor/index.js
@@ -7,17 +7,16 @@
 import PropTypes from "prop-types";
 import React, { PureComponent } from "react";
 import { bindActionCreators } from "redux";
 import ReactDOM from "react-dom";
 import { connect } from "../../utils/connect";
 import classnames from "classnames";
 import { throttle } from "lodash";
 
-import { isLoaded } from "../../utils/source";
 import { isFirefox } from "devtools-environment";
 import { features } from "../../utils/prefs";
 import { getIndentation } from "../../utils/indentation";
 
 import { showMenu } from "devtools-contextmenu";
 import {
   createBreakpointItems,
   breakpointItemActions
@@ -26,17 +25,17 @@ import {
 import { continueToHereItem, editorItemActions } from "./menus/editor";
 
 import type { BreakpointItemActions } from "./menus/breakpoints";
 import type { EditorItemActions } from "./menus/editor";
 
 import {
   getActiveSearch,
   getSelectedLocation,
-  getSelectedSource,
+  getSelectedSourceWithContent,
   getConditionalPanelLocation,
   getSymbols,
   getIsPaused,
   getCurrentThread,
   getThreadContext,
   getSkipPausing
 } from "../../selectors";
 
@@ -76,26 +75,30 @@ import {
 import { resizeToggleButton, resizeBreakpointGutter } from "../../utils/ui";
 
 import "./Editor.css";
 import "./Breakpoints.css";
 import "./Highlight.css";
 
 import type SourceEditor from "../../utils/editor/source-editor";
 import type { SymbolDeclarations } from "../../workers/parser";
-import type { SourceLocation, Source, ThreadContext } from "../../types";
+import type {
+  SourceLocation,
+  SourceWithContent,
+  ThreadContext
+} from "../../types";
 
 const cssVars = {
   searchbarHeight: "var(--editor-searchbar-height)"
 };
 
 export type Props = {
   cx: ThreadContext,
   selectedLocation: ?SourceLocation,
-  selectedSource: ?Source,
+  selectedSourceWithContent: ?SourceWithContent,
   searchOn: boolean,
   startPanelSize: number,
   endPanelSize: number,
   conditionalPanelLocation: SourceLocation,
   symbols: SymbolDeclarations,
   isPaused: boolean,
   skipPausing: boolean,
 
@@ -128,27 +131,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();
@@ -216,21 +222,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) });
@@ -240,23 +246,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") {
@@ -317,28 +323,28 @@ class Editor extends PureComponent<Props
   };
 
   openMenu(event: MouseEvent) {
     event.stopPropagation();
     event.preventDefault();
 
     const {
       cx,
-      selectedSource,
+      selectedSourceWithContent,
       breakpointActions,
       editorActions,
       isPaused
     } = this.props;
     const { editor } = this.state;
-    if (!selectedSource || !editor) {
+    if (!selectedSourceWithContent || !editor) {
       return;
     }
 
     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 };
 
@@ -364,125 +370,129 @@ class Editor extends PureComponent<Props
   onGutterClick = (
     cm: Object,
     line: number,
     gutter: string,
     ev: MouseEvent
   ) => {
     const {
       cx,
-      selectedSource,
+      selectedSourceWithContent,
       conditionalPanelLocation,
       closeConditionalPanel,
       addBreakpointAtLine,
       continueToHere
     } = this.props;
 
     // ignore right clicks in the gutter
     if (
       (ev.ctrlKey && ev.button === 0) ||
       ev.button === 2 ||
-      (selectedSource && selectedSource.isBlackBoxed) ||
-      !selectedSource
+      (selectedSourceWithContent &&
+        selectedSourceWithContent.source.isBlackBoxed) ||
+      !selectedSourceWithContent
     ) {
       return;
     }
 
     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 ||
-      !isLoaded(nextProps.selectedSource)
+      !nextProps.selectedSourceWithContent.content
     ) {
       return false;
     }
 
     const isFirstLoad =
-      (!selectedSource || !isLoaded(selectedSource)) &&
-      isLoaded(nextProps.selectedSource);
+      (!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);
     }
   }
 
@@ -495,38 +505,46 @@ 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 (!isLoaded(selectedSource)) {
+    if (!selectedSourceWithContent.content) {
       return showLoading(editor);
     }
 
-    if (selectedSource.error) {
-      return this.showErrorMessage(selectedSource.error);
+    if (selectedSourceWithContent.content.state === "rejected") {
+      let { value } = selectedSourceWithContent.content;
+      if (typeof value !== "string") {
+        value = "Unexpected source error";
+      }
+
+      return this.showErrorMessage(value);
     }
 
-    if (selectedSource) {
-      return showSourceText(editor, selectedSource, symbols);
-    }
+    return showSourceText(
+      editor,
+      selectedSourceWithContent.source,
+      selectedSourceWithContent.content.value,
+      symbols
+    );
   }
 
   clearEditor() {
     const { editor } = this.state;
     if (!editor) {
       return;
     }
 
@@ -552,63 +570,73 @@ class Editor extends PureComponent<Props
     }
 
     return {
       height: "100%"
     };
   }
 
   renderItems() {
-    const { cx, selectedSource, conditionalPanelLocation } = this.props;
+    const {
+      cx,
+      selectedSourceWithContent,
+      conditionalPanelLocation
+    } = 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}
       </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()}
         />
@@ -619,25 +647,28 @@ class Editor extends PureComponent<Props
   }
 }
 
 Editor.contextTypes = {
   shortcuts: PropTypes.object
 };
 
 const mapStateToProps = state => {
-  const selectedSource = getSelectedSource(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
@@ -12,21 +12,24 @@ import {
   isPretty,
   getRawSourceURL,
   getFilename,
   shouldBlackbox
 } from "../../../utils/source";
 
 import { downloadFile } from "../../../utils/utils";
 
+import { isFulfilled } from "../../../utils/async-value";
 import actions from "../../../actions";
 
 import type {
   Source,
   SourceLocation,
+  SourceContent,
+  SourceWithContent,
   Context,
   ThreadContext
 } from "../../../types";
 
 function isMapped(selectedSource) {
   return isOriginalId(selectedSource.id) || !!selectedSource.sourceMapURL;
 }
 
@@ -41,28 +44,27 @@ export const continueToHereItem = (
   click: () => editorActions.continueToHere(cx, location.line, location.column),
   id: "node-menu-continue-to-here",
   label: L10N.getStr("editor.continueToHere.label")
 });
 
 // menu items
 
 const copyToClipboardItem = (
-  selectedSource: Source,
+  selectedContent: SourceContent,
   editorActions: EditorItemActions
 ) => {
   return {
     id: "node-menu-copy-to-clipboard",
     label: L10N.getStr("copyToClipboard.label"),
     accesskey: L10N.getStr("copyToClipboard.accesskey"),
     disabled: false,
     click: () =>
-      !selectedSource.isWasm &&
-      typeof selectedSource.text == "string" &&
-      copyToTheClipboard(selectedSource.text)
+      selectedContent.type === "text" &&
+      copyToTheClipboard(selectedContent.value)
   };
 };
 
 const copySourceItem = (
   selectedSource: Source,
   selectionText: string,
   editorActions: EditorItemActions
 ) => {
@@ -155,61 +157,67 @@ const evaluateInConsoleItem = (
 ) => ({
   id: "node-menu-evaluate-in-console",
   label: L10N.getStr("evaluateInConsole.label"),
   click: () => editorActions.evaluateInConsole(selectionText)
 });
 
 const downloadFileItem = (
   selectedSource: Source,
+  selectedContent: SourceContent,
   editorActions: EditorItemActions
 ) => {
   return {
     id: "node-menu-download-file",
     label: L10N.getStr("downloadFile.label"),
     accesskey: L10N.getStr("downloadFile.accesskey"),
-    click: () => downloadFile(selectedSource, getFilename(selectedSource))
+    click: () => downloadFile(selectedContent, getFilename(selectedSource))
   };
 };
 
 export function editorMenuItems({
   cx,
   editorActions,
-  selectedSource,
+  selectedSourceWithContent,
   location,
   selectionText,
   hasPrettySource,
   isTextSelected,
   isPaused
 }: {
   cx: ThreadContext,
   editorActions: EditorItemActions,
-  selectedSource: Source,
+  selectedSourceWithContent: SourceWithContent,
   location: SourceLocation,
   selectionText: string,
   hasPrettySource: boolean,
   isTextSelected: boolean,
   isPaused: boolean
 }) {
   const items = [];
+  const { source: selectedSource, content } = selectedSourceWithContent;
 
   items.push(
     jumpToMappedLocationItem(
       cx,
       selectedSource,
       location,
       hasPrettySource,
       editorActions
     ),
     continueToHereItem(cx, location, isPaused, editorActions),
     { type: "separator" },
-    copyToClipboardItem(selectedSource, editorActions),
+    ...(content && isFulfilled(content)
+      ? [copyToClipboardItem(content.value, editorActions)]
+      : []),
     copySourceItem(selectedSource, selectionText, editorActions),
     copySourceUri2Item(selectedSource, editorActions),
-    downloadFileItem(selectedSource, editorActions),
+    ...(content && isFulfilled(content)
+      ? [downloadFileItem(selectedSource, content.value, editorActions)]
+      : []),
     { type: "separator" },
     showSourceMenuItem(cx, selectedSource, editorActions),
     blackBoxMenuItem(cx, selectedSource, editorActions)
   );
 
   if (isTextSelected) {
     items.push(
       { type: "separator" },
--- a/devtools/client/debugger/src/components/Editor/tests/DebugLine.spec.js
+++ b/devtools/client/debugger/src/components/Editor/tests/DebugLine.spec.js
@@ -5,16 +5,18 @@
 
 // @flow
 
 import React from "react";
 import { shallow } from "enzyme";
 
 import DebugLine from "../DebugLine";
 
+import type { SourceWithContent } from "../../../types";
+import * as asyncValue from "../../../utils/async-value";
 import { createSourceObject } from "../../../utils/test-head";
 import { setDocument, toEditorLine } from "../../../utils/editor";
 
 function createMockDocument(clear) {
   const doc = {
     addLineClass: jest.fn(),
     removeLineClass: jest.fn(),
     markText: jest.fn(() => ({ clear })),
@@ -26,17 +28,20 @@ function createMockDocument(clear) {
 
 function generateDefaults(editor, overrides) {
   return {
     editor,
     pauseInfo: {
       why: { type: "breakpoint" }
     },
     frame: null,
-    source: createSourceObject("foo"),
+    source: ({
+      source: createSourceObject("foo"),
+      content: null
+    }: SourceWithContent),
     ...overrides
   };
 }
 
 function createFrame(line) {
   return {
     location: {
       sourceId: "foo",
@@ -47,46 +52,60 @@ 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", { loadedState: "loaded" })
+        source: {
+          source: createSourceObject("foo"),
+          content: asyncValue.fulfilled({
+            type: "text",
+            value: "",
+            contentType: undefined
+          })
+        }
       });
       const line = 2;
       const frame = createFrame(line);
 
       component.setProps({ ...props, frame });
 
       expect(doc.removeLineClass.mock.calls).toEqual([]);
       expect(doc.addLineClass.mock.calls).toEqual([
         [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", { loadedState: "loaded" })
+          source: {
+            source: createSourceObject("foo"),
+            content: asyncValue.fulfilled({
+              type: "text",
+              value: "",
+              contentType: undefined
+            })
+          }
         });
 
         component.instance().debugExpression = { clear: jest.fn() };
         const firstLine = 2;
         const secondLine = 2;
 
         component.setProps({ ...props, frame: createFrame(firstLine) });
         component.setProps({
--- 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,19 @@
  * 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 { JsSource, 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(),
     ...overrides
   };
@@ -48,23 +50,62 @@ function createMockEditor() {
         setValue: newVal => (val = newVal)
       };
     },
     replaceDocument: jest.fn(),
     setMode: jest.fn()
   };
 }
 
-function createMockSource(overrides) {
-  return {
+function createMockSourceWithContent(
+  overrides: $Shape<
+    Source & {
+      loadedState: "loaded" | "loading" | "unloaded",
+      text: string,
+      contentType: ?string,
+      error: string,
+      isWasm: boolean
+    }
+  >
+): SourceWithContent {
+  const {
+    loadedState = "loaded",
+    text = "the text",
+    contentType = undefined,
+    error = undefined,
+    ...otherOverrides
+  } = overrides;
+
+  const source: JsSource = ({
     id: "foo",
-    text: "the text",
-    loadedState: "loaded",
     url: "foo",
-    ...overrides
+    text,
+    loadedState,
+    contentType,
+    error,
+    ...otherOverrides
+  }: any);
+  let content = null;
+  if (loadedState === "loaded") {
+    if (typeof text !== "string") {
+      throw new Error("Cannot create a non-text source");
+    }
+
+    content = error
+      ? asyncValue.rejected(error)
+      : asyncValue.fulfilled({
+          type: "text",
+          value: text,
+          contentType: contentType || undefined
+        });
+  }
+
+  return {
+    source,
+    content
   };
 }
 
 function render(overrides = {}) {
   const props = generateDefaults(overrides);
   const mockEditor = createMockEditor();
 
   // $FlowIgnore
@@ -86,50 +127,55 @@ 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: { loadedState: "loading" }
+        selectedSourceWithContent: {
+          source: { loadedState: "loading" },
+          content: null
+        }
       });
 
       expect(mockEditor.replaceDocument.mock.calls[0][0].getValue()).toBe(
         "Loading…"
       );
       expect(mockEditor.codeMirror.scrollTo.mock.calls).toEqual([]);
     });
   });
 
   describe("When loaded", () => {
     it("should show text", async () => {
       const { component, mockEditor, props } = render({});
 
       await component.setState({ editor: mockEditor });
       await component.setProps({
         ...props,
-        selectedSource: createMockSource({ loadedState: "loaded" }),
+        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]]);
     });
   });
 
   describe("When error", () => {
     it("should show error text", async () => {
       const { component, mockEditor, props } = render({});
 
       await component.setState({ editor: mockEditor });
       await component.setProps({
         ...props,
-        selectedSource: createMockSource({
+        selectedSourceWithContent: createMockSourceWithContent({
           loadedState: "loaded",
           text: undefined,
           error: "error text"
         }),
         selectedLocation: { sourceId: "bad-foo", line: 3, column: 1 }
       });
 
       expect(mockEditor.setText.mock.calls).toEqual([
@@ -138,17 +184,17 @@ describe("Editor", () => {
     });
 
     it("should show wasm error", async () => {
       const { component, mockEditor, props } = render({});
 
       await component.setState({ editor: mockEditor });
       await component.setProps({
         ...props,
-        selectedSource: createMockSource({
+        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 }
       });
 
@@ -160,24 +206,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: createMockSource({ loadedState: "loaded" }),
+        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: createMockSource({
+        selectedSourceWithContent: createMockSourceWithContent({
           id: "bar",
           loadedState: "loading"
         }),
         selectedLocation: { sourceId: "bar", line: 1, column: 1 }
       });
 
       expect(mockEditor.replaceDocument.mock.calls[1][0].getValue()).toBe(
         "Loading…"
@@ -188,56 +236,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 = createMockSource({
+      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, symbols });
+      await component.setProps({
+        ...props,
+        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 = createMockSource({
+      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, symbols });
+      await component.setProps({
+        ...props,
+        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" }]
       ]);
@@ -246,24 +304,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: createMockSource({ loadedState: "loading" }),
+        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: createMockSource({
+        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,27 +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/Editor/tests/SearchBar.spec.js
+++ b/devtools/client/debugger/src/components/Editor/tests/SearchBar.spec.js
@@ -27,16 +27,17 @@ function generateDefaults() {
     searchOn: true,
     symbolSearchOn: true,
     editor: {},
     searchResults: {},
     selectedSymbolType: "functions",
     selectedSource: {
       text: " text text query text"
     },
+    selectedContentLoaded: true,
     setFileSearchQuery: msg => msg,
     symbolSearchResults: [],
     modifiers: {
       get: jest.fn(),
       toJS: () => ({ caseSensitive: true, wholeWord: false, regexMatch: false })
     },
     selectedResultIndex: 0,
     updateSearchResults: jest.fn(),
--- a/devtools/client/debugger/src/components/PrimaryPanes/Outline.js
+++ b/devtools/client/debugger/src/components/PrimaryPanes/Outline.js
@@ -9,17 +9,17 @@ import { showMenu } from "devtools-conte
 import { connect } from "../../utils/connect";
 import { score as fuzzaldrinScore } from "fuzzaldrin-plus";
 
 import { copyToTheClipboard } from "../../utils/clipboard";
 import { findFunctionText } from "../../utils/function";
 
 import actions from "../../actions";
 import {
-  getSelectedSource,
+  getSelectedSourceWithContent,
   getSymbols,
   getSelectedLocation,
   getContext
 } from "../../selectors";
 
 import OutlineFilter from "./OutlineFilter";
 import "./Outline.css";
 import PreviewFunction from "../shared/PreviewFunction";
@@ -257,23 +257,25 @@ export class Outline extends Component<P
           {this.renderFooter()}
         </div>
       </div>
     );
   }
 }
 
 const mapStateToProps = state => {
-  const selectedSource = getSelectedSource(state);
-  const symbols = selectedSource ? getSymbols(state, selectedSource) : null;
+  const selectedSource = getSelectedSourceWithContent(state);
+  const symbols = selectedSource
+    ? getSymbols(state, selectedSource.source)
+    : null;
 
   return {
     cx: getContext(state),
     symbols,
-    selectedSource,
+    selectedSource: selectedSource && selectedSource.source,
     selectedLocation: getSelectedLocation(state),
     getFunctionText: line => {
       if (selectedSource) {
         return findFunctionText(line, selectedSource, symbols);
       }
 
       return null;
     }
--- a/devtools/client/debugger/src/components/QuickOpenModal.js
+++ b/devtools/client/debugger/src/components/QuickOpenModal.js
@@ -10,16 +10,17 @@ import { basename } from "../utils/path"
 
 import actions from "../actions";
 import {
   getDisplayedSourcesList,
   getQuickOpenEnabled,
   getQuickOpenQuery,
   getQuickOpenType,
   getSelectedSource,
+  getSourceContent,
   getSymbols,
   getTabs,
   isSymbolsLoading,
   getContext
 } from "../selectors";
 import { scrollList } from "../utils/result-list";
 import {
   formatSymbols,
@@ -42,16 +43,17 @@ import type { Tab } from "../reducers/ta
 
 import "./QuickOpenModal.css";
 
 type Props = {
   cx: Context,
   enabled: boolean,
   sources: Array<Object>,
   selectedSource?: Source,
+  selectedContentLoaded?: boolean,
   query: string,
   searchType: QuickOpenType,
   symbols: FormattedSymbolDeclarations,
   symbolsLoading: boolean,
   tabs: Tab[],
   shortcutsModalEnabled: boolean,
   selectSpecificLocation: typeof actions.selectSpecificLocation,
   setQuickOpenQuery: typeof actions.setQuickOpenQuery,
@@ -262,19 +264,23 @@ export class QuickOpenModal extends Comp
         line: location.line,
         column: location.column
       });
       this.closeModal();
     }
   };
 
   onChange = (e: SyntheticInputEvent<HTMLInputElement>) => {
-    const { selectedSource, setQuickOpenQuery } = this.props;
+    const {
+      selectedSource,
+      selectedContentLoaded,
+      setQuickOpenQuery
+    } = this.props;
     setQuickOpenQuery(e.target.value);
-    const noSource = !selectedSource || !selectedSource.text;
+    const noSource = !selectedSource || !selectedContentLoaded;
     if ((this.isSymbolSearch() && noSource) || this.isGotoQuery()) {
       return;
     }
 
     this.updateResults(e.target.value);
   };
 
   onKeyDown = (e: SyntheticKeyboardEvent<HTMLInputElement>) => {
@@ -425,16 +431,19 @@ export class QuickOpenModal extends Comp
 function mapStateToProps(state) {
   const selectedSource = getSelectedSource(state);
 
   return {
     cx: getContext(state),
     enabled: getQuickOpenEnabled(state),
     sources: formatSources(getDisplayedSourcesList(state), getTabs(state)),
     selectedSource,
+    selectedContentLoaded: selectedSource
+      ? !!getSourceContent(state, selectedSource.id)
+      : undefined,
     symbols: formatSymbols(getSymbols(state, selectedSource)),
     symbolsLoading: isSymbolsLoading(state, selectedSource),
     query: getQuickOpenQuery(state),
     searchType: getQuickOpenType(state),
     tabs: getTabs(state)
   };
 }
 
--- a/devtools/client/debugger/src/components/test/QuickOpenModal.spec.js
+++ b/devtools/client/debugger/src/components/test/QuickOpenModal.spec.js
@@ -243,17 +243,18 @@ describe("QuickOpenModal", () => {
           enabled: true,
           searchType: "functions",
           symbols: {
             functions: [],
             variables: []
           },
           // symbol searching relies on a source being selected.
           // So we dummy out the source and the API.
-          selectedSource: { id: "foo", text: "yo" }
+          selectedSource: { id: "foo", text: "yo" },
+          selectedContentLoaded: true
         },
         "mount"
       );
 
       wrapper
         .find("input")
         .simulate("change", { target: { value: "@someFunc" } });
 
@@ -269,17 +270,18 @@ describe("QuickOpenModal", () => {
           enabled: true,
           searchType: "functions",
           symbols: {
             functions: [],
             variables: []
           },
           // symbol searching relies on a source being selected.
           // So we dummy out the source and the API.
-          selectedSource: null
+          selectedSource: null,
+          selectedContentLoaded: false
         },
         "mount"
       );
       wrapper
         .find("input")
         .simulate("change", { target: { value: "@someFunc" } });
       expect(filter).not.toHaveBeenCalled();
     });
@@ -325,17 +327,18 @@ describe("QuickOpenModal", () => {
 
     it("on Enter go to location with sourceId", () => {
       const sourceId = "source_id";
       const { wrapper, props } = generateModal(
         {
           enabled: true,
           query: ":34:12",
           searchType: "goto",
-          selectedSource: { id: sourceId }
+          selectedSource: { id: sourceId },
+          selectedContentLoaded: true
         },
         "shallow"
       );
       const event = {
         key: "Enter"
       };
       wrapper.find("SearchInput").simulate("keydown", event);
       expect(props.selectSpecificLocation).toHaveBeenCalledWith(mockcx, {
@@ -581,16 +584,17 @@ describe("QuickOpenModal", () => {
     it("on ArrowUp, traverse results up with functions", () => {
       const sourceId = "sourceId";
       const { wrapper, props } = generateModal(
         {
           enabled: true,
           query: "test",
           searchType: "functions",
           selectedSource: { id: sourceId },
+          selectedContentLoaded: true,
           symbols: {
             functions: [],
             variables: {}
           }
         },
         "shallow"
       );
       const event = {
@@ -646,16 +650,17 @@ describe("QuickOpenModal", () => {
     it("on ArrowUp, traverse results up to function with no location", () => {
       const sourceId = "sourceId";
       const { wrapper, props } = generateModal(
         {
           enabled: true,
           query: "test",
           searchType: "functions",
           selectedSource: { id: sourceId },
+          selectedContentLoaded: true,
           symbols: {
             functions: [],
             variables: {}
           }
         },
         "shallow"
       );
       const event = {
@@ -679,16 +684,17 @@ describe("QuickOpenModal", () => {
         "taking action if no selectedSource",
       () => {
         const { wrapper, props } = generateModal(
           {
             enabled: true,
             query: "test",
             searchType: "variables",
             selectedSource: null,
+            selectedContentLoaded: true,
             symbols: {
               functions: [],
               variables: {}
             }
           },
           "shallow"
         );
         const event = {
@@ -718,16 +724,17 @@ describe("QuickOpenModal", () => {
       () => {
         const sourceId = "sourceId";
         const { wrapper, props } = generateModal(
           {
             enabled: true,
             query: "test",
             searchType: "other",
             selectedSource: { id: sourceId },
+            selectedContentLoaded: true,
             symbols: {
               functions: [],
               variables: {}
             }
           },
           "shallow"
         );
         const event = {
--- a/devtools/client/debugger/src/reducers/sources.js
+++ b/devtools/client/debugger/src/reducers/sources.js
@@ -15,32 +15,36 @@ import {
   underRoot,
   getRelativeUrl,
   isGenerated,
   isOriginal as isOriginalSource,
   isUrlExtension,
   getPlainUrl
 } from "../utils/source";
 
+import * as asyncValue from "../utils/async-value";
+import type { AsyncValue, SettledValue } from "../utils/async-value";
 import { originalToGeneratedId } from "devtools-source-map";
 import { prefs } from "../utils/prefs";
 
 import {
   hasSourceActor,
   getSourceActor,
   getSourceActors,
   getThreadsBySource,
   type SourceActorId,
   type SourceActorOuterState
 } from "./source-actors";
 import type {
   Source,
   SourceId,
   SourceActor,
   SourceLocation,
+  SourceContent,
+  SourceWithContent,
   ThreadId
 } from "../types";
 import type { PendingSelectedLocation, Selector } from "./types";
 import type { Action, DonePromiseAction, FocusItem } from "../actions/types";
 import type { LoadSourceAction } from "../actions/types/SourceAction";
 import { uniq } from "lodash";
 
 export type SourcesMap = { [SourceId]: Source };
@@ -337,18 +341,17 @@ function updateProjectDirectoryRoot(stat
   prefs.projectDirectoryRoot = root;
 
   return updateAllSources({ ...state, projectDirectoryRoot: root }, source => ({
     relativeUrl: getRelativeUrl(source, root)
   }));
 }
 
 /*
- * Update a source's loaded state fields
- * i.e. loadedState, text, error
+ * Update a source's loaded text content.
  */
 function updateLoadedState(
   state: SourcesState,
   action: LoadSourceAction
 ): SourcesState {
   const { sourceId } = action;
   let source;
 
@@ -610,16 +613,84 @@ export const getSelectedSource: Selector
     if (!selectedLocation) {
       return;
     }
 
     return sources[selectedLocation.sourceId];
   }
 );
 
+type GSSWC = Selector<?SourceWithContent>;
+export const getSelectedSourceWithContent: GSSWC = createSelector(
+  getSelectedLocation,
+  getSources,
+  (
+    selectedLocation: ?SourceLocation,
+    sources: SourcesMap
+  ): SourceWithContent | null => {
+    const source = selectedLocation && sources[selectedLocation.sourceId];
+    return source ? getSourceWithContentInner(sources, source.id) : null;
+  }
+);
+export function getSourceWithContent(
+  state: OuterState,
+  id: SourceId
+): SourceWithContent {
+  return getSourceWithContentInner(state.sources.sources, id);
+}
+export function getSourceContent(
+  state: OuterState,
+  id: SourceId
+): AsyncValue<SourceContent> | null {
+  return getSourceWithContentInner(state.sources.sources, id).content;
+}
+
+const contentLookup: WeakMap<Source, SourceWithContent> = new WeakMap();
+function getSourceWithContentInner(
+  sources: SourcesMap,
+  id: SourceId
+): SourceWithContent {
+  const source = sources[id];
+  if (!source) {
+    throw new Error("Unknown Source ID");
+  }
+
+  let result = contentLookup.get(source);
+  if (!result) {
+    let content = null;
+    if (source.loadedState === "loaded") {
+      if (source.error) {
+        content = asyncValue.rejected(source.error);
+      } else if (source.isWasm) {
+        if (typeof source.text !== "object") {
+          throw new Error("Expected WASM value.");
+        }
+        content = asyncValue.fulfilled({
+          type: "wasm",
+          value: source.text
+        });
+      } else {
+        content = asyncValue.fulfilled({
+          type: "text",
+          value: source.text || "",
+          contentType: source.contentType || undefined
+        });
+      }
+    }
+
+    result = {
+      source,
+      content
+    };
+    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 {
   return state.sources.projectDirectoryRoot;
 }
--- a/devtools/client/debugger/src/types.js
+++ b/devtools/client/debugger/src/types.js
@@ -1,14 +1,15 @@
 /* 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 { SettledValue, FulfilledValue } from "./utils/async-value";
 import type { SourcePayload } from "./client/firefox/types";
 import type { SourceActorId, SourceActor } from "./reducers/source-actors";
 
 export type { SourceActorId, SourceActor };
 
 export type SearchModifiers = {
   caseSensitive: boolean,
   wholeWord: boolean,
@@ -350,16 +351,36 @@ export type Grip = {
   sealed: boolean,
   type: string,
   url?: string,
   fileName?: string,
   message?: string,
   name?: string
 };
 
+export type TextSourceContent = {|
+  type: "text",
+  value: string,
+  contentType: string | void
+|};
+export type WasmSourceContent = {|
+  type: "wasm",
+  value: {| binary: Object |}
+|};
+export type SourceContent = TextSourceContent | WasmSourceContent;
+
+export type SourceWithContent = {|
+  source: Source,
+  +content: SettledValue<SourceContent> | null
+|};
+export type SourceWithContentAndType<+Content: SourceContent> = {|
+  source: Source,
+  +content: FulfilledValue<Content>
+|};
+
 /**
  * BaseSource
  *
  * @memberof types
  * @static
  */
 
 type BaseSource = {|
new file mode 100644
--- /dev/null
+++ b/devtools/client/debugger/src/utils/async-value.js
@@ -0,0 +1,39 @@
+/* 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
+
+export type FulfilledValue<+T> = {|
+  state: "fulfilled",
+  +value: T
+|};
+export type RejectedValue = {|
+  state: "rejected",
+  value: mixed
+|};
+export type PendingValue = {|
+  state: "pending"
+|};
+export type SettledValue<+T> = FulfilledValue<T> | RejectedValue;
+export type AsyncValue<+T> = SettledValue<T> | PendingValue;
+
+export function pending(): PendingValue {
+  return { state: "pending" };
+}
+export function fulfilled<+T>(value: T): FulfilledValue<T> {
+  return { state: "fulfilled", value };
+}
+export function rejected(value: mixed): RejectedValue {
+  return { state: "rejected", value };
+}
+
+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/index.js
+++ b/devtools/client/debugger/src/utils/editor/index.js
@@ -13,17 +13,22 @@ export { onMouseOver } from "./token-eve
 import { createEditor } from "./create-editor";
 import { shouldPrettyPrint } from "../source";
 import { findNext, findPrev } from "./source-search";
 
 import { isWasm, lineToWasmOffset, wasmOffsetToLine } from "../wasm";
 
 import type { AstLocation } from "../../workers/parser";
 import type { EditorPosition, EditorRange } from "../editor/types";
-import type { SearchModifiers, Source, SourceLocation } from "../../types";
+import type {
+  SearchModifiers,
+  Source,
+  SourceContent,
+  SourceLocation
+} from "../../types";
 type Editor = Object;
 
 let editor: ?Editor;
 
 export function getEditor() {
   if (editor) {
     return editor;
   }
@@ -53,18 +58,18 @@ export function endOperation() {
   const codeMirror = getCodeMirror();
   if (!codeMirror) {
     return;
   }
 
   codeMirror.endOperation();
 }
 
-export function shouldShowPrettyPrint(source: Source) {
-  return shouldPrettyPrint(source);
+export function shouldShowPrettyPrint(source: Source, content: SourceContent) {
+  return shouldPrettyPrint(source, content);
 }
 
 export function traverseResults(
   e: Event,
   ctx: any,
   query: string,
   dir: string,
   modifiers: SearchModifiers
--- a/devtools/client/debugger/src/utils/editor/source-documents.js
+++ b/devtools/client/debugger/src/utils/editor/source-documents.js
@@ -5,17 +5,22 @@
 // @flow
 
 import { getMode } from "../source";
 
 import { isWasm, getWasmLineNumberFormatter, renderWasmText } from "../wasm";
 import { resizeBreakpointGutter, resizeToggleButton } from "../ui";
 import SourceEditor from "./source-editor";
 
-import type { Source, SourceDocuments } from "../../types";
+import type {
+  SourceId,
+  Source,
+  SourceContent,
+  SourceDocuments
+} from "../../types";
 import type { SymbolDeclarations } from "../../workers/parser";
 
 let sourceDocs: SourceDocuments = {};
 
 export function getDocument(key: string) {
   return sourceDocs[key];
 }
 
@@ -96,61 +101,62 @@ export function showErrorMessage(editor:
   }
   const doc = editor.createDocument();
   editor.replaceDocument(doc);
   editor.setText(error);
   editor.setMode({ name: "text" });
   resetLineNumberFormat(editor);
 }
 
-function setEditorText(editor: Object, source: Source) {
-  if (source.isWasm) {
-    const wasmLines = renderWasmText(source);
+function setEditorText(
+  editor: Object,
+  sourceId: SourceId,
+  content: SourceContent
+) {
+  if (content.type === "wasm") {
+    const wasmLines = renderWasmText(sourceId, content);
     // cm will try to split into lines anyway, saving memory
     const wasmText = { split: () => wasmLines, match: () => false };
     editor.setText(wasmText);
   } else {
-    editor.setText(source.text);
+    editor.setText(content.value);
   }
 }
 
-function setMode(editor, source, symbols) {
-  const mode = getMode(source, symbols);
+function setMode(editor, source: Source, content: SourceContent, symbols) {
+  const mode = getMode(source, content, symbols);
   const currentMode = editor.codeMirror.getOption("mode");
   if (!currentMode || currentMode.name != mode.name) {
     editor.setMode(mode);
   }
 }
 
 /**
  * Handle getting the source document or creating a new
  * document with the correct mode and text.
  */
 export function showSourceText(
   editor: Object,
   source: Source,
+  content: SourceContent,
   symbols?: SymbolDeclarations
 ) {
-  if (!source) {
-    return;
-  }
-
   if (hasDocument(source.id)) {
     const doc = getDocument(source.id);
     if (editor.codeMirror.doc === doc) {
-      setMode(editor, source, symbols);
+      setMode(editor, source, content, symbols);
       return;
     }
 
     editor.replaceDocument(doc);
     updateLineNumberFormat(editor, source.id);
-    setMode(editor, source, symbols);
+    setMode(editor, source, content, symbols);
     return doc;
   }
 
   const doc = editor.createDocument();
   setDocument(source.id, doc);
   editor.replaceDocument(doc);
 
-  setEditorText(editor, source);
-  setMode(editor, source, symbols);
+  setEditorText(editor, source.id, content);
+  setMode(editor, source, content, symbols);
   updateLineNumberFormat(editor, source.id);
 }
--- a/devtools/client/debugger/src/utils/editor/tests/editor.spec.js
+++ b/devtools/client/debugger/src/utils/editor/tests/editor.spec.js
@@ -17,27 +17,27 @@ import {
   getSourceLocationFromMouseEvent,
   forEachLine,
   removeLineClass,
   clearLineClass,
   getTextForLine,
   getCursorLine
 } from "../index";
 
-import { makeMockSource } from "../../test-mockup";
+import { makeMockSource, makeMockSourceAndContent } from "../../test-mockup";
 
 describe("shouldShowPrettyPrint", () => {
   it("shows pretty print for a source", () => {
-    const source = makeMockSource(
+    const { source, content } = makeMockSourceAndContent(
       "http://example.com/index.js",
       "test-id-123",
       "text/javascript",
       "some text here"
     );
-    expect(shouldShowPrettyPrint(source)).toEqual(true);
+    expect(shouldShowPrettyPrint(source, content)).toEqual(true);
   });
 });
 
 describe("traverseResults", () => {
   const e: any = { stopPropagation: jest.fn(), preventDefault: jest.fn() };
   const ctx = {};
   const query = "Awesome books";
   const modifiers = {
--- a/devtools/client/debugger/src/utils/function.js
+++ b/devtools/client/debugger/src/utils/function.js
@@ -1,37 +1,44 @@
 /* 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 { isFulfilled } from "./async-value";
 import { findClosestFunction } from "./ast";
 import { correctIndentation } from "./indentation";
-import type { Source } from "../types";
+import type { SourceWithContent } from "../types";
 import type { Symbols } from "../reducers/ast";
 
 export function findFunctionText(
   line: number,
-  source: Source,
+  { source, content }: SourceWithContent,
   symbols: ?Symbols
 ): ?string {
   const func = findClosestFunction(symbols, {
     sourceId: source.id,
     line,
     column: Infinity
   });
 
-  if (source.isWasm || !func || !source.text) {
+  if (
+    source.isWasm ||
+    !func ||
+    !content ||
+    !isFulfilled(content) ||
+    content.value.type !== "text"
+  ) {
     return null;
   }
 
   const {
     location: { start, end }
   } = func;
-  const lines = source.text.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
@@ -1,35 +1,33 @@
 /* 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 { Source } from "../types";
+import type { SourceWithContent } from "../types";
+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: Source) {
+export function isMinified({ source, content }: SourceWithContent) {
   if (_minifiedCache.has(source.id)) {
     return _minifiedCache.get(source.id);
   }
 
-  if (source.isWasm) {
+  if (!content || !isFulfilled(content) || content.value.type !== "text") {
     return false;
   }
 
-  let text = source.text;
-  if (!text) {
-    return false;
-  }
+  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/moz.build
+++ b/devtools/client/debugger/src/utils/moz.build
@@ -9,16 +9,17 @@ DIRS += [
     'pause',
     'resource',
     'sources-tree',
 ]
 
 CompiledModules(
     'assert.js',
     'ast.js',
+    'async-value.js',
     'asyncStoreHelper.js',
     'bootstrap.js',
     'breakable-lines.js',
     'build-query.js',
     'clipboard.js',
     'connect.js',
     'context.js',
     'dbg.js',
--- a/devtools/client/debugger/src/utils/pause/mapScopes/index.js
+++ b/devtools/client/debugger/src/utils/pause/mapScopes/index.js
@@ -37,24 +37,26 @@ import {
 } from "./getApplicableBindingsForOriginalPosition";
 
 import { log } from "../../log";
 import type {
   PartialPosition,
   Frame,
   Scope,
   Source,
+  SourceContent,
   BindingContents,
   ScopeBindings
 } from "../../../types";
 
 export type OriginalScope = RenderableScope;
 
 export async function buildMappedScopes(
   source: Source,
+  content: SourceContent,
   frame: Frame,
   scopes: Scope,
   sourceMaps: any,
   client: any
 ): Promise<?{
   mappings: {
     [string]: string
   },
@@ -84,16 +86,17 @@ export async function buildMappedScopes(
     frame.this
   );
 
   const {
     mappedOriginalScopes,
     expressionLookup
   } = await mapOriginalBindingsToGenerated(
     source,
+    content,
     originalRanges,
     originalAstScopes,
     generatedAstBindings,
     client,
     sourceMaps
   );
 
   const mappedGeneratedScopes = generateClientScope(
@@ -103,16 +106,17 @@ export async function buildMappedScopes(
 
   return isReliableScope(mappedGeneratedScopes)
     ? { mappings: expressionLookup, scope: mappedGeneratedScopes }
     : null;
 }
 
 async function mapOriginalBindingsToGenerated(
   source: Source,
+  content: SourceContent,
   originalRanges: Array<MappedOriginalRange>,
   originalAstScopes,
   generatedAstBindings,
   client,
   sourceMaps
 ) {
   const expressionLookup = {};
   const mappedOriginalScopes = [];
@@ -128,16 +132,17 @@ async function mapOriginalBindingsToGene
 
     for (const name of Object.keys(item.bindings)) {
       const binding = item.bindings[name];
 
       const result = await findGeneratedBinding(
         cachedSourceMaps,
         client,
         source,
+        content,
         name,
         binding,
         originalRanges,
         generatedAstBindings
       );
 
       if (result) {
         generatedBindings[name] = result.grip;
@@ -337,16 +342,17 @@ function hasValidIdent(range: MappedOrig
   );
 }
 
 // eslint-disable-next-line complexity
 async function findGeneratedBinding(
   sourceMaps: any,
   client: any,
   source: Source,
+  content: SourceContent,
   name: string,
   originalBinding: BindingData,
   originalRanges: Array<MappedOriginalRange>,
   generatedAstBindings: Array<GeneratedBindingLocation>
 ): Promise<?{
   grip: BindingContents,
   expression: string | null
 }> {
@@ -404,18 +410,18 @@ async function findGeneratedBinding(
         genContent = await findGeneratedImportReference(applicableBindings);
       } else {
         genContent = await findGeneratedReference(applicableBindings);
       }
     }
 
     if (
       (pos.type === "class-decl" || pos.type === "class-inner") &&
-      source.contentType &&
-      source.contentType.match(/\/typescript/)
+      content.contentType &&
+      content.contentType.match(/\/typescript/)
     ) {
       const declRange = findMatchingRange(originalRanges, pos.declaration);
       if (declRange && declRange.type !== "multiple") {
         const applicableDeclBindings = await loadApplicableBindings(
           pos.declaration,
           pos.type
         );
 
--- a/devtools/client/debugger/src/utils/source.js
+++ b/devtools/client/debugger/src/utils/source.js
@@ -16,17 +16,18 @@ import { endTruncateStr } from "./utils"
 import { truncateMiddleText } from "../utils/text";
 import { parse as parseURL } from "../utils/url";
 import { renderWasmText } from "./wasm";
 import { toEditorPosition } from "./editor";
 export { isMinified } from "./isMinified";
 import { getURL, getFileExtension } from "./sources-tree";
 import { prefs, features } from "./prefs";
 
-import type { Source, SourceLocation, JsSource } from "../types";
+import type { SourceId, Source, SourceContent, SourceLocation } from "../types";
+import { isFulfilled, type AsyncValue } from "./async-value";
 import type { Symbols } from "../reducers/types";
 
 type transformUrlCallback = string => string;
 
 export const sourceTypes = {
   coffee: "coffeescript",
   js: "javascript",
   jsx: "react",
@@ -65,21 +66,24 @@ export function shouldBlackbox(source: ?
 
   if (isOriginalId(source.id) && !features.originalBlackbox) {
     return false;
   }
 
   return true;
 }
 
-export function shouldPrettyPrint(source: Source) {
+export function shouldPrettyPrint(
+  source: Source,
+  content: SourceContent
+): boolean {
   if (
     !source ||
     isPretty(source) ||
-    !isJavaScript(source) ||
+    !isJavaScript(source, content) ||
     isOriginal(source) ||
     (prefs.clientSourceMapsEnabled && source.sourceMapURL)
   ) {
     return false;
   }
 
   return true;
 }
@@ -89,19 +93,19 @@ export function shouldPrettyPrint(source
  * javascript files.
  *
  * @return boolean
  *         True if the source is likely javascript.
  *
  * @memberof utils/source
  * @static
  */
-export function isJavaScript(source: Source): boolean {
+export function isJavaScript(source: Source, content: SourceContent): boolean {
   const url = source.url;
-  const contentType = source.contentType;
+  const contentType = content.type === "wasm" ? null : content.contentType;
   return (
     (url && /\.(jsm|js)?$/.test(trimUrlQuery(url))) ||
     !!(contentType && contentType.includes("javascript"))
   );
 }
 
 /**
  * @memberof utils/source
@@ -278,25 +282,23 @@ export function getSourcePath(url: strin
   // for URLs like "about:home" the path is null so we pass the full href
   return path || href;
 }
 
 /**
  * Returns amount of lines in the source. If source is a WebAssembly binary,
  * the function returns amount of bytes.
  */
-export function getSourceLineCount(source: Source) {
-  if (source.error) {
-    return 0;
-  }
-  if (source.isWasm) {
-    const { binary } = (source.text: any);
+export function getSourceLineCount(content: SourceContent): number {
+  if (content.type === "wasm") {
+    const { binary } = content.value;
     return binary.length;
   }
-  return source.text != undefined ? source.text.split("\n").length : 0;
+
+  return content.value.split("\n").length;
 }
 
 /**
  *
  * Checks if a source is minified based on some heuristics
  * @param key
  * @param text
  * @return boolean
@@ -307,28 +309,29 @@ export function getSourceLineCount(sourc
 /**
  *
  * Returns Code Mirror mode for source content type
  * @param contentType
  * @return String
  * @memberof utils/source
  * @static
  */
-
+// eslint-disable-next-line complexity
 export function getMode(
   source: Source,
+  content: SourceContent,
   symbols?: Symbols
 ): { name: string, base?: Object } {
-  if (source.isWasm) {
+  const { url } = source;
+
+  if (content.type !== "text") {
     return { name: "text" };
   }
-  const { contentType, text, url } = source;
-  if (!text) {
-    return { name: "text" };
-  }
+
+  const { contentType, value: text } = content;
 
   if ((url && url.match(/\.jsx$/i)) || (symbols && symbols.hasJsx)) {
     if (symbols && symbols.hasTypes) {
       return { name: "text/typescript-jsx" };
     }
     return { name: "jsx" };
   }
 
@@ -389,40 +392,40 @@ export function getMode(
 
   if (isHTMLLike) {
     return { name: "htmlmixed" };
   }
 
   return { name: "text" };
 }
 
-export function isLoaded(source: Source) {
-  return source.loadedState === "loaded";
-}
-
 export function isInlineScript(source: Source): boolean {
   return source.introductionType === "scriptElement";
 }
 
-export function getTextAtPosition(source: ?Source, location: SourceLocation) {
-  if (!source || !source.text) {
+export function getTextAtPosition(
+  sourceId: SourceId,
+  asyncContent: AsyncValue<SourceContent> | null,
+  location: SourceLocation
+) {
+  if (!asyncContent || !isFulfilled(asyncContent)) {
     return "";
   }
 
+  const content = asyncContent.value;
   const line = location.line;
   const column = location.column || 0;
 
-  if (source.isWasm) {
+  if (content.type === "wasm") {
     const { line: editorLine } = toEditorPosition(location);
-    const lines = renderWasmText(source);
+    const lines = renderWasmText(sourceId, content);
     return lines[editorLine];
   }
 
-  const text = ((source: any): JsSource).text || "";
-  const lineText = text.split("\n")[line - 1];
+  const lineText = content.value.split("\n")[line - 1];
   if (!lineText) {
     return "";
   }
 
   return lineText.slice(column, column + 100).trim();
 }
 
 export function getSourceClassnames(source: Object, symbols?: Symbols) {
--- a/devtools/client/debugger/src/utils/sources-tree/tests/__snapshots__/updateTree.spec.js.snap
+++ b/devtools/client/debugger/src/utils/sources-tree/tests/__snapshots__/updateTree.spec.js.snap
@@ -15,40 +15,40 @@ exports[`calls updateTree.js adds one so
           \\"type\\": \\"source\\",
           \\"name\\": \\"(index)\\",
           \\"path\\": \\"https://davidwalsh.name/\\",
           \\"contents\\": {
             \\"id\\": \\"server1.conn13.child1/39\\",
             \\"url\\": \\"https://davidwalsh.name/\\",
             \\"isBlackBoxed\\": false,
             \\"isPrettyPrinted\\": false,
-            \\"loadedState\\": \\"unloaded\\",
             \\"relativeUrl\\": \\"https://davidwalsh.name/\\",
             \\"introductionUrl\\": null,
             \\"isWasm\\": false,
+            \\"isExtension\\": false,
+            \\"loadedState\\": \\"unloaded\\",
             \\"contentType\\": \\"text/javascript\\",
-            \\"isExtension\\": false,
             \\"text\\": \\"\\"
           }
         },
         {
           \\"type\\": \\"source\\",
           \\"name\\": \\"source1.js\\",
           \\"path\\": \\"davidwalsh.name/source1.js\\",
           \\"contents\\": {
             \\"id\\": \\"server1.conn13.child1/37\\",
             \\"url\\": \\"https://davidwalsh.name/source1.js\\",
             \\"isBlackBoxed\\": false,
             \\"isPrettyPrinted\\": false,
-            \\"loadedState\\": \\"unloaded\\",
             \\"relativeUrl\\": \\"https://davidwalsh.name/source1.js\\",
             \\"introductionUrl\\": null,
             \\"isWasm\\": false,
+            \\"isExtension\\": false,
+            \\"loadedState\\": \\"unloaded\\",
             \\"contentType\\": \\"text/javascript\\",
-            \\"isExtension\\": false,
             \\"text\\": \\"\\"
           }
         }
       ]
     }
   ]
 }"
 `;
@@ -68,58 +68,58 @@ exports[`calls updateTree.js adds two so
           \\"type\\": \\"source\\",
           \\"name\\": \\"(index)\\",
           \\"path\\": \\"https://davidwalsh.name/\\",
           \\"contents\\": {
             \\"id\\": \\"server1.conn13.child1/39\\",
             \\"url\\": \\"https://davidwalsh.name/\\",
             \\"isBlackBoxed\\": false,
             \\"isPrettyPrinted\\": false,
-            \\"loadedState\\": \\"unloaded\\",
             \\"relativeUrl\\": \\"https://davidwalsh.name/\\",
             \\"introductionUrl\\": null,
             \\"isWasm\\": false,
+            \\"isExtension\\": false,
+            \\"loadedState\\": \\"unloaded\\",
             \\"contentType\\": \\"text/javascript\\",
-            \\"isExtension\\": false,
             \\"text\\": \\"\\"
           }
         },
         {
           \\"type\\": \\"source\\",
           \\"name\\": \\"source1.js\\",
           \\"path\\": \\"davidwalsh.name/source1.js\\",
           \\"contents\\": {
             \\"id\\": \\"server1.conn13.child1/37\\",
             \\"url\\": \\"https://davidwalsh.name/source1.js\\",
             \\"isBlackBoxed\\": false,
             \\"isPrettyPrinted\\": false,
-            \\"loadedState\\": \\"unloaded\\",
             \\"relativeUrl\\": \\"https://davidwalsh.name/source1.js\\",
             \\"introductionUrl\\": null,
             \\"isWasm\\": false,
+            \\"isExtension\\": false,
+            \\"loadedState\\": \\"unloaded\\",
             \\"contentType\\": \\"text/javascript\\",
-            \\"isExtension\\": false,
             \\"text\\": \\"\\"
           }
         },
         {
           \\"type\\": \\"source\\",
           \\"name\\": \\"source2.js\\",
           \\"path\\": \\"davidwalsh.name/source2.js\\",
           \\"contents\\": {
             \\"id\\": \\"server1.conn13.child1/40\\",
             \\"url\\": \\"https://davidwalsh.name/source2.js\\",
             \\"isBlackBoxed\\": false,
             \\"isPrettyPrinted\\": false,
-            \\"loadedState\\": \\"unloaded\\",
             \\"relativeUrl\\": \\"https://davidwalsh.name/source2.js\\",
             \\"introductionUrl\\": null,
             \\"isWasm\\": false,
+            \\"isExtension\\": false,
+            \\"loadedState\\": \\"unloaded\\",
             \\"contentType\\": \\"text/javascript\\",
-            \\"isExtension\\": false,
             \\"text\\": \\"\\"
           }
         }
       ]
     }
   ]
 }"
 `;
@@ -139,40 +139,40 @@ exports[`calls updateTree.js shows all t
           \\"type\\": \\"source\\",
           \\"name\\": \\"(index)\\",
           \\"path\\": \\"https://davidwalsh.name/\\",
           \\"contents\\": {
             \\"id\\": \\"server1.conn13.child1/39\\",
             \\"url\\": \\"https://davidwalsh.name/\\",
             \\"isBlackBoxed\\": false,
             \\"isPrettyPrinted\\": false,
-            \\"loadedState\\": \\"unloaded\\",
             \\"relativeUrl\\": \\"https://davidwalsh.name/\\",
             \\"introductionUrl\\": null,
             \\"isWasm\\": false,
+            \\"isExtension\\": false,
+            \\"loadedState\\": \\"unloaded\\",
             \\"contentType\\": \\"text/javascript\\",
-            \\"isExtension\\": false,
             \\"text\\": \\"\\"
           }
         },
         {
           \\"type\\": \\"source\\",
           \\"name\\": \\"source1.js\\",
           \\"path\\": \\"davidwalsh.name/source1.js\\",
           \\"contents\\": {
             \\"id\\": \\"server1.conn13.child1/37\\",
             \\"url\\": \\"https://davidwalsh.name/source1.js\\",
             \\"isBlackBoxed\\": false,
             \\"isPrettyPrinted\\": false,
-            \\"loadedState\\": \\"unloaded\\",
             \\"relativeUrl\\": \\"https://davidwalsh.name/source1.js\\",
             \\"introductionUrl\\": null,
             \\"isWasm\\": false,
+            \\"isExtension\\": false,
+            \\"loadedState\\": \\"unloaded\\",
             \\"contentType\\": \\"text/javascript\\",
-            \\"isExtension\\": false,
             \\"text\\": \\"\\"
           }
         }
       ]
     }
   ]
 }"
 `;
--- a/devtools/client/debugger/src/utils/test-mockup.js
+++ b/devtools/client/debugger/src/utils/test-mockup.js
@@ -17,54 +17,121 @@ import type {
   Expression,
   Frame,
   FrameId,
   Scope,
   JsSource,
   WasmSource,
   Source,
   SourceId,
+  SourceWithContentAndType,
+  SourceWithContent,
+  TextSourceContent,
+  WasmSourceContent,
   Why
 } from "../types";
+import * as asyncValue from "./async-value";
 
 function makeMockSource(
   url: string = "url",
-  id: SourceId = "source",
-  contentType: string = "text/javascript",
-  text: string = ""
+  id: SourceId = "source"
 ): JsSource {
   return {
     id,
     url,
     isBlackBoxed: false,
     isPrettyPrinted: false,
-    loadedState: text ? "loaded" : "unloaded",
     relativeUrl: url,
     introductionUrl: null,
     introductionType: undefined,
     isWasm: false,
-    contentType,
     isExtension: false,
-    text
+    loadedState: "unloaded",
+    contentType: "text/javascript",
+    text: ""
   };
 }
 
-function makeMockWasmSource(text: {| binary: Object |}): WasmSource {
+function makeMockSourceWithContent(
+  url?: string,
+  id?: SourceId,
+  contentType?: string = "text/javascript",
+  text?: string = ""
+): SourceWithContent {
+  const source = makeMockSource(url, id);
+  source.contentType = contentType;
+  source.text = text;
+  if (text) {
+    source.loadedState = "loaded";
+  }
+
+  return {
+    source,
+    content: text
+      ? asyncValue.fulfilled({
+          type: "text",
+          value: text,
+          contentType
+        })
+      : null
+  };
+}
+
+function makeMockSourceAndContent(
+  url?: string,
+  id?: SourceId,
+  contentType?: string = "text/javascript",
+  text: string = ""
+): { source: Source, content: TextSourceContent } {
+  const source = makeMockSource(url, id);
+  source.contentType = contentType;
+  source.text = text;
+  if (text) {
+    source.loadedState = "loaded";
+  }
+
+  return {
+    source,
+    content: {
+      type: "text",
+      value: text,
+      contentType
+    }
+  };
+}
+
+function makeMockWasmSource(): WasmSource {
   return {
     id: "wasm-source-id",
     url: "url",
     isBlackBoxed: false,
     isPrettyPrinted: false,
     loadedState: "unloaded",
     relativeUrl: "url",
     introductionUrl: null,
     introductionType: undefined,
     isWasm: true,
     isExtension: false,
-    text
+    text: undefined
+  };
+}
+
+function makeMockWasmSourceWithContent(text: {|
+  binary: Object
+|}): SourceWithContentAndType<WasmSourceContent> {
+  const source = makeMockWasmSource();
+  source.text = text;
+  source.loadedState = "loaded";
+
+  return {
+    source,
+    content: asyncValue.fulfilled({
+      type: "wasm",
+      value: text
+    })
   };
 }
 
 function makeMockScope(
   actor: ActorId = "scope-actor",
   type: string = "block",
   parent: ?Scope = null
 ): Scope {
@@ -158,17 +225,20 @@ const mockthreadcx = {
   navigateCounter: 0,
   thread: "FakeThread",
   pauseCounter: 0,
   isPaused: false
 };
 
 export {
   makeMockSource,
+  makeMockSourceWithContent,
+  makeMockSourceAndContent,
   makeMockWasmSource,
+  makeMockWasmSourceWithContent,
   makeMockScope,
   mockScopeAddVariable,
   makeMockBreakpoint,
   makeMockFrame,
   makeMockFrameWithURL,
   makeWhyNormal,
   makeWhyThrow,
   makeMockExpression,
--- 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/isMinified.spec.js
+++ b/devtools/client/debugger/src/utils/tests/isMinified.spec.js
@@ -1,16 +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 { isMinified } from "../isMinified";
-import { makeMockSource } from "../test-mockup";
+import { makeMockSourceWithContent } from "../test-mockup";
 
 describe("isMinified", () => {
   it("no indents", () => {
-    const source = makeMockSource();
-    source.text = "function base(boo) {\n}";
-    expect(isMinified(source)).toBe(true);
+    const sourceWithContent = makeMockSourceWithContent(
+      undefined,
+      undefined,
+      undefined,
+      "function base(boo) {\n}"
+    );
+    expect(isMinified(sourceWithContent)).toBe(true);
   });
 });
--- a/devtools/client/debugger/src/utils/tests/source.spec.js
+++ b/devtools/client/debugger/src/utils/tests/source.spec.js
@@ -11,17 +11,23 @@ import {
   getDisplayPath,
   getMode,
   getSourceLineCount,
   isThirdParty,
   isJavaScript,
   isUrlExtension
 } from "../source.js";
 
-import { makeMockSource, makeMockWasmSource } from "../test-mockup";
+import {
+  makeMockSource,
+  makeMockSourceWithContent,
+  makeMockSourceAndContent,
+  makeMockWasmSourceWithContent
+} from "../test-mockup";
+import { isFulfilled } from "../async-value.js";
 
 const defaultSymbolDeclarations = {
   classes: [],
   functions: [],
   memberExpressions: [],
   callExpressions: [],
   objectProperties: [],
   identifiers: [],
@@ -225,35 +231,59 @@ describe("sources", () => {
       expect(
         getFileURL(makeMockSource(`http://${encodedUnicode.repeat(39)}.html`))
       ).toBe(`…ttp://${unicode.repeat(39)}.html`);
     });
   });
 
   describe("isJavaScript", () => {
     it("is not JavaScript", () => {
-      expect(isJavaScript(makeMockSource("foo.html", undefined, ""))).toBe(
-        false
-      );
-      expect(
-        isJavaScript(makeMockSource(undefined, undefined, "text/html"))
-      ).toBe(false);
+      {
+        const { source, content } = makeMockSourceAndContent(
+          "foo.html",
+          undefined,
+          ""
+        );
+        expect(isJavaScript(source, content)).toBe(false);
+      }
+      {
+        const { source, content } = makeMockSourceAndContent(
+          undefined,
+          undefined,
+          "text/html"
+        );
+        expect(isJavaScript(source, content)).toBe(false);
+      }
     });
 
     it("is JavaScript", () => {
-      expect(isJavaScript(makeMockSource("foo.js"))).toBe(true);
-      expect(isJavaScript(makeMockSource("bar.jsm"))).toBe(true);
-      expect(
-        isJavaScript(makeMockSource(undefined, undefined, "text/javascript"))
-      ).toBe(true);
-      expect(
-        isJavaScript(
-          makeMockSource(undefined, undefined, "application/javascript")
-        )
-      ).toBe(true);
+      {
+        const { source, content } = makeMockSourceAndContent("foo.js");
+        expect(isJavaScript(source, content)).toBe(true);
+      }
+      {
+        const { source, content } = makeMockSourceAndContent("bar.jsm");
+        expect(isJavaScript(source, content)).toBe(true);
+      }
+      {
+        const { source, content } = makeMockSourceAndContent(
+          undefined,
+          undefined,
+          "text/javascript"
+        );
+        expect(isJavaScript(source, content)).toBe(true);
+      }
+      {
+        const { source, content } = makeMockSourceAndContent(
+          undefined,
+          undefined,
+          "application/javascript"
+        );
+        expect(isJavaScript(source, content)).toBe(true);
+      }
     });
   });
 
   describe("isThirdParty", () => {
     it("node_modules", () => {
       expect(isThirdParty(makeMockSource("/node_modules/foo.js"))).toBe(true);
     });
 
@@ -265,176 +295,198 @@ describe("sources", () => {
 
     it("not third party", () => {
       expect(isThirdParty(makeMockSource("/bar/foo.js"))).toBe(false);
     });
   });
 
   describe("getMode", () => {
     it("//@flow", () => {
-      const source = makeMockSource(
+      const { source, content } = makeMockSourceAndContent(
         undefined,
         undefined,
         "text/javascript",
         "// @flow"
       );
-      expect(getMode(source)).toEqual({ name: "javascript", typescript: true });
+      expect(getMode(source, content)).toEqual({
+        name: "javascript",
+        typescript: true
+      });
     });
 
     it("/* @flow */", () => {
-      const source = makeMockSource(
+      const { source, content } = makeMockSourceAndContent(
         undefined,
         undefined,
         "text/javascript",
         "   /* @flow */"
       );
-      expect(getMode(source)).toEqual({ name: "javascript", typescript: true });
+      expect(getMode(source, content)).toEqual({
+        name: "javascript",
+        typescript: true
+      });
     });
 
     it("mixed html", () => {
-      const source = makeMockSource(undefined, undefined, "", " <html");
-      expect(getMode(source)).toEqual({ name: "htmlmixed" });
+      const { source, content } = makeMockSourceAndContent(
+        undefined,
+        undefined,
+        "",
+        " <html"
+      );
+      expect(getMode(source, content)).toEqual({ name: "htmlmixed" });
     });
 
     it("elm", () => {
-      const source = makeMockSource(
+      const { source, content } = makeMockSourceAndContent(
         undefined,
         undefined,
         "text/x-elm",
         'main = text "Hello, World!"'
       );
-      expect(getMode(source)).toEqual({ name: "elm" });
+      expect(getMode(source, content)).toEqual({ name: "elm" });
     });
 
     it("returns jsx if contentType jsx is given", () => {
-      const source = makeMockSource(
+      const { source, content } = makeMockSourceAndContent(
         undefined,
         undefined,
         "text/jsx",
         "<h1></h1>"
       );
-      expect(getMode(source)).toEqual({ name: "jsx" });
+      expect(getMode(source, content)).toEqual({ name: "jsx" });
     });
 
     it("returns jsx if sourceMetaData says it's a react component", () => {
-      const source = makeMockSource(undefined, undefined, "", "<h1></h1>");
+      const { source, content } = makeMockSourceAndContent(
+        undefined,
+        undefined,
+        "",
+        "<h1></h1>"
+      );
       expect(
-        getMode(source, { ...defaultSymbolDeclarations, hasJsx: true })
+        getMode(source, content, { ...defaultSymbolDeclarations, hasJsx: true })
       ).toEqual({ name: "jsx" });
     });
 
     it("returns jsx if the fileExtension is .jsx", () => {
-      const source = makeMockSource(
+      const { source, content } = makeMockSourceAndContent(
         "myComponent.jsx",
         undefined,
         "",
         "<h1></h1>"
       );
-      expect(getMode(source)).toEqual({ name: "jsx" });
+      expect(getMode(source, content)).toEqual({ name: "jsx" });
     });
 
     it("returns text/x-haxe if the file extension is .hx", () => {
-      const source = makeMockSource(
+      const { source, content } = makeMockSourceAndContent(
         "myComponent.hx",
         undefined,
         "",
         "function foo(){}"
       );
-      expect(getMode(source)).toEqual({ name: "text/x-haxe" });
+      expect(getMode(source, content)).toEqual({ name: "text/x-haxe" });
     });
 
     it("typescript", () => {
-      const source = makeMockSource(
+      const { source, content } = makeMockSourceAndContent(
         undefined,
         undefined,
         "text/typescript",
         "function foo(){}"
       );
-      expect(getMode(source)).toEqual({ name: "javascript", typescript: true });
+      expect(getMode(source, content)).toEqual({
+        name: "javascript",
+        typescript: true
+      });
     });
 
     it("typescript-jsx", () => {
-      const source = makeMockSource(
+      const { source, content } = makeMockSourceAndContent(
         undefined,
         undefined,
         "text/typescript-jsx",
         "<h1></h1>"
       );
-      expect(getMode(source).base).toEqual({
+      expect(getMode(source, content).base).toEqual({
         name: "javascript",
         typescript: true
       });
     });
 
     it("cross-platform clojure(script) with reader conditionals", () => {
-      const source = makeMockSource(
+      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)).toEqual({ name: "clojure" });
+      expect(getMode(source, content)).toEqual({ name: "clojure" });
     });
 
     it("clojurescript", () => {
-      const source = makeMockSource(
+      const { source, content } = makeMockSourceAndContent(
         "my-clojurescript-source.cljs",
         undefined,
         "text/x-clojurescript",
         "(+ 1 2 3)"
       );
-      expect(getMode(source)).toEqual({ name: "clojure" });
+      expect(getMode(source, content)).toEqual({ name: "clojure" });
     });
 
     it("coffeescript", () => {
-      const source = makeMockSource(
+      const { source, content } = makeMockSourceAndContent(
         undefined,
         undefined,
         "text/coffeescript",
         "x = (a) -> 3"
       );
-      expect(getMode(source)).toEqual({ name: "coffeescript" });
+      expect(getMode(source, content)).toEqual({ name: "coffeescript" });
     });
 
     it("wasm", () => {
-      const source = makeMockWasmSource({
+      const { source, content } = makeMockWasmSourceWithContent({
         binary: "\x00asm\x01\x00\x00\x00"
       });
-      expect(getMode(source)).toEqual({ name: "text" });
+      expect(getMode(source, content.value)).toEqual({ name: "text" });
     });
 
     it("marko", () => {
-      const source = makeMockSource(
+      const { source, content } = makeMockSourceAndContent(
         "http://localhost.com:7999/increment/sometestfile.marko",
         undefined,
         "does not matter",
         "function foo(){}"
       );
-      expect(getMode(source)).toEqual({ name: "javascript" });
+      expect(getMode(source, content)).toEqual({ name: "javascript" });
     });
   });
 
   describe("getSourceLineCount", () => {
     it("should give us the amount bytes for wasm source", () => {
-      const source = makeMockWasmSource({
+      const { content } = makeMockWasmSourceWithContent({
         binary: "\x00asm\x01\x00\x00\x00"
       });
-      expect(getSourceLineCount(source)).toEqual(8);
+      expect(getSourceLineCount(content.value)).toEqual(8);
     });
 
     it("should give us the amout of lines for js source", () => {
-      const source = makeMockSource(
+      const { content } = makeMockSourceWithContent(
         undefined,
         undefined,
         "text/javascript",
         "function foo(){\n}"
       );
-      expect(getSourceLineCount(source)).toEqual(2);
+      if (!content || !isFulfilled(content)) {
+        throw new Error("Unexpected content value");
+      }
+      expect(getSourceLineCount(content.value)).toEqual(2);
     });
   });
 
   describe("isUrlExtension", () => {
     it("should detect mozilla extenstion", () => {
       expect(isUrlExtension("moz-extension://id/js/content.js")).toBe(true);
     });
     it("should detect chrome extenstion", () => {
--- a/devtools/client/debugger/src/utils/tests/wasm.spec.js
+++ b/devtools/client/debugger/src/utils/tests/wasm.spec.js
@@ -7,17 +7,17 @@
 import {
   isWasm,
   lineToWasmOffset,
   wasmOffsetToLine,
   clearWasmStates,
   renderWasmText
 } from "../wasm.js";
 
-import { makeMockWasmSource } from "../test-mockup";
+import { makeMockWasmSourceWithContent } from "../test-mockup";
 
 describe("wasm", () => {
   // Compiled version of `(module (func (nop)))`
   const SIMPLE_WASM = {
     binary:
       "\x00asm\x01\x00\x00\x00\x01\x84\x80\x80\x80\x00\x01`\x00\x00" +
       "\x03\x82\x80\x80\x80\x00\x01\x00\x06\x81\x80\x80\x80\x00\x00" +
       "\n\x89\x80\x80\x80\x00\x01\x83\x80\x80\x80\x00\x00\x01\v"
@@ -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 = makeMockWasmSource(SIMPLE_WASM);
-      renderWasmText(source);
+      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 = makeMockWasmSource(SIMPLE_WASM);
-      const lines = renderWasmText(source);
+      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 = makeMockWasmSource(SIMPLE_WASM);
-      renderWasmText(source);
+      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 = makeMockWasmSource(SIMPLE_WASM);
-      renderWasmText(source);
+      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/utils/utils.js
+++ b/devtools/client/debugger/src/utils/utils.js
@@ -1,15 +1,15 @@
 /* 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 { Source } from "../types";
+import type { SourceContent } from "../types";
 
 /**
  * Utils for utils, by utils
  * @module utils/utils
  */
 
 /**
  * @memberof utils/utils
@@ -50,22 +50,22 @@ export function endTruncateStr(str: any,
   }
   return str;
 }
 
 export function waitForMs(ms: number): Promise<void> {
   return new Promise(resolve => setTimeout(resolve, ms));
 }
 
-export function downloadFile(source: Source, fileName: string) {
-  if (source.isWasm) {
+export function downloadFile(content: SourceContent, fileName: string) {
+  if (content.type !== "text") {
     return;
   }
 
-  const data = source.text;
+  const data = content.value;
   const { body } = document;
   if (!body) {
     return;
   }
 
   const a = document.createElement("a");
   body.appendChild(a);
   a.className = "download-anchor";
--- a/devtools/client/debugger/src/utils/wasm.js
+++ b/devtools/client/debugger/src/utils/wasm.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 { BinaryReader } from "wasmparser/dist/WasmParser";
 import { WasmDisassembler, NameSectionReader } from "wasmparser/dist/WasmDis";
 
-import type { WasmSource } from "../types";
+import type { SourceId, WasmSourceContent } from "../types";
 type WasmState = {
   lines: Array<number>,
   offsets: Array<number>
 };
 
 var wasmStates: { [string]: WasmState } = (Object.create(null): any);
 
 function maybeWasmSectionNameResolver(data: Uint8Array) {
@@ -133,34 +133,33 @@ export function wasmOffsetToLine(sourceI
 /**
  * @memberof utils/wasm
  * @static
  */
 export function clearWasmStates() {
   wasmStates = (Object.create(null): any);
 }
 
-const wasmLines: WeakMap<WasmSource, string[]> = new WeakMap();
-export function renderWasmText(source: WasmSource): string[] {
-  if (wasmLines.has(source)) {
-    return wasmLines.get(source) || [];
-  }
-
-  if (!source.text) {
-    return [];
+const wasmLines: WeakMap<WasmSourceContent, string[]> = new WeakMap();
+export function renderWasmText(
+  sourceId: SourceId,
+  content: WasmSourceContent
+): string[] {
+  if (wasmLines.has(content)) {
+    return wasmLines.get(content) || [];
   }
 
   // binary does not survive as Uint8Array, converting from string
-  const { binary } = source.text;
+  const { binary } = content.value;
   const data = new Uint8Array(binary.length);
   for (let i = 0; i < data.length; i++) {
     data[i] = binary.charCodeAt(i);
   }
-  const { lines } = getWasmText(source.id, data);
+  const { lines } = getWasmText(sourceId, data);
   const MAX_LINES = 1000000;
   if (lines.length > MAX_LINES) {
     lines.splice(MAX_LINES, lines.length - MAX_LINES);
     lines.push(";; .... text is truncated due to the size");
   }
 
-  wasmLines.set(source, lines);
+  wasmLines.set(content, lines);
   return lines;
 }
--- a/devtools/client/debugger/src/workers/parser/index.js
+++ b/devtools/client/debugger/src/workers/parser/index.js
@@ -3,17 +3,17 @@
  * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
 
 // @flow
 
 import { workerUtils } from "devtools-utils";
 const { WorkerDispatcher } = workerUtils;
 
 import type { AstSource, AstLocation, AstPosition } from "./types";
-import type { SourceLocation, Source, SourceId } from "../../types";
+import type { SourceLocation, SourceId, SourceContent } from "../../types";
 import type { SourceScope } from "./getScopes/visitor";
 import type { SymbolDeclarations } from "./getSymbols";
 
 const dispatcher = new WorkerDispatcher();
 export const start = (url: string, win: any = window) =>
   dispatcher.start(url, win);
 export const stop = () => dispatcher.stop();
 
@@ -41,22 +41,25 @@ export const clearScopes = async (): Pro
 
 export const clearSymbols = async (): Promise<void> =>
   dispatcher.invoke("clearSymbols");
 
 export const getSymbols = async (
   sourceId: string
 ): Promise<SymbolDeclarations> => dispatcher.invoke("getSymbols", sourceId);
 
-export const setSource = async (source: Source): Promise<void> => {
+export const setSource = async (
+  sourceId: SourceId,
+  content: SourceContent
+): Promise<void> => {
   const astSource: AstSource = {
-    id: source.id,
-    text: source.isWasm ? "" : source.text || "",
-    contentType: source.contentType || null,
-    isWasm: source.isWasm
+    id: sourceId,
+    text: content.type === "wasm" ? "" : content.value,
+    contentType: content.contentType || null,
+    isWasm: content.type === "wasm"
   };
 
   await dispatcher.invoke("setSource", astSource);
 };
 
 export const clearSources = async (): Promise<void> =>
   dispatcher.invoke("clearSources");
 
--- 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
@@ -2,61 +2,108 @@
  * 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 fs from "fs";
 import path from "path";
 
-import type { Source } from "../../../../types";
-import { makeMockSource } from "../../../../utils/test-mockup";
+import type {
+  Source,
+  TextSourceContent,
+  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(
     path.join(__dirname, `../fixtures/${name}.${type}`),
     "utf8"
   );
 }
 
-export function getSource(name: string, type: string = "js"): Source {
+function getSourceContent(
+  name: string,
+  type: string = "js"
+): TextSourceContent {
   const text = getFixture(name, type);
   let contentType = "text/javascript";
   if (type === "html") {
     contentType = "text/html";
   } else if (type === "vue") {
     contentType = "text/vue";
   } else if (type === "ts") {
     contentType = "text/typescript";
   } else if (type === "tsx") {
     contentType = "text/typescript-jsx";
   }
 
-  return makeMockSource(undefined, name, contentType, text);
+  return {
+    type: "text",
+    value: text,
+    contentType
+  };
+}
+
+export function getSource(name: string, type?: string): Source {
+  return getSourceWithContent(name, type).source;
 }
 
-export function getOriginalSource(name: string, type: string = "js"): Source {
-  const source = getSource(name, type);
-  return ({ ...source, id: `${name}/originalSource-1` }: any);
+export function getSourceWithContent(
+  name: string,
+  type?: string
+): { source: Source, content: TextSourceContent } {
+  const { value: text, contentType } = getSourceContent(name, type);
+
+  return makeMockSourceAndContent(undefined, name, contentType, text);
 }
 
-export function populateSource(name: string, type?: string): Source {
-  const source = getSource(name, type);
+export function populateSource(name: string, type?: string): SourceWithContent {
+  const { source, content } = getSourceWithContent(name, type);
   setSource({
     id: source.id,
-    text: source.isWasm ? "" : source.text || "",
-    contentType: source.contentType,
+    text: content.value,
+    contentType: content.contentType,
     isWasm: false
   });
-  return source;
+  return {
+    source,
+    content: asyncValue.fulfilled(content)
+  };
+}
+
+export function getOriginalSource(name: string, type?: string): Source {
+  return getOriginalSourceWithContent(name, type).source;
 }
 
-export function populateOriginalSource(name: string, type?: string): Source {
-  const source = getOriginalSource(name, type);
+export function getOriginalSourceWithContent(
+  name: string,
+  type?: string
+): { 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 { source, content } = getOriginalSourceWithContent(name, type);
   setSource({
     id: source.id,
-    text: source.isWasm ? "" : source.text || "",
-    contentType: source.contentType,
+    text: content.value,
+    contentType: content.contentType,
     isWasm: false
   });
-  return source;
+  return {
+    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
@@ -3,40 +3,41 @@
  * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
 
 // @flow
 
 import { getAst } from "../ast";
 import { setSource } from "../../sources";
 import cases from "jest-in-case";
 
-import { makeMockSource } from "../../../../utils/test-mockup";
-
-function createSource(contentType) {
-  return makeMockSource(undefined, "foo", contentType, "2");
-}
+import { makeMockSourceAndContent } from "../../../../utils/test-mockup";
 
 const astKeys = [
   "type",
   "start",
   "end",
   "loc",
   "program",
   "comments",
   "tokens"
 ];
 
 cases(
   "ast.getAst",
   ({ name }) => {
-    const source = createSource(name);
+    const { source, content } = makeMockSourceAndContent(
+      undefined,
+      "foo",
+      name,
+      "2"
+    );
     setSource({
       id: source.id,
-      text: source.text || "",
-      contentType: source.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/src/workers/pretty-print/index.js
+++ b/devtools/client/debugger/src/workers/pretty-print/index.js
@@ -1,34 +1,25 @@
 /* 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 { workerUtils } from "devtools-utils";
 const { WorkerDispatcher } = workerUtils;
-import { isJavaScript } from "../../utils/source";
-import assert from "../../utils/assert";
-
-import type { Source } from "../../types";
 
 const dispatcher = new WorkerDispatcher();
 export const start = dispatcher.start.bind(dispatcher);
 export const stop = dispatcher.stop.bind(dispatcher);
-const _prettyPrint = dispatcher.task("prettyPrint");
 
 type PrettyPrintOpts = {
-  source: Source,
+  text: string,
   url: string
 };
 
-export async function prettyPrint({ source, url }: PrettyPrintOpts) {
-  const indent = 2;
-
-  assert(isJavaScript(source), "Can't prettify non-javascript files.");
-
-  return _prettyPrint({
+export async function prettyPrint({ text, url }: PrettyPrintOpts) {
+  return dispatcher.invoke("prettyPrint", {
     url,
-    indent,
-    sourceText: source.text
+    indent: 2,
+    sourceText: text
   });
 }
--- a/devtools/client/debugger/src/workers/search/project-search.js
+++ b/devtools/client/debugger/src/workers/search/project-search.js
@@ -3,36 +3,40 @@
  * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
 
 // @flow
 
 // Maybe reuse file search's functions?
 
 import getMatches from "./get-matches";
 
-import type { Source } from "../../types";
+import type { SourceId, TextSourceContent } from "../../types";
 
-export function findSourceMatches(source: Source, queryText: string): Object[] {
-  const { id, loadedState, text } = source;
-  if (loadedState != "loaded" || typeof text != "string" || queryText == "") {
+export function findSourceMatches(
+  sourceId: SourceId,
+  content: TextSourceContent,
+  queryText: string
+): Object[] {
+  if (queryText == "") {
     return [];
   }
 
   const modifiers = {
     caseSensitive: false,
     regexMatch: false,
     wholeWord: false
   };
 
+  const text = content.value;
   const lines = text.split("\n");
 
   return getMatches(queryText, text, modifiers).map(({ line, ch }) => {
     const { value, matchIndex } = truncateLine(lines[line], ch);
     return {
-      sourceId: id,
+      sourceId,
       line: line + 1,
       column: ch,
       matchIndex,
       match: queryText,
       value
     };
   });
 }
@@ -42,17 +46,17 @@ const startRegex = /([ !@#$%^&*()_+\-=\[
 // Similarly, find
 const endRegex = new RegExp(
   [
     "([ !@#$%^&*()_+-=[]{};':\"\\|,.<>/?])",
     '[^ !@#$%^&*()_+-=[]{};\':"\\|,.<>/?]*$"/'
   ].join("")
 );
 
-function truncateLine(text, column) {
+function truncateLine(text: string, column: number) {
   if (text.length < 100) {
     return {
       matchIndex: column,
       value: text
     };
   }
 
   // Initially take 40 chars left to the match
--- a/devtools/client/debugger/src/workers/search/tests/project-search.spec.js
+++ b/devtools/client/debugger/src/workers/search/tests/project-search.spec.js
@@ -10,47 +10,31 @@ const text = `
   function foo() {
     foo();
   }
 `;
 
 describe("project search", () => {
   const emptyResults = [];
 
-  it("throws on lack of source", () => {
-    const needle = "test";
-    const source: any = null;
-    const matches = () => findSourceMatches(source, needle);
-    expect(matches).toThrow(TypeError);
-  });
-
-  it("handles empty source object", () => {
-    const needle = "test";
-    const source: any = {};
-    const matches = findSourceMatches(source, needle);
-    expect(matches).toEqual(emptyResults);
-  });
-
   it("finds matches", () => {
     const needle = "foo";
-    const source: any = {
-      text,
-      loadedState: "loaded",
-      id: "bar.js",
-      url: "http://example.com/foo/bar.js"
+    const content = {
+      type: "text",
+      value: text,
+      contentType: undefined
     };
 
-    const matches = findSourceMatches(source, needle);
+    const matches = findSourceMatches("bar.js", content, needle);
     expect(matches).toMatchSnapshot();
   });
 
   it("finds no matches in source", () => {
     const needle = "test";
-    const source: any = {
-      text,
-      loadedState: "loaded",
-      id: "bar.js",
-      url: "http://example.com/foo/bar.js"
+    const content = {
+      type: "text",
+      value: text,
+      contentType: undefined
     };
-    const matches = findSourceMatches(source, needle);
+    const matches = findSourceMatches("bar.js", content, needle);
     expect(matches).toEqual(emptyResults);
   });
 });
--- a/devtools/client/debugger/test/mochitest/.eslintrc
+++ b/devtools/client/debugger/test/mochitest/.eslintrc
@@ -81,16 +81,17 @@
     "assertHighlightLocation": false,
     "assertScopes": false,
     "createDebuggerContext": false,
     "initDebugger": false,
     "invokeInTab": false,
     "invokeWithBreakpoint": false,
     "findBreakpoint": false,
     "findSource": false,
+    "findSourceContent": false,
     "findElement": false,
     "findElementWithSelector": false,
     "findAllElements": false,
     "getIsPaused": false,
     "getThreadContext": false,
     "getWorkers": false,
     "openNewTabAndToolbox": false,
     "selectLocation": false,
--- a/devtools/client/debugger/test/mochitest/browser_dbg-editor-highlight.js
+++ b/devtools/client/debugger/test/mochitest/browser_dbg-editor-highlight.js
@@ -4,17 +4,17 @@
 
 // Tests that the editor will always highight the right line, no
 // matter if the source text doesn't exist yet or even if the source
 // doesn't exist.
 
 add_task(async function() {
   const dbg = await initDebugger("doc-scripts.html", "long.js");
   const {
-    selectors: { getSource },
+    selectors: { getSource, getSourceContent },
     getState
   } = dbg;
   const sourceUrl = `${EXAMPLE_URL}long.js`;
 
   // The source itself doesn't even exist yet, and using
   // `selectSourceURL` will set a pending request to load this source
   // and highlight a specific line.
 
@@ -36,14 +36,14 @@ add_task(async function() {
   // Test jumping to a line in a source that exists but hasn't been
   // loaded yet.
   log("Select an unloaded source");
   selectSource(dbg, "simple1.js", 6);
 
   // Make sure the source is in the loading state, wait for it to be
   // fully loaded, and check the highlighted line.
   const simple1 = findSource(dbg, "simple1.js");
-  is(getSource(simple1.id).loadedState, "loading");
+  is(getSourceContent(simple1.id), null);
 
   await waitForSelectedSource(dbg, "simple1.js");
-  ok(getSource(simple1.id).text);
+  ok(getSourceContent(simple1.id).value.value);
   assertHighlightLocation(dbg, "simple1.js", 6);
 });
--- a/devtools/client/debugger/test/mochitest/browser_dbg-inline-cache.js
+++ b/devtools/client/debugger/test/mochitest/browser_dbg-inline-cache.js
@@ -69,82 +69,82 @@ add_task(async function() {
   const dbg = createDebuggerContext(toolbox);
   info("Reload tab to ensure debugger finds script");
   await reloadTabAndDebugger(tab, dbg);
   let pageValue = await getPageValue(tab);
   is(pageValue, "let x = 1;", "Content loads from network, has doc value 1");
   await waitForSource(dbg, "inline-cache.html");
   await selectSource(dbg, "inline-cache.html");
   await waitForLoadedSource(dbg, "inline-cache.html");
-  let dbgValue = await findSource(dbg, "inline-cache.html");
-  info(`Debugger text: ${dbgValue.text}`);
+  let dbgValue = findSourceContent(dbg, "inline-cache.html");
+  info(`Debugger text: ${dbgValue.value}`);
   ok(
-    dbgValue.text.includes(pageValue),
+    dbgValue.value.includes(pageValue),
     "Debugger loads from cache, gets value 1 like page"
   );
 
   info("Disable HTTP cache for page");
   await toolbox.target.reconfigure({ options: { cacheDisabled: true } });
   makeChanges();
 
   info("Reload inside debugger with toolbox caching disabled (attempt 1)");
   await reloadTabAndDebugger(tab, dbg);
   pageValue = await getPageValue(tab);
   is(pageValue, "let x = 2;", "Content loads from network, has doc value 2");
   await waitForLoadedSource(dbg, "inline-cache.html");
-  dbgValue = await findSource(dbg, "inline-cache.html");
-  info(`Debugger text: ${dbgValue.text}`);
+  dbgValue = findSourceContent(dbg, "inline-cache.html");
+  info(`Debugger text: ${dbgValue.value}`);
   ok(
-    dbgValue.text.includes(pageValue),
+    dbgValue.value.includes(pageValue),
     "Debugger loads from network, gets value 2 like page"
   );
 
   makeChanges();
 
   info("Reload inside debugger with toolbox caching disabled (attempt 2)");
   await reloadTabAndDebugger(tab, dbg);
   pageValue = await getPageValue(tab);
   is(pageValue, "let x = 3;", "Content loads from network, has doc value 3");
   await waitForLoadedSource(dbg, "inline-cache.html");
-  dbgValue = await findSource(dbg, "inline-cache.html");
-  info(`Debugger text: ${dbgValue.text}`);
+  dbgValue = findSourceContent(dbg, "inline-cache.html");
+  info(`Debugger text: ${dbgValue.value}`);
   ok(
-    dbgValue.text.includes(pageValue),
+    dbgValue.value.includes(pageValue),
     "Debugger loads from network, gets value 3 like page"
   );
 
   info("Enable HTTP cache for page");
   await toolbox.target.reconfigure({ options: { cacheDisabled: false } });
   makeChanges();
 
   // Even though the HTTP cache is now enabled, Gecko sets the VALIDATE_ALWAYS flag when
   // reloading the page.  So, it will always make a request to the server for the main
   // document contents.
 
   info("Reload inside debugger with toolbox caching enabled (attempt 1)");
   await reloadTabAndDebugger(tab, dbg);
   pageValue = await getPageValue(tab);
   is(pageValue, "let x = 4;", "Content loads from network, has doc value 4");
   await waitForLoadedSource(dbg, "inline-cache.html");
-  dbgValue = await findSource(dbg, "inline-cache.html");
-  info(`Debugger text: ${dbgValue.text}`);
+  dbgValue = findSourceContent(dbg, "inline-cache.html");
+  info(`Debugger text: ${dbgValue.value}`);
   ok(
-    dbgValue.text.includes(pageValue),
+    dbgValue.value.includes(pageValue),
     "Debugger loads from cache, gets value 4 like page"
   );
 
   makeChanges();
 
   info("Reload inside debugger with toolbox caching enabled (attempt 2)");
   await reloadTabAndDebugger(tab, dbg);
   pageValue = await getPageValue(tab);
   is(pageValue, "let x = 5;", "Content loads from network, has doc value 5");
   await waitForLoadedSource(dbg, "inline-cache.html");
-  dbgValue = await findSource(dbg, "inline-cache.html");
-  info(`Debugger text: ${dbgValue.text}`);
+  dbgValue = findSourceContent(dbg, "inline-cache.html");
+  info(`Debugger text: ${dbgValue.value}`);
   ok(
-    dbgValue.text.includes(pageValue),
+    dbgValue.value.includes(pageValue),
     "Debugger loads from cache, gets value 5 like page"
   );
 
   await toolbox.destroy();
   await removeTab(tab);
 });
--- a/devtools/client/debugger/test/mochitest/helpers.js
+++ b/devtools/client/debugger/test/mochitest/helpers.js
@@ -17,20 +17,16 @@ Services.scriptloader.loadSubScript(
 var { Toolbox } = require("devtools/client/framework/toolbox");
 var { Task } = require("devtools/shared/task");
 var asyncStorage = require("devtools/shared/async-storage");
 
 const {
   getSelectedLocation
 } = require("devtools/client/debugger/src/utils/source-maps");
 
-const sourceUtils = {
-  isLoaded: source => source.loadedState === "loaded"
-};
-
 function log(msg, data) {
   info(`${msg} ${!data ? "" : JSON.stringify(data)}`);
 }
 
 function logThreadEvents(dbg, event) {
   const thread = dbg.toolbox.threadClient;
 
   thread.addListener(event, function onEvent(eventName, ...args) {
@@ -226,27 +222,26 @@ function waitForSelectedLocation(dbg, li
   return waitForState(dbg, state => {
     const location = dbg.selectors.getSelectedLocation();
     return location && location.line == line;
   });
 }
 
 function waitForSelectedSource(dbg, url) {
   const {
-    getSelectedSource,
+    getSelectedSourceWithContent,
     hasSymbols,
     hasBreakpointPositions
   } = dbg.selectors;
 
   return waitForState(
     dbg,
     state => {
-      const source = getSelectedSource();
-      const isLoaded = source && sourceUtils.isLoaded(source);
-      if (!isLoaded) {
+      const { source, content } = getSelectedSourceWithContent() || {};
+      if (!content) {
         return false;
       }
 
       if (!url) {
         return true;
       }
 
       const newSource = findSource(dbg, url, { silent: true });
@@ -312,18 +307,18 @@ 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.getSelectedSource();
-  if (source && source.loadedState == "loading") {
+  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;
   }
 
@@ -508,24 +503,23 @@ 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.getSelectedSource();
+  const { source, content } = dbg.selectors.getSelectedSourceWithContent() || {};
 
   if (!source) {
     return false;
   }
 
-  const isLoaded = source.loadedState && sourceUtils.isLoaded(source);
-  if (!isLoaded) {
+  if (!content) {
     return false;
   }
 
   return source.id == sourceId;
 }
 
 /**
  * Clear all the debugger related preferences.
@@ -611,34 +605,55 @@ function findSource(dbg, url, { silent }
     }
 
     throw new Error(`Unable to find source: ${url}`);
   }
 
   return source;
 }
 
+function findSourceContent(dbg, url, opts) {
+  const source = findSource(dbg, url, opts);
+
+  if (!source) return null;
+
+  const content = dbg.selectors.getSourceContent(source.id);
+
+  if (!content) {
+    return null;
+  }
+
+  if (content.state !== "fulfilled") {
+    throw new Error("Expected loaded source, got" + content.value);
+  }
+
+  return content.value;
+}
+
 function sourceExists(dbg, url) {
   return !!findSource(dbg, url, { silent: true });
 }
 
 function waitForLoadedSource(dbg, url) {
   return waitForState(
     dbg,
-    state => findSource(dbg, url, { silent: true }).loadedState == "loaded",
+    state => {
+      const source = findSource(dbg, url, { silent: true });
+      return source && dbg.selectors.getSourceContent(source.id);
+    },
     "loaded source"
   );
 }
 
 function waitForLoadedSources(dbg) {
   return waitForState(
     dbg,
     state => {
       const sources = Object.values(dbg.selectors.getSources());
-      return !sources.some(source => source.loadedState == "loading");
+      return sources.every(source => !!dbg.selectors.getSourceContent(source.id));
     },
     "loaded source"
   );
 }
 
 function getContext(dbg) {
   return dbg.selectors.getContext();
 }
--- a/testing/talos/talos/tests/devtools/addon/content/tests/debugger/debugger-helpers.js
+++ b/testing/talos/talos/tests/devtools/addon/content/tests/debugger/debugger-helpers.js
@@ -179,19 +179,18 @@ function selectSource(dbg, url) {
   dump(`Selecting source: ${url}\n`);
   const line = 1;
   const source = findSource(dbg, url);
   const cx = dbg.selectors.getContext(dbg.getState());
   dbg.actions.selectLocation(cx, { sourceId: source.id, line });
   return waitForState(
     dbg,
     state => {
-      const source = dbg.selectors.getSelectedSource(state);
-      const isLoaded = source && source.loadedState === "loaded";
-      if (!isLoaded) {
+      const { source, content } = dbg.selectors.getSelectedSourceWithContent(state);
+      if (!content) {
         return false;
       }
 
       // wait for symbols -- a flat map of all named variables in a file -- to be calculated.
       // this is a slow process and becomes slower the larger the file is
       return dbg.selectors.hasSymbols(state, source);
     },
     "selected source"