merge mozilla-inbound to mozilla-central. r=merge a=merge
authorSebastian Hengst <archaeopteryx@coole-files.de>
Sun, 30 Jul 2017 11:19:17 +0200
changeset 371910 8b577b152383f5560ab57fdc307fa872cd268ea7
parent 371846 6ee53b46474f74c231b1b569e8f2ecd9d0948a8f (current diff)
parent 371909 57ff68e8e3fc476bbe5bc73addcca13bb5ce0097 (diff)
child 371911 7051e8c01179ae255a323197e1e1c32d832e20d7
child 371924 ef19aed2b62bfcf18d92f3f04c5142abbb797393
push id32256
push userarchaeopteryx@coole-files.de
push dateSun, 30 Jul 2017 09:19:42 +0000
treeherdermozilla-central@8b577b152383 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge, merge
milestone56.0a1
first release with
nightly linux32
8b577b152383 / 56.0a1 / 20170730100307 / files
nightly linux64
8b577b152383 / 56.0a1 / 20170730100307 / files
nightly mac
8b577b152383 / 56.0a1 / 20170730100307 / files
nightly win32
8b577b152383 / 56.0a1 / 20170730100307 / files
nightly win64
8b577b152383 / 56.0a1 / 20170730100307 / files
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
releases
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
merge mozilla-inbound to mozilla-central. r=merge a=merge MozReview-Commit-ID: 44WBcWjnVo
browser/app/profile/firefox.js
browser/base/content/test/performance/browser_windowopen_reflows.js
dom/html/nsGenericHTMLElement.cpp
media/libpng/sse2/filter_sse2_intrinsics.c
media/libpng/sse2/intel_init.c
taskcluster/actions/__init__.py
taskcluster/actions/add-new-jobs.py
taskcluster/actions/hello-action.py
taskcluster/actions/registry.py
taskcluster/actions/retrigger.py
taskcluster/actions/run_missing_tests.py
taskcluster/actions/test-retrigger-action.py
taskcluster/actions/util.py
taskcluster/taskgraph/decision.py
testing/web-platform/meta/css/selectors4/focus-within-009.html.ini
testing/web-platform/meta/css/vendor-imports/mozilla/mozilla-central-reftests/images3/object-fit-cover-004e.html.ini
testing/web-platform/meta/css/vendor-imports/mozilla/mozilla-central-reftests/images3/object-fit-cover-004o.html.ini
testing/web-platform/meta/streams/byte-length-queuing-strategy.dedicatedworker.html.ini
testing/web-platform/meta/streams/byte-length-queuing-strategy.html.ini
testing/web-platform/meta/streams/byte-length-queuing-strategy.serviceworker.https.html.ini
testing/web-platform/meta/streams/byte-length-queuing-strategy.sharedworker.html.ini
testing/web-platform/meta/streams/count-queuing-strategy.dedicatedworker.html.ini
testing/web-platform/meta/streams/count-queuing-strategy.html.ini
testing/web-platform/meta/streams/count-queuing-strategy.serviceworker.https.html.ini
testing/web-platform/meta/streams/count-queuing-strategy.sharedworker.html.ini
testing/web-platform/meta/streams/readable-byte-streams/general.dedicatedworker.html.ini
testing/web-platform/meta/streams/readable-byte-streams/general.html.ini
testing/web-platform/meta/streams/readable-byte-streams/general.serviceworker.https.html.ini
testing/web-platform/meta/streams/readable-byte-streams/general.sharedworker.html.ini
testing/web-platform/meta/streams/readable-streams/bad-strategies.dedicatedworker.html.ini
testing/web-platform/meta/streams/readable-streams/bad-strategies.html.ini
testing/web-platform/meta/streams/readable-streams/bad-strategies.serviceworker.https.html.ini
testing/web-platform/meta/streams/readable-streams/bad-strategies.sharedworker.html.ini
testing/web-platform/meta/streams/readable-streams/bad-underlying-sources.dedicatedworker.html.ini
testing/web-platform/meta/streams/readable-streams/bad-underlying-sources.html.ini
testing/web-platform/meta/streams/readable-streams/bad-underlying-sources.serviceworker.https.html.ini
testing/web-platform/meta/streams/readable-streams/bad-underlying-sources.sharedworker.html.ini
testing/web-platform/meta/streams/readable-streams/cancel.dedicatedworker.html.ini
testing/web-platform/meta/streams/readable-streams/cancel.html.ini
testing/web-platform/meta/streams/readable-streams/cancel.serviceworker.https.html.ini
testing/web-platform/meta/streams/readable-streams/cancel.sharedworker.html.ini
testing/web-platform/meta/streams/readable-streams/count-queuing-strategy-integration.dedicatedworker.html.ini
testing/web-platform/meta/streams/readable-streams/count-queuing-strategy-integration.html.ini
testing/web-platform/meta/streams/readable-streams/count-queuing-strategy-integration.serviceworker.https.html.ini
testing/web-platform/meta/streams/readable-streams/count-queuing-strategy-integration.sharedworker.html.ini
testing/web-platform/meta/streams/readable-streams/default-reader.dedicatedworker.html.ini
testing/web-platform/meta/streams/readable-streams/default-reader.html.ini
testing/web-platform/meta/streams/readable-streams/default-reader.serviceworker.https.html.ini
testing/web-platform/meta/streams/readable-streams/default-reader.sharedworker.html.ini
testing/web-platform/meta/streams/readable-streams/floating-point-total-queue-size.dedicatedworker.html.ini
testing/web-platform/meta/streams/readable-streams/floating-point-total-queue-size.html.ini
testing/web-platform/meta/streams/readable-streams/floating-point-total-queue-size.serviceworker.https.html.ini
testing/web-platform/meta/streams/readable-streams/floating-point-total-queue-size.sharedworker.html.ini
testing/web-platform/meta/streams/readable-streams/general.dedicatedworker.html.ini
testing/web-platform/meta/streams/readable-streams/general.html.ini
testing/web-platform/meta/streams/readable-streams/general.serviceworker.https.html.ini
testing/web-platform/meta/streams/readable-streams/general.sharedworker.html.ini
testing/web-platform/meta/streams/readable-streams/tee.dedicatedworker.html.ini
testing/web-platform/meta/streams/readable-streams/tee.html.ini
testing/web-platform/meta/streams/readable-streams/tee.serviceworker.https.html.ini
testing/web-platform/meta/streams/readable-streams/tee.sharedworker.html.ini
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -1052,21 +1052,17 @@ pref("dom.ipc.plugins.sandbox-level.flas
 #endif
 
 #if defined(MOZ_CONTENT_SANDBOX)
 // This controls the strength of the Windows content process sandbox for testing
 // purposes. This will require a restart.
 // On windows these levels are:
 // See - security/sandbox/win/src/sandboxbroker/sandboxBroker.cpp
 // SetSecurityLevelForContentProcess() for what the different settings mean.
-#if defined(NIGHTLY_BUILD)
 pref("security.sandbox.content.level", 3);
-#else
-pref("security.sandbox.content.level", 1);
-#endif
 
 // This controls the depth of stack trace that is logged when Windows sandbox
 // logging is turned on.  This is only currently available for the content
 // process because the only other sandbox (for GMP) has too strict a policy to
 // allow stack tracing.  This does not require a restart to take effect.
 pref("security.sandbox.windows.log.stackTraceDepth", 0);
 #endif
 
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -8712,17 +8712,17 @@ var ToolbarIconColor = {
         break;
     }
   },
 
   // a cache of luminance values for each toolbar
   // to avoid unnecessary calls to getComputedStyle
   _toolbarLuminanceCache: new Map(),
 
-  inferFromText(reason, reasonValue) {
+  async inferFromText(reason, reasonValue) {
     if (!this._initialized)
       return;
     function parseRGB(aColorString) {
       let rgb = aColorString.match(/^rgba?\((\d+), (\d+), (\d+)/);
       rgb.shift();
       return rgb.map(x => parseInt(x));
     }
 
@@ -8749,30 +8749,32 @@ var ToolbarIconColor = {
     let toolbarSelector = "#navigator-toolbox > toolbar:not([collapsed=true]):not(#addon-bar)";
     if (AppConstants.platform == "macosx")
       toolbarSelector += ":not([type=menubar])";
 
     // The getComputedStyle calls and setting the brighttext are separated in
     // two loops to avoid flushing layout and making it dirty repeatedly.
     let cachedLuminances = this._toolbarLuminanceCache;
     let luminances = new Map();
-    for (let toolbar of document.querySelectorAll(toolbarSelector)) {
-      // toolbars *should* all have ids, but guard anyway to avoid blowing up
-      let cacheKey = toolbar.id && toolbar.id + JSON.stringify(this._windowState);
-      // lookup cached luminance value for this toolbar in this window state
-      let luminance = cacheKey && cachedLuminances.get(cacheKey);
-      if (isNaN(luminance)) {
-        let [r, g, b] = parseRGB(getComputedStyle(toolbar).color);
-        luminance = 0.2125 * r + 0.7154 * g + 0.0721 * b;
-        if (cacheKey) {
-          cachedLuminances.set(cacheKey, luminance);
+    await BrowserUtils.promiseLayoutFlushed(document, "style", () => {
+      for (let toolbar of document.querySelectorAll(toolbarSelector)) {
+        // toolbars *should* all have ids, but guard anyway to avoid blowing up
+        let cacheKey = toolbar.id && toolbar.id + JSON.stringify(this._windowState);
+        // lookup cached luminance value for this toolbar in this window state
+        let luminance = cacheKey && cachedLuminances.get(cacheKey);
+        if (isNaN(luminance)) {
+          let [r, g, b] = parseRGB(getComputedStyle(toolbar).color);
+          luminance = 0.2125 * r + 0.7154 * g + 0.0721 * b;
+          if (cacheKey) {
+            cachedLuminances.set(cacheKey, luminance);
+          }
         }
+        luminances.set(toolbar, luminance);
       }
-      luminances.set(toolbar, luminance);
-    }
+    });
 
     for (let [toolbar, luminance] of luminances) {
       if (luminance <= 110)
         toolbar.removeAttribute("brighttext");
       else
         toolbar.setAttribute("brighttext", "true");
     }
   }
--- a/browser/base/content/tab-content.js
+++ b/browser/base/content/tab-content.js
@@ -906,16 +906,17 @@ var RefreshBlocker = {
       }
     }
   },
 
   enable() {
     this._filter = Cc["@mozilla.org/appshell/component/browser-status-filter;1"]
                      .createInstance(Ci.nsIWebProgress);
     this._filter.addProgressListener(this, Ci.nsIWebProgress.NOTIFY_ALL);
+    this._filter.target = tabEventTarget;
 
     let webProgress = docShell.QueryInterface(Ci.nsIInterfaceRequestor)
                               .getInterface(Ci.nsIWebProgress);
     webProgress.addProgressListener(this._filter, Ci.nsIWebProgress.NOTIFY_ALL);
 
     addMessageListener("RefreshBlocker:Refresh", this);
   },
 
--- a/browser/base/content/test/performance/browser_windowopen_reflows.js
+++ b/browser/base/content/test/performance/browser_windowopen_reflows.js
@@ -27,29 +27,33 @@ if (Services.appinfo.OS == "Linux") {
         "handleEvent@chrome://browser/content/tabbrowser.xml",
         "EventListener.handleEvent*tabbrowser-tabs_XBL_Constructor@chrome://browser/content/tabbrowser.xml",
       ],
     );
   } else {
     EXPECTED_REFLOWS.push(
       [
         "handleEvent@chrome://browser/content/tabbrowser.xml",
-        "inferFromText@chrome://browser/content/browser.js",
-        "handleEvent@chrome://browser/content/browser.js",
+        "inferFromText/<@chrome://browser/content/browser.js",
+        "promiseReflowed/</<@resource://gre/modules/BrowserUtils.jsm",
+        "_onReflow@resource://gre/modules/BrowserUtils.jsm",
+        "reflowInterruptible@resource://gre/modules/BrowserUtils.jsm",
       ],
     );
   }
 }
 
 if (Services.appinfo.OS == "Darwin") {
   EXPECTED_REFLOWS.push(
     [
       "handleEvent@chrome://browser/content/tabbrowser.xml",
-      "inferFromText@chrome://browser/content/browser.js",
-      "handleEvent@chrome://browser/content/browser.js",
+      "inferFromText/<@chrome://browser/content/browser.js",
+      "promiseReflowed/</<@resource://gre/modules/BrowserUtils.jsm",
+      "_onReflow@resource://gre/modules/BrowserUtils.jsm",
+      "reflowInterruptible@resource://gre/modules/BrowserUtils.jsm",
     ],
   );
 }
 
 if (Services.appinfo.OS == "WINNT") {
   EXPECTED_REFLOWS.push(
     [
       "verticalMargins@chrome://browser/content/browser-tabsintitlebar.js",
@@ -62,18 +66,20 @@ if (Services.appinfo.OS == "WINNT") {
       "verticalMargins@chrome://browser/content/browser-tabsintitlebar.js",
       "_update@chrome://browser/content/browser-tabsintitlebar.js",
       "updateAppearance@chrome://browser/content/browser-tabsintitlebar.js",
       "handleEvent@chrome://browser/content/tabbrowser.xml",
     ],
 
     [
       "handleEvent@chrome://browser/content/tabbrowser.xml",
-      "inferFromText@chrome://browser/content/browser.js",
-      "handleEvent@chrome://browser/content/browser.js",
+      "inferFromText/<@chrome://browser/content/browser.js",
+      "promiseReflowed/</<@resource://gre/modules/BrowserUtils.jsm",
+      "_onReflow@resource://gre/modules/BrowserUtils.jsm",
+      "reflowInterruptible@resource://gre/modules/BrowserUtils.jsm",
     ],
 
     [
       "handleEvent@chrome://browser/content/tabbrowser.xml",
       "EventListener.handleEvent*tabbrowser-tabs_XBL_Constructor@chrome://browser/content/tabbrowser.xml",
     ],
   );
 }
--- a/browser/components/extensions/test/browser/browser-common.ini
+++ b/browser/components/extensions/test/browser/browser-common.ini
@@ -17,16 +17,17 @@ support-files =
   file_bypass_cache.sjs
   file_language_fr_en.html
   file_language_ja.html
   file_language_tlh.html
   file_dummy.html
   file_title.html
   file_inspectedwindow_reload_target.sjs
   file_serviceWorker.html
+  locale/chrome.manifest
   webNav_createdTarget.html
   webNav_createdTargetSource.html
   webNav_createdTargetSource_subframe.html
   serviceWorker.js
   searchSuggestionEngine.xml
   searchSuggestionEngine.sjs
   ../../../../../toolkit/components/extensions/test/mochitest/head_webrequest.js
 
--- a/browser/components/extensions/test/browser/head_pageAction.js
+++ b/browser/components/extensions/test/browser/head_pageAction.js
@@ -1,16 +1,28 @@
 /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set sts=2 sw=2 et tw=80: */
 "use strict";
 
 /* exported runTests */
 // This file is imported into the same scope as head.js.
 /* import-globals-from head.js */
 
+{
+  const chromeRegistry = Cc["@mozilla.org/chrome/chrome-registry;1"].getService(Ci.nsIChromeRegistry);
+
+  let localeDir = new URL("locale/", gTestPath).href;
+  let {file} = chromeRegistry.convertChromeURL(Services.io.newURI(localeDir)).QueryInterface(Ci.nsIFileURL);
+
+  Components.manager.addBootstrappedManifestLocation(file);
+  registerCleanupFunction(() => {
+    Components.manager.removeBootstrappedManifestLocation(file);
+  });
+}
+
 async function runTests(options) {
   function background(getTests) {
     let tabs;
     let tests;
 
     // Gets the current details of the page action, and returns a
     // promise that resolves to an object containing them.
     async function getDetails() {
new file mode 100644
--- /dev/null
+++ b/browser/components/extensions/test/browser/locale/chrome.manifest
@@ -0,0 +1,1 @@
+locale global es-ES resource://gre/chrome/en-US/locale/en-US/global/
--- a/browser/modules/LaterRun.jsm
+++ b/browser/modules/LaterRun.jsm
@@ -3,17 +3,16 @@
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
 
 this.EXPORTED_SYMBOLS = ["LaterRun"];
 
-Cu.import("resource://gre/modules/Preferences.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 
 const kEnabledPref = "browser.laterrun.enabled";
 const kPagePrefRoot = "browser.laterrun.pages.";
 // Number of sessions we've been active in
 const kSessionCountPref = "browser.laterrun.bookkeeping.sessionCount";
 // Time the profile was created at:
 const kProfileCreationTime = "browser.laterrun.bookkeeping.profileCreationTime";
@@ -28,17 +27,17 @@ class Page {
     this.pref = pref;
     this.minimumHoursSinceInstall = minimumHoursSinceInstall || 0;
     this.minimumSessionCount = minimumSessionCount || 1;
     this.requireBoth = requireBoth || false;
     this.url = url;
   }
 
   get hasRun() {
-    return Preferences.get(this.pref + "hasRun", false);
+    return Services.prefs.getBoolPref(this.pref + "hasRun", false);
   }
 
   applies(sessionInfo) {
     if (this.hasRun) {
       return false;
     }
     if (this.requireBoth) {
       return sessionInfo.sessionCount >= this.minimumSessionCount &&
@@ -50,83 +49,83 @@ class Page {
 }
 
 let LaterRun = {
   init() {
     if (!this.enabled) {
       return;
     }
     // If this is the first run, set the time we were installed
-    if (!Preferences.has(kProfileCreationTime)) {
+    if (Services.prefs.getPrefType(kProfileCreationTime) == Ci.nsIPrefBranch.PREF_INVALID) {
       // We need to store seconds in order to fit within int prefs.
-      Preferences.set(kProfileCreationTime, Math.floor(Date.now() / 1000));
+      Services.prefs.setIntPref(kProfileCreationTime, Math.floor(Date.now() / 1000));
     }
     this.sessionCount++;
 
     if (this.hoursSinceInstall > kSelfDestructHoursLimit ||
         this.sessionCount > kSelfDestructSessionLimit) {
       this.selfDestruct();
     }
   },
 
   // The enabled, hoursSinceInstall and sessionCount properties mirror the
   // preferences system, and are here for convenience.
   get enabled() {
-    return Preferences.get(kEnabledPref, false);
+    return Services.prefs.getBoolPref(kEnabledPref, false);
   },
 
   set enabled(val) {
     let wasEnabled = this.enabled;
-    Preferences.set(kEnabledPref, val);
+    Services.prefs.setBoolPref(kEnabledPref, val);
     if (val && !wasEnabled) {
       this.init();
     }
   },
 
   get hoursSinceInstall() {
-    let installStamp = Preferences.get(kProfileCreationTime, Date.now() / 1000);
+    let installStamp = Services.prefs.getIntPref(kProfileCreationTime, Date.now() / 1000);
     return Math.floor((Date.now() / 1000 - installStamp) / 3600);
   },
 
   get sessionCount() {
     if (this._sessionCount) {
       return this._sessionCount;
     }
-    return this._sessionCount = Preferences.get(kSessionCountPref, 0);
+    return this._sessionCount = Services.prefs.getIntPref(kSessionCountPref, 0);
   },
 
   set sessionCount(val) {
     this._sessionCount = val;
-    Preferences.set(kSessionCountPref, val);
+    Services.prefs.setIntPref(kSessionCountPref, val);
   },
 
   // Because we don't want to keep incrementing this indefinitely for no reason,
   // we will turn ourselves off after a set amount of time/sessions (see top of
   // file).
   selfDestruct() {
-    Preferences.set(kEnabledPref, false);
+    Services.prefs.setBoolPref(kEnabledPref, false);
   },
 
   // Create an array of Page objects based on the currently set prefs
   readPages() {
     // Enumerate all the pages.
     let allPrefsForPages = Services.prefs.getChildList(kPagePrefRoot);
     let pageDataStore = new Map();
     for (let pref of allPrefsForPages) {
       let [slug, prop] = pref.substring(kPagePrefRoot.length).split(".");
       if (!pageDataStore.has(slug)) {
         pageDataStore.set(slug, {pref: pref.substring(0, pref.length - prop.length)});
       }
-      let defaultPrefValue = 0;
       if (prop == "requireBoth" || prop == "hasRun") {
-        defaultPrefValue = false;
+        pageDataStore.get(slug)[prop] = Services.prefs.getBoolPref(pref, false);
       } else if (prop == "url") {
-        defaultPrefValue = "";
+        pageDataStore.get(slug)[prop] = Services.prefs.getStringPref(pref, "");
+      } else {
+        pageDataStore.get(slug)[prop] = Services.prefs.getIntPref(pref, 0);
       }
-      pageDataStore.get(slug)[prop] = Preferences.get(pref, defaultPrefValue);
     }
     let rv = [];
     for (let [, pageData] of pageDataStore) {
       if (pageData.url) {
         let uri = null;
         try {
           let urlString = Services.urlFormatter.formatURL(pageData.url.trim());
           uri = Services.io.newURI(urlString);
--- a/devtools/client/debugger/new/debugger.css
+++ b/devtools/client/debugger/new/debugger.css
@@ -354,21 +354,23 @@ body {
 }
 
 :root.theme-dark .CodeMirror-scrollbar-filler {
   background: transparent;
 }
 :root.theme-light,
 :root .theme-light {
   --search-overlays-semitransparent: rgba(221, 225, 228, 0.66);
+  --popup-shadow-color: #d0d0d0;
 }
 
 :root.theme-dark,
 :root .theme-dark {
   --search-overlays-semitransparent: rgba(42, 46, 56, 0.66);
+  --popup-shadow-color: #5c667b;
 }
 .debugger {
   display: flex;
   flex: 1;
   height: 100%;
 }
 
 .editor-pane {
@@ -399,16 +401,17 @@ body {
   background-color: var(--search-overlays-semitransparent);
 }
 
 .search-container .close-button {
   width: 16px;
   margin-top: 25px;
   margin-right: 20px;
 }
+
 menupopup {
   position: fixed;
   z-index: 10000;
   background: white;
   border: 1px solid #cccccc;
   padding: 5px 0;
   background: #f2f2f2;
   border-radius: 5px;
@@ -1952,16 +1955,21 @@ html[dir="rtl"] .arrow svg,
   width: 350px;
   min-height: 80px;
   border: 1px solid var(--theme-splitter-color);
   padding: 10px;
   height: auto;
   min-height: inherit;
   max-height: 200px;
   overflow: auto;
+  box-shadow: 1px 2px 3px var(--popup-shadow-color);
+}
+
+.theme-dark .popover .preview {
+  box-shadow: 1px 2px 3px var(--popup-shadow-color);
 }
 
 .popover .preview .header {
   width: 100%;
   line-height: 20px;
   border-bottom: 1px solid #cccccc;
   display: flex;
   flex-direction: column;
@@ -2390,17 +2398,17 @@ html[dir="rtl"] .editor-mount {
 .breakpoints-list * {
   -moz-user-select: none;
   user-select: none;
 }
 
 .breakpoints-list .breakpoint {
   font-size: 12px;
   color: var(--theme-content-color1);
-  padding: 0.5em 12px 0.5em 5px;
+  padding: 0.5em 1em 0.5em 0.5em;
   line-height: 1em;
   position: relative;
   transition: all 0.25s ease;
 }
 
 html[dir="rtl"] .breakpoints-list .breakpoint {
   border-right: 4px solid transparent;
 }
@@ -2505,48 +2513,57 @@ html .breakpoints-list .breakpoint.pause
   opacity: 1;
 }
 
 .input-expression:focus {
   outline: none;
   cursor: text;
 }
 
+.expressions-list {
+  /* TODO: add normalize */
+  margin: 0;
+  padding: 0.5em 0;
+}
 .expression-input-container {
   padding: 0.5em;
   display: flex;
 }
 
 .expression-container {
   border: 1px;
-  padding: 8px 5px 0px 0px;
+  padding: 0.25em 1em 0.25em 0.5em;
   width: 100%;
   color: var(--theme-body-color);
   background-color: var(--theme-body-background);
-  display: flex;
+  display: block;
   position: relative;
 }
 
 .expression-container > .tree {
   width: 100%;
   overflow: hidden;
 }
 
 :root.theme-light .expression-container:hover {
   background-color: var(--theme-tab-toolbar-background);
 }
 
 :root.theme-dark .expression-container:hover {
   background-color: var(--search-overlays-semitransparent);
 }
 
-.expression-container .close-btn {
+.expression-container__close-btn {
   position: absolute;
-  offset-inline-end: 6px;
-  top: 6px;
+  offset-inline-end: 0px;
+  top: 4px;
+}
+
+.expression-content {
+  position: relative;
 }
 
 .expression-container .close-btn {
   display: none;
 }
 
 .expression-container:hover .close-btn {
   display: block;
@@ -2951,16 +2968,27 @@ html .command-bar > button:disabled {
 }
 .secondary-panes {
   display: flex;
   flex-direction: column;
   flex: 1;
   white-space: nowrap;
 }
 
+/*
+  We apply overflow to the container with the commandbar.
+  This allows the commandbar to remain fixed when scrolling
+  until the content completely ends. Not just the height of
+  the wrapper.
+  Ref: https://github.com/devtools-html/debugger.html/issues/3426
+*/
+.secondary-panes--sticky-commandbar {
+  overflow-y: scroll;
+}
+
 .secondary-panes .accordion {
   flex: 1 0 auto;
 }
 
 .pane {
   color: var(--theme-body-color);
 }
 
@@ -3192,20 +3220,24 @@ html[dir="rtl"] .dropdown {
 
 .symbol-modal {
   position: absolute;
   left: calc(50% - 250px);
   z-index: 10;
   width: 500px;
   height: 230px;
   background-color: var(--theme-codemirror-gutter-background);
-  box-shadow: 2px 4px 6px #dde1e4;
+  box-shadow: 2px 4px 6px var(--popup-shadow-color);
   top: 30px;
 }
 
+.theme-dark .symbol-modal {
+  box-shadow: 2px 4px 6px var(--popup-shadow-color);
+}
+
 .symbol-modal .input-wrapper {
   display: -webkit-box;
   display: -ms-flexbox;
   display: flex;
   -webkit-box-pack: center;
   -ms-flex-pack: center;
   justify-content: center;
 }
--- a/devtools/client/debugger/new/debugger.js
+++ b/devtools/client/debugger/new/debugger.js
@@ -19516,22 +19516,22 @@ return /******/ (function(modules) { // 
 	 * @memberof actions/pause
 	 * @static
 	 */
 	function resumed() {
 	  return (_ref) => {
 	    var dispatch = _ref.dispatch,
 	        client = _ref.client;
 
-	    // dispatch(evaluateExpressions(null));
-
-	    return dispatch({
+	    dispatch({
 	      type: "RESUME",
 	      value: undefined
 	    });
+
+	    dispatch((0, _expressions.evaluateExpressions)(null));
 	  };
 	}
 
 	/**
 	 * Debugger has just paused
 	 *
 	 * @param {object} pauseInfo
 	 * @memberof actions/pause
@@ -22480,37 +22480,28 @@ return /******/ (function(modules) { // 
 /* 391 */
 /***/ function(module, exports, __webpack_require__) {
 
 	"use strict";
 
 	Object.defineProperty(exports, "__esModule", {
 	  value: true
 	});
-	exports.isExactUrlMatch = exports.getURL = exports.getDirectories = exports.createTree = exports.collapseTree = exports.addToTree = exports.isDirectory = exports.createParentMap = exports.nodeHasChildren = exports.createNode = undefined;
+	exports.formatTree = exports.isExactUrlMatch = exports.getURL = exports.getDirectories = exports.createTree = exports.collapseTree = exports.addToTree = exports.isDirectory = exports.createParentMap = exports.nodeHasChildren = exports.createNode = undefined;
 
 	var _url = __webpack_require__(334);
 
-	var _DevToolsUtils = __webpack_require__(222);
-
-	var _DevToolsUtils2 = _interopRequireDefault(_DevToolsUtils);
-
 	var _source = __webpack_require__(233);
 
 	var _merge = __webpack_require__(392);
 
 	var _merge2 = _interopRequireDefault(_merge);
 
 	function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
 
-	/**
-	 * Utils for Sources Tree Component
-	 * @module utils/sources-tree
-	 */
-
 	var IGNORED_URLS = ["debugger eval code", "XStringBundle"];
 
 	/**
 	 * Temporary Source type to be used only within this module
 	 * TODO: Replace with real Source type definition when refactoring types
 	 * @memberof utils/sources-tree
 	 * @static
 	 */
@@ -22519,16 +22510,21 @@ return /******/ (function(modules) { // 
 	/**
 	 * TODO: createNode is exported so this type could be useful to other modules
 	 * @memberof utils/sources-tree
 	 * @static
 	 */
 
 
 	/**
+	 * Utils for Sources Tree Component
+	 * @module utils/sources-tree
+	 */
+
+	/**
 	 * @memberof utils/sources-tree
 	 * @static
 	 */
 	function nodeHasChildren(item) {
 	  return Array.isArray(item.contents);
 	}
 
 	/**
@@ -22686,17 +22682,23 @@ return /******/ (function(modules) { // 
 	    var isLastPart = i === parts.length - 1;
 
 	    // Currently we assume that we are descending into a node with
 	    // children. This will fail if a path has a directory named the
 	    // same as another file, like `foo/bar.js/file.js`.
 	    //
 	    // TODO: Be smarter about this, which we'll probably do when we
 	    // are smarter about folders and collapsing empty ones.
-	    (0, _DevToolsUtils2.default)(nodeHasChildren(subtree), `${subtree.name} should have children`);
+
+	    if (!nodeHasChildren(subtree)) {
+	      return {
+	        v: void 0
+	      };
+	    }
+
 	    var children = subtree.contents;
 
 	    var index = determineFileSortOrder(children, part, isLastPart, i === 0 ? debuggeeUrl : "");
 
 	    var child = children.find(c => c.name === part);
 	    if (child) {
 	      // A node with the same name already exists, simply traverse
 	      // into it.
@@ -22710,17 +22712,19 @@ return /******/ (function(modules) { // 
 	      subtree = children[where];
 	    }
 
 	    // Keep track of the children so we can tag each node with them.
 	    path = `${path}/${part}`;
 	  };
 
 	  for (var i = 0; i < parts.length; i++) {
-	    _loop(i);
+	    var _ret = _loop(i);
+
+	    if (typeof _ret === "object") return _ret.v;
 	  }
 
 	  // Overwrite the contents of the final node to store the source
 	  // there.
 	  if (!isDir) {
 	    subtree.contents = source;
 	  } else if (!subtree.contents.find(c => c.name === "(index)")) {
 	    subtree.contents.unshift(createNode("(index)", source.get("url"), source));
@@ -22866,26 +22870,50 @@ return /******/ (function(modules) { // 
 	    node = parentMap.get(node);
 	    if (!node) {
 	      return directories;
 	    }
 	    directories.push(node);
 	  }
 	}
 
+	function formatTree(tree) {
+	  var depth = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0;
+	  var str = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : "";
+
+	  var whitespace = new Array(depth * 2).join(" ");
+
+	  if (!tree.contents) {
+	    return str;
+	  }
+
+	  if (tree.contents.length > 0) {
+	    str += `${whitespace} - ${tree.name} path=${tree.path} \n`;
+	    tree.contents.forEach(t => {
+	      str = formatTree(t, depth + 1, str);
+	    });
+	  } else if (tree.contents.toJS) {
+	    var id = tree.contents.get("id");
+	    str += `${whitespace} - ${tree.name} path=${tree.path} source_id=${id} \n`;
+	  }
+
+	  return str;
+	}
+
 	exports.createNode = createNode;
 	exports.nodeHasChildren = nodeHasChildren;
 	exports.createParentMap = createParentMap;
 	exports.isDirectory = isDirectory;
 	exports.addToTree = addToTree;
 	exports.collapseTree = collapseTree;
 	exports.createTree = createTree;
 	exports.getDirectories = getDirectories;
 	exports.getURL = getURL;
 	exports.isExactUrlMatch = isExactUrlMatch;
+	exports.formatTree = formatTree;
 
 /***/ },
 /* 392 */
 /***/ function(module, exports, __webpack_require__) {
 
 	var baseMerge = __webpack_require__(393),
 	    createAssigner = __webpack_require__(410);
 
@@ -24249,17 +24277,17 @@ return /******/ (function(modules) { // 
 
 	    shortcuts.on("CmdOrCtrl+B", this.onToggleBreakpoint);
 	    shortcuts.on("CmdOrCtrl+Shift+B", this.onToggleBreakpoint);
 	    shortcuts.on("Esc", this.onEscape);
 	    shortcuts.on(searchAgainPrevKey, this.onSearchAgain);
 	    shortcuts.on(searchAgainKey, this.onSearchAgain);
 
 	    if (selectedLocation && !!selectedLocation.line) {
-	      this.pendingJumpLocation = selectedLocation;
+	      this.pendingJumpLine = selectedLocation.line;
 	    }
 
 	    (0, _editor.updateDocument)(editor, selectedSource);
 	  }
 
 	  componentWillUnmount() {
 	    this.state.editor.destroy();
 	    this.setState({ editor: null });
@@ -24478,17 +24506,18 @@ return /******/ (function(modules) { // 
 	    var condition = breakpoint ? breakpoint.condition : "";
 
 	    var panel = (0, _ConditionalPanel.renderConditionalPanel)({
 	      condition,
 	      setBreakpoint: value => setBreakpointCondition(location, { condition: value }),
 	      closePanel: this.closeConditionalPanel
 	    });
 
-	    this.cbPanel = this.state.editor.codeMirror.addLineWidget(line, panel, {
+	    var editorLine = line - 1;
+	    this.cbPanel = this.state.editor.codeMirror.addLineWidget(editorLine, panel, {
 	      coverGutter: true,
 	      noHScroll: false
 	    });
 	    this.cbPanel.node.querySelector("input").focus();
 	  }
 
 	  closeConditionalPanel() {
 	    this.cbPanel.clear();
@@ -28092,16 +28121,17 @@ return /******/ (function(modules) { // 
 	      type: "checkbox",
 	      "aria-label": breakpointsDisabled ? L10N.getStr("breakpoints.enable") : L10N.getStr("breakpoints.disable"),
 	      className: boxClassName,
 	      disabled: breakpointsLoading,
 	      onChange: e => {
 	        e.stopPropagation();
 	        toggleAllBreakpoints(!breakpointsDisabled);
 	      },
+	      onClick: e => e.stopPropagation(),
 	      checked: !breakpointsDisabled && !isIndeterminate,
 	      ref: input => {
 	        if (input) {
 	          input.indeterminate = isIndeterminate;
 	        }
 	      },
 	      title: breakpointsDisabled ? L10N.getStr("breakpoints.enable") : L10N.getStr("breakpoints.disable")
 	    });
@@ -28203,17 +28233,17 @@ return /******/ (function(modules) { // 
 	      splitterSize: 1,
 	      startPanel: Accordion({ items: this.getStartItems() }),
 	      endPanel: Accordion({ items: this.getEndItems() })
 	    });
 	  }
 
 	  render() {
 	    return _react.DOM.div({
-	      className: "secondary-panes"
+	      className: "secondary-panes secondary-panes--sticky-commandbar"
 	    }, CommandBar(), this.props.horizontal ? this.renderHorizontalLayout() : this.renderVerticalLayout());
 	  }
 	}
 
 	SecondaryPanes.propTypes = {
 	  evaluateExpressions: _react.PropTypes.func.isRequired,
 	  pauseData: _react.PropTypes.object,
 	  horizontal: _react.PropTypes.bool,
@@ -28414,26 +28444,28 @@ return /******/ (function(modules) { // 
 	    }
 
 	    var root = {
 	      name: expression.input,
 	      path,
 	      contents: { value }
 	    };
 
-	    return _react.DOM.div({
+	    return _react.DOM.li({
 	      className: "expression-container",
 	      key: `${path}/${input}`
-	    }, ObjectInspector({
+	    }, _react.DOM.div({ className: "expression-content" }, ObjectInspector({
 	      roots: [root],
 	      getObjectProperties: id => loadedObjects[id],
 	      autoExpandDepth: 0,
 	      onDoubleClick: (item, options) => this.editExpression(expression, options),
 	      loadObjectProperties
-	    }), CloseButton({ handleClick: e => this.deleteExpression(e, expression) }));
+	    }), _react.DOM.div({ className: "expression-container__close-btn" }, CloseButton({
+	      handleClick: e => this.deleteExpression(e, expression)
+	    }))));
 	  }
 
 	  componentDidUpdate() {
 	    if (this._input) {
 	      this._input.focus();
 	    }
 	  }
 
@@ -28447,31 +28479,31 @@ return /******/ (function(modules) { // 
 	      if (value == "") {
 	        return;
 	      }
 
 	      e.stopPropagation();
 	      e.target.value = "";
 	      this.props.addExpression(value);
 	    };
-	    return _react.DOM.span({ className: "expression-input-container" }, _react.DOM.input({
+	    return _react.DOM.li({ className: "expression-input-container" }, _react.DOM.input({
 	      type: "text",
 	      className: "input-expression",
 	      placeholder: L10N.getStr("expressions.placeholder"),
 	      onBlur: e => {
 	        e.target.value = "";
 	      },
 	      onKeyPress
 	    }));
 	  }
 
 	  render() {
 	    var expressions = this.props.expressions;
 
-	    return _react.DOM.span({ className: "pane expressions-list" }, expressions.map(this.renderExpression), this.renderNewExpressionInput());
+	    return _react.DOM.ul({ className: "pane expressions-list" }, expressions.map(this.renderExpression), this.renderNewExpressionInput());
 	  }
 	}
 
 	Expressions.displayName = "Expressions";
 
 	exports.default = (0, _reactRedux.connect)(state => ({
 	  pauseInfo: (0, _selectors.getPause)(state),
 	  expressions: (0, _selectors.getVisibleExpressions)(state),
@@ -45926,19 +45958,17 @@ return /******/ (function(modules) { // 
 
 	"use strict";
 
 	/* 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/. */
 
 	function forEachLine(codeMirror, iter) {
-	  codeMirror.operation(() => {
-	    codeMirror.doc.iter(0, codeMirror.lineCount(), iter);
-	  });
+	  codeMirror.doc.iter(0, codeMirror.lineCount(), iter);
 	}
 
 	function removeLineClass(codeMirror, line, className) {
 	  codeMirror.removeLineClass(line, "line", className);
 	}
 
 	function clearLineClass(codeMirror, className) {
 	  forEachLine(codeMirror, line => {
@@ -45949,33 +45979,16 @@ return /******/ (function(modules) { // 
 	function getTextForLine(codeMirror, line) {
 	  return codeMirror.getLine(line - 1).trim();
 	}
 
 	function getCursorLine(codeMirror) {
 	  return codeMirror.getCursor().line;
 	}
 
-	function getTokenLocation(codeMirror, tokenEl) {
-	  var lineOffset = 1;
-
-	  var _tokenEl$getBoundingC = tokenEl.getBoundingClientRect(),
-	      left = _tokenEl$getBoundingC.left,
-	      top = _tokenEl$getBoundingC.top;
-
-	  var _codeMirror$coordsCha = codeMirror.coordsChar({ left, top }),
-	      line = _codeMirror$coordsCha.line,
-	      ch = _codeMirror$coordsCha.ch;
-
-	  return {
-	    line: line + lineOffset,
-	    column: ch
-	  };
-	}
-
 	/**
 	 * Forces the breakpoint gutter to be the same size as the line
 	 * numbers gutter. Editor CSS will absolutely position the gutter
 	 * beneath the line numbers. This makes it easy to be flexible with
 	 * how we overlay breakpoints.
 	 */
 	function resizeBreakpointGutter(editor) {
 	  var gutters = editor.display.gutters;
@@ -45984,17 +45997,16 @@ return /******/ (function(modules) { // 
 	  breakpoints.style.width = `${lineNumbers.clientWidth}px`;
 	}
 
 	module.exports = {
 	  removeLineClass,
 	  clearLineClass,
 	  getTextForLine,
 	  getCursorLine,
-	  getTokenLocation,
 	  resizeBreakpointGutter
 	};
 
 /***/ },
 /* 997 */
 /***/ function(module, exports) {
 
 	module.exports = "<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" version=\"1.1\" viewBox=\"0 0 32 32\"><script></script><path fill=\"#444444\" d=\"M16 9.875l-9.539-5.438v23.698l9.539-5.438 9.539 5.438v-23.698l-9.539 5.438zM11.248 16.286l4.752-2.709 4.752 2.709-4.752 2.709-4.752-2.709zM9.618 9.643l3.399 1.938-3.399 1.938v-3.876zM9.618 19.053l3.145 1.792-3.145 1.793v-3.585zM22.382 22.638l-3.145-1.793 3.145-1.793v3.585zM18.982 11.581l3.399-1.938v3.876l-3.399-1.938z\"></path></svg>"
--- a/devtools/client/debugger/new/search-worker.js
+++ b/devtools/client/debugger/new/search-worker.js
@@ -778,29 +778,36 @@ return /******/ (function(modules) { // 
 	exports.default = getMatches;
 
 	var _buildQuery = __webpack_require__(1138);
 
 	var _buildQuery2 = _interopRequireDefault(_buildQuery);
 
 	function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
 
+	var MAX_LENGTH = 100000;
+
 	function getMatches(query, text, modifiers) {
 	  if (!query || !text || !modifiers) {
 	    return [];
 	  }
 	  var regexQuery = (0, _buildQuery2.default)(query, modifiers, {
 	    isGlobal: true
 	  });
 	  var matchedLocations = [];
 	  var lines = text.split("\n");
 	  for (var i = 0; i < lines.length; i++) {
 	    var singleMatch = void 0;
-	    while ((singleMatch = regexQuery.exec(lines[i])) !== null) {
-	      matchedLocations.push({ line: i, ch: singleMatch.index });
+	    var line = lines[i];
+	    if (line.length <= MAX_LENGTH) {
+	      while ((singleMatch = regexQuery.exec(line)) !== null) {
+	        matchedLocations.push({ line: i, ch: singleMatch.index });
+	      }
+	    } else {
+	      return [];
 	    }
 	  }
 	  return matchedLocations;
 	}
 
 /***/ }
 
 /******/ })
--- a/devtools/client/debugger/new/test/mochitest/browser_dbg-breakpoints-cond.js
+++ b/devtools/client/debugger/new/test/mochitest/browser_dbg-breakpoints-cond.js
@@ -2,16 +2,33 @@
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 function findBreakpoint(dbg, url, line) {
   const { selectors: { getBreakpoint }, getState } = dbg;
   const source = findSource(dbg, url);
   return getBreakpoint(getState(), { sourceId: source.id, line });
 }
 
+function getLineEl(dbg, line) {
+  const lines = dbg.win.document.querySelectorAll(".CodeMirror-code > div");
+  return lines[line - 1];
+}
+
+function assertEditorBreakpoint(dbg, line, shouldExist) {
+  const exists = getLineEl(dbg, line).classList.contains("has-condition");
+
+  ok(
+    exists === shouldExist,
+    "Breakpoint " +
+      (shouldExist ? "exists" : "does not exist") +
+      " on line " +
+      line
+  );
+}
+
 function setConditionalBreakpoint(dbg, index, condition) {
   return Task.spawn(function*() {
     rightClickElement(dbg, "gutter", index);
     selectMenuItem(dbg, 2);
     yield waitForElement(dbg, ".conditional-breakpoint-panel input");
     findElementWithSelector(dbg, ".conditional-breakpoint-panel input").focus();
     // Position cursor reliably at the end of the text.
     pressKey(dbg, "End");
@@ -24,30 +41,34 @@ add_task(function*() {
   const dbg = yield initDebugger("doc-scripts.html");
   yield selectSource(dbg, "simple2");
 
   // Adding a conditional Breakpoint
   yield setConditionalBreakpoint(dbg, 5, "1");
   yield waitForDispatch(dbg, "ADD_BREAKPOINT");
   let bp = findBreakpoint(dbg, "simple2", 5);
   is(bp.condition, "1", "breakpoint is created with the condition");
+  assertEditorBreakpoint(dbg, 5, true);
 
   // Editing a conditional Breakpoint
   yield setConditionalBreakpoint(dbg, 5, "2");
   yield waitForDispatch(dbg, "SET_BREAKPOINT_CONDITION");
   bp = findBreakpoint(dbg, "simple2", 5);
   is(bp.condition, "12", "breakpoint is created with the condition");
+  assertEditorBreakpoint(dbg, 5, true);
 
   // Removing a conditional breakpoint
   clickElement(dbg, "gutter", 5);
   yield waitForDispatch(dbg, "REMOVE_BREAKPOINT");
   bp = findBreakpoint(dbg, "simple2", 5);
   is(bp, null, "breakpoint was removed");
+  assertEditorBreakpoint(dbg, 5, false);
 
   // Adding a condition to a breakpoint
   clickElement(dbg, "gutter", 5);
   yield waitForDispatch(dbg, "ADD_BREAKPOINT");
   yield setConditionalBreakpoint(dbg, 5, "1");
   yield waitForDispatch(dbg, "SET_BREAKPOINT_CONDITION");
 
   bp = findBreakpoint(dbg, "simple2", 5);
   is(bp.condition, "1", "breakpoint is created with the condition");
+  assertEditorBreakpoint(dbg, 5, true);
 });
--- a/devtools/client/debugger/new/test/mochitest/browser_dbg-breakpoints-reloading.js
+++ b/devtools/client/debugger/new/test/mochitest/browser_dbg-breakpoints-reloading.js
@@ -19,16 +19,18 @@ function addBreakpoint(dbg, line) {
 }
 
 function assertEditorBreakpoint(dbg, line) {
   const exists = !!getLineEl(dbg, line).querySelector(".new-breakpoint");
   ok(exists, `Breakpoint exists on line ${line}`);
 }
 
 add_task(function*() {
+  requestLongerTimeout(2);
+
   const dbg = yield initDebugger("doc-scripts.html");
   const { selectors: { getBreakpoints, getBreakpoint }, getState } = dbg;
   const source = findSource(dbg, "simple1.js");
 
   yield selectSource(dbg, source.url);
   yield addBreakpoint(dbg, 5);
   yield addBreakpoint(dbg, 2);
 
--- a/devtools/client/debugger/new/test/mochitest/browser_dbg-expressions.js
+++ b/devtools/client/debugger/new/test/mochitest/browser_dbg-expressions.js
@@ -15,16 +15,20 @@ const expressionSelectors = {
 function getLabel(dbg, index) {
   return findElement(dbg, "expressionNode", index).innerText;
 }
 
 function getValue(dbg, index) {
   return findElement(dbg, "expressionValue", index).innerText;
 }
 
+function toggleExpression(dbg, index) {
+  findElement(dbg, "expressionNode", index).click();
+}
+
 async function addExpression(dbg, input) {
   info("Adding an expression");
   findElementWithSelector(dbg, expressionSelectors.input).focus();
   type(dbg, input);
   pressKey(dbg, "Enter");
 
   await waitForDispatch(dbg, "EVALUATE_EXPRESSION");
 }
@@ -48,11 +52,20 @@ add_task(function*() {
   yield addExpression(dbg, "f");
   is(getLabel(dbg, 1), "f");
   is(getValue(dbg, 1), "(unavailable)");
 
   yield editExpression(dbg, "oo");
   is(getLabel(dbg, 1), "foo()");
   is(getValue(dbg, 1), "");
 
+  yield addExpression(dbg, "location");
+  is(getLabel(dbg, 2), "location");
+  ok(getValue(dbg, 2).includes("Location"), "has a value");
+
+  // can expand an expression
+  toggleExpression(dbg, 2);
+  yield waitForDispatch(dbg, "LOAD_OBJECT_PROPERTIES");
+
   yield deleteExpression(dbg, "foo");
+  yield deleteExpression(dbg, "location");
   is(findAllElements(dbg, "expressionNodes").length, 0);
 });
--- a/devtools/client/debugger/new/test/mochitest/head.js
+++ b/devtools/client/debugger/new/test/mochitest/head.js
@@ -648,19 +648,19 @@ function isVisibleWithin(outerEl, innerE
   const outerRect = outerEl.getBoundingClientRect();
   return innerRect.top > outerRect.top && innerRect.bottom < outerRect.bottom;
 }
 
 const selectors = {
   callStackHeader: ".call-stack-pane ._header",
   callStackBody: ".call-stack-pane .pane",
   expressionNode: i =>
-    `.expressions-list .tree-node:nth-child(${i}) .object-label`,
+    `.expressions-list .expression-container:nth-child(${i}) .object-label`,
   expressionValue: i =>
-    `.expressions-list .tree-node:nth-child(${i}) .object-value`,
+    `.expressions-list .expression-container:nth-child(${i}) .object-value`,
   expressionClose: i =>
     `.expressions-list .expression-container:nth-child(${i}) .close`,
   expressionNodes: ".expressions-list .tree-node",
   scopesHeader: ".scopes-pane ._header",
   breakpointItem: i => `.breakpoints-list .breakpoint:nth-child(${i})`,
   scopeNode: i => `.scopes-list .tree-node:nth-child(${i}) .object-label`,
   scopeValue: i => `.scopes-list .tree-node:nth-child(${i}) .object-value`,
   frame: i => `.frames ul li:nth-child(${i})`,
--- a/devtools/client/themes/rules.css
+++ b/devtools/client/themes/rules.css
@@ -51,25 +51,20 @@
 
 #ruleview-command-toolbar {
   display: flex;
 }
 
 .ruleview-reveal-panel {
   display: flex;
   overflow: hidden;
-  transition: height 150ms ease;
 }
 
 .ruleview-reveal-panel[hidden] {
-  height: 0px;
-}
-
-#pseudo-class-panel:not([hidden]) {
-  height: 24px;
+  display: none;
 }
 
 .ruleview-reveal-panel label {
   -moz-user-select: none;
   flex-grow: 1;
   display: flex;
   align-items: center;
 }
--- a/docshell/shistory/nsSHEntry.cpp
+++ b/docshell/shistory/nsSHEntry.cpp
@@ -40,16 +40,17 @@ nsSHEntry::nsSHEntry()
   , mLoadedInThisProcess(false)
 {
 }
 
 nsSHEntry::nsSHEntry(const nsSHEntry& aOther)
   : mShared(aOther.mShared)
   , mURI(aOther.mURI)
   , mOriginalURI(aOther.mOriginalURI)
+  , mResultPrincipalURI(aOther.mResultPrincipalURI)
   , mReferrerURI(aOther.mReferrerURI)
   , mReferrerPolicy(aOther.mReferrerPolicy)
   , mTitle(aOther.mTitle)
   , mPostData(aOther.mPostData)
   , mLoadType(0)         // XXX why not copy?
   , mID(aOther.mID)
   , mScrollPositionX(0)  // XXX why not copy?
   , mScrollPositionY(0)  // XXX why not copy?
--- a/dom/base/Selection.h
+++ b/dom/base/Selection.h
@@ -432,17 +432,17 @@ private:
   // us to perform binary searches when searching for existence of a range,
   // giving us O(log n) search time.
   //
   // Inserting a new range requires finding the overlapping interval, requiring
   // two binary searches plus up to an additional 6 DOM comparisons. If this
   // proves to be a performance concern, then an interval tree may be a
   // possible solution, allowing the calculation of the overlap interval in
   // O(log n) time, though this would require rebalancing and other overhead.
-  nsTArray<RangeData> mRanges;
+  AutoTArray<RangeData, 1> mRanges;
 
   RefPtr<nsRange> mAnchorFocusRange;
   RefPtr<nsFrameSelection> mFrameSelection;
   RefPtr<nsAutoScrollTimer> mAutoScrollTimer;
   FallibleTArray<nsCOMPtr<nsISelectionListener>> mSelectionListeners;
   nsRevocableEventPtr<ScrollSelectionIntoViewEvent> mScrollEvent;
   CachedOffsetForFrame* mCachedOffsetForFrame;
   nsDirection mDirection;
--- a/dom/base/TabGroup.h
+++ b/dom/base/TabGroup.h
@@ -2,16 +2,17 @@
 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
 /* 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/. */
 
 #ifndef TabGroup_h
 #define TabGroup_h
 
+#include "nsHashKeys.h"
 #include "nsISupportsImpl.h"
 #include "nsIPrincipal.h"
 #include "nsTHashtable.h"
 #include "nsString.h"
 
 #include "mozilla/Atomics.h"
 #include "mozilla/SchedulerGroup.h"
 #include "mozilla/RefPtr.h"
@@ -38,16 +39,17 @@ class TabChild;
 // A TabGroup is a set of browsing contexts which are all "related". Within a
 // TabGroup, browsing contexts are broken into "similar-origin" DocGroups. In
 // more detail, a DocGroup is actually a collection of documents, and a
 // TabGroup is a collection of DocGroups. A TabGroup typically will contain
 // (through its DocGroups) the documents from one or more tabs related by
 // window.opener. A DocGroup is a member of exactly one TabGroup.
 
 class DocGroup;
+class TabChild;
 
 class TabGroup final : public SchedulerGroup
 {
 private:
   class HashEntry : public nsCStringHashKey
   {
   public:
     // NOTE: Weak reference. The DocGroup destructor removes itself from its
--- a/dom/base/nsDOMWindowUtils.cpp
+++ b/dom/base/nsDOMWindowUtils.cpp
@@ -1821,16 +1821,49 @@ nsDOMWindowUtils::GetBoundsWithoutFlushi
     rect->SetLayoutRect(r);
   }
 
   rect.forget(aResult);
   return NS_OK;
 }
 
 NS_IMETHODIMP
+nsDOMWindowUtils::NeedsFlush(int32_t aFlushType, bool* aResult)
+{
+  MOZ_ASSERT(aResult);
+
+  nsCOMPtr<nsIDocument> doc = GetDocument();
+  NS_ENSURE_STATE(doc);
+
+  nsIPresShell* presShell = doc->GetShell();
+  NS_ENSURE_STATE(presShell);
+
+  FlushType flushType;
+  switch (aFlushType) {
+  case FLUSH_STYLE:
+    flushType = FlushType::Style;
+    break;
+
+  case FLUSH_LAYOUT:
+    flushType = FlushType::Layout;
+    break;
+
+  case FLUSH_DISPLAY:
+    flushType = FlushType::Display;
+    break;
+
+  default:
+    return NS_ERROR_INVALID_ARG;
+  }
+
+  *aResult = presShell->NeedFlush(flushType);
+  return NS_OK;
+}
+
+NS_IMETHODIMP
 nsDOMWindowUtils::GetRootBounds(nsIDOMClientRect** aResult)
 {
   nsIDocument* doc = GetDocument();
   NS_ENSURE_STATE(doc);
 
   nsRect bounds(0, 0, 0, 0);
   nsIPresShell* presShell = doc->GetShell();
   if (presShell) {
--- a/dom/base/nsFrameMessageManager.cpp
+++ b/dom/base/nsFrameMessageManager.cpp
@@ -845,16 +845,23 @@ nsFrameMessageManager::GetContent(mozIDO
 NS_IMETHODIMP
 nsFrameMessageManager::GetDocShell(nsIDocShell** aDocShell)
 {
   *aDocShell = nullptr;
   return NS_OK;
 }
 
 NS_IMETHODIMP
+nsFrameMessageManager::GetTabEventTarget(nsIEventTarget** aTarget)
+{
+  *aTarget = nullptr;
+  return NS_OK;
+}
+
+NS_IMETHODIMP
 nsFrameMessageManager::Btoa(const nsAString& aBinaryData,
                             nsAString& aAsciiBase64String)
 {
   return nsContentUtils::Btoa(aBinaryData, aAsciiBase64String);
 }
 
 NS_IMETHODIMP
 nsFrameMessageManager::Atob(const nsAString& aAsciiString,
--- a/dom/base/nsIMessageManager.idl
+++ b/dom/base/nsIMessageManager.idl
@@ -3,16 +3,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/. */
 
 #include "nsISupports.idl"
 
 interface mozIDOMWindowProxy;
 interface nsIDocShell;
 interface nsIContent;
+interface nsIEventTarget;
 interface nsIFrameLoader;
 interface nsIPrincipal;
 
 /**
  * Message managers provide a way for chrome-privileged JS code to
  * communicate with each other, even across process boundaries.
  *
  * Message managers are separated into "parent side" and "child side".
@@ -393,16 +394,22 @@ interface nsIContentFrameMessageManager 
    * The current top level window in the frame or null.
    */
   readonly attribute mozIDOMWindowProxy content;
 
   /**
    * The top level docshell or null.
    */
   readonly attribute nsIDocShell docShell;
+
+  /**
+   * Returns the SchedulerEventTarget corresponding to the TabGroup
+   * for this frame.
+   */
+  readonly attribute nsIEventTarget tabEventTarget;
 };
 
 [uuid(b39a3324-b574-4f85-8cdb-274d04f807ef)]
 interface nsIInProcessContentFrameMessageManager : nsIContentFrameMessageManager
 {
   [notxpcom] nsIContent getOwnerContent();
   [notxpcom] void cacheFrameLoader(in nsIFrameLoader aFrameLoader);
 };
--- a/dom/base/nsInProcessTabChildGlobal.cpp
+++ b/dom/base/nsInProcessTabChildGlobal.cpp
@@ -194,16 +194,24 @@ nsInProcessTabChildGlobal::GetContent(mo
 
 NS_IMETHODIMP
 nsInProcessTabChildGlobal::GetDocShell(nsIDocShell** aDocShell)
 {
   NS_IF_ADDREF(*aDocShell = mDocShell);
   return NS_OK;
 }
 
+NS_IMETHODIMP
+nsInProcessTabChildGlobal::GetTabEventTarget(nsIEventTarget** aTarget)
+{
+  nsCOMPtr<nsIEventTarget> target = GetMainThreadEventTarget();
+  target.forget(aTarget);
+  return NS_OK;
+}
+
 void
 nsInProcessTabChildGlobal::FireUnloadEvent()
 {
   // We're called from nsDocument::MaybeInitializeFinalizeFrameLoaders, so it
   // should be safe to run script.
   MOZ_ASSERT(nsContentUtils::IsSafeToRunScript());
 
   // Don't let the unload event propagate to chrome event handlers.
--- a/dom/base/nsInProcessTabChildGlobal.h
+++ b/dom/base/nsInProcessTabChildGlobal.h
@@ -72,16 +72,17 @@ public:
   {
     return mMessageManager
       ? mMessageManager->SendRpcMessage(aMessageName, aObject, aRemote,
                                         aPrincipal, aCx, aArgc, aRetval)
       : NS_ERROR_NULL_POINTER;
   }
   NS_IMETHOD GetContent(mozIDOMWindowProxy** aContent) override;
   NS_IMETHOD GetDocShell(nsIDocShell** aDocShell) override;
+  NS_IMETHOD GetTabEventTarget(nsIEventTarget** aTarget) override;
 
   NS_DECL_NSIINPROCESSCONTENTFRAMEMESSAGEMANAGER
 
   /**
    * MessageManagerCallback methods that we override.
    */
   virtual bool DoSendBlockingMessage(JSContext* aCx,
                                       const nsAString& aMessage,
--- a/dom/html/HTMLFormControlsCollection.cpp
+++ b/dom/html/HTMLFormControlsCollection.cpp
@@ -256,17 +256,17 @@ HTMLFormControlsCollection::RemoveElemen
     return NS_OK;
   }
 
   return mForm->RemoveElementFromTableInternal(mNameLookupTable, aChild, aName);
 }
 
 nsresult
 HTMLFormControlsCollection::GetSortedControls(
-  nsTArray<nsGenericHTMLFormElement*>& aControls) const
+  nsTArray<RefPtr<nsGenericHTMLFormElement>>& aControls) const
 {
 #ifdef DEBUG
   HTMLFormElement::AssertDocumentOrder(mElements, mForm);
   HTMLFormElement::AssertDocumentOrder(mNotInElements, mForm);
 #endif
 
   aControls.Clear();
 
--- a/dom/html/HTMLFormControlsCollection.h
+++ b/dom/html/HTMLFormControlsCollection.h
@@ -10,16 +10,18 @@
 #include "mozilla/dom/Element.h" // DOMProxyHandler::getOwnPropertyDescriptor
 #include "nsIHTMLCollection.h"
 #include "nsInterfaceHashtable.h"
 #include "nsTArray.h"
 #include "nsWrapperCache.h"
 
 class nsGenericHTMLFormElement;
 class nsIFormControl;
+template <class T>
+class RefPtr;
 
 namespace mozilla {
 namespace dom {
 class HTMLFormElement;
 class HTMLImageElement;
 class OwningRadioNodeListOrElement;
 template<typename> struct Nullable;
 
@@ -70,17 +72,17 @@ public:
    * Create a sorted list of form control elements. This list is sorted
    * in document order and contains the controls in the mElements and
    * mNotInElements list. This function does not add references to the
    * elements.
    *
    * @param aControls The list of sorted controls[out].
    * @return NS_OK or NS_ERROR_OUT_OF_MEMORY.
    */
-  nsresult GetSortedControls(nsTArray<nsGenericHTMLFormElement*>& aControls) const;
+  nsresult GetSortedControls(nsTArray<RefPtr<nsGenericHTMLFormElement>>& aControls) const;
 
   // nsWrapperCache
   using nsWrapperCache::GetWrapperPreserveColor;
   using nsWrapperCache::PreserveWrapper;
   virtual JSObject* WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override;
 protected:
   virtual ~HTMLFormControlsCollection();
   virtual JSObject* GetWrapperPreserveColorInternal() override
--- a/dom/html/HTMLFormElement.cpp
+++ b/dom/html/HTMLFormElement.cpp
@@ -1016,41 +1016,32 @@ HTMLFormElement::NotifySubmitObservers(n
 
   return rv;
 }
 
 
 nsresult
 HTMLFormElement::WalkFormElements(HTMLFormSubmission* aFormSubmission)
 {
-  nsTArray<nsGenericHTMLFormElement*> sortedControls;
+  // This shouldn't be called recursively, so use a rather large value
+  // for the preallocated buffer.
+  AutoTArray<RefPtr<nsGenericHTMLFormElement>, 100> sortedControls;
   nsresult rv = mControls->GetSortedControls(sortedControls);
   NS_ENSURE_SUCCESS(rv, rv);
 
   uint32_t len = sortedControls.Length();
 
-  // Hold a reference to the elements so they can't be deleted while
-  // calling SubmitNamesValues().
-  for (uint32_t i = 0; i < len; ++i) {
-    static_cast<nsGenericHTMLElement*>(sortedControls[i])->AddRef();
-  }
-
   //
   // Walk the list of nodes and call SubmitNamesValues() on the controls
   //
   for (uint32_t i = 0; i < len; ++i) {
     // Tell the control to submit its name/value pairs to the submission
     sortedControls[i]->SubmitNamesValues(aFormSubmission);
   }
 
-  // Release the references.
-  for (uint32_t i = 0; i < len; ++i) {
-    static_cast<nsGenericHTMLElement*>(sortedControls[i])->Release();
-  }
-
   return NS_OK;
 }
 
 // nsIForm
 
 NS_IMETHODIMP_(uint32_t)
 HTMLFormElement::GetElementCount() const
 {
@@ -1134,16 +1125,42 @@ HTMLFormElement::AssertDocumentOrder(
   if (!aControls.IsEmpty()) {
     for (uint32_t i = 0; i < aControls.Length() - 1; ++i) {
       NS_ASSERTION(CompareFormControlPosition(aControls[i], aControls[i + 1],
                                               aForm) < 0,
                    "Form controls not ordered correctly");
     }
   }
 }
+
+/**
+ * Copy of the above function, but with RefPtrs.
+ *
+ * @param aControls List of form controls to check.
+ * @param aForm Parent form of the controls.
+ */
+/* static */ void
+HTMLFormElement::AssertDocumentOrder(
+  const nsTArray<RefPtr<nsGenericHTMLFormElement>>& aControls, nsIContent* aForm)
+{
+  // TODO: remove the return statement with bug 598468.
+  // This is done to prevent asserts in some edge cases.
+  return;
+
+  // Only iterate if aControls is not empty, since otherwise
+  // |aControls.Length() - 1| will be a very large unsigned number... not what
+  // we want here.
+  if (!aControls.IsEmpty()) {
+    for (uint32_t i = 0; i < aControls.Length() - 1; ++i) {
+      NS_ASSERTION(CompareFormControlPosition(aControls[i], aControls[i + 1],
+                                              aForm) < 0,
+                   "Form controls not ordered correctly");
+    }
+  }
+}
 #endif
 
 void
 HTMLFormElement::PostPasswordEvent()
 {
   // Don't fire another add event if we have a pending add event.
   if (mFormPasswordEventDispatcher.get()) {
     return;
@@ -1863,29 +1880,25 @@ HTMLFormElement::ForgetCurrentSubmission
   mWebProgress = nullptr;
 }
 
 bool
 HTMLFormElement::CheckFormValidity(nsIMutableArray* aInvalidElements) const
 {
   bool ret = true;
 
-  nsTArray<nsGenericHTMLFormElement*> sortedControls;
+  // This shouldn't be called recursively, so use a rather large value
+  // for the preallocated buffer.
+  AutoTArray<RefPtr<nsGenericHTMLFormElement>, 100> sortedControls;
   if (NS_FAILED(mControls->GetSortedControls(sortedControls))) {
     return false;
   }
 
   uint32_t len = sortedControls.Length();
 
-  // Hold a reference to the elements so they can't be deleted while calling
-  // the invalid events.
-  for (uint32_t i = 0; i < len; ++i) {
-    sortedControls[i]->AddRef();
-  }
-
   for (uint32_t i = 0; i < len; ++i) {
     nsCOMPtr<nsIConstraintValidation> cvElmt = do_QueryObject(sortedControls[i]);
     if (cvElmt && cvElmt->IsCandidateForConstraintValidation() &&
         !cvElmt->IsValid()) {
       ret = false;
       bool defaultAction = true;
       nsContentUtils::DispatchTrustedEvent(sortedControls[i]->OwnerDoc(),
                                            static_cast<nsIContent*>(sortedControls[i]),
@@ -1896,21 +1909,16 @@ HTMLFormElement::CheckFormValidity(nsIMu
       // requested them.
       if (defaultAction && aInvalidElements) {
         aInvalidElements->AppendElement(ToSupports(sortedControls[i]),
                                         false);
       }
     }
   }
 
-  // Release the references.
-  for (uint32_t i = 0; i < len; ++i) {
-    static_cast<nsGenericHTMLElement*>(sortedControls[i])->Release();
-  }
-
   return ret;
 }
 
 bool
 HTMLFormElement::CheckValidFormSubmission()
 {
   /**
    * Check for form validity: do not submit a form if there are unhandled
--- a/dom/html/HTMLFormElement.h
+++ b/dom/html/HTMLFormElement.h
@@ -405,16 +405,19 @@ public:
 
   static int32_t
   CompareFormControlPosition(Element* aElement1, Element* aElement2,
                              const nsIContent* aForm);
 #ifdef DEBUG
   static void
   AssertDocumentOrder(const nsTArray<nsGenericHTMLFormElement*>& aControls,
                       nsIContent* aForm);
+  static void
+  AssertDocumentOrder(const nsTArray<RefPtr<nsGenericHTMLFormElement>>& aControls,
+                      nsIContent* aForm);
 #endif
 
   js::ExpandoAndGeneration mExpandoAndGeneration;
 
 protected:
   virtual JSObject* WrapNode(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override;
 
   void PostPasswordEvent();
--- a/dom/html/nsGenericHTMLElement.cpp
+++ b/dom/html/nsGenericHTMLElement.cpp
@@ -482,17 +482,17 @@ nsGenericHTMLElement::BindToTree(nsIDocu
 {
   nsresult rv = nsGenericHTMLElementBase::BindToTree(aDocument, aParent,
                                                      aBindingParent,
                                                      aCompileEventHandlers);
   NS_ENSURE_SUCCESS(rv, rv);
 
   if (aDocument) {
     RegAccessKey();
-    if (CanHaveName(NodeInfo()->NameAtom()) && HasName()) {
+    if (HasName() && CanHaveName(NodeInfo()->NameAtom())) {
       aDocument->
         AddToNameTable(this, GetParsedAttr(nsGkAtoms::name)->GetAtomValue());
     }
 
     if (HasFlag(NODE_IS_EDITABLE) && GetContentEditableValue() == eTrue) {
       nsCOMPtr<nsIHTMLDocument> htmlDocument = do_QueryInterface(aDocument);
       if (htmlDocument) {
         htmlDocument->ChangeContentEditableCount(this, +1);
--- a/dom/html/nsGenericHTMLElement.h
+++ b/dom/html/nsGenericHTMLElement.h
@@ -728,17 +728,17 @@ protected:
   void AddToNameTable(nsIAtom* aName) {
     NS_ASSERTION(HasName(), "Node doesn't have name?");
     nsIDocument* doc = GetUncomposedDoc();
     if (doc && !IsInAnonymousSubtree()) {
       doc->AddToNameTable(this, aName);
     }
   }
   void RemoveFromNameTable() {
-    if (CanHaveName(NodeInfo()->NameAtom()) && HasName()) {
+    if (HasName() && CanHaveName(NodeInfo()->NameAtom())) {
       nsIDocument* doc = GetUncomposedDoc();
       if (doc) {
         doc->RemoveFromNameTable(this, GetParsedAttr(nsGkAtoms::name)->
                                          GetAtomValue());
       }
     }
   }
 
--- a/dom/indexedDB/ActorsParent.cpp
+++ b/dom/indexedDB/ActorsParent.cpp
@@ -29266,17 +29266,17 @@ PermissionRequestHelper::ActorDestroy(Ac
   mActorDestroyed = true;
 }
 
 #ifdef DEBUG
 
 NS_IMPL_ISUPPORTS(DEBUGThreadSlower, nsIThreadObserver)
 
 NS_IMETHODIMP
-DEBUGThreadSlower::OnDispatchedEvent(nsIThreadInternal* /* aThread */)
+DEBUGThreadSlower::OnDispatchedEvent()
 {
   MOZ_CRASH("Should never be called!");
 }
 
 NS_IMETHODIMP
 DEBUGThreadSlower::OnProcessNextEvent(nsIThreadInternal* /* aThread */,
                                       bool /* aMayWait */)
 {
--- a/dom/interfaces/base/nsIDOMWindowUtils.idl
+++ b/dom/interfaces/base/nsIDOMWindowUtils.idl
@@ -990,16 +990,25 @@ interface nsIDOMWindowUtils : nsISupport
    */
   void getScrollbarSize(in boolean aFlushLayout, out long aWidth, out long aHeight);
 
   /**
    * Returns the given element's bounds without flushing pending layout changes.
    */
   nsIDOMClientRect getBoundsWithoutFlushing(in nsIDOMElement aElement);
 
+  const long FLUSH_STYLE = 0;
+  const long FLUSH_LAYOUT = 1;
+  const long FLUSH_DISPLAY = 2;
+
+  /**
+   * Returns true if a flush of the given type is needed.
+   */
+  bool needsFlush(in long aFlushtype);
+
   /**
    * Returns the bounds of the window's currently loaded document. This will
    * generally be (0, 0, pageWidth, pageHeight) but in some cases (e.g. RTL
    * documents) may have a negative left value.
    */
   nsIDOMClientRect getRootBounds();
 
   /**
--- a/dom/ipc/ContentChild.cpp
+++ b/dom/ipc/ContentChild.cpp
@@ -980,32 +980,29 @@ ContentChild::ProvideWindowCommon(TabChi
 
   // =====================
   // End Nested Event Loop
   // =====================
 
   // Handle the error which we got back from the parent process, if we got
   // one.
   if (NS_FAILED(rv)) {
-    PRenderFrameChild::Send__delete__(renderFrame);
     return rv;
   }
 
   if (!*aWindowIsNew) {
-    PRenderFrameChild::Send__delete__(renderFrame);
     return NS_ERROR_ABORT;
   }
 
   // If the TabChild has been torn down, we don't need to do this anymore.
   if (NS_WARN_IF(!newChild->IPCOpen())) {
     return NS_ERROR_ABORT;
   }
 
   if (layersId == 0) { // if renderFrame is invalid.
-    PRenderFrameChild::Send__delete__(renderFrame);
     renderFrame = nullptr;
   }
 
   ShowInfo showInfo(EmptyString(), false, false, true, false, 0, 0, 0);
   auto* opener = nsPIDOMWindowOuter::From(aParent);
   nsIDocShell* openerShell;
   if (opener && (openerShell = opener->GetDocShell())) {
     nsCOMPtr<nsILoadContext> context = do_QueryInterface(openerShell);
@@ -2869,30 +2866,31 @@ ContentChild::RecvShutdown()
   // If we receive the shutdown message from within a nested event loop, we want
   // to wait for that event loop to finish. Otherwise we could prematurely
   // terminate an "unload" or "pagehide" event handler (which might be doing a
   // sync XHR, for example).
 #if defined(MOZ_CRASHREPORTER)
   CrashReporter::AnnotateCrashReport(NS_LITERAL_CSTRING("IPCShutdownState"),
                                      NS_LITERAL_CSTRING("RecvShutdown"));
 #endif
-  nsCOMPtr<nsIThread> thread;
-  nsresult rv = NS_GetMainThread(getter_AddRefs(thread));
-  if (NS_SUCCEEDED(rv) && thread) {
-    RefPtr<nsThread> mainThread(thread.forget().downcast<nsThread>());
-    if (mainThread->RecursionDepth() > 1) {
-      // We're in a nested event loop. Let's delay for an arbitrary period of
-      // time (100ms) in the hopes that the event loop will have finished by
-      // then.
-      MessageLoop::current()->PostDelayedTask(
-        NewRunnableMethod(
-          "dom::ContentChild::RecvShutdown", this, &ContentChild::RecvShutdown),
-        100);
-      return IPC_OK();
-    }
+  MOZ_ASSERT(NS_IsMainThread());
+  RefPtr<nsThread> mainThread = nsThreadManager::get().GetCurrentThread();
+  // Note that we only have to check the recursion count for the current
+  // cooperative thread. Since the Shutdown message is not labeled with a
+  // SchedulerGroup, there can be no other cooperative threads doing work while
+  // we're running.
+  if (mainThread && mainThread->RecursionDepth() > 1) {
+    // We're in a nested event loop. Let's delay for an arbitrary period of
+    // time (100ms) in the hopes that the event loop will have finished by
+    // then.
+    MessageLoop::current()->PostDelayedTask(
+      NewRunnableMethod(
+        "dom::ContentChild::RecvShutdown", this, &ContentChild::RecvShutdown),
+      100);
+    return IPC_OK();
   }
 
   mShuttingDown = true;
 
   if (mPolicy) {
     mPolicy->Deactivate();
     mPolicy = nullptr;
   }
--- a/dom/ipc/ContentPrefs.cpp
+++ b/dom/ipc/ContentPrefs.cpp
@@ -100,16 +100,17 @@ const char* mozilla::dom::ContentPrefs::
   "javascript.options.ion",
   "javascript.options.ion.offthread_compilation",
   "javascript.options.ion.threshold",
   "javascript.options.ion.unsafe_eager_compilation",
   "javascript.options.jit.full_debug_checks",
   "javascript.options.native_regexp",
   "javascript.options.parallel_parsing",
   "javascript.options.shared_memory",
+  "javascript.options.streams",
   "javascript.options.strict",
   "javascript.options.strict.debug",
   "javascript.options.throw_on_asmjs_validation_failure",
   "javascript.options.throw_on_debuggee_would_run",
   "javascript.options.wasm",
   "javascript.options.wasm_baselinejit",
   "javascript.options.werror",
   "javascript.use_us_english_locale",
--- a/dom/ipc/TabChild.cpp
+++ b/dom/ipc/TabChild.cpp
@@ -3502,16 +3502,24 @@ TabChildGlobal::GetDocShell(nsIDocShell*
   *aDocShell = nullptr;
   if (!mTabChild)
     return NS_ERROR_NULL_POINTER;
   nsCOMPtr<nsIDocShell> docShell = do_GetInterface(mTabChild->WebNavigation());
   docShell.swap(*aDocShell);
   return NS_OK;
 }
 
+NS_IMETHODIMP
+TabChildGlobal::GetTabEventTarget(nsIEventTarget** aTarget)
+{
+  nsCOMPtr<nsIEventTarget> target = EventTargetFor(TaskCategory::Other);
+  target.forget(aTarget);
+  return NS_OK;
+}
+
 nsIPrincipal*
 TabChildGlobal::GetPrincipal()
 {
   if (!mTabChild)
     return nullptr;
   return mTabChild->GetPrincipal();
 }
 
--- a/dom/ipc/TabChild.h
+++ b/dom/ipc/TabChild.h
@@ -109,16 +109,17 @@ public:
   {
     return mMessageManager
       ? mMessageManager->SendRpcMessage(aMessageName, aObject, aRemote,
                                         aPrincipal, aCx, aArgc, aRetval)
       : NS_ERROR_NULL_POINTER;
   }
   NS_IMETHOD GetContent(mozIDOMWindowProxy** aContent) override;
   NS_IMETHOD GetDocShell(nsIDocShell** aDocShell) override;
+  NS_IMETHOD GetTabEventTarget(nsIEventTarget** aTarget) override;
 
   nsresult AddEventListener(const nsAString& aType,
                             nsIDOMEventListener* aListener,
                             bool aUseCapture)
   {
     // By default add listeners only for trusted events!
     return DOMEventTargetHelper::AddEventListener(aType, aListener,
                                                   aUseCapture, false, 2);
--- a/dom/push/PushDB.jsm
+++ b/dom/push/PushDB.jsm
@@ -2,17 +2,16 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 const Cu = Components.utils;
 Cu.import("resource://gre/modules/IndexedDBHelper.jsm");
-Cu.import("resource://gre/modules/Preferences.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.importGlobalProperties(["indexedDB"]);
 
 this.EXPORTED_SYMBOLS = ["PushDB"];
 
 XPCOMUtils.defineLazyGetter(this, "console", () => {
   let {ConsoleAPI} = Cu.import("resource://gre/modules/Console.jsm", {});
   return new ConsoleAPI({
--- a/dom/push/PushRecord.jsm
+++ b/dom/push/PushRecord.jsm
@@ -5,32 +5,31 @@
 "use strict";
 
 const Cc = Components.classes;
 const Ci = Components.interfaces;
 const Cu = Components.utils;
 const Cr = Components.results;
 
 Cu.import("resource://gre/modules/AppConstants.jsm");
-Cu.import("resource://gre/modules/Preferences.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "EventDispatcher",
                                   "resource://gre/modules/Messaging.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
                                   "resource://gre/modules/PlacesUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
                                   "resource://gre/modules/PrivateBrowsingUtils.jsm");
 
 
 this.EXPORTED_SYMBOLS = ["PushRecord"];
 
-const prefs = new Preferences("dom.push.");
+const prefs = Services.prefs.getBranch("dom.push.");
 
 /**
  * The push subscription record, stored in IndexedDB.
  */
 function PushRecord(props) {
   this.pushEndpoint = props.pushEndpoint;
   this.scope = props.scope;
   this.originAttributes = props.originAttributes;
@@ -45,25 +44,25 @@ function PushRecord(props) {
   this.setQuota(props.quota);
   this.ctime = (typeof props.ctime === "number") ? props.ctime : 0;
 }
 
 PushRecord.prototype = {
   setQuota(suggestedQuota) {
     if (this.quotaApplies()) {
       let quota = +suggestedQuota;
-      this.quota = quota >= 0 ? quota : prefs.get("maxQuotaPerSubscription");
+      this.quota = quota >= 0 ? quota : prefs.getIntPref("maxQuotaPerSubscription");
     } else {
       this.quota = Infinity;
     }
   },
 
   resetQuota() {
     this.quota = this.quotaApplies() ?
-                 prefs.get("maxQuotaPerSubscription") : Infinity;
+                 prefs.getIntPref("maxQuotaPerSubscription") : Infinity;
   },
 
   updateQuota(lastVisit) {
     if (this.isExpired() || !this.quotaApplies()) {
       // Ignore updates if the registration is already expired, or isn't
       // subject to quota.
       return;
     }
@@ -76,17 +75,17 @@ PushRecord.prototype = {
     if (lastVisit > this.lastPush) {
       // If the user visited the site since the last time we received a
       // notification, reset the quota. `Math.max(0, ...)` ensures the
       // last visit date isn't in the future.
       let daysElapsed =
         Math.max(0, (Date.now() - lastVisit) / 24 / 60 / 60 / 1000);
       this.quota = Math.min(
         Math.round(8 * Math.pow(daysElapsed, -0.8)),
-        prefs.get("maxQuotaPerSubscription")
+        prefs.getIntPref("maxQuotaPerSubscription")
       );
     }
   },
 
   receivedPush(lastVisit) {
     this.updateQuota(lastVisit);
     this.pushCount++;
     this.lastPush = Date.now();
@@ -101,17 +100,17 @@ PushRecord.prototype = {
     if (this.recentMessageIDs) {
       this.recentMessageIDs.unshift(id);
     } else {
       this.recentMessageIDs = [id];
     }
     // Drop older message IDs from the end of the list.
     let maxRecentMessageIDs = Math.min(
       this.recentMessageIDs.length,
-      Math.max(prefs.get("maxRecentMessageIDsPerSubscription"), 0)
+      Math.max(prefs.getIntPref("maxRecentMessageIDsPerSubscription"), 0)
     );
     this.recentMessageIDs.length = maxRecentMessageIDs || 0;
   },
 
   hasRecentMessageID(id) {
     return this.recentMessageIDs && this.recentMessageIDs.includes(id);
   },
 
@@ -209,17 +208,17 @@ PushRecord.prototype = {
   },
 
   /**
    * Indicates whether the registration can deliver push messages to its
    * associated service worker. System subscriptions are exempt from the
    * permission check.
    */
   hasPermission() {
-    if (this.systemRecord || prefs.get("testing.ignorePermission")) {
+    if (this.systemRecord || prefs.getBoolPref("testing.ignorePermission", false)) {
       return true;
     }
     let permission = Services.perms.testExactPermissionFromPrincipal(
       this.principal, "desktop-notification");
     return permission == Ci.nsIPermissionManager.ALLOW_ACTION;
   },
 
   quotaChanged() {
--- a/dom/push/PushServiceHttp2.jsm
+++ b/dom/push/PushServiceHttp2.jsm
@@ -12,34 +12,33 @@ const Cr = Components.results;
 
 const {PushDB} = Cu.import("resource://gre/modules/PushDB.jsm");
 const {PushRecord} = Cu.import("resource://gre/modules/PushRecord.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/NetUtil.jsm");
 Cu.import("resource://gre/modules/IndexedDBHelper.jsm");
 Cu.import("resource://gre/modules/Timer.jsm");
-Cu.import("resource://gre/modules/Preferences.jsm");
 
 const {
   PushCrypto,
   concatArray,
 } = Cu.import("resource://gre/modules/PushCrypto.jsm");
 
 this.EXPORTED_SYMBOLS = ["PushServiceHttp2"];
 
 XPCOMUtils.defineLazyGetter(this, "console", () => {
   let {ConsoleAPI} = Cu.import("resource://gre/modules/Console.jsm", {});
   return new ConsoleAPI({
     maxLogLevelPref: "dom.push.loglevel",
     prefix: "PushServiceHttp2",
   });
 });
 
-const prefs = new Preferences("dom.push.");
+const prefs = Services.prefs.getBranch("dom.push.");
 
 const kPUSHHTTP2DB_DB_NAME = "pushHttp2";
 const kPUSHHTTP2DB_DB_VERSION = 5; // Change this if the IndexedDB format changes
 const kPUSHHTTP2DB_STORE_NAME = "pushHttp2";
 
 /**
  * A proxy between the PushService and connections listening for incoming push
  * messages. The PushService can silence messages from the connections by
@@ -252,17 +251,17 @@ SubscriptionListener.prototype = {
     if (!Components.isSuccessCode(aStatus)) {
       this._reject(new Error("Error listening for messages: " + aStatus));
       return;
     }
 
     var statusCode = aRequest.QueryInterface(Ci.nsIHttpChannel).responseStatus;
 
     if (Math.floor(statusCode / 100) == 5) {
-      if (this._subInfo.retries < prefs.get("http2.maxRetries")) {
+      if (this._subInfo.retries < prefs.getIntPref("http2.maxRetries")) {
         this._subInfo.retries++;
         var retryAfter = retryAfterParser(aRequest);
         this._retryTimeoutID = setTimeout(_ =>
           {
             this._reject(
               {
                 retry: true,
                 subInfo: this._subInfo
@@ -427,17 +426,17 @@ this.PushServiceHttp2 = {
   },
 
   hasmainPushService: function() {
     return this._mainPushService !== null;
   },
 
   validServerURI: function(serverURI) {
     if (serverURI.scheme == "http") {
-      return !!prefs.get("testing.allowInsecureServerURL");
+      return !!prefs.getBoolPref("testing.allowInsecureServerURL", false);
     }
     return serverURI.scheme == "https";
   },
 
   connect: function(subscriptions) {
     this.startConnections(subscriptions);
   },
 
@@ -577,39 +576,39 @@ this.PushServiceHttp2 = {
     this._serverURI = aServerURL;
 
     return Promise.resolve();
   },
 
   _retryAfterBackoff: function(aSubscriptionUri, retryAfter) {
     console.debug("retryAfterBackoff()");
 
-    var resetRetryCount = prefs.get("http2.reset_retry_count_after_ms");
+    var resetRetryCount = prefs.getIntPref("http2.reset_retry_count_after_ms");
     // If it was running for some time, reset retry counter.
     if ((Date.now() - this._conns[aSubscriptionUri].lastStartListening) >
         resetRetryCount) {
       this._conns[aSubscriptionUri].countUnableToConnect = 0;
     }
 
-    let maxRetries = prefs.get("http2.maxRetries");
+    let maxRetries = prefs.getIntPref("http2.maxRetries");
     if (this._conns[aSubscriptionUri].countUnableToConnect >= maxRetries) {
       this._shutdownSubscription(aSubscriptionUri);
       this._resubscribe(aSubscriptionUri);
       return;
     }
 
     if (retryAfter !== -1) {
       // This is a 5xx response.
       this._conns[aSubscriptionUri].countUnableToConnect++;
       this._conns[aSubscriptionUri].retryTimerID =
         setTimeout(_ => this._listenForMsgs(aSubscriptionUri), retryAfter);
       return;
     }
 
-    retryAfter = prefs.get("http2.retryInterval") *
+    retryAfter = prefs.getIntPref("http2.retryInterval") *
       Math.pow(2, this._conns[aSubscriptionUri].countUnableToConnect);
 
     retryAfter = retryAfter * (0.8 + Math.random() * 0.4); // add +/-20%.
 
     this._conns[aSubscriptionUri].countUnableToConnect++;
     this._conns[aSubscriptionUri].retryTimerID =
       setTimeout(_ => this._listenForMsgs(aSubscriptionUri), retryAfter);
 
--- a/dom/storage/StorageDBThread.cpp
+++ b/dom/storage/StorageDBThread.cpp
@@ -422,17 +422,17 @@ StorageDBThread::ThreadFunc()
     threadInternal->SetObserver(nullptr);
   }
 }
 
 
 NS_IMPL_ISUPPORTS(StorageDBThread::ThreadObserver, nsIThreadObserver)
 
 NS_IMETHODIMP
-StorageDBThread::ThreadObserver::OnDispatchedEvent(nsIThreadInternal* aThread)
+StorageDBThread::ThreadObserver::OnDispatchedEvent()
 {
   MonitorAutoLock lock(mMonitor);
   mHasPendingEvents = true;
   lock.Notify();
   return NS_OK;
 }
 
 NS_IMETHODIMP
--- a/dom/tests/mochitest/general/test_interfaces.js
+++ b/dom/tests/mochitest/general/test_interfaces.js
@@ -24,16 +24,18 @@
 // IMPORTANT: Do not change this list without review from
 //            a JavaScript Engine peer!
 var ecmaGlobals =
   [
     "Array",
     "ArrayBuffer",
     "Atomics",
     "Boolean",
+    {name: "ByteLengthQueuingStrategy", disabled: !SpecialPowers.Cu.getJSTestingFunctions().streamsAreEnabled()},
+    {name: "CountQueuingStrategy", disabled: !SpecialPowers.Cu.getJSTestingFunctions().streamsAreEnabled()},
     "DataView",
     "Date",
     "Error",
     "EvalError",
     "Float32Array",
     "Float64Array",
     "Function",
     // NB: We haven't bothered to resolve constants like Infinity and NaN on
@@ -50,16 +52,17 @@ var ecmaGlobals =
     "Map",
     "Math",
     {name: "NaN", xbl: false},
     "Number",
     "Object",
     "Promise",
     "Proxy",
     "RangeError",
+    {name: "ReadableStream", disabled: !SpecialPowers.Cu.getJSTestingFunctions().streamsAreEnabled()},
     "ReferenceError",
     "Reflect",
     "RegExp",
     "Set",
     "SharedArrayBuffer",
     {name: "SIMD", nightly: true},
     "StopIteration",
     "String",
--- a/dom/workers/RuntimeService.cpp
+++ b/dom/workers/RuntimeService.cpp
@@ -312,16 +312,17 @@ LoadContextOptions(const char* aPrefName
                 .setBaseline(GetWorkerPref<bool>(NS_LITERAL_CSTRING("baselinejit")))
                 .setIon(GetWorkerPref<bool>(NS_LITERAL_CSTRING("ion")))
                 .setNativeRegExp(GetWorkerPref<bool>(NS_LITERAL_CSTRING("native_regexp")))
                 .setAsyncStack(GetWorkerPref<bool>(NS_LITERAL_CSTRING("asyncstack")))
                 .setWerror(GetWorkerPref<bool>(NS_LITERAL_CSTRING("werror")))
 #ifdef FUZZING
                 .setFuzzing(GetWorkerPref<bool>(NS_LITERAL_CSTRING("fuzzing.enabled")))
 #endif
+                .setStreams(GetWorkerPref<bool>(NS_LITERAL_CSTRING("streams")))
                 .setExtraWarnings(GetWorkerPref<bool>(NS_LITERAL_CSTRING("strict")));
 
   RuntimeService::SetDefaultContextOptions(contextOptions);
 
   if (rts) {
     rts->UpdateAllWorkerContextOptions();
   }
 }
--- a/dom/workers/WorkerThread.cpp
+++ b/dom/workers/WorkerThread.cpp
@@ -61,16 +61,17 @@ private:
     mWorkerPrivate->AssertIsOnWorkerThread();
   }
 
   NS_DECL_NSITHREADOBSERVER
 };
 
 WorkerThread::WorkerThread()
   : nsThread(nsThread::NOT_MAIN_THREAD, kWorkerStackSize)
+  , mLock("WorkerThread::mLock")
   , mWorkerPrivateCondVar(mLock, "WorkerThread::mWorkerPrivateCondVar")
   , mWorkerPrivate(nullptr)
   , mOtherThreadsDispatchingViaEventTarget(0)
 #ifdef DEBUG
   , mAcceptingNonWorkerRunnables(true)
 #endif
 {
 }
@@ -308,17 +309,17 @@ WorkerThread::RecursionDepth(const Worke
   MOZ_ASSERT(PR_GetCurrentThread() == mThread);
 
   return mNestedEventLoopDepth;
 }
 
 NS_IMPL_ISUPPORTS(WorkerThread::Observer, nsIThreadObserver)
 
 NS_IMETHODIMP
-WorkerThread::Observer::OnDispatchedEvent(nsIThreadInternal* /* aThread */)
+WorkerThread::Observer::OnDispatchedEvent()
 {
   MOZ_CRASH("OnDispatchedEvent() should never be called!");
 }
 
 NS_IMETHODIMP
 WorkerThread::Observer::OnProcessNextEvent(nsIThreadInternal* /* aThread */,
                                            bool aMayWait)
 {
--- a/dom/workers/WorkerThread.h
+++ b/dom/workers/WorkerThread.h
@@ -38,16 +38,17 @@ class WorkerThreadFriendKey
   ~WorkerThreadFriendKey();
 };
 
 class WorkerThread final
   : public nsThread
 {
   class Observer;
 
+  Mutex mLock;
   CondVar mWorkerPrivateCondVar;
 
   // Protected by nsThread::mLock.
   WorkerPrivate* mWorkerPrivate;
 
   // Only touched on the target thread.
   RefPtr<Observer> mObserver;
 
--- a/dom/workers/test/browser_bug1047663.js
+++ b/dom/workers/test/browser_bug1047663.js
@@ -4,24 +4,20 @@
 "use strict";
 
 const TAB_URL = EXAMPLE_URL + "bug1047663_tab.html";
 const WORKER_URL = EXAMPLE_URL + "bug1047663_worker.sjs";
 
 function test() {
   waitForExplicitFinish();
 
-  // Disable rcwn to make cache behavior deterministic.
-  let rcwnEnabled = Preferences.get("network.http.rcwn.enabled");
-  Preferences.set("network.http.rcwn.enabled", false);
-  registerCleanupFunction(()=>{
-    Preferences.set("network.http.rcwn.enabled", rcwnEnabled);
-  });
+  (async function() {
+    // Disable rcwn to make cache behavior deterministic.
+    await SpecialPowers.pushPrefEnv({set: [["network.http.rcwn.enabled", false]]});
 
-  (async function() {
     let tab = await addTab(TAB_URL);
 
     // Create a worker. Post a message to it, and check the reply. Since the
     // server side JavaScript file returns the first source for the first
     // request, the reply should be "one". If the reply is correct, terminate
     // the worker.
     await createWorkerInTab(tab, WORKER_URL);
     let message = await postMessageToWorkerInTab(tab, WORKER_URL, "ping");
--- a/dom/workers/test/serviceworkers/test_serviceworker_interfaces.js
+++ b/dom/workers/test/serviceworkers/test_serviceworker_interfaces.js
@@ -22,16 +22,18 @@
 // IMPORTANT: Do not change this list without review from
 //            a JavaScript Engine peer!
 var ecmaGlobals =
   [
     "Array",
     "ArrayBuffer",
     "Atomics",
     "Boolean",
+    {name: "ByteLengthQueuingStrategy", optional: true},
+    {name: "CountQueuingStrategy", optional: true},
     "DataView",
     "Date",
     "Error",
     "EvalError",
     "Float32Array",
     "Float64Array",
     "Function",
     "Infinity",
@@ -45,16 +47,17 @@ var ecmaGlobals =
     "Map",
     "Math",
     "NaN",
     "Number",
     "Object",
     "Promise",
     "Proxy",
     "RangeError",
+    {name: "ReadableStream", optional: true},
     "ReferenceError",
     "Reflect",
     "RegExp",
     "Set",
     "SharedArrayBuffer",
     {name: "SIMD", nightly: true},
     "StopIteration",
     "String",
--- a/dom/workers/test/test_worker_interfaces.js
+++ b/dom/workers/test/test_worker_interfaces.js
@@ -22,16 +22,18 @@
 // IMPORTANT: Do not change this list without review from
 //            a JavaScript Engine peer!
 var ecmaGlobals =
   [
     "Array",
     "ArrayBuffer",
     "Atomics",
     "Boolean",
+    {name: "ByteLengthQueuingStrategy", optional: true},
+    {name: "CountQueuingStrategy", optional: true},
     "DataView",
     "Date",
     "Error",
     "EvalError",
     "Float32Array",
     "Float64Array",
     "Function",
     "Infinity",
@@ -45,16 +47,17 @@ var ecmaGlobals =
     "Map",
     "Math",
     "NaN",
     "Number",
     "Object",
     "Promise",
     "Proxy",
     "RangeError",
+    {name: "ReadableStream", optional: true},
     "ReferenceError",
     "Reflect",
     "RegExp",
     "Set",
     "SharedArrayBuffer",
     {name: "SIMD", nightly: true},
     "StopIteration",
     "String",
--- a/gfx/2d/DrawTarget.cpp
+++ b/gfx/2d/DrawTarget.cpp
@@ -213,19 +213,27 @@ DrawTarget::StrokeGlyphs(ScaledFont* aFo
   RefPtr<Path> path = aFont->GetPathForGlyphs(aBuffer, this);
   Stroke(path, aPattern, aStrokeOptions, aOptions);
 }
 
 already_AddRefed<SourceSurface>
 DrawTarget::IntoLuminanceSource(LuminanceType aMaskType, float aOpacity)
 {
   RefPtr<SourceSurface> surface = Snapshot();
+  if (!surface) {
+    return nullptr;
+  }
+
   IntSize size = surface->GetSize();
 
   RefPtr<DataSourceSurface> maskSurface = surface->GetDataSurface();
+  if (!maskSurface) {
+    return nullptr;
+  }
+
   DataSourceSurface::MappedSurface map;
   if (!maskSurface->Map(DataSourceSurface::MapType::READ, &map)) {
     return nullptr;
   }
 
   // Create alpha channel mask for output
   RefPtr<DataSourceSurface> destMaskSurface =
     Factory::CreateDataSourceSurface(size, SurfaceFormat::A8);
--- a/gfx/layers/LayerScope.cpp
+++ b/gfx/layers/LayerScope.cpp
@@ -623,17 +623,17 @@ protected:
 class DebugGLDrawData final: public DebugGLData {
 public:
     DebugGLDrawData(float aOffsetX,
                     float aOffsetY,
                     const gfx::Matrix4x4& aMVMatrix,
                     size_t aRects,
                     const gfx::Rect* aLayerRects,
                     const gfx::Rect* aTextureRects,
-                    const std::list<GLuint> aTexIDs,
+                    const std::list<GLuint>& aTexIDs,
                     void* aLayerRef)
         : DebugGLData(Packet::DRAW),
           mOffsetX(aOffsetX),
           mOffsetY(aOffsetY),
           mMVMatrix(aMVMatrix),
           mRects(aRects),
           mTexIDs(aTexIDs),
           mLayerRef(reinterpret_cast<uint64_t>(aLayerRef))
--- a/ipc/glue/GeckoChildProcessHost.cpp
+++ b/ipc/glue/GeckoChildProcessHost.cpp
@@ -142,17 +142,24 @@ GeckoChildProcessHost::GetPathToBinary(F
 {
   if (sRunSelfAsContentProc &&
       (processType == GeckoProcessType_Content || processType == GeckoProcessType_GPU)) {
 #if defined(OS_WIN)
     wchar_t exePathBuf[MAXPATHLEN];
     if (!::GetModuleFileNameW(nullptr, exePathBuf, MAXPATHLEN)) {
       MOZ_CRASH("GetModuleFileNameW failed (FIXME)");
     }
-    exePath = FilePath::FromWStringHack(exePathBuf);
+    std::wstring exePathStr = exePathBuf;
+#if defined(MOZ_SANDBOX)
+    // We need to start the child process using the real path, so that the
+    // sandbox policy rules will match for DLLs loaded from the bin dir after
+    // we have lowered the sandbox.
+    widget::WinUtils::ResolveJunctionPointsAndSymLinks(exePathStr);
+#endif
+    exePath = FilePath::FromWStringHack(exePathStr);
 #elif defined(OS_POSIX)
     exePath = FilePath(CommandLine::ForCurrentProcess()->argv()[0]);
 #else
 #  error Sorry; target OS not supported yet.
 #endif
     return BinaryPathType::Self;
   }
 
--- a/ipc/glue/MessagePump.cpp
+++ b/ipc/glue/MessagePump.cpp
@@ -433,17 +433,17 @@ MessagePumpForNonMainUIThreads::DoRunLoo
   }
 
   ClearInWait();
 
   ti->SetObserver(nullptr);
 }
 
 NS_IMETHODIMP
-MessagePumpForNonMainUIThreads::OnDispatchedEvent(nsIThreadInternal *thread)
+MessagePumpForNonMainUIThreads::OnDispatchedEvent()
 {
   // If our thread is sleeping in DoRunLoop's call to WaitForWork() and an
   // event posts to the nsIThread event queue - break our thread out of
   // chromium's WaitForWork.
   if (GetInWait()) {
     ScheduleWork();
   }
   return NS_OK;
new file mode 100644
--- /dev/null
+++ b/js/public/Stream.h
@@ -0,0 +1,522 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*-
+ * vim: set ts=8 sts=4 et sw=4 tw=99:
+ * 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/. */
+
+/*
+ * JSAPI functions and callbacks related to WHATWG Stream objects.
+ *
+ * Much of the API here mirrors the JS API of ReadableStream and associated
+ * classes, e.g. ReadableStreamDefaultReader, ReadableStreamBYOBReader,
+ * ReadableStreamDefaultController, ReadableByteStreamController, and
+ * ReadableStreamBYOBRequest.
+ *
+ * There are some crucial differences, though: Functionality that's exposed
+ * as methods/accessors on controllers in JS is exposed as functions taking
+ * ReadableStream instances instead. This is because an analysis of how
+ * the API would be used showed that all functions that'd take controllers
+ * would do so by first getting the controller from the stream instance it's
+ * associated with and then call the function taking it. I.e., it would purely
+ * add boilerplate without any gains in ease of use of the API.
+ *
+ * It would probably still make sense to factor the API the same as the JS API
+ * if we had to keep any API stability guarantees: the JS API won't change, so
+ * we could be sure that the C++ API could stay the same, too. Given that we
+ * don't guarantee API stability, this concern isn't too pressing.
+ *
+ * Some functions exposed here deal with ReadableStream instances that have an
+ * embedding-provided underlying source. These instances are largely similar
+ * to byte streams as created using |new ReadableStream({type: "bytes"})|:
+ * They enable users to acquire ReadableStreamBYOBReaders and only vend chunks
+ * that're typed array instances.
+ *
+ * When creating an "external readable stream" using
+ * JS::NewReadableExternalSourceStreamObject, an underlying source and a set
+ * of flags can be passed to be stored on the stream. The underlying source is
+ * treated as an opaque void* pointer by the JS engine: it's purely meant as
+ * a reference to be used by the embedding to identify whatever actual source
+ * it uses to supply data for the stream. Similarly, the flags aren't
+ * interpreted by the JS engine, but are passed to some of the callbacks below
+ * and can be retrieved using JS::ReadableStreamGetEmbeddingFlags.
+ *
+ * External readable streams are optimized to allow the embedding to interact
+ * with them with a minimum of overhead: chunks aren't enqueued as individual
+ * typed array instances; instead, the embedding only updates the amount of
+ * data available using ReadableStreamUpdateDataAvailableFromSource.
+ * When content requests data by reading from a reader,
+ * WriteIntoReadRequestBufferCallback is invoked, asking the embedding to
+ * write data directly into the buffer we're about to hand to content.
+ *
+ * Additionally, ReadableStreamGetExternalUnderlyingSource can be used to
+ * get the void* pointer to the underlying source. This is equivalent to
+ * acquiring a reader for the stream in that it locks the stream until it
+ * is released again using JS::ReadableStreamReleaseExternalUnderlyingSource.
+ *
+ * Embeddings are expected to detect situations where an API exposed to JS
+ * takes a ReadableStream to read from that has an external underlying source.
+ * In those situations, it might be preferable to directly perform data
+ * transfers from the stream's underlying source to whatever sink the
+ * embedding uses, assuming that such direct transfers can be performed
+ * more efficiently.
+ *
+ * An example of such an optimized operation might be a ServiceWorker piping a
+ * fetch Response body to a TextDecoder: instead of writing chunks of data
+ * into JS typed array buffers only to immediately read from them again, the
+ * embedding can presumably directly feed the incoming data to the
+ * TextDecoder's underlying implementation.
+ */
+
+#ifndef js_Stream_h
+#define js_Stream_h
+
+#include "jstypes.h"
+
+#include "js/TypeDecls.h"
+
+namespace JS {
+
+/**
+ * Invoked whenever a reader desires more data from a ReadableStream's
+ * embedding-provided underlying source.
+ *
+ * The given |desiredSize| is the absolute size, not a delta from the previous
+ * desired size.
+ */
+typedef void
+(* RequestReadableStreamDataCallback)(JSContext* cx, HandleObject stream,
+                                      void* underlyingSource, uint8_t flags, size_t desiredSize);
+
+/**
+ * Invoked to cause the embedding to fill the given |buffer| with data from
+ * the given embedding-provided underlying source.
+ *
+ * This can only happen after the embedding has updated the amount of data
+ * available using JS::ReadableStreamUpdateDataAvailableFromSource. If at
+ * least one read request is pending when
+ * JS::ReadableStreamUpdateDataAvailableFromSource is called,
+ * the WriteIntoReadRequestBufferCallback is invoked immediately from under
+ * the call to JS::WriteIntoReadRequestBufferCallback. If not, it is invoked
+ * if and when a new read request is made.
+ *
+ * Note: This callback *must not cause GC*, because that could potentially
+ * invalidate the |buffer| pointer.
+ */
+typedef void
+(* WriteIntoReadRequestBufferCallback)(JSContext* cx, HandleObject stream,
+                                       void* underlyingSource, uint8_t flags, void* buffer,
+                                       size_t length, size_t* bytesWritten);
+
+/**
+ * Invoked in reaction to the ReadableStream being canceled to allow the
+ * embedding to free the underlying source.
+ *
+ * This is equivalent to calling |cancel| on non-external underlying sources
+ * provided to the ReadableStream constructor in JavaScript.
+ *
+ * The given |reason| is the JS::Value that was passed as an argument to
+ * ReadableStream#cancel().
+ *
+ * The returned JS::Value will be used to resolve the Promise returned by
+ * ReadableStream#cancel().
+ */
+typedef Value
+(* CancelReadableStreamCallback)(JSContext* cx, HandleObject stream,
+                                 void* underlyingSource, uint8_t flags, HandleValue reason);
+
+/**
+ * Invoked in reaction to a ReadableStream with an embedding-provided
+ * underlying source being closed.
+ */
+typedef void
+(* ReadableStreamClosedCallback)(JSContext* cx, HandleObject stream, void* underlyingSource,
+                                 uint8_t flags);
+
+/**
+ * Invoked in reaction to a ReadableStream with an embedding-provided
+ * underlying source being errored with the
+ * given reason.
+ */
+typedef void
+(* ReadableStreamErroredCallback)(JSContext* cx, HandleObject stream, void* underlyingSource,
+                                  uint8_t flags, HandleValue reason);
+
+/**
+ * Invoked in reaction to a ReadableStream with an embedding-provided
+ * underlying source being finalized. Only the underlying source is passed
+ * as an argument, while the ReadableStream itself is not to prevent the
+ * embedding from operating on a JSObject that might not be in a valid state
+ * anymore.
+ *
+ * Note: the ReadableStream might be finalized on a background thread. That
+ * means this callback might be invoked from an arbitrary thread, which the
+ * embedding must be able to handle.
+ */
+typedef void
+(* ReadableStreamFinalizeCallback)(void* underlyingSource, uint8_t flags);
+
+/**
+ * Sets runtime-wide callbacks to use for interacting with embedding-provided
+ * hooks for operating on ReadableStream instances.
+ *
+ * See the documentation for the individual callback types for details.
+ */
+extern JS_PUBLIC_API(void)
+SetReadableStreamCallbacks(JSContext* cx,
+                           RequestReadableStreamDataCallback dataRequestCallback,
+                           WriteIntoReadRequestBufferCallback writeIntoReadRequestCallback,
+                           CancelReadableStreamCallback cancelCallback,
+                           ReadableStreamClosedCallback closedCallback,
+                           ReadableStreamErroredCallback erroredCallback,
+                           ReadableStreamFinalizeCallback finalizeCallback);
+
+extern JS_PUBLIC_API(bool)
+HasReadableStreamCallbacks(JSContext* cx);
+
+/**
+ * Returns a new instance of the ReadableStream builtin class in the current
+ * compartment, configured as a default stream.
+ * If a |proto| is passed, that gets set as the instance's [[Prototype]]
+ * instead of the original value of |ReadableStream.prototype|.
+ */
+extern JS_PUBLIC_API(JSObject*)
+NewReadableDefaultStreamObject(JSContext* cx, HandleObject underlyingSource = nullptr,
+                               HandleFunction size = nullptr, double highWaterMark = 1,
+                               HandleObject proto = nullptr);
+
+/**
+ * Returns a new instance of the ReadableStream builtin class in the current
+ * compartment, configured as a byte stream.
+ * If a |proto| is passed, that gets set as the instance's [[Prototype]]
+ * instead of the original value of |ReadableStream.prototype|.
+ */
+extern JS_PUBLIC_API(JSObject*)
+NewReadableByteStreamObject(JSContext* cx, HandleObject underlyingSource = nullptr,
+                            double highWaterMark = 0, HandleObject proto = nullptr);
+
+/**
+ * Returns a new instance of the ReadableStream builtin class in the current
+ * compartment, with the right slot layout. If a |proto| is passed, that gets
+ * set as the instance's [[Prototype]] instead of the original value of
+ * |ReadableStream.prototype|.
+ *
+ * The instance is optimized for operating as a byte stream backed by an
+ * embedding-provided underlying source, using the callbacks set via
+ * |JS::SetReadableStreamCallbacks|.
+ *
+ * The given |flags| will be passed to all applicable callbacks and can be
+ * used to disambiguate between different types of stream sources the
+ * embedding might support.
+ *
+ * Note: the embedding is responsible for ensuring that the pointer to the
+ * underlying source stays valid as long as the stream can be read from.
+ * The underlying source can be freed if the tree is canceled or errored.
+ * It can also be freed if the stream is destroyed. The embedding is notified
+ * of that using ReadableStreamFinalizeCallback.
+ */
+extern JS_PUBLIC_API(JSObject*)
+NewReadableExternalSourceStreamObject(JSContext* cx, void* underlyingSource,
+                                      uint8_t flags = 0, HandleObject proto = nullptr);
+
+/**
+ * Returns the flags that were passed to NewReadableExternalSourceStreamObject
+ * when creating the given stream.
+ *
+ * Asserts that the given stream has an embedding-provided underlying source.
+ */
+extern JS_PUBLIC_API(uint8_t)
+ReadableStreamGetEmbeddingFlags(const JSObject* stream);
+
+/**
+ * Returns the embedding-provided underlying source of the given |stream|.
+ *
+ * Can be used to optimize operations if both the underlying source and the
+ * intended sink are embedding-provided. In that case it might be
+ * preferrable to pipe data directly from source to sink without interacting
+ * with the stream at all.
+ *
+ * Locks the stream until ReadableStreamReleaseExternalUnderlyingSource is
+ * called.
+ *
+ * Throws an exception if the stream is locked, i.e. if a reader has been
+ * acquired for the stream, or if ReadableStreamGetExternalUnderlyingSource
+ * has been used previously without releasing the external source again.
+ *
+ * Throws an exception if the stream isn't readable, i.e if it is errored or
+ * closed. This is different from ReadableStreamGetReader because we don't
+ * have a Promise to resolve/reject, which a reader provides.
+ *
+ * Asserts that the stream has an embedding-provided underlying source.
+ */
+extern JS_PUBLIC_API(bool)
+ReadableStreamGetExternalUnderlyingSource(JSContext* cx, HandleObject stream, void** source);
+
+/**
+ * Releases the embedding-provided underlying source of the given |stream|,
+ * returning the stream into an unlocked state.
+ *
+ * Asserts that the stream was locked through
+ * ReadableStreamGetExternalUnderlyingSource.
+ *
+ * Asserts that the stream has an embedding-provided underlying source.
+ */
+extern JS_PUBLIC_API(void)
+ReadableStreamReleaseExternalUnderlyingSource(JSObject* stream);
+
+/**
+ * Update the amount of data available at the underlying source of the given
+ * |stream|.
+ *
+ * Can only be used for streams with an embedding-provided underlying source.
+ * The JS engine will use the given value to satisfy read requests for the
+ * stream by invoking the JS::WriteIntoReadRequestBuffer callback.
+ */
+extern JS_PUBLIC_API(bool)
+ReadableStreamUpdateDataAvailableFromSource(JSContext* cx, HandleObject stream,
+                                            uint32_t availableData);
+
+/**
+ * Returns true if the given object is an unwrapped ReadableStream object,
+ * false otherwise.
+ */
+extern JS_PUBLIC_API(bool)
+IsReadableStream(const JSObject* obj);
+
+/**
+ * Returns true if the given object is an unwrapped
+ * ReadableStreamDefaultReader or ReadableStreamBYOBReader object,
+ * false otherwise.
+ */
+extern JS_PUBLIC_API(bool)
+IsReadableStreamReader(const JSObject* obj);
+
+/**
+ * Returns true if the given object is an unwrapped
+ * ReadableStreamDefaultReader object, false otherwise.
+ */
+extern JS_PUBLIC_API(bool)
+IsReadableStreamDefaultReader(const JSObject* obj);
+
+/**
+ * Returns true if the given object is an unwrapped
+ * ReadableStreamBYOBReader object, false otherwise.
+ */
+extern JS_PUBLIC_API(bool)
+IsReadableStreamBYOBReader(const JSObject* obj);
+
+enum class ReadableStreamMode {
+    Default,
+    Byte,
+    ExternalSource
+};
+
+/**
+ * Returns the stream's ReadableStreamMode. If the mode is |Byte| or
+ * |ExternalSource|, it's possible to acquire a BYOB reader for more optimized
+ * operations.
+ *
+ * Asserts that |stream| is an unwrapped ReadableStream instance.
+ */
+extern JS_PUBLIC_API(ReadableStreamMode)
+ReadableStreamGetMode(const JSObject* stream);
+
+enum class ReadableStreamReaderMode {
+    Default,
+    BYOB
+};
+
+/**
+ * Returns true if the given ReadableStream is readable, false if not.
+ *
+ * Asserts that |stream| is an unwrapped ReadableStream instance.
+ */
+extern JS_PUBLIC_API(bool)
+ReadableStreamIsReadable(const JSObject* stream);
+
+/**
+ * Returns true if the given ReadableStream is locked, false if not.
+ *
+ * Asserts that |stream| is an unwrapped ReadableStream instance.
+ */
+extern JS_PUBLIC_API(bool)
+ReadableStreamIsLocked(const JSObject* stream);
+
+/**
+ * Returns true if the given ReadableStream is disturbed, false if not.
+ *
+ * Asserts that |stream| is an ReadableStream instance.
+ */
+extern JS_PUBLIC_API(bool)
+ReadableStreamIsDisturbed(const JSObject* stream);
+
+/**
+ * Cancels the given ReadableStream with the given reason and returns a
+ * Promise resolved according to the result.
+ *
+ * Asserts that |stream| is an unwrapped ReadableStream instance.
+ */
+extern JS_PUBLIC_API(JSObject*)
+ReadableStreamCancel(JSContext* cx, HandleObject stream, HandleValue reason);
+
+/**
+ * Creates a reader of the type specified by the mode option and locks the
+ * stream to the new reader.
+ *
+ * Asserts that |stream| is an unwrapped ReadableStream instance.
+ */
+extern JS_PUBLIC_API(JSObject*)
+ReadableStreamGetReader(JSContext* cx, HandleObject stream, ReadableStreamReaderMode mode);
+
+/**
+ * Tees the given ReadableStream and stores the two resulting streams in
+ * outparams. Returns false if the operation fails, e.g. because the stream is
+ * locked.
+ *
+ * Asserts that |stream| is an unwrapped ReadableStream instance.
+ */
+extern JS_PUBLIC_API(bool)
+ReadableStreamTee(JSContext* cx, HandleObject stream,
+                  MutableHandleObject branch1Stream, MutableHandleObject branch2Stream);
+
+/**
+ * Retrieves the desired combined size of additional chunks to fill the given
+ * ReadableStream's queue. Stores the result in |value| and sets |hasValue| to
+ * true on success, returns false on failure.
+ *
+ * If the stream is errored, the call will succeed but no value will be stored
+ * in |value| and |hasValue| will be set to false.
+ *
+ * Note: This is semantically equivalent to the |desiredSize| getter on
+ * the stream controller's prototype in JS. We expose it with the stream
+ * itself as a target for simplicity.
+ *
+ * Asserts that |stream| is an unwrapped ReadableStream instance.
+ */
+extern JS_PUBLIC_API(void)
+ReadableStreamGetDesiredSize(JSObject* stream, bool* hasValue, double* value);
+
+/**
+ * Closes the given ReadableStream.
+ *
+ * Throws a TypeError and returns false if the closing operation fails.
+ *
+ * Note: This is semantically equivalent to the |close| method on
+ * the stream controller's prototype in JS. We expose it with the stream
+ * itself as a target for simplicity.
+ *
+ * Asserts that |stream| is an unwrapped ReadableStream instance.
+ */
+extern JS_PUBLIC_API(bool)
+ReadableStreamClose(JSContext* cx, HandleObject stream);
+
+/**
+ * Returns true if the given ReadableStream reader is locked, false otherwise.
+ *
+ * Asserts that |reader| is an unwrapped ReadableStreamDefaultReader or
+ * ReadableStreamBYOBReader instance.
+ */
+extern JS_PUBLIC_API(bool)
+ReadableStreamReaderIsClosed(const JSObject* reader);
+
+/**
+ * Enqueues the given chunk in the given ReadableStream.
+ *
+ * Throws a TypeError and returns false if the enqueing operation fails.
+ *
+ * Note: This is semantically equivalent to the |enqueue| method on
+ * the stream controller's prototype in JS. We expose it with the stream
+ * itself as a target for simplicity.
+ *
+ * If the ReadableStream has an underlying byte source, the given chunk must
+ * be a typed array or a DataView. Consider using
+ * ReadableByteStreamEnqueueBuffer.
+ *
+ * Asserts that |stream| is an unwrapped ReadableStream instance.
+ */
+extern JS_PUBLIC_API(bool)
+ReadableStreamEnqueue(JSContext* cx, HandleObject stream, HandleValue chunk);
+
+/**
+ * Enqueues the given buffer as a chunk in the given ReadableStream.
+ *
+ * Throws a TypeError and returns false if the enqueing operation fails.
+ *
+ * Note: This is semantically equivalent to the |enqueue| method on
+ * the stream controller's prototype in JS. We expose it with the stream
+ * itself as a target for simplicity. Additionally, the JS version only
+ * takes typed arrays and ArrayBufferView instances as arguments, whereas
+ * this takes an ArrayBuffer, obviating the need to wrap it into a typed
+ * array.
+ *
+ * Asserts that |stream| is an unwrapped ReadableStream instance and |buffer|
+ * an unwrapped ArrayBuffer instance.
+ */
+extern JS_PUBLIC_API(bool)
+ReadableByteStreamEnqueueBuffer(JSContext* cx, HandleObject stream, HandleObject buffer);
+
+/**
+ * Errors the given ReadableStream, causing all future interactions to fail
+ * with the given error value.
+ *
+ * Throws a TypeError and returns false if the erroring operation fails.
+ *
+ * Note: This is semantically equivalent to the |error| method on
+ * the stream controller's prototype in JS. We expose it with the stream
+ * itself as a target for simplicity.
+ *
+ * Asserts that |stream| is an unwrapped ReadableStream instance.
+ */
+extern JS_PUBLIC_API(bool)
+ReadableStreamError(JSContext* cx, HandleObject stream, HandleValue error);
+
+/**
+ * Cancels the given ReadableStream reader's associated stream.
+ *
+ * Throws a TypeError and returns false if the given reader isn't active.
+ *
+ * Asserts that |reader| is an unwrapped ReadableStreamDefaultReader or
+ * ReadableStreamBYOBReader instance.
+ */
+extern JS_PUBLIC_API(bool)
+ReadableStreamReaderCancel(JSContext* cx, HandleObject reader, HandleValue reason);
+
+/**
+ * Cancels the given ReadableStream reader's associated stream.
+ *
+ * Throws a TypeError and returns false if the given reader has pending
+ * read or readInto (for default or byob readers, respectively) requests.
+ *
+ * Asserts that |reader| is an unwrapped ReadableStreamDefaultReader or
+ * ReadableStreamBYOBReader instance.
+ */
+extern JS_PUBLIC_API(bool)
+ReadableStreamReaderReleaseLock(JSContext* cx, HandleObject reader);
+
+/**
+ * Requests a read from the reader's associated ReadableStream and returns the
+ * resulting PromiseObject.
+ *
+ * Returns a Promise that's resolved with the read result once available or
+ * rejected immediately if the stream is errored or the operation failed.
+ *
+ * Asserts that |reader| is an unwrapped ReadableStreamDefaultReader instance.
+ */
+extern JS_PUBLIC_API(JSObject*)
+ReadableStreamDefaultReaderRead(JSContext* cx, HandleObject reader);
+
+/**
+ * Requests a read from the reader's associated ReadableStream into the given
+ * ArrayBufferView and returns the resulting PromiseObject.
+ *
+ * Returns a Promise that's resolved with the read result once available or
+ * rejected immediately if the stream is errored or the operation failed.
+ *
+ * Asserts that |reader| is an unwrapped ReadableStreamDefaultReader and
+ * |view| an unwrapped typed array or DataView instance.
+ */
+extern JS_PUBLIC_API(JSObject*)
+ReadableStreamBYOBReaderRead(JSContext* cx, HandleObject reader, HandleObject view);
+
+} // namespace JS
+
+#endif // js_Realm_h
--- a/js/src/build/Makefile.in
+++ b/js/src/build/Makefile.in
@@ -29,23 +29,23 @@ check-vanilla-allocations-aggressive:
 	$(PYTHON) $(topsrcdir)/config/check_vanilla_allocations.py --aggressive $(REAL_LIBRARY)
 
 ifeq ($(OS_ARCH),Linux)
 ifeq (,$(filter -flto,$(CFLAGS) $(CXXFLAGS) $(LDFLAGS)))
 check:: check-vanilla-allocations
 endif
 endif
 
-SCRIPTS = $(JS_CONFIG_NAME)
-
 $(LIBRARY_NAME).pc: js.pc
 	cp $^ $@
 
+# Install versioned file, for parallel installability in Linux distributions
 install:: $(LIBRARY_NAME).pc
-	$(SYSINSTALL) $^ $(DESTDIR)$(libdir)/pkgconfig
+	cp $^ $(JS_LIBRARY_NAME).pc
+	$(SYSINSTALL) $(JS_LIBRARY_NAME).pc $(DESTDIR)$(libdir)/pkgconfig
 
 install:: ../js-config.h
 	$(SYSINSTALL) $^ $(DESTDIR)$(includedir)
 
 ######################################################
 # BEGIN SpiderMonkey header installation
 #
 # Mozilla/Gecko/Firefox mostly doesn't concern itself with defining a sensible
@@ -71,18 +71,20 @@ install:: ../js-config.h
 
 install::
 	$(call py_action,process_install_manifest,--no-remove --no-symlinks $(DESTDIR)$(includedir) $(DEPTH)/_build_manifests/install/dist_include)
 
 #
 # END SpiderMonkey header installation
 #############################################
 
-install:: $(SCRIPTS)
-	$(SYSINSTALL) $^ $(DESTDIR)$(bindir)
+# Install versioned script, for parallel installability in Linux distributions
+install:: js-config
+	cp $^ js$(MOZJS_MAJOR_VERSION)-config
+	$(SYSINSTALL) js$(MOZJS_MAJOR_VERSION)-config $(DESTDIR)$(bindir)
 
 install:: $(REAL_LIBRARY) $(SHARED_LIBRARY) $(IMPORT_LIBRARY)
 ifneq (,$(REAL_LIBRARY))
 	$(SYSINSTALL) $(REAL_LIBRARY) $(DESTDIR)$(libdir)
 	mv -f $(DESTDIR)$(libdir)/$(REAL_LIBRARY) $(subst $(STATIC_LIBRARY_NAME),$(LIBRARY_NAME),$(DESTDIR)$(libdir)/$(REAL_LIBRARY))
 endif
 ifneq (,$(SHARED_LIBRARY))
 	$(SYSINSTALL) $(SHARED_LIBRARY) $(DESTDIR)$(libdir)
--- a/js/src/builtin/Intl.js
+++ b/js/src/builtin/Intl.js
@@ -102,21 +102,21 @@ function removeUnicodeExtensions(locale)
     // the concatenation.
     var pos = callFunction(std_String_indexOf, locale, "-x-");
     if (pos < 0)
         pos = locale.length;
 
     var left = callFunction(String_substring, locale, 0, pos);
     var right = callFunction(String_substring, locale, pos);
 
-    var extensions;
     var unicodeLocaleExtensionSequenceRE = getUnicodeLocaleExtensionSequenceRE();
-    while ((extensions = regexp_exec_no_statics(unicodeLocaleExtensionSequenceRE, left)) !== null) {
-        left = StringReplaceString(left, extensions[0], "");
-        unicodeLocaleExtensionSequenceRE.lastIndex = 0;
+    var extensions = regexp_exec_no_statics(unicodeLocaleExtensionSequenceRE, left);
+    if (extensions !== null) {
+        left = callFunction(String_substring, left, 0, extensions.index) +
+               callFunction(String_substring, left, extensions.index + extensions[0].length);
     }
 
     var combined = left + right;
     assert(IsStructurallyValidLanguageTag(combined), "recombination produced an invalid language tag");
     assert(function() {
         var uindex = callFunction(std_String_indexOf, combined, "-u-");
         if (uindex < 0)
             return true;
--- a/js/src/builtin/ModuleObject.cpp
+++ b/js/src/builtin/ModuleObject.cpp
@@ -6,16 +6,18 @@
 
 #include "builtin/ModuleObject.h"
 
 #include "builtin/SelfHostingDefines.h"
 #include "frontend/ParseNode.h"
 #include "frontend/SharedContext.h"
 #include "gc/Policy.h"
 #include "gc/Tracer.h"
+#include "vm/AsyncFunction.h"
+#include "vm/AsyncIteration.h"
 
 #include "jsobjinlines.h"
 #include "jsscriptinlines.h"
 
 using namespace js;
 using namespace js::frontend;
 
 static_assert(MODULE_STATE_FAILED < MODULE_STATE_PARSED &&
@@ -881,25 +883,34 @@ ModuleObject::instantiateFunctionDeclara
     FunctionDeclarationVector* funDecls = self->functionDeclarations();
     if (!funDecls) {
         JS_ReportErrorASCII(cx, "Module function declarations have already been instantiated");
         return false;
     }
 
     RootedModuleEnvironmentObject env(cx, &self->initialEnvironment());
     RootedFunction fun(cx);
+    RootedObject obj(cx);
     RootedValue value(cx);
 
     for (const auto& funDecl : *funDecls) {
         fun = funDecl.fun;
-        RootedObject obj(cx, Lambda(cx, fun, env));
+        obj = Lambda(cx, fun, env);
         if (!obj)
             return false;
 
-        value = ObjectValue(*fun);
+        if (fun->isAsync()) {
+            if (fun->isStarGenerator()) {
+                obj = WrapAsyncGenerator(cx, obj.as<JSFunction>());
+            } else {
+                obj = WrapAsyncFunction(cx, obj.as<JSFunction>());
+            }
+        }
+
+        value = ObjectValue(*obj);
         if (!SetProperty(cx, env, funDecl.name->asPropertyName(), value))
             return false;
     }
 
     js_delete(funDecls);
     self->setReservedSlot(FunctionDeclarationsSlot, UndefinedValue());
     return true;
 }
--- a/js/src/builtin/Object.cpp
+++ b/js/src/builtin/Object.cpp
@@ -578,33 +578,33 @@ js::obj_toString(JSContext* cx, unsigned
 
     // Step 16.
     if (!tag.isString()) {
         // Non-standard (bug 1277801): Use ClassName as a fallback in the interim
         if (!builtinTag) {
             const char* className = GetObjectClassName(cx, obj);
             StringBuffer sb(cx);
             if (!sb.append("[object ") || !sb.append(className, strlen(className)) ||
-                !sb.append("]"))
+                !sb.append(']'))
             {
                 return false;
             }
 
             builtinTag = sb.finishAtom();
             if (!builtinTag)
                 return false;
         }
 
         args.rval().setString(builtinTag);
         return true;
     }
 
     // Step 17.
     StringBuffer sb(cx);
-    if (!sb.append("[object ") || !sb.append(tag.toString()) || !sb.append("]"))
+    if (!sb.append("[object ") || !sb.append(tag.toString()) || !sb.append(']'))
         return false;
 
     JSString* str = sb.finishAtom();
     if (!str)
         return false;
 
     args.rval().setString(str);
     return true;
new file mode 100644
--- /dev/null
+++ b/js/src/builtin/Stream.cpp
@@ -0,0 +1,5493 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*-
+ * vim: set ts=8 sts=4 et sw=4 tw=99:
+ * 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/. */
+
+#include "builtin/Stream.h"
+
+#include "js/Stream.h"
+
+#include "jscntxt.h"
+
+#include "gc/Heap.h"
+#include "vm/SelfHosting.h"
+
+#include "jsobjinlines.h"
+
+#include "vm/NativeObject-inl.h"
+
+using namespace js;
+
+enum StreamSlots {
+    StreamSlot_Controller,
+    StreamSlot_Reader,
+    StreamSlot_State,
+    StreamSlot_StoredError,
+    StreamSlotCount
+};
+
+enum ReaderSlots {
+    ReaderSlot_Stream,
+    ReaderSlot_Requests,
+    ReaderSlot_ClosedPromise,
+    ReaderSlotCount,
+};
+
+enum ReaderType {
+    ReaderType_Default,
+    ReaderType_BYOB
+};
+
+// ReadableStreamDefaultController and ReadableByteStreamController are both
+// queue containers and must have these slots at identical offsets.
+enum QueueContainerSlots {
+    QueueContainerSlot_Queue,
+    QueueContainerSlot_TotalSize,
+    QueueContainerSlotCount
+};
+
+// These slots are identical between the two types of ReadableStream
+// controllers.
+enum ControllerSlots {
+    ControllerSlot_Stream = QueueContainerSlotCount,
+    ControllerSlot_UnderlyingSource,
+    ControllerSlot_StrategyHWM,
+    ControllerSlot_Flags,
+    ControllerSlotCount
+};
+
+enum DefaultControllerSlots {
+    DefaultControllerSlot_StrategySize = ControllerSlotCount,
+    DefaultControllerSlotCount
+};
+
+enum ByteControllerSlots {
+    ByteControllerSlot_BYOBRequest = ControllerSlotCount,
+    ByteControllerSlot_PendingPullIntos,
+    ByteControllerSlot_AutoAllocateSize,
+    ByteControllerSlotCount
+};
+
+enum ControllerFlags {
+    ControllerFlag_Started        = 1 << 0,
+    ControllerFlag_Pulling        = 1 << 1,
+    ControllerFlag_PullAgain      = 1 << 2,
+    ControllerFlag_CloseRequested = 1 << 3,
+    ControllerFlag_TeeBranch      = 1 << 4,
+    ControllerFlag_TeeBranch1     = 1 << 5,
+    ControllerFlag_TeeBranch2     = 1 << 6,
+    ControllerFlag_ExternalSource = 1 << 7,
+    ControllerFlag_SourceLocked   = 1 << 8,
+};
+
+// Offset at which embedding flags are stored.
+constexpr uint8_t ControllerEmbeddingFlagsOffset = 24;
+
+enum BYOBRequestSlots {
+    BYOBRequestSlot_Controller,
+    BYOBRequestSlot_View,
+    BYOBRequestSlotCount
+};
+
+template<class T>
+MOZ_ALWAYS_INLINE bool
+Is(const HandleValue v)
+{
+    return v.isObject() && v.toObject().is<T>();
+}
+
+#ifdef DEBUG
+static bool
+IsReadableStreamController(const JSObject* controller)
+{
+    return controller->is<ReadableStreamDefaultController>() ||
+           controller->is<ReadableByteStreamController>();
+}
+#endif // DEBUG
+
+static inline uint32_t
+ControllerFlags(const NativeObject* controller)
+{
+    MOZ_ASSERT(IsReadableStreamController(controller));
+    return controller->getFixedSlot(ControllerSlot_Flags).toInt32();
+}
+
+static inline void
+AddControllerFlags(NativeObject* controller, uint32_t flags)
+{
+    MOZ_ASSERT(IsReadableStreamController(controller));
+    controller->setFixedSlot(ControllerSlot_Flags,
+                             Int32Value(ControllerFlags(controller) | flags));
+}
+
+static inline void
+RemoveControllerFlags(NativeObject* controller, uint32_t flags)
+{
+    MOZ_ASSERT(IsReadableStreamController(controller));
+    controller->setFixedSlot(ControllerSlot_Flags,
+                             Int32Value(ControllerFlags(controller) & ~flags));
+}
+
+static inline uint32_t
+StreamState(const ReadableStream* stream)
+{
+    return stream->getFixedSlot(StreamSlot_State).toInt32();
+}
+
+static inline void
+SetStreamState(ReadableStream* stream, uint32_t state)
+{
+    MOZ_ASSERT_IF(stream->disturbed(), state & ReadableStream::Disturbed);
+    MOZ_ASSERT_IF(stream->closed() || stream->errored(), !(state & ReadableStream::Readable));
+    stream->setFixedSlot(StreamSlot_State, Int32Value(state));
+}
+
+bool
+ReadableStream::readable() const
+{
+    return StreamState(this) & Readable;
+}
+
+bool
+ReadableStream::closed() const
+{
+    return StreamState(this) & Closed;
+}
+
+bool
+ReadableStream::errored() const
+{
+    return StreamState(this) & Errored;
+}
+
+bool
+ReadableStream::disturbed() const
+{
+    return StreamState(this) & Disturbed;
+}
+
+inline static bool
+ReaderHasStream(const NativeObject* reader)
+{
+    MOZ_ASSERT(JS::IsReadableStreamReader(reader));
+    return !reader->getFixedSlot(ReaderSlot_Stream).isUndefined();
+}
+
+bool
+js::ReadableStreamReaderIsClosed(const JSObject* reader)
+{
+    return !ReaderHasStream(&reader->as<NativeObject>());
+}
+
+inline static MOZ_MUST_USE ReadableStream*
+StreamFromController(const NativeObject* controller)
+{
+    MOZ_ASSERT(IsReadableStreamController(controller));
+    return &controller->getFixedSlot(ControllerSlot_Stream).toObject().as<ReadableStream>();
+}
+
+inline static MOZ_MUST_USE NativeObject*
+ControllerFromStream(const ReadableStream* stream)
+{
+    Value controllerVal = stream->getFixedSlot(StreamSlot_Controller);
+    MOZ_ASSERT(IsReadableStreamController(&controllerVal.toObject()));
+    return &controllerVal.toObject().as<NativeObject>();
+}
+
+inline static bool
+HasController(const ReadableStream* stream)
+{
+    return !stream->getFixedSlot(StreamSlot_Controller).isUndefined();
+}
+
+JS::ReadableStreamMode
+ReadableStream::mode() const
+{
+    NativeObject* controller = ControllerFromStream(this);
+    if (controller->is<ReadableStreamDefaultController>())
+        return JS::ReadableStreamMode::Default;
+    return controller->as<ReadableByteStreamController>().hasExternalSource()
+           ? JS::ReadableStreamMode::ExternalSource
+           : JS::ReadableStreamMode::Byte;
+}
+
+inline static MOZ_MUST_USE ReadableStream*
+StreamFromReader(const NativeObject* reader)
+{
+    MOZ_ASSERT(ReaderHasStream(reader));
+    return &reader->getFixedSlot(ReaderSlot_Stream).toObject().as<ReadableStream>();
+}
+
+inline static MOZ_MUST_USE NativeObject*
+ReaderFromStream(const NativeObject* stream)
+{
+    Value readerVal = stream->getFixedSlot(StreamSlot_Reader);
+    MOZ_ASSERT(JS::IsReadableStreamReader(&readerVal.toObject()));
+    return &readerVal.toObject().as<NativeObject>();
+}
+
+inline static bool
+HasReader(const ReadableStream* stream)
+{
+    return !stream->getFixedSlot(StreamSlot_Reader).isUndefined();
+}
+
+inline static MOZ_MUST_USE JSFunction*
+NewHandler(JSContext *cx, Native handler, HandleObject target)
+{
+    RootedAtom funName(cx, cx->names().empty);
+    RootedFunction handlerFun(cx, NewNativeFunction(cx, handler, 0, funName,
+                                                    gc::AllocKind::FUNCTION_EXTENDED,
+                                                    GenericObject));
+    if (!handlerFun)
+        return nullptr;
+    handlerFun->setExtendedSlot(0, ObjectValue(*target));
+    return handlerFun;
+}
+
+template<class T>
+inline static MOZ_MUST_USE T*
+TargetFromHandler(JSObject& handler)
+{
+    return &handler.as<JSFunction>().getExtendedSlot(0).toObject().as<T>();
+}
+
+inline static MOZ_MUST_USE bool
+ResetQueue(JSContext* cx, HandleNativeObject container);
+
+inline static MOZ_MUST_USE bool
+InvokeOrNoop(JSContext* cx, HandleValue O, HandlePropertyName P, HandleValue arg,
+             MutableHandleValue rval);
+
+static MOZ_MUST_USE JSObject*
+PromiseInvokeOrNoop(JSContext* cx, HandleValue O, HandlePropertyName P, HandleValue arg);
+
+static MOZ_MUST_USE JSObject*
+PromiseRejectedWithPendingError(JSContext* cx) {
+    // Not much we can do about uncatchable exceptions, just bail.
+    RootedValue exn(cx);
+    if (!GetAndClearException(cx, &exn))
+        return nullptr;
+    return PromiseObject::unforgeableReject(cx, exn);
+}
+
+static bool
+ReportArgTypeError(JSContext* cx, const char* funName, const char* expectedType,
+                   HandleValue arg)
+{
+    UniqueChars bytes = DecompileValueGenerator(cx, JSDVG_SEARCH_STACK, arg, nullptr);
+    if (!bytes)
+        return false;
+
+    return JS_ReportErrorFlagsAndNumberLatin1(cx, JSREPORT_ERROR, GetErrorMessage,
+                                              nullptr, JSMSG_NOT_EXPECTED_TYPE,
+                                              funName, expectedType, bytes.get());
+}
+
+static MOZ_MUST_USE bool
+RejectWithPendingError(JSContext* cx, Handle<PromiseObject*> promise) {
+    // Not much we can do about uncatchable exceptions, just bail.
+    RootedValue exn(cx);
+    if (!GetAndClearException(cx, &exn))
+        return false;
+    return PromiseObject::reject(cx, promise, exn);
+}
+
+static MOZ_MUST_USE bool
+ReturnPromiseRejectedWithPendingError(JSContext* cx, const CallArgs& args)
+{
+    JSObject* promise = PromiseRejectedWithPendingError(cx);
+    if (!promise)
+        return false;
+
+    args.rval().setObject(*promise);
+    return true;
+}
+
+static MOZ_MUST_USE bool
+RejectNonGenericMethod(JSContext* cx, const CallArgs& args,
+                       const char* className, const char* methodName)
+{
+    ReportValueError3(cx, JSMSG_INCOMPATIBLE_PROTO, JSDVG_SEARCH_STACK, args.thisv(),
+                      nullptr, className, methodName);
+
+    return ReturnPromiseRejectedWithPendingError(cx, args);
+}
+
+inline static MOZ_MUST_USE NativeObject*
+SetNewList(JSContext* cx, HandleNativeObject container, uint32_t slot)
+{
+    NativeObject* list = NewObjectWithNullTaggedProto<PlainObject>(cx);
+    if (!list)
+        return nullptr;
+    container->setFixedSlot(slot, ObjectValue(*list));
+    return list;
+}
+
+inline static MOZ_MUST_USE bool
+AppendToList(JSContext* cx, HandleNativeObject list, HandleValue value)
+{
+    uint32_t length = list->getDenseInitializedLength();
+
+    if (!list->ensureElements(cx, length + 1))
+        return false;
+
+    list->ensureDenseInitializedLength(cx, length, 1);
+    list->setDenseElement(length, value);
+
+    return true;
+}
+
+template<class T>
+inline static MOZ_MUST_USE T*
+PeekList(NativeObject* list)
+{
+    MOZ_ASSERT(list->getDenseInitializedLength() > 0);
+    return &list->getDenseElement(0).toObject().as<T>();
+}
+
+template<class T>
+inline static MOZ_MUST_USE T*
+ShiftFromList(JSContext* cx, HandleNativeObject list)
+{
+    uint32_t length = list->getDenseInitializedLength();
+    MOZ_ASSERT(length > 0);
+
+    Rooted<T*> entry(cx, &list->getDenseElement(0).toObject().as<T>());
+    if (!list->tryShiftDenseElements(1)) {
+        list->moveDenseElements(0, 1, length - 1);
+        list->shrinkElements(cx, length - 1);
+    }
+
+    list->setDenseInitializedLength(length - 1);
+
+    return entry;
+}
+
+class ByteStreamChunk : public NativeObject
+{
+  private:
+    enum Slots {
+        Slot_Buffer = 0,
+        Slot_ByteOffset,
+        Slot_ByteLength,
+        SlotCount
+    };
+
+  public:
+    static const Class class_;
+
+    ArrayBufferObject* buffer() {
+        return &getFixedSlot(Slot_Buffer).toObject().as<ArrayBufferObject>();
+    }
+    uint32_t byteOffset() { return getFixedSlot(Slot_ByteOffset).toInt32(); }
+    void SetByteOffset(uint32_t offset) {
+        setFixedSlot(Slot_ByteOffset, Int32Value(offset));
+    }
+    uint32_t byteLength() { return getFixedSlot(Slot_ByteLength).toInt32(); }
+    void SetByteLength(uint32_t length) {
+        setFixedSlot(Slot_ByteLength, Int32Value(length));
+    }
+
+    static ByteStreamChunk* create(JSContext* cx, HandleObject buffer, uint32_t byteOffset,
+                                   uint32_t byteLength)
+   {
+        Rooted<ByteStreamChunk*> chunk(cx, NewObjectWithClassProto<ByteStreamChunk>(cx));
+        if (!chunk)
+            return nullptr;
+
+        chunk->setFixedSlot(Slot_Buffer, ObjectValue(*buffer));
+        chunk->setFixedSlot(Slot_ByteOffset, Int32Value(byteOffset));
+        chunk->setFixedSlot(Slot_ByteLength, Int32Value(byteLength));
+        return chunk;
+   }
+};
+
+const Class ByteStreamChunk::class_ = {
+    "ByteStreamChunk",
+    JSCLASS_HAS_RESERVED_SLOTS(SlotCount)
+};
+
+class PullIntoDescriptor : public NativeObject
+{
+  private:
+    enum Slots {
+        Slot_buffer,
+        Slot_ByteOffset,
+        Slot_ByteLength,
+        Slot_BytesFilled,
+        Slot_ElementSize,
+        Slot_Ctor,
+        Slot_ReaderType,
+        SlotCount
+    };
+  public:
+    static const Class class_;
+
+    ArrayBufferObject* buffer() {
+        return &getFixedSlot(Slot_buffer).toObject().as<ArrayBufferObject>();
+    }
+    void setBuffer(ArrayBufferObject* buffer) { setFixedSlot(Slot_buffer, ObjectValue(*buffer)); }
+    JSObject* ctor() { return getFixedSlot(Slot_Ctor).toObjectOrNull(); }
+    uint32_t byteOffset() const { return getFixedSlot(Slot_ByteOffset).toInt32(); }
+    uint32_t byteLength() const { return getFixedSlot(Slot_ByteLength).toInt32(); }
+    uint32_t bytesFilled() const { return getFixedSlot(Slot_BytesFilled).toInt32(); }
+    void setBytesFilled(int32_t bytes) { setFixedSlot(Slot_BytesFilled, Int32Value(bytes)); }
+    uint32_t elementSize() const { return getFixedSlot(Slot_ElementSize).toInt32(); }
+    uint32_t readerType() const { return getFixedSlot(Slot_ReaderType).toInt32(); }
+
+    static PullIntoDescriptor* create(JSContext* cx, HandleArrayBufferObject buffer,
+                                      uint32_t byteOffset, uint32_t byteLength,
+                                      uint32_t bytesFilled, uint32_t elementSize,
+                                      HandleObject ctor, uint32_t readerType)
+   {
+        Rooted<PullIntoDescriptor*> descriptor(cx, NewObjectWithClassProto<PullIntoDescriptor>(cx));
+        if (!descriptor)
+            return nullptr;
+
+        descriptor->setFixedSlot(Slot_buffer, ObjectValue(*buffer));
+        descriptor->setFixedSlot(Slot_Ctor, ObjectOrNullValue(ctor));
+        descriptor->setFixedSlot(Slot_ByteOffset, Int32Value(byteOffset));
+        descriptor->setFixedSlot(Slot_ByteLength, Int32Value(byteLength));
+        descriptor->setFixedSlot(Slot_BytesFilled, Int32Value(bytesFilled));
+        descriptor->setFixedSlot(Slot_ElementSize, Int32Value(elementSize));
+        descriptor->setFixedSlot(Slot_ReaderType, Int32Value(readerType));
+        return descriptor;
+   }
+};
+
+const Class PullIntoDescriptor::class_ = {
+    "PullIntoDescriptor",
+    JSCLASS_HAS_RESERVED_SLOTS(SlotCount)
+};
+
+class QueueEntry : public NativeObject
+{
+  private:
+    enum Slots {
+        Slot_Value = 0,
+        Slot_Size,
+        SlotCount
+    };
+
+  public:
+    static const Class class_;
+
+    Value value() { return getFixedSlot(Slot_Value); }
+    double size() { return getFixedSlot(Slot_Size).toNumber(); }
+
+    static QueueEntry* create(JSContext* cx, HandleValue value, double size)
+   {
+        Rooted<QueueEntry*> entry(cx, NewObjectWithClassProto<QueueEntry>(cx));
+        if (!entry)
+            return nullptr;
+
+        entry->setFixedSlot(Slot_Value, value);
+        entry->setFixedSlot(Slot_Size, NumberValue(size));
+
+        return entry;
+   }
+};
+
+const Class QueueEntry::class_ = {
+    "QueueEntry",
+    JSCLASS_HAS_RESERVED_SLOTS(SlotCount)
+};
+
+class TeeState : public NativeObject
+{
+  private:
+    enum Slots {
+        Slot_Flags = 0,
+        Slot_Reason1,
+        Slot_Reason2,
+        Slot_Promise,
+        Slot_Stream,
+        Slot_Branch1,
+        Slot_Branch2,
+        SlotCount
+    };
+
+    enum Flags
+    {
+        Flag_ClosedOrErrored = 1 << 0,
+        Flag_Canceled1 =       1 << 1,
+        Flag_Canceled2 =       1 << 2,
+        Flag_CloneForBranch2 = 1 << 3,
+    };
+    uint32_t flags() const { return getFixedSlot(Slot_Flags).toInt32(); }
+    void setFlags(uint32_t flags) { setFixedSlot(Slot_Flags, Int32Value(flags)); }
+
+  public:
+    static const Class class_;
+
+    bool cloneForBranch2() const { return flags() & Flag_CloneForBranch2; }
+
+    bool closedOrErrored() const { return flags() & Flag_ClosedOrErrored; }
+    void setClosedOrErrored() {
+        MOZ_ASSERT(!(flags() & Flag_ClosedOrErrored));
+        setFlags(flags() | Flag_ClosedOrErrored);
+    }
+
+    bool canceled1() const { return flags() & Flag_Canceled1; }
+    void setCanceled1(HandleValue reason) {
+        MOZ_ASSERT(!(flags() & Flag_Canceled1));
+        setFlags(flags() | Flag_Canceled1);
+        setFixedSlot(Slot_Reason1, reason);
+    }
+
+    bool canceled2() const { return flags() & Flag_Canceled2; }
+    void setCanceled2(HandleValue reason) {
+        MOZ_ASSERT(!(flags() & Flag_Canceled2));
+        setFlags(flags() | Flag_Canceled2);
+        setFixedSlot(Slot_Reason2, reason);
+    }
+
+    Value reason1() const {
+        MOZ_ASSERT(canceled1());
+        return getFixedSlot(Slot_Reason1);
+    }
+
+    Value reason2() const {
+        MOZ_ASSERT(canceled2());
+        return getFixedSlot(Slot_Reason2);
+    }
+
+    PromiseObject* promise() {
+        return &getFixedSlot(Slot_Promise).toObject().as<PromiseObject>();
+    }
+    ReadableStream* stream() {
+        return &getFixedSlot(Slot_Stream).toObject().as<ReadableStream>();
+    }
+    ReadableStreamDefaultReader* reader() {
+        return &ReaderFromStream(stream())->as<ReadableStreamDefaultReader>();
+    }
+
+    ReadableStreamDefaultController* branch1() {
+        ReadableStreamDefaultController* controller = &getFixedSlot(Slot_Branch1).toObject()
+                                                       .as<ReadableStreamDefaultController>();
+        MOZ_ASSERT(ControllerFlags(controller) & ControllerFlag_TeeBranch);
+        MOZ_ASSERT(ControllerFlags(controller) & ControllerFlag_TeeBranch1);
+        return controller;
+    }
+    void setBranch1(ReadableStreamDefaultController* controller) {
+        MOZ_ASSERT(ControllerFlags(controller) & ControllerFlag_TeeBranch);
+        MOZ_ASSERT(ControllerFlags(controller) & ControllerFlag_TeeBranch1);
+        setFixedSlot(Slot_Branch1, ObjectValue(*controller));
+    }
+
+    ReadableStreamDefaultController* branch2() {
+        ReadableStreamDefaultController* controller = &getFixedSlot(Slot_Branch2).toObject()
+                                                       .as<ReadableStreamDefaultController>();
+        MOZ_ASSERT(ControllerFlags(controller) & ControllerFlag_TeeBranch);
+        MOZ_ASSERT(ControllerFlags(controller) & ControllerFlag_TeeBranch2);
+        return controller;
+    }
+    void setBranch2(ReadableStreamDefaultController* controller) {
+        MOZ_ASSERT(ControllerFlags(controller) & ControllerFlag_TeeBranch);
+        MOZ_ASSERT(ControllerFlags(controller) & ControllerFlag_TeeBranch2);
+        setFixedSlot(Slot_Branch2, ObjectValue(*controller));
+    }
+
+    static TeeState* create(JSContext* cx, Handle<ReadableStream*> stream) {
+        Rooted<TeeState*> state(cx, NewObjectWithClassProto<TeeState>(cx));
+        if (!state)
+            return nullptr;
+
+        Rooted<PromiseObject*> promise(cx, PromiseObject::createSkippingExecutor(cx));
+        if (!promise)
+            return nullptr;
+
+        state->setFixedSlot(Slot_Flags, Int32Value(0));
+        state->setFixedSlot(Slot_Promise, ObjectValue(*promise));
+        state->setFixedSlot(Slot_Stream, ObjectValue(*stream));
+
+        return state;
+   }
+};
+
+const Class TeeState::class_ = {
+    "TeeState",
+    JSCLASS_HAS_RESERVED_SLOTS(SlotCount)
+};
+
+#define CLASS_SPEC(cls, nCtorArgs, nSlots, specFlags, classFlags, classOps) \
+const ClassSpec cls::classSpec_ = { \
+    GenericCreateConstructor<cls::constructor, nCtorArgs, gc::AllocKind::FUNCTION>, \
+    GenericCreatePrototype, \
+    nullptr, \
+    nullptr, \
+    cls##_methods, \
+    cls##_properties, \
+    nullptr, \
+    specFlags \
+}; \
+\
+const Class cls::class_ = { \
+    #cls, \
+    JSCLASS_HAS_RESERVED_SLOTS(nSlots) | \
+    JSCLASS_HAS_CACHED_PROTO(JSProto_##cls) | \
+    classFlags, \
+    classOps, \
+    &cls::classSpec_ \
+}; \
+\
+const Class cls::protoClass_ = { \
+    "object", \
+    JSCLASS_HAS_CACHED_PROTO(JSProto_##cls), \
+    JS_NULL_CLASS_OPS, \
+    &cls::classSpec_ \
+};
+
+// Streams spec, 3.2.3., steps 1-4.
+ReadableStream*
+ReadableStream::createStream(JSContext* cx, HandleObject proto /* = nullptr */)
+{
+    Rooted<ReadableStream*> stream(cx, NewObjectWithClassProto<ReadableStream>(cx, proto));
+    if (!stream)
+        return nullptr;
+
+    // Step 1: Set this.[[state]] to "readable".
+    // Step 2: Set this.[[reader]] and this.[[storedError]] to undefined (implicit).
+    // Step 3: Set this.[[disturbed]] to false (implicit).
+    // Step 4: Set this.[[readableStreamController]] to undefined (implicit).
+    stream->setFixedSlot(StreamSlot_State, Int32Value(Readable));
+
+    return stream;
+}
+
+static MOZ_MUST_USE ReadableStreamDefaultController*
+CreateReadableStreamDefaultController(JSContext* cx, Handle<ReadableStream*> stream,
+                                      HandleValue underlyingSource, HandleValue size,
+                                      HandleValue highWaterMarkVal);
+
+// Streams spec, 3.2.3., steps 1-4, 8.
+ReadableStream*
+ReadableStream::createDefaultStream(JSContext* cx, HandleValue underlyingSource,
+                                    HandleValue size, HandleValue highWaterMark,
+                                    HandleObject proto /* = nullptr */)
+{
+    // Steps 1-4.
+    Rooted<ReadableStream*> stream(cx, createStream(cx));
+    if (!stream)
+        return nullptr;
+
+    // Step 8.b: Set this.[[readableStreamController]] to
+    //           ? Construct(ReadableStreamDefaultController,
+    //                       « this, underlyingSource, size,
+    //                         highWaterMark »).
+    RootedObject controller(cx, CreateReadableStreamDefaultController(cx, stream,
+                                                                      underlyingSource,
+                                                                      size,
+                                                                      highWaterMark));
+    if (!controller)
+        return nullptr;
+
+    stream->setFixedSlot(StreamSlot_Controller, ObjectValue(*controller));
+
+    return stream;
+}
+
+static MOZ_MUST_USE ReadableByteStreamController*
+CreateReadableByteStreamController(JSContext* cx, Handle<ReadableStream*> stream,
+                                   HandleValue underlyingByteSource,
+                                   HandleValue highWaterMarkVal);
+
+// Streams spec, 3.2.3., steps 1-4, 7.
+ReadableStream*
+ReadableStream::createByteStream(JSContext* cx, HandleValue underlyingSource,
+                                 HandleValue highWaterMark, HandleObject proto /* = nullptr */)
+{
+    // Steps 1-4.
+    Rooted<ReadableStream*> stream(cx, createStream(cx, proto));
+    if (!stream)
+        return nullptr;
+
+    // Step 7.b: Set this.[[readableStreamController]] to
+    //           ? Construct(ReadableByteStreamController,
+    //                       « this, underlyingSource, highWaterMark »).
+    RootedObject controller(cx, CreateReadableByteStreamController(cx, stream,
+                                                                   underlyingSource,
+                                                                   highWaterMark));
+    if (!controller)
+        return nullptr;
+
+    stream->setFixedSlot(StreamSlot_Controller, ObjectValue(*controller));
+
+    return stream;
+}
+
+static MOZ_MUST_USE ReadableByteStreamController*
+CreateReadableByteStreamController(JSContext* cx, Handle<ReadableStream*> stream,
+                                   void* underlyingSource);
+
+ReadableStream*
+ReadableStream::createExternalSourceStream(JSContext* cx, void* underlyingSource,
+                                           uint8_t flags, HandleObject proto /* = nullptr */)
+{
+    Rooted<ReadableStream*> stream(cx, createStream(cx, proto));
+    if (!stream)
+        return nullptr;
+
+    RootedNativeObject controller(cx, CreateReadableByteStreamController(cx, stream,
+                                                                         underlyingSource));
+    if (!controller)
+        return nullptr;
+
+    stream->setFixedSlot(StreamSlot_Controller, ObjectValue(*controller));
+    AddControllerFlags(controller, flags << ControllerEmbeddingFlagsOffset);
+
+    return stream;
+}
+
+// Streams spec, 3.2.3.
+bool
+ReadableStream::constructor(JSContext* cx, unsigned argc, Value* vp)
+{
+    CallArgs args = CallArgsFromVp(argc, vp);
+
+    RootedValue val(cx, args.get(0));
+    RootedValue underlyingSource(cx, args.get(0));
+    RootedValue options(cx, args.get(1));
+
+    // Do argument handling first to keep the right order of error reporting.
+    if (underlyingSource.isUndefined()) {
+        RootedObject sourceObj(cx, NewBuiltinClassInstance<PlainObject>(cx));
+        if (!sourceObj)
+            return false;
+        underlyingSource = ObjectValue(*sourceObj);
+    }
+    RootedValue size(cx);
+    RootedValue highWaterMark(cx);
+
+    if (!options.isUndefined()) {
+        if (!GetProperty(cx, options, cx->names().size, &size))
+            return false;
+
+        if (!GetProperty(cx, options, cx->names().highWaterMark, &highWaterMark))
+            return false;
+    }
+
+    if (!ThrowIfNotConstructing(cx, args, "ReadableStream"))
+        return false;
+
+    // Step 5: Let type be ? GetV(underlyingSource, "type").
+    RootedValue typeVal(cx);
+    if (!GetProperty(cx, underlyingSource, cx->names().type, &typeVal))
+        return false;
+
+    // Step 6: Let typeString be ? ToString(type).
+    RootedString type(cx, ToString<CanGC>(cx, typeVal));
+    if (!type)
+        return false;
+
+    int32_t notByteStream;
+    if (!CompareStrings(cx, type, cx->names().bytes, &notByteStream))
+        return false;
+
+    // Step 7.a & 8.a (reordered): If highWaterMark is undefined, let
+    //                             highWaterMark be 1 (or 0 for byte streams).
+    if (highWaterMark.isUndefined())
+        highWaterMark = Int32Value(notByteStream ? 1 : 0);
+
+    Rooted<ReadableStream*> stream(cx);
+
+    // Step 7: If typeString is "bytes",
+    if (!notByteStream) {
+        // Step 7.b: Set this.[[readableStreamController]] to
+        //           ? Construct(ReadableByteStreamController,
+        //                       « this, underlyingSource, highWaterMark »).
+        stream = createByteStream(cx, underlyingSource, highWaterMark);
+    } else if (typeVal.isUndefined()) {
+        // Step 8: Otherwise, if type is undefined,
+        // Step 8.b: Set this.[[readableStreamController]] to
+        //           ? Construct(ReadableStreamDefaultController,
+        //                       « this, underlyingSource, size, highWaterMark »).
+        stream = createDefaultStream(cx, underlyingSource, size, highWaterMark);
+    } else {
+        // Step 9: Otherwise, throw a RangeError exception.
+        JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
+                                  JSMSG_READABLESTREAM_UNDERLYINGSOURCE_TYPE_WRONG);
+        return false;
+    }
+    if (!stream)
+        return false;
+
+    args.rval().setObject(*stream);
+    return true;
+}
+
+// Streams spec, 3.2.4.1. get locked
+static MOZ_MUST_USE bool
+ReadableStream_locked_impl(JSContext* cx, const CallArgs& args)
+{
+    Rooted<ReadableStream*> stream(cx, &args.thisv().toObject().as<ReadableStream>());
+
+    // Step 2: Return ! IsReadableStreamLocked(this).
+    args.rval().setBoolean(stream->locked());
+    return true;
+}
+
+static bool
+ReadableStream_locked(JSContext* cx, unsigned argc, Value* vp)
+{
+    // Step 1: If ! IsReadableStream(this) is false, throw a TypeError exception.
+    CallArgs args = CallArgsFromVp(argc, vp);
+    return CallNonGenericMethod<Is<ReadableStream>, ReadableStream_locked_impl>(cx, args);
+}
+
+// Streams spec, 3.2.4.2. cancel ( reason )
+static MOZ_MUST_USE bool
+ReadableStream_cancel(JSContext* cx, unsigned argc, Value* vp)
+{
+    CallArgs args = CallArgsFromVp(argc, vp);
+    // Step 1: If ! IsReadableStream(this) is false, return a promise rejected
+    //         with a TypeError exception.
+    if (!Is<ReadableStream>(args.thisv())) {
+        ReportValueError3(cx, JSMSG_INCOMPATIBLE_PROTO, JSDVG_SEARCH_STACK, args.thisv(),
+                          nullptr, "cancel", "");
+        return ReturnPromiseRejectedWithPendingError(cx, args);
+    }
+
+    Rooted<ReadableStream*> stream(cx, &args.thisv().toObject().as<ReadableStream>());
+
+    // Step 2: If ! IsReadableStreamLocked(this) is true, return a promise
+    //         rejected with a TypeError exception.
+    if (stream->locked()) {
+        JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
+                                  JSMSG_READABLESTREAM_NOT_LOCKED, "cancel");
+        return ReturnPromiseRejectedWithPendingError(cx, args);
+    }
+
+    // Step 3: Return ! ReadableStreamCancel(this, reason).
+    RootedObject cancelPromise(cx, ReadableStream::cancel(cx, stream, args.get(0)));
+    if (!cancelPromise)
+        return false;
+    args.rval().setObject(*cancelPromise);
+    return true;
+}
+
+static MOZ_MUST_USE ReadableStreamDefaultReader*
+CreateReadableStreamDefaultReader(JSContext* cx, Handle<ReadableStream*> stream);
+
+static MOZ_MUST_USE ReadableStreamBYOBReader*
+CreateReadableStreamBYOBReader(JSContext* cx, Handle<ReadableStream*> stream);
+
+// Streams spec, 3.2.4.3. getReader()
+static MOZ_MUST_USE bool
+ReadableStream_getReader_impl(JSContext* cx, const CallArgs& args)
+{
+    Rooted<ReadableStream*> stream(cx, &args.thisv().toObject().as<ReadableStream>());
+    RootedObject reader(cx);
+
+    // Step 2: If mode is undefined, return
+    //         ? AcquireReadableStreamDefaultReader(this).
+    RootedValue modeVal(cx);
+    HandleValue optionsVal = args.get(0);
+    if (!optionsVal.isUndefined()) {
+        if (!GetProperty(cx, optionsVal, cx->names().mode, &modeVal))
+            return false;
+    }
+
+    if (modeVal.isUndefined()) {
+        reader = CreateReadableStreamDefaultReader(cx, stream);
+    } else {
+        // Step 3: Set mode to ? ToString(mode) (implicit).
+        RootedString mode(cx, ToString<CanGC>(cx, modeVal));
+        if (!mode)
+            return false;
+
+        // Step 4: If mode is "byob", return ? AcquireReadableStreamBYOBReader(this).
+        int32_t notByob;
+        if (!CompareStrings(cx, mode, cx->names().byob, &notByob))
+            return false;
+        if (notByob) {
+            JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
+                                      JSMSG_READABLESTREAM_INVALID_READER_MODE);
+            // Step 5: Throw a RangeError exception.
+            return false;
+
+        }
+        reader = CreateReadableStreamBYOBReader(cx, stream);
+    }
+
+    // Reordered second part of steps 2 and 4.
+    if (!reader)
+        return false;
+    args.rval().setObject(*reader);
+    return true;
+}
+
+static bool
+ReadableStream_getReader(JSContext* cx, unsigned argc, Value* vp)
+{
+    // Step 1: If ! IsReadableStream(this) is false, throw a TypeError exception.
+    CallArgs args = CallArgsFromVp(argc, vp);
+    return CallNonGenericMethod<Is<ReadableStream>, ReadableStream_getReader_impl>(cx, args);
+}
+
+// Streams spec, 3.2.4.4. pipeThrough({ writable, readable }, options)
+static MOZ_MUST_USE bool
+ReadableStream_pipeThrough(JSContext* cx, unsigned argc, Value* vp)
+{
+    JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
+                              JSMSG_READABLESTREAM_METHOD_NOT_IMPLEMENTED, "pipeThrough");
+    return false;
+    // // Step 1: Perform ? Invoke(this, "pipeTo", « writable, options »).
+
+    // // Step 2: Return readable.
+    // return readable;
+}
+
+// Streams spec, 3.2.4.5. pipeTo(dest, { preventClose, preventAbort, preventCancel } = {})
+// TODO: Unimplemented since spec is not complete yet.
+static MOZ_MUST_USE bool
+ReadableStream_pipeTo(JSContext* cx, unsigned argc, Value* vp)
+{
+    JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
+                              JSMSG_READABLESTREAM_METHOD_NOT_IMPLEMENTED, "pipeTo");
+    return false;
+}
+
+static MOZ_MUST_USE bool
+ReadableStreamTee(JSContext* cx, Handle<ReadableStream*> stream, bool cloneForBranch2,
+                  MutableHandle<ReadableStream*> branch1, MutableHandle<ReadableStream*> branch2);
+
+// Streams spec, 3.2.4.6. tee()
+static MOZ_MUST_USE bool
+ReadableStream_tee_impl(JSContext* cx, const CallArgs& args)
+{
+    Rooted<ReadableStream*> stream(cx, &args.thisv().toObject().as<ReadableStream>());
+
+    // Step 2: Let branches be ? ReadableStreamTee(this, false).
+    Rooted<ReadableStream*> branch1(cx);
+    Rooted<ReadableStream*> branch2(cx);
+    if (!ReadableStreamTee(cx, stream, false, &branch1, &branch2))
+        return false;
+
+    // Step 3: Return ! CreateArrayFromList(branches).
+    RootedNativeObject branches(cx, NewDenseFullyAllocatedArray(cx, 2));
+    if (!branches)
+        return false;
+    branches->setDenseInitializedLength(2);
+    branches->initDenseElement(0, ObjectValue(*branch1));
+    branches->initDenseElement(1, ObjectValue(*branch2));
+
+    args.rval().setObject(*branches);
+    return true;
+}
+
+static bool
+ReadableStream_tee(JSContext* cx, unsigned argc, Value* vp)
+{
+    // Step 1: If ! IsReadableStream(this) is false, throw a TypeError exception
+    CallArgs args = CallArgsFromVp(argc, vp);
+    return CallNonGenericMethod<Is<ReadableStream>, ReadableStream_tee_impl>(cx, args);
+}
+
+static const JSFunctionSpec ReadableStream_methods[] = {
+    JS_FN("cancel",         ReadableStream_cancel,      1, 0),
+    JS_FN("getReader",      ReadableStream_getReader,   0, 0),
+    JS_FN("pipeThrough",    ReadableStream_pipeThrough, 2, 0),
+    JS_FN("pipeTo",         ReadableStream_pipeTo,      1, 0),
+    JS_FN("tee",            ReadableStream_tee,         0, 0),
+    JS_FS_END
+};
+
+static const JSPropertySpec ReadableStream_properties[] = {
+    JS_PSG("locked", ReadableStream_locked, 0),
+    JS_PS_END
+};
+
+CLASS_SPEC(ReadableStream, 0, StreamSlotCount, 0, 0, JS_NULL_CLASS_OPS);
+
+// Streams spec, 3.3.1. AcquireReadableStreamBYOBReader ( stream )
+// Always inlined.
+
+// Streams spec, 3.3.2. AcquireReadableStreamDefaultReader ( stream )
+// Always inlined.
+
+// Streams spec, 3.3.3. IsReadableStream ( x )
+// Using is<T> instead.
+
+// Streams spec, 3.3.4. IsReadableStreamDisturbed ( stream )
+// Using stream->disturbed() instead.
+
+// Streams spec, 3.3.5. IsReadableStreamLocked ( stream )
+bool
+ReadableStream::locked() const
+{
+    // Step 1: Assert: ! IsReadableStream(stream) is true (implicit).
+    // Step 2: If stream.[[reader]] is undefined, return false.
+    // Step 3: Return true.
+    // Special-casing for streams with external sources. Those can be locked
+    // explicitly via JSAPI, which is indicated by a controller flag.
+    // IsReadableStreamLocked is called from the controller's constructor, at
+    // which point we can't yet call ControllerFromStream(stream), but the
+    // source also can't be locked yet.
+    if (HasController(this) &&
+        (ControllerFlags(ControllerFromStream(this)) & ControllerFlag_SourceLocked))
+    {
+        return true;
+    }
+    return HasReader(this);
+}
+
+static MOZ_MUST_USE bool
+ReadableStreamDefaultControllerClose(JSContext* cx,
+                                     Handle<ReadableStreamDefaultController*> controller);
+static MOZ_MUST_USE bool
+ReadableStreamDefaultControllerEnqueue(JSContext* cx,
+                                       Handle<ReadableStreamDefaultController*> controller,
+                                       HandleValue chunk);
+
+static bool
+TeeReaderReadHandler(JSContext* cx, unsigned argc, Value* vp)
+{
+    CallArgs args = CallArgsFromVp(argc, vp);
+    Rooted<TeeState*> teeState(cx, TargetFromHandler<TeeState>(args.callee()));
+    HandleValue resultVal = args.get(0);
+
+    // Step a: Assert: Type(result) is Object.
+    RootedObject result(cx, &resultVal.toObject());
+
+    // Step b: Let value be ? Get(result, "value").
+    RootedValue value(cx);
+    if (!GetPropertyPure(cx, result, NameToId(cx->names().value), value.address()))
+        return false;
+
+    // Step c: Let done be ? Get(result, "done").
+    RootedValue doneVal(cx);
+    if (!GetPropertyPure(cx, result, NameToId(cx->names().done), doneVal.address()))
+        return false;
+
+    // Step d: Assert: Type(done) is Boolean.
+    bool done = doneVal.toBoolean();
+
+    // Step e: If done is true and teeState.[[closedOrErrored]] is false,
+    if (done && !teeState->closedOrErrored()) {
+        // Step i: If teeState.[[canceled1]] is false,
+        if (!teeState->canceled1()) {
+            // Step 1: Perform ! ReadableStreamDefaultControllerClose(branch1).
+            Rooted<ReadableStreamDefaultController*> branch1(cx, teeState->branch1());
+            if (!ReadableStreamDefaultControllerClose(cx, branch1))
+                return false;
+        }
+
+        // Step ii: If teeState.[[canceled2]] is false,
+        if (!teeState->canceled2()) {
+            // Step 1: Perform ! ReadableStreamDefaultControllerClose(branch1).
+            Rooted<ReadableStreamDefaultController*> branch2(cx, teeState->branch2());
+            if (!ReadableStreamDefaultControllerClose(cx, branch2))
+                return false;
+        }
+
+        // Step iii: Set teeState.[[closedOrErrored]] to true.
+        teeState->setClosedOrErrored();
+    }
+
+    // Step f: If teeState.[[closedOrErrored]] is true, return.
+    if (teeState->closedOrErrored())
+        return true;
+
+    // Step g: Let value1 and value2 be value.
+    RootedValue value1(cx, value);
+    RootedValue value2(cx, value);
+
+    // Step h: If teeState.[[canceled2]] is false and cloneForBranch2 is
+    //         true, set value2 to
+    //         ? StructuredDeserialize(StructuredSerialize(value2),
+    //                                 the current Realm Record).
+    // TODO: add StructuredClone() intrinsic.
+    MOZ_ASSERT(!teeState->cloneForBranch2(), "tee(cloneForBranch2=true) should not be exposed");
+
+    // Step i: If teeState.[[canceled1]] is false, perform
+    //         ? ReadableStreamDefaultControllerEnqueue(branch1, value1).
+    Rooted<ReadableStreamDefaultController*> controller(cx);
+    if (!teeState->canceled1()) {
+        controller = teeState->branch1();
+        if (!ReadableStreamDefaultControllerEnqueue(cx, controller, value1))
+            return false;
+    }
+
+    // Step j: If teeState.[[canceled2]] is false,
+    //         perform ? ReadableStreamDefaultControllerEnqueue(branch2, value2).
+    if (!teeState->canceled2()) {
+        controller = teeState->branch2();
+        if (!ReadableStreamDefaultControllerEnqueue(cx, controller, value2))
+            return false;
+    }
+
+    args.rval().setUndefined();
+    return true;
+}
+
+static MOZ_MUST_USE JSObject*
+ReadableStreamTee_Pull(JSContext* cx, Handle<TeeState*> teeState,
+                       Handle<ReadableStream*> branchStream)
+{
+    // Step 1: Let reader be F.[[reader]], branch1 be F.[[branch1]],
+    //         branch2 be F.[[branch2]], teeState be F.[[teeState]], and
+    //         cloneForBranch2 be F.[[cloneForBranch2]].
+
+    // Step 2: Return the result of transforming
+    //         ! ReadableStreamDefaultReaderRead(reader) by a fulfillment
+    //         handler which takes the argument result and performs the
+    //         following steps:
+    Rooted<ReadableStreamDefaultReader*> reader(cx, teeState->reader());
+    RootedObject readPromise(cx, ReadableStreamDefaultReader::read(cx, reader));
+    if (!readPromise)
+        return nullptr;
+
+    RootedObject onFulfilled(cx, NewHandler(cx, TeeReaderReadHandler, teeState));
+    if (!onFulfilled)
+        return nullptr;
+
+    return JS::CallOriginalPromiseThen(cx, readPromise, onFulfilled, nullptr);
+}
+
+static MOZ_MUST_USE JSObject*
+ReadableStreamTee_Cancel(JSContext* cx, Handle<TeeState*> teeState,
+                         Handle<ReadableStreamDefaultController*> branch, HandleValue reason)
+{
+    // Step 1: Let stream be F.[[stream]] and teeState be F.[[teeState]].
+    Rooted<ReadableStream*> stream(cx, teeState->stream());
+
+    bool bothBranchesCanceled = false;
+
+    // Step 2: Set teeState.[[canceled1]] to true.
+    // Step 3: Set teeState.[[reason1]] to reason.
+    if (ControllerFlags(branch) & ControllerFlag_TeeBranch1) {
+        teeState->setCanceled1(reason);
+        bothBranchesCanceled = teeState->canceled2();
+    } else {
+        MOZ_ASSERT(ControllerFlags(branch) & ControllerFlag_TeeBranch2);
+        teeState->setCanceled2(reason);
+        bothBranchesCanceled = teeState->canceled1();
+    }
+
+    // Step 4: If teeState.[[canceled1]] is true,
+    // Step 4: If teeState.[[canceled2]] is true,
+    if (bothBranchesCanceled) {
+        // Step a: Let compositeReason be
+        //         ! CreateArrayFromList(« teeState.[[reason1]], teeState.[[reason2]] »).
+        RootedNativeObject compositeReason(cx, NewDenseFullyAllocatedArray(cx, 2));
+        if (!compositeReason)
+            return nullptr;
+
+        compositeReason->setDenseInitializedLength(2);
+        compositeReason->initDenseElement(0, teeState->reason1());
+        compositeReason->initDenseElement(1, teeState->reason2());
+        RootedValue compositeReasonVal(cx, ObjectValue(*compositeReason));
+
+        Rooted<PromiseObject*> promise(cx, teeState->promise());
+
+        // Step b: Let cancelResult be ! ReadableStreamCancel(stream, compositeReason).
+        RootedObject cancelResult(cx, ReadableStream::cancel(cx, stream, compositeReasonVal));
+        if (!cancelResult) {
+            if (!RejectWithPendingError(cx, promise))
+                return nullptr;
+        } else {
+            // Step c: Resolve teeState.[[promise]] with cancelResult.
+            RootedValue resultVal(cx, ObjectValue(*cancelResult));
+            if (!PromiseObject::resolve(cx, promise, resultVal))
+                return nullptr;
+        }
+    }
+
+    // Step 5: Return teeState.[[promise]].
+    return teeState->promise();
+}
+
+static MOZ_MUST_USE bool
+ReadableStreamControllerError(JSContext* cx, HandleNativeObject controller, HandleValue e);
+
+// Streams spec, 3.3.6. step 21:
+// Upon rejection of reader.[[closedPromise]] with reason r,
+static bool
+TeeReaderClosedHandler(JSContext* cx, unsigned argc, Value* vp)
+{
+    CallArgs args = CallArgsFromVp(argc, vp);
+    Rooted<TeeState*> teeState(cx, TargetFromHandler<TeeState>(args.callee()));
+    HandleValue reason = args.get(0);
+
+    // Step a: If teeState.[[closedOrErrored]] is false, then:
+    if (!teeState->closedOrErrored()) {
+        // Step a.i: Perform ! ReadableStreamDefaultControllerError(pull.[[branch1]], r).
+        Rooted<ReadableStreamDefaultController*> branch1(cx, teeState->branch1());
+        if (!ReadableStreamControllerError(cx, branch1, reason))
+            return false;
+
+        // Step a.ii: Perform ! ReadableStreamDefaultControllerError(pull.[[branch2]], r).
+        Rooted<ReadableStreamDefaultController*> branch2(cx, teeState->branch2());
+        if (!ReadableStreamControllerError(cx, branch2, reason))
+            return false;
+
+        // Step a.iii: Set teeState.[[closedOrErrored]] to true.
+        teeState->setClosedOrErrored();
+    }
+
+    return true;
+}
+
+// Streams spec, 3.3.6. ReadableStreamTee ( stream, cloneForBranch2 )
+static MOZ_MUST_USE bool
+ReadableStreamTee(JSContext* cx, Handle<ReadableStream*> stream, bool cloneForBranch2,
+                  MutableHandle<ReadableStream*> branch1Stream,
+                  MutableHandle<ReadableStream*> branch2Stream)
+{
+    // Step 1: Assert: ! IsReadableStream(stream) is true (implicit).
+    // Step 2: Assert: Type(cloneForBranch2) is Boolean (implicit).
+
+    // Step 3: Let reader be ? AcquireReadableStreamDefaultReader(stream).
+    Rooted<ReadableStreamDefaultReader*> reader(cx, CreateReadableStreamDefaultReader(cx, stream));
+    if (!reader)
+        return false;
+
+    // Step 4: Let teeState be Record {[[closedOrErrored]]: false,
+    //                                 [[canceled1]]: false,
+    //                                 [[canceled2]]: false,
+    //                                 [[reason1]]: undefined,
+    //                                 [[reason2]]: undefined,
+    //                                 [[promise]]: a new promise}.
+    Rooted<TeeState*> teeState(cx, TeeState::create(cx, stream));
+    if (!teeState)
+        return false;
+
+    // Steps 5-10 omitted because our implementation works differently.
+
+    // Step 5: Let pull be a new ReadableStreamTee pull function.
+    // Step 6: Set pull.[[reader]] to reader, pull.[[teeState]] to teeState, and
+    //         pull.[[cloneForBranch2]] to cloneForBranch2.
+    // Step 7: Let cancel1 be a new ReadableStreamTee branch 1 cancel function.
+    // Step 8: Set cancel1.[[stream]] to stream and cancel1.[[teeState]] to
+    //         teeState.
+
+    // Step 9: Let cancel2 be a new ReadableStreamTee branch 2 cancel function.
+    // Step 10: Set cancel2.[[stream]] to stream and cancel2.[[teeState]] to
+    //          teeState.
+
+    // Step 11: Let underlyingSource1 be ! ObjectCreate(%ObjectPrototype%).
+    // Step 12: Perform ! CreateDataProperty(underlyingSource1, "pull", pull).
+    // Step 13: Perform ! CreateDataProperty(underlyingSource1, "cancel", cancel1).
+
+    // Step 14: Let branch1Stream be ! Construct(ReadableStream, underlyingSource1).
+    RootedValue hwmValue(cx, NumberValue(1));
+    RootedValue underlyingSource(cx, ObjectValue(*teeState));
+    branch1Stream.set(ReadableStream::createDefaultStream(cx, underlyingSource,
+                                                          UndefinedHandleValue,
+                                                          hwmValue));
+    if (!branch1Stream)
+        return false;
+
+    Rooted<ReadableStreamDefaultController*> branch1(cx);
+    branch1 = &ControllerFromStream(branch1Stream)->as<ReadableStreamDefaultController>();
+    AddControllerFlags(branch1, ControllerFlag_TeeBranch | ControllerFlag_TeeBranch1);
+    teeState->setBranch1(branch1);
+
+    // Step 15: Let underlyingSource2 be ! ObjectCreate(%ObjectPrototype%).
+    // Step 16: Perform ! CreateDataProperty(underlyingSource2, "pull", pull).
+    // Step 17: Perform ! CreateDataProperty(underlyingSource2, "cancel", cancel2).
+
+    // Step 18: Let branch2Stream be ! Construct(ReadableStream, underlyingSource2).
+    branch2Stream.set(ReadableStream::createDefaultStream(cx, underlyingSource,
+                                                          UndefinedHandleValue,
+                                                          hwmValue));
+    if (!branch2Stream)
+        return false;
+
+    Rooted<ReadableStreamDefaultController*> branch2(cx);
+    branch2 = &ControllerFromStream(branch2Stream)->as<ReadableStreamDefaultController>();
+    AddControllerFlags(branch2, ControllerFlag_TeeBranch | ControllerFlag_TeeBranch2);
+    teeState->setBranch2(branch2);
+
+    // Step 19: Set pull.[[branch1]] to branch1Stream.[[readableStreamController]].
+    // Step 20: Set pull.[[branch2]] to branch2Stream.[[readableStreamController]].
+
+    // Step 21: Upon rejection of reader.[[closedPromise]] with reason r,
+    RootedObject closedPromise(cx, &reader->getFixedSlot(ReaderSlot_ClosedPromise).toObject());
+
+    RootedObject onRejected(cx, NewHandler(cx, TeeReaderClosedHandler, teeState));
+    if (!onRejected)
+        return false;
+
+    if (!JS::AddPromiseReactions(cx, closedPromise, nullptr, onRejected))
+        return false;
+
+    // Step 22: Return « branch1, branch2 ».
+    return true;
+}
+
+// Streams spec, 3.4.1. ReadableStreamAddReadIntoRequest ( stream )
+static MOZ_MUST_USE PromiseObject*
+ReadableStreamAddReadIntoRequest(JSContext* cx, Handle<ReadableStream*> stream)
+{
+    // Step 1: MOZ_ASSERT: ! IsReadableStreamBYOBReader(stream.[[reader]]) is true.
+    RootedValue val(cx, stream->getFixedSlot(StreamSlot_Reader));
+    RootedNativeObject reader(cx, &val.toObject().as<ReadableStreamBYOBReader>());
+
+    // Step 2: MOZ_ASSERT: stream.[[state]] is "readable" or "closed".
+    MOZ_ASSERT(stream->readable() || stream->closed());
+
+    // Step 3: Let promise be a new promise.
+    Rooted<PromiseObject*> promise(cx, PromiseObject::createSkippingExecutor(cx));
+    if (!promise)
+        return nullptr;
+
+    // Step 4: Let readIntoRequest be Record {[[promise]]: promise}.
+    // Step 5: Append readIntoRequest as the last element of stream.[[reader]].[[readIntoRequests]].
+    val = reader->getFixedSlot(ReaderSlot_Requests);
+    RootedNativeObject readIntoRequests(cx, &val.toObject().as<NativeObject>());
+    // Since [[promise]] is the Record's only field, we store it directly.
+    val = ObjectValue(*promise);
+    if (!AppendToList(cx, readIntoRequests, val))
+        return nullptr;
+
+    // Step 6: Return promise.
+    return promise;
+}
+
+// Streams spec, 3.4.2. ReadableStreamAddReadRequest ( stream )
+static MOZ_MUST_USE PromiseObject*
+ReadableStreamAddReadRequest(JSContext* cx, Handle<ReadableStream*> stream)
+{
+  MOZ_ASSERT(stream->is<ReadableStream>());
+
+  // Step 1: Assert: ! IsReadableStreamDefaultReader(stream.[[reader]]) is true.
+  RootedNativeObject reader(cx, ReaderFromStream(stream));
+
+  // Step 2: Assert: stream.[[state]] is "readable".
+  MOZ_ASSERT(stream->readable());
+
+  // Step 3: Let promise be a new promise.
+  Rooted<PromiseObject*> promise(cx, PromiseObject::createSkippingExecutor(cx));
+  if (!promise)
+      return nullptr;
+
+  // Step 4: Let readRequest be Record {[[promise]]: promise}.
+  // Step 5: Append readRequest as the last element of stream.[[reader]].[[readRequests]].
+  RootedValue val(cx, reader->getFixedSlot(ReaderSlot_Requests));
+  RootedNativeObject readRequests(cx, &val.toObject().as<NativeObject>());
+
+  // Since [[promise]] is the Record's only field, we store it directly.
+  val = ObjectValue(*promise);
+  if (!AppendToList(cx, readRequests, val))
+      return nullptr;
+
+  // Step 6: Return promise.
+  return promise;
+}
+
+static MOZ_MUST_USE JSObject*
+ReadableStreamControllerCancelSteps(JSContext* cx,
+                                    HandleNativeObject controller, HandleValue reason);
+
+// Used for transforming the result of promise fulfillment/rejection.
+static bool
+ReturnUndefined(JSContext* cx, unsigned argc, Value* vp)
+{
+    CallArgs args = CallArgsFromVp(argc, vp);
+    args.rval().setUndefined();
+    return true;
+}
+
+MOZ_MUST_USE bool
+ReadableStreamCloseInternal(JSContext* cx, Handle<ReadableStream*> stream);
+
+// Streams spec, 3.4.3. ReadableStreamCancel ( stream, reason )
+/* static */ MOZ_MUST_USE JSObject*
+ReadableStream::cancel(JSContext* cx, Handle<ReadableStream*> stream, HandleValue reason)
+{
+    // Step 1: Set stream.[[disturbed]] to true.
+    uint32_t state = StreamState(stream) | ReadableStream::Disturbed;
+    SetStreamState(stream, state);
+
+    // Step 2: If stream.[[state]] is "closed", return a new promise resolved
+    //         with undefined.
+    if (stream->closed())
+        return PromiseObject::unforgeableResolve(cx, UndefinedHandleValue);
+
+    // Step 3: If stream.[[state]] is "errored", return a new promise rejected
+    //         with stream.[[storedError]].
+    if (stream->errored()) {
+        RootedValue storedError(cx, stream->getFixedSlot(StreamSlot_StoredError));
+        return PromiseObject::unforgeableReject(cx, storedError);
+    }
+
+    // Step 4: Perform ! ReadableStreamClose(stream).
+    if (!ReadableStreamCloseInternal(cx, stream))
+        return nullptr;
+
+    // Step 5: Let sourceCancelPromise be
+    //         ! stream.[[readableStreamController]].[[CancelSteps]](reason).
+    RootedNativeObject controller(cx, ControllerFromStream(stream));
+    RootedObject sourceCancelPromise(cx);
+    sourceCancelPromise = ReadableStreamControllerCancelSteps(cx, controller, reason);
+    if (!sourceCancelPromise)
+        return nullptr;
+
+    // Step 6: Return the result of transforming sourceCancelPromise by a
+    //         fulfillment handler that returns undefined.
+    RootedAtom funName(cx, cx->names().empty);
+    RootedFunction returnUndefined(cx, NewNativeFunction(cx, ReturnUndefined, 0, funName));
+    if (!returnUndefined)
+        return nullptr;
+    return JS::CallOriginalPromiseThen(cx, sourceCancelPromise, returnUndefined, nullptr);
+}
+
+// Streams spec, 3.4.4. ReadableStreamClose ( stream )
+MOZ_MUST_USE bool
+ReadableStreamCloseInternal(JSContext* cx, Handle<ReadableStream*> stream)
+{
+  // Step 1: Assert: stream.[[state]] is "readable".
+  MOZ_ASSERT(stream->readable());
+
+  uint32_t state = StreamState(stream);
+  // Step 2: Set stream.[[state]] to "closed".
+  SetStreamState(stream, (state & ReadableStream::Disturbed) | ReadableStream::Closed);
+
+  // Step 3: Let reader be stream.[[reader]].
+  RootedValue val(cx, stream->getFixedSlot(StreamSlot_Reader));
+
+  // Step 4: If reader is undefined, return.
+  if (val.isUndefined())
+      return true;
+
+  // Step 5: If ! IsReadableStreamDefaultReader(reader) is true,
+  RootedNativeObject reader(cx, &val.toObject().as<NativeObject>());
+  if (reader->is<ReadableStreamDefaultReader>()) {
+      // Step a: Repeat for each readRequest that is an element of
+      //         reader.[[readRequests]],
+      val = reader->getFixedSlot(ReaderSlot_Requests);
+      if (!val.isUndefined()) {
+          RootedNativeObject readRequests(cx, &val.toObject().as<NativeObject>());
+          uint32_t len = readRequests->getDenseInitializedLength();
+          RootedObject readRequest(cx);
+          RootedObject resultObj(cx);
+          RootedValue resultVal(cx);
+          for (uint32_t i = 0; i < len; i++) {
+              // Step i: Resolve readRequest.[[promise]] with
+              //         ! CreateIterResultObject(undefined, true).
+              readRequest = &readRequests->getDenseElement(i).toObject();
+              resultObj = CreateIterResultObject(cx, UndefinedHandleValue, true);
+              if (!resultObj)
+                  return false;
+              resultVal = ObjectValue(*resultObj);
+              if (!ResolvePromise(cx, readRequest, resultVal))
+                  return false;
+          }
+
+          // Step b: Set reader.[[readRequests]] to an empty List.
+          reader->setFixedSlot(ReaderSlot_Requests, UndefinedValue());
+      }
+  }
+
+  // Step 6: Resolve reader.[[closedPromise]] with undefined.
+  // Step 7: Return (implicit).
+  RootedObject closedPromise(cx, &reader->getFixedSlot(ReaderSlot_ClosedPromise).toObject());
+  if (!ResolvePromise(cx, closedPromise, UndefinedHandleValue))
+      return false;
+
+  if (stream->mode() == JS::ReadableStreamMode::ExternalSource &&
+      cx->runtime()->readableStreamClosedCallback)
+  {
+      NativeObject* controller = ControllerFromStream(stream);
+      void* source = controller->getFixedSlot(ControllerSlot_UnderlyingSource).toPrivate();
+      cx->runtime()->readableStreamClosedCallback(cx, stream, source, stream->embeddingFlags());
+  }
+
+  return true;
+}
+
+// Streams spec, 3.4.5. ReadableStreamError ( stream, e )
+MOZ_MUST_USE bool
+ReadableStreamErrorInternal(JSContext* cx, Handle<ReadableStream*> stream, HandleValue e)
+{
+    // Step 1: Assert: ! IsReadableStream(stream) is true (implicit).
+
+    // Step 2: Assert: stream.[[state]] is "readable".
+    MOZ_ASSERT(stream->readable());
+
+    // Step 3: Set stream.[[state]] to "errored".
+    uint32_t state = StreamState(stream);
+    SetStreamState(stream, (state & ReadableStream::Disturbed) | ReadableStream::Errored);
+
+    // Step 4: Set stream.[[storedError]] to e.
+    stream->setFixedSlot(StreamSlot_StoredError, e);
+
+    // Step 5: Let reader be stream.[[reader]].
+    RootedValue val(cx, stream->getFixedSlot(StreamSlot_Reader));
+
+    // Step 6: If reader is undefined, return.
+    if (val.isUndefined())
+        return true;
+    RootedNativeObject reader(cx, &val.toObject().as<NativeObject>());
+
+    // Steps 7,8: (Identical in our implementation.)
+    // Step a: Repeat for each readRequest that is an element of
+    //         reader.[[readRequests]],
+    val = reader->getFixedSlot(ReaderSlot_Requests);
+    RootedNativeObject readRequests(cx, &val.toObject().as<NativeObject>());
+    Rooted<PromiseObject*> readRequest(cx);
+    uint32_t len = readRequests->getDenseInitializedLength();
+    for (uint32_t i = 0; i < len; i++) {
+        // Step i: Reject readRequest.[[promise]] with e.
+        val = readRequests->getDenseElement(i);
+        readRequest = &val.toObject().as<PromiseObject>();
+        if (!PromiseObject::reject(cx, readRequest, e))
+            return false;
+    }
+
+    // Step b: Set reader.[[readRequests]] to a new empty List.
+    if (!SetNewList(cx, reader, ReaderSlot_Requests))
+        return false;
+
+    // Step 9: Reject reader.[[closedPromise]] with e.
+    val = reader->getFixedSlot(ReaderSlot_ClosedPromise);
+    Rooted<PromiseObject*> closedPromise(cx, &val.toObject().as<PromiseObject>());
+    if (!PromiseObject::reject(cx, closedPromise, e))
+        return false;
+
+    if (stream->mode() == JS::ReadableStreamMode::ExternalSource &&
+        cx->runtime()->readableStreamErroredCallback)
+    {
+        NativeObject* controller = ControllerFromStream(stream);
+        void* source = controller->getFixedSlot(ControllerSlot_UnderlyingSource).toPrivate();
+        cx->runtime()->readableStreamErroredCallback(cx, stream, source,
+                                                     stream->embeddingFlags(), e);
+    }
+
+    return true;
+}
+
+// Streams spec, 3.4.6. ReadableStreamFulfillReadIntoRequest( stream, chunk, done )
+// Streams spec, 3.4.7. ReadableStreamFulfillReadRequest ( stream, chunk, done )
+// These two spec functions are identical in our implementation.
+static MOZ_MUST_USE bool
+ReadableStreamFulfillReadOrReadIntoRequest(JSContext* cx, Handle<ReadableStream*> stream,
+                                           HandleValue chunk, bool done)
+{
+    // Step 1: Let reader be stream.[[reader]].
+    RootedValue val(cx, stream->getFixedSlot(StreamSlot_Reader));
+    RootedNativeObject reader(cx, &val.toObject().as<NativeObject>());
+
+    // Step 2: Let readIntoRequest be the first element of
+    //         reader.[[readIntoRequests]].
+    // Step 3: Remove readIntoRequest from reader.[[readIntoRequests]], shifting
+    //         all other elements downward (so that the second becomes the first,
+    //         and so on).
+    val = reader->getFixedSlot(ReaderSlot_Requests);
+    RootedNativeObject readIntoRequests(cx, &val.toObject().as<NativeObject>());
+    Rooted<PromiseObject*> readIntoRequest(cx);
+    readIntoRequest = ShiftFromList<PromiseObject>(cx, readIntoRequests);
+    MOZ_ASSERT(readIntoRequest);
+
+    // Step 4: Resolve readIntoRequest.[[promise]] with
+    //         ! CreateIterResultObject(chunk, done).
+    RootedObject iterResult(cx, CreateIterResultObject(cx, chunk, done));
+    if (!iterResult)
+        return false;
+    val = ObjectValue(*iterResult);
+    return PromiseObject::resolve(cx, readIntoRequest, val);
+}
+
+// Streams spec, 3.4.8. ReadableStreamGetNumReadIntoRequests ( stream )
+// Streams spec, 3.4.9. ReadableStreamGetNumReadRequests ( stream )
+// (Identical implementation.)
+static uint32_t
+ReadableStreamGetNumReadRequests(ReadableStream* stream)
+{
+    // Step 1: Return the number of elements in
+    //         stream.[[reader]].[[readRequests]].
+    if (!HasReader(stream))
+        return 0;
+    NativeObject* reader = ReaderFromStream(stream);
+    Value readRequests = reader->getFixedSlot(ReaderSlot_Requests);
+    return readRequests.toObject().as<NativeObject>().getDenseInitializedLength();
+}
+
+// Stream spec 3.4.10. ReadableStreamHasBYOBReader ( stream )
+static MOZ_MUST_USE bool
+ReadableStreamHasBYOBReader(ReadableStream* stream)
+{
+    // Step 1: Let reader be stream.[[reader]].
+    // Step 2: If reader is undefined, return false.
+    // Step 3: If ! IsReadableStreamBYOBReader(reader) is false, return false.
+    // Step 4: Return true.
+    Value reader = stream->getFixedSlot(StreamSlot_Reader);
+    return reader.isObject() && reader.toObject().is<ReadableStreamBYOBReader>();
+}
+
+// Streap spec 3.4.11. ReadableStreamHasDefaultReader ( stream )
+static MOZ_MUST_USE bool
+ReadableStreamHasDefaultReader(ReadableStream* stream)
+{
+    // Step 1: Let reader be stream.[[reader]].
+    // Step 2: If reader is undefined, return false.
+    // Step 3: If ! ReadableStreamDefaultReader(reader) is false, return false.
+    // Step 4: Return true.
+    Value reader = stream->getFixedSlot(StreamSlot_Reader);
+    return reader.isObject() && reader.toObject().is<ReadableStreamDefaultReader>();
+}
+
+static MOZ_MUST_USE bool
+ReadableStreamReaderGenericInitialize(JSContext* cx,
+                                      HandleNativeObject reader,
+                                      Handle<ReadableStream*> stream);
+
+// Stream spec, 3.5.3. new ReadableStreamDefaultReader ( stream )
+// Steps 2-4.
+static MOZ_MUST_USE ReadableStreamDefaultReader*
+CreateReadableStreamDefaultReader(JSContext* cx, Handle<ReadableStream*> stream)
+{
+    Rooted<ReadableStreamDefaultReader*> reader(cx);
+    reader = NewBuiltinClassInstance<ReadableStreamDefaultReader>(cx);
+    if (!reader)
+        return nullptr;
+
+    // Step 2: If ! IsReadableStreamLocked(stream) is true, throw a TypeError
+    //         exception.
+    if (stream->locked()) {
+        JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
+                                  JSMSG_READABLESTREAM_LOCKED);
+        return nullptr;
+    }
+
+    // Step 3: Perform ! ReadableStreamReaderGenericInitialize(this, stream).
+    if (!ReadableStreamReaderGenericInitialize(cx, reader, stream))
+        return nullptr;
+
+    // Step 4: Set this.[[readRequests]] to a new empty List.
+    if (!SetNewList(cx, reader, ReaderSlot_Requests))
+        return nullptr;
+
+    return reader;
+}
+
+// Stream spec, 3.5.3. new ReadableStreamDefaultReader ( stream )
+bool
+ReadableStreamDefaultReader::constructor(JSContext* cx, unsigned argc, Value* vp)
+{
+    CallArgs args = CallArgsFromVp(argc, vp);
+
+    if (!ThrowIfNotConstructing(cx, args, "ReadableStreamDefaultReader"))
+        return false;
+
+    // Step 1: If ! IsReadableStream(stream) is false, throw a TypeError exception.
+    if (!Is<ReadableStream>(args.get(0))) {
+        ReportArgTypeError(cx, "ReadableStreamDefaultReader", "ReadableStream",
+                           args.get(0));
+        return false;
+    }
+
+    Rooted<ReadableStream*> stream(cx, &args.get(0).toObject().as<ReadableStream>());
+
+    RootedObject reader(cx, CreateReadableStreamDefaultReader(cx, stream));
+    if (!reader)
+        return false;
+
+    args.rval().setObject(*reader);
+    return true;
+}
+
+// Streams spec, 3.5.4.1 get closed
+static MOZ_MUST_USE bool
+ReadableStreamDefaultReader_closed(JSContext* cx, unsigned argc, Value* vp)
+{
+    CallArgs args = CallArgsFromVp(argc, vp);
+
+    // Step 1: If ! IsReadableStreamDefaultReader(this) is false, return a promise
+    //         rejected with a TypeError exception.
+    if (!Is<ReadableStreamDefaultReader>(args.thisv()))
+        return RejectNonGenericMethod(cx, args, "ReadableStreamDefaultReader", "get closed");
+
+    // Step 2: Return this.[[closedPromise]].
+    NativeObject* reader = &args.thisv().toObject().as<NativeObject>();
+    args.rval().set(reader->getFixedSlot(ReaderSlot_ClosedPromise));
+    return true;
+}
+
+static MOZ_MUST_USE JSObject*
+ReadableStreamReaderGenericCancel(JSContext* cx, HandleNativeObject reader, HandleValue reason);
+
+// Streams spec, 3.5.4.2. cancel ( reason )
+static MOZ_MUST_USE bool
+ReadableStreamDefaultReader_cancel(JSContext* cx, unsigned argc, Value* vp)
+{
+    CallArgs args = CallArgsFromVp(argc, vp);
+
+    // Step 1: If ! IsReadableStreamDefaultReader(this) is false, return a promise
+    //         rejected with a TypeError exception.
+    if (!Is<ReadableStreamDefaultReader>(args.thisv()))
+        return RejectNonGenericMethod(cx, args, "ReadableStreamDefaultReader", "cancel");
+
+    // Step 2: If this.[[ownerReadableStream]] is undefined, return a promise
+    //         rejected with a TypeError exception.
+    RootedNativeObject reader(cx, &args.thisv().toObject().as<NativeObject>());
+    if (!ReaderHasStream(reader)) {
+        JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
+                                  JSMSG_READABLESTREAMREADER_NOT_OWNED, "cancel");
+        return ReturnPromiseRejectedWithPendingError(cx, args);
+    }
+
+    // Step 3: Return ! ReadableStreamReaderGenericCancel(this, reason).
+    JSObject* cancelPromise = ReadableStreamReaderGenericCancel(cx, reader, args.get(0));
+    if (!cancelPromise)
+        return false;
+    args.rval().setObject(*cancelPromise);
+    return true;
+}
+
+// Streams spec, 3.5.4.3 read ( )
+static MOZ_MUST_USE bool
+ReadableStreamDefaultReader_read(JSContext* cx, unsigned argc, Value* vp)
+{
+    CallArgs args = CallArgsFromVp(argc, vp);
+
+    // Step 1: If ! IsReadableStreamDefaultReader(this) is false, return a promise
+    //         rejected with a TypeError exception.
+    if (!Is<ReadableStreamDefaultReader>(args.thisv()))
+        return RejectNonGenericMethod(cx, args, "ReadableStreamDefaultReader", "read");
+
+    // Step 2: If this.[[ownerReadableStream]] is undefined, return a promise
+    //         rejected with a TypeError exception.
+    Rooted<ReadableStreamDefaultReader*> reader(cx);
+    reader = &args.thisv().toObject().as<ReadableStreamDefaultReader>();
+    if (!ReaderHasStream(reader)) {
+        JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
+                                  JSMSG_READABLESTREAMREADER_NOT_OWNED, "read");
+        return ReturnPromiseRejectedWithPendingError(cx, args);
+    }
+
+    // Step 3: Return ! ReadableStreamDefaultReaderRead(this).
+    JSObject* readPromise = ReadableStreamDefaultReader::read(cx, reader);
+    if (!readPromise)
+        return false;
+    args.rval().setObject(*readPromise);
+    return true;
+}
+
+static MOZ_MUST_USE bool
+ReadableStreamReaderGenericRelease(JSContext* cx, HandleNativeObject reader);
+
+// Streams spec, 3.5.4.4. releaseLock ( )
+static MOZ_MUST_USE bool
+ReadableStreamDefaultReader_releaseLock_impl(JSContext* cx, const CallArgs& args)
+{
+    Rooted<ReadableStreamDefaultReader*> reader(cx);
+    reader = &args.thisv().toObject().as<ReadableStreamDefaultReader>();
+
+    // Step 2: If this.[[ownerReadableStream]] is undefined, return.
+    if (!ReaderHasStream(reader)) {
+        args.rval().setUndefined();
+        return true;
+    }
+
+    // Step 3: If this.[[readRequests]] is not empty, throw a TypeError exception.
+    Value val = reader->getFixedSlot(ReaderSlot_Requests);
+    if (!val.isUndefined()) {
+        NativeObject* readRequests = &val.toObject().as<NativeObject>();
+        uint32_t len = readRequests->getDenseInitializedLength();
+        if (len != 0) {
+            JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
+                                      JSMSG_READABLESTREAMREADER_NOT_EMPTY,
+                                      "releaseLock");
+            return false;
+        }
+    }
+
+    // Step 4: Perform ! ReadableStreamReaderGenericRelease(this).
+    return ReadableStreamReaderGenericRelease(cx, reader);
+}
+
+static bool
+ReadableStreamDefaultReader_releaseLock(JSContext* cx, unsigned argc, Value* vp)
+{
+    // Step 1: If ! IsReadableStreamDefaultReader(this) is false,
+    //         throw a TypeError exception.
+    CallArgs args = CallArgsFromVp(argc, vp);
+    return CallNonGenericMethod<Is<ReadableStreamDefaultReader>,
+                                ReadableStreamDefaultReader_releaseLock_impl>(cx, args);
+}
+
+static const JSFunctionSpec ReadableStreamDefaultReader_methods[] = {
+    JS_FN("cancel",         ReadableStreamDefaultReader_cancel,         1, 0),
+    JS_FN("read",           ReadableStreamDefaultReader_read,           0, 0),
+    JS_FN("releaseLock",    ReadableStreamDefaultReader_releaseLock,    0, 0),
+    JS_FS_END
+};
+
+static const JSPropertySpec ReadableStreamDefaultReader_properties[] = {
+    JS_PSG("closed", ReadableStreamDefaultReader_closed, 0),
+    JS_PS_END
+};
+
+CLASS_SPEC(ReadableStreamDefaultReader, 1, ReaderSlotCount, ClassSpec::DontDefineConstructor, 0,
+           JS_NULL_CLASS_OPS);
+
+
+// Streams spec, 3.6.3 new ReadableStreamBYOBReader ( stream )
+// Steps 2-5.
+static MOZ_MUST_USE ReadableStreamBYOBReader*
+CreateReadableStreamBYOBReader(JSContext* cx, Handle<ReadableStream*> stream)
+{
+    // Step 2: If ! IsReadableByteStreamController(stream.[[readableStreamController]])
+    //         is false, throw a TypeError exception.
+    if (!ControllerFromStream(stream)->is<ReadableByteStreamController>()) {
+        JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
+                                  JSMSG_READABLESTREAM_NOT_BYTE_STREAM_CONTROLLER,
+                                  "ReadableStream.getReader('byob')");
+        return nullptr;
+    }
+
+    // Step 3: If ! IsReadableStreamLocked(stream) is true, throw a TypeError
+    //         exception.
+    if (stream->locked()) {
+        JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_READABLESTREAM_LOCKED);
+        return nullptr;
+    }
+
+    Rooted<ReadableStreamBYOBReader*> reader(cx);
+    reader = NewBuiltinClassInstance<ReadableStreamBYOBReader>(cx);
+    if (!reader)
+        return nullptr;
+
+    // Step 4: Perform ! ReadableStreamReaderGenericInitialize(this, stream).
+    if (!ReadableStreamReaderGenericInitialize(cx, reader, stream))
+        return nullptr;
+
+    // Step 5: Set this.[[readIntoRequests]] to a new empty List.
+    if (!SetNewList(cx, reader, ReaderSlot_Requests))
+        return nullptr;
+
+    return reader;
+}
+
+// Streams spec, 3.6.3 new ReadableStreamBYOBReader ( stream )
+bool
+ReadableStreamBYOBReader::constructor(JSContext* cx, unsigned argc, Value* vp)
+{
+    CallArgs args = CallArgsFromVp(argc, vp);
+
+    if (!ThrowIfNotConstructing(cx, args, "ReadableStreamBYOBReader"))
+        return false;
+
+    // Step 1: If ! IsReadableStream(stream) is false, throw a TypeError exception.
+    if (!Is<ReadableStream>(args.get(0))) {
+        ReportArgTypeError(cx, "ReadableStreamBYOBReader", "ReadableStream", args.get(0));
+        return false;
+    }
+
+    Rooted<ReadableStream*> stream(cx, &args.get(0).toObject().as<ReadableStream>());
+    RootedObject reader(cx, CreateReadableStreamBYOBReader(cx, stream));
+    if (!reader)
+        return false;
+
+    args.rval().setObject(*reader);
+    return true;
+}
+
+// Streams spec, 3.6.4.1 get closed
+static MOZ_MUST_USE bool
+ReadableStreamBYOBReader_closed(JSContext* cx, unsigned argc, Value* vp)
+{
+    CallArgs args = CallArgsFromVp(argc, vp);
+
+    // Step 1: If ! IsReadableStreamBYOBReader(this) is false, return a promise
+    //         rejected with a TypeError exception.
+    if (!Is<ReadableStreamBYOBReader>(args.thisv()))
+        return RejectNonGenericMethod(cx, args, "ReadableStreamBYOBReader", "get closed");
+
+    // Step 2: Return this.[[closedPromise]].
+    NativeObject* reader = &args.thisv().toObject().as<NativeObject>();
+    args.rval().set(reader->getFixedSlot(ReaderSlot_ClosedPromise));
+    return true;
+}
+
+// Streams spec, 3.6.4.2. cancel ( reason )
+static MOZ_MUST_USE bool
+ReadableStreamBYOBReader_cancel(JSContext* cx, unsigned argc, Value* vp)
+{
+    CallArgs args = CallArgsFromVp(argc, vp);
+
+    // Step 1: If ! IsReadableStreamBYOBReader(this) is false, return a promise
+    //         rejected with a TypeError exception.
+    if (!Is<ReadableStreamBYOBReader>(args.thisv()))
+        return RejectNonGenericMethod(cx, args, "ReadableStreamBYOBReader", "cancel");
+
+    // Step 2: If this.[[ownerReadableStream]] is undefined, return a promise
+    //         rejected with a TypeError exception.
+    RootedNativeObject reader(cx, &args.thisv().toObject().as<NativeObject>());
+    if (!ReaderHasStream(reader)) {
+        JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
+                                  JSMSG_READABLESTREAMREADER_NOT_OWNED, "cancel");
+        return ReturnPromiseRejectedWithPendingError(cx, args);
+    }
+
+    // Step 3: Return ! ReadableStreamReaderGenericCancel(this, reason).
+    JSObject* cancelPromise = ReadableStreamReaderGenericCancel(cx, reader, args.get(0));
+    if (!cancelPromise)
+        return false;
+    args.rval().setObject(*cancelPromise);
+    return true;
+}
+
+// Streams spec, 3.6.4.3 read ( )
+static MOZ_MUST_USE bool
+ReadableStreamBYOBReader_read(JSContext* cx, unsigned argc, Value* vp)
+{
+    CallArgs args = CallArgsFromVp(argc, vp);
+    HandleValue viewVal = args.get(0);
+
+    // Step 1: If ! IsReadableStreamBYOBReader(this) is false, return a promise
+    //         rejected with a TypeError exception.
+    if (!Is<ReadableStreamBYOBReader>(args.thisv()))
+        return RejectNonGenericMethod(cx, args, "ReadableStreamBYOBReader", "read");
+
+    // Step 2: If this.[[ownerReadableStream]] is undefined, return a promise
+    //         rejected with a TypeError exception.
+    Rooted<ReadableStreamBYOBReader*> reader(cx);
+    reader = &args.thisv().toObject().as<ReadableStreamBYOBReader>();
+    if (!ReaderHasStream(reader)) {
+        JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
+                                  JSMSG_READABLESTREAMREADER_NOT_OWNED, "read");
+        return ReturnPromiseRejectedWithPendingError(cx, args);
+    }
+
+    // Step 3: If Type(view) is not Object, return a promise rejected with a
+    //         TypeError exception.
+    // Step 4: If view does not have a [[ViewedArrayBuffer]] internal slot,
+    //         return a promise rejected with a TypeError exception.
+    if (!Is<ArrayBufferViewObject>(viewVal)) {
+        ReportArgTypeError(cx, "ReadableStreamBYOBReader.read", "Typed Array", viewVal);
+        return ReturnPromiseRejectedWithPendingError(cx, args);
+    }
+
+    Rooted<ArrayBufferViewObject*> view(cx, &viewVal.toObject().as<ArrayBufferViewObject>());
+
+    // Step 5: If view.[[ByteLength]] is 0, return a promise rejected with a
+    //         TypeError exception.
+    // Note: It's ok to use the length in number of elements here because all we
+    // want to know is whether it's < 0.
+    if (JS_GetArrayBufferViewByteLength(view) == 0) {
+        JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
+                                  JSMSG_READABLESTREAMBYOBREADER_READ_EMPTY_VIEW);
+        return ReturnPromiseRejectedWithPendingError(cx, args);
+    }
+
+    // Step 6: Return ! ReadableStreamBYOBReaderRead(this, view).
+    JSObject* readPromise = ReadableStreamBYOBReader::read(cx, reader, view);
+    if (!readPromise)
+        return false;
+    args.rval().setObject(*readPromise);
+    return true;
+}
+
+static MOZ_MUST_USE bool
+ReadableStreamReaderGenericRelease(JSContext* cx, HandleNativeObject reader);
+
+// Streams spec, 3.6.4.4. releaseLock ( )
+static MOZ_MUST_USE bool
+ReadableStreamBYOBReader_releaseLock_impl(JSContext* cx, const CallArgs& args)
+{
+    Rooted<ReadableStreamBYOBReader*> reader(cx);
+    reader = &args.thisv().toObject().as<ReadableStreamBYOBReader>();
+
+    // Step 2: If this.[[ownerReadableStream]] is undefined, return.
+    if (!ReaderHasStream(reader)) {
+        args.rval().setUndefined();
+        return true;
+    }
+
+    // Step 3: If this.[[readRequests]] is not empty, throw a TypeError exception.
+    Value val = reader->getFixedSlot(ReaderSlot_Requests);
+    if (!val.isUndefined()) {
+        NativeObject* readRequests = &val.toObject().as<NativeObject>();
+        uint32_t len = readRequests->getDenseInitializedLength();
+        if (len != 0) {
+            JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
+                                      JSMSG_READABLESTREAMREADER_NOT_EMPTY, "releaseLock");
+            return false;
+        }
+    }
+
+    // Step 4: Perform ! ReadableStreamReaderGenericRelease(this).
+    return ReadableStreamReaderGenericRelease(cx, reader);
+}
+
+static bool
+ReadableStreamBYOBReader_releaseLock(JSContext* cx, unsigned argc, Value* vp)
+{
+    // Step 1: If ! IsReadableStreamBYOBReader(this) is false,
+    //         throw a TypeError exception.
+    CallArgs args = CallArgsFromVp(argc, vp);
+    return CallNonGenericMethod<Is<ReadableStreamBYOBReader>,
+                                ReadableStreamBYOBReader_releaseLock_impl>(cx, args);
+}
+
+static const JSPropertySpec ReadableStreamBYOBReader_properties[] = {
+    JS_PSG("closed", ReadableStreamBYOBReader_closed, 0),
+    JS_PS_END
+};
+
+static const JSFunctionSpec ReadableStreamBYOBReader_methods[] = {
+    JS_FN("cancel",         ReadableStreamBYOBReader_cancel,        1, 0),
+    JS_FN("read",           ReadableStreamBYOBReader_read,          1, 0),
+    JS_FN("releaseLock",    ReadableStreamBYOBReader_releaseLock,   0, 0),
+    JS_FS_END
+};
+
+CLASS_SPEC(ReadableStreamBYOBReader, 1, 3, ClassSpec::DontDefineConstructor, 0, JS_NULL_CLASS_OPS);
+
+inline static MOZ_MUST_USE bool
+ReadableStreamControllerCallPullIfNeeded(JSContext* cx, HandleNativeObject controller);
+
+// Streams spec, 3.7.1. IsReadableStreamDefaultReader ( x )
+// Implemented via intrinsic_isInstanceOfBuiltin<ReadableStreamDefaultReader>()
+
+// Streams spec, 3.7.2. IsReadableStreamBYOBReader ( x )
+// Implemented via intrinsic_isInstanceOfBuiltin<ReadableStreamBYOBReader>()
+
+// Streams spec, 3.7.3. ReadableStreamReaderGenericCancel ( reader, reason )
+static MOZ_MUST_USE JSObject*
+ReadableStreamReaderGenericCancel(JSContext* cx, HandleNativeObject reader, HandleValue reason)
+{
+    // Step 1: Let stream be reader.[[ownerReadableStream]].
+    Rooted<ReadableStream*> stream(cx, StreamFromReader(reader));
+
+    // Step 2: Assert: stream is not undefined (implicit).
+
+    // Step 3: Return ! ReadableStreamCancel(stream, reason).
+    return &ReadableStreamCancel(cx, stream, reason)->as<PromiseObject>();
+}
+
+// Streams spec, 3.7.4. ReadableStreamReaderGenericInitialize ( reader, stream )
+static MOZ_MUST_USE bool
+ReadableStreamReaderGenericInitialize(JSContext* cx, HandleNativeObject reader,
+                                      Handle<ReadableStream*> stream)
+{
+    // Step 1: Set reader.[[ownerReadableStream]] to stream.
+    reader->setFixedSlot(ReaderSlot_Stream, ObjectValue(*stream));
+
+    // Step 2: Set stream.[[reader]] to reader.
+    stream->setFixedSlot(StreamSlot_Reader, ObjectValue(*reader));
+
+    // Step 3: If stream.[[state]] is "readable",
+    RootedObject promise(cx);
+    if (stream->readable()) {
+        // Step a: Set reader.[[closedPromise]] to a new promise.
+        promise = PromiseObject::createSkippingExecutor(cx);
+    } else if (stream->closed()) {
+        // Step 4: Otherwise
+        // Step a: If stream.[[state]] is "closed",
+        // Step i: Set reader.[[closedPromise]] to a new promise resolved with
+        //         undefined.
+        promise = PromiseObject::unforgeableResolve(cx, UndefinedHandleValue);
+    } else {
+        // Step b: Otherwise,
+        // Step i: Assert: stream.[[state]] is "errored".
+        MOZ_ASSERT(stream->errored());
+
+        // Step ii: Set reader.[[closedPromise]] to a new promise rejected with
+        //          stream.[[storedError]].
+        RootedValue storedError(cx, stream->getFixedSlot(StreamSlot_StoredError));
+        promise = PromiseObject::unforgeableReject(cx, storedError);
+    }
+
+    if (!promise)
+        return false;
+
+    reader->setFixedSlot(ReaderSlot_ClosedPromise, ObjectValue(*promise));
+    return true;
+}
+
+// Streams spec, 3.7.5. ReadableStreamReaderGenericRelease ( reader )
+static MOZ_MUST_USE bool
+ReadableStreamReaderGenericRelease(JSContext* cx, HandleNativeObject reader)
+{
+    // Step 1: Assert: reader.[[ownerReadableStream]] is not undefined.
+    Rooted<ReadableStream*> stream(cx, StreamFromReader(reader));
+
+    // Step 2: Assert: reader.[[ownerReadableStream]].[[reader]] is reader.
+    MOZ_ASSERT(&stream->getFixedSlot(StreamSlot_Reader).toObject() == reader);
+
+    // Create an exception to reject promises with below. We don't have a
+    // clean way to do this, unfortunately.
+    JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_READABLESTREAMREADER_RELEASED);
+    RootedValue exn(cx);
+    // Not much we can do about uncatchable exceptions, just bail.
+    if (!GetAndClearException(cx, &exn))
+        return false;
+
+    // Step 3: If reader.[[ownerReadableStream]].[[state]] is "readable", reject
+    //         reader.[[closedPromise]] with a TypeError exception.
+    if (stream->readable()) {
+            Value val = reader->getFixedSlot(ReaderSlot_ClosedPromise);
+            Rooted<PromiseObject*> closedPromise(cx, &val.toObject().as<PromiseObject>());
+            if (!PromiseObject::reject(cx, closedPromise, exn))
+                return false;
+    } else {
+        // Step 4: Otherwise, set reader.[[closedPromise]] to a new promise rejected
+        //         with a TypeError exception.
+        RootedObject closedPromise(cx, PromiseObject::unforgeableReject(cx, exn));
+        if (!closedPromise)
+            return false;
+        reader->setFixedSlot(ReaderSlot_ClosedPromise, ObjectValue(*closedPromise));
+    }
+
+    // Step 5: Set reader.[[ownerReadableStream]].[[reader]] to undefined.
+    stream->setFixedSlot(StreamSlot_Reader, UndefinedValue());
+
+    // Step 6: Set reader.[[ownerReadableStream]] to undefined.
+    reader->setFixedSlot(ReaderSlot_Stream, UndefinedValue());
+
+    return true;
+}
+
+static MOZ_MUST_USE JSObject*
+ReadableByteStreamControllerPullInto(JSContext* cx,
+                                     Handle<ReadableByteStreamController*> controller,
+                                     Handle<ArrayBufferViewObject*> view);
+
+// Streams spec, 3.7.6. ReadableStreamBYOBReaderRead ( reader, view )
+/* static */ MOZ_MUST_USE JSObject*
+ReadableStreamBYOBReader::read(JSContext* cx, Handle<ReadableStreamBYOBReader*> reader,
+                               Handle<ArrayBufferViewObject*> view)
+{
+    MOZ_ASSERT(reader->is<ReadableStreamBYOBReader>());
+
+    // Step 1: Let stream be reader.[[ownerReadableStream]].
+    // Step 2: Assert: stream is not undefined.
+    Rooted<ReadableStream*> stream(cx, StreamFromReader(reader));
+
+    // Step 3: Set stream.[[disturbed]] to true.
+    SetStreamState(stream, StreamState(stream) | ReadableStream::Disturbed);
+
+    // Step 4: If stream.[[state]] is "errored", return a promise rejected with
+    //         stream.[[storedError]].
+    if (stream->errored()) {
+        RootedValue storedError(cx, stream->getFixedSlot(StreamSlot_StoredError));
+        return PromiseObject::unforgeableReject(cx, storedError);
+    }
+
+    // Step 5: Return ! ReadableByteStreamControllerPullInto(stream.[[readableStreamController]], view).
+    Rooted<ReadableByteStreamController*> controller(cx);
+    controller = &ControllerFromStream(stream)->as<ReadableByteStreamController>();
+    return ReadableByteStreamControllerPullInto(cx, controller, view);
+}
+
+static MOZ_MUST_USE JSObject*
+ReadableStreamControllerPullSteps(JSContext* cx, HandleNativeObject controller);
+
+// Streams spec, 3.7.7. ReadableStreamDefaultReaderRead ( reader )
+MOZ_MUST_USE JSObject*
+ReadableStreamDefaultReader::read(JSContext* cx, Handle<ReadableStreamDefaultReader*> reader)
+{
+    // Step 1: Let stream be reader.[[ownerReadableStream]].
+    // Step 2: Assert: stream is not undefined.
+    Rooted<ReadableStream*> stream(cx, StreamFromReader(reader));
+
+    // Step 3: Set stream.[[disturbed]] to true.
+    SetStreamState(stream, StreamState(stream) | ReadableStream::Disturbed);
+
+    // Step 4: If stream.[[state]] is "closed", return a new promise resolved with
+    //         ! CreateIterResultObject(undefined, true).
+    if (stream->closed()) {
+        RootedObject iterResult(cx, CreateIterResultObject(cx, UndefinedHandleValue, true));
+        if (!iterResult)
+            return nullptr;
+        RootedValue iterResultVal(cx, ObjectValue(*iterResult));
+        return PromiseObject::unforgeableResolve(cx, iterResultVal);
+    }
+
+    // Step 5: If stream.[[state]] is "errored", return a new promise rejected with
+    //         stream.[[storedError]].
+    if (stream->errored()) {
+        RootedValue storedError(cx, stream->getFixedSlot(StreamSlot_StoredError));
+        return PromiseObject::unforgeableReject(cx, storedError);
+    }
+
+    // Step 6: Assert: stream.[[state]] is "readable".
+    MOZ_ASSERT(stream->readable());
+
+    // Step 7: Return ! stream.[[readableStreamController]].[[PullSteps]]().
+    RootedNativeObject controller(cx, ControllerFromStream(stream));
+    return ReadableStreamControllerPullSteps(cx, controller);
+}
+
+// Streams spec, 3.8.3, step 11.a.
+// and
+// Streams spec, 3.10.3, step 16.a.
+static bool
+ControllerStartHandler(JSContext* cx, unsigned argc, Value* vp)
+{
+    CallArgs args = CallArgsFromVp(argc, vp);
+    RootedNativeObject controller(cx, TargetFromHandler<NativeObject>(args.callee()));
+
+    // Step i: Set controller.[[started]] to true.
+    AddControllerFlags(controller, ControllerFlag_Started);
+
+    // Step ii: Assert: controller.[[pulling]] is false.
+    // Step iii: Assert: controller.[[pullAgain]] is false.
+    MOZ_ASSERT(!(ControllerFlags(controller) &
+                 (ControllerFlag_Pulling | ControllerFlag_PullAgain)));
+
+    // Step iv: Perform ! ReadableStreamDefaultControllerCallPullIfNeeded(controller).
+    // or
+    // Step iv: Perform ! ReadableByteStreamControllerCallPullIfNeeded((controller).
+    if (!ReadableStreamControllerCallPullIfNeeded(cx, controller))
+        return false;
+    args.rval().setUndefined();
+    return true;
+}
+
+static MOZ_MUST_USE bool
+ReadableStreamDefaultControllerErrorIfNeeded(JSContext* cx,
+                                             Handle<ReadableStreamDefaultController*> controller,
+                                             HandleValue e);
+
+static MOZ_MUST_USE bool
+ReadableStreamControllerError(JSContext* cx, HandleNativeObject controller, HandleValue e);
+
+// Streams spec, 3.8.3, step 11.b.
+// and
+// Streams spec, 3.10.3, step 16.b.
+static bool
+ControllerStartFailedHandler(JSContext* cx, unsigned argc, Value* vp)
+{
+    CallArgs args = CallArgsFromVp(argc, vp);
+    RootedNativeObject controllerObj(cx, TargetFromHandler<NativeObject>(args.callee()));
+
+    // 3.8.3, Step 11.b.i:
+    // Perform ! ReadableStreamDefaultControllerErrorIfNeeded(controller, r).
+    if (controllerObj->is<ReadableStreamDefaultController>()) {
+        Rooted<ReadableStreamDefaultController*> controller(cx);
+        controller = &controllerObj->as<ReadableStreamDefaultController>();
+        return ReadableStreamDefaultControllerErrorIfNeeded(cx, controller, args.get(0));
+    }
+
+    // 3.10.3, Step 16.b.i: If stream.[[state]] is "readable", perform
+    //                      ! ReadableByteStreamControllerError(controller, r).
+    if (StreamFromController(controllerObj)->readable())
+        return ReadableStreamControllerError(cx, controllerObj, args.get(0));
+
+    args.rval().setUndefined();
+    return true;
+}
+
+static MOZ_MUST_USE bool
+ValidateAndNormalizeHighWaterMark(JSContext* cx,
+                                  HandleValue highWaterMarkVal,
+                                  double* highWaterMark);
+
+static MOZ_MUST_USE bool
+ValidateAndNormalizeQueuingStrategy(JSContext* cx,
+                                    HandleValue size,
+                                    HandleValue highWaterMarkVal,
+                                    double* highWaterMark);
+
+// Streams spec, 3.8.3 new ReadableStreamDefaultController ( stream, underlyingSource,
+//                                                           size, highWaterMark )
+// Steps 3 - 11.
+static MOZ_MUST_USE ReadableStreamDefaultController*
+CreateReadableStreamDefaultController(JSContext* cx, Handle<ReadableStream*> stream,
+                                      HandleValue underlyingSource, HandleValue size,
+                                      HandleValue highWaterMarkVal)
+{
+    Rooted<ReadableStreamDefaultController*> controller(cx);
+    controller = NewBuiltinClassInstance<ReadableStreamDefaultController>(cx);
+    if (!controller)
+        return nullptr;
+
+    // Step 3: Set this.[[controlledReadableStream]] to stream.
+    controller->setFixedSlot(ControllerSlot_Stream, ObjectValue(*stream));
+
+    // Step 4: Set this.[[underlyingSource]] to underlyingSource.
+    controller->setFixedSlot(ControllerSlot_UnderlyingSource, underlyingSource);
+
+    // Step 5: Perform ! ResetQueue(this).
+    if (!ResetQueue(cx, controller))
+        return nullptr;
+
+    // Step 6: Set this.[[started]], this.[[closeRequested]], this.[[pullAgain]],
+    //         and this.[[pulling]] to false.
+    controller->setFixedSlot(ControllerSlot_Flags, Int32Value(0));
+
+    // Step 7: Let normalizedStrategy be
+    //         ? ValidateAndNormalizeQueuingStrategy(size, highWaterMark).
+    double highWaterMark;
+    if (!ValidateAndNormalizeQueuingStrategy(cx, size, highWaterMarkVal, &highWaterMark))
+        return nullptr;
+
+    // Step 8: Set this.[[strategySize]] to normalizedStrategy.[[size]] and
+    //         this.[[strategyHWM]] to normalizedStrategy.[[highWaterMark]].
+    controller->setFixedSlot(DefaultControllerSlot_StrategySize, size);
+    controller->setFixedSlot(ControllerSlot_StrategyHWM, NumberValue(highWaterMark));
+
+    // Step 9: Let controller be this (implicit).
+
+    // Step 10: Let startResult be
+    //          ? InvokeOrNoop(underlyingSource, "start", « this »).
+    RootedValue startResult(cx);
+    RootedValue controllerVal(cx, ObjectValue(*controller));
+    if (!InvokeOrNoop(cx, underlyingSource, cx->names().start, controllerVal, &startResult))
+        return nullptr;
+
+    // Step 11: Let startPromise be a promise resolved with startResult:
+    RootedObject startPromise(cx, PromiseObject::unforgeableResolve(cx, startResult));
+    if (!startPromise)
+        return nullptr;
+
+    RootedObject onStartFulfilled(cx, NewHandler(cx, ControllerStartHandler, controller));
+    if (!onStartFulfilled)
+        return nullptr;
+
+    RootedObject onStartRejected(cx, NewHandler(cx, ControllerStartFailedHandler, controller));
+    if (!onStartRejected)
+        return nullptr;
+
+    if (!JS::AddPromiseReactions(cx, startPromise, onStartFulfilled, onStartRejected))
+        return nullptr;
+
+    return controller;
+}
+
+// Streams spec, 3.8.3.
+// new ReadableStreamDefaultController( stream, underlyingSource, size,
+//                                      highWaterMark )
+bool
+ReadableStreamDefaultController::constructor(JSContext* cx, unsigned argc, Value* vp)
+{
+    CallArgs args = CallArgsFromVp(argc, vp);
+
+    if (!ThrowIfNotConstructing(cx, args, "ReadableStreamDefaultController"))
+        return false;
+
+    // Step 1: If ! IsReadableStream(stream) is false, throw a TypeError exception.
+    HandleValue streamVal = args.get(0);
+    if (!Is<ReadableStream>(streamVal)) {
+        ReportArgTypeError(cx, "ReadableStreamDefaultController", "ReadableStream",
+                           args.get(0));
+        return false;
+    }
+
+    Rooted<ReadableStream*> stream(cx, &streamVal.toObject().as<ReadableStream>());
+
+    // Step 2: If stream.[[readableStreamController]] is not undefined, throw a
+    //         TypeError exception.
+    if (HasController(stream)) {
+        JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
+                                  JSMSG_READABLESTREAM_CONTROLLER_SET);
+        return false;
+    }
+
+    // Steps 3-11.
+    RootedObject controller(cx, CreateReadableStreamDefaultController(cx, stream, args.get(1),
+                                                                      args.get(2), args.get(3)));
+    if (!controller)
+        return false;
+
+    args.rval().setObject(*controller);
+    return true;
+}
+
+static MOZ_MUST_USE double
+ReadableStreamControllerGetDesiredSizeUnchecked(NativeObject* controller);
+
+// Streams spec, 3.8.4.1. get desiredSize
+// and
+// Streams spec, 3.10.4.2. get desiredSize
+static MOZ_MUST_USE bool
+ReadableStreamController_desiredSize_impl(JSContext* cx, const CallArgs& args)
+{
+    RootedNativeObject controller(cx);
+    controller = &args.thisv().toObject().as<NativeObject>();
+
+    // Streams spec, 3.9.8. steps 1-4.
+    // 3.9.8. Step 1: Let stream be controller.[[controlledReadableStream]].
+    ReadableStream* stream = StreamFromController(controller);
+
+    // 3.9.8. Step 2: Let state be stream.[[state]].
+    // 3.9.8. Step 3: If state is "errored", return null.
+    if (stream->errored()) {
+        args.rval().setNull();
+        return true;
+    }
+
+    // 3.9.8. Step 4: If state is "closed", return 0.
+    if (stream->closed()) {
+        args.rval().setInt32(0);
+        return true;
+    }
+
+    // Step 2: Return ! ReadableStreamDefaultControllerGetDesiredSize(this).
+    args.rval().setNumber(ReadableStreamControllerGetDesiredSizeUnchecked(controller));
+    return true;
+}
+
+static bool
+ReadableStreamDefaultController_desiredSize(JSContext* cx, unsigned argc, Value* vp)
+{
+    // Step 1: If ! IsReadableStreamDefaultController(this) is false, throw a
+    //         TypeError exception.
+    CallArgs args = CallArgsFromVp(argc, vp);
+    return CallNonGenericMethod<Is<ReadableStreamDefaultController>,
+                                ReadableStreamController_desiredSize_impl>(cx, args);
+}
+
+static MOZ_MUST_USE bool
+ReadableStreamDefaultControllerClose(JSContext* cx,
+                                     Handle<ReadableStreamDefaultController*> controller);
+
+// Unified implementation of steps 2-3 of 3.8.4.2 and 3.10.4.3.
+static MOZ_MUST_USE bool
+VerifyControllerStateForClosing(JSContext* cx, HandleNativeObject controller)
+{
+    // Step 2: If this.[[closeRequested]] is true, throw a TypeError exception.
+    if (ControllerFlags(controller) & ControllerFlag_CloseRequested) {
+        JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
+                                  JSMSG_READABLESTREAMCONTROLLER_CLOSED, "close");
+        return false;
+    }
+
+    // Step 3: If this.[[controlledReadableStream]].[[state]] is not "readable",
+    //         throw a TypeError exception.
+    ReadableStream* stream = StreamFromController(controller);
+    if (!stream->readable()) {
+        JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
+                                  JSMSG_READABLESTREAMCONTROLLER_NOT_READABLE, "close");
+        return false;
+    }
+
+    return true;
+}
+
+// Streams spec, 3.8.4.2 close()
+static MOZ_MUST_USE bool
+ReadableStreamDefaultController_close_impl(JSContext* cx, const CallArgs& args)
+{
+    Rooted<ReadableStreamDefaultController*> controller(cx);
+    controller = &args.thisv().toObject().as<ReadableStreamDefaultController>();
+
+    // Steps 2-3.
+    if (!VerifyControllerStateForClosing(cx, controller))
+        return false;
+
+    // Step 4: Perform ! ReadableStreamDefaultControllerClose(this).
+    if (!ReadableStreamDefaultControllerClose(cx, controller))
+        return false;
+    args.rval().setUndefined();
+    return true;
+}
+
+static bool
+ReadableStreamDefaultController_close(JSContext* cx, unsigned argc, Value* vp)
+{
+    // Step 1: If ! IsReadableStreamDefaultController(this) is false, throw a
+    //         TypeError exception.
+
+    CallArgs args = CallArgsFromVp(argc, vp);
+    return CallNonGenericMethod<Is<ReadableStreamDefaultController>,
+                                ReadableStreamDefaultController_close_impl>(cx, args);
+}
+
+static MOZ_MUST_USE bool
+ReadableStreamDefaultControllerEnqueue(JSContext* cx,
+                                       Handle<ReadableStreamDefaultController*> controller,
+                                       HandleValue chunk);
+
+// Streams spec, 3.8.4.3. enqueue ( chunk )
+static MOZ_MUST_USE bool
+ReadableStreamDefaultController_enqueue_impl(JSContext* cx, const CallArgs& args)
+{
+    Rooted<ReadableStreamDefaultController*> controller(cx);
+    controller = &args.thisv().toObject().as<ReadableStreamDefaultController>();
+
+    // Step 2: If this.[[closeRequested]] is true, throw a TypeError exception.
+    if (ControllerFlags(controller) & ControllerFlag_CloseRequested) {
+        JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
+                                  JSMSG_READABLESTREAMCONTROLLER_CLOSED, "close");
+        return false;
+    }
+
+    // Step 3: If this.[[controlledReadableStream]].[[state]] is not "readable",
+    //         throw a TypeError exception.
+    ReadableStream* stream = StreamFromController(controller);
+    if (!stream->readable()) {
+        JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
+                                  JSMSG_READABLESTREAMCONTROLLER_NOT_READABLE, "close");
+        return false;
+    }
+
+    // Step 4: Return ! ReadableStreamDefaultControllerEnqueue(this, chunk).
+    if (!ReadableStreamDefaultControllerEnqueue(cx, controller, args.get(0)))
+        return false;
+    args.rval().setUndefined();
+    return true;
+}
+
+static bool
+ReadableStreamDefaultController_enqueue(JSContext* cx, unsigned argc, Value* vp)
+{
+    // Step 1: If ! IsReadableStreamDefaultController(this) is false, throw a
+    //         TypeError exception.
+
+    CallArgs args = CallArgsFromVp(argc, vp);
+    return CallNonGenericMethod<Is<ReadableStreamDefaultController>,
+                                ReadableStreamDefaultController_enqueue_impl>(cx, args);
+}
+
+// Streams spec, 3.8.4.4. error ( e )
+static MOZ_MUST_USE bool
+ReadableStreamDefaultController_error_impl(JSContext* cx, const CallArgs& args)
+{
+    Rooted<ReadableStreamDefaultController*> controller(cx);
+    controller = &args.thisv().toObject().as<ReadableStreamDefaultController>();
+
+    // Step 2: Let stream be this.[[controlledReadableStream]].
+    // Step 3: If stream.[[state]] is not "readable", throw a TypeError exception.
+    if (!StreamFromController(controller)->readable()) {
+        JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
+                                  JSMSG_READABLESTREAMCONTROLLER_NOT_READABLE, "error");
+        return false;
+    }
+
+    // Step 4: Perform ! ReadableStreamDefaultControllerError(this, e).
+    if (!ReadableStreamControllerError(cx, controller, args.get(0)))
+        return false;
+    args.rval().setUndefined();
+    return true;
+}
+
+static bool
+ReadableStreamDefaultController_error(JSContext* cx, unsigned argc, Value* vp)
+{
+    // Step 1: If ! IsReadableStreamDefaultController(this) is false, throw a
+    //         TypeError exception.
+
+    CallArgs args = CallArgsFromVp(argc, vp);
+    return CallNonGenericMethod<Is<ReadableStreamDefaultController>,
+                                ReadableStreamDefaultController_error_impl>(cx, args);
+}
+
+static const JSPropertySpec ReadableStreamDefaultController_properties[] = {
+    JS_PSG("desiredSize", ReadableStreamDefaultController_desiredSize, 0),
+    JS_PS_END
+};
+
+static const JSFunctionSpec ReadableStreamDefaultController_methods[] = {
+    JS_FN("close",      ReadableStreamDefaultController_close,      0, 0),
+    JS_FN("enqueue",    ReadableStreamDefaultController_enqueue,    1, 0),
+    JS_FN("error",      ReadableStreamDefaultController_error,      1, 0),
+    JS_FS_END
+};
+
+CLASS_SPEC(ReadableStreamDefaultController, 4, 7, ClassSpec::DontDefineConstructor, 0,
+           JS_NULL_CLASS_OPS);
+
+/**
+ * Unified implementation of ReadableStream controllers' [[CancelSteps]] internal
+ * methods.
+ * Streams spec, 3.8.5.1. [[CancelSteps]] ( reason )
+ * and
+ * Streams spec, 3.10.5.1. [[CancelSteps]] ( reason )
+ */
+static MOZ_MUST_USE JSObject*
+ReadableStreamControllerCancelSteps(JSContext* cx, HandleNativeObject controller,
+                                    HandleValue reason)
+{
+    MOZ_ASSERT(IsReadableStreamController(controller));
+
+    // Step 1 of 3.10.5.1: If this.[[pendingPullIntos]] is not empty,
+    if (!controller->is<ReadableStreamDefaultController>()) {
+        Value val = controller->getFixedSlot(ByteControllerSlot_PendingPullIntos);
+        RootedNativeObject pendingPullIntos(cx, &val.toObject().as<NativeObject>());
+
+        if (pendingPullIntos->getDenseInitializedLength() != 0) {
+            // Step a: Let firstDescriptor be the first element of
+            //         this.[[pendingPullIntos]].
+            // Step b: Set firstDescriptor.[[bytesFilled]] to 0.
+            Rooted<PullIntoDescriptor*> firstDescriptor(cx);
+            firstDescriptor = PeekList<PullIntoDescriptor>(pendingPullIntos);
+            firstDescriptor->setBytesFilled(0);
+        }
+    }
+
+    // Step 1 of 3.8.5.1, step 2 of 3.10.5.1: Perform ! ResetQueue(this).
+    if (!ResetQueue(cx, controller))
+        return nullptr;
+
+    // Step 2 of 3.8.5.1, step 3 of 3.10.5.1:
+    // Return ! PromiseInvokeOrNoop(this.[[underlying(Byte)Source]],
+    //                              "cancel", « reason »)
+    RootedValue underlyingSource(cx);
+    underlyingSource = controller->getFixedSlot(ControllerSlot_UnderlyingSource);
+
+    if (Is<TeeState>(underlyingSource)) {
+        Rooted<TeeState*> teeState(cx, &underlyingSource.toObject().as<TeeState>());
+        Rooted<ReadableStreamDefaultController*> defaultController(cx);
+        defaultController = &controller->as<ReadableStreamDefaultController>();
+        return ReadableStreamTee_Cancel(cx, teeState, defaultController, reason);
+    }
+
+    if (ControllerFlags(controller) & ControllerFlag_ExternalSource) {
+        void* source = underlyingSource.toPrivate();
+        Rooted<ReadableStream*> stream(cx, StreamFromController(controller));
+        RootedValue rval(cx);
+        rval = cx->runtime()->readableStreamCancelCallback(cx, stream, source,
+                                                           stream->embeddingFlags(), reason);
+        return PromiseObject::unforgeableResolve(cx, rval);
+    }
+
+    return PromiseInvokeOrNoop(cx, underlyingSource, cx->names().cancel, reason);
+}
+
+inline static MOZ_MUST_USE bool
+DequeueValue(JSContext* cx, HandleNativeObject container, MutableHandleValue chunk);
+
+// Streams spec, 3.8.5.2. ReadableStreamDefaultController [[PullSteps]]()
+static JSObject*
+ReadableStreamDefaultControllerPullSteps(JSContext* cx, HandleNativeObject controller)
+{
+    MOZ_ASSERT(controller->is<ReadableStreamDefaultController>());
+
+    // Step 1: Let stream be this.[[controlledReadableStream]].
+    Rooted<ReadableStream*> stream(cx, StreamFromController(controller));
+
+    // Step 2: If this.[[queue]] is not empty,
+    RootedNativeObject queue(cx);
+    RootedValue val(cx, controller->getFixedSlot(QueueContainerSlot_Queue));
+    if (val.isObject())
+        queue = &val.toObject().as<NativeObject>();
+
+    if (queue && queue->getDenseInitializedLength() != 0) {
+        // Step a: Let chunk be ! DequeueValue(this.[[queue]]).
+        RootedValue chunk(cx);
+        if (!DequeueValue(cx, controller, &chunk))
+            return nullptr;
+
+        // Step b: If this.[[closeRequested]] is true and this.[[queue]] is empty,
+        //         perform ! ReadableStreamClose(stream).
+        bool closeRequested = ControllerFlags(controller) & ControllerFlag_CloseRequested;
+        if (closeRequested && queue->getDenseInitializedLength() == 0) {
+            if (!ReadableStreamCloseInternal(cx, stream))
+                return nullptr;
+        }
+
+        // Step c: Otherwise, perform ! ReadableStreamDefaultControllerCallPullIfNeeded(this).
+        else {
+        if (!ReadableStreamControllerCallPullIfNeeded(cx, controller))
+            return nullptr;
+        }
+
+        // Step d: Return a promise resolved with ! CreateIterResultObject(chunk, false).
+        RootedObject iterResultObj(cx, CreateIterResultObject(cx, chunk, false));
+        if (!iterResultObj)
+          return nullptr;
+        RootedValue iterResult(cx, ObjectValue(*iterResultObj));
+        return PromiseObject::unforgeableResolve(cx, iterResult);
+    }
+
+    // Step 3: Let pendingPromise be ! ReadableStreamAddReadRequest(stream).
+    Rooted<PromiseObject*> pendingPromise(cx, ReadableStreamAddReadRequest(cx, stream));
+    if (!pendingPromise)
+        return nullptr;
+
+    // Step 4: Perform ! ReadableStreamDefaultControllerCallPullIfNeeded(this).
+    if (!ReadableStreamControllerCallPullIfNeeded(cx, controller))
+        return nullptr;
+
+    // Step 5: Return pendingPromise.
+    return pendingPromise;
+}
+
+// Streams spec, 3.9.2 and 3.12.3. step 7:
+// Upon fulfillment of pullPromise,
+static bool
+ControllerPullHandler(JSContext* cx, unsigned argc, Value* vp)
+{
+    CallArgs args = CallArgsFromVp(argc, vp);
+    RootedNativeObject controller(cx, TargetFromHandler<NativeObject>(args.callee()));
+    uint32_t flags = ControllerFlags(controller);
+
+    // Step a: Set controller.[[pulling]] to false.
+    // Step b.i: Set controller.[[pullAgain]] to false.
+    RemoveControllerFlags(controller, ControllerFlag_Pulling | ControllerFlag_PullAgain);
+
+    // Step b: If controller.[[pullAgain]] is true,
+    if (flags & ControllerFlag_PullAgain) {
+        // Step ii: Perform ! ReadableByteStreamControllerCallPullIfNeeded(controller).
+        if (!ReadableStreamControllerCallPullIfNeeded(cx, controller))
+            return false;
+    }
+
+    args.rval().setUndefined();
+    return true;
+}
+
+// Streams spec, 3.9.2 and 3.12.3. step 8:
+// Upon rejection of pullPromise with reason e,
+static bool
+ControllerPullFailedHandler(JSContext* cx, unsigned argc, Value* vp)
+{
+    CallArgs args = CallArgsFromVp(argc, vp);
+    RootedNativeObject controller(cx, TargetFromHandler<NativeObject>(args.callee()));
+    HandleValue e = args.get(0);
+
+    // Step a: If controller.[[controlledReadableStream]].[[state]] is "readable",
+    //         perform ! ReadableByteStreamControllerError(controller, e).
+    if (StreamFromController(controller)->readable()) {
+        if (!ReadableStreamControllerError(cx, controller, e))
+            return false;
+    }
+
+    args.rval().setUndefined();
+    return true;
+}
+
+static bool
+ReadableStreamControllerShouldCallPull(NativeObject* controller);
+
+static MOZ_MUST_USE double
+ReadableStreamControllerGetDesiredSizeUnchecked(NativeObject* controller);
+
+// Streams spec, 3.9.2 ReadableStreamDefaultControllerCallPullIfNeeded ( controller )
+// and
+// Streams spec, 3.12.3. ReadableByteStreamControllerCallPullIfNeeded ( controller )
+inline static MOZ_MUST_USE bool
+ReadableStreamControllerCallPullIfNeeded(JSContext* cx, HandleNativeObject controller)
+{
+    // Step 1: Let shouldPull be
+    //         ! ReadableByteStreamControllerShouldCallPull(controller).
+    bool shouldPull = ReadableStreamControllerShouldCallPull(controller);
+
+    // Step 2: If shouldPull is false, return.
+    if (!shouldPull)
+        return true;
+
+    // Step 3: If controller.[[pulling]] is true,
+    if (ControllerFlags(controller) & ControllerFlag_Pulling) {
+        // Step a: Set controller.[[pullAgain]] to true.
+        AddControllerFlags(controller, ControllerFlag_PullAgain);
+
+        // Step b: Return.
+        return true;
+    }
+
+    // Step 4: Assert: controller.[[pullAgain]] is false.
+    MOZ_ASSERT(!(ControllerFlags(controller) & ControllerFlag_PullAgain));
+
+    // Step 5: Set controller.[[pulling]] to true.
+    AddControllerFlags(controller, ControllerFlag_Pulling);
+
+    // Step 6: Let pullPromise be
+    //         ! PromiseInvokeOrNoop(controller.[[underlyingByteSource]], "pull", controller).
+    RootedObject pullPromise(cx);
+    RootedValue underlyingSource(cx);
+    underlyingSource = controller->getFixedSlot(ControllerSlot_UnderlyingSource);
+    RootedValue controllerVal(cx, ObjectValue(*controller));
+
+    if (Is<TeeState>(underlyingSource)) {
+        Rooted<TeeState*> teeState(cx, &underlyingSource.toObject().as<TeeState>());
+        Rooted<ReadableStream*> stream(cx, StreamFromController(controller));
+        pullPromise = ReadableStreamTee_Pull(cx, teeState, stream);
+    } else if (ControllerFlags(controller) & ControllerFlag_ExternalSource) {
+        void* source = underlyingSource.toPrivate();
+        Rooted<ReadableStream*> stream(cx, StreamFromController(controller));
+        double desiredSize = ReadableStreamControllerGetDesiredSizeUnchecked(controller);
+        cx->runtime()->readableStreamDataRequestCallback(cx, stream, source,
+                                                         stream->embeddingFlags(), desiredSize);
+        pullPromise = PromiseObject::unforgeableResolve(cx, UndefinedHandleValue);
+    } else {
+        pullPromise = PromiseInvokeOrNoop(cx, underlyingSource, cx->names().pull, controllerVal);
+    }
+    if (!pullPromise)
+        return false;
+
+    RootedObject onPullFulfilled(cx, NewHandler(cx, ControllerPullHandler, controller));
+    if (!onPullFulfilled)
+        return false;
+
+    RootedObject onPullRejected(cx, NewHandler(cx, ControllerPullFailedHandler, controller));
+    if (!onPullRejected)
+        return false;
+
+    return JS::AddPromiseReactions(cx, pullPromise, onPullFulfilled, onPullRejected);
+
+    // Steps 7-8 implemented in functions above.
+}
+
+// Streams spec, 3.9.3. ReadableStreamDefaultControllerShouldCallPull ( controller )
+// and
+// Streams spec, 3.12.24. ReadableByteStreamControllerShouldCallPull ( controller )
+static bool
+ReadableStreamControllerShouldCallPull(NativeObject* controller)
+{
+    // Step 1: Let stream be controller.[[controlledReadableStream]].
+    ReadableStream* stream = StreamFromController(controller);
+
+    // Step 2: If stream.[[state]] is "closed" or stream.[[state]] is "errored",
+    //         return false.
+    // or, equivalently
+    // Step 2: If stream.[[state]] is not "readable", return false.
+    if (!stream->readable())
+        return false;
+
+    // Step 3: If controller.[[closeRequested]] is true, return false.
+    uint32_t flags = ControllerFlags(controller);
+    if (flags & ControllerFlag_CloseRequested)
+        return false;
+
+    // Step 4: If controller.[[started]] is false, return false.
+    if (!(flags & ControllerFlag_Started))
+        return false;
+
+    // Step 5: If ! IsReadableStreamLocked(stream) is true and
+    //         ! ReadableStreamGetNumReadRequests(stream) > 0, return true.
+    // Steps 5-6 of 3.12.24 are equivalent in our implementation.
+    if (stream->locked() && ReadableStreamGetNumReadRequests(stream) > 0)
+        return true;
+
+    // Step 6: Let desiredSize be ReadableStreamDefaultControllerGetDesiredSize(controller).
+    double desiredSize = ReadableStreamControllerGetDesiredSizeUnchecked(controller);
+
+    // Step 7: If desiredSize > 0, return true.
+    // Step 8: Return false.
+    // Steps 7-8 of 3.12.24 are equivalent in our implementation.
+    return desiredSize > 0;
+}
+
+// Streams spec, 3.9.4. ReadableStreamDefaultControllerClose ( controller )
+static MOZ_MUST_USE bool
+ReadableStreamDefaultControllerClose(JSContext* cx,
+                                     Handle<ReadableStreamDefaultController*> controller)
+{
+    // Step 1: Let stream be controller.[[controlledReadableStream]].
+    Rooted<ReadableStream*> stream(cx, StreamFromController(controller));
+
+    // Step 2: Assert: controller.[[closeRequested]] is false.
+    MOZ_ASSERT(!(ControllerFlags(controller) & ControllerFlag_CloseRequested));
+
+    // Step 3: Assert: stream.[[state]] is "readable".
+    MOZ_ASSERT(stream->readable());
+
+    // Step 4: Set controller.[[closeRequested]] to true.
+    AddControllerFlags(controller, ControllerFlag_CloseRequested);
+
+    // Step 5: If controller.[[queue]] is empty, perform ! ReadableStreamClose(stream).
+    RootedNativeObject queue(cx);
+    queue = &controller->getFixedSlot(QueueContainerSlot_Queue).toObject().as<NativeObject>();
+    if (queue->getDenseInitializedLength() == 0)
+        return ReadableStreamCloseInternal(cx, stream);
+
+    return true;
+}
+
+static MOZ_MUST_USE bool
+EnqueueValueWithSize(JSContext* cx, HandleNativeObject container, HandleValue value,
+                     HandleValue sizeVal);
+
+// Streams spec, 3.9.5. ReadableStreamDefaultControllerEnqueue ( controller, chunk )
+static MOZ_MUST_USE bool
+ReadableStreamDefaultControllerEnqueue(JSContext* cx,
+                                       Handle<ReadableStreamDefaultController*> controller,
+                                       HandleValue chunk)
+{
+    // Step 1: Let stream be controller.[[controlledReadableStream]].
+    Rooted<ReadableStream*> stream(cx, StreamFromController(controller));
+
+    // Step 2: Assert: controller.[[closeRequested]] is false.
+    MOZ_ASSERT(!(ControllerFlags(controller) & ControllerFlag_CloseRequested));
+
+    // Step 3: Assert: stream.[[state]] is "readable".
+    MOZ_ASSERT(stream->readable());
+
+    // Step 4: If ! IsReadableStreamLocked(stream) is true and
+    //         ! ReadableStreamGetNumReadRequests(stream) > 0, perform
+    //         ! ReadableStreamFulfillReadRequest(stream, chunk, false).
+    if (stream->locked() && ReadableStreamGetNumReadRequests(stream) > 0) {
+        if (!ReadableStreamFulfillReadOrReadIntoRequest(cx, stream, chunk, false))
+            return false;
+    } else {
+        // Step 5: Otherwise,
+        // Step a: Let chunkSize be 1.
+        RootedValue chunkSize(cx, NumberValue(1));
+        bool success = true;
+
+        // Step b: If controller.[[strategySize]] is not undefined,
+        RootedValue strategySize(cx);
+        strategySize = controller->getFixedSlot(DefaultControllerSlot_StrategySize);
+        if (!strategySize.isUndefined()) {
+            // Step i: Set chunkSize to Call(stream.[[strategySize]], undefined, chunk).
+            success = Call(cx, strategySize, UndefinedHandleValue, chunk, &chunkSize);
+        }
+
+        // Step c: Let enqueueResult be
+        //         EnqueueValueWithSize(controller, chunk, chunkSize).
+        if (success)
+            success = EnqueueValueWithSize(cx, controller, chunk, chunkSize);
+
+        if (!success) {
+            // Step b.ii: If chunkSize is an abrupt completion,
+            // and
+            // Step d: If enqueueResult is an abrupt completion,
+            RootedValue exn(cx);
+            if (!cx->getPendingException(&exn))
+                return false;
+
+            // Step b.ii.1: Perform
+            //         ! ReadableStreamDefaultControllerErrorIfNeeded(controller,
+            //                                                        chunkSize.[[Value]]).
+            if (!ReadableStreamDefaultControllerErrorIfNeeded(cx, controller, exn))
+                return false;
+
+            // Step b.ii.2: Return chunkSize.
+            return false;
+        }
+    }
+
+    // Step 6: Perform ! ReadableStreamDefaultControllerCallPullIfNeeded(controller).
+    if (!ReadableStreamControllerCallPullIfNeeded(cx, controller))
+        return false;
+
+    // Step 7: Return.
+    return true;
+}
+
+static MOZ_MUST_USE bool
+ReadableByteStreamControllerClearPendingPullIntos(JSContext* cx, HandleNativeObject controller);
+
+// Streams spec, 3.9.6. ReadableStreamDefaultControllerError ( controller, e )
+// and
+// Streams spec, 3.12.10. ReadableByteStreamControllerError ( controller, e )
+static MOZ_MUST_USE bool
+ReadableStreamControllerError(JSContext* cx, HandleNativeObject controller, HandleValue e)
+{
+    MOZ_ASSERT(IsReadableStreamController(controller));
+
+    // Step 1: Let stream be controller.[[controlledReadableStream]].
+    Rooted<ReadableStream*> stream(cx, StreamFromController(controller));
+
+    // Step 2: Assert: stream.[[state]] is "readable".
+    MOZ_ASSERT(stream->readable());
+
+    // Step 3 of 3.12.10:
+    // Perform ! ReadableByteStreamControllerClearPendingPullIntos(controller).
+    if (controller->is<ReadableByteStreamController>()) {
+        Rooted<ReadableByteStreamController*> byteStreamController(cx);
+        byteStreamController = &controller->as<ReadableByteStreamController>();
+        if (!ReadableByteStreamControllerClearPendingPullIntos(cx, byteStreamController))
+            return false;
+    }
+
+    // Step 3 (or 4): Perform ! ResetQueue(controller).
+    if (!ResetQueue(cx, controller))
+        return false;
+
+    // Step 4 (or 5): Perform ! ReadableStreamError(stream, e).
+    return ReadableStreamErrorInternal(cx, stream, e);
+}
+
+// Streams spec, 3.9.7. ReadableStreamDefaultControllerErrorIfNeeded ( controller, e ) nothrow
+static MOZ_MUST_USE bool
+ReadableStreamDefaultControllerErrorIfNeeded(JSContext* cx,
+                                             Handle<ReadableStreamDefaultController*> controller,
+                                             HandleValue e)
+{
+    // Step 1: If controller.[[controlledReadableStream]].[[state]] is "readable",
+    //         perform ! ReadableStreamDefaultControllerError(controller, e).
+    Rooted<ReadableStream*> stream(cx, StreamFromController(controller));
+    if (stream->readable())
+        return ReadableStreamControllerError(cx, controller, e);
+    return true;
+}
+
+// Streams spec, 3.9.8. ReadableStreamDefaultControllerGetDesiredSize ( controller )
+// and
+// Streams spec 3.12.13. ReadableByteStreamControllerGetDesiredSize ( controller )
+static MOZ_MUST_USE double
+ReadableStreamControllerGetDesiredSizeUnchecked(NativeObject* controller)
+{
+    // Steps 1-4 done at callsites, so only assert that they have been done.
+#if DEBUG
+    ReadableStream* stream = StreamFromController(controller);
+    MOZ_ASSERT(!(stream->errored() || stream->closed()));
+#endif // DEBUG
+
+    // Step 5: Return controller.[[strategyHWM]] − controller.[[queueTotalSize]].
+    double strategyHWM = controller->getFixedSlot(ControllerSlot_StrategyHWM).toNumber();
+    double queueSize = controller->getFixedSlot(QueueContainerSlot_TotalSize).toNumber();
+    return strategyHWM - queueSize;
+}
+
+// Streams spec, 3.10.3 new ReadableByteStreamController ( stream, underlyingSource,
+//                                                         highWaterMark )
+// Steps 3 - 16.
+static MOZ_MUST_USE ReadableByteStreamController*
+CreateReadableByteStreamController(JSContext* cx, Handle<ReadableStream*> stream,
+                                   HandleValue underlyingByteSource,
+                                   HandleValue highWaterMarkVal)
+{
+    Rooted<ReadableByteStreamController*> controller(cx);
+    controller = NewBuiltinClassInstance<ReadableByteStreamController>(cx);
+    if (!controller)
+        return nullptr;
+
+    // Step 3: Set this.[[controlledReadableStream]] to stream.
+    controller->setFixedSlot(ControllerSlot_Stream, ObjectValue(*stream));
+
+    // Step 4: Set this.[[underlyingByteSource]] to underlyingByteSource.
+    controller->setFixedSlot(ControllerSlot_UnderlyingSource, underlyingByteSource);
+
+    // Step 5: Set this.[[pullAgain]], and this.[[pulling]] to false.
+    controller->setFixedSlot(ControllerSlot_Flags, Int32Value(0));
+
+    // Step 6: Perform ! ReadableByteStreamControllerClearPendingPullIntos(this).
+    if (!ReadableByteStreamControllerClearPendingPullIntos(cx, controller))
+        return nullptr;
+
+    // Step 7: Perform ! ResetQueue(this).
+    if (!ResetQueue(cx, controller))
+        return nullptr;
+
+    // Step 8: Set this.[[started]] and this.[[closeRequested]] to false.
+    // These should be false by default, unchanged since step 5.
+    MOZ_ASSERT(ControllerFlags(controller) == 0);
+
+    // Step 9: Set this.[[strategyHWM]] to
+    //         ? ValidateAndNormalizeHighWaterMark(highWaterMark).
+    double highWaterMark;
+    if (!ValidateAndNormalizeHighWaterMark(cx, highWaterMarkVal, &highWaterMark))
+        return nullptr;
+    controller->setFixedSlot(ControllerSlot_StrategyHWM, NumberValue(highWaterMark));
+
+    // Step 10: Let autoAllocateChunkSize be
+    //          ? GetV(underlyingByteSource, "autoAllocateChunkSize").
+    RootedValue autoAllocateChunkSize(cx);
+    if (!GetProperty(cx, underlyingByteSource, cx->names().autoAllocateChunkSize,
+                     &autoAllocateChunkSize))
+    {
+        return nullptr;
+    }
+
+    // Step 11: If autoAllocateChunkSize is not undefined,
+    if (!autoAllocateChunkSize.isUndefined()) {
+        // Step a: If ! IsInteger(autoAllocateChunkSize) is false, or if
+        //         autoAllocateChunkSize ≤ 0, throw a RangeError exception.
+        if (!IsInteger(autoAllocateChunkSize) || autoAllocateChunkSize.toNumber() <= 0) {
+            JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
+                                      JSMSG_READABLEBYTESTREAMCONTROLLER_BAD_CHUNKSIZE);
+            return nullptr;
+    }
+    }
+
+    // Step 12: Set this.[[autoAllocateChunkSize]] to autoAllocateChunkSize.
+    controller->setFixedSlot(ByteControllerSlot_AutoAllocateSize, autoAllocateChunkSize);
+
+    // Step 13: Set this.[[pendingPullIntos]] to a new empty List.
+    if (!SetNewList(cx, controller, ByteControllerSlot_PendingPullIntos))
+        return nullptr;
+
+    // Step 14: Let controller be this (implicit).
+
+    // Step 15: Let startResult be
+    //          ? InvokeOrNoop(underlyingSource, "start", « this »).
+    RootedValue startResult(cx);
+    RootedValue controllerVal(cx, ObjectValue(*controller));
+    if (!InvokeOrNoop(cx, underlyingByteSource, cx->names().start, controllerVal, &startResult))
+        return nullptr;
+
+    // Step 16: Let startPromise be a promise resolved with startResult:
+    RootedObject startPromise(cx, PromiseObject::unforgeableResolve(cx, startResult));
+    if (!startPromise)
+        return nullptr;
+
+    RootedObject onStartFulfilled(cx, NewHandler(cx, ControllerStartHandler, controller));
+    if (!onStartFulfilled)
+        return nullptr;
+
+    RootedObject onStartRejected(cx, NewHandler(cx, ControllerStartFailedHandler, controller));
+    if (!onStartRejected)
+        return nullptr;
+
+    if (!JS::AddPromiseReactions(cx, startPromise, onStartFulfilled, onStartRejected))
+        return nullptr;
+
+    return controller;
+}
+
+bool
+ReadableByteStreamController::hasExternalSource() {
+    return ControllerFlags(this) & ControllerFlag_ExternalSource;
+}
+
+// Streams spec, 3.10.3.
+// new ReadableByteStreamController ( stream, underlyingByteSource,
+//                                    highWaterMark )
+bool
+ReadableByteStreamController::constructor(JSContext* cx, unsigned argc, Value* vp)
+{
+    CallArgs args = CallArgsFromVp(argc, vp);
+
+    if (!ThrowIfNotConstructing(cx, args, "ReadableByteStreamController"))
+        return false;
+
+    // Step 1: If ! IsReadableStream(stream) is false, throw a TypeError exception.
+    HandleValue streamVal = args.get(0);
+    if (!Is<ReadableStream>(streamVal)) {
+        ReportArgTypeError(cx, "ReadableStreamDefaultController", "ReadableStream",
+                           args.get(0));
+        return false;
+    }
+
+    Rooted<ReadableStream*> stream(cx, &streamVal.toObject().as<ReadableStream>());
+
+    // Step 2: If stream.[[readableStreamController]] is not undefined, throw a
+    //         TypeError exception.
+    if (HasController(stream)) {
+        JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
+                                  JSMSG_READABLESTREAM_CONTROLLER_SET);
+        return false;
+    }
+
+    RootedObject controller(cx, CreateReadableByteStreamController(cx, stream, args.get(1),
+                                                                   args.get(2)));
+    if (!controller)
+        return false;
+
+    args.rval().setObject(*controller);
+    return true;
+}
+
+// Version of the ReadableByteStreamConstructor that's specialized for
+// handling external, embedding-provided, underlying sources.
+static MOZ_MUST_USE ReadableByteStreamController*
+CreateReadableByteStreamController(JSContext* cx, Handle<ReadableStream*> stream,
+                                   void* underlyingSource)
+{
+    Rooted<ReadableByteStreamController*> controller(cx);
+    controller = NewBuiltinClassInstance<ReadableByteStreamController>(cx);
+    if (!controller)
+        return nullptr;
+
+    // Step 3: Set this.[[controlledReadableStream]] to stream.
+    controller->setFixedSlot(ControllerSlot_Stream, ObjectValue(*stream));
+
+    // Step 4: Set this.[[underlyingByteSource]] to underlyingByteSource.
+    controller->setFixedSlot(ControllerSlot_UnderlyingSource, PrivateValue(underlyingSource));
+
+    // Step 5: Set this.[[pullAgain]], and this.[[pulling]] to false.
+    controller->setFixedSlot(ControllerSlot_Flags, Int32Value(ControllerFlag_ExternalSource));
+
+    // Step 6: Perform ! ReadableByteStreamControllerClearPendingPullIntos(this).
+    // Omitted.
+
+    // Step 7: Perform ! ResetQueue(this).
+    controller->setFixedSlot(QueueContainerSlot_TotalSize, Int32Value(0));
+
+    // Step 8: Set this.[[started]] and this.[[closeRequested]] to false.
+    // Step 9: Set this.[[strategyHWM]] to
+    //         ? ValidateAndNormalizeHighWaterMark(highWaterMark).
+    controller->setFixedSlot(ControllerSlot_StrategyHWM, Int32Value(0));
+
+    // Step 10: Let autoAllocateChunkSize be
+    //          ? GetV(underlyingByteSource, "autoAllocateChunkSize").
+    // Step 11: If autoAllocateChunkSize is not undefined,
+    // Step 12: Set this.[[autoAllocateChunkSize]] to autoAllocateChunkSize.
+    // Omitted.
+
+    // Step 13: Set this.[[pendingPullIntos]] to a new empty List.
+    if (!SetNewList(cx, controller, ByteControllerSlot_PendingPullIntos))
+        return nullptr;
+
+    // Step 14: Let controller be this (implicit).
+    // Step 15: Let startResult be
+    //          ? InvokeOrNoop(underlyingSource, "start", « this »).
+    // Omitted.
+
+    // Step 16: Let startPromise be a promise resolved with startResult:
+    RootedObject startPromise(cx, PromiseObject::unforgeableResolve(cx, UndefinedHandleValue));
+    if (!startPromise)
+        return nullptr;
+
+    RootedObject onStartFulfilled(cx, NewHandler(cx, ControllerStartHandler, controller));
+    if (!onStartFulfilled)
+        return nullptr;
+
+    RootedObject onStartRejected(cx, NewHandler(cx, ControllerStartFailedHandler, controller));
+    if (!onStartRejected)
+        return nullptr;
+
+    if (!JS::AddPromiseReactions(cx, startPromise, onStartFulfilled, onStartRejected))
+        return nullptr;
+
+    return controller;
+}
+
+static MOZ_MUST_USE ReadableStreamBYOBRequest*
+CreateReadableStreamBYOBRequest(JSContext* cx, Handle<ReadableByteStreamController*> controller,
+                                HandleObject view);
+
+// Streams spec, 3.10.4.1. get byobRequest
+static MOZ_MUST_USE bool
+ReadableByteStreamController_byobRequest_impl(JSContext* cx, const CallArgs& args)
+{
+    Rooted<ReadableByteStreamController*> controller(cx);
+    controller = &args.thisv().toObject().as<ReadableByteStreamController>();
+
+    // Step 2: If this.[[byobRequest]] is undefined and this.[[pendingPullIntos]]
+    //         is not empty,
+    Value val = controller->getFixedSlot(ByteControllerSlot_BYOBRequest);
+    RootedValue byobRequest(cx, val);
+    val = controller->getFixedSlot(ByteControllerSlot_PendingPullIntos);
+    RootedNativeObject pendingPullIntos(cx, &val.toObject().as<NativeObject>());
+
+    if (byobRequest.isUndefined() && pendingPullIntos->getDenseInitializedLength() != 0) {
+        // Step a: Let firstDescriptor be the first element of this.[[pendingPullIntos]].
+        Rooted<PullIntoDescriptor*> firstDescriptor(cx);
+        firstDescriptor = PeekList<PullIntoDescriptor>(pendingPullIntos);
+
+        // Step b: Let view be ! Construct(%Uint8Array%,
+        //  « firstDescriptor.[[buffer]],
+        //  firstDescriptor.[[byteOffset]] + firstDescriptor.[[bytesFilled]],
+        //  firstDescriptor.[[byteLength]] − firstDescriptor.[[bytesFilled]] »).
+        RootedArrayBufferObject buffer(cx, firstDescriptor->buffer());
+        uint32_t bytesFilled = firstDescriptor->bytesFilled();
+        RootedObject view(cx, JS_NewUint8ArrayWithBuffer(cx, buffer,
+                                                         firstDescriptor->byteOffset() + bytesFilled,
+                                                         firstDescriptor->byteLength() - bytesFilled));
+        if (!view)
+            return false;
+
+        // Step c: Set this.[[byobRequest]] to
+        //         ! Construct(ReadableStreamBYOBRequest, « this, view »).
+        RootedObject request(cx, CreateReadableStreamBYOBRequest(cx, controller, view));
+        if (!request)
+            return false;
+        byobRequest = ObjectValue(*request);
+        controller->setFixedSlot(ByteControllerSlot_BYOBRequest, byobRequest);
+    }
+
+    // Step 3: Return this.[[byobRequest]].
+    args.rval().set(byobRequest);
+    return true;
+}
+
+static bool
+ReadableByteStreamController_byobRequest(JSContext* cx, unsigned argc, Value* vp)
+{
+    // Step 1: If IsReadableByteStreamController(this) is false, throw a TypeError
+    //         exception.
+    CallArgs args = CallArgsFromVp(argc, vp);
+    return CallNonGenericMethod<Is<ReadableByteStreamController>,
+                                ReadableByteStreamController_byobRequest_impl>(cx, args);
+}
+
+// Streams spec, 3.10.4.2. get desiredSize
+// Combined with 3.8.4.1 above.
+
+static bool
+ReadableByteStreamController_desiredSize(JSContext* cx, unsigned argc, Value* vp)
+{
+    // Step 1: If ! IsReadableByteStreamController(this) is false, throw a
+    //         TypeError exception.
+    CallArgs args = CallArgsFromVp(argc, vp);
+    return CallNonGenericMethod<Is<ReadableByteStreamController>,
+                                ReadableStreamController_desiredSize_impl>(cx, args);
+}
+
+static MOZ_MUST_USE bool
+ReadableByteStreamControllerClose(JSContext* cx, Handle<ReadableByteStreamController*> controller);
+
+// Streams spec, 3.10.4.3. close()
+static MOZ_MUST_USE bool
+ReadableByteStreamController_close_impl(JSContext* cx, const CallArgs& args)
+{
+    Rooted<ReadableByteStreamController*> controller(cx);
+    controller = &args.thisv().toObject().as<ReadableByteStreamController>();
+
+    // Steps 2-3.
+    if (!VerifyControllerStateForClosing(cx, controller))
+        return false;
+
+    // Step 4: Perform ? ReadableByteStreamControllerClose(this).
+    if (!ReadableByteStreamControllerClose(cx, controller))
+        return false;
+    args.rval().setUndefined();
+    return true;
+}
+
+static bool
+ReadableByteStreamController_close(JSContext* cx, unsigned argc, Value* vp)
+{
+    // Step 1: If ! IsReadableByteStreamController(this) is false, throw a
+    //         TypeError exception.
+    CallArgs args = CallArgsFromVp(argc, vp);
+    return CallNonGenericMethod<Is<ReadableByteStreamController>,
+                                ReadableByteStreamController_close_impl>(cx, args);
+}
+
+static MOZ_MUST_USE bool
+ReadableByteStreamControllerEnqueue(JSContext* cx,
+                                    Handle<ReadableByteStreamController*> controller,
+                                    HandleObject chunk);
+
+// Streams spec, 3.10.4.4. enqueue ( chunk )
+static MOZ_MUST_USE bool
+ReadableByteStreamController_enqueue_impl(JSContext* cx, const CallArgs& args)
+{
+    Rooted<ReadableByteStreamController*> controller(cx);
+    controller = &args.thisv().toObject().as<ReadableByteStreamController>();
+    HandleValue chunkVal = args.get(0);
+
+    // Step 2: If this.[[closeRequested]] is true, throw a TypeError exception.
+    if (ControllerFlags(controller) & ControllerFlag_CloseRequested) {
+        JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
+                                  JSMSG_READABLESTREAMCONTROLLER_CLOSED, "enqueue");
+        return false;
+    }
+
+    // Step 3: If this.[[controlledReadableStream]].[[state]] is not "readable",
+    //         throw a TypeError exception.
+    if (!StreamFromController(controller)->readable()) {
+        JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
+                                  JSMSG_READABLESTREAMCONTROLLER_NOT_READABLE, "enqueue");
+        return false;
+    }
+
+    // Step 4: If Type(chunk) is not Object, throw a TypeError exception.
+    // Step 5: If chunk does not have a [[ViewedArrayBuffer]] internal slot,
+    //         throw a TypeError exception.
+    if (!chunkVal.isObject() || !JS_IsArrayBufferViewObject(&chunkVal.toObject())) {
+        JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
+                                  JSMSG_READABLEBYTESTREAMCONTROLLER_BAD_CHUNK,
+                                  "ReadableByteStreamController#enqueue");
+        return false;
+    }
+    RootedObject chunk(cx, &chunkVal.toObject());
+
+    // Step 6: Return ! ReadableByteStreamControllerEnqueue(this, chunk).
+    if (!ReadableByteStreamControllerEnqueue(cx, controller, chunk))
+        return false;
+    args.rval().setUndefined();
+    return true;
+}
+
+static bool
+ReadableByteStreamController_enqueue(JSContext* cx, unsigned argc, Value* vp)
+{
+    // Step 1: If ! IsReadableByteStreamController(this) is false, throw a
+    //         TypeError exception.
+    CallArgs args = CallArgsFromVp(argc, vp);
+    return CallNonGenericMethod<Is<ReadableByteStreamController>,
+                                ReadableByteStreamController_enqueue_impl>(cx, args);
+}
+
+// Streams spec, 3.10.4.5. error ( e )
+static MOZ_MUST_USE bool
+ReadableByteStreamController_error_impl(JSContext* cx, const CallArgs& args)
+{
+    Rooted<ReadableByteStreamController*> controller(cx);
+    controller = &args.thisv().toObject().as<ReadableByteStreamController>();
+    HandleValue e = args.get(0);
+
+    // Step 2: Let stream be this.[[controlledReadableStream]].
+    // Step 3: If stream.[[state]] is not "readable", throw a TypeError exception.
+    if (!StreamFromController(controller)->readable()) {
+        JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
+                                  JSMSG_READABLESTREAMCONTROLLER_NOT_READABLE, "error");
+        return false;
+    }
+
+    // Step 4: Perform ! ReadableByteStreamControllerError(this, e).
+    if (!ReadableStreamControllerError(cx, controller, e))
+        return false;
+    args.rval().setUndefined();
+    return true;
+}
+
+static bool
+ReadableByteStreamController_error(JSContext* cx, unsigned argc, Value* vp)
+{
+    // Step 1: If ! IsReadableByteStreamController(this) is false, throw a
+    //         TypeError exception.
+    CallArgs args = CallArgsFromVp(argc, vp);
+    return CallNonGenericMethod<Is<ReadableByteStreamController>,
+                                ReadableByteStreamController_error_impl>(cx, args);
+}
+
+static const JSPropertySpec ReadableByteStreamController_properties[] = {
+    JS_PSG("byobRequest", ReadableByteStreamController_byobRequest, 0),
+    JS_PSG("desiredSize", ReadableByteStreamController_desiredSize, 0),
+    JS_PS_END
+};
+
+static const JSFunctionSpec ReadableByteStreamController_methods[] = {
+    JS_FN("close",      ReadableByteStreamController_close,     0, 0),
+    JS_FN("enqueue",    ReadableByteStreamController_enqueue,   1, 0),
+    JS_FN("error",      ReadableByteStreamController_error,     1, 0),
+    JS_FS_END
+};
+
+static void
+ReadableByteStreamControllerFinalize(FreeOp* fop, JSObject* obj)
+{
+    ReadableByteStreamController& controller = obj->as<ReadableByteStreamController>();
+
+    if (controller.getFixedSlot(ControllerSlot_Flags).isUndefined())
+        return;
+
+    uint32_t flags = ControllerFlags(&controller);
+    if (!(flags & ControllerFlag_ExternalSource))
+        return;
+
+    uint8_t embeddingFlags = flags >> ControllerEmbeddingFlagsOffset;
+
+    void* underlyingSource = controller.getFixedSlot(ControllerSlot_UnderlyingSource).toPrivate();
+    obj->runtimeFromAnyThread()->readableStreamFinalizeCallback(underlyingSource, embeddingFlags);
+}
+
+static const ClassOps ReadableByteStreamControllerClassOps = {
+    nullptr,        /* addProperty */
+    nullptr,        /* delProperty */
+    nullptr,        /* getProperty */
+    nullptr,        /* setProperty */
+    nullptr,        /* enumerate */
+    nullptr,        /* newEnumerate */
+    nullptr,        /* resolve */
+    nullptr,        /* mayResolve */
+    ReadableByteStreamControllerFinalize,
+    nullptr,        /* call        */
+    nullptr,        /* hasInstance */
+    nullptr,        /* construct   */
+    nullptr,        /* trace   */
+};
+
+CLASS_SPEC(ReadableByteStreamController, 3, 9, ClassSpec::DontDefineConstructor,
+           JSCLASS_BACKGROUND_FINALIZE, &ReadableByteStreamControllerClassOps);
+
+// Streams spec, 3.10.5.1. [[PullSteps]] ()
+// Unified with 3.8.5.1 above.
+
+static MOZ_MUST_USE bool
+ReadableByteStreamControllerHandleQueueDrain(JSContext* cx, HandleNativeObject controller);
+
+// Streams spec, 3.10.5.2. [[PullSteps]] ()
+static JSObject*
+ReadableByteStreamControllerPullSteps(JSContext* cx, HandleNativeObject controller)
+{
+    // Step 1: Let stream be this.[[controlledReadableStream]].
+    Rooted<ReadableStream*> stream(cx, StreamFromController(controller));
+
+    // Step 2: MOZ_ASSERT: ! ReadableStreamHasDefaultReader(stream) is true.
+    MOZ_ASSERT(ReadableStreamHasDefaultReader(stream));
+
+    RootedValue val(cx);
+    // Step 3: If this.[[queueTotalSize]] > 0,
+    double queueTotalSize = controller->getFixedSlot(QueueContainerSlot_TotalSize).toNumber();
+    if (queueTotalSize > 0) {
+        // Step 3.a: MOZ_ASSERT: ! ReadableStreamGetNumReadRequests(_stream_) is 0.
+        MOZ_ASSERT(ReadableStreamGetNumReadRequests(stream) == 0);
+
+        RootedObject view(cx);
+
+        if (stream->mode() == JS::ReadableStreamMode::ExternalSource) {
+            val = controller->getFixedSlot(ControllerSlot_UnderlyingSource);
+            void* underlyingSource = val.toPrivate();
+
+            view = JS_NewUint8Array(cx, queueTotalSize);
+            if (!view)
+                return nullptr;
+
+            size_t bytesWritten;
+            {
+                JS::AutoSuppressGCAnalysis noGC(cx);
+                bool dummy;
+                void* buffer = JS_GetArrayBufferViewData(view, &dummy, noGC);
+                auto cb = cx->runtime()->readableStreamWriteIntoReadRequestCallback;
+                MOZ_ASSERT(cb);
+                // TODO: use bytesWritten to correctly update the request's state.
+                cb(cx, stream, underlyingSource, stream->embeddingFlags(), buffer,
+                   queueTotalSize, &bytesWritten);
+            }
+
+            queueTotalSize = queueTotalSize - bytesWritten;
+        } else {
+            // Step 3.b: Let entry be the first element of this.[[queue]].
+            // Step 3.c: Remove entry from this.[[queue]], shifting all other elements
+            //           downward (so that the second becomes the first, and so on).
+            val = controller->getFixedSlot(QueueContainerSlot_Queue);
+            RootedNativeObject queue(cx, &val.toObject().as<NativeObject>());
+            Rooted<ByteStreamChunk*> entry(cx, ShiftFromList<ByteStreamChunk>(cx, queue));
+            MOZ_ASSERT(entry);
+
+            queueTotalSize = queueTotalSize - entry->byteLength();
+
+            // Step 3.f: Let view be ! Construct(%Uint8Array%, « entry.[[buffer]],
+            //                                   entry.[[byteOffset]], entry.[[byteLength]] »).
+            // (reordered)
+            RootedObject buffer(cx, entry->buffer());
+
+            uint32_t byteOffset = entry->byteOffset();
+            view = JS_NewUint8ArrayWithBuffer(cx, buffer, byteOffset, entry->byteLength());
+            if (!view)
+                return nullptr;
+        }
+
+        // Step 3.d: Set this.[[queueTotalSize]] to
+        //           this.[[queueTotalSize]] − entry.[[byteLength]].
+        // (reordered)
+        controller->setFixedSlot(QueueContainerSlot_TotalSize, Int32Value(queueTotalSize));
+
+        // Step 3.e: Perform ! ReadableByteStreamControllerHandleQueueDrain(this).
+        // (reordered)
+        if (!ReadableByteStreamControllerHandleQueueDrain(cx, controller))
+            return nullptr;
+
+        // Step 3.g: Return a promise resolved with ! CreateIterResultObject(view, false).
+        val.setObject(*view);
+        RootedObject iterResult(cx, CreateIterResultObject(cx, val, false));
+        if (!iterResult)
+            return nullptr;
+        val.setObject(*iterResult);
+
+        return PromiseObject::unforgeableResolve(cx, val);
+    }
+
+    // Step 4: Let autoAllocateChunkSize be this.[[autoAllocateChunkSize]].
+    val = controller->getFixedSlot(ByteControllerSlot_AutoAllocateSize);
+
+    // Step 5: If autoAllocateChunkSize is not undefined,
+    if (!val.isUndefined()) {
+        double autoAllocateChunkSize = val.toNumber();
+
+        // Step 5.a: Let buffer be Construct(%ArrayBuffer%, « autoAllocateChunkSize »).
+        RootedObject bufferObj(cx, JS_NewArrayBuffer(cx, autoAllocateChunkSize));
+
+        // Step 5.b: If buffer is an abrupt completion,
+        //           return a promise rejected with buffer.[[Value]].
+        if (!bufferObj)
+            return PromiseRejectedWithPendingError(cx);
+
+        RootedArrayBufferObject buffer(cx, &bufferObj->as<ArrayBufferObject>());
+
+        // Step 5.c: Let pullIntoDescriptor be Record {[[buffer]]: buffer.[[Value]],
+        //                                             [[byteOffset]]: 0,
+        //                                             [[byteLength]]: autoAllocateChunkSize,
+        //                                             [[bytesFilled]]: 0, [[elementSize]]: 1,
+        //                                             [[ctor]]: %Uint8Array%,
+        //                                             [[readerType]]: `"default"`}.
+        RootedObject pullIntoDescriptor(cx);
+        pullIntoDescriptor = PullIntoDescriptor::create(cx, buffer, 0,
+                                                        autoAllocateChunkSize, 0, 1,
+                                                        nullptr,
+                                                        ReaderType_Default);
+        if (!pullIntoDescriptor)
+            return PromiseRejectedWithPendingError(cx);
+
+        // Step 5.d: Append pullIntoDescriptor as the last element of this.[[pendingPullIntos]].
+        val = controller->getFixedSlot(ByteControllerSlot_PendingPullIntos);
+        RootedNativeObject pendingPullIntos(cx, &val.toObject().as<NativeObject>());
+        val = ObjectValue(*pullIntoDescriptor);
+        if (!AppendToList(cx, pendingPullIntos, val))
+            return nullptr;
+    }
+
+    // Step 6: Let promise be ! ReadableStreamAddReadRequest(stream).
+    Rooted<PromiseObject*> promise(cx, ReadableStreamAddReadRequest(cx, stream));
+    if (!promise)
+        return nullptr;
+
+    // Step 7: Perform ! ReadableByteStreamControllerCallPullIfNeeded(this).
+    if (!ReadableStreamControllerCallPullIfNeeded(cx, controller))
+        return nullptr;
+
+    // Step 8: Return promise.
+    return promise;
+}
+
+/**
+ * Unified implementation of ReadableStream controllers' [[PullSteps]] internal
+ * methods.
+ * Streams spec, 3.8.5.2. [[PullSteps]] ()
+ * and
+ * Streams spec, 3.10.5.2. [[PullSteps]] ()
+ */
+static MOZ_MUST_USE JSObject*
+ReadableStreamControllerPullSteps(JSContext* cx, HandleNativeObject controller)
+{
+    MOZ_ASSERT(IsReadableStreamController(controller));
+
+    if (controller->is<ReadableStreamDefaultController>())
+        return ReadableStreamDefaultControllerPullSteps(cx, controller);
+
+    return ReadableByteStreamControllerPullSteps(cx, controller);
+}
+
+
+static MOZ_MUST_USE ReadableStreamBYOBRequest*
+CreateReadableStreamBYOBRequest(JSContext* cx, Handle<ReadableByteStreamController*> controller,
+                                HandleObject view)
+{
+    MOZ_ASSERT(controller);
+    MOZ_ASSERT(JS_IsArrayBufferViewObject(view));
+
+    Rooted<ReadableStreamBYOBRequest*> request(cx);
+    request = NewBuiltinClassInstance<ReadableStreamBYOBRequest>(cx);
+    if (!request)
+        return nullptr;
+
+  // Step 1: Set this.[[associatedReadableByteStreamController]] to controller.
+  request->setFixedSlot(BYOBRequestSlot_Controller, ObjectValue(*controller));
+
+  // Step 2: Set this.[[view]] to view.
+  request->setFixedSlot(BYOBRequestSlot_View, ObjectValue(*view));
+
+  return request;
+}
+
+// Streams spec, 3.11.3. new ReadableStreamBYOBRequest ( controller, view )
+bool
+ReadableStreamBYOBRequest::constructor(JSContext* cx, unsigned argc, Value* vp)
+{
+    CallArgs args = CallArgsFromVp(argc, vp);
+    HandleValue controllerVal = args.get(0);
+    HandleValue viewVal = args.get(1);
+
+    if (!ThrowIfNotConstructing(cx, args, "ReadableStreamBYOBRequest"))
+        return false;
+
+    // TODO: open PR against spec to add these checks.
+    // They're expected to have happened in code using requests.
+    if (!Is<ReadableByteStreamController>(controllerVal)) {
+        ReportArgTypeError(cx, "ReadableStreamBYOBRequest",
+                           "ReadableByteStreamController", args.get(0));
+        return false;
+    }
+
+    Rooted<ReadableByteStreamController*> controller(cx);
+    controller = &controllerVal.toObject().as<ReadableByteStreamController>();
+
+    if (!viewVal.isObject() || !JS_IsArrayBufferViewObject(&viewVal.toObject())) {
+        ReportArgTypeError(cx, "ReadableStreamBYOBRequest", "ArrayBuffer view",
+                           args.get(1));
+        return false;
+    }
+
+    RootedArrayBufferObject view(cx, &viewVal.toObject().as<ArrayBufferObject>());
+
+    RootedObject request(cx, CreateReadableStreamBYOBRequest(cx, controller, view));
+    if (!request)
+        return false;
+
+    args.rval().setObject(*request);
+    return true;
+}
+
+// Streams spec, 3.11.4.1 get view
+static MOZ_MUST_USE bool
+ReadableStreamBYOBRequest_view_impl(JSContext* cx, const CallArgs& args)
+{
+    // Step 2: Return this.[[view]].
+    NativeObject* request = &args.thisv().toObject().as<NativeObject>();
+    args.rval().set(request->getFixedSlot(BYOBRequestSlot_View));
+    return true;
+}
+
+static bool
+ReadableStreamBYOBRequest_view(JSContext* cx, unsigned argc, Value* vp)
+{
+    // Step 1: If ! IsReadableStreamBYOBRequest(this) is false, throw a TypeError
+    //         exception.
+    CallArgs args = CallArgsFromVp(argc, vp);
+    return CallNonGenericMethod<Is<ReadableStreamBYOBRequest>,
+                                ReadableStreamBYOBRequest_view_impl>(cx, args);
+}
+
+static MOZ_MUST_USE bool
+ReadableByteStreamControllerRespond(JSContext* cx,
+                                    Handle<ReadableByteStreamController*> controller,
+                                    HandleValue bytesWrittenVal);
+
+// Streams spec, 3.11.4.2. respond ( bytesWritten )
+static MOZ_MUST_USE bool
+ReadableStreamBYOBRequest_respond_impl(JSContext* cx, const CallArgs& args)
+{
+    Rooted<ReadableStreamBYOBRequest*> request(cx);
+    request = &args.thisv().toObject().as<ReadableStreamBYOBRequest>();
+    HandleValue bytesWritten = args.get(0);
+
+    // Step 2: If this.[[associatedReadableByteStreamController]] is undefined,
+    //         throw a TypeError exception.
+    RootedValue controllerVal(cx, request->getFixedSlot(BYOBRequestSlot_Controller));
+    if (controllerVal.isUndefined()) {
+        JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
+                                  JSMSG_READABLESTREAMBYOBREQUEST_NO_CONTROLLER, "respond");
+        return false;
+    }
+
+    // Step 3: Return ?
+    // ReadableByteStreamControllerRespond(this.[[associatedReadableByteStreamController]],
+    //                                     bytesWritten).
+    Rooted<ReadableByteStreamController*> controller(cx);
+    controller = &controllerVal.toObject().as<ReadableByteStreamController>();
+
+    if (!ReadableByteStreamControllerRespond(cx, controller, bytesWritten))
+        return false;
+
+    args.rval().setUndefined();
+    return true;
+}
+
+static bool
+ReadableStreamBYOBRequest_respond(JSContext* cx, unsigned argc, Value* vp)
+{
+    // Step 1: If ! IsReadableStreamBYOBRequest(this) is false, throw a TypeError
+    //         exception.
+    CallArgs args = CallArgsFromVp(argc, vp);
+    return CallNonGenericMethod<Is<ReadableStreamBYOBRequest>,
+                                ReadableStreamBYOBRequest_respond_impl>(cx, args);
+}
+
+static MOZ_MUST_USE bool
+ReadableByteStreamControllerRespondWithNewView(JSContext* cx,
+                                               Handle<ReadableByteStreamController*> controller,
+                                               HandleObject view);
+
+// Streams spec, 3.11.4.3. respondWithNewView ( view )
+static MOZ_MUST_USE bool
+ReadableStreamBYOBRequest_respondWithNewView_impl(JSContext* cx, const CallArgs& args)
+{
+    Rooted<ReadableStreamBYOBRequest*> request(cx);
+    request = &args.thisv().toObject().as<ReadableStreamBYOBRequest>();
+    HandleValue viewVal = args.get(0);
+
+    // Step 2: If this.[[associatedReadableByteStreamController]] is undefined,
+    //         throw a TypeError exception.
+    RootedValue controllerVal(cx, request->getFixedSlot(BYOBRequestSlot_Controller));
+    if (controllerVal.isUndefined()) {
+        JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
+                                  JSMSG_READABLESTREAMBYOBREQUEST_NO_CONTROLLER, "respondWithNewView");
+        return false;
+    }
+
+    // Step 3: If Type(chunk) is not Object, throw a TypeError exception.
+    // Step 4: If view does not have a [[ViewedArrayBuffer]] internal slot, throw
+    //         a TypeError exception.
+    if (!viewVal.isObject() || !JS_IsArrayBufferViewObject(&viewVal.toObject())) {
+        JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
+                                  JSMSG_READABLEBYTESTREAMCONTROLLER_BAD_CHUNK,
+                                  "ReadableStreamBYOBRequest#respondWithNewView");
+        return false;
+    }
+
+    // Step 5: Return ?
+    // ReadableByteStreamControllerRespondWithNewView(this.[[associatedReadableByteStreamController]],
+    //                                                view).
+    Rooted<ReadableByteStreamController*> controller(cx);
+    controller = &controllerVal.toObject().as<ReadableByteStreamController>();
+    RootedObject view(cx, &viewVal.toObject());
+
+    if (!ReadableByteStreamControllerRespondWithNewView(cx, controller, view))
+        return false;
+
+    args.rval().setUndefined();
+    return true;
+}
+
+static bool
+ReadableStreamBYOBRequest_respondWithNewView(JSContext* cx, unsigned argc, Value* vp)
+{
+    // Step 1: If ! IsReadableStreamBYOBRequest(this) is false, throw a TypeError
+    //         exception.
+    CallArgs args = CallArgsFromVp(argc, vp);
+    return CallNonGenericMethod<Is<ReadableStreamBYOBRequest>,
+                                ReadableStreamBYOBRequest_respondWithNewView_impl>(cx, args);
+}
+
+static const JSPropertySpec ReadableStreamBYOBRequest_properties[] = {
+    JS_PSG("view", ReadableStreamBYOBRequest_view, 0),
+    JS_PS_END
+};
+
+static const JSFunctionSpec ReadableStreamBYOBRequest_methods[] = {
+    JS_FN("respond",            ReadableStreamBYOBRequest_respond,            1, 0),
+    JS_FN("respondWithNewView", ReadableStreamBYOBRequest_respondWithNewView, 1, 0),
+    JS_FS_END
+};
+
+CLASS_SPEC(ReadableStreamBYOBRequest, 3, 2, ClassSpec::DontDefineConstructor, 0,
+           JS_NULL_CLASS_OPS);
+
+// Streams spec, 3.12.1. IsReadableStreamBYOBRequest ( x )
+// Implemented via is<ReadableStreamBYOBRequest>()
+
+// Streams spec, 3.12.2. IsReadableByteStreamController ( x )
+// Implemented via is<ReadableByteStreamController>()
+
+// Streams spec, 3.12.3. ReadableByteStreamControllerCallPullIfNeeded ( controller )
+// Unified with 3.9.2 above.
+
+static void
+ReadableByteStreamControllerInvalidateBYOBRequest(NativeObject* controller);
+
+// Streams spec, 3.12.4. ReadableByteStreamControllerClearPendingPullIntos ( controller )
+static MOZ_MUST_USE bool
+ReadableByteStreamControllerClearPendingPullIntos(JSContext* cx, HandleNativeObject controller)
+{
+    MOZ_ASSERT(controller->is<ReadableByteStreamController>());
+
+    // Step 1: Perform ! ReadableByteStreamControllerInvalidateBYOBRequest(controller).
+    ReadableByteStreamControllerInvalidateBYOBRequest(controller);
+
+    // Step 2: Set controller.[[pendingPullIntos]] to a new empty List.
+    return SetNewList(cx, controller, ByteControllerSlot_PendingPullIntos);
+}
+
+// Streams spec, 3.12.5. ReadableByteStreamControllerClose ( controller )
+static MOZ_MUST_USE bool
+ReadableByteStreamControllerClose(JSContext* cx, Handle<ReadableByteStreamController*> controller)
+{
+    // Step 1: Let stream be controller.[[controlledReadableStream]].
+    Rooted<ReadableStream*> stream(cx, StreamFromController(controller));
+
+    // Step 2: Assert: controller.[[closeRequested]] is false.
+    MOZ_ASSERT(!(ControllerFlags(controller) & ControllerFlag_CloseRequested));
+
+    // Step 3: Assert: stream.[[state]] is "readable".
+    MOZ_ASSERT(stream->readable());
+
+    // Step 4: If controller.[[queueTotalSize]] > 0,
+    double queueTotalSize = controller->getFixedSlot(QueueContainerSlot_TotalSize).toNumber();
+    if (queueTotalSize > 0) {
+        // Step a: Set controller.[[closeRequested]] to true.
+        AddControllerFlags(controller, ControllerFlag_CloseRequested);
+
+        // Step b: Return
+        return true;
+    }
+
+    // Step 5: If controller.[[pendingPullIntos]] is not empty,
+    RootedValue val(cx, controller->getFixedSlot(ByteControllerSlot_PendingPullIntos));
+    RootedNativeObject pendingPullIntos(cx, &val.toObject().as<NativeObject>());
+    if (pendingPullIntos->getDenseInitializedLength() != 0) {
+        // Step a: Let firstPendingPullInto be the first element of
+        //         controller.[[pendingPullIntos]].
+        Rooted<PullIntoDescriptor*> firstPendingPullInto(cx);
+        firstPendingPullInto = PeekList<PullIntoDescriptor>(pendingPullIntos);
+
+        // Step b: If firstPendingPullInto.[[bytesFilled]] > 0,
+        if (firstPendingPullInto->bytesFilled() > 0) {
+            // Step i: Let e be a new TypeError exception. {
+            JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
+                                      JSMSG_READABLEBYTESTREAMCONTROLLER_CLOSE_PENDING_PULL);
+            RootedValue e(cx);
+            // Not much we can do about uncatchable exceptions, just bail.
+            if (!cx->getPendingException(&e))
+                return false;
+            // Step ii: Perform ! ReadableByteStreamControllerError(controller, e).
+            if (!ReadableStreamControllerError(cx, controller, e))
+                return false;
+
+            // Step iii: Throw e.
+            return false;
+        }
+    }
+
+    // Step 6: Perform ! ReadableStreamClose(stream).
+    return ReadableStreamCloseInternal(cx, stream);
+}
+
+static MOZ_MUST_USE JSObject*
+ReadableByteStreamControllerConvertPullIntoDescriptor(JSContext* cx,
+                                                      Handle<PullIntoDescriptor*> pullIntoDescriptor);
+
+// Streams spec, 3.12.6. ReadableByteStreamControllerCommitPullIntoDescriptor ( stream, pullIntoDescriptor )
+static MOZ_MUST_USE bool
+ReadableByteStreamControllerCommitPullIntoDescriptor(JSContext* cx, Handle<ReadableStream*> stream,
+                                                     Handle<PullIntoDescriptor*> pullIntoDescriptor)
+{
+    // Step 1: MOZ_ASSERT: stream.[[state]] is not "errored".
+    MOZ_ASSERT(!stream->errored());
+
+    // Step 2: Let done be false.
+    bool done = false;
+
+    // Step 3: If stream.[[state]] is "closed",
+    if (stream->closed()) {
+        // Step a: MOZ_ASSERT: pullIntoDescriptor.[[bytesFilled]] is 0.
+        MOZ_ASSERT(pullIntoDescriptor->bytesFilled() == 0);
+
+        // Step b: Set done to true.
+        done = true;
+    }
+
+    // Step 4: Let filledView be
+    //         ! ReadableByteStreamControllerConvertPullIntoDescriptor(pullIntoDescriptor).
+    RootedObject filledView(cx);
+    filledView = ReadableByteStreamControllerConvertPullIntoDescriptor(cx, pullIntoDescriptor);
+    if (!filledView)
+        return false;
+
+    // Step 5: If pullIntoDescriptor.[[readerType]] is "default",
+    uint32_t readerType = pullIntoDescriptor->readerType();
+    RootedValue filledViewVal(cx, ObjectValue(*filledView));
+    if (readerType == ReaderType_Default) {
+        // Step a: Perform ! ReadableStreamFulfillReadRequest(stream, filledView, done).
+        if (!ReadableStreamFulfillReadOrReadIntoRequest(cx, stream, filledViewVal, done))
+            return false;
+    } else {
+        // Step 6: Otherwise,
+        // Step a: MOZ_ASSERT: pullIntoDescriptor.[[readerType]] is "byob".
+        MOZ_ASSERT(readerType == ReaderType_BYOB);
+
+        // Step b: Perform ! ReadableStreamFulfillReadIntoRequest(stream, filledView, done).
+        if (!ReadableStreamFulfillReadOrReadIntoRequest(cx, stream, filledViewVal, done))
+            return false;
+    }
+
+    return true;
+}
+
+// Streams spec, 3.12.7. ReadableByteStreamControllerConvertPullIntoDescriptor ( pullIntoDescriptor )
+static MOZ_MUST_USE JSObject*
+ReadableByteStreamControllerConvertPullIntoDescriptor(JSContext* cx,
+                                                      Handle<PullIntoDescriptor*> pullIntoDescriptor)
+{
+    // Step 1: Let bytesFilled be pullIntoDescriptor.[[bytesFilled]].
+    uint32_t bytesFilled = pullIntoDescriptor->bytesFilled();
+
+    // Step 2: Let elementSize be pullIntoDescriptor.[[elementSize]].
+    uint32_t elementSize = pullIntoDescriptor->elementSize();
+
+    // Step 3: Assert: bytesFilled <= pullIntoDescriptor.[[byteLength]].
+    MOZ_ASSERT(bytesFilled <= pullIntoDescriptor->byteLength());
+
+    // Step 4: Assert: bytesFilled mod elementSize is 0.
+    MOZ_ASSERT(bytesFilled % elementSize == 0);
+
+    // Step 5: Return ! Construct(pullIntoDescriptor.[[ctor]],
+    //                            pullIntoDescriptor.[[buffer]],
+    //                            pullIntoDescriptor.[[byteOffset]],
+    //                            bytesFilled / elementSize).
+    RootedObject ctor(cx, pullIntoDescriptor->ctor());
+    if (!ctor) {
+        if (!GetBuiltinConstructor(cx, JSProto_Uint8Array, &ctor))
+            return nullptr;
+    }
+    RootedObject buffer(cx, pullIntoDescriptor->buffer());
+    uint32_t byteOffset = pullIntoDescriptor->byteOffset();
+    FixedConstructArgs<3> args(cx);
+    args[0].setObject(*buffer);
+    args[1].setInt32(byteOffset);
+    args[2].setInt32(bytesFilled / elementSize);
+    return JS_New(cx, ctor, args);
+}
+
+static MOZ_MUST_USE bool
+ReadableByteStreamControllerEnqueueChunkToQueue(JSContext* cx,
+                                                Handle<ReadableByteStreamController*> controller,
+                                                HandleObject buffer, uint32_t byteOffset,
+                                                uint32_t byteLength);
+
+static MOZ_MUST_USE ArrayBufferObject*
+TransferArrayBuffer(JSContext* cx, HandleObject buffer);
+
+static MOZ_MUST_USE bool
+ReadableByteStreamControllerProcessPullIntoDescriptorsUsingQueue(JSContext* cx,
+                                                                 Handle<ReadableByteStreamController*> controller);
+
+// Streams spec, 3.12.8. ReadableByteStreamControllerEnqueue ( controller, chunk )
+static MOZ_MUST_USE bool
+ReadableByteStreamControllerEnqueue(JSContext* cx,
+                                    Handle<ReadableByteStreamController*> controller,
+                                    HandleObject chunk)
+{
+    // Step 1: Let stream be controller.[[controlledReadableStream]].
+    Rooted<ReadableStream*> stream(cx, StreamFromController(controller));
+
+    // Step 2: Assert: controller.[[closeRequested]] is false.
+    MOZ_ASSERT(!(ControllerFlags(controller) & ControllerFlag_CloseRequested));
+
+    // Step 3: Assert: stream.[[state]] is "readable".
+    MOZ_ASSERT(stream->readable());
+
+    // To make enqueuing chunks via JSAPI nicer, we want to be able to deal
+    // with ArrayBuffer objects in addition to ArrayBuffer views here.
+    // This cannot happen when enqueuing happens via
+    // ReadableByteStreamController_enqueue because that throws if invoked
+    // with anything but an ArrayBuffer view.
+
+    Rooted<ArrayBufferObject*> buffer(cx);
+    uint32_t byteOffset;
+    uint32_t byteLength;
+
+    if (chunk->is<ArrayBufferObject>()) {
+        // Steps 4-6 for ArrayBuffer objects.
+        buffer = &chunk->as<ArrayBufferObject>();
+        byteOffset = 0;
+        byteLength = buffer->byteLength();
+    } else {
+        // Step 4: Let buffer be chunk.[[ViewedArrayBuffer]].
+        bool dummy;
+        JSObject* bufferObj = JS_GetArrayBufferViewBuffer(cx, chunk, &dummy);
+        if (!bufferObj)
+            return false;
+        buffer = &bufferObj->as<ArrayBufferObject>();
+
+        // Step 5: Let byteOffset be chunk.[[ByteOffset]].
+        byteOffset = JS_GetArrayBufferViewByteOffset(chunk);
+
+        // Step 6: Let byteLength be chunk.[[ByteLength]].
+        byteLength = JS_GetArrayBufferViewByteLength(chunk);
+    }
+
+    // Step 7: Let transferredBuffer be ! TransferArrayBuffer(buffer).
+    RootedArrayBufferObject transferredBuffer(cx, TransferArrayBuffer(cx, buffer));
+    if (!transferredBuffer)
+        return false;
+
+    // Step 8: If ! ReadableStreamHasDefaultReader(stream) is true
+    if (ReadableStreamHasDefaultReader(stream)) {
+        // Step a: If ! ReadableStreamGetNumReadRequests(stream) is 0,
+        if (ReadableStreamGetNumReadRequests(stream) == 0) {
+            // Step i: Perform
+            //         ! ReadableByteStreamControllerEnqueueChunkToQueue(controller,
+            //                                                           transferredBuffer,
+            //                                                           byteOffset,
+            //                                                           byteLength).
+            if (!ReadableByteStreamControllerEnqueueChunkToQueue(cx, controller, transferredBuffer,
+                                                                 byteOffset, byteLength))
+            {
+                return false;
+            }
+        } else {
+            // Step b: Otherwise,
+            // Step i: Assert: controller.[[queue]] is empty.
+#if DEBUG
+            RootedValue val(cx, controller->getFixedSlot(QueueContainerSlot_Queue));
+            RootedNativeObject queue(cx, &val.toObject().as<NativeObject>());
+            MOZ_ASSERT(queue->getDenseInitializedLength() == 0);
+#endif // DEBUG
+
+            // Step ii: Let transferredView be
+            //          ! Construct(%Uint8Array%, transferredBuffer, byteOffset, byteLength).
+            RootedObject transferredView(cx, JS_NewUint8ArrayWithBuffer(cx, transferredBuffer,
+                                                                        byteOffset, byteLength));
+            if (!transferredView)
+                return false;
+
+            // Step iii: Perform ! ReadableStreamFulfillReadRequest(stream, transferredView, false).
+            RootedValue chunk(cx, ObjectValue(*transferredView));
+            if (!ReadableStreamFulfillReadOrReadIntoRequest(cx, stream, chunk, false))
+                return false;
+        }
+    } else if (ReadableStreamHasBYOBReader(stream)) {
+        // Step 9: Otherwise,
+        // Step a: If ! ReadableStreamHasBYOBReader(stream) is true,
+        // Step i: Perform
+        //         ! ReadableByteStreamControllerEnqueueChunkToQueue(controller,
+        //                                                           transferredBuffer,
+        //                                                           byteOffset,
+        //                                                           byteLength).
+        if (!ReadableByteStreamControllerEnqueueChunkToQueue(cx, controller, transferredBuffer,
+                                                             byteOffset, byteLength))
+        {
+            return false;
+        }
+
+        // Step ii: Perform ! ReadableByteStreamControllerProcessPullIntoDescriptorsUsingQueue(controller).
+        if (!ReadableByteStreamControllerProcessPullIntoDescriptorsUsingQueue(cx, controller))
+            return false;
+    } else {
+        // Step b: Otherwise,
+        // Step i: Assert: ! IsReadableStreamLocked(stream) is false.
+        MOZ_ASSERT(!stream->locked());
+
+        // Step ii: Perform
+        //          ! ReadableByteStreamControllerEnqueueChunkToQueue(controller,
+        //                                                            transferredBuffer,
+        //                                                            byteOffset,
+        //                                                            byteLength).
+        if (!ReadableByteStreamControllerEnqueueChunkToQueue(cx, controller, transferredBuffer,
+                                                            byteOffset, byteLength))
+        {
+            return false;
+        }
+    }
+
+    return true;
+}
+
+// Streams spec, 3.12.9.
+// ReadableByteStreamControllerEnqueueChunkToQueue ( controller, buffer,
+//                                                   byteOffset, byteLength )
+static MOZ_MUST_USE bool
+ReadableByteStreamControllerEnqueueChunkToQueue(JSContext* cx,
+                                                Handle<ReadableByteStreamController*> controller,
+                                                HandleObject buffer, uint32_t byteOffset,
+                                                uint32_t byteLength)
+{
+    MOZ_ASSERT(controller->is<ReadableByteStreamController>(), "must operate on ReadableByteStreamController");
+
+    // Step 1: Append Record {[[buffer]]: buffer,
+    //                        [[byteOffset]]: byteOffset,
+    //                        [[byteLength]]: byteLength}
+    //         as the last element of controller.[[queue]].
+    RootedValue val(cx, controller->getFixedSlot(QueueContainerSlot_Queue));
+    RootedNativeObject queue(cx, &val.toObject().as<NativeObject>());
+
+    Rooted<ByteStreamChunk*> chunk(cx);
+    chunk = ByteStreamChunk::create(cx, buffer, byteOffset, byteLength);
+    if (!chunk)
+        return false;
+
+    RootedValue chunkVal(cx, ObjectValue(*chunk));
+    if (!AppendToList(cx, queue, chunkVal))
+        return false;
+
+    // Step 2: Add byteLength to controller.[[queueTotalSize]].
+    double queueTotalSize = controller->getFixedSlot(QueueContainerSlot_TotalSize).toNumber();
+    controller->setFixedSlot(QueueContainerSlot_TotalSize,
+                             NumberValue(queueTotalSize + byteLength));
+
+    return true;
+}
+
+// Streams spec, 3.12.10. ReadableByteStreamControllerError ( controller, e )
+// Unified with 3.9.6 above.
+
+// Streams spec, 3.12.11. ReadableByteStreamControllerFillHeadPullIntoDescriptor ( controler, size, pullIntoDescriptor )
+static void
+ReadableByteStreamControllerFillHeadPullIntoDescriptor(ReadableByteStreamController* controller, uint32_t size,
+                                                       Handle<PullIntoDescriptor*> pullIntoDescriptor)
+{
+    // Step 1: Assert: either controller.[[pendingPullIntos]] is empty, or the
+    //         first element of controller.[[pendingPullIntos]] is pullIntoDescriptor.
+#if DEBUG
+    Value val = controller->getFixedSlot(ByteControllerSlot_PendingPullIntos);
+    NativeObject* pendingPullIntos = &val.toObject().as<NativeObject>();
+    MOZ_ASSERT(pendingPullIntos->getDenseInitializedLength() == 0 ||
+               &pendingPullIntos->getDenseElement(0).toObject() == pullIntoDescriptor);
+#endif // DEBUG
+
+    // Step 2: Perform ! ReadableByteStreamControllerInvalidateBYOBRequest(controller).
+    ReadableByteStreamControllerInvalidateBYOBRequest(controller);
+
+    // Step 3: Set pullIntoDescriptor.[[bytesFilled]] to pullIntoDescriptor.[[bytesFilled]] + size.
+    pullIntoDescriptor->setBytesFilled(pullIntoDescriptor->bytesFilled() + size);
+}
+
+// Streams spec, 3.12.12. ReadableByteStreamControllerFillPullIntoDescriptorFromQueue ( controller, pullIntoDescriptor )
+static MOZ_MUST_USE bool
+ReadableByteStreamControllerFillPullIntoDescriptorFromQueue(JSContext* cx,
+                                                            Handle<ReadableByteStreamController*> controller,
+                                                            Handle<PullIntoDescriptor*> pullIntoDescriptor,
+                                                            bool* ready)
+{
+    *ready = false;
+
+    // Step 1: Let elementSize be pullIntoDescriptor.[[elementSize]].
+    uint32_t elementSize = pullIntoDescriptor->elementSize();
+
+    // Step 2: Let currentAlignedBytes be pullIntoDescriptor.[[bytesFilled]] −
+    //         (pullIntoDescriptor.[[bytesFilled]] mod elementSize).
+    uint32_t bytesFilled = pullIntoDescriptor->bytesFilled();
+    uint32_t currentAlignedBytes = bytesFilled - (bytesFilled % elementSize);
+
+    // Step 3: Let maxBytesToCopy be min(controller.[[queueTotalSize]],
+    //         pullIntoDescriptor.[[byteLength]] − pullIntoDescriptor.[[bytesFilled]]).
+    uint32_t byteLength = pullIntoDescriptor->byteLength();
+
+    // The queue size could be negative or overflow uint32_t. We cannot
+    // validly have a maxBytesToCopy value that'd overflow uint32_t, though,
+    // so just clamp to that.
+    Value sizeVal = controller->getFixedSlot(QueueContainerSlot_TotalSize);
+    uint32_t queueTotalSize = JS::ToUint32(sizeVal.toNumber());
+    uint32_t maxBytesToCopy = std::min(queueTotalSize, byteLength - bytesFilled);
+
+    // Step 4: Let maxBytesFilled be pullIntoDescriptor.[[bytesFilled]] + maxBytesToCopy.
+    uint32_t maxBytesFilled = bytesFilled + maxBytesToCopy;
+
+    // Step 5: Let maxAlignedBytes be maxBytesFilled − (maxBytesFilled mod elementSize).
+    uint32_t maxAlignedBytes = maxBytesFilled - (maxBytesFilled % elementSize);
+
+    // Step 6: Let totalBytesToCopyRemaining be maxBytesToCopy.
+    uint32_t totalBytesToCopyRemaining = maxBytesToCopy;
+
+    // Step 7: Let ready be false (implicit).
+
+    // Step 8: If maxAlignedBytes > currentAlignedBytes,
+    if (maxAlignedBytes > currentAlignedBytes) {
+        // Step a: Set totalBytesToCopyRemaining to maxAlignedBytes −
+        //         pullIntoDescriptor.[[bytesFilled]].
+        totalBytesToCopyRemaining = maxAlignedBytes - bytesFilled;
+
+        // Step b: Let ready be true.
+        *ready = true;
+    }
+
+    if (ControllerFlags(controller) & ControllerFlag_ExternalSource) {
+        // TODO: it probably makes sense to eagerly drain the underlying source.
+        // We have a buffer lying around anyway, whereas the source might be
+        // able to free or reuse buffers once their content is copied into
+        // our buffer.
+        if (!ready)
+            return true;
+
+        Value val = controller->getFixedSlot(ControllerSlot_UnderlyingSource);
+        void* underlyingSource = val.toPrivate();
+
+        RootedArrayBufferObject targetBuffer(cx, pullIntoDescriptor->buffer());
+        Rooted<ReadableStream*> stream(cx, StreamFromController(controller));
+
+        size_t bytesWritten;
+        {
+            JS::AutoSuppressGCAnalysis noGC(cx);
+            bool dummy;
+            uint8_t* buffer = JS_GetArrayBufferData(targetBuffer, &dummy, noGC);
+            buffer += bytesFilled;
+            auto cb = cx->runtime()->readableStreamWriteIntoReadRequestCallback;
+            MOZ_ASSERT(cb);
+            cb(cx, stream, underlyingSource, stream->embeddingFlags(), buffer,
+               totalBytesToCopyRemaining, &bytesWritten);
+            pullIntoDescriptor->setBytesFilled(bytesFilled + bytesWritten);
+        }
+
+        queueTotalSize -= bytesWritten;
+        controller->setFixedSlot(QueueContainerSlot_TotalSize, Int32Value(queueTotalSize));
+
+        return true;
+    }
+
+    // Step 9: Let queue be controller.[[queue]].
+    RootedValue val(cx, controller->getFixedSlot(QueueContainerSlot_Queue));
+    RootedNativeObject queue(cx, &val.toObject().as<NativeObject>());
+
+    // Step 10: Repeat the following steps while totalBytesToCopyRemaining > 0,
+    Rooted<ByteStreamChunk*> headOfQueue(cx);
+    while (totalBytesToCopyRemaining > 0) {
+        MOZ_ASSERT(queue->getDenseInitializedLength() != 0);
+
+        // Step a: Let headOfQueue be the first element of queue.
+        headOfQueue = PeekList<ByteStreamChunk>(queue);
+
+        // Step b: Let bytesToCopy be min(totalBytesToCopyRemaining,
+        //                                headOfQueue.[[byteLength]]).
+        uint32_t byteLength = headOfQueue->byteLength();
+        uint32_t bytesToCopy = std::min(totalBytesToCopyRemaining, byteLength);
+
+        // Step c: Let destStart be pullIntoDescriptor.[[byteOffset]] +
+        //         pullIntoDescriptor.[[bytesFilled]].
+        uint32_t destStart = pullIntoDescriptor->byteOffset() + bytesFilled;
+
+        // Step d: Perform ! CopyDataBlockBytes(pullIntoDescriptor.[[buffer]].[[ArrayBufferData]],
+        //                                      destStart,
+        //                                      headOfQueue.[[buffer]].[[ArrayBufferData]],
+        //                                      headOfQueue.[[byteOffset]],
+        //                                      bytesToCopy).
+        RootedArrayBufferObject sourceBuffer(cx, headOfQueue->buffer());
+        uint32_t sourceOffset = headOfQueue->byteOffset();
+        RootedArrayBufferObject targetBuffer(cx, pullIntoDescriptor->buffer());
+        ArrayBufferObject::copyData(targetBuffer, destStart, sourceBuffer, sourceOffset,
+                                    bytesToCopy);
+
+        // Step e: If headOfQueue.[[byteLength]] is bytesToCopy,
+        if (byteLength == bytesToCopy) {
+            // Step i: Remove the first element of queue, shifting all other elements
+            //         downward (so that the second becomes the first, and so on).
+            headOfQueue = ShiftFromList<ByteStreamChunk>(cx, queue);
+            MOZ_ASSERT(headOfQueue);
+        } else {
+            // Step f: Otherwise,
+            // Step i: Set headOfQueue.[[byteOffset]] to headOfQueue.[[byteOffset]] +
+            //         bytesToCopy.
+            headOfQueue->SetByteOffset(sourceOffset + bytesToCopy);
+
+            // Step ii: Set headOfQueue.[[byteLength]] to headOfQueue.[[byteLength]] −
+            //          bytesToCopy.
+            headOfQueue->SetByteLength(byteLength - bytesToCopy);
+        }
+
+        // Step g: Set controller.[[queueTotalSize]] to
+        //         controller.[[queueTotalSize]] − bytesToCopy.
+        queueTotalSize = controller->getFixedSlot(QueueContainerSlot_TotalSize).toNumber();
+        queueTotalSize -= bytesToCopy;
+        controller->setFixedSlot(QueueContainerSlot_TotalSize, NumberValue(queueTotalSize));
+
+        // Step h: Perform ! ReadableByteStreamControllerFillHeadPullIntoDescriptor(controller,
+        //                                                                          bytesToCopy,
+        //                                                                          pullIntoDescriptor).
+        ReadableByteStreamControllerFillHeadPullIntoDescriptor(controller, bytesToCopy,
+                                                               pullIntoDescriptor);
+        bytesFilled += bytesToCopy;
+        MOZ_ASSERT(bytesFilled == pullIntoDescriptor->bytesFilled());
+
+        // Step i: Set totalBytesToCopyRemaining to totalBytesToCopyRemaining − bytesToCopy.
+        totalBytesToCopyRemaining -= bytesToCopy;
+    }
+
+    // Step 11: If ready is false,
+    if (!*ready) {
+        // Step a: Assert: controller.[[queueTotalSize]] is 0.
+        MOZ_ASSERT(controller->getFixedSlot(QueueContainerSlot_TotalSize).toNumber() == 0);
+
+        // Step b: Assert: pullIntoDescriptor.[[bytesFilled]] > 0.
+        MOZ_ASSERT(bytesFilled > 0, "should have filled some bytes");
+
+        // Step c: Assert: pullIntoDescriptor.[[bytesFilled]] <
+        //         pullIntoDescriptor.[[elementSize]].
+        MOZ_ASSERT(bytesFilled < elementSize);
+    }
+
+    // Step 12: Return ready.
+    return true;
+}
+
+// Streams spec 3.12.13. ReadableByteStreamControllerGetDesiredSize ( controller )
+// Unified with 3.9.8 above.
+
+// Streams spec, 3.12.14. ReadableByteStreamControllerHandleQueueDrain ( controller )
+static MOZ_MUST_USE bool
+ReadableByteStreamControllerHandleQueueDrain(JSContext* cx, HandleNativeObject controller)
+{
+    MOZ_ASSERT(controller->is<ReadableByteStreamController>());
+
+    // Step 1: Assert: controller.[[controlledReadableStream]].[[state]] is "readable".
+    Rooted<ReadableStream*> stream(cx, StreamFromController(controller));
+    MOZ_ASSERT(stream->readable());
+
+    // Step 2: If controller.[[queueTotalSize]] is 0 and
+    //         controller.[[closeRequested]] is true,
+    double totalSize = controller->getFixedSlot(QueueContainerSlot_TotalSize).toNumber();
+    bool closeRequested = ControllerFlags(controller) & ControllerFlag_CloseRequested;
+    if (totalSize == 0 && closeRequested) {
+      // Step a: Perform ! ReadableStreamClose(controller.[[controlledReadableStream]]).
+      return ReadableStreamCloseInternal(cx, stream);
+    }
+
+    // Step 3: Otherwise,
+    // Step a: Perform ! ReadableByteStreamControllerCallPullIfNeeded(controller).
+    return ReadableStreamControllerCallPullIfNeeded(cx, controller);
+}
+
+// Streams spec 3.12.15. ReadableByteStreamControllerInvalidateBYOBRequest ( controller )
+static void
+ReadableByteStreamControllerInvalidateBYOBRequest(NativeObject* controller)
+{
+    MOZ_ASSERT(controller->is<ReadableByteStreamController>());
+
+    // Step 1: If controller.[[byobRequest]] is undefined, return.
+    Value byobRequestVal = controller->getFixedSlot(ByteControllerSlot_BYOBRequest);
+    if (byobRequestVal.isUndefined())
+        return;
+
+    NativeObject* byobRequest = &byobRequestVal.toObject().as<NativeObject>();
+    // Step 2: Set controller.[[byobRequest]].[[associatedReadableByteStreamController]]
+    //         to undefined.
+    byobRequest->setFixedSlot(BYOBRequestSlot_Controller, UndefinedValue());
+
+    // Step 3: Set controller.[[byobRequest]].[[view]] to undefined.
+    byobRequest->setFixedSlot(BYOBRequestSlot_View, UndefinedValue());
+
+    // Step 4: Set controller.[[byobRequest]] to undefined.
+    controller->setFixedSlot(ByteControllerSlot_BYOBRequest, UndefinedValue());
+}
+
+static MOZ_MUST_USE PullIntoDescriptor*
+ReadableByteStreamControllerShiftPendingPullInto(JSContext* cx, HandleNativeObject controller);
+
+// Streams spec 3.12.16. ReadableByteStreamControllerProcessPullIntoDescriptorsUsingQueue ( controller )
+static MOZ_MUST_USE bool
+ReadableByteStreamControllerProcessPullIntoDescriptorsUsingQueue(JSContext* cx,
+                                                                 Handle<ReadableByteStreamController*> controller)
+{
+    Rooted<ReadableStream*> stream(cx, StreamFromController(controller));
+
+    // Step 1: Assert: controller.[[closeRequested]] is false.
+    MOZ_ASSERT(!(ControllerFlags(controller) & ControllerFlag_CloseRequested));
+
+    // Step 2: Repeat the following steps while controller.[[pendingPullIntos]]
+    //         is not empty,
+    RootedValue val(cx, controller->getFixedSlot(ByteControllerSlot_PendingPullIntos));
+    RootedNativeObject pendingPullIntos(cx, &val.toObject().as<NativeObject>());
+    Rooted<PullIntoDescriptor*> pullIntoDescriptor(cx);
+    while (pendingPullIntos->getDenseInitializedLength() != 0) {
+        // Step a: If controller.[[queueTotalSize]] is 0, return.
+        double queueTotalSize = controller->getFixedSlot(QueueContainerSlot_TotalSize).toNumber();
+        if (queueTotalSize == 0)
+            return true;
+
+        // Step b: Let pullIntoDescriptor be the first element of
+        //         controller.[[pendingPullIntos]].
+        pullIntoDescriptor = PeekList<PullIntoDescriptor>(pendingPullIntos);
+
+        // Step c: If ! ReadableByteStreamControllerFillPullIntoDescriptorFromQueue(controller, pullIntoDescriptor)
+        //         is true,
+        bool ready;
+        if (!ReadableByteStreamControllerFillPullIntoDescriptorFromQueue(cx, controller,
+                                                                         pullIntoDescriptor,
+                                                                         &ready))
+        {
+            return false;
+        }
+        if (ready) {
+            // Step i: Perform ! ReadableByteStreamControllerShiftPendingPullInto(controller).
+            if (!ReadableByteStreamControllerShiftPendingPullInto(cx, controller))
+                return false;
+
+            // Step ii: Perform ! ReadableByteStreamControllerCommitPullIntoDescriptor(controller.[[controlledReadableStream]],
+            //                                                                         pullIntoDescriptor).
+            if (!ReadableByteStreamControllerCommitPullIntoDescriptor(cx, stream,
+                                                                      pullIntoDescriptor))
+            {
+                return false;
+            }
+        }
+    }
+
+    return true;
+}
+
+// Streams spec, 3.12.17. ReadableByteStreamControllerPullInto ( controller, view )
+static MOZ_MUST_USE JSObject*
+ReadableByteStreamControllerPullInto(JSContext* cx,
+                                     Handle<ReadableByteStreamController*> controller,
+                                     Handle<ArrayBufferViewObject*> view)
+{
+    MOZ_ASSERT(controller->is<ReadableByteStreamController>());
+
+    // Step 1: Let stream be controller.[[controlledReadableStream]].
+    Rooted<ReadableStream*> stream(cx, StreamFromController(controller));
+
+    // Step 2: Let elementSize be 1.
+    uint32_t elementSize = 1;
+
+    RootedObject ctor(cx);
+    // Step 4: If view has a [[TypedArrayName]] internal slot (i.e., it is not a
+    //         DataView),
+    if (view->is<TypedArrayObject>()) {
+        JSProtoKey protoKey = StandardProtoKeyOrNull(view);
+        MOZ_ASSERT(protoKey);
+
+        if (!GetBuiltinConstructor(cx, protoKey, &ctor))
+            return nullptr;
+        elementSize = 1 << TypedArrayShift(view->as<TypedArrayObject>().type());
+    } else {
+        // Step 3: Let ctor be %DataView% (reordered).
+        if (!GetBuiltinConstructor(cx, JSProto_DataView, &ctor))
+            return nullptr;
+    }
+
+    // Step 5: Let pullIntoDescriptor be Record {[[buffer]]: view.[[ViewedArrayBuffer]],
+    //                                           [[byteOffset]]: view.[[ByteOffset]],
+    //                                           [[byteLength]]: view.[[ByteLength]],
+    //                                           [[bytesFilled]]: 0,
+    //                                           [[elementSize]]: elementSize,
+    //                                           [[ctor]]: ctor,
+    //                                           [[readerType]]: "byob"}.
+    bool dummy;
+    RootedArrayBufferObject buffer(cx, &JS_GetArrayBufferViewBuffer(cx, view, &dummy)
+                                       ->as<ArrayBufferObject>());
+    if (!buffer)
+        return nullptr;
+
+    uint32_t byteOffset = JS_GetArrayBufferViewByteOffset(view);
+    uint32_t byteLength = JS_GetArrayBufferViewByteLength(view);
+    Rooted<PullIntoDescriptor*> pullIntoDescriptor(cx);
+    pullIntoDescriptor = PullIntoDescriptor::create(cx, buffer, byteOffset, byteLength, 0,
+                                                    elementSize, ctor,
+                                                    ReaderType_BYOB);
+    if (!pullIntoDescriptor)
+        return nullptr;
+
+    // Step 6: If controller.[[pendingPullIntos]] is not empty,
+    RootedValue val(cx, controller->getFixedSlot(ByteControllerSlot_PendingPullIntos));
+    RootedNativeObject pendingPullIntos(cx, &val.toObject().as<NativeObject>());
+    if (pendingPullIntos->getDenseInitializedLength() != 0) {
+        // Step a: Set pullIntoDescriptor.[[buffer]] to
+        //         ! TransferArrayBuffer(pullIntoDescriptor.[[buffer]]).
+        RootedArrayBufferObject transferredBuffer(cx, TransferArrayBuffer(cx, buffer));
+        if (!transferredBuffer)
+            return nullptr;
+        pullIntoDescriptor->setBuffer(transferredBuffer);
+
+        // Step b: Append pullIntoDescriptor as the last element of
+        //         controller.[[pendingPullIntos]].
+        val = ObjectValue(*pullIntoDescriptor);
+        if (!AppendToList(cx, pendingPullIntos, val))
+            return nullptr;
+
+        // Step c: Return ! ReadableStreamAddReadIntoRequest(stream).
+        return ReadableStreamAddReadIntoRequest(cx, stream);
+    }
+
+    // Step 7: If stream.[[state]] is "closed",
+    if (stream->closed()) {
+        // Step a: Let emptyView be ! Construct(ctor, pullIntoDescriptor.[[buffer]],
+        //                                            pullIntoDescriptor.[[byteOffset]], 0).
+        FixedConstructArgs<3> args(cx);
+        args[0].setObject(*buffer);
+        args[1].setInt32(byteOffset);
+        args[2].setInt32(0);
+        RootedObject emptyView(cx, JS_New(cx, ctor, args));
+        if (!emptyView)
+            return nullptr;
+
+        // Step b: Return a promise resolved with
+        //         ! CreateIterResultObject(emptyView, true).
+        RootedValue val(cx, ObjectValue(*emptyView));
+        RootedObject iterResult(cx, CreateIterResultObject(cx, val, true));
+        if (!iterResult)
+            return nullptr;
+        val = ObjectValue(*iterResult);
+        return PromiseObject::unforgeableResolve(cx, val);
+    }
+
+    // Step 8: If controller.[[queueTotalSize]] > 0,
+    double queueTotalSize = controller->getFixedSlot(QueueContainerSlot_TotalSize).toNumber();
+    if (queueTotalSize > 0) {
+        // Step a: If ! ReadableByteStreamControllerFillPullIntoDescriptorFromQueue(controller,
+        //                                                                          pullIntoDescriptor)
+        //         is true,
+        bool ready;
+        if (!ReadableByteStreamControllerFillPullIntoDescriptorFromQueue(cx, controller,
+                                                                         pullIntoDescriptor, &ready))
+        {
+            return nullptr;
+        }
+
+        if (ready) {
+            // Step i: Let filledView be
+            //         ! ReadableByteStreamControllerConvertPullIntoDescriptor(pullIntoDescriptor).
+            RootedObject filledView(cx);
+            filledView = ReadableByteStreamControllerConvertPullIntoDescriptor(cx,
+                                                                               pullIntoDescriptor);
+            if (!filledView)
+                return nullptr;
+
+            // Step ii: Perform ! ReadableByteStreamControllerHandleQueueDrain(controller).
+            if (!ReadableByteStreamControllerHandleQueueDrain(cx, controller))
+                return nullptr;
+
+            // Step iii: Return a promise resolved with
+            //           ! CreateIterResultObject(filledView, false).
+            val = ObjectValue(*filledView);
+            RootedObject iterResult(cx, CreateIterResultObject(cx, val, false));
+            if (!iterResult)
+                return nullptr;
+            val = ObjectValue(*iterResult);
+            return PromiseObject::unforgeableResolve(cx, val);
+        }
+
+        // Step b: If controller.[[closeRequested]] is true,
+        if (ControllerFlags(controller) & ControllerFlag_CloseRequested) {
+            // Step i: Let e be a TypeError exception.
+            JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
+                                      JSMSG_READABLESTREAMCONTROLLER_CLOSED, "read");
+
+            // Not much we can do about uncatchable exceptions, just bail.
+            RootedValue e(cx);
+            if (!GetAndClearException(cx, &e))
+                return nullptr;
+
+            // Step ii: Perform ! ReadableByteStreamControllerError(controller, e).
+            if (!ReadableStreamControllerError(cx, controller, e))
+                return nullptr;
+
+            // Step iii: Return a promise rejected with e.
+            return PromiseObject::unforgeableReject(cx, e);
+        }
+    }
+
+    // Step 9: Set pullIntoDescriptor.[[buffer]] to
+    //         ! TransferArrayBuffer(pullIntoDescriptor.[[buffer]]).
+    RootedArrayBufferObject transferredBuffer(cx, TransferArrayBuffer(cx, buffer));
+    if (!transferredBuffer)
+        return nullptr;
+    pullIntoDescriptor->setBuffer(transferredBuffer);
+
+    // Step 10: Append pullIntoDescriptor as the last element of
+    //          controller.[[pendingPullIntos]].
+    val = ObjectValue(*pullIntoDescriptor);
+    if (!AppendToList(cx, pendingPullIntos, val))
+        return nullptr;
+
+    // Step 11: Let promise be ! ReadableStreamAddReadIntoRequest(stream).
+    Rooted<PromiseObject*> promise(cx, ReadableStreamAddReadIntoRequest(cx, stream));
+    if (!promise)
+        return nullptr;
+
+    // Step 12: Perform ! ReadableByteStreamControllerCallPullIfNeeded(controller).
+    if (!ReadableStreamControllerCallPullIfNeeded(cx, controller))
+        return nullptr;
+
+    // Step 13: Return promise.
+    return promise;
+}
+
+static MOZ_MUST_USE bool
+ReadableByteStreamControllerRespondInternal(JSContext* cx,
+                                            Handle<ReadableByteStreamController*> controller,
+                                            double bytesWritten);
+
+// Streams spec 3.12.18. ReadableByteStreamControllerRespond( controller, bytesWritten )
+static MOZ_MUST_USE bool
+ReadableByteStreamControllerRespond(JSContext* cx,
+                                    Handle<ReadableByteStreamController*> controller,
+                                    HandleValue bytesWrittenVal)
+{
+    MOZ_ASSERT(controller->is<ReadableByteStreamController>());
+
+    // Step 1: Let bytesWritten be ? ToNumber(bytesWritten).
+    double bytesWritten;
+    if (!ToNumber(cx, bytesWrittenVal, &bytesWritten))
+        return false;
+
+    // Step 2: If ! IsFiniteNonNegativeNumber(bytesWritten) is false,
+    if (bytesWritten < 0 || mozilla::IsNaN(bytesWritten) || mozilla::IsInfinite(bytesWritten)) {
+        JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
+                                  JSMSG_NUMBER_MUST_BE_FINITE_NON_NEGATIVE, "bytesWritten");
+        return false;
+    }
+
+    // Step 3: Assert: controller.[[pendingPullIntos]] is not empty.
+#if DEBUG
+    Value val = controller->getFixedSlot(ByteControllerSlot_PendingPullIntos);
+    RootedNativeObject pendingPullIntos(cx, &val.toObject().as<NativeObject>());
+    MOZ_ASSERT(pendingPullIntos->getDenseInitializedLength() != 0);
+#endif // DEBUG
+
+    // Step 4: Perform ? ReadableByteStreamControllerRespondInternal(controller, bytesWritten).
+    return ReadableByteStreamControllerRespondInternal(cx, controller, bytesWritten);
+}
+
+// Streams spec 3.12.19. ReadableByteStreamControllerRespondInClosedState( controller, firstDescriptor )
+static MOZ_MUST_USE bool
+ReadableByteStreamControllerRespondInClosedState(JSContext* cx,
+                                                   Handle<ReadableByteStreamController*> controller,
+                                                   Handle<PullIntoDescriptor*> firstDescriptor)
+{
+    // Step 1: Set firstDescriptor.[[buffer]] to
+    //         ! TransferArrayBuffer(firstDescriptor.[[buffer]]).
+    RootedArrayBufferObject buffer(cx, firstDescriptor->buffer());
+    RootedArrayBufferObject transferredBuffer(cx, TransferArrayBuffer(cx, buffer));
+    if (!transferredBuffer)
+        return false;
+    firstDescriptor->setBuffer(transferredBuffer);
+
+    // Step 2: Assert: firstDescriptor.[[bytesFilled]] is 0.
+    MOZ_ASSERT(firstDescriptor->bytesFilled() == 0);
+
+    // Step 3: Let stream be controller.[[controlledReadableStream]].
+    Rooted<ReadableStream*> stream(cx, StreamFromController(controller));
+
+    // Step 4: If ReadableStreamHasBYOBReader(stream) is true,
+    if (ReadableStreamHasBYOBReader(stream)) {
+        // Step a: Repeat the following steps while
+        //         ! ReadableStreamGetNumReadIntoRequests(stream) > 0,
+        Rooted<PullIntoDescriptor*> descriptor(cx);
+        while (ReadableStreamGetNumReadRequests(stream) > 0) {
+            // Step i: Let pullIntoDescriptor be
+            //         ! ReadableByteStreamControllerShiftPendingPullInto(controller).
+            descriptor = ReadableByteStreamControllerShiftPendingPullInto(cx, controller);
+            if (!descriptor)
+                return false;
+
+            // Step ii: Perform !
+            //          ReadableByteStreamControllerCommitPullIntoDescriptor(stream,
+            //                                                               pullIntoDescriptor).
+            if (!ReadableByteStreamControllerCommitPullIntoDescriptor(cx, stream, descriptor))
+                return false;
+        }
+    }
+
+    return true;
+}
+
+// Streams spec 3.12.20.
+// ReadableByteStreamControllerRespondInReadableState( controller, bytesWritten, pullIntoDescriptor )
+static MOZ_MUST_USE bool
+ReadableByteStreamControllerRespondInReadableState(JSContext* cx,
+                                                   Handle<ReadableByteStreamController*> controller,
+                                                   uint32_t bytesWritten,
+                                                   Handle<PullIntoDescriptor*> pullIntoDescriptor)
+{
+    // Step 1: If pullIntoDescriptor.[[bytesFilled]] + bytesWritten > pullIntoDescriptor.[[byteLength]],
+    //         throw a RangeError exception.
+    uint32_t bytesFilled = pullIntoDescriptor->bytesFilled();
+    uint32_t byteLength = pullIntoDescriptor->byteLength();
+    if (bytesFilled + bytesWritten > byteLength) {
+        JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
+                                  JSMSG_READABLEBYTESTREAMCONTROLLER_INVALID_BYTESWRITTEN);
+        return false;
+    }
+
+    // Step 2: Perform ! ReadableByteStreamControllerFillHeadPullIntoDescriptor(controller,
+    //                                                                          bytesWritten,
+    //                                                                          pullIntoDescriptor).
+    ReadableByteStreamControllerFillHeadPullIntoDescriptor(controller, bytesWritten,
+                                                           pullIntoDescriptor);
+    bytesFilled += bytesWritten;
+
+    // Step 3: If pullIntoDescriptor.[[bytesFilled]] <
+    //         pullIntoDescriptor.[[elementSize]], return.
+    uint32_t elementSize = pullIntoDescriptor->elementSize();
+    if (bytesFilled < elementSize)
+        return true;
+
+    // Step 4: Perform ! ReadableByteStreamControllerShiftPendingPullInto(controller).
+    if (!ReadableByteStreamControllerShiftPendingPullInto(cx, controller))
+        return false;
+
+    // Step 5: Let remainderSize be pullIntoDescriptor.[[bytesFilled]] mod
+    //         pullIntoDescriptor.[[elementSize]].
+    uint32_t remainderSize = bytesFilled % elementSize;
+
+    // Step 6: If remainderSize > 0,
+    RootedArrayBufferObject buffer(cx, pullIntoDescriptor->buffer());
+    if (remainderSize > 0) {
+        // Step a: Let end be pullIntoDescriptor.[[byteOffset]] +
+        //         pullIntoDescriptor.[[bytesFilled]].
+        uint32_t end = pullIntoDescriptor->byteOffset() + bytesFilled;
+
+        // Step b: Let remainder be ? CloneArrayBuffer(pullIntoDescriptor.[[buffer]],
+        //                                             end − remainderSize,
+        //                                             remainderSize, %ArrayBuffer%).
+        // TODO: this really, really should just use a slot to store the remainder.
+        RootedObject remainderObj(cx, JS_NewArrayBuffer(cx, remainderSize));
+        if (!remainderObj)
+            return false;
+        RootedArrayBufferObject remainder(cx, &remainderObj->as<ArrayBufferObject>());
+        ArrayBufferObject::copyData(remainder, 0, buffer, end - remainderSize, remainderSize);
+
+        // Step c: Perform ! ReadableByteStreamControllerEnqueueChunkToQueue(controller,
+        //                                                                   remainder, 0,
+        //                                                                   remainder.[[ByteLength]]).
+        // Note: `remainderSize` is equivalent to remainder.[[ByteLength]].
+        if (!ReadableByteStreamControllerEnqueueChunkToQueue(cx, controller, remainder, 0,
+                                                             remainderSize))
+        {
+            return false;
+        }
+    }
+
+    // Step 7: Set pullIntoDescriptor.[[buffer]] to
+    //         ! TransferArrayBuffer(pullIntoDescriptor.[[buffer]]).
+    RootedArrayBufferObject transferredBuffer(cx, TransferArrayBuffer(cx, buffer));
+    if (!transferredBuffer)
+        return false;
+    pullIntoDescriptor->setBuffer(transferredBuffer);
+
+    // Step 8: Set pullIntoDescriptor.[[bytesFilled]] to pullIntoDescriptor.[[bytesFilled]] −
+    //         remainderSize.
+    pullIntoDescriptor->setBytesFilled(bytesFilled - remainderSize);
+
+    // Step 9: Perform ! ReadableByteStreamControllerCommitPullIntoDescriptor(controller.[[controlledReadableStream]],
+    //                                                                        pullIntoDescriptor).
+    Rooted<ReadableStream*> stream(cx, StreamFromController(controller));
+    if (!ReadableByteStreamControllerCommitPullIntoDescriptor(cx, stream, pullIntoDescriptor))
+        return false;
+
+    // Step 10: Perform ! ReadableByteStreamControllerProcessPullIntoDescriptorsUsingQueue(controller).
+    return ReadableByteStreamControllerProcessPullIntoDescriptorsUsingQueue(cx, controller);
+}
+
+// Streams spec, 3.12.21. ReadableByteStreamControllerRespondInternal ( controller, bytesWritten )
+static MOZ_MUST_USE bool
+ReadableByteStreamControllerRespondInternal(JSContext* cx,
+                                            Handle<ReadableByteStreamController*> controller,
+                                            double bytesWritten)
+{
+    // Step 1: Let firstDescriptor be the first element of controller.[[pendingPullIntos]].
+    RootedValue val(cx, controller->getFixedSlot(ByteControllerSlot_PendingPullIntos));
+    RootedNativeObject pendingPullIntos(cx, &val.toObject().as<NativeObject>());
+    Rooted<PullIntoDescriptor*> firstDescriptor(cx);
+    firstDescriptor = PeekList<PullIntoDescriptor>(pendingPullIntos);
+
+    // Step 2: Let stream be controller.[[controlledReadableStream]].
+    Rooted<ReadableStream*> stream(cx, StreamFromController(controller));
+
+    // Step 3: If stream.[[state]] is "closed",
+    if (stream->closed()) {
+        // Step a: If bytesWritten is not 0, throw a TypeError exception.
+        if (bytesWritten != 0) {
+            JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
+                                      JSMSG_READABLESTREAMBYOBREQUEST_RESPOND_CLOSED);
+            return false;
+        }
+
+        // Step b: Perform
+        //         ! ReadableByteStreamControllerRespondInClosedState(controller, firstDescriptor).
+        return ReadableByteStreamControllerRespondInClosedState(cx, controller, firstDescriptor);
+    }
+
+    // Step 4: Otherwise,
+    // Step a: Assert: stream.[[state]] is "readable".
+    MOZ_ASSERT(stream->readable());
+
+    // Step b: Perform ? ReadableByteStreamControllerRespondInReadableState(controller,
+    //                                                                      bytesWritten,
+    //                                                                      firstDescriptor).
+    return ReadableByteStreamControllerRespondInReadableState(cx, controller, bytesWritten,
+                                                              firstDescriptor);
+}
+
+// Streams spec, 3.12.22. ReadableByteStreamControllerRespondWithNewView ( controller, view )
+static MOZ_MUST_USE bool
+ReadableByteStreamControllerRespondWithNewView(JSContext* cx,
+                                               Handle<ReadableByteStreamController*> controller,
+                                               HandleObject view)
+{
+    // Step 1: Assert: controller.[[pendingPullIntos]] is not empty.
+    RootedValue val(cx, controller->getFixedSlot(ByteControllerSlot_PendingPullIntos));
+    RootedNativeObject pendingPullIntos(cx, &val.toObject().as<NativeObject>());
+    MOZ_ASSERT(pendingPullIntos->getDenseInitializedLength() != 0);
+
+    // Step 2: Let firstDescriptor be the first element of controller.[[pendingPullIntos]].
+    Rooted<PullIntoDescriptor*> firstDescriptor(cx);
+    firstDescriptor = PeekList<PullIntoDescriptor>(pendingPullIntos);
+
+    // Step 3: If firstDescriptor.[[byteOffset]] + firstDescriptor.[[bytesFilled]]
+    //         is not view.[[ByteOffset]], throw a RangeError exception.
+    uint32_t byteOffset = uint32_t(JS_GetArrayBufferViewByteOffset(view));
+    if (firstDescriptor->byteOffset() + firstDescriptor->bytesFilled() != byteOffset) {
+        JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
+                                  JSMSG_READABLEBYTESTREAMCONTROLLER_INVALID_VIEW_OFFSET);
+        return false;
+    }
+
+    // Step 4: If firstDescriptor.[[byteLength]] is not view.[[ByteLength]],
+    //         throw a RangeError exception.
+    uint32_t byteLength = JS_GetArrayBufferViewByteLength(view);
+    if (firstDescriptor->byteLength() != byteLength) {
+        JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
+                                  JSMSG_READABLEBYTESTREAMCONTROLLER_INVALID_VIEW_SIZE);
+        return false;
+    }
+
+    // Step 5: Set firstDescriptor.[[buffer]] to view.[[ViewedArrayBuffer]].
+    bool dummy;
+    RootedArrayBufferObject buffer(cx,
+                                   &AsArrayBuffer(JS_GetArrayBufferViewBuffer(cx, view, &dummy)));
+    if (!buffer)
+        return false;
+    firstDescriptor->setBuffer(buffer);
+
+    // Step 6: Perform ? ReadableByteStreamControllerRespondInternal(controller,
+    //                                                               view.[[ByteLength]]).
+    return ReadableByteStreamControllerRespondInternal(cx, controller, byteLength);
+}
+
+// Streams spec, 3.12.23. ReadableByteStreamControllerShiftPendingPullInto ( controller )
+static MOZ_MUST_USE PullIntoDescriptor*
+ReadableByteStreamControllerShiftPendingPullInto(JSContext* cx, HandleNativeObject controller)
+{
+    MOZ_ASSERT(controller->is<ReadableByteStreamController>());
+
+    // Step 1: Let descriptor be the first element of controller.[[pendingPullIntos]].
+    // Step 2: Remove descriptor from controller.[[pendingPullIntos]], shifting
+    //         all other elements downward (so that the second becomes the first,
+    //         and so on).
+    RootedValue val(cx, controller->getFixedSlot(ByteControllerSlot_PendingPullIntos));
+    RootedNativeObject pendingPullIntos(cx, &val.toObject().as<NativeObject>());
+    Rooted<PullIntoDescriptor*> descriptor(cx);
+    descriptor = ShiftFromList<PullIntoDescriptor>(cx, pendingPullIntos);
+    MOZ_ASSERT(descriptor);
+
+    // Step 3: Perform ! ReadableByteStreamControllerInvalidateBYOBRequest(controller).
+    ReadableByteStreamControllerInvalidateBYOBRequest(controller);
+
+    // Step 4: Return descriptor.
+    return descriptor;
+}
+
+// Streams spec, 3.12.24. ReadableByteStreamControllerShouldCallPull ( controller )
+// Unified with 3.9.3 above.
+
+// Streams spec, 6.1.2. new ByteLengthQueuingStrategy({ highWaterMark })
+bool
+js::ByteLengthQueuingStrategy::constructor(JSContext* cx, unsigned argc, Value* vp)
+{
+    CallArgs args = CallArgsFromVp(argc, vp);
+
+    RootedObject strategy(cx, NewBuiltinClassInstance<ByteLengthQueuingStrategy>(cx));
+    if (!strategy)
+        return false;
+
+    RootedObject argObj(cx, ToObject(cx, args.get(0)));
+    if (!argObj)
+      return false;
+
+    RootedValue highWaterMark(cx);
+    if (!GetProperty(cx, argObj, argObj, cx->names().highWaterMark, &highWaterMark))
+      return false;
+
+    if (!SetProperty(cx, strategy, cx->names().highWaterMark, highWaterMark))
+      return false;
+
+    args.rval().setObject(*strategy);
+    return true;
+}
+
+// Streams spec 6.1.3.1. size ( chunk )
+bool
+ByteLengthQueuingStrategy_size(JSContext* cx, unsigned argc, Value* vp)
+{
+    CallArgs args = CallArgsFromVp(argc, vp);
+
+    // Step 1: Return ? GetV(chunk, "byteLength").
+    return GetProperty(cx, args.get(0), cx->names().byteLength, args.rval());
+}
+
+static const JSPropertySpec ByteLengthQueuingStrategy_properties[] = {
+    JS_PS_END
+};
+
+static const JSFunctionSpec ByteLengthQueuingStrategy_methods[] = {
+    JS_FN("size", ByteLengthQueuingStrategy_size, 1, 0),
+    JS_FS_END
+};
+
+CLASS_SPEC(ByteLengthQueuingStrategy, 1, 0, 0, 0, JS_NULL_CLASS_OPS);
+
+// Streams spec, 6.2.2. new CountQueuingStrategy({ highWaterMark })
+bool
+js::CountQueuingStrategy::constructor(JSContext* cx, unsigned argc, Value* vp)
+{
+    CallArgs args = CallArgsFromVp(argc, vp);
+
+    Rooted<CountQueuingStrategy*> strategy(cx, NewBuiltinClassInstance<CountQueuingStrategy>(cx));
+    if (!strategy)
+        return false;
+
+    RootedObject argObj(cx, ToObject(cx, args.get(0)));
+    if (!argObj)
+      return false;
+
+    RootedValue highWaterMark(cx);
+    if (!GetProperty(cx, argObj, argObj, cx->names().highWaterMark, &highWaterMark))
+      return false;
+
+    if (!SetProperty(cx, strategy, cx->names().highWaterMark, highWaterMark))
+      return false;
+
+    args.rval().setObject(*strategy);
+    return true;
+}
+
+// Streams spec 6.2.3.1. size ( chunk )
+bool
+CountQueuingStrategy_size(JSContext* cx, unsigned argc, Value* vp)
+{
+    CallArgs args = CallArgsFromVp(argc, vp);
+
+    // Step 1: Return 1.
+    args.rval().setInt32(1);
+    return true;
+}
+
+static const JSPropertySpec CountQueuingStrategy_properties[] = {
+    JS_PS_END
+};
+
+static const JSFunctionSpec CountQueuingStrategy_methods[] = {
+    JS_FN("size", CountQueuingStrategy_size, 0, 0),
+    JS_FS_END
+};
+
+CLASS_SPEC(CountQueuingStrategy, 1, 0, 0, 0, JS_NULL_CLASS_OPS);
+
+#undef CLASS_SPEC
+
+// Streams spec, 6.3.1. DequeueValue ( container ) nothrow
+inline static MOZ_MUST_USE bool
+DequeueValue(JSContext* cx, HandleNativeObject container, MutableHandleValue chunk)
+{
+    // Step 1: Assert: container has [[queue]] and [[queueTotalSize]] internal
+    //         slots.
+    MOZ_ASSERT(IsReadableStreamController(container));
+
+    // Step 2: Assert: queue is not empty.
+    RootedValue val(cx, container->getFixedSlot(QueueContainerSlot_Queue));
+    RootedNativeObject queue(cx, &val.toObject().as<NativeObject>());
+    MOZ_ASSERT(queue->getDenseInitializedLength() > 0);
+
+    // Step 3. Let pair be the first element of queue.
+    // Step 4. Remove pair from queue, shifting all other elements downward
+    //         (so that the second becomes the first, and so on).
+    Rooted<QueueEntry*> pair(cx, ShiftFromList<QueueEntry>(cx, queue));
+    MOZ_ASSERT(pair);
+
+    // Step 5: Set container.[[queueTotalSize]] to
+    //         container.[[queueTotalSize]] − pair.[[size]].
+    // Step 6: If container.[[queueTotalSize]] < 0, set
+    //         container.[[queueTotalSize]] to 0.
+    //         (This can occur due to rounding errors.)
+    double totalSize = container->getFixedSlot(QueueContainerSlot_TotalSize).toNumber();
+
+    totalSize -= pair->size();
+    if (totalSize < 0)
+        totalSize = 0;
+    container->setFixedSlot(QueueContainerSlot_TotalSize, NumberValue(totalSize));
+
+    // Step 7: Return pair.[[value]].
+    chunk.set(pair->value());
+    return true;
+}
+
+// Streams spec, 6.3.2. EnqueueValueWithSize ( container, value, size ) throws
+static MOZ_MUST_USE bool
+EnqueueValueWithSize(JSContext* cx, HandleNativeObject container, HandleValue value,
+                     HandleValue sizeVal)
+{
+    // Step 1: Assert: container has [[queue]] and [[queueTotalSize]] internal
+    //         slots.
+    MOZ_ASSERT(IsReadableStreamController(container));
+
+    // Step 2: Let size be ? ToNumber(size).
+    double size;
+    if (!ToNumber(cx, sizeVal, &size))
+        return false;
+
+    // Step 3: If ! IsFiniteNonNegativeNumber(size) is false, throw a RangeError
+    //         exception.
+    if (size < 0 || mozilla::IsNaN(size) || mozilla::IsInfinite(size)) {
+        JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
+                                  JSMSG_NUMBER_MUST_BE_FINITE_NON_NEGATIVE, "size");
+        return false;
+    }
+
+    // Step 4: Append Record {[[value]]: value, [[size]]: size} as the last element
+    //         of container.[[queue]].
+    RootedValue val(cx, container->getFixedSlot(QueueContainerSlot_Queue));
+    RootedNativeObject queue(cx, &val.toObject().as<NativeObject>());
+
+    QueueEntry* entry = QueueEntry::create(cx, value, size);
+    if (!entry)
+        return false;
+    val = ObjectValue(*entry);
+    if (!AppendToList(cx, queue, val))
+        return false;
+
+    // Step 5: Set container.[[queueTotalSize]] to
+    //         container.[[queueTotalSize]] + size.
+    double totalSize = container->getFixedSlot(QueueContainerSlot_TotalSize).toNumber();
+    container->setFixedSlot(QueueContainerSlot_TotalSize, NumberValue(totalSize + size));
+
+    return true;
+}
+
+// Streams spec, 6.3.3. PeekQueueValue ( container ) nothrow
+// Used by WritableStream.
+// static MOZ_MUST_USE Value
+// PeekQueueValue(NativeObject* container)
+// {
+//     // Step 1: Assert: container has [[queue]] and [[queueTotalSize]] internal
+//     //         slots.
+//     MOZ_ASSERT(IsReadableStreamController(container));
+
+//     // Step 2: Assert: queue is not empty.
+//     Value val = container->getFixedSlot(QueueContainerSlot_Queue);
+//     NativeObject* queue = &val.toObject().as<NativeObject>();
+//     MOZ_ASSERT(queue->getDenseInitializedLength() > 0);
+
+//     // Step 3: Let pair be the first element of container.[[queue]].
+//     QueueEntry* pair = PeekList<QueueEntry>(queue);
+
+//     // Step 4: Return pair.[[value]].
+//     return pair->value();
+// }
+
+/**
+ * Streams spec, 6.3.4. ResetQueue ( container ) nothrow
+ */
+inline static MOZ_MUST_USE bool
+ResetQueue(JSContext* cx, HandleNativeObject container)
+{
+    // Step 1: Assert: container has [[queue]] and [[queueTotalSize]] internal
+    //         slots.
+    MOZ_ASSERT(IsReadableStreamController(container));
+
+    // Step 2: Set container.[[queue]] to a new empty List.
+    if (!SetNewList(cx, container, QueueContainerSlot_Queue))
+        return false;
+
+    // Step 3: Set container.[[queueTotalSize]] to 0.
+    container->setFixedSlot(QueueContainerSlot_TotalSize, NumberValue(0));
+
+    return true;
+}
+
+
+/**
+ * Streams spec, 6.4.1. InvokeOrNoop ( O, P, args )
+ */
+inline static MOZ_MUST_USE bool
+InvokeOrNoop(JSContext* cx, HandleValue O, HandlePropertyName P, HandleValue arg,
+             MutableHandleValue rval)
+{
+    // Step 1: Assert: P is a valid property key (omitted).
+    // Step 2: If args was not passed, let args be a new empty List (omitted).
+    // Step 3: Let method be ? GetV(O, P).
+    RootedValue method(cx);
+    if (!GetProperty(cx, O, P, &method))
+        return false;
+
+    // Step 4: If method is undefined, return.
+    if (method.isUndefined())
+        return true;
+
+    // Step 5: Return ? Call(method, O, args).
+    return Call(cx, method, O, arg, rval);
+}
+
+/**
+ * Streams spec, 6.4.3. PromiseInvokeOrNoop ( O, P, args )
+ * Specialized to one arg, because that's what all stream related callers use.
+ */
+static MOZ_MUST_USE JSObject*
+PromiseInvokeOrNoop(JSContext* cx, HandleValue O, HandlePropertyName P, HandleValue arg)
+{
+    // Step 1: Assert: O is not undefined.
+    MOZ_ASSERT(!O.isUndefined());
+
+    // Step 2: Assert: ! IsPropertyKey(P) is true (implicit).
+    // Step 3: Assert: args is a List (omitted).
+
+    // Step 4: Let returnValue be InvokeOrNoop(O, P, args).
+    // Step 5: If returnValue is an abrupt completion, return a promise
+    //         rejected with returnValue.[[Value]].
+    RootedValue returnValue(cx);
+    if (!InvokeOrNoop(cx, O, P, arg, &returnValue))
+        return PromiseRejectedWithPendingError(cx);
+
+    // Step 6: Otherwise, return a promise resolved with returnValue.[[Value]].
+    return PromiseObject::unforgeableResolve(cx, returnValue);
+}
+
+/**
+ * Streams spec, 6.4.4 TransferArrayBuffer ( O )
+ */
+static MOZ_MUST_USE ArrayBufferObject*
+TransferArrayBuffer(JSContext* cx, HandleObject buffer)
+{
+    // Step 1 (implicit).
+
+    // Step 2.
+    MOZ_ASSERT(buffer->is<ArrayBufferObject>());
+
+    // Step 3.
+    MOZ_ASSERT(!JS_IsDetachedArrayBufferObject(buffer));
+
+    // Step 5 (reordered).
+    uint32_t size = buffer->as<ArrayBufferObject>().byteLength();
+
+    // Steps 4, 6.
+    void* contents = JS_StealArrayBufferContents(cx, buffer);
+    if (!contents)
+        return nullptr;
+    MOZ_ASSERT(JS_IsDetachedArrayBufferObject(buffer));
+
+    // Step 7.
+    RootedObject transferredBuffer(cx, JS_NewArrayBufferWithContents(cx, size, contents));
+    if (!transferredBuffer)
+        return nullptr;
+    return &transferredBuffer->as<ArrayBufferObject>();
+}
+
+// Streams spec, 6.4.5. ValidateAndNormalizeHighWaterMark ( highWaterMark )
+static MOZ_MUST_USE bool
+ValidateAndNormalizeHighWaterMark(JSContext* cx, HandleValue highWaterMarkVal, double* highWaterMark)
+{
+    // Step 1: Set highWaterMark to ? ToNumber(highWaterMark).
+    if (!ToNumber(cx, highWaterMarkVal, highWaterMark))
+        return false;
+
+    // Step 2: If highWaterMark is NaN, throw a TypeError exception.
+    // Step 3: If highWaterMark < 0, throw a RangeError exception.
+    if (mozilla::IsNaN(*highWaterMark) || *highWaterMark < 0) {
+        JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_STREAM_INVALID_HIGHWATERMARK);
+        return false;
+    }
+
+    // Step 4: Return highWaterMark.
+    return true;
+}
+
+// Streams spec, 6.4.6. ValidateAndNormalizeQueuingStrategy ( size, highWaterMark )
+static MOZ_MUST_USE bool
+ValidateAndNormalizeQueuingStrategy(JSContext* cx, HandleValue size,
+                                    HandleValue highWaterMarkVal, double* highWaterMark)
+{
+    // Step 1: If size is not undefined and ! IsCallable(size) is false, throw a
+    //         TypeError exception.
+    if (!size.isUndefined() && !IsCallable(size)) {
+        JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_NOT_FUNCTION,
+                                  "ReadableStream argument options.size");
+        return false;
+    }
+
+    // Step 2: Let highWaterMark be ? ValidateAndNormalizeHighWaterMark(highWaterMark).
+    if (!ValidateAndNormalizeHighWaterMark(cx, highWaterMarkVal, highWaterMark))
+        return false;
+
+    // Step 3: Return Record {[[size]]: size, [[highWaterMark]]: highWaterMark}.
+    return true;
+}
+
+MOZ_MUST_USE bool
+js::ReadableStreamReaderCancel(JSContext* cx, HandleObject readerObj, HandleValue reason)
+{
+    MOZ_ASSERT(IsReadableStreamReader(readerObj));
+    RootedNativeObject reader(cx, &readerObj->as<NativeObject>());
+    MOZ_ASSERT(StreamFromReader(reader));
+    return ReadableStreamReaderGenericCancel(cx, reader, reason);
+}
+
+MOZ_MUST_USE bool
+js::ReadableStreamReaderReleaseLock(JSContext* cx, HandleObject readerObj)
+{
+    MOZ_ASSERT(IsReadableStreamReader(readerObj));
+    RootedNativeObject reader(cx, &readerObj->as<NativeObject>());
+    MOZ_ASSERT(ReadableStreamGetNumReadRequests(StreamFromReader(reader)) == 0);
+    return ReadableStreamReaderGenericRelease(cx, reader);
+}
+
+MOZ_MUST_USE bool
+ReadableStream::enqueue(JSContext* cx, Handle<ReadableStream*> stream, HandleValue chunk)
+{
+    Rooted<ReadableStreamDefaultController*> controller(cx);
+    controller = &ControllerFromStream(stream)->as<ReadableStreamDefaultController>();
+
+    MOZ_ASSERT(!(ControllerFlags(controller) & ControllerFlag_CloseRequested));
+    MOZ_ASSERT(stream->readable());
+
+    return ReadableStreamDefaultControllerEnqueue(cx, controller, chunk);
+}
+
+MOZ_MUST_USE bool
+ReadableStream::enqueueBuffer(JSContext* cx, Handle<ReadableStream*> stream,
+                              Handle<ArrayBufferObject*> chunk)
+{
+    Rooted<ReadableByteStreamController*> controller(cx);
+    controller = &ControllerFromStream(stream)->as<ReadableByteStreamController>();
+
+    MOZ_ASSERT(!(ControllerFlags(controller) & ControllerFlag_CloseRequested));
+    MOZ_ASSERT(stream->readable());
+
+    return ReadableByteStreamControllerEnqueue(cx, controller, chunk);
+}
+
+void
+ReadableStream::desiredSize(bool* hasSize, double* size) const
+{
+    if (errored()) {
+        *hasSize = false;
+        return;
+    }
+
+    *hasSize = true;
+
+    if (closed()) {
+        *size = 0;
+        return;
+    }
+
+    NativeObject* controller = ControllerFromStream(this);
+    *size = ReadableStreamControllerGetDesiredSizeUnchecked(controller);
+}
+
+/*static */ bool
+ReadableStream::getExternalSource(JSContext* cx, Handle<ReadableStream*> stream, void** source)
+{
+    MOZ_ASSERT(stream->mode() == JS::ReadableStreamMode::ExternalSource);
+    if (stream->locked()) {
+        JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_READABLESTREAM_LOCKED);
+        return false;
+    }
+    if (!stream->readable()) {
+        JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
+                                  JSMSG_READABLESTREAMCONTROLLER_NOT_READABLE,
+                                  "ReadableStreamGetExternalUnderlyingSource");
+        return false;
+    }
+
+    auto controller = &ControllerFromStream(stream)->as<ReadableByteStreamController>();
+    AddControllerFlags(controller, ControllerFlag_SourceLocked);
+    *source = controller->getFixedSlot(ControllerSlot_UnderlyingSource).toPrivate();
+    return true;
+}
+
+void
+ReadableStream::releaseExternalSource()
+{
+    MOZ_ASSERT(mode() == JS::ReadableStreamMode::ExternalSource);
+    MOZ_ASSERT(locked());
+    auto controller = ControllerFromStream(this);
+    MOZ_ASSERT(ControllerFlags(controller) & ControllerFlag_SourceLocked);
+    RemoveControllerFlags(controller, ControllerFlag_SourceLocked);
+}
+
+uint8_t
+ReadableStream::embeddingFlags() const
+{
+    uint8_t flags = ControllerFlags(ControllerFromStream(this)) >> ControllerEmbeddingFlagsOffset;
+    MOZ_ASSERT_IF(flags, mode() == JS::ReadableStreamMode::ExternalSource);
+    return flags;
+}
+
+// Streams spec, 3.10.4.4. steps 1-3
+// and
+// Streams spec, 3.12.8. steps 8-9
+//
+// Adapted to handling updates signaled by the embedding for streams with
+// external underlying sources.
+//
+// The remaining steps of those two functions perform checks and asserts that
+// don't apply to streams with external underlying sources.
+MOZ_MUST_USE bool
+ReadableStream::updateDataAvailableFromSource(JSContext* cx, Handle<ReadableStream*> stream,
+                                              uint32_t availableData)
+{
+    Rooted<ReadableByteStreamController*> controller(cx);
+    controller = &ControllerFromStream(stream)->as<ReadableByteStreamController>();
+
+    // Step 2: If this.[[closeRequested]] is true, throw a TypeError exception.
+    if (ControllerFlags(controller) & ControllerFlag_CloseRequested) {
+        JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
+                                  JSMSG_READABLESTREAMCONTROLLER_CLOSED, "enqueue");
+        return false;
+    }
+
+    // Step 3: If this.[[controlledReadableStream]].[[state]] is not "readable",
+    //         throw a TypeError exception.
+    if (!StreamFromController(controller)->readable()) {
+        JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
+                                  JSMSG_READABLESTREAMCONTROLLER_NOT_READABLE, "enqueue");
+        return false;
+    }
+
+    RemoveControllerFlags(controller, ControllerFlag_Pulling | ControllerFlag_PullAgain);
+
+#if DEBUG
+    uint32_t oldAvailableData = controller->getFixedSlot(QueueContainerSlot_TotalSize).toInt32();
+#endif // DEBUG
+    controller->setFixedSlot(QueueContainerSlot_TotalSize, Int32Value(availableData));
+
+    // Step 8.a: If ! ReadableStreamGetNumReadRequests(stream) is 0,
+    // Reordered because for externally-sourced streams it applies regardless
+    // of reader type.
+    if (ReadableStreamGetNumReadRequests(stream) == 0)
+        return true;
+
+    // Step 8: If ! ReadableStreamHasDefaultReader(stream) is true
+    if (ReadableStreamHasDefaultReader(stream)) {
+        // Step b: Otherwise,
+        // Step i: Assert: controller.[[queue]] is empty.
+        MOZ_ASSERT(oldAvailableData == 0);
+
+        // Step ii: Let transferredView be
+        //          ! Construct(%Uint8Array%, transferredBuffer, byteOffset, byteLength).
+        JSObject* viewObj = JS_NewUint8Array(cx, availableData);
+        Rooted<ArrayBufferViewObject*> transferredView(cx, &viewObj->as<ArrayBufferViewObject>());
+        if (!transferredView)
+            return false;
+
+        Value val = controller->getFixedSlot(ControllerSlot_UnderlyingSource);
+        void* underlyingSource = val.toPrivate();
+
+        size_t bytesWritten;
+        {
+            JS::AutoSuppressGCAnalysis noGC(cx);
+            bool dummy;
+            void* buffer = JS_GetArrayBufferViewData(transferredView, &dummy, noGC);
+            auto cb = cx->runtime()->readableStreamWriteIntoReadRequestCallback;
+            MOZ_ASSERT(cb);
+            // TODO: use bytesWritten to correctly update the request's state.
+            cb(cx, stream, underlyingSource, stream->embeddingFlags(), buffer,
+               availableData, &bytesWritten);
+        }
+
+        // Step iii: Perform ! ReadableStreamFulfillReadRequest(stream, transferredView, false).
+        RootedValue chunk(cx, ObjectValue(*transferredView));
+        if (!ReadableStreamFulfillReadOrReadIntoRequest(cx, stream, chunk, false))
+            return false;
+
+        controller->setFixedSlot(QueueContainerSlot_TotalSize,
+                                 Int32Value(availableData - bytesWritten));
+    } else if (ReadableStreamHasBYOBReader(stream)) {
+        // Step 9: Otherwise,
+        // Step a: If ! ReadableStreamHasBYOBReader(stream) is true,
+        // Step i: Perform
+        // (Not needed for external underlying sources.)
+
+        // Step ii: Perform ! ReadableByteStreamControllerProcessPullIntoDescriptorsUsingQueue(controller).
+        if (!ReadableByteStreamControllerProcessPullIntoDescriptorsUsingQueue(cx, controller))
+            return false;
+    } else {
+        // Step b: Otherwise,
+        // Step i: Assert: ! IsReadableStreamLocked(stream) is false.
+        MOZ_ASSERT(!stream->locked());
+
+        // Step ii: Perform
+        //          ! ReadableByteStreamControllerEnqueueChunkToQueue(controller,
+        //                                                            transferredBuffer,
+        //                                                            byteOffset,
+        //                                                            byteLength).
+        // (Not needed for external underlying sources.)
+    }
+
+    return true;
+}
+
+MOZ_MUST_USE bool
+ReadableStream::close(JSContext* cx, Handle<ReadableStream*> stream)
+{
+    RootedNativeObject controllerObj(cx, ControllerFromStream(stream));
+    if (!VerifyControllerStateForClosing(cx, controllerObj))
+        return false;
+
+    if (controllerObj->is<ReadableStreamDefaultController>()) {
+        Rooted<ReadableStreamDefaultController*> controller(cx);
+        controller = &controllerObj->as<ReadableStreamDefaultController>();
+        return ReadableStreamDefaultControllerClose(cx, controller);
+    }
+
+    Rooted<ReadableByteStreamController*> controller(cx);
+    controller = &controllerObj->as<ReadableByteStreamController>();
+    return ReadableByteStreamControllerClose(cx, controller);
+}
+
+MOZ_MUST_USE bool
+ReadableStream::error(JSContext* cx, Handle<ReadableStream*> stream, HandleValue reason)
+{
+    // Step 3: If stream.[[state]] is not "readable", throw a TypeError exception.
+    if (!stream->readable()) {
+        JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
+                                  JSMSG_READABLESTREAMCONTROLLER_NOT_READABLE, "error");
+        return false;
+    }
+
+    // Step 4: Perform ! ReadableStreamDefaultControllerError(this, e).
+    RootedNativeObject controller(cx, ControllerFromStream(stream));
+    return ReadableStreamControllerError(cx, controller, reason);
+}
+
+MOZ_MUST_USE bool
+ReadableStream::tee(JSContext* cx, Handle<ReadableStream*> stream, bool cloneForBranch2,
+                    MutableHandle<ReadableStream*> branch1Stream,
+                    MutableHandle<ReadableStream*> branch2Stream)
+{
+    return ReadableStreamTee(cx, stream, false, branch1Stream, branch2Stream);
+}
+
+MOZ_MUST_USE NativeObject*
+ReadableStream::getReader(JSContext* cx, Handle<ReadableStream*> stream,
+                          JS::ReadableStreamReaderMode mode)
+{
+    if (mode == JS::ReadableStreamReaderMode::Default)
+        return CreateReadableStreamDefaultReader(cx, stream);
+    return CreateReadableStreamBYOBReader(cx, stream);
+}
new file mode 100644
--- /dev/null
+++ b/js/src/builtin/Stream.h
@@ -0,0 +1,171 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*-
+ * vim: set ts=8 sts=4 et sw=4 tw=99:
+ * 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/. */
+
+#ifndef builtin_Stream_h
+#define builtin_Stream_h
+
+#include "builtin/Promise.h"
+#include "vm/NativeObject.h"
+
+
+namespace js {
+
+class AutoSetNewObjectMetadata;
+
+class ReadableStream : public NativeObject
+{
+  public:
+    static ReadableStream* createDefaultStream(JSContext* cx, HandleValue underlyingSource,
+                                               HandleValue size, HandleValue highWaterMark,
+                                               HandleObject proto = nullptr);
+    static ReadableStream* createByteStream(JSContext* cx, HandleValue underlyingSource,
+                                            HandleValue highWaterMark,
+                                            HandleObject proto = nullptr);
+    static ReadableStream* createExternalSourceStream(JSContext* cx, void* underlyingSource,
+                                                      uint8_t flags, HandleObject proto = nullptr);
+
+    bool readable() const;
+    bool closed() const;
+    bool errored() const;
+    bool disturbed() const;
+
+    bool locked() const;
+
+    void desiredSize(bool* hasSize, double* size) const;
+
+    JS::ReadableStreamMode mode() const;
+
+    static MOZ_MUST_USE bool close(JSContext* cx, Handle<ReadableStream*> stream);
+    static MOZ_MUST_USE JSObject* cancel(JSContext* cx, Handle<ReadableStream*> stream,
+                                         HandleValue reason);
+    static MOZ_MUST_USE bool error(JSContext* cx, Handle<ReadableStream*> stream,
+                                   HandleValue error);
+
+    static MOZ_MUST_USE NativeObject* getReader(JSContext* cx, Handle<ReadableStream*> stream,
+                                                JS::ReadableStreamReaderMode mode);
+
+    static MOZ_MUST_USE bool tee(JSContext* cx,
+                                 Handle<ReadableStream*> stream, bool cloneForBranch2,
+                                 MutableHandle<ReadableStream*> branch1Stream,
+                                 MutableHandle<ReadableStream*> branch2Stream);
+
+    static MOZ_MUST_USE bool enqueue(JSContext* cx, Handle<ReadableStream*> stream,
+                                     HandleValue chunk);
+    static MOZ_MUST_USE bool enqueueBuffer(JSContext* cx, Handle<ReadableStream*> stream,
+                                           Handle<ArrayBufferObject*> chunk);
+    static MOZ_MUST_USE bool getExternalSource(JSContext* cx, Handle<ReadableStream*> stream,
+                                               void** source);
+    void releaseExternalSource();
+    uint8_t embeddingFlags() const;
+    static MOZ_MUST_USE bool updateDataAvailableFromSource(JSContext* cx,
+                                                           Handle<ReadableStream*> stream,
+                                                           uint32_t availableData);
+
+    enum State {
+         Readable  = 1 << 0,
+         Closed    = 1 << 1,
+         Errored   = 1 << 2,
+         Disturbed = 1 << 3
+    };
+
+  private:
+    static MOZ_MUST_USE ReadableStream* createStream(JSContext* cx, HandleObject proto = nullptr);
+
+  public:
+    static bool constructor(JSContext* cx, unsigned argc, Value* vp);
+    static const ClassSpec classSpec_;
+    static const Class class_;
+    static const ClassSpec protoClassSpec_;
+    static const Class protoClass_;
+};
+
+class ReadableStreamDefaultReader : public NativeObject
+{
+  public:
+    static MOZ_MUST_USE JSObject* read(JSContext* cx, Handle<ReadableStreamDefaultReader*> reader);
+
+    static bool constructor(JSContext* cx, unsigned argc, Value* vp);
+    static const ClassSpec classSpec_;
+    static const Class class_;
+    static const ClassSpec protoClassSpec_;
+    static const Class protoClass_;
+};
+
+class ReadableStreamBYOBReader : public NativeObject
+{
+  public:
+    static MOZ_MUST_USE JSObject* read(JSContext* cx, Handle<ReadableStreamBYOBReader*> reader,
+                                       Handle<ArrayBufferViewObject*> view);
+
+    static bool constructor(JSContext* cx, unsigned argc, Value* vp);
+    static const ClassSpec classSpec_;
+    static const Class class_;
+    static const ClassSpec protoClassSpec_;
+    static const Class protoClass_;
+};
+
+bool ReadableStreamReaderIsClosed(const JSObject* reader);
+
+MOZ_MUST_USE bool ReadableStreamReaderCancel(JSContext* cx, HandleObject reader,
+                                             HandleValue reason);
+
+MOZ_MUST_USE bool ReadableStreamReaderReleaseLock(JSContext* cx, HandleObject reader);
+
+class ReadableStreamDefaultController : public NativeObject
+{
+  public:
+    static bool constructor(JSContext* cx, unsigned argc, Value* vp);
+    static const ClassSpec classSpec_;
+    static const Class class_;
+    static const ClassSpec protoClassSpec_;
+    static const Class protoClass_;
+};
+
+class ReadableByteStreamController : public NativeObject
+{
+  public:
+    bool hasExternalSource();
+
+    static bool constructor(JSContext* cx, unsigned argc, Value* vp);
+    static const ClassSpec classSpec_;
+    static const Class class_;
+    static const ClassSpec protoClassSpec_;
+    static const Class protoClass_;
+};
+
+class ReadableStreamBYOBRequest : public NativeObject
+{
+  public:
+    static bool constructor(JSContext* cx, unsigned argc, Value* vp);
+    static const ClassSpec classSpec_;
+    static const Class class_;
+    static const ClassSpec protoClassSpec_;
+    static const Class protoClass_;
+};
+
+class ByteLengthQueuingStrategy : public NativeObject
+{
+  public:
+    static bool constructor(JSContext* cx, unsigned argc, Value* vp);
+    static const ClassSpec classSpec_;
+    static const Class class_;
+    static const ClassSpec protoClassSpec_;
+    static const Class protoClass_;
+};
+
+class CountQueuingStrategy : public NativeObject
+{
+  public:
+    static bool constructor(JSContext* cx, unsigned argc, Value* vp);
+    static const ClassSpec classSpec_;
+    static const Class class_;
+    static const ClassSpec protoClassSpec_;
+    static const Class protoClass_;
+};
+
+} // namespace js
+
+#endif /* builtin_Stream_h */
--- a/js/src/builtin/TestingFunctions.cpp
+++ b/js/src/builtin/TestingFunctions.cpp
@@ -1708,16 +1708,24 @@ RejectPromise(JSContext* cx, unsigned ar
     }
 
     bool result = JS::RejectPromise(cx, promise, reason);
     if (result)
         args.rval().setUndefined();
     return result;
 }
 
+static bool
+StreamsAreEnabled(JSContext* cx, unsigned argc, Value* vp)
+{
+    CallArgs args = CallArgsFromVp(argc, vp);
+    args.rval().setBoolean(cx->options().streams());
+    return true;
+}
+
 static unsigned finalizeCount = 0;
 
 static void
 finalize_counter_finalize(JSFreeOp* fop, JSObject* obj)
 {
     ++finalizeCount;
 }
 
@@ -4518,16 +4526,20 @@ static const JSFunctionSpecWithHelp Test
 "  Promise."),
 JS_FN_HELP("resolvePromise", ResolvePromise, 2, 0,
 "resolvePromise(promise, resolution)",
 "  Resolve a Promise by calling the JSAPI function JS::ResolvePromise."),
 JS_FN_HELP("rejectPromise", RejectPromise, 2, 0,
 "rejectPromise(promise, reason)",
 "  Reject a Promise by calling the JSAPI function JS::RejectPromise."),
 
+JS_FN_HELP("streamsAreEnabled", StreamsAreEnabled, 0, 0,
+"streamsAreEnabled()",
+"  Returns a boolean indicating whether WHATWG Streams are enabled for the current compartment."),
+
     JS_FN_HELP("makeFinalizeObserver", MakeFinalizeObserver, 0, 0,
 "makeFinalizeObserver()",
 "  Get a special object whose finalization increases the counter returned\n"
 "  by the finalizeCount function."),
 
     JS_FN_HELP("finalizeCount", FinalizeCount, 0, 0,
 "finalizeCount()",
 "  Return the current value of the finalization counter that is incremented\n"
--- a/js/src/jit/MCallOptimize.cpp
+++ b/js/src/jit/MCallOptimize.cpp
@@ -2760,61 +2760,67 @@ IonBuilder::InliningResult
 IonBuilder::inlineUnsafeSetReservedSlot(CallInfo& callInfo)
 {
     if (callInfo.argc() != 3 || callInfo.constructing()) {
         trackOptimizationOutcome(TrackedOutcome::CantInlineNativeBadForm);
         return InliningStatus_NotInlined;
     }
     if (getInlineReturnType() != MIRType::Undefined)
         return InliningStatus_NotInlined;
-    if (callInfo.getArg(0)->type() != MIRType::Object)
-        return InliningStatus_NotInlined;
-    if (callInfo.getArg(1)->type() != MIRType::Int32)
+
+    MDefinition* obj = callInfo.getArg(0);
+    if (obj->type() != MIRType::Object && obj->type() != MIRType::Value)
+        return InliningStatus_NotInlined;
+
+    MDefinition* arg = callInfo.getArg(1);
+    if (arg->type() != MIRType::Int32)
         return InliningStatus_NotInlined;
 
     // Don't inline if we don't have a constant slot.
-    MDefinition* arg = callInfo.getArg(1);
     if (!arg->isConstant())
         return InliningStatus_NotInlined;
     uint32_t slot = uint32_t(arg->toConstant()->toInt32());
 
     callInfo.setImplicitlyUsedUnchecked();
 
     MStoreFixedSlot* store =
-        MStoreFixedSlot::NewBarriered(alloc(), callInfo.getArg(0), slot, callInfo.getArg(2));
+        MStoreFixedSlot::NewBarriered(alloc(), obj, slot, callInfo.getArg(2));
     current->add(store);
     current->push(store);
 
     if (NeedsPostBarrier(callInfo.getArg(2)))
-        current->add(MPostWriteBarrier::New(alloc(), callInfo.getArg(0), callInfo.getArg(2)));
+        current->add(MPostWriteBarrier::New(alloc(), obj, callInfo.getArg(2)));
 
     return InliningStatus_Inlined;
 }
 
 IonBuilder::InliningResult
 IonBuilder::inlineUnsafeGetReservedSlot(CallInfo& callInfo, MIRType knownValueType)
 {
     if (callInfo.argc() != 2 || callInfo.constructing()) {
         trackOptimizationOutcome(TrackedOutcome::CantInlineNativeBadForm);
         return InliningStatus_NotInlined;
     }
-    if (callInfo.getArg(0)->type() != MIRType::Object)
-        return InliningStatus_NotInlined;
-    if (callInfo.getArg(1)->type() != MIRType::Int32)
+
+    MDefinition* obj = callInfo.getArg(0);
+    if (obj->type() != MIRType::Object && obj->type() != MIRType::Value)
+        return InliningStatus_NotInlined;
+
+    MDefinition* arg = callInfo.getArg(1);
+    if (arg->type() != MIRType::Int32)
         return InliningStatus_NotInlined;
 
     // Don't inline if we don't have a constant slot.
-    MDefinition* arg = callInfo.getArg(1);
     if (!arg->isConstant())
         return InliningStatus_NotInlined;
     uint32_t slot = uint32_t(arg->toConstant()->toInt32());
 
     callInfo.setImplicitlyUsedUnchecked();
 
-    MLoadFixedSlot* load = MLoadFixedSlot::New(alloc(), callInfo.getArg(0), slot);
+    MLoadFixedSlot* load = MLoadFixedSlot::New(alloc(), obj, slot);
     current->add(load);
     current->push(load);
     if (knownValueType != MIRType::Value) {
         // We know what type we have in this slot.  Assert that this is in fact
         // what we've seen coming from this slot in the past, then tell the
         // MLoadFixedSlot about its result type.  That will make us do an
         // infallible unbox as part of the slot load and then we'll barrier on
         // the unbox result.  That way the type barrier code won't end up doing
--- a/js/src/js.msg
+++ b/js/src/js.msg
@@ -598,8 +598,36 @@ MSG_DEF(JSMSG_PROMISE_ERROR_IN_WRAPPED_R
 MSG_DEF(JSMSG_RETURN_NOT_CALLABLE,     0, JSEXN_TYPEERR, "property 'return' of iterator is not callable")
 MSG_DEF(JSMSG_ITERATOR_NO_THROW,       0, JSEXN_TYPEERR, "iterator does not have a 'throw' method")
 
 // Async Iteration
 MSG_DEF(JSMSG_FOR_AWAIT_NOT_OF,        0, JSEXN_SYNTAXERR, "'for await' loop should be used with 'of'")
 MSG_DEF(JSMSG_NOT_AN_ASYNC_GENERATOR,  0, JSEXN_TYPEERR, "Not an async generator")
 MSG_DEF(JSMSG_NOT_AN_ASYNC_ITERATOR,   0, JSEXN_TYPEERR, "Not an async from sync iterator")
 MSG_DEF(JSMSG_GET_ASYNC_ITER_RETURNED_PRIMITIVE, 0, JSEXN_TYPEERR, "[Symbol.asyncIterator]() returned a non-object value")
+
+// ReadableStream
+MSG_DEF(JSMSG_READABLESTREAM_UNDERLYINGSOURCE_TYPE_WRONG,0, JSEXN_RANGEERR,"'underlyingSource.type' must be \"bytes\" or undefined.")
+MSG_DEF(JSMSG_READABLESTREAM_INVALID_READER_MODE,        0, JSEXN_RANGEERR,"'mode' must be \"byob\" or undefined.")
+MSG_DEF(JSMSG_NUMBER_MUST_BE_FINITE_NON_NEGATIVE, 1, JSEXN_RANGEERR, "'{0}' must be a finite, non-negative number.")
+MSG_DEF(JSMSG_READABLEBYTESTREAMCONTROLLER_INVALID_BYTESWRITTEN, 0, JSEXN_RANGEERR, "'bytesWritten' exceeds remaining length.")
+MSG_DEF(JSMSG_READABLEBYTESTREAMCONTROLLER_INVALID_VIEW_SIZE, 0, JSEXN_RANGEERR, "view size does not match requested data.")
+MSG_DEF(JSMSG_READABLEBYTESTREAMCONTROLLER_INVALID_VIEW_OFFSET, 0, JSEXN_RANGEERR, "view offset does not match requested position.")
+MSG_DEF(JSMSG_READABLESTREAM_NOT_LOCKED,                 1, JSEXN_TYPEERR, "'{0}' may only be called on a locked stream.")
+MSG_DEF(JSMSG_READABLESTREAM_LOCKED,                     0, JSEXN_TYPEERR, "A Reader may only be created for an unlocked ReadableStream.")
+MSG_DEF(JSMSG_READABLESTREAM_NOT_BYTE_STREAM_CONTROLLER, 1, JSEXN_TYPEERR, "{0} requires a ReadableByteStreamController.")
+MSG_DEF(JSMSG_READABLESTREAM_NOT_DEFAULT_CONTROLLER,     1, JSEXN_TYPEERR, "{0} requires a ReadableStreamDefaultController.")
+MSG_DEF(JSMSG_READABLESTREAM_CONTROLLER_SET,             0, JSEXN_TYPEERR, "The ReadableStream already has a controller defined.")
+MSG_DEF(JSMSG_READABLESTREAMREADER_NOT_OWNED,            1, JSEXN_TYPEERR, "The ReadableStream reader method '{0}' may only be called on a reader owned by a stream.")
+MSG_DEF(JSMSG_READABLESTREAMREADER_NOT_EMPTY,            1, JSEXN_TYPEERR, "The ReadableStream reader method '{0}' may not be called on a reader with read requests.")
+MSG_DEF(JSMSG_READABLESTREAMBYOBREADER_READ_EMPTY_VIEW,  0, JSEXN_TYPEERR, "ReadableStreamBYOBReader.read() was passed an empty TypedArrayBuffer view.")
+MSG_DEF(JSMSG_READABLESTREAMREADER_RELEASED,             0, JSEXN_TYPEERR, "The ReadableStream reader was released.")
+MSG_DEF(JSMSG_READABLESTREAMCONTROLLER_CLOSED,           1, JSEXN_TYPEERR, "'{0}' called on a stream already closing.")
+MSG_DEF(JSMSG_READABLESTREAMCONTROLLER_NOT_READABLE,     1, JSEXN_TYPEERR, "'{0}' may only be called on a stream in the 'readable' state.")
+MSG_DEF(JSMSG_READABLEBYTESTREAMCONTROLLER_BAD_CHUNKSIZE,0, JSEXN_RANGEERR, "ReadableByteStreamController requires a positive integer or undefined for 'autoAllocateChunkSize'.")
+MSG_DEF(JSMSG_READABLEBYTESTREAMCONTROLLER_BAD_CHUNK,    1, JSEXN_TYPEERR, "{0} passed a bad chunk.")
+MSG_DEF(JSMSG_READABLEBYTESTREAMCONTROLLER_CLOSE_PENDING_PULL, 0, JSEXN_TYPEERR, "The ReadableByteStreamController cannot be closed while the buffer is being filled.")
+MSG_DEF(JSMSG_READABLESTREAMBYOBREQUEST_NO_CONTROLLER,   1, JSEXN_TYPEERR, "ReadableStreamBYOBRequest method '{0}' called on a request with no controller.")
+MSG_DEF(JSMSG_READABLESTREAMBYOBREQUEST_RESPOND_CLOSED,  0, JSEXN_TYPEERR, "ReadableStreamBYOBRequest method 'respond' called with non-zero number of bytes with a closed controller.")
+MSG_DEF(JSMSG_READABLESTREAM_METHOD_NOT_IMPLEMENTED,     1, JSEXN_TYPEERR, "ReadableStream method {0} not yet implemented")
+
+// Other Stream-related
+MSG_DEF(JSMSG_STREAM_INVALID_HIGHWATERMARK,             0, JSEXN_RANGEERR, "'highWaterMark' must be a non-negative, non-NaN number.")
--- a/js/src/jsapi-tests/moz.build
+++ b/js/src/jsapi-tests/moz.build
@@ -120,16 +120,21 @@ if CONFIG['ENABLE_ION']:
         'testJitMacroAssembler.cpp',
         'testJitMoveEmitterCycles-mips32.cpp',
         'testJitMoveEmitterCycles.cpp',
         'testJitRangeAnalysis.cpp',
         'testJitRegisterSet.cpp',
         'testJitRValueAlloc.cpp',
     ]
 
+if CONFIG['ENABLE_STREAMS']:
+    UNIFIED_SOURCES += [
+        'testReadableStream.cpp',
+    ]
+
 DEFINES['EXPORT_JS_API'] = True
 
 LOCAL_INCLUDES += [
     '!..',
     '..',
 ]
 
 if CONFIG['ENABLE_INTL_API'] and CONFIG['MOZ_ICU_DATA_ARCHIVE']:
--- a/js/src/jsapi-tests/testIntTypesABI.cpp
+++ b/js/src/jsapi-tests/testIntTypesABI.cpp
@@ -25,16 +25,17 @@
 #include "js/Id.h"
 /* LegacyIntTypes.h is deliberately exempted from this requirement */
 #include "js/MemoryMetrics.h"
 #include "js/ProfilingStack.h"
 #include "js/RefCounted.h"
 #include "js/RequiredDefines.h"
 #include "js/RootingAPI.h"
 #include "js/SliceBudget.h"
+#include "js/Stream.h"
 #include "js/StructuredClone.h"
 #include "js/TracingAPI.h"
 #include "js/TrackedOptimizationInfo.h"
 #include "js/TypeDecls.h"
 #include "js/UbiNode.h"
 #include "js/Utility.h"
 #include "js/Value.h"
 #include "js/Vector.h"
new file mode 100644
--- /dev/null
+++ b/js/src/jsapi-tests/testReadableStream.cpp
@@ -0,0 +1,678 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*-
+ * vim: set ts=8 sts=4 et sw=4 tw=99:
+ */
+/* 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/. */
+
+#include "jsapi.h"
+
+#include "jsapi-tests/tests.h"
+
+using namespace JS;
+
+char test_buffer_data[] = "1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
+
+static JSObject*
+NewDefaultStream(JSContext* cx, HandleObject source = nullptr, HandleFunction size = nullptr,
+                 double highWaterMark = 1, HandleObject proto = nullptr)
+{
+    RootedObject stream(cx, NewReadableDefaultStreamObject(cx, source, size, highWaterMark,
+                                                           proto));
+    MOZ_ASSERT_IF(stream, IsReadableStream(stream));
+    return stream;
+}
+
+static JSObject*
+NewByteStream(JSContext* cx, double highWaterMark = 0, HandleObject proto = nullptr)
+{
+    RootedObject source(cx, JS_NewPlainObject(cx));
+    MOZ_ASSERT(source);
+
+    RootedObject stream(cx, NewReadableByteStreamObject(cx, source, highWaterMark, proto));
+    MOZ_ASSERT_IF(stream, IsReadableStream(stream));
+    return stream;
+}
+
+static bool dataRequestCBCalled = false;
+static void
+DataRequestCB(JSContext* cx, HandleObject stream, void* underlyingSource, uint8_t flags,
+              size_t desiredSize)
+{
+    MOZ_ASSERT(!dataRequestCBCalled, "Invalid test setup");
+    dataRequestCBCalled = true;
+}
+
+static bool writeIntoRequestBufferCBCalled = false;
+static void
+WriteIntoRequestBufferCB(JSContext* cx, HandleObject stream, void* underlyingSource, uint8_t flags,
+                         void* buffer, size_t length, size_t* bytesWritten)
+{
+    MOZ_ASSERT(!writeIntoRequestBufferCBCalled, "Invalid test setup");
+    MOZ_ASSERT(length <= sizeof(test_buffer_data));
+    memcpy(buffer, test_buffer_data, length);
+    writeIntoRequestBufferCBCalled = true;
+    *bytesWritten = length;
+}
+
+static bool cancelStreamCBCalled = false;
+static Value cancelStreamReason;
+static Value
+CancelStreamCB(JSContext* cx, HandleObject stream, void* underlyingSource, uint8_t flags,
+               HandleValue reason)
+{
+    MOZ_ASSERT(!cancelStreamCBCalled, "Invalid test setup");
+    cancelStreamCBCalled = true;
+    cancelStreamReason = reason;
+    return reason;
+}
+
+static bool streamClosedCBCalled = false;
+static Value streamClosedReason;
+static void
+StreamClosedCB(JSContext* cx, HandleObject stream, void* underlyingSource, uint8_t flags)
+{
+    MOZ_ASSERT(!streamClosedCBCalled, "Invalid test setup");
+    streamClosedCBCalled = true;
+}
+
+static bool streamErroredCBCalled = false;
+static Value streamErroredReason;
+static void
+StreamErroredCB(JSContext* cx, HandleObject stream, void* underlyingSource, uint8_t flags,
+                HandleValue reason)
+{
+    MOZ_ASSERT(!streamErroredCBCalled, "Invalid test setup");
+    streamErroredCBCalled = true;
+    streamErroredReason = reason;
+}
+
+static bool finalizeStreamCBCalled = false;
+static void* finalizedStreamUnderlyingSource;
+static void
+FinalizeStreamCB(void* underlyingSource, uint8_t flags)
+{
+    MOZ_ASSERT(!finalizeStreamCBCalled, "Invalid test setup");
+    finalizeStreamCBCalled = true;
+    finalizedStreamUnderlyingSource = underlyingSource;
+}
+
+static void
+ResetCallbacks()
+{
+    dataRequestCBCalled = false;
+    writeIntoRequestBufferCBCalled = false;
+    cancelStreamReason = UndefinedValue();
+    cancelStreamCBCalled = false;
+    streamClosedCBCalled = false;
+    streamErroredCBCalled = false;
+    finalizeStreamCBCalled = false;
+}
+
+static bool
+GetIterResult(JSContext* cx, HandleObject promise, MutableHandleValue value, bool* done)
+{
+    RootedObject iterResult(cx, &GetPromiseResult(promise).toObject());
+
+    bool found;
+    if (!JS_HasProperty(cx, iterResult, "value", &found))
+        return false;
+    MOZ_ASSERT(found);
+    if (!JS_HasProperty(cx, iterResult, "done", &found))
+        return false;
+    MOZ_ASSERT(found);
+
+    RootedValue doneVal(cx);
+    if (!JS_GetProperty(cx, iterResult, "value", value))
+        return false;
+    if (!JS_GetProperty(cx, iterResult, "done", &doneVal))
+        return false;
+
+    *done = doneVal.toBoolean();
+    MOZ_ASSERT_IF(*done, value.isUndefined());
+
+    return true;
+}
+
+static JSObject*
+GetReadChunk(JSContext* cx, HandleObject readRequest)
+{
+    MOZ_ASSERT(GetPromiseState(readRequest) == PromiseState::Fulfilled);
+    RootedValue resultVal(cx, GetPromiseResult(readRequest));
+    MOZ_ASSERT(resultVal.isObject());
+    RootedObject result(cx, &resultVal.toObject());
+    RootedValue chunkVal(cx);
+    JS_GetProperty(cx, result, "value", &chunkVal);
+    return &chunkVal.toObject();
+}
+
+BEGIN_TEST(testReadableStream_NewReadableStream)
+{
+    RootedObject stream(cx, NewDefaultStream(cx));
+    CHECK(stream);
+    CHECK(ReadableStreamGetMode(stream) == ReadableStreamMode::Default);
+    return true;
+}
+END_TEST(testReadableStream_NewReadableStream)
+