Bug 1385890 - Fix Streams implementation in multiple-global uses. r=baku,tcampbell,jorendorff
authorTill Schneidereit <till@tillschneidereit.net>, Jason Orendorff <jorendorff@mozilla.com>
Thu, 11 Oct 2018 14:18:43 -0500
changeset 499269 a4f93ead3508287a3fe044d35e4913e2f9436f6d
parent 499268 0eef1b37d8ec0e657816a9b0529e68087a09bb30
child 499270 e642ac929fe4373dc46787f27b0ae28655ccd18b
push id1864
push userffxbld-merge
push dateMon, 03 Dec 2018 15:51:40 +0000
treeherdermozilla-release@f040763d99ad [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbaku, tcampbell, jorendorff
bugs1385890, 1389628
milestone64.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1385890 - Fix Streams implementation in multiple-global uses. r=baku,tcampbell,jorendorff Streams have multiple parts that can be JS objects from different compartments. For example, the [[reader]] internal slot of a stream can point to a reader object in another compartment. This patch makes the ReadableStream implementation robust against mixing and matching stream-related objects and methods from different globals. This also removes ReadableStreamBYOBReader and ReadableStreamBYOBRequest for now, with a view toward enabling basic ReadableStream features by default in bug 1389628. Differential Revision: https://phabricator.services.mozilla.com/D8450
dom/cache/Cache.cpp
dom/cache/TypeUtils.cpp
dom/fetch/Fetch.cpp
dom/fetch/Fetch.h
dom/fetch/FetchConsumer.cpp
dom/fetch/FetchStream.cpp
dom/fetch/FetchUtil.cpp
dom/fetch/Request.cpp
dom/fetch/Response.cpp
dom/serviceworkers/ServiceWorkerEvents.cpp
dom/webidl/Fetch.webidl
js/public/ProtoKey.h
js/public/Stream.h
js/src/builtin/Promise.cpp
js/src/builtin/Promise.h
js/src/builtin/Stream.cpp
js/src/builtin/Stream.h
js/src/jit-test/tests/realms/bug1385890-c50.js
js/src/js.msg
js/src/jsapi-tests/moz.build
js/src/jsapi-tests/testReadableStream.cpp
js/src/jsapi-tests/tests.h
js/src/jsapi.cpp
js/src/tests/whatwg/shell.js
js/src/tests/whatwg/streams/readable-stream-globals.js
js/src/tests/whatwg/streams/shell.js
js/src/vm/CommonPropertyNames.h
js/src/vm/GlobalObject.cpp
js/src/vm/JSObject.h
js/src/vm/SelfHosting.cpp
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/dom/cache/Cache.cpp
+++ b/dom/cache/Cache.cpp
@@ -347,17 +347,17 @@ Cache::Add(JSContext* aContext, const Re
     return nullptr;
   }
 
   GlobalObject global(aContext, mGlobal->GetGlobalJSObject());
   MOZ_DIAGNOSTIC_ASSERT(!global.Failed());
 
   nsTArray<RefPtr<Request>> requestList(1);
   RefPtr<Request> request = Request::Constructor(global, aRequest,
-                                                   RequestInit(), aRv);
+                                                 RequestInit(), aRv);
   if (NS_WARN_IF(aRv.Failed())) {
     return nullptr;
   }
 
   nsAutoString url;
   request->GetUrl(url);
   if (NS_WARN_IF(!IsValidPutRequestURL(url, aRv))) {
     return nullptr;
@@ -395,17 +395,17 @@ Cache::AddAll(JSContext* aContext,
       }
     } else {
       requestOrString.SetAsUSVString().Rebind(
         aRequestList[i].GetAsUSVString().Data(),
         aRequestList[i].GetAsUSVString().Length());
     }
 
     RefPtr<Request> request = Request::Constructor(global, requestOrString,
-                                                     RequestInit(), aRv);
+                                                   RequestInit(), aRv);
     if (NS_WARN_IF(aRv.Failed())) {
       return nullptr;
     }
 
     nsAutoString url;
     request->GetUrl(url);
     if (NS_WARN_IF(!IsValidPutRequestURL(url, aRv))) {
       return nullptr;
--- a/dom/cache/TypeUtils.cpp
+++ b/dom/cache/TypeUtils.cpp
@@ -207,17 +207,21 @@ TypeUtils::ToCacheResponseWithoutBody(Ca
   aOut.paddingSize() = aIn.GetPaddingSize();
 }
 
 void
 TypeUtils::ToCacheResponse(JSContext* aCx, CacheResponse& aOut, Response& aIn,
                            nsTArray<UniquePtr<AutoIPCStream>>& aStreamCleanupList,
                            ErrorResult& aRv)
 {
-  if (aIn.BodyUsed()) {
+  bool bodyUsed = aIn.GetBodyUsed(aRv);
+  if (NS_WARN_IF(aRv.Failed())) {
+    return;
+  }
+  if (bodyUsed) {
     aRv.ThrowTypeError<MSG_FETCH_BODY_CONSUMED_ERROR>();
     return;
   }
 
   RefPtr<InternalResponse> ir = aIn.GetInternalResponse();
   ToCacheResponseWithoutBody(aOut, *ir, aRv);
   if (NS_WARN_IF(aRv.Failed())) {
     return;
@@ -441,17 +445,21 @@ TypeUtils::CheckAndSetBodyUsed(JSContext
                                BodyAction aBodyAction, ErrorResult& aRv)
 {
   MOZ_DIAGNOSTIC_ASSERT(aRequest);
 
   if (aBodyAction == IgnoreBody) {
     return;
   }
 
-  if (aRequest->BodyUsed()) {
+  bool bodyUsed = aRequest->GetBodyUsed(aRv);
+  if (NS_WARN_IF(aRv.Failed())) {
+    return;
+  }
+  if (bodyUsed) {
     aRv.ThrowTypeError<MSG_FETCH_BODY_CONSUMED_ERROR>();
     return;
   }
 
   nsCOMPtr<nsIInputStream> stream;
   aRequest->GetBody(getter_AddRefs(stream));
   if (stream) {
     aRequest->SetBodyUsed(aCx, aRv);
--- a/dom/fetch/Fetch.cpp
+++ b/dom/fetch/Fetch.cpp
@@ -51,30 +51,37 @@
 #include "mozilla/dom/WorkerScope.h"
 
 namespace mozilla {
 namespace dom {
 
 namespace {
 
 void
-AbortStream(JSContext* aCx, JS::Handle<JSObject*> aStream)
+AbortStream(JSContext* aCx, JS::Handle<JSObject*> aStream, ErrorResult& aRv)
 {
-  if (!JS::ReadableStreamIsReadable(aStream)) {
+  bool isReadable;
+  if (!JS::ReadableStreamIsReadable(aCx, aStream, &isReadable)) {
+    aRv.StealExceptionFromJSContext(aCx);
+    return;
+  }
+  if (!isReadable) {
     return;
   }
 
   RefPtr<DOMException> e = DOMException::Create(NS_ERROR_DOM_ABORT_ERR);
 
   JS::Rooted<JS::Value> value(aCx);
   if (!GetOrCreateDOMReflector(aCx, e, &value)) {
     return;
   }
 
-  JS::ReadableStreamError(aCx, aStream, value);
+  if (!JS::ReadableStreamError(aCx, aStream, value)) {
+    aRv.StealExceptionFromJSContext(aCx);
+  }
 }
 
 } // anonymous
 
 class AbortSignalMainThread final : public AbortSignalImpl
 {
 public:
   NS_DECL_CYCLE_COLLECTING_ISUPPORTS
@@ -1096,49 +1103,68 @@ FetchBody<Derived>::~FetchBody()
 template
 FetchBody<Request>::~FetchBody();
 
 template
 FetchBody<Response>::~FetchBody();
 
 template <class Derived>
 bool
-FetchBody<Derived>::BodyUsed() const
+FetchBody<Derived>::GetBodyUsed(ErrorResult& aRv) const
 {
   if (mBodyUsed) {
     return true;
   }
 
-  // If this object is disturbed or locked, return false.
+  // If this stream is disturbed or locked, return true.
   if (mReadableStreamBody) {
     AutoJSAPI jsapi;
     if (!jsapi.Init(mOwner)) {
+      aRv.Throw(NS_ERROR_FAILURE);
       return true;
     }
 
     JSContext* cx = jsapi.cx();
+    JS::Rooted<JSObject*> body(cx, mReadableStreamBody);
+    bool disturbed;
+    bool locked;
+    bool readable;
+    if (!JS::ReadableStreamIsDisturbed(cx, body, &disturbed) ||
+        !JS::ReadableStreamIsLocked(cx, body, &locked) ||
+        !JS::ReadableStreamIsReadable(cx, body, &readable)) {
+      aRv.StealExceptionFromJSContext(cx);
+      return false;
+    }
 
-    JS::Rooted<JSObject*> body(cx, mReadableStreamBody);
-    if (JS::ReadableStreamIsDisturbed(body) ||
-        JS::ReadableStreamIsLocked(body) ||
-        !JS::ReadableStreamIsReadable(body)) {
-      return true;
-    }
+    return disturbed || locked || !readable;
   }
 
   return false;
 }
 
 template
 bool
-FetchBody<Request>::BodyUsed() const;
+FetchBody<Request>::GetBodyUsed(ErrorResult&) const;
 
 template
 bool
-FetchBody<Response>::BodyUsed() const;
+FetchBody<Response>::GetBodyUsed(ErrorResult&) const;
+
+template <class Derived>
+bool
+FetchBody<Derived>::CheckBodyUsed() const
+{
+  IgnoredErrorResult result;
+  bool bodyUsed = GetBodyUsed(result);
+  if (result.Failed()) {
+    // Ignore the error.
+    return true;
+  }
+  return bodyUsed;
+}
 
 template <class Derived>
 void
 FetchBody<Derived>::SetBodyUsed(JSContext* aCx, ErrorResult& aRv)
 {
   MOZ_ASSERT(aCx);
   MOZ_ASSERT(mOwner->EventTargetFor(TaskCategory::Other)->IsOnCurrentThread());
 
@@ -1147,18 +1173,24 @@ FetchBody<Derived>::SetBodyUsed(JSContex
   }
 
   mBodyUsed = true;
 
   // If we already have a ReadableStreamBody and it has been created by DOM, we
   // have to lock it now because it can have been shared with other objects.
   if (mReadableStreamBody) {
     JS::Rooted<JSObject*> readableStreamObj(aCx, mReadableStreamBody);
-    if (JS::ReadableStreamGetMode(readableStreamObj) ==
-          JS::ReadableStreamMode::ExternalSource) {
+
+    JS::ReadableStreamMode mode;
+    if (!JS::ReadableStreamGetMode(aCx, readableStreamObj, &mode)) {
+      aRv.StealExceptionFromJSContext(aCx);
+      return;
+    }
+
+    if (mode == JS::ReadableStreamMode::ExternalSource) {
       LockStream(aCx, readableStreamObj, aRv);
       if (NS_WARN_IF(aRv.Failed())) {
         return;
       }
     } else {
       // If this is not a native ReadableStream, let's activate the
       // FetchStreamReader.
       MOZ_ASSERT(mFetchStreamReader);
@@ -1187,17 +1219,21 @@ FetchBody<Derived>::ConsumeBody(JSContex
                                 ErrorResult& aRv)
 {
   RefPtr<AbortSignalImpl> signalImpl = DerivedClass()->GetSignalImpl();
   if (signalImpl && signalImpl->Aborted()) {
     aRv.Throw(NS_ERROR_DOM_ABORT_ERR);
     return nullptr;
   }
 
-  if (BodyUsed()) {
+  bool bodyUsed = GetBodyUsed(aRv);
+  if (NS_WARN_IF(aRv.Failed())) {
+    return nullptr;
+  }
+  if (bodyUsed) {
     aRv.ThrowTypeError<MSG_FETCH_BODY_CONSUMED_ERROR>();
     return nullptr;
   }
 
   SetBodyUsed(aCx, aRv);
   if (NS_WARN_IF(aRv.Failed())) {
     return nullptr;
   }
@@ -1293,17 +1329,21 @@ FetchBody<Derived>::SetReadableStreamBod
   RefPtr<AbortSignalImpl> signalImpl = DerivedClass()->GetSignalImpl();
   if (!signalImpl) {
     return;
   }
 
   bool aborted = signalImpl->Aborted();
   if (aborted) {
     JS::Rooted<JSObject*> body(aCx, mReadableStreamBody);
-    AbortStream(aCx, body);
+    IgnoredErrorResult result;
+    AbortStream(aCx, body, result);
+    if (NS_WARN_IF(result.Failed())) {
+      return;
+    }
   } else if (!IsFollowing()) {
     Follow(signalImpl);
   }
 }
 
 template
 void
 FetchBody<Request>::SetReadableStreamBody(JSContext* aCx, JSObject* aBody);
@@ -1336,27 +1376,34 @@ FetchBody<Derived>::GetBody(JSContext* a
                       inputStream, &body, aRv);
   if (NS_WARN_IF(aRv.Failed())) {
     return;
   }
 
   MOZ_ASSERT(body);
 
   // If the body has been already consumed, we lock the stream.
-  if (BodyUsed()) {
+  bool bodyUsed = GetBodyUsed(aRv);
+  if (NS_WARN_IF(aRv.Failed())) {
+    return;
+  }
+  if (bodyUsed) {
     LockStream(aCx, body, aRv);
     if (NS_WARN_IF(aRv.Failed())) {
       return;
     }
   }
 
   RefPtr<AbortSignalImpl> signalImpl = DerivedClass()->GetSignalImpl();
   if (signalImpl) {
     if (signalImpl->Aborted()) {
-      AbortStream(aCx, body);
+      AbortStream(aCx, body, aRv);
+      if (NS_WARN_IF(aRv.Failed())) {
+        return;
+      }
     } else if (!IsFollowing()) {
       Follow(signalImpl);
     }
   }
 
   mReadableStreamBody = body;
   aBodyOut.set(mReadableStreamBody);
 }
@@ -1374,18 +1421,24 @@ FetchBody<Response>::GetBody(JSContext* 
                              ErrorResult& aRv);
 
 template <class Derived>
 void
 FetchBody<Derived>::LockStream(JSContext* aCx,
                                JS::HandleObject aStream,
                                ErrorResult& aRv)
 {
-  MOZ_ASSERT(JS::ReadableStreamGetMode(aStream) ==
-               JS::ReadableStreamMode::ExternalSource);
+#if DEBUG
+  JS::ReadableStreamMode streamMode;
+  if (!JS::ReadableStreamGetMode(aCx, aStream, &streamMode)) {
+    aRv.StealExceptionFromJSContext(aCx);
+    return;
+  }
+  MOZ_ASSERT(streamMode == JS::ReadableStreamMode::ExternalSource);
+#endif // DEBUG
 
   // This is native stream, creating a reader will not execute any JS code.
   JS::Rooted<JSObject*> reader(aCx,
                                JS::ReadableStreamGetReader(aCx, aStream,
                                                            JS::ReadableStreamReaderMode::Default));
   if (!reader) {
     aRv.StealExceptionFromJSContext(aCx);
     return;
@@ -1411,32 +1464,37 @@ void
 FetchBody<Derived>::MaybeTeeReadableStreamBody(JSContext* aCx,
                                                JS::MutableHandle<JSObject*> aBodyOut,
                                                FetchStreamReader** aStreamReader,
                                                nsIInputStream** aInputStream,
                                                ErrorResult& aRv)
 {
   MOZ_DIAGNOSTIC_ASSERT(aStreamReader);
   MOZ_DIAGNOSTIC_ASSERT(aInputStream);
-  MOZ_DIAGNOSTIC_ASSERT(!BodyUsed());
+  MOZ_DIAGNOSTIC_ASSERT(!CheckBodyUsed());
 
   aBodyOut.set(nullptr);
   *aStreamReader = nullptr;
   *aInputStream = nullptr;
 
   if (!mReadableStreamBody) {
     return;
   }
 
   JS::Rooted<JSObject*> stream(aCx, mReadableStreamBody);
 
   // If this is a ReadableStream with an external source, this has been
   // generated by a Fetch. In this case, Fetch will be able to recreate it
   // again when GetBody() is called.
-  if (JS::ReadableStreamGetMode(stream) == JS::ReadableStreamMode::ExternalSource) {
+  JS::ReadableStreamMode streamMode;
+  if (!JS::ReadableStreamGetMode(aCx, stream, &streamMode)) {
+    aRv.StealExceptionFromJSContext(aCx);
+    return;
+  }
+  if (streamMode == JS::ReadableStreamMode::ExternalSource) {
     aBodyOut.set(nullptr);
     return;
   }
 
   JS::Rooted<JSObject*> branch1(aCx);
   JS::Rooted<JSObject*> branch2(aCx);
 
   if (!JS::ReadableStreamTee(aCx, stream, &branch1, &branch2)) {
@@ -1480,17 +1538,18 @@ FetchBody<Derived>::Abort()
   AutoJSAPI jsapi;
   if (!jsapi.Init(mOwner)) {
     return;
   }
 
   JSContext* cx = jsapi.cx();
 
   JS::Rooted<JSObject*> body(cx, mReadableStreamBody);
-  AbortStream(cx, body);
+  IgnoredErrorResult result;
+  AbortStream(cx, body, result);
 }
 
 template
 void
 FetchBody<Request>::Abort();
 
 template
 void
--- a/dom/fetch/Fetch.h
+++ b/dom/fetch/Fetch.h
@@ -145,17 +145,22 @@ public:
 template <class Derived>
 class FetchBody : public FetchStreamHolder
                 , public AbortFollower
 {
 public:
   friend class FetchBodyConsumer<Derived>;
 
   bool
-  BodyUsed() const;
+  GetBodyUsed(ErrorResult& aRv) const;
+
+  // For use in assertions. On success, returns true if the body is used, false
+  // if not. On error, this sweeps the error under the rug and returns true.
+  bool
+  CheckBodyUsed() const;
 
   already_AddRefed<Promise>
   ArrayBuffer(JSContext* aCx, ErrorResult& aRv)
   {
     return ConsumeBody(aCx, CONSUME_ARRAYBUFFER, aRv);
   }
 
   already_AddRefed<Promise>
--- a/dom/fetch/FetchConsumer.cpp
+++ b/dom/fetch/FetchConsumer.cpp
@@ -746,17 +746,17 @@ FetchBodyConsumer<Derived>::ContinueCons
 
   if (mBodyConsumed) {
     return;
   }
   mBodyConsumed = true;
 
   // Just a precaution to ensure ContinueConsumeBody is not called out of
   // sync with a body read.
-  MOZ_ASSERT(mBody->BodyUsed());
+  MOZ_ASSERT(mBody->CheckBodyUsed());
 
   MOZ_ASSERT(mConsumePromise);
   RefPtr<Promise> localPromise = mConsumePromise.forget();
 
   RefPtr<FetchBodyConsumer<Derived>> self = this;
   auto autoReleaseObject = mozilla::MakeScopeExit([self] {
     self->ReleaseObject();
   });
@@ -864,17 +864,17 @@ FetchBodyConsumer<Derived>::ContinueCons
 
   if (mBodyConsumed) {
     return;
   }
   mBodyConsumed = true;
 
   // Just a precaution to ensure ContinueConsumeBody is not called out of
   // sync with a body read.
-  MOZ_ASSERT(mBody->BodyUsed());
+  MOZ_ASSERT(mBody->CheckBodyUsed());
 
   MOZ_ASSERT(mConsumePromise);
   RefPtr<Promise> localPromise = mConsumePromise.forget();
 
   if (!aShuttingDown) {
     RefPtr<dom::Blob> blob = dom::Blob::Create(mGlobal, aBlobImpl);
     MOZ_ASSERT(blob);
 
--- a/dom/fetch/FetchStream.cpp
+++ b/dom/fetch/FetchStream.cpp
@@ -128,17 +128,24 @@ FetchStream::Create(JSContext* aCx, Fetc
 FetchStream::RequestDataCallback(JSContext* aCx,
                                  JS::HandleObject aStream,
                                  void* aUnderlyingSource,
                                  uint8_t aFlags,
                                  size_t aDesiredSize)
 {
   MOZ_DIAGNOSTIC_ASSERT(aUnderlyingSource);
   MOZ_DIAGNOSTIC_ASSERT(aFlags == FETCH_STREAM_FLAG);
-  MOZ_DIAGNOSTIC_ASSERT(JS::ReadableStreamIsDisturbed(aStream));
+#if MOZ_DIAGNOSTIC_ASSERT_ENABLED
+  bool disturbed;
+  if (!JS::ReadableStreamIsDisturbed(aCx, aStream, &disturbed)) {
+    JS_ClearPendingException(aCx);
+  } else {
+    MOZ_DIAGNOSTIC_ASSERT(disturbed);
+  }
+#endif
 
   RefPtr<FetchStream> stream = static_cast<FetchStream*>(aUnderlyingSource);
   stream->AssertIsOnOwningThread();
 
   MutexAutoLock lock(stream->mMutex);
 
   MOZ_DIAGNOSTIC_ASSERT(stream->mState == eInitializing ||
                         stream->mState == eWaiting ||
@@ -486,17 +493,21 @@ FetchStream::CloseAndReleaseObjects(JSCo
                                     JS::HandleObject aStream)
 {
   AssertIsOnOwningThread();
   MOZ_DIAGNOSTIC_ASSERT(mState != eClosed);
 
   ReleaseObjects(aProofOfLock);
 
   MutexAutoUnlock unlock(mMutex);
-  if (JS::ReadableStreamIsReadable(aStream)) {
+  bool readable;
+  if (!JS::ReadableStreamIsReadable(aCx, aStream, &readable)) {
+    return;
+  }
+  if (readable) {
     JS::ReadableStreamClose(aCx, aStream);
   }
 }
 
 void
 FetchStream::ReleaseObjects()
 {
   MutexAutoLock lock(mMutex);
--- a/dom/fetch/FetchUtil.cpp
+++ b/dom/fetch/FetchUtil.cpp
@@ -557,26 +557,30 @@ FetchUtil::StreamResponseToJS(JSContext*
       response->Type() != ResponseType::Default) {
     return ThrowException(aCx, JSMSG_BAD_RESPONSE_CORS_SAME_ORIGIN);
   }
 
   if (!response->Ok()) {
     return ThrowException(aCx, JSMSG_BAD_RESPONSE_STATUS);
   }
 
-  if (response->BodyUsed()) {
+  IgnoredErrorResult result;
+  bool used = response->GetBodyUsed(result);
+  if (NS_WARN_IF(result.Failed())) {
+    return ThrowException(aCx, JSMSG_ERROR_CONSUMING_RESPONSE);
+  }
+  if (used) {
     return ThrowException(aCx, JSMSG_RESPONSE_ALREADY_CONSUMED);
   }
 
   switch (aMimeType) {
     case JS::MimeType::Wasm:
       nsAutoString url;
       response->GetUrl(url);
 
-      IgnoredErrorResult result;
       nsCString sourceMapUrl;
       response->GetInternalHeaders()->Get(NS_LITERAL_CSTRING("SourceMap"), sourceMapUrl, result);
       if (NS_WARN_IF(result.Failed())) {
         return ThrowException(aCx, JSMSG_ERROR_CONSUMING_RESPONSE);
       }
       NS_ConvertUTF16toUTF8 urlUTF8(url);
       aConsumer->noteResponseURLs(urlUTF8.get(),
                                   sourceMapUrl.IsVoid() ? nullptr : sourceMapUrl.get());
--- a/dom/fetch/Request.cpp
+++ b/dom/fetch/Request.cpp
@@ -264,30 +264,35 @@ private:
   nsresult& mResult;
 };
 
 } // namespace
 
 /*static*/ already_AddRefed<Request>
 Request::Constructor(const GlobalObject& aGlobal,
                      const RequestOrUSVString& aInput,
-                     const RequestInit& aInit, ErrorResult& aRv)
+                     const RequestInit& aInit,
+                     ErrorResult& aRv)
 {
   bool hasCopiedBody = false;
   RefPtr<InternalRequest> request;
 
   nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(aGlobal.GetAsSupports());
 
   RefPtr<AbortSignal> signal;
 
   if (aInput.IsRequest()) {
     RefPtr<Request> inputReq = &aInput.GetAsRequest();
     nsCOMPtr<nsIInputStream> body;
     inputReq->GetBody(getter_AddRefs(body));
-    if (inputReq->BodyUsed()) {
+    bool used = inputReq->GetBodyUsed(aRv);
+    if (NS_WARN_IF(aRv.Failed())) {
+      return nullptr;
+    }
+    if (used) {
       aRv.ThrowTypeError<MSG_FETCH_BODY_CONSUMED_ERROR>();
       return nullptr;
     }
 
     // The body will be copied when GetRequestConstructorCopy() is executed.
     if (body) {
       hasCopiedBody = true;
     }
@@ -593,17 +598,21 @@ Request::Constructor(const GlobalObject&
     }
   }
   return domRequest.forget();
 }
 
 already_AddRefed<Request>
 Request::Clone(ErrorResult& aRv)
 {
-  if (BodyUsed()) {
+  bool used = GetBodyUsed(aRv);
+  if (NS_WARN_IF(aRv.Failed())) {
+    return nullptr;
+  }
+  if (used) {
     aRv.ThrowTypeError<MSG_FETCH_BODY_CONSUMED_ERROR>();
     return nullptr;
   }
 
   RefPtr<InternalRequest> ir = mRequest->Clone();
   if (!ir) {
     aRv.Throw(NS_ERROR_FAILURE);
     return nullptr;
--- a/dom/fetch/Response.cpp
+++ b/dom/fetch/Response.cpp
@@ -231,50 +231,64 @@ Response::Constructor(const GlobalObject
     }
 
     nsCString contentTypeWithCharset;
     nsCOMPtr<nsIInputStream> bodyStream;
     int64_t bodySize = InternalResponse::UNKNOWN_BODY_SIZE;
 
     const fetch::ResponseBodyInit& body = aBody.Value().Value();
     if (body.IsReadableStream()) {
+      JSContext* cx = aGlobal.Context();
       const ReadableStream& readableStream = body.GetAsReadableStream();
 
-      JS::Rooted<JSObject*> readableStreamObj(aGlobal.Context(),
-                                              readableStream.Obj());
+      JS::Rooted<JSObject*> readableStreamObj(cx, readableStream.Obj());
 
-      if (JS::ReadableStreamIsDisturbed(readableStreamObj) ||
-          JS::ReadableStreamIsLocked(readableStreamObj) ||
-          !JS::ReadableStreamIsReadable(readableStreamObj)) {
+      bool disturbed;
+      bool locked;
+      bool readable;
+      if (!JS::ReadableStreamIsDisturbed(cx, readableStreamObj, &disturbed) ||
+          !JS::ReadableStreamIsLocked(cx, readableStreamObj, &locked) ||
+          !JS::ReadableStreamIsReadable(cx, readableStreamObj, &readable)) {
+        aRv.StealExceptionFromJSContext(cx);
+        return nullptr;
+      }
+      if (disturbed || locked || !readable) {
         aRv.ThrowTypeError<MSG_FETCH_BODY_CONSUMED_ERROR>();
         return nullptr;
       }
 
-      r->SetReadableStreamBody(aGlobal.Context(), readableStreamObj);
+      r->SetReadableStreamBody(cx, readableStreamObj);
 
-      if (JS::ReadableStreamGetMode(readableStreamObj) ==
-            JS::ReadableStreamMode::ExternalSource) {
+      JS::ReadableStreamMode streamMode;
+      if (!JS::ReadableStreamGetMode(cx, readableStreamObj, &streamMode)) {
+        aRv.StealExceptionFromJSContext(cx);
+        return nullptr;
+      }
+      if (streamMode == JS::ReadableStreamMode::ExternalSource) {
         // If this is a DOM generated ReadableStream, we can extract the
         // inputStream directly.
         void* underlyingSource = nullptr;
-        if (!JS::ReadableStreamGetExternalUnderlyingSource(aGlobal.Context(),
+        if (!JS::ReadableStreamGetExternalUnderlyingSource(cx,
                                                            readableStreamObj,
                                                            &underlyingSource)) {
-          aRv.StealExceptionFromJSContext(aGlobal.Context());
+          aRv.StealExceptionFromJSContext(cx);
           return nullptr;
         }
 
         MOZ_ASSERT(underlyingSource);
 
         aRv = FetchStream::RetrieveInputStream(underlyingSource,
                                                getter_AddRefs(bodyStream));
 
         // The releasing of the external source is needed in order to avoid an
         // extra stream lock.
-        JS::ReadableStreamReleaseExternalUnderlyingSource(readableStreamObj);
+        if (!JS::ReadableStreamReleaseExternalUnderlyingSource(cx, readableStreamObj)) {
+          aRv.StealExceptionFromJSContext(cx);
+          return nullptr;
+        }
         if (NS_WARN_IF(aRv.Failed())) {
           return nullptr;
         }
       } else {
         // If this is a JS-created ReadableStream, let's create a
         // FetchStreamReader.
         aRv = FetchStreamReader::Create(aGlobal.Context(), global,
                                         getter_AddRefs(r->mFetchStreamReader),
@@ -315,17 +329,21 @@ Response::Constructor(const GlobalObject
 
   r->SetMimeType();
   return r.forget();
 }
 
 already_AddRefed<Response>
 Response::Clone(JSContext* aCx, ErrorResult& aRv)
 {
-  if (BodyUsed()) {
+  bool bodyUsed = GetBodyUsed(aRv);
+  if (NS_WARN_IF(aRv.Failed())) {
+    return nullptr;
+  }
+  if (bodyUsed) {
     aRv.ThrowTypeError<MSG_FETCH_BODY_CONSUMED_ERROR>();
     return nullptr;
   }
 
   RefPtr<FetchStreamReader> streamReader;
   nsCOMPtr<nsIInputStream> inputStream;
 
   JS::Rooted<JSObject*> body(aCx);
@@ -357,17 +375,17 @@ Response::Clone(JSContext* aCx, ErrorRes
   }
 
   return response.forget();
 }
 
 already_AddRefed<Response>
 Response::CloneUnfiltered(JSContext* aCx, ErrorResult& aRv)
 {
-  if (BodyUsed()) {
+  if (GetBodyUsed(aRv)) {
     aRv.ThrowTypeError<MSG_FETCH_BODY_CONSUMED_ERROR>();
     return nullptr;
   }
 
   RefPtr<FetchStreamReader> streamReader;
   nsCOMPtr<nsIInputStream> inputStream;
 
   JS::Rooted<JSObject*> body(aCx);
@@ -400,17 +418,17 @@ Response::CloneUnfiltered(JSContext* aCx
   }
 
   return ref.forget();
 }
 
 void
 Response::SetBody(nsIInputStream* aBody, int64_t aBodySize)
 {
-  MOZ_ASSERT(!BodyUsed());
+  MOZ_ASSERT(!CheckBodyUsed());
   mInternalResponse->SetBody(aBody, aBodySize);
 }
 
 already_AddRefed<InternalResponse>
 Response::GetInternalResponse() const
 {
   RefPtr<InternalResponse> ref = mInternalResponse;
   return ref.forget();
--- a/dom/serviceworkers/ServiceWorkerEvents.cpp
+++ b/dom/serviceworkers/ServiceWorkerEvents.cpp
@@ -669,20 +669,28 @@ RespondWithHandler::ResolvedCallback(JSC
   }
 
   if (mRequestRedirectMode != RequestRedirect::Follow && response->Redirected()) {
     autoCancel.SetCancelMessage(
       NS_LITERAL_CSTRING("BadRedirectModeInterceptionWithURL"), mRequestURL);
     return;
   }
 
-  if (NS_WARN_IF(response->BodyUsed())) {
-    autoCancel.SetCancelMessage(
-      NS_LITERAL_CSTRING("InterceptedUsedResponseWithURL"), mRequestURL);
-    return;
+  {
+    ErrorResult error;
+    bool bodyUsed = response->GetBodyUsed(error);
+    if (NS_WARN_IF(error.Failed())) {
+      autoCancel.SetCancelErrorResult(aCx, error);
+      return;
+    }
+    if (NS_WARN_IF(bodyUsed)) {
+      autoCancel.SetCancelMessage(
+        NS_LITERAL_CSTRING("InterceptedUsedResponseWithURL"), mRequestURL);
+      return;
+    }
   }
 
   RefPtr<InternalResponse> ir = response->GetInternalResponse();
   if (NS_WARN_IF(!ir)) {
     return;
   }
 
   // An extra safety check to make sure our invariant that opaque and cors
--- a/dom/webidl/Fetch.webidl
+++ b/dom/webidl/Fetch.webidl
@@ -7,16 +7,17 @@
  * http://fetch.spec.whatwg.org/
  */
 
 typedef object JSON;
 typedef (Blob or BufferSource or FormData or URLSearchParams or USVString) BodyInit;
 
 [NoInterfaceObject, Exposed=(Window,Worker)]
 interface Body {
+  [Throws]
   readonly attribute boolean bodyUsed;
   [Throws]
   Promise<ArrayBuffer> arrayBuffer();
   [Throws]
   Promise<Blob> blob();
   [Throws]
   Promise<FormData> formData();
   [Throws]
--- a/js/public/ProtoKey.h
+++ b/js/public/ProtoKey.h
@@ -101,20 +101,18 @@ IF_BDATA(real,imaginary)(TypedObject,   
     real(Reflect,               InitReflect,            nullptr) \
     real(WeakSet,               InitWeakSetClass,       OCLASP(WeakSet)) \
     real(TypedArray,            InitViaClassSpec,       &js::TypedArrayObject::sharedTypedArrayPrototypeClass) \
 IF_SAB(real,imaginary)(Atomics, InitAtomicsClass, OCLASP(Atomics)) \
     real(SavedFrame,            InitViaClassSpec,       &js::SavedFrame::class_) \
     real(Promise,               InitViaClassSpec,       OCLASP(Promise)) \
     real(ReadableStream,        InitViaClassSpec,       &js::ReadableStream::class_) \
     real(ReadableStreamDefaultReader,           InitViaClassSpec, &js::ReadableStreamDefaultReader::class_) \
-    real(ReadableStreamBYOBReader,              InitViaClassSpec, &js::ReadableStreamBYOBReader::class_) \
     real(ReadableStreamDefaultController,       InitViaClassSpec, &js::ReadableStreamDefaultController::class_) \
     real(ReadableByteStreamController,          InitViaClassSpec, &js::ReadableByteStreamController::class_) \
-    real(ReadableStreamBYOBRequest,             InitViaClassSpec, &js::ReadableStreamBYOBRequest::class_) \
     imaginary(WritableStream,   dummy,                  dummy) \
     imaginary(WritableStreamDefaultWriter,      dummy,  dummy) \
     imaginary(WritableStreamDefaultController,  dummy,  dummy) \
     real(ByteLengthQueuingStrategy,             InitViaClassSpec, &js::ByteLengthQueuingStrategy::class_) \
     real(CountQueuingStrategy,  InitViaClassSpec,       &js::CountQueuingStrategy::class_) \
     real(WebAssembly,           InitWebAssemblyClass,   CLASP(WebAssembly)) \
     imaginary(WasmModule,       dummy,                  dummy) \
     imaginary(WasmInstance,     dummy,                  dummy) \
--- a/js/public/Stream.h
+++ b/js/public/Stream.h
@@ -181,26 +181,16 @@ HasReadableStreamCallbacks(JSContext* cx
  */
 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|.
  *
@@ -208,29 +198,34 @@ NewReadableByteStreamObject(JSContext* c
  * 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.
+ *
+ * Note: |underlyingSource| must have an even address.
  */
 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 |stream| is a ReadableStream object or an unwrappable wrapper
+ * for one.
+ *
  * Asserts that the given stream has an embedding-provided underlying source.
  */
-extern JS_PUBLIC_API(uint8_t)
-ReadableStreamGetEmbeddingFlags(const JSObject* stream);
+extern JS_PUBLIC_API(bool)
+ReadableStreamGetEmbeddingFlags(JSContext* cx, const JSObject* stream, uint8_t* flags);
 
 /**
  * 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.
@@ -241,143 +236,152 @@ ReadableStreamGetEmbeddingFlags(const JS
  * 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 |stream| is a ReadableStream object or an unwrappable wrapper
+ * for one.
+ *
  * 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 |stream| is a ReadableStream object or an unwrappable wrapper
+ * for one.
+ *
  * Asserts that the stream has an embedding-provided underlying source.
  */
-extern JS_PUBLIC_API(void)
-ReadableStreamReleaseExternalUnderlyingSource(JSObject* stream);
+extern JS_PUBLIC_API(bool)
+ReadableStreamReleaseExternalUnderlyingSource(JSContext* cx, 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.
+ *
+ * Asserts that |stream| is a ReadableStream object or an unwrappable wrapper
+ * for one.
  */
 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.
+ * Returns true if the given object is a ReadableStream object or an
+ * unwrappable wrapper for one, 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.
+ * Returns true if the given object is a ReadableStreamDefaultReader or
+ * ReadableStreamBYOBReader object or an unwrappable wrapper for one, false
+ * otherwise.
  */
 extern JS_PUBLIC_API(bool)
 IsReadableStreamReader(const JSObject* obj);
 
 /**
- * Returns true if the given object is an unwrapped
- * ReadableStreamDefaultReader object, false otherwise.
+ * Returns true if the given object is a ReadableStreamDefaultReader object
+ * or an unwrappable wrapper for one, 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.
+ * Asserts that |stream| is a ReadableStream object or an unwrappable wrapper
+ * for one.
  */
-extern JS_PUBLIC_API(ReadableStreamMode)
-ReadableStreamGetMode(const JSObject* stream);
+extern JS_PUBLIC_API(bool)
+ReadableStreamGetMode(JSContext* cx, const JSObject* stream, ReadableStreamMode* mode);
 
 enum class ReadableStreamReaderMode {
-    Default,
-    BYOB
+    Default
 };
 
 /**
  * Returns true if the given ReadableStream is readable, false if not.
  *
- * Asserts that |stream| is an unwrapped ReadableStream instance.
+ * Asserts that |stream| is a ReadableStream object or an unwrappable wrapper
+ * for one.
  */
 extern JS_PUBLIC_API(bool)
-ReadableStreamIsReadable(const JSObject* stream);
+ReadableStreamIsReadable(JSContext* cx, const JSObject* stream, bool* result);
 
 /**
  * Returns true if the given ReadableStream is locked, false if not.
  *
- * Asserts that |stream| is an unwrapped ReadableStream instance.
+ * Asserts that |stream| is a ReadableStream object or an unwrappable wrapper
+ * for one.
  */
 extern JS_PUBLIC_API(bool)
-ReadableStreamIsLocked(const JSObject* stream);
+ReadableStreamIsLocked(JSContext* cx, const JSObject* stream, bool* result);
 
 /**
  * Returns true if the given ReadableStream is disturbed, false if not.
  *
- * Asserts that |stream| is an ReadableStream instance.
+ * Asserts that |stream| is a ReadableStream object or an unwrappable wrapper
+ * for one.
  */
 extern JS_PUBLIC_API(bool)
-ReadableStreamIsDisturbed(const JSObject* stream);
+ReadableStreamIsDisturbed(JSContext* cx, const JSObject* stream, bool* result);
 
 /**
  * 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.
+ * Asserts that |stream| is a ReadableStream object or an unwrappable wrapper
+ * for one.
  */
 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.
+ * Asserts that |stream| is a ReadableStream object or an unwrappable wrapper
+ * for one. The returned object will always be created in the
+ * current cx compartment.
  */
 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.
+ * Asserts that |stream| is a ReadableStream object or an unwrappable wrapper
+ * for one.
  */
 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
@@ -385,138 +389,112 @@ ReadableStreamTee(JSContext* cx, HandleO
  *
  * 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.
+ * Asserts that |stream| is a ReadableStream object or an unwrappable wrapper
+ * for one.
  */
-extern JS_PUBLIC_API(void)
-ReadableStreamGetDesiredSize(JSObject* stream, bool* hasValue, double* value);
+extern JS_PUBLIC_API(bool)
+ReadableStreamGetDesiredSize(JSContext* cx, 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.
+ * Asserts that |stream| is a ReadableStream object or an unwrappable wrapper
+ * for one.
  */
 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.
+ * Asserts that |reader| is a ReadableStreamDefaultReader or
+ * ReadableStreamBYOBReader object or an unwrappable wrapper for one.
  */
 extern JS_PUBLIC_API(bool)
-ReadableStreamReaderIsClosed(const JSObject* reader);
+ReadableStreamReaderIsClosed(JSContext* cx, const JSObject* reader, bool* result);
 
 /**
  * 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.
+ * Asserts that |stream| is a ReadableStream object or an unwrappable wrapper
+ * for one.
  */
 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.
+ * Asserts that |stream| is a ReadableStream object or an unwrappable wrapper
+ * for one.
  */
 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.
+ * Asserts that |reader| is a ReadableStreamDefaultReader or
+ * ReadableStreamBYOBReader object or an unwrappable wrapper for one.
  */
 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.
+ * Asserts that |reader| is a ReadableStreamDefaultReader or
+ * ReadableStreamBYOBReader object or an unwrappable wrapper for one.
  */
 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.
+ * Asserts that |reader| is a ReadableStreamDefaultReader object or an
+ * unwrappable wrapper for one.
  */
 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/builtin/Promise.cpp
+++ b/js/src/builtin/Promise.cpp
@@ -3317,26 +3317,21 @@ PromiseThenNewPromiseCapability(JSContex
         }
     }
 
     return true;
 }
 
 // ES2016, 25.4.5.3., steps 3-5.
 MOZ_MUST_USE bool
-js::OriginalPromiseThen(JSContext* cx, Handle<PromiseObject*> promise,
+js::OriginalPromiseThen(JSContext* cx, HandleObject promiseObj,
                         HandleValue onFulfilled, HandleValue onRejected,
                         MutableHandleObject dependent, CreateDependentPromise createDependent)
 {
-    RootedObject promiseObj(cx, promise);
-    if (promise->compartment() != cx->compartment()) {
-        if (!cx->compartment()->wrap(cx, &promiseObj)) {
-            return false;
-        }
-    }
+    Rooted<PromiseObject*> promise(cx, &CheckedUnwrap(promiseObj)->as<PromiseObject>());
 
     // Steps 3-4.
     Rooted<PromiseCapability> resultCapability(cx);
     if (!PromiseThenNewPromiseCapability(cx, promiseObj, createDependent, &resultCapability)) {
         return false;
     }
 
     // Step 5.
@@ -4057,40 +4052,37 @@ Promise_then_impl(JSContext* cx, HandleV
     }
 
     // Fast path when the default Promise state is intact.
     if (CanCallOriginalPromiseThenBuiltin(cx, promiseVal)) {
         return OriginalPromiseThenBuiltin(cx, promiseVal, onFulfilled, onRejected, rval, rvalUsed);
     }
 
     RootedObject promiseObj(cx, &promiseVal.toObject());
-    Rooted<PromiseObject*> promise(cx);
-
-    if (promiseObj->is<PromiseObject>()) {
-        promise = &promiseObj->as<PromiseObject>();
-    } else {
+
+    if (!promiseObj->is<PromiseObject>()) {
         JSObject* unwrappedPromiseObj = CheckedUnwrap(promiseObj);
         if (!unwrappedPromiseObj) {
             ReportAccessDenied(cx);
             return false;
         }
         if (!unwrappedPromiseObj->is<PromiseObject>()) {
-            JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INCOMPATIBLE_PROTO,
-                                      "Promise", "then", "value");
+            JS_ReportErrorNumberLatin1(cx, GetErrorMessage, nullptr, JSMSG_INCOMPATIBLE_PROTO,
+                                       "Promise", "then",
+                                       InformalValueTypeName(ObjectValue(*promiseObj)));
             return false;
         }
-        promise = &unwrappedPromiseObj->as<PromiseObject>();
     }
 
     // Steps 3-5.
     CreateDependentPromise createDependent = rvalUsed
                                              ? CreateDependentPromise::Always
                                              : CreateDependentPromise::SkipIfCtorUnobservable;
     RootedObject resultPromise(cx);
-    if (!OriginalPromiseThen(cx, promise, onFulfilled, onRejected, &resultPromise,
+    if (!OriginalPromiseThen(cx, promiseObj, onFulfilled, onRejected, &resultPromise,
                              createDependent))
     {
         return false;
     }
 
     if (rvalUsed) {
         rval.setObject(*resultPromise);
     } else {
--- a/js/src/builtin/Promise.h
+++ b/js/src/builtin/Promise.h
@@ -203,19 +203,21 @@ enum class CreateDependentPromise {
  * Enqueues resolve/reject reactions in the given Promise's reactions lists
  * as though calling the original value of Promise.prototype.then.
  *
  * If the `createDependent` flag is not set, no dependent Promise will be
  * created. This is used internally to implement DOM functionality.
  * Note: In this case, the reactions pushed using this function contain a
  * `promise` field that can contain null. That field is only ever used by
  * devtools, which have to treat these reactions specially.
+ *
+ * Asserts that `promiseObj` is a, maybe wrapped, instance of Promise.
  */
 MOZ_MUST_USE bool
-OriginalPromiseThen(JSContext* cx, Handle<PromiseObject*> promise,
+OriginalPromiseThen(JSContext* cx, HandleObject promiseObj,
                     HandleValue onFulfilled, HandleValue onRejected,
                     MutableHandleObject dependent, CreateDependentPromise createDependent);
 
 /**
  * PromiseResolve ( C, x )
  *
  * The abstract operation PromiseResolve, given a constructor and a value,
  * returns a new promise resolved with that value.
--- a/js/src/builtin/Stream.cpp
+++ b/js/src/builtin/Stream.cpp
@@ -7,64 +7,135 @@
 #include "builtin/Stream.h"
 
 #include "js/Stream.h"
 
 #include "gc/Heap.h"
 #include "vm/JSContext.h"
 #include "vm/SelfHosting.h"
 
+#include "vm/Compartment-inl.h"
 #include "vm/List-inl.h"
 #include "vm/NativeObject-inl.h"
 
 using namespace js;
 
+/**
+ * Memory layout of Stream instances.
+ *
+ * See https://streams.spec.whatwg.org/#rs-internal-slots for details on the
+ * stored state. [[state]] and [[disturbed]] are stored in StreamSlot_State as
+ * ReadableStream::State enum values.
+ *
+ * Of the stored values, Reader and StoredError might be cross-compartment
+ * wrappers. This can happen if the Reader was created by applying a different
+ * compartment's ReadableStream.prototype.getReader method.
+ *
+ * A stream's associated controller is always created from under the stream's
+ * constructor and thus cannot be in a different compartment.
+ */
 enum StreamSlots {
     StreamSlot_Controller,
     StreamSlot_Reader,
     StreamSlot_State,
     StreamSlot_StoredError,
     StreamSlotCount
 };
 
+/**
+ * Memory layout of Stream Reader instances.
+ *
+ * See https://streams.spec.whatwg.org/#default-reader-internal-slots and
+ * https://streams.spec.whatwg.org/#byob-reader-internal-slots for details.
+ *
+ * Note that [[readRequests]] and [[readIntoRequests]] are treated the same in
+ * our implementation.
+ *
+ * Of the stored values, Stream and ClosedPromise might be cross-compartment
+ * wrapper wrappers.
+ *
+ * For Stream, this can happen if the Reader was created by applying a
+ * different compartment's ReadableStream.prototype.getReader method.
+ *
+ * For ClosedPromise, it can be caused by applying a different compartment's
+ * ReadableStream*Reader.prototype.releaseLock method.
+ *
+ * Requests is guaranteed to be in the same compartment as the Reader, but can
+ * contain wrapped request objects from other globals.
+ */
 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.
+/**
+ * Memory layout for queue containers.
+ *
+ * Both ReadableStreamDefaultController and ReadableByteStreamController are
+ * queue containers and must have these slots at identical offsets.
+ *
+ * The queue is guaranteed to be in the  same compartment as the container,
+ * but might contain wrappers for objects from other compartments.
+ */
 enum QueueContainerSlots {
     QueueContainerSlot_Queue,
     QueueContainerSlot_TotalSize,
     QueueContainerSlotCount
 };
 
-// These slots are identical between the two types of ReadableStream
-// controllers.
+/**
+ * Memory layout for ReadableStream controllers, starting after the slots
+ * reserved for queue container usage.
+ *
+ * UnderlyingSource is usually treated as an opaque value. It might be a
+ * wrapped object from another compartment, but that case is handled
+ * correctly by all operations the controller might invoke on it.
+ *
+ * The only case where we don't treat underlyingSource as an opaque value is
+ * if it's a TeeState. All functions operating on TeeState properly handle
+ * TeeState instances from other compartments.
+ *
+ * StrategyHWM and Flags are both primitive (numeric) values.
+ */
 enum ControllerSlots {
     ControllerSlot_Stream = QueueContainerSlotCount,
     ControllerSlot_UnderlyingSource,
     ControllerSlot_StrategyHWM,
     ControllerSlot_Flags,
     ControllerSlotCount
 };
 
+/**
+ * Memory layout for ReadableStreamDefaultControllers, starting after the
+ * slots shared among all types of controllers.
+ *
+ * StrategySize is treated as an opaque value when stored. The only use site
+ * ensures that it's wrapped into the current cx compartment.
+ */
 enum DefaultControllerSlots {
     DefaultControllerSlot_StrategySize = ControllerSlotCount,
     DefaultControllerSlotCount
 };
 
+/**
+ * Memory layout for ReadableByteStreamControllers, starting after the
+ * slots shared among all types of controllers.
+ *
+ * PendingPullIntos is guaranteed to be in the  same compartment as the
+ * controller, but might contain wrappers for objects from other compartments.
+ *
+ * AutoAllocateSize is a primitive (numeric) value.
+ */
 enum ByteControllerSlots {
     ByteControllerSlot_BYOBRequest = ControllerSlotCount,
     ByteControllerSlot_PendingPullIntos,
     ByteControllerSlot_AutoAllocateSize,
     ByteControllerSlotCount
 };
 
 enum ControllerFlags {
@@ -90,44 +161,50 @@ enum BYOBRequestSlots {
 
 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)
+template<class T>
+MOZ_ALWAYS_INLINE bool
+IsMaybeWrapped(const HandleValue v)
 {
-    return controller->is<ReadableStreamDefaultController>() ||
-           controller->is<ReadableByteStreamController>();
+    if (!v.isObject()) {
+        return false;
+    }
+    JSObject* obj = &v.toObject();
+    if (obj->is<T>()) {
+        return true;
+    }
+    obj = CheckedUnwrap(obj);
+    if (!obj) {
+        return false;
+    }
+    return obj->is<T>();
 }
-#endif // DEBUG
 
 static inline uint32_t
-ControllerFlags(const NativeObject* controller)
+ControllerFlags(const ReadableStreamController* controller)
 {
-    MOZ_ASSERT(IsReadableStreamController(controller));
     return controller->getFixedSlot(ControllerSlot_Flags).toInt32();
 }
 
 static inline void
-AddControllerFlags(NativeObject* controller, uint32_t flags)
+AddControllerFlags(ReadableStreamController* 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)
+RemoveControllerFlags(ReadableStreamController* 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();
@@ -161,84 +238,245 @@ ReadableStream::errored() const
 
 bool
 ReadableStream::disturbed() const
 {
     return StreamState(this) & Disturbed;
 }
 
 inline static bool
-ReaderHasStream(const NativeObject* reader)
+ReaderHasStream(const ReadableStreamReader* reader)
 {
-    MOZ_ASSERT(JS::IsReadableStreamReader(reader));
     return !reader->getFixedSlot(ReaderSlot_Stream).isUndefined();
 }
 
 bool
 js::ReadableStreamReaderIsClosed(const JSObject* reader)
 {
-    return !ReaderHasStream(&reader->as<NativeObject>());
+    return !ReaderHasStream(&reader->as<ReadableStreamReader>());
 }
 
 inline static MOZ_MUST_USE ReadableStream*
-StreamFromController(const NativeObject* controller)
+StreamFromController(const ReadableStreamController* controller)
 {
-    MOZ_ASSERT(IsReadableStreamController(controller));
     return &controller->getFixedSlot(ControllerSlot_Stream).toObject().as<ReadableStream>();
 }
 
-inline static MOZ_MUST_USE NativeObject*
+inline static MOZ_MUST_USE ReadableStreamController*
 ControllerFromStream(const ReadableStream* stream)
 {
-    Value controllerVal = stream->getFixedSlot(StreamSlot_Controller);
-    MOZ_ASSERT(IsReadableStreamController(&controllerVal.toObject()));
-    return &controllerVal.toObject().as<NativeObject>();
+    return &stream->getFixedSlot(StreamSlot_Controller).toObject().as<ReadableStreamController>();
 }
 
 inline static bool
 HasController(const ReadableStream* stream)
 {
     return !stream->getFixedSlot(StreamSlot_Controller).isUndefined();
 }
 
 JS::ReadableStreamMode
 ReadableStream::mode() const
 {
-    NativeObject* controller = ControllerFromStream(this);
+    ReadableStreamController* 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)
+template <>
+inline bool
+JSObject::is<ReadableStreamController>() const
+{
+    return is<ReadableStreamDefaultController>() || is<ReadableByteStreamController>();
+}
+
+template <>
+inline bool
+JSObject::is<ReadableStreamReader>() const
+{
+    return is<ReadableStreamDefaultReader>();
+}
+
+/**
+ * Checks that |obj| is an unwrapped instance of T or throws an error.
+ *
+ * This overload must only be used if the caller can ensure that failure to
+ * unwrap is the only possible source of exceptions.
+ */
+template<class T>
+static MOZ_ALWAYS_INLINE T*
+ToUnwrapped(JSContext* cx, JSObject* obj)
+{
+    if (IsWrapper(obj)) {
+        obj = CheckedUnwrap(obj);
+        if (!obj) {
+            ReportAccessDenied(cx);
+            return nullptr;
+        }
+    }
+
+    return &obj->as<T>();
+}
+
+/**
+ * Checks that |obj| is an unwrapped instance of T or throws an error.
+ */
+template<class T>
+static MOZ_ALWAYS_INLINE T*
+ToUnwrapped(JSContext* cx, JSObject* obj, const char* description)
+{
+    if (IsWrapper(obj)) {
+        obj = CheckedUnwrap(obj);
+        if (!obj) {
+            ReportAccessDenied(cx);
+            return nullptr;
+        }
+    }
+
+    if (!obj->is<T>()) {
+        JS_ReportErrorNumberLatin1(cx, GetErrorMessage, nullptr, JSMSG_NOT_EXPECTED_TYPE,
+                                   description, T::class_->name,
+                                   InformalValueTypeName(ObjectValue(*obj)));
+        return nullptr;
+    }
+
+    return &obj->as<T>();
+}
+
+/**
+ * Checks that |obj| is an unwrapped instance of T or throws an error.
+ *
+ * If the caller can ensure that failure to unwrap is the only possible
+ * source of exceptions, it may omit the error messages.
+ */
+template<class T>
+static MOZ_ALWAYS_INLINE T*
+ToUnwrapped(JSContext* cx, JSObject* obj, const char* className, const char* methodName)
+{
+    if (IsWrapper(obj)) {
+        obj = CheckedUnwrap(obj);
+        if (!obj) {
+            ReportAccessDenied(cx);
+            return nullptr;
+        }
+    }
+
+    if (!obj->is<T>()) {
+        JS_ReportErrorNumberLatin1(cx, GetErrorMessage, nullptr, JSMSG_INCOMPATIBLE_PROTO,
+                                   className, methodName, InformalValueTypeName(ObjectValue(*obj)));
+        return nullptr;
+    }
+
+    return &obj->as<T>();
+}
+
+/**
+ * Converts the given value to an object and ensures that it is an unwrapped
+ * instance of T.
+ *
+ * Throws exceptions if the value isn't an object, cannot be unwrapped, or
+ * isn't an instance of the expected type.
+ *
+ * If the caller can ensure that failure to unwrap is the only possible
+ * source of exceptions, it may omit the error messages.
+ */
+template<class T>
+static MOZ_ALWAYS_INLINE T*
+ToUnwrapped(JSContext* cx,
+            HandleValue val,
+            const char* className = "",
+            const char* methodName = "")
+{
+    if (!val.isObject()) {
+        JS_ReportErrorNumberLatin1(cx, GetErrorMessage, nullptr, JSMSG_INCOMPATIBLE_PROTO,
+                                   className, methodName, InformalValueTypeName(val));
+        return nullptr;
+    }
+
+    return ToUnwrapped<T>(cx, &val.toObject(), className, methodName);
+}
+
+/**
+ * Returns the stream associated with the given reader.
+ *
+ * The returned object will always be unwrapped, but might be from another
+ * compartment.
+ *
+ * If the stream cannot be unwrapped, which can only happen if it's a dead
+ * wrapper object, a nullptr is returned. If a JSContext is available, this
+ * case will also report an exception. The JSContext isn't mandatory to make
+ * use easier for call sites that don't otherwise need a JSContext and can
+ * provide useful defaults in case the stream is a dead wrapper.
+ */
+MOZ_ALWAYS_INLINE static MOZ_MUST_USE ReadableStream*
+StreamFromReader(JSContext *maybeCx, const ReadableStreamReader* reader)
 {
     MOZ_ASSERT(ReaderHasStream(reader));
-    return &reader->getFixedSlot(ReaderSlot_Stream).toObject().as<ReadableStream>();
+    JSObject* streamObj = &reader->getFixedSlot(ReaderSlot_Stream).toObject();
+    if (IsProxy(streamObj)) {
+        if (JS_IsDeadWrapper(streamObj)) {
+            if (maybeCx) {
+                JS_ReportErrorNumberASCII(maybeCx, GetErrorMessage, nullptr, JSMSG_DEAD_OBJECT);
+            }
+            return nullptr;
+        }
+
+        // It's ok to do an unchecked unwrap here: the stream wouldn't have
+        // been stored on the reader if it couldn't be unwrapped.
+        streamObj = UncheckedUnwrap(streamObj);
+    }
+    return &streamObj->as<ReadableStream>();
 }
 
-inline static MOZ_MUST_USE NativeObject*
-ReaderFromStream(const NativeObject* stream)
+/**
+ * Returns the reader associated with the given stream.
+ *
+ * Must only be called on ReadableStreams that already have a reader
+ * associated with them.
+ *
+ * If the reader is a wrapper, it will be unwrapped, so it might not be an
+ * object from the currently active compartment.
+ *
+ * If the reader cannot be unwrapped, which can only happen if it's a dead
+ * wrapper object, a nullptr is returned. If a JSContext is available, an
+ * exception is reported, too. The JSContext isn't mandatory to make use
+ * easier for call sites that don't otherwise need a JSContext and can provide
+ * useful defaults in case the reader is a dead object wrapper.
+ */
+MOZ_ALWAYS_INLINE static MOZ_MUST_USE ReadableStreamReader*
+ReaderFromStream(JSContext* maybeCx, const ReadableStream* stream)
 {
-    Value readerVal = stream->getFixedSlot(StreamSlot_Reader);
-    MOZ_ASSERT(JS::IsReadableStreamReader(&readerVal.toObject()));
-    return &readerVal.toObject().as<NativeObject>();
+    JSObject* readerObj = &stream->getFixedSlot(StreamSlot_Reader).toObject();
+    if (IsProxy(readerObj)) {
+        if (JS_IsDeadWrapper(readerObj)) {
+            if (maybeCx) {
+                JS_ReportErrorNumberASCII(maybeCx, GetErrorMessage, nullptr, JSMSG_DEAD_OBJECT);
+            }
+            return nullptr;
+        }
+
+        // It's ok to do an unchecked unwrap here: the reader wouldn't have
+        // been stored on the stream if it couldn't be unwrapped.
+        readerObj = UncheckedUnwrap(readerObj);
+    }
+
+    return &readerObj->as<ReadableStreamReader>();
 }
 
 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)
+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;
     }
@@ -249,17 +487,17 @@ NewHandler(JSContext *cx, Native handler
 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);
+ResetQueue(JSContext* cx, Handle<ReadableStreamController*> 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);
 
@@ -302,29 +540,33 @@ ReturnPromiseRejectedWithPendingError(JS
     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)
-{
-    ReportValueError(cx, JSMSG_INCOMPATIBLE_PROTO, JSDVG_SEARCH_STACK, args.thisv(),
-                     nullptr, className, methodName);
-
-    return ReturnPromiseRejectedWithPendingError(cx, args);
-}
-
+/**
+ * Creates a NativeObject to be used as a list and stores it on the given
+ * container at the given fixed slot offset.
+ *
+ * Note: to make handling of lists easier, SetNewList ensures that the list
+ * is created in the container's compartment. If the container isn't from the
+ * currently entered compartment, then it's compartment is entered prior to
+ * creating the list. The list is returned unwrapped in that case, so won't
+ * be in the currently entered compartment, either.
+ */
 inline static MOZ_MUST_USE NativeObject*
 SetNewList(JSContext* cx, HandleNativeObject container, uint32_t slot)
 {
+    mozilla::Maybe<AutoRealm> ar;
+    if (container->compartment() != cx->compartment()) {
+        ar.emplace(cx, container);
+    }
     NativeObject* list = NewList(cx);
     if (!list) {
         return nullptr;
     }
     container->setFixedSlot(slot, ObjectValue(*list));
     return list;
 }
 
@@ -459,29 +701,48 @@ class QueueEntry : public NativeObject
 const Class QueueEntry::class_ = {
     "QueueEntry",
     JSCLASS_HAS_RESERVED_SLOTS(SlotCount)
 };
 
 class TeeState : public NativeObject
 {
   private:
+    /**
+     * Memory layout for TeeState instances.
+     *
+     * The Reason1 and Reason2 slots store opaque values, which might be
+     * wrapped objects from other compartments. Since we don't treat them as
+     * objects in Streams-specific code, we don't have to worry about that
+     * apart from ensuring that the values are properly wrapped before storing
+     * them.
+     *
+     * Promise is always created in TeeState::create below, so is guaranteed
+     * to be in the same compartment as the TeeState instance itself.
+     *
+     * Stream can be from another compartment. It is automatically wrapped
+     * before storing it and unwrapped upon retrieval. That means that
+     * TeeState consumers need to be able to deal with unwrapped
+     * ReadableStream instances from non-current compartments.
+     *
+     * Branch1 and Branch2 are always created in the same compartment as the
+     * TeeState instance, so cannot be from another compartment.
+     */
     enum Slots {
         Slot_Flags = 0,
         Slot_Reason1,
         Slot_Reason2,
         Slot_Promise,
         Slot_Stream,
         Slot_Branch1,
         Slot_Branch2,
         SlotCount
     };
 
-    enum Flags
-    {
+    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)); }
 
@@ -518,21 +779,20 @@ class TeeState : public NativeObject
     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>();
+
+    static MOZ_MUST_USE ReadableStream* stream(JSContext* cx, TeeState* teeState) {
+        RootedValue streamVal(cx, teeState->getFixedSlot(Slot_Stream));
+        return ToUnwrapped<ReadableStream>(cx, streamVal);
     }
 
     ReadableStreamDefaultController* branch1() {
         ReadableStreamDefaultController* controller = &getFixedSlot(Slot_Branch1).toObject()
                                                        .as<ReadableStreamDefaultController>();
         MOZ_ASSERT(ControllerFlags(controller) & ControllerFlag_TeeBranch);
         MOZ_ASSERT(ControllerFlags(controller) & ControllerFlag_TeeBranch1);
         return controller;
@@ -564,17 +824,21 @@ class TeeState : public NativeObject
 
         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));
+        RootedObject wrappedStream(cx, stream);
+        if (!cx->compartment()->wrap(cx, &wrappedStream)) {
+            return nullptr;
+        }
+        state->setFixedSlot(Slot_Stream, ObjectValue(*wrappedStream));
 
         return state;
    }
 };
 
 const Class TeeState::class_ = {
     "TeeState",
     JSCLASS_HAS_RESERVED_SLOTS(SlotCount)
@@ -657,60 +921,29 @@ ReadableStream::createDefaultStream(JSCo
 
     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));
+    Rooted<ReadableStreamController*> controller(cx);
+    controller = CreateReadableByteStreamController(cx, stream, underlyingSource);
     if (!controller) {
         return nullptr;
     }
 
     stream->setFixedSlot(StreamSlot_Controller, ObjectValue(*controller));
     AddControllerFlags(controller, flags << ControllerEmbeddingFlagsOffset);
 
     return stream;
@@ -776,17 +1009,19 @@ ReadableStream::constructor(JSContext* c
 
     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);
+        JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
+                                  JSMSG_READABLESTREAM_BYTES_TYPE_NOT_IMPLEMENTED);
+        return false;
     } 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.
@@ -801,45 +1036,45 @@ ReadableStream::constructor(JSContext* c
     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>());
+    Rooted<ReadableStream*> stream(cx);
+    stream = &UncheckedUnwrap(&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);
+    return CallNonGenericMethod<IsMaybeWrapped<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())) {
-        ReportValueError(cx, JSMSG_INCOMPATIBLE_PROTO, JSDVG_SEARCH_STACK, args.thisv(),
-                         nullptr, "cancel", "");
+    Rooted<ReadableStream*> stream(cx);
+    stream = ToUnwrapped<ReadableStream>(cx, args.thisv(), "ReadableStream", "cancel");
+    if (!stream)
         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_LOCKED_METHOD, "cancel");
         return ReturnPromiseRejectedWithPendingError(cx, args);
     }
@@ -851,24 +1086,28 @@ ReadableStream_cancel(JSContext* cx, uns
     }
     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)
+static bool
+ReadableStream_getReader(JSContext* cx, unsigned argc, Value* vp)
 {
-    Rooted<ReadableStream*> stream(cx, &args.thisv().toObject().as<ReadableStream>());
+    CallArgs args = CallArgsFromVp(argc, vp);
+
+    // Step 1: If ! IsReadableStream(this) is false, throw a TypeError exception.
+    Rooted<ReadableStream*> stream(cx);
+    stream = ToUnwrapped<ReadableStream>(cx, args.thisv(), "ReadableStream", "getReader");
+    if (!stream)
+        return false;
+
     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)) {
@@ -890,48 +1129,40 @@ ReadableStream_getReader_impl(JSContext*
         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);
+
+        JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
+                                  JSMSG_READABLESTREAM_BYTES_TYPE_NOT_IMPLEMENTED);
     }
 
     // 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,
@@ -939,20 +1170,26 @@ ReadableStream_pipeTo(JSContext* cx, uns
     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)
+static bool
+ReadableStream_tee(JSContext* cx, unsigned argc, Value* vp)
 {
-    Rooted<ReadableStream*> stream(cx, &args.thisv().toObject().as<ReadableStream>());
+    CallArgs args = CallArgsFromVp(argc, vp);
+
+    // Step 1: If ! IsReadableStream(this) is false, throw a TypeError exception.
+    Rooted<ReadableStream*> stream(cx);
+    stream = ToUnwrapped<ReadableStream>(cx, args.thisv(), "ReadableStream", "tee");
+    if (!stream)
+        return false;
 
     // 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;
     }
 
@@ -964,24 +1201,16 @@ ReadableStream_tee_impl(JSContext* cx, c
     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
 };
@@ -1128,120 +1357,175 @@ ReadableStreamTee_Pull(JSContext* cx, Ha
     // 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());
+    Rooted<ReadableStream*> stream(cx, TeeState::stream(cx, teeState));
+    if (!stream)
+        return nullptr;
+    RootedObject readerObj(cx, ReaderFromStream(cx, stream));
+    if (!readerObj)
+        return nullptr;
+    Rooted<ReadableStreamDefaultReader*> reader(cx,
+                                                &readerObj->as<ReadableStreamDefaultReader>());
+
     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);
 }
 
+/**
+ * Cancel a tee'd stream's |branch| with the given |reason_|.
+ *
+ * Note: can operate on unwrapped values for |teeState| and |branch|.
+ *
+ * Objects created in the course of this function's operation are always
+ * created in the current cx compartment.
+ */
 static MOZ_MUST_USE JSObject*
 ReadableStreamTee_Cancel(JSContext* cx, Handle<TeeState*> teeState,
-                         Handle<ReadableStreamDefaultController*> branch, HandleValue reason)
+                         Handle<ReadableStreamDefaultController*> branch, HandleValue reason_)
 {
     // Step 1: Let stream be F.[[stream]] and teeState be F.[[teeState]].
-    Rooted<ReadableStream*> stream(cx, teeState->stream());
+    Rooted<ReadableStream*> stream(cx, TeeState::stream(cx, teeState));
+    if (!stream)
+        return nullptr;
 
     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();
+    {
+        RootedValue reason(cx, reason_);
+        if (reason.isGCThing() &&
+            reason.toGCThing()->maybeCompartment() != teeState->compartment())
+        {
+            AutoRealm ar(cx, teeState);
+            if (!cx->compartment()->wrap(cx, &reason))
+                return nullptr;
+        }
+        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 reason1(cx, teeState->reason1());
+        RootedValue reason2(cx, teeState->reason2());
+        if (teeState->compartment() != cx->compartment()) {
+            if (!cx->compartment()->wrap(cx, &reason1) || !cx->compartment()->wrap(cx, &reason2))
+                return nullptr;
+        }
+        compositeReason->initDenseElement(0, reason1);
+        compositeReason->initDenseElement(1, 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;
+        {
+            AutoRealm ar(cx, promise);
+            if (!cancelResult) {
+                if (!RejectWithPendingError(cx, promise)) {
+                    return nullptr;
+                }
+            } else {
+                // Step c: Resolve teeState.[[promise]] with cancelResult.
+                RootedValue resultVal(cx, ObjectValue(*cancelResult));
+                if (!cx->compartment()->wrap(cx, &resultVal))
+                    return nullptr;
+                if (!PromiseObject::resolve(cx, promise, resultVal)) {
+                    return nullptr;
+                }
             }
         }
     }
 
     // Step 5: Return teeState.[[promise]].
-    return teeState->promise();
+    RootedObject promise(cx, teeState->promise());
+    if (promise->compartment() != cx->compartment()) {
+        if (!cx->compartment()->wrap(cx, &promise))
+            return nullptr;
+    }
+    return promise;
 }
 
 static MOZ_MUST_USE bool
-ReadableStreamControllerError(JSContext* cx, HandleNativeObject controller, HandleValue e);
+ReadableStreamDefaultControllerErrorIfNeeded(JSContext* cx,
+                                             Handle<ReadableStreamDefaultController*> 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).
+        // Step a.iii: Set teeState.[[closedOrErrored]] to true.
+        // Reordered to ensure that internal errors in the other steps don't
+        // leave the teeState in an undefined state.
+        teeState->setClosedOrErrored();
+
+        // Step a.i: Perform ! ReadableStreamDefaultControllerErrorIfNeeded(pull.[[branch1]], r).
         Rooted<ReadableStreamDefaultController*> branch1(cx, teeState->branch1());
-        if (!ReadableStreamControllerError(cx, branch1, reason)) {
+        if (!ReadableStreamDefaultControllerErrorIfNeeded(cx, branch1, reason)) {
             return false;
         }
 
-        // Step a.ii: Perform ! ReadableStreamDefaultControllerError(pull.[[branch2]], r).
+        // Step a.ii: Perform ! ReadableStreamDefaultControllerErrorIfNeeded(pull.[[branch2]], r).
         Rooted<ReadableStreamDefaultController*> branch2(cx, teeState->branch2());
-        if (!ReadableStreamControllerError(cx, branch2, reason)) {
+        if (!ReadableStreamDefaultControllerErrorIfNeeded(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 )
+/**
+ * Streams spec, 3.3.6. ReadableStreamTee ( stream, cloneForBranch2 )
+ *
+ * Note: can operate on unwrapped ReadableStream instances from
+ * another compartment. The returned branch streams and their associated
+ * controllers  are always created in the current cx compartment.
+ */
 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).
 
@@ -1308,16 +1592,17 @@ ReadableStreamTee(JSContext* cx, Handle<
 
     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]].
+    // Our implementation stores the controllers on the TeeState instead.
 
     // 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;
     }
@@ -1325,401 +1610,488 @@ ReadableStreamTee(JSContext* cx, Handle<
     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)
+inline static MOZ_MUST_USE bool
+AppendToListAtSlot(JSContext* cx, HandleNativeObject container, uint32_t slot, HandleObject obj);
+
+/**
+ * Streams spec, 3.4.1. ReadableStreamAddReadIntoRequest ( stream )
+ * Streams spec, 3.4.2. ReadableStreamAddReadRequest ( stream )
+ *
+ * Note: can operate on unwrapped ReadableStream instances from another
+ * compartment.
+ *
+ * Note: The returned Promise is created in the current cx compartment.
+ */
+static MOZ_MUST_USE JSObject*
+ReadableStreamAddReadOrReadIntoRequest(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 1: Assert: ! IsReadableStreamBYOBReader(stream.[[reader]]) is true.
+    // Skipped: handles both kinds of readers.
+    Rooted<ReadableStreamReader*> reader(cx, ReaderFromStream(cx, stream));
+    if (!reader)
+        return nullptr;
+
+    // Step 2 of 3.4.2: Assert: stream.[[state]] is "readable".
+    MOZ_ASSERT_IF(reader->is<ReadableStreamDefaultReader>(), stream->readable());
 
     // Step 3: Let promise be a new promise.
-    Rooted<PromiseObject*> promise(cx, PromiseObject::createSkippingExecutor(cx));
+    RootedObject 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>());
+    // Step 4: Let read{Into}Request be Record {[[promise]]: promise}.
+    // Step 5: Append read{Into}Request as the last element of
+    //         stream.[[reader]].[[read{Into}Requests]].
     // Since [[promise]] is the Record's only field, we store it directly.
-    val = ObjectValue(*promise);
-    if (!AppendToList(cx, readIntoRequests, val)) {
+    if (!AppendToListAtSlot(cx, reader, ReaderSlot_Requests, promise)) {
         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);
+ReadableStreamControllerCancelSteps(JSContext* cx, Handle<ReadableStreamController*> 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 )
+/**
+ * Streams spec, 3.4.3. ReadableStreamCancel ( stream, reason )
+ *
+ * Note: can operate on unwrapped ReadableStream instances from
+ * another compartment. `reason` must be in the cx compartment.
+ */
 /* static */ MOZ_MUST_USE JSObject*
 ReadableStream::cancel(JSContext* cx, Handle<ReadableStream*> stream, HandleValue reason)
 {
+    AssertSameCompartment(cx, 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));
+        if (!cx->compartment()->wrap(cx, &storedError))
+            return nullptr;
         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));
+    Rooted<ReadableStreamController*> 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 )
+/**
+ * Streams spec, 3.4.4. ReadableStreamClose ( stream )
+ *
+ * Note: can operate on unwrapped ReadableStream instances from
+ * another compartment.
+ */
 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;
+    // 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 4: If reader is undefined, return (reordered).
+    if (!HasReader(stream)) {
+        return true;
+    }
+
+    // Step 3: Let reader be stream.[[reader]].
+    Rooted<ReadableStreamReader*> reader(cx, ReaderFromStream(cx, stream));
+    if (!reader) {
+        return false;
+    }
+
+    // If the close operation was triggered from another global,
+    // the reader's read requests and close Promise might not be objects or
+    // wrappers from the current compartment.
+    bool needsWrapping = reader->compartment() != cx->compartment();
+
+    // Step 5: If ! IsReadableStreamDefaultReader(reader) is true,
+    if (reader->is<ReadableStreamDefaultReader>()) {
+        // Step a: Repeat for each readRequest that is an element of
+        //         reader.[[readRequests]],
+        RootedValue val(cx, 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();
+                if (needsWrapping && !cx->compartment()->wrap(cx, &readRequest)) {
+                    return false;
+                }
+
+                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 (needsWrapping && !cx->compartment()->wrap(cx, &closedPromise)) {
+        return false;
+    }
+    if (!ResolvePromise(cx, closedPromise, UndefinedHandleValue)) {
+        return false;
+    }
+
+    if (stream->mode() == JS::ReadableStreamMode::ExternalSource &&
+        cx->runtime()->readableStreamClosedCallback)
+    {
+        // Make sure we're in the stream's compartment.
+        AutoRealm ar(cx, stream);
+        ReadableStreamController* 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 )
+/**
+ * Streams spec, 3.4.5. ReadableStreamError ( stream, e )
+ *
+ * Note: can operate on unwrapped ReadableStream instances from
+ * another compartment.
+ */
 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 6: If reader is undefined, return (reordered).
+    if (!HasReader(stream)) {
+        return true;
+    }
+
     // 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>());
+    Rooted<ReadableStreamReader*> reader(cx, ReaderFromStream(cx, stream));
+    if (!reader) {
+        return false;
+    }
 
     // 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);
+    RootedValue val(cx, reader->getFixedSlot(ReaderSlot_Requests));
     RootedNativeObject readRequests(cx, &val.toObject().as<NativeObject>());
-    Rooted<PromiseObject*> readRequest(cx);
+    RootedObject 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)) {
+        readRequest = &val.toObject();
+
+        // Responses have to be created in the compartment from which the
+        // error was triggered, which might not be the same as the one the
+        // request was created in, so we have to wrap requests here.
+        if (!cx->compartment()->wrap(cx, &readRequest))
+            return false;
+
+        if (!RejectPromise(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)) {
+    RootedObject closedPromise(cx, &reader->getFixedSlot(ReaderSlot_ClosedPromise).toObject());
+
+    // The closedPromise might have been created in another compartment.
+    // RejectPromise can deal with wrapped Promise objects, but has to be
+    // with all arguments in the current compartment, so we do need to wrap
+    // the Promise.
+    if (!cx->compartment()->wrap(cx, &closedPromise)) {
+        return false;
+    }
+    if (!RejectPromise(cx, closedPromise, e)) {
         return false;
     }
 
     if (stream->mode() == JS::ReadableStreamMode::ExternalSource &&
         cx->runtime()->readableStreamErroredCallback)
     {
-        NativeObject* controller = ControllerFromStream(stream);
+        // Make sure we're in the stream's compartment.
+        AutoRealm ar(cx, stream);
+        ReadableStreamController* controller = ControllerFromStream(stream);
         void* source = controller->getFixedSlot(ControllerSlot_UnderlyingSource).toPrivate();
+
+        // Ensure that the embedding doesn't have to deal with
+        // mixed-compartment arguments to the callback.
+        RootedValue error(cx, e);
+        if (!cx->compartment()->wrap(cx, &error))
+            return false;
+
         cx->runtime()->readableStreamErroredCallback(cx, stream, source,
-                                                     stream->embeddingFlags(), e);
+                                                     stream->embeddingFlags(), error);
     }
 
     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.
+/**
+ * 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.
+ *
+ * Note: can operate on unwrapped values from other compartments for either
+ * |stream| and/or |chunk|. The iteration result object created in the course
+ * of this function's operation is created in the current cx compartment.
+ */
 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>());
+    Rooted<ReadableStreamReader*> reader(cx, ReaderFromStream(cx, stream));
+    if (!reader)
+        return false;
 
     // 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);
+    RootedValue val(cx, reader->getFixedSlot(ReaderSlot_Requests));
     RootedNativeObject readIntoRequests(cx, &val.toObject().as<NativeObject>());
-    Rooted<PromiseObject*> readIntoRequest(cx);
-    readIntoRequest = ShiftFromList<PromiseObject>(cx, readIntoRequests);
+    RootedObject readIntoRequest(cx, ShiftFromList<JSObject>(cx, readIntoRequests));
     MOZ_ASSERT(readIntoRequest);
+    if (!cx->compartment()->wrap(cx, &readIntoRequest))
+        return false;
 
     // Step 4: Resolve readIntoRequest.[[promise]] with
     //         ! CreateIterResultObject(chunk, done).
-    RootedObject iterResult(cx, CreateIterResultObject(cx, chunk, done));
+    RootedValue wrappedChunk(cx, chunk);
+    if (!cx->compartment()->wrap(cx, &wrappedChunk))
+        return false;
+    RootedObject iterResult(cx, CreateIterResultObject(cx, wrappedChunk, done));
     if (!iterResult) {
         return false;
     }
     val = ObjectValue(*iterResult);
-    return PromiseObject::resolve(cx, readIntoRequest, val);
+    return ResolvePromise(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);
+
+    JS::AutoSuppressGCAnalysis nogc;
+    ReadableStreamReader* reader = ReaderFromStream(nullptr, stream);
+
+    // Reader is a dead wrapper, treat it as non-existent.
+    if (!reader) {
+        return 0;
+    }
+
     Value readRequests = reader->getFixedSlot(ReaderSlot_Requests);
     return readRequests.toObject().as<NativeObject>().getDenseInitializedLength();
 }
 
-// Stream spec 3.4.10. ReadableStreamHasBYOBReader ( stream )
+enum class ReaderMode
+{
+    None,
+    Default,
+};
+
+#if DEBUG
+// Streams spec 3.4.11. ReadableStreamHasDefaultReader ( stream )
 static MOZ_MUST_USE bool
-ReadableStreamHasBYOBReader(ReadableStream* stream)
+ReadableStreamHasDefaultReader(JSContext* cx, ReadableStream* stream, bool* result)
 {
     // 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.
+    if (stream->getFixedSlot(StreamSlot_Reader).isUndefined()) {
+        *result = false;
+        return true;
+    }
+
     // 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>();
+    JSObject* readerObj = ReaderFromStream(cx, stream);
+    if (!readerObj)
+        return false;
+
+    *result = readerObj->is<ReadableStreamDefaultReader>();
+    return true;
+}
+#endif // DEBUG
+
+static MOZ_MUST_USE bool
+ReadableStreamGetReaderMode(JSContext* cx, ReadableStream* stream, ReaderMode* mode)
+{
+    if (stream->getFixedSlot(StreamSlot_Reader).isUndefined()) {
+        *mode = ReaderMode::None;
+        return true;
+    }
+
+    JSObject* readerObj = ReaderFromStream(cx, stream);
+    if (!readerObj)
+        return false;
+
+    *mode = ReaderMode::Default;
+
+    return true;
 }
 
 static MOZ_MUST_USE bool
 ReadableStreamReaderGenericInitialize(JSContext* cx,
-                                      HandleNativeObject reader,
+                                      Handle<ReadableStreamReader*> reader,
                                       Handle<ReadableStream*> stream);
 
-// Stream spec, 3.5.3. new ReadableStreamDefaultReader ( stream )
-// Steps 2-4.
+/**
+ * Stream spec, 3.5.3. new ReadableStreamDefaultReader ( stream )
+ * Steps 2-4.
+ *
+ * Note: can operate on unwrapped ReadableStream instances from
+ * another compartment. The returned object will always be created in the
+ * current cx compartment.
+ */
 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);
+        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 )
+/**
+ * Stream spec, 3.5.3. new ReadableStreamDefaultReader ( stream )
+ *
+ * Note: can handle ReadableStream instances from another compartment.
+ */
 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));
+    if (!IsMaybeWrapped<ReadableStream>(args.get(0))) {
+        ReportArgTypeError(cx, "ReadableStreamDefaultReader", "ReadableStream", args.get(0));
         return false;
     }
 
-    Rooted<ReadableStream*> stream(cx, &args.get(0).toObject().as<ReadableStream>());
+    Rooted<ReadableStream*> stream(cx,
+                                   &CheckedUnwrap(&args.get(0).toObject())->as<ReadableStream>());
 
     RootedObject reader(cx, CreateReadableStreamDefaultReader(cx, stream));
     if (!reader) {
         return false;
     }
 
     args.rval().setObject(*reader);
     return true;
@@ -1728,44 +2100,54 @@ ReadableStreamDefaultReader::constructor
 // 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");
+    auto reader = ToUnwrapped<ReadableStreamDefaultReader>(cx, args.thisv(),
+                                                           "ReadableStreamDefaultReader",
+                                                           "get closed");
+    if (!reader) {
+        return ReturnPromiseRejectedWithPendingError(cx, args);
     }
 
     // Step 2: Return this.[[closedPromise]].
-    NativeObject* reader = &args.thisv().toObject().as<NativeObject>();
-    args.rval().set(reader->getFixedSlot(ReaderSlot_ClosedPromise));
+    RootedValue closedPromise(cx, reader->getFixedSlot(ReaderSlot_ClosedPromise));
+    if (!cx->compartment()->wrap(cx, &closedPromise)) {
+        return false;
+    }
+
+    args.rval().set(closedPromise);
     return true;
 }
 
 static MOZ_MUST_USE JSObject*
-ReadableStreamReaderGenericCancel(JSContext* cx, HandleNativeObject reader, HandleValue reason);
+ReadableStreamReaderGenericCancel(JSContext* cx, Handle<ReadableStreamReader*> 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");
+    Rooted<ReadableStreamDefaultReader*> reader(cx);
+    reader = ToUnwrapped<ReadableStreamDefaultReader>(cx, args.thisv(),
+                                                      "ReadableStreamDefaultReader", "cancel");
+    if (!reader) {
+        return ReturnPromiseRejectedWithPendingError(cx, args);
     }
 
     // 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));
@@ -1779,353 +2161,163 @@ ReadableStreamDefaultReader_cancel(JSCon
 // 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");
+    Rooted<ReadableStreamDefaultReader*> reader(cx);
+    reader = ToUnwrapped<ReadableStreamDefaultReader>(cx, args.thisv(),
+                                                      "ReadableStreamDefaultReader", "read");
+    if (!reader) {
+        return ReturnPromiseRejectedWithPendingError(cx, args);
     }
 
     // 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
+ReadableStreamReaderGenericRelease(JSContext* cx, Handle<ReadableStreamReader*> reader);
+
+/**
+ * Streams spec, 3.5.4.4. releaseLock ( )
+ *
+ * Note: can operate on unwrapped ReadableStreamDefaultReader instances from
+ * another compartment.
+ */
+static bool
 ReadableStreamDefaultReader_releaseLock_impl(JSContext* cx, const CallArgs& args)
 {
     Rooted<ReadableStreamDefaultReader*> reader(cx);
-    reader = &args.thisv().toObject().as<ReadableStreamDefaultReader>();
+    reader = &UncheckedUnwrap(&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");
+                                        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>,
+    return CallNonGenericMethod<IsMaybeWrapped<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
 };
 
+const Class ReadableStreamReader::class_ = {
+    "ReadableStreamReader"
+};
+
 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);
+ReadableStreamControllerCallPullIfNeeded(JSContext* cx,
+                                         Handle<ReadableStreamController*> 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 )
+/**
+ * Streams spec, 3.7.3. ReadableStreamReaderGenericCancel ( reader, reason )
+ *
+ * Note: can operate on unwrapped ReadableStream reader instances from
+ * another compartment.
+ */
 static MOZ_MUST_USE JSObject*
-ReadableStreamReaderGenericCancel(JSContext* cx, HandleNativeObject reader, HandleValue reason)
+ReadableStreamReaderGenericCancel(JSContext* cx, Handle<ReadableStreamReader*> reader,
+                                  HandleValue reason)
 {
     // Step 1: Let stream be reader.[[ownerReadableStream]].
-    Rooted<ReadableStream*> stream(cx, StreamFromReader(reader));
-
     // Step 2: Assert: stream is not undefined (implicit).
+    Rooted<ReadableStream*> stream(cx, StreamFromReader(cx, reader));
+    if (!stream)
+        return nullptr;
 
     // Step 3: Return ! ReadableStreamCancel(stream, reason).
-    return &ReadableStreamCancel(cx, stream, reason)->as<PromiseObject>();
+    return ReadableStream::cancel(cx, stream, reason);
 }
 
-// Streams spec, 3.7.4. ReadableStreamReaderGenericInitialize ( reader, stream )
+/**
+ * Streams spec, 3.7.4. ReadableStreamReaderGenericInitialize ( reader, stream )
+ *
+ * Note: can operate on unwrapped ReadableStream reader instances from
+ * another compartment.
+ */
 static MOZ_MUST_USE bool
-ReadableStreamReaderGenericInitialize(JSContext* cx, HandleNativeObject reader,
+ReadableStreamReaderGenericInitialize(JSContext* cx, Handle<ReadableStreamReader*> 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));
+    if (!IsObjectInContextCompartment(stream, cx)) {
+        RootedObject wrappedStream(cx, stream);
+        if (!cx->compartment()->wrap(cx, &wrappedStream))
+            return false;
+        reader->setFixedSlot(ReaderSlot_Stream, ObjectValue(*wrappedStream));
+        AutoRealm ar(cx, stream);
+        RootedObject wrappedReader(cx, reader);
+        if (!cx->compartment()->wrap(cx, &wrappedReader))
+            return false;
+        stream->setFixedSlot(StreamSlot_Reader, ObjectValue(*wrappedReader));
+    } else {
+        reader->setFixedSlot(ReaderSlot_Stream, ObjectValue(*stream));
+        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
@@ -2136,115 +2328,116 @@ ReadableStreamReaderGenericInitialize(JS
     } 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));
+        if (!cx->compartment()->wrap(cx, &storedError))
+            return false;
         promise = PromiseObject::unforgeableReject(cx, storedError);
     }
 
     if (!promise) {
         return false;
     }
 
     reader->setFixedSlot(ReaderSlot_ClosedPromise, ObjectValue(*promise));
     return true;
 }
 
-// Streams spec, 3.7.5. ReadableStreamReaderGenericRelease ( reader )
+/**
+ * Streams spec, 3.7.5. ReadableStreamReaderGenericRelease ( reader )
+ *
+ * Note: can operate on unwrapped ReadableStream reader instances from
+ * another compartment.
+ */
 static MOZ_MUST_USE bool
-ReadableStreamReaderGenericRelease(JSContext* cx, HandleNativeObject reader)
+ReadableStreamReaderGenericRelease(JSContext* cx, Handle<ReadableStreamReader*> reader)
 {
     // Step 1: Assert: reader.[[ownerReadableStream]] is not undefined.
-    Rooted<ReadableStream*> stream(cx, StreamFromReader(reader));
+    Rooted<ReadableStream*> stream(cx, StreamFromReader(cx, reader));
+    if (!stream)
+        return false;
 
     // Step 2: Assert: reader.[[ownerReadableStream]].[[reader]] is reader.
-    MOZ_ASSERT(&stream->getFixedSlot(StreamSlot_Reader).toObject() == reader);
+    MOZ_ASSERT(ReaderFromStream(cx, stream) == 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;
     }
 
+    // The reader might be from another compartment. In that case we need to
+    // enter the reader's compartment before storing the above-created
+    // exception. We might delay entering the compartment until we have also
+    // created the closedPromise in step 4 below.
+    mozilla::Maybe<AutoRealm> ar;
+
     // 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)) {
+        Value val = reader->getFixedSlot(ReaderSlot_ClosedPromise);
+        Rooted<PromiseObject*> closedPromise(cx, &val.toObject().as<PromiseObject>());
+        if (closedPromise->compartment() != cx->compartment()) {
+            ar.emplace(cx, closedPromise);
+            if (!cx->compartment()->wrap(cx, &exn)) {
                 return false;
             }
+        }
+        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;
         }
+        if (reader->compartment() != cx->compartment()) {
+            ar.emplace(cx, reader);
+            if (!cx->compartment()->wrap(cx, &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 )
+ReadableStreamControllerPullSteps(JSContext* cx, Handle<ReadableStreamController*> controller);
+
+/**
+ * Streams spec, 3.7.7. ReadableStreamDefaultReaderRead ( reader )
+ *
+ * Note: can operate on unwrapped ReadableStreamDefaultReader instances from
+ * another compartment.
+ */
 /* 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));
+    Rooted<ReadableStream*> stream(cx, StreamFromReader(cx, reader));
+    if (!stream)
+        return nullptr;
 
     // 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));
@@ -2254,35 +2447,38 @@ ReadableStreamDefaultReader::read(JSCont
         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));
+        if (!cx->compartment()->wrap(cx, &storedError))
+            return nullptr;
         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));
+    Rooted<ReadableStreamController*> 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()));
+    Rooted<ReadableStreamController*> controller(cx);
+    controller = TargetFromHandler<ReadableStreamController>(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)));
@@ -2293,31 +2489,28 @@ ControllerStartHandler(JSContext* cx, un
     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);
+ReadableStreamControllerError(JSContext* cx, Handle<ReadableStreamController*> 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()));
+    Rooted<ReadableStreamController*> controllerObj(cx);
+    controllerObj = TargetFromHandler<ReadableStreamController>(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));
     }
@@ -2338,19 +2531,25 @@ ValidateAndNormalizeHighWaterMark(JSCont
                                   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.
+/**
+ * Streams spec, 3.8.3 new ReadableStreamDefaultController ( stream, underlyingSource,
+ *                                                           size, highWaterMark )
+ * Steps 3 - 11.
+ *
+ * Note: can NOT operate on unwrapped ReadableStream instances from
+ * another compartment: ReadableStream controllers must be created in the same
+ * compartment as the stream.
+ */
 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) {
@@ -2454,26 +2653,26 @@ ReadableStreamDefaultController::constru
         return false;
     }
 
     args.rval().setObject(*controller);
     return true;
 }
 
 static MOZ_MUST_USE double
-ReadableStreamControllerGetDesiredSizeUnchecked(NativeObject* controller);
+ReadableStreamControllerGetDesiredSizeUnchecked(ReadableStreamController* 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>();
+    Rooted<ReadableStreamController*> controller(cx);
+    controller = &args.thisv().toObject().as<ReadableStreamController>();
 
     // 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()) {
@@ -2501,19 +2700,24 @@ ReadableStreamDefaultController_desiredS
     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.
+/**
+ * Unified implementation of steps 2-3 of 3.8.4.2 and 3.10.4.3.
+ *
+ * Note: can operate on unwrapped ReadableStream controller instances from
+ * another compartment.
+ */
 static MOZ_MUST_USE bool
-VerifyControllerStateForClosing(JSContext* cx, HandleNativeObject controller)
+VerifyControllerStateForClosing(JSContext* cx, Handle<ReadableStreamController*> 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;
     }
 
@@ -2524,22 +2728,27 @@ VerifyControllerStateForClosing(JSContex
         JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
                                   JSMSG_READABLESTREAMCONTROLLER_NOT_READABLE, "close");
         return false;
     }
 
     return true;
 }
 
-// Streams spec, 3.8.4.2 close()
+/**
+ * Streams spec, 3.8.4.2 close()
+ *
+ * Note: can operate on unwrapped ReadableStreamDefaultController instances from
+ * another compartment.
+ */
 static MOZ_MUST_USE bool
 ReadableStreamDefaultController_close_impl(JSContext* cx, const CallArgs& args)
 {
     Rooted<ReadableStreamDefaultController*> controller(cx);
-    controller = &args.thisv().toObject().as<ReadableStreamDefaultController>();
+    controller = &UncheckedUnwrap(&args.thisv().toObject())->as<ReadableStreamDefaultController>();
 
     // Steps 2-3.
     if (!VerifyControllerStateForClosing(cx, controller)) {
         return false;
     }
 
     // Step 4: Perform ! ReadableStreamDefaultControllerClose(this).
     if (!ReadableStreamDefaultControllerClose(cx, controller)) {
@@ -2551,73 +2760,72 @@ ReadableStreamDefaultController_close_im
 
 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>,
+    return CallNonGenericMethod<IsMaybeWrapped<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>();
+    controller = &UncheckedUnwrap(&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");
+                                  JSMSG_READABLESTREAMCONTROLLER_CLOSED, "enqueue");
         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");
+                                  JSMSG_READABLESTREAMCONTROLLER_NOT_READABLE, "enqueue");
         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>,
+    return CallNonGenericMethod<IsMaybeWrapped<ReadableStreamDefaultController>,
                                 ReadableStreamDefaultController_enqueue_impl>(cx, args);
 }
 
-// Streams spec, 3.8.4.4. error ( e )
+/**
+ * Streams spec, 3.8.4.4. error ( e )
+ *
+ * Note: can operate on unwrapped ReadableStreamDefaultController instances from
+ * another compartment.
+ */
 static MOZ_MUST_USE bool
 ReadableStreamDefaultController_error_impl(JSContext* cx, const CallArgs& args)
 {
     Rooted<ReadableStreamDefaultController*> controller(cx);
-    controller = &args.thisv().toObject().as<ReadableStreamDefaultController>();
+    controller = &UncheckedUnwrap(&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;
     }
@@ -2632,99 +2840,140 @@ ReadableStreamDefaultController_error_im
 
 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>,
+    return CallNonGenericMethod<IsMaybeWrapped<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
 };
 
+const Class ReadableStreamController::class_ = {
+    "ReadableStreamController"
+};
+
 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 )
+ *
+ * Note: can operate on unwrapped ReadableStream |controller| instances
+ * from another compartment. |reason| must be in the current cx compartment.
  */
 static MOZ_MUST_USE JSObject*
-ReadableStreamControllerCancelSteps(JSContext* cx, HandleNativeObject controller,
+ReadableStreamControllerCancelSteps(JSContext* cx, Handle<ReadableStreamController*> controller,
                                     HandleValue reason)
 {
-    MOZ_ASSERT(IsReadableStreamController(controller));
+    AssertSameCompartment(cx, reason);
 
     // 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);
+            PullIntoDescriptor* descriptor;
+            descriptor = ToUnwrapped<PullIntoDescriptor>(cx, PeekList<JSObject>(pendingPullIntos));
+            if (!descriptor)
+                return nullptr;
+            descriptor->setBytesFilled(0);
         }
     }
 
+    RootedValue underlyingSource(cx);
+    underlyingSource = controller->getFixedSlot(ControllerSlot_UnderlyingSource);
+
     // 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>());
+    // Note: this special-cases the underlying source of tee'd stream's
+    // branches. Instead of storing a JSFunction as the "cancel" property on
+    // those, we check if the source is a, maybe wrapped, TeeState instance
+    // and manually dispatch to the right internal function. TeeState is fully
+    // under our control, so this isn't content-observable.
+    if (IsMaybeWrapped<TeeState>(underlyingSource)) {
+        Rooted<TeeState*> teeState(cx);
+        teeState = &UncheckedUnwrap(&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));
+        bool needsWrapping = controller->compartment() != cx->compartment();
         RootedValue rval(cx);
-        rval = cx->runtime()->readableStreamCancelCallback(cx, stream, source,
-                                                           stream->embeddingFlags(), reason);
+        {
+            RootedValue wrappedReason(cx, reason);
+            mozilla::Maybe<AutoRealm> ar;
+            if (needsWrapping) {
+                ar.emplace(cx, controller);
+                if (!cx->compartment()->wrap(cx, &wrappedReason))
+                    return nullptr;
+            }
+            void* source = underlyingSource.toPrivate();
+            Rooted<ReadableStream*> stream(cx, StreamFromController(controller));
+            rval = cx->runtime()->readableStreamCancelCallback(cx, stream, source,
+                                                                stream->embeddingFlags(),
+                                                                wrappedReason);
+        }
+
+        if (needsWrapping && !cx->compartment()->wrap(cx, &rval))
+            return nullptr;
         return PromiseObject::unforgeableResolve(cx, rval);
     }
 
+    // If the stream and its controller aren't in the cx compartment, we have
+    // to ensure that the underlying source is correctly wrapped before
+    // operating on it.
+    if (!cx->compartment()->wrap(cx, &underlyingSource))
+        return nullptr;
+
     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]]()
+DequeueValue(JSContext* cx, Handle<ReadableStreamController*> container, MutableHandleValue chunk);
+
+/**
+ * Streams spec, 3.8.5.2. ReadableStreamDefaultController [[PullSteps]]()
+ *
+ * Note: can operate on unwrapped ReadableStreamDefaultController instances
+ * from another compartment.
+ */
 static JSObject*
-ReadableStreamDefaultControllerPullSteps(JSContext* cx, HandleNativeObject controller)
+ReadableStreamDefaultControllerPullSteps(JSContext* cx,
+                                         Handle<ReadableStreamController*> 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);
@@ -2752,26 +3001,28 @@ ReadableStreamDefaultControllerPullSteps
         // Step c: Otherwise, perform ! ReadableStreamDefaultControllerCallPullIfNeeded(this).
         else {
         if (!ReadableStreamControllerCallPullIfNeeded(cx, controller)) {
             return nullptr;
         }
         }
 
         // Step d: Return a promise resolved with ! CreateIterResultObject(chunk, false).
+        if (!cx->compartment()->wrap(cx, &chunk))
+            return nullptr;
         RootedObject iterResultObj(cx, CreateIterResultObject(cx, chunk, false));
         if (!iterResultObj) {
-          return nullptr;
+            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));
+    RootedObject pendingPromise(cx, ReadableStreamAddReadOrReadIntoRequest(cx, stream));
     if (!pendingPromise) {
         return nullptr;
     }
 
     // Step 4: Perform ! ReadableStreamDefaultControllerCallPullIfNeeded(this).
     if (!ReadableStreamControllerCallPullIfNeeded(cx, controller)) {
         return nullptr;
     }
@@ -2781,17 +3032,23 @@ ReadableStreamDefaultControllerPullSteps
 }
 
 // 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()));
+
+    RootedValue controllerVal(cx, args.callee().as<JSFunction>().getExtendedSlot(0));
+    Rooted<ReadableStreamController*> controller(cx);
+    controller = ToUnwrapped<ReadableStreamController>(cx, controllerVal);
+    if (!controller)
+        return false;
+
     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) {
@@ -2806,42 +3063,52 @@ ControllerPullHandler(JSContext* cx, uns
 }
 
 // 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);
 
+    RootedValue controllerVal(cx, args.callee().as<JSFunction>().getExtendedSlot(0));
+    Rooted<ReadableStreamController*> controller(cx);
+    controller = ToUnwrapped<ReadableStreamController>(cx, controllerVal);
+    if (!controller)
+        return false;
+
     // 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);
+ReadableStreamControllerShouldCallPull(ReadableStreamController* controller);
 
 static MOZ_MUST_USE double
-ReadableStreamControllerGetDesiredSizeUnchecked(NativeObject* controller);
-
-// Streams spec, 3.9.2 ReadableStreamDefaultControllerCallPullIfNeeded ( controller )
-// and
-// Streams spec, 3.12.3. ReadableByteStreamControllerCallPullIfNeeded ( controller )
+ReadableStreamControllerGetDesiredSizeUnchecked(ReadableStreamController* controller);
+
+/**
+ * Streams spec, 3.9.2 ReadableStreamDefaultControllerCallPullIfNeeded ( controller )
+ * Streams spec, 3.12.3. ReadableByteStreamControllerCallPullIfNeeded ( controller )
+ *
+ * Note: can operate on unwrapped instances from other compartments for
+ * |controller|.
+ */
 inline static MOZ_MUST_USE bool
-ReadableStreamControllerCallPullIfNeeded(JSContext* cx, HandleNativeObject controller)
+ReadableStreamControllerCallPullIfNeeded(JSContext* cx,
+                                         Handle<ReadableStreamController*> controller)
 {
     // Step 1: Let shouldPull be
     //         ! ReadableByteStreamControllerShouldCallPull(controller).
     bool shouldPull = ReadableStreamControllerShouldCallPull(controller);
 
     // Step 2: If shouldPull is false, return.
     if (!shouldPull) {
         return true;
@@ -2859,59 +3126,67 @@ ReadableStreamControllerCallPullIfNeeded
     // 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);
+    RootedObject wrappedController(cx, controller);
+    if (!cx->compartment()->wrap(cx, &wrappedController))
+        return false;
+    RootedValue controllerVal(cx, ObjectValue(*wrappedController));
     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>());
+    RootedObject pullPromise(cx);
+
+    if (IsMaybeWrapped<TeeState>(underlyingSource)) {
+        Rooted<TeeState*> teeState(cx);
+        teeState = &UncheckedUnwrap(&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));
+    RootedObject onPullFulfilled(cx, NewHandler(cx, ControllerPullHandler, wrappedController));
     if (!onPullFulfilled) {
         return false;
     }
 
-    RootedObject onPullRejected(cx, NewHandler(cx, ControllerPullFailedHandler, controller));
+    RootedObject onPullRejected(cx, NewHandler(cx, ControllerPullFailedHandler, wrappedController));
     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 )
+/**
+ * Streams spec, 3.9.3. ReadableStreamDefaultControllerShouldCallPull ( controller )
+ * Streams spec, 3.12.24. ReadableByteStreamControllerShouldCallPull ( controller )
+ *
+ * Note: can operate on unwrapped ReadableStream controller instances from
+ * another compartment.
+ */
 static bool
-ReadableStreamControllerShouldCallPull(NativeObject* controller)
+ReadableStreamControllerShouldCallPull(ReadableStreamController* 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.
@@ -2941,17 +3216,22 @@ ReadableStreamControllerShouldCallPull(N
     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 )
+/**
+ * Streams spec, 3.9.4. ReadableStreamDefaultControllerClose ( controller )
+ *
+ * Note: can operate on unwrapped ReadableStream controller instances from
+ * another compartment.
+ */
 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.
@@ -2969,25 +3249,32 @@ ReadableStreamDefaultControllerClose(JSC
     if (queue->getDenseInitializedLength() == 0) {
         return ReadableStreamCloseInternal(cx, stream);
     }
 
     return true;
 }
 
 static MOZ_MUST_USE bool
-EnqueueValueWithSize(JSContext* cx, HandleNativeObject container, HandleValue value,
+EnqueueValueWithSize(JSContext* cx, Handle<ReadableStreamController*> container, HandleValue value,
                      HandleValue sizeVal);
 
-// Streams spec, 3.9.5. ReadableStreamDefaultControllerEnqueue ( controller, chunk )
+/**
+ * Streams spec, 3.9.5. ReadableStreamDefaultControllerEnqueue ( controller, chunk )
+ *
+ * Note: can operate on unwrapped instances from other compartments for
+ * |controller|. |chunk| must be in the current cx compartment.
+ */
 static MOZ_MUST_USE bool
 ReadableStreamDefaultControllerEnqueue(JSContext* cx,
                                        Handle<ReadableStreamDefaultController*> controller,
                                        HandleValue chunk)
 {
+    AssertSameCompartment(cx, 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());
@@ -3005,16 +3292,18 @@ ReadableStreamDefaultControllerEnqueue(J
         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).
+            if (!cx->compartment()->wrap(cx, &strategySize))
+                return false;
             success = Call(cx, strategySize, UndefinedHandleValue, chunk, &chunkSize);
         }
 
         // Step c: Let enqueueResult be
         //         EnqueueValueWithSize(controller, chunk, chunkSize).
         if (success) {
             success = EnqueueValueWithSize(cx, controller, chunk, chunkSize);
         }
@@ -3036,34 +3325,36 @@ ReadableStreamDefaultControllerEnqueue(J
             }
 
             // Step b.ii.2: Return chunkSize.
             return false;
         }
     }
 
     // Step 6: Perform ! ReadableStreamDefaultControllerCallPullIfNeeded(controller).
-    if (!ReadableStreamControllerCallPullIfNeeded(cx, controller)) {
-        return false;
-    }
-
     // Step 7: Return.
-    return true;
+    return ReadableStreamControllerCallPullIfNeeded(cx, controller);
 }
 
 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 )
+ReadableByteStreamControllerClearPendingPullIntos(JSContext* cx,
+                                                  Handle<ReadableStreamController*> controller);
+
+/**
+ * Streams spec, 3.9.6. ReadableStreamDefaultControllerError ( controller, e )
+ * Streams spec, 3.12.10. ReadableByteStreamControllerError ( controller, e )
+ *
+ * Note: can operate on unwrapped ReadableStream controller instances from
+ * another compartment.
+ */
 static MOZ_MUST_USE bool
-ReadableStreamControllerError(JSContext* cx, HandleNativeObject controller, HandleValue e)
+ReadableStreamControllerError(JSContext* cx, Handle<ReadableStreamController*> controller,
+                              HandleValue e)
 {
-    MOZ_ASSERT(IsReadableStreamController(controller));
+    AssertSameCompartment(cx, e);
 
     // 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:
@@ -3080,52 +3371,67 @@ ReadableStreamControllerError(JSContext*
     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
+inline static double QueueSize(const NativeObject* container);
+inline static void SetQueueSize(NativeObject* container, double size);
+
+/**
+ * Streams spec, 3.9.7. ReadableStreamDefaultControllerErrorIfNeeded ( controller, e ) nothrow
+ *
+ * Note: can operate on unwrapped ReadableStreamDefaultController instances from
+ * another compartment.
+ */
 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 )
+/**
+ * Streams spec, 3.9.8. ReadableStreamDefaultControllerGetDesiredSize ( controller )
+ * Streams spec 3.12.13. ReadableByteStreamControllerGetDesiredSize ( controller )
+ */
 static MOZ_MUST_USE double
-ReadableStreamControllerGetDesiredSizeUnchecked(NativeObject* controller)
+ReadableStreamControllerGetDesiredSizeUnchecked(ReadableStreamController* 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();
+    double queueSize = QueueSize(controller);
     return strategyHWM - queueSize;
 }
 
-// Streams spec, 3.10.3 new ReadableByteStreamController ( stream, underlyingSource,
-//                                                         highWaterMark )
-// Steps 3 - 16.
+/**
+ * Streams spec, 3.10.3 new ReadableByteStreamController ( stream, underlyingSource,
+ *                                                         highWaterMark )
+ * Steps 3 - 16.
+ *
+ * Note: can NOT operate on unwrapped ReadableStream instances from
+ * another compartment: ReadableStream controllers must be created in the same
+ * compartment as the stream.
+ */
 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) {
@@ -3289,17 +3595,17 @@ CreateReadableByteStreamController(JSCon
 
     // 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));
+    SetQueueSize(controller, 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").
@@ -3335,224 +3641,21 @@ CreateReadableByteStreamController(JSCon
 
     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>();
 
@@ -3587,33 +3690,45 @@ static const ClassOps ReadableByteStream
 
 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)
+ReadableByteStreamControllerHandleQueueDrain(JSContext* cx,
+                                             Handle<ReadableStreamController*> controller);
+
+/**
+ * Streams spec, 3.10.5.2. [[PullSteps]] ()
+ *
+ * Note: can operate on unwrapped instances from other compartments for
+ * |controller|. Any instances created in the course of this
+ * function's operation are created in the current cx compartment.
+ */
+static MOZ_MUST_USE JSObject*
+ReadableByteStreamControllerPullSteps(JSContext* cx, Handle<ReadableStreamController*> 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));
+    // Step 2: Assert: ! ReadableStreamHasDefaultReader(stream) is true.
+#ifdef DEBUG
+    bool result;
+    if (!ReadableStreamHasDefaultReader(cx, stream, &result))
+        return nullptr;
+    MOZ_ASSERT(result);
+#endif
 
     RootedValue val(cx);
     // Step 3: If this.[[queueTotalSize]] > 0,
-    double queueTotalSize = controller->getFixedSlot(QueueContainerSlot_TotalSize).toNumber();
+    double queueTotalSize = QueueSize(controller);
     if (queueTotalSize > 0) {
-        // Step 3.a: MOZ_ASSERT: ! ReadableStreamGetNumReadRequests(_stream_) is 0.
+        // Step 3.a: 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();
 
@@ -3626,48 +3741,53 @@ ReadableByteStreamControllerPullSteps(JS
             {
                 JS::AutoSuppressGCAnalysis suppressGC(cx);
                 JS::AutoCheckCannotGC noGC;
                 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.
+                // TODO: make this compartment-safe.
                 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);
+            Rooted<ByteStreamChunk*> entry(cx);
+            entry = ToUnwrapped<ByteStreamChunk>(cx, ShiftFromList<JSObject>(cx, queue));
+            if (!entry)
+                return nullptr;
 
             queueTotalSize = queueTotalSize - entry->byteLength();
 
             // Step 3.f: Let view be ! Construct(%Uint8Array%, « entry.[[buffer]],
             //                                   entry.[[byteOffset]], entry.[[byteLength]] »).
             // (reordered)
             RootedObject buffer(cx, entry->buffer());
+            if (!cx->compartment()->wrap(cx, &buffer))
+                return nullptr;
 
             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));
+        SetQueueSize(controller, 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).
@@ -3710,26 +3830,25 @@ ReadableByteStreamControllerPullSteps(JS
                                                         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)) {
+        if (!AppendToListAtSlot(cx, controller, ByteControllerSlot_PendingPullIntos,
+                                pullIntoDescriptor))
+        {
             return nullptr;
         }
     }
 
     // Step 6: Let promise be ! ReadableStreamAddReadRequest(stream).
-    Rooted<PromiseObject*> promise(cx, ReadableStreamAddReadRequest(cx, stream));
+    RootedObject promise(cx, ReadableStreamAddReadOrReadIntoRequest(cx, stream));
     if (!promise) {
         return nullptr;
     }
 
     // Step 7: Perform ! ReadableByteStreamControllerCallPullIfNeeded(this).
     if (!ReadableStreamControllerCallPullIfNeeded(cx, controller)) {
         return nullptr;
     }
@@ -3739,1360 +3858,201 @@ ReadableByteStreamControllerPullSteps(JS
 }
 
 /**
  * Unified implementation of ReadableStream controllers' [[PullSteps]] internal
  * methods.
  * Streams spec, 3.8.5.2. [[PullSteps]] ()
  * and
  * Streams spec, 3.10.5.2. [[PullSteps]] ()
+ *
+ * Note: can operate on unwrapped ReadableStream controller instances from
+ * another compartment.
  */
 static MOZ_MUST_USE JSObject*
-ReadableStreamControllerPullSteps(JSContext* cx, HandleNativeObject controller)
+ReadableStreamControllerPullSteps(JSContext* cx, Handle<ReadableStreamController*> 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;
-    }
-
-    Rooted<ArrayBufferViewObject*> view(cx, &viewVal.toObject().as<ArrayBufferViewObject>());
-
-    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)
+ReadableByteStreamControllerInvalidateBYOBRequest(JSContext* cx,
+                                                  Handle<ReadableStreamController*> controller);
+
+/**
+ * Streams spec, 3.12.4. ReadableByteStreamControllerClearPendingPullIntos ( controller )
+ *
+ * Note: can operate on unwrapped instances from other compartments for
+ * |controller|. The List created in step 2 is guaranteed to be in the same
+ * compartment as the controller.
+ */
+static MOZ_MUST_USE bool
+ReadableByteStreamControllerClearPendingPullIntos(JSContext* cx,
+                                                  Handle<ReadableStreamController*> controller)
 {
     MOZ_ASSERT(controller->is<ReadableByteStreamController>());
 
     // Step 1: Perform ! ReadableByteStreamControllerInvalidateBYOBRequest(controller).
-    ReadableByteStreamControllerInvalidateBYOBRequest(controller);
+    if (!ReadableByteStreamControllerInvalidateBYOBRequest(cx, controller))
+        return false;
 
     // Step 2: Set controller.[[pendingPullIntos]] to a new empty List.
     return SetNewList(cx, controller, ByteControllerSlot_PendingPullIntos);
 }
 
-// Streams spec, 3.12.5. ReadableByteStreamControllerClose ( controller )
+/**
+ * Streams spec, 3.12.5. ReadableByteStreamControllerClose ( controller )
+ *
+ * Note: can operate on unwrapped ReadableByteStreamController instances from
+ * another compartment.
+ */
 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();
+    double queueTotalSize = QueueSize(controller);
     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);
+        firstPendingPullInto = ToUnwrapped<PullIntoDescriptor>(cx,
+                                                               PeekList<JSObject>(pendingPullIntos));
+        if (!firstPendingPullInto)
+            return false;
 
         // Step b: If firstPendingPullInto.[[bytesFilled]] > 0,
         if (firstPendingPullInto->bytesFilled() > 0) {
-            // Step i: Let e be a new TypeError exception. {
+            // 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) {
-        ctor = GlobalObject::getOrCreateConstructor(cx, JSProto_Uint8Array);
-        if (!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 suppressGC(cx);
-            JS::AutoCheckCannotGC noGC;
-            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 )
+/**
+ * Streams spec, 3.12.14. ReadableByteStreamControllerHandleQueueDrain ( controller )
+ *
+ * Note: can operate on unwrapped instances from other compartments for
+ * |controller|.
+ */
 static MOZ_MUST_USE bool
-ReadableByteStreamControllerHandleQueueDrain(JSContext* cx, HandleNativeObject controller)
+ReadableByteStreamControllerHandleQueueDrain(JSContext* cx,
+                                             Handle<ReadableStreamController*> 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();
+    double totalSize = QueueSize(controller);
     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)
+/**
+ * Streams spec 3.12.15. ReadableByteStreamControllerInvalidateBYOBRequest ( controller )
+ *
+ * Note: can operate on unwrapped instances from other compartments for
+ * |controller|.
+ */
+static MOZ_MUST_USE bool
+ReadableByteStreamControllerInvalidateBYOBRequest(JSContext* cx,
+                                                  Handle<ReadableStreamController*> controller)
 {
     MOZ_ASSERT(controller->is<ReadableByteStreamController>());
 
     // Step 1: If controller.[[byobRequest]] is undefined, return.
-    Value byobRequestVal = controller->getFixedSlot(ByteControllerSlot_BYOBRequest);
+    RootedValue byobRequestVal(cx, controller->getFixedSlot(ByteControllerSlot_BYOBRequest));
     if (byobRequestVal.isUndefined()) {
-        return;
-    }
-
-    NativeObject* byobRequest = &byobRequestVal.toObject().as<NativeObject>();
+        return true;
+    }
+
+    RootedNativeObject byobRequest(cx, ToUnwrapped<NativeObject>(cx, byobRequestVal));
+    if (!byobRequest) {
+        return false;
+    }
+
     // 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);
-
-        ctor = GlobalObject::getOrCreateConstructor(cx, protoKey);
-        if (!ctor) {
-            return nullptr;
-        }
-        elementSize = 1 << TypedArrayShift(view->as<TypedArrayObject>().type());
-    } else {
-        // Step 3: Let ctor be %DataView% (reordered).
-        ctor = GlobalObject::getOrCreateConstructor(cx, JSProto_DataView);
-        if (!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);
@@ -5189,62 +4149,75 @@ static const JSFunctionSpec CountQueuing
     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
+/**
+ * Streams spec, 6.3.1. DequeueValue ( container ) nothrow
+ *
+ * Note: can operate on unwrapped queue container instances from another
+ * compartment. In that case, the returned chunk will be wrapped into the
+ * current compartment.
+ */
 inline static MOZ_MUST_USE bool
-DequeueValue(JSContext* cx, HandleNativeObject container, MutableHandleValue chunk)
+DequeueValue(JSContext* cx, Handle<ReadableStreamController*> container, MutableHandleValue chunk)
 {
     // Step 1: Assert: container has [[queue]] and [[queueTotalSize]] internal
-    //         slots.
-    MOZ_ASSERT(IsReadableStreamController(container));
-
+    //         slots (implicit).
     // 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();
+    double totalSize = QueueSize(container);
 
     totalSize -= pair->size();
     if (totalSize < 0) {
         totalSize = 0;
     }
-    container->setFixedSlot(QueueContainerSlot_TotalSize, NumberValue(totalSize));
+    SetQueueSize(container, totalSize);
+
+    val = pair->value();
+    if (container->compartment() != cx->compartment() && !cx->compartment()->wrap(cx, &val)) {
+        return false;
+    }
 
     // Step 7: Return pair.[[value]].
-    chunk.set(pair->value());
+    chunk.set(val);
     return true;
 }
 
-// Streams spec, 6.3.2. EnqueueValueWithSize ( container, value, size ) throws
+/**
+ * Streams spec, 6.3.2. EnqueueValueWithSize ( container, value, size ) throws
+ *
+ * Note: can operate on unwrapped queue container instances from another
+ * compartment than the current one. In that case, the given value will be
+ * wrapped into the container compartment.
+ */
 static MOZ_MUST_USE bool
-EnqueueValueWithSize(JSContext* cx, HandleNativeObject container, HandleValue value,
+EnqueueValueWithSize(JSContext* cx, Handle<ReadableStreamController*> container, HandleValue value,
                      HandleValue sizeVal)
 {
     // Step 1: Assert: container has [[queue]] and [[queueTotalSize]] internal
-    //         slots.
-    MOZ_ASSERT(IsReadableStreamController(container));
-
+    //         slots (implicit).
     // 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.
@@ -5254,75 +4227,113 @@ EnqueueValueWithSize(JSContext* cx, Hand
         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;
+    RootedValue wrappedVal(cx, value);
+    {
+        mozilla::Maybe<AutoRealm> ar;
+        if (container->compartment() != cx->compartment()) {
+            ar.emplace(cx, container);
+            if (!cx->compartment()->wrap(cx, &wrappedVal)) {
+                return false;
+            }
+        }
+
+        QueueEntry* entry = QueueEntry::create(cx, wrappedVal, 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));
+    SetQueueSize(container, QueueSize(container) + size);
 
     return true;
 }
 
 // Streams spec, 6.3.3. PeekQueueValue ( container ) nothrow
 // Used by WritableStream.
 // static MOZ_MUST_USE Value
-// PeekQueueValue(NativeObject* container)
+// PeekQueueValue(ReadableStreamController* container)
 // {
 //     // Step 1: Assert: container has [[queue]] and [[queueTotalSize]] internal
-//     //         slots.
-//     MOZ_ASSERT(IsReadableStreamController(container));
-
+//     //         slots (implicit).
 //     // 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)
+ResetQueue(JSContext* cx, Handle<ReadableStreamController*> container)
 {
     // Step 1: Assert: container has [[queue]] and [[queueTotalSize]] internal
-    //         slots.
-    MOZ_ASSERT(IsReadableStreamController(container));
-
+    //         slots (implicit).
     // 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));
+    SetQueueSize(container, 0);
 
     return true;
 }
 
+inline static double
+QueueSize(const NativeObject* container)
+{
+    return container->getFixedSlot(QueueContainerSlot_TotalSize).toNumber();
+}
+
+inline static void
+SetQueueSize(NativeObject* container, double size)
+{
+    container->setFixedSlot(QueueContainerSlot_TotalSize, NumberValue(size));
+}
+
+/**
+ * Appends the given |obj| to the given list |container|'s list.
+ *
+ * Note: can operate on |container| and |obj| combinations from different
+ * compartments, in which case |obj| is wrapped before storing it.
+ */
+inline static MOZ_MUST_USE bool
+AppendToListAtSlot(JSContext* cx, HandleNativeObject container, uint32_t slot, HandleObject obj)
+{
+    RootedValue val(cx, container->getFixedSlot(slot));
+    RootedNativeObject list(cx, &val.toObject().as<NativeObject>());
+
+    val = ObjectValue(*obj);
+
+    AutoRealm ar(cx, list);
+    if (!cx->compartment()->wrap(cx, &val))
+        return false;
+    return AppendToList(cx, list, val);
+}
+
 
 /**
  * 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)
 {
@@ -5363,48 +4374,16 @@ PromiseInvokeOrNoop(JSContext* cx, Handl
     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;
     }
@@ -5440,72 +4419,62 @@ ValidateAndNormalizeQueuingStrategy(JSCo
 
     // 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));
+    Rooted<ReadableStreamReader*> reader(cx, &readerObj->as<ReadableStreamReader>());
+    ReadableStream* stream = StreamFromReader(cx, reader);
+    if (!stream)
+        return false;
     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);
+    Rooted<ReadableStreamReader*> reader(cx, &readerObj->as<ReadableStreamReader>());
+    ReadableStream* stream = StreamFromReader(cx, reader);
+    if (!stream)
+        return false;
+    MOZ_ASSERT(ReadableStreamGetNumReadRequests(stream) == 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);
+    ReadableStreamController* 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()) {
@@ -5574,27 +4543,31 @@ ReadableStream::updateDataAvailableFromS
         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));
+    SetQueueSize(controller, 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)) {
+    ReaderMode readerMode;
+    if (!ReadableStreamGetReaderMode(cx, stream, &readerMode))
+        return false;
+
+    if (readerMode == ReaderMode::Default) {
         // 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>());
@@ -5609,38 +4582,28 @@ ReadableStream::updateDataAvailableFromS
         {
             JS::AutoSuppressGCAnalysis suppressGC(cx);
             JS::AutoCheckCannotGC noGC;
             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.
+            // TODO: make cross-compartment safe.
             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;
-        }
+        SetQueueSize(controller, availableData - bytesWritten);
     } else {
         // Step b: Otherwise,
         // Step i: Assert: ! IsReadableStreamLocked(stream) is false.
         MOZ_ASSERT(!stream->locked());
 
         // Step ii: Perform
         //          ! ReadableByteStreamControllerEnqueueChunkToQueue(controller,
         //                                                            transferredBuffer,
@@ -5650,17 +4613,17 @@ ReadableStream::updateDataAvailableFromS
     }
 
     return true;
 }
 
 MOZ_MUST_USE bool
 ReadableStream::close(JSContext* cx, Handle<ReadableStream*> stream)
 {
-    RootedNativeObject controllerObj(cx, ControllerFromStream(stream));
+    Rooted<ReadableStreamController*> 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);
@@ -5677,36 +4640,33 @@ ReadableStream::error(JSContext* cx, Han
     // 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));
+    Rooted<ReadableStreamController*> 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*
+MOZ_MUST_USE ReadableStreamReader*
 ReadableStream::getReader(JSContext* cx, Handle<ReadableStream*> stream,
                           JS::ReadableStreamReaderMode mode)
 {
-    if (mode == JS::ReadableStreamReaderMode::Default) {
-        return CreateReadableStreamDefaultReader(cx, stream);
-    }
-    return CreateReadableStreamBYOBReader(cx, stream);
+    return CreateReadableStreamDefaultReader(cx, stream);
 }
 
 JS_FRIEND_API(JSObject*)
 js::UnwrapReadableStream(JSObject* obj)
 {
     if (JSObject* unwrapped = CheckedUnwrap(obj)) {
         return unwrapped->is<ReadableStream>() ? unwrapped : nullptr;
     }
--- a/js/src/builtin/Stream.h
+++ b/js/src/builtin/Stream.h
@@ -8,25 +8,28 @@
 #define builtin_Stream_h
 
 #include "builtin/Promise.h"
 #include "vm/NativeObject.h"
 
 
 namespace js {
 
+class ReadableStreamReader : public NativeObject
+{
+  public:
+    static const Class class_;
+};
+
 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;
 
@@ -37,28 +40,27 @@ class ReadableStream : public NativeObje
     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 ReadableStreamReader* 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);
 
@@ -75,80 +77,63 @@ class ReadableStream : public NativeObje
   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
+class ReadableStreamDefaultReader : public ReadableStreamReader
 {
   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
+class ReadableStreamController : public NativeObject
+{
+  public:
+    static const Class class_;
+};
+
+class ReadableStreamDefaultController : public ReadableStreamController
 {
   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
+class ReadableByteStreamController : public ReadableStreamController
 {
   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_;
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/realms/bug1385890-c50.js
@@ -0,0 +1,20 @@
+// |jit-test| skip-if: !this.hasOwnProperty("ReadableStream")
+// See <https://bugzilla.mozilla.org/show_bug.cgi?id=1385890#c50>.
+
+let otherGlobal = newGlobal();
+function getFreshInstances(type, otherType = type) {
+    stream = new ReadableStream({
+        start(c) {
+            controller = c;
+        },
+        type
+    });
+}
+getFreshInstances();
+let [branch1, branch2] = otherGlobal.ReadableStream.prototype.tee.call(stream);
+cancelPromise1 = ReadableStream.prototype.cancel.call(branch1, {
+    name: "cancel 1"
+});
+cancelPromise2 = ReadableStream.prototype.cancel.call(branch2, {
+    name: "cancel 2"
+});
--- a/js/src/js.msg
+++ b/js/src/js.msg
@@ -630,16 +630,17 @@ MSG_DEF(JSMSG_ITERATOR_NO_THROW,       0
 // 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_BYTES_TYPE_NOT_IMPLEMENTED, 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_LOCKED_METHOD,              1, JSEXN_TYPEERR, "'{0}' can't 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.")
--- a/js/src/jsapi-tests/moz.build
+++ b/js/src/jsapi-tests/moz.build
@@ -79,16 +79,17 @@ UNIFIED_SOURCES += [
     'testParseJSON.cpp',
     'testPersistentRooted.cpp',
     'testPreserveJitCode.cpp',
     'testPrintf.cpp',
     'testPrivateGCThingValue.cpp',
     'testProfileStrings.cpp',
     'testPromise.cpp',
     'testPropCache.cpp',
+    'testReadableStream.cpp',
     'testRegExp.cpp',
     'testResolveRecursion.cpp',
     'tests.cpp',
     'testSameValue.cpp',
     'testSavedStacks.cpp',
     'testScriptInfo.cpp',
     'testScriptObject.cpp',
     'testSetProperty.cpp',
--- a/js/src/jsapi-tests/testReadableStream.cpp
+++ b/js/src/jsapi-tests/testReadableStream.cpp
@@ -6,57 +6,60 @@
  * 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";
+struct StubExternalUnderlyingSource {
+    void* buffer;
+};
+
+char testBufferData[] = "1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
+
+StubExternalUnderlyingSource stubExternalUnderlyingSource = {
+    testBufferData
+};
+
+static_assert(MOZ_ALIGNOF(StubExternalUnderlyingSource) > 1,
+              "UnderlyingSource pointers must not have the low bit set");
 
 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;
+
+    MOZ_ASSERT(underlyingSource == &stubExternalUnderlyingSource);
+    MOZ_ASSERT(stubExternalUnderlyingSource.buffer == testBufferData);
+    MOZ_ASSERT(length <= sizeof(testBufferData));
+    memcpy(buffer, testBufferData, length);
     *bytesWritten = length;
 }
 
 static bool cancelStreamCBCalled = false;
 static Value cancelStreamReason;
 static Value
 CancelStreamCB(JSContext* cx, HandleObject stream, void* underlyingSource, uint8_t flags,
                HandleValue reason)
@@ -145,111 +148,95 @@ GetReadChunk(JSContext* cx, HandleObject
     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)
+struct StreamTestFixture : public JSAPITest
+{
+    virtual ~StreamTestFixture() {}
+};
+
+BEGIN_FIXTURE_TEST(StreamTestFixture,
+                   testReadableStream_NewReadableStream)
 {
     RootedObject stream(cx, NewDefaultStream(cx));
     CHECK(stream);
-    CHECK(ReadableStreamGetMode(stream) == ReadableStreamMode::Default);
+    ReadableStreamMode mode;
+    CHECK(ReadableStreamGetMode(cx, stream, &mode));
+    CHECK(mode == ReadableStreamMode::Default);
     return true;
 }
-END_TEST(testReadableStream_NewReadableStream)
+END_FIXTURE_TEST(StreamTestFixture,
+                 testReadableStream_NewReadableStream)
 
-BEGIN_TEST(testReadableStream_NewReadableByteStream)
-{
-    RootedObject stream(cx, NewByteStream(cx));
-    CHECK(stream);
-    CHECK(ReadableStreamGetMode(stream) == ReadableStreamMode::Byte);
-    return true;
-}
-END_TEST(testReadableStream_NewReadableByteStream)
-
-BEGIN_TEST(testReadableStream_ReadableStreamGetReaderDefault)
+BEGIN_FIXTURE_TEST(StreamTestFixture,
+                   testReadableStream_ReadableStreamGetReaderDefault)
 {
     RootedObject stream(cx, NewDefaultStream(cx));
     CHECK(stream);
 
     RootedObject reader(cx, ReadableStreamGetReader(cx, stream, ReadableStreamReaderMode::Default));
     CHECK(reader);
     CHECK(IsReadableStreamDefaultReader(reader));
-    CHECK(ReadableStreamIsLocked(stream));
-    CHECK(!ReadableStreamReaderIsClosed(reader));
+    bool locked;
+    CHECK(ReadableStreamIsLocked(cx, stream, &locked));
+    CHECK(locked);
+    bool closed;
+    CHECK(ReadableStreamReaderIsClosed(cx, reader, &closed));
+    CHECK(!closed);
 
     return true;
 }
-END_TEST(testReadableStream_ReadableStreamGetReaderDefault)
-
-BEGIN_TEST(testReadableStream_ReadableStreamGetReaderBYOB)
-{
-    RootedObject stream(cx, NewByteStream(cx));
-    CHECK(stream);
+END_FIXTURE_TEST(StreamTestFixture,
+                 testReadableStream_ReadableStreamGetReaderDefault)
 
-    RootedObject reader(cx, ReadableStreamGetReader(cx, stream, ReadableStreamReaderMode::BYOB));
-    CHECK(reader);
-    CHECK(IsReadableStreamBYOBReader(reader));
-    CHECK(ReadableStreamIsLocked(stream));
-    CHECK(!ReadableStreamReaderIsClosed(reader));
-
-    return true;
-}
-END_TEST(testReadableStream_ReadableStreamGetReaderBYOB)
-
-BEGIN_TEST(testReadableStream_ReadableStreamTee)
+BEGIN_FIXTURE_TEST(StreamTestFixture,
+                   testReadableStream_ReadableStreamTee)
 {
     RootedObject stream(cx, NewDefaultStream(cx));
     CHECK(stream);
 
     RootedObject leftStream(cx);
     RootedObject rightStream(cx);
     CHECK(ReadableStreamTee(cx, stream, &leftStream, &rightStream));
-    CHECK(ReadableStreamIsLocked(stream));
+    bool locked;
+    CHECK(ReadableStreamIsLocked(cx, stream, &locked));
+    CHECK(locked);
     CHECK(leftStream);
     CHECK(IsReadableStream(leftStream));
     CHECK(rightStream);
     CHECK(IsReadableStream(rightStream));
 
     return true;
 }
-END_TEST(testReadableStream_ReadableStreamTee)
+END_FIXTURE_TEST(StreamTestFixture,
+                 testReadableStream_ReadableStreamTee)
 
-BEGIN_TEST(testReadableStream_ReadableStreamEnqueue)
+BEGIN_FIXTURE_TEST(StreamTestFixture,
+                   testReadableStream_ReadableStreamEnqueue)
 {
     RootedObject stream(cx, NewDefaultStream(cx));
     CHECK(stream);
 
     RootedObject chunk(cx, JS_NewPlainObject(cx));
     CHECK(chunk);
     RootedValue chunkVal(cx, ObjectValue(*chunk));
     CHECK(ReadableStreamEnqueue(cx, stream, chunkVal));
 
     return true;
 }
-END_TEST(testReadableStream_ReadableStreamEnqueue)
-
-BEGIN_TEST(testReadableStream_ReadableByteStreamEnqueue)
-{
-    RootedObject stream(cx, NewDefaultStream(cx));
-    CHECK(stream);
+END_FIXTURE_TEST(StreamTestFixture,
+                 testReadableStream_ReadableStreamEnqueue)
 
-    RootedObject chunk(cx, JS_NewUint8Array(cx, 42));
-    CHECK(chunk);
-    CHECK(!ReadableByteStreamEnqueueBuffer(cx, stream, chunk));
-    CHECK(JS_IsExceptionPending(cx));
-
-    return true;
-}
-END_TEST(testReadableStream_ReadableByteStreamEnqueue)
-
-BEGIN_TEST(testReadableStream_ReadableStreamDefaultReaderRead)
+BEGIN_FIXTURE_TEST(StreamTestFixture,
+                   testReadableStream_ReadableStreamDefaultReaderRead)
 {
     RootedObject stream(cx, NewDefaultStream(cx));
     CHECK(stream);
     RootedObject reader(cx, ReadableStreamGetReader(cx, stream, ReadableStreamReaderMode::Default));
     CHECK(reader);
 
     RootedObject request(cx, ReadableStreamDefaultReaderRead(cx, reader));
     CHECK(request);
@@ -260,102 +247,25 @@ BEGIN_TEST(testReadableStream_ReadableSt
     CHECK(chunk);
     RootedValue chunkVal(cx, ObjectValue(*chunk));
     CHECK(ReadableStreamEnqueue(cx, stream, chunkVal));
 
     CHECK(GetReadChunk(cx, request) == chunk);
 
     return true;
 }
-END_TEST(testReadableStream_ReadableStreamDefaultReaderRead)
-
-BEGIN_TEST(testReadableStream_ReadableByteStreamDefaultReaderRead)
-{
-    RootedObject stream(cx, NewByteStream(cx));
-    CHECK(stream);
-
-    RootedObject reader(cx, ReadableStreamGetReader(cx, stream, ReadableStreamReaderMode::Default));
-    CHECK(reader);
-
-    RootedObject request(cx, ReadableStreamDefaultReaderRead(cx, reader));
-    CHECK(request);
-    CHECK(IsPromiseObject(request));
-    CHECK(GetPromiseState(request) == PromiseState::Pending);
-
-    size_t length = sizeof(test_buffer_data);
-    RootedObject buffer(cx, JS_NewArrayBufferWithExternalContents(cx, length, test_buffer_data));
-    CHECK(buffer);
-    RootedObject chunk(cx, JS_NewUint8ArrayWithBuffer(cx, buffer, 0, length));
-    CHECK(chunk);
-    bool isShared;
-    CHECK(!JS_IsDetachedArrayBufferObject(buffer));
-
-    CHECK(ReadableByteStreamEnqueueBuffer(cx, stream, chunk));
-
-    CHECK(JS_IsDetachedArrayBufferObject(buffer));
-    RootedObject readChunk(cx, GetReadChunk(cx, request));
-    CHECK(JS_IsUint8Array(readChunk));
-    void* readBufferData;
-    {
-        JS::AutoCheckCannotGC autoNoGC(cx);
-        readBufferData = JS_GetArrayBufferViewData(readChunk, &isShared, autoNoGC);
-    }
-    CHECK(readBufferData);
-    CHECK(!memcmp(test_buffer_data, readBufferData, length));
-
-    return true;
-}
-END_TEST(testReadableStream_ReadableByteStreamDefaultReaderRead)
+END_FIXTURE_TEST(StreamTestFixture,
+                 testReadableStream_ReadableStreamDefaultReaderRead)
 
-BEGIN_TEST(testReadableStream_ReadableByteStreamBYOBReaderRead)
-{
-    RootedObject stream(cx, NewByteStream(cx));
-    CHECK(stream);
-
-    RootedObject reader(cx, ReadableStreamGetReader(cx, stream, ReadableStreamReaderMode::BYOB));
-    CHECK(reader);
-
-    size_t length = sizeof(test_buffer_data);
-    RootedObject targetArray(cx, JS_NewUint8Array(cx, length));
-    bool isShared;
-
-    RootedObject request(cx, ReadableStreamBYOBReaderRead(cx, reader, targetArray));
-    CHECK(request);
-    CHECK(IsPromiseObject(request));
-    CHECK(GetPromiseState(request) == PromiseState::Pending);
-    CHECK(JS_IsDetachedArrayBufferObject(JS_GetArrayBufferViewBuffer(cx, targetArray, &isShared)));
-
-    RootedObject buffer(cx, JS_NewArrayBufferWithExternalContents(cx, length, test_buffer_data));
-    CHECK(buffer);
-    CHECK(!JS_IsDetachedArrayBufferObject(buffer));
-
-    CHECK(ReadableByteStreamEnqueueBuffer(cx, stream, buffer));
-
-    CHECK(JS_IsDetachedArrayBufferObject(buffer));
-    RootedObject readChunk(cx, GetReadChunk(cx, request));
-    CHECK(JS_IsUint8Array(readChunk));
-    void* readBufferData;
-    {
-        JS::AutoCheckCannotGC autoNoGC(cx);
-        readBufferData = JS_GetArrayBufferViewData(readChunk, &isShared, autoNoGC);
-    }
-    CHECK(readBufferData);
-    CHECK(!memcmp(test_buffer_data, readBufferData, length));
-    // TODO: eliminate the memcpy that happens here.
-//    CHECK(readBufferData == test_buffer_data);
-
-    return true;
-}
-END_TEST(testReadableStream_ReadableByteStreamBYOBReaderRead)
-
-BEGIN_TEST(testReadableStream_ReadableStreamDefaultReaderClose)
+BEGIN_FIXTURE_TEST(StreamTestFixture,
+                   testReadableStream_ReadableStreamDefaultReaderClose)
 {
     SetReadableStreamCallbacks(cx, &DataRequestCB, &WriteIntoRequestBufferCB,
-                                             &CancelStreamCB, &StreamClosedCB, &StreamErroredCB,
-                                             &FinalizeStreamCB);
+                               &CancelStreamCB, &StreamClosedCB, &StreamErroredCB,
+                               &FinalizeStreamCB);
     RootedObject stream(cx, NewDefaultStream(cx));
     CHECK(stream);
     RootedObject reader(cx, ReadableStreamGetReader(cx, stream, ReadableStreamReaderMode::Default));
     CHECK(reader);
 
     RootedObject request(cx, ReadableStreamDefaultReaderRead(cx, reader));
     CHECK(request);
     CHECK(IsPromiseObject(request));
@@ -370,180 +280,183 @@ BEGIN_TEST(testReadableStream_ReadableSt
     CHECK(value.isUndefined());
     CHECK(done);
 
     // The callbacks are only invoked for external streams.
     CHECK(!streamClosedCBCalled);
 
     return true;
 }
-END_TEST(testReadableStream_ReadableStreamDefaultReaderClose)
+END_FIXTURE_TEST(StreamTestFixture,
+                 testReadableStream_ReadableStreamDefaultReaderClose)
 
-BEGIN_TEST(testReadableStream_ReadableStreamDefaultReaderError)
+BEGIN_FIXTURE_TEST(StreamTestFixture,
+                   testReadableStream_ReadableStreamDefaultReaderError)
 {
     ResetCallbacks();
     SetReadableStreamCallbacks(cx, &DataRequestCB, &WriteIntoRequestBufferCB,
-                                             &CancelStreamCB, &StreamClosedCB, &StreamErroredCB,
-                                             &FinalizeStreamCB);
+                               &CancelStreamCB, &StreamClosedCB, &StreamErroredCB,
+                               &FinalizeStreamCB);
     RootedObject stream(cx, NewDefaultStream(cx));
     CHECK(stream);
     RootedObject reader(cx, ReadableStreamGetReader(cx, stream, ReadableStreamReaderMode::Default));
     CHECK(reader);
 
     RootedObject request(cx, ReadableStreamDefaultReaderRead(cx, reader));
     CHECK(request);
     CHECK(IsPromiseObject(request));
     CHECK(GetPromiseState(request) == PromiseState::Pending);
 
-    CHECK(ReadableStreamIsLocked(stream));
-    CHECK(ReadableStreamIsReadable(stream));
+    bool locked;
+    CHECK(ReadableStreamIsLocked(cx, stream, &locked));
+    CHECK(locked);
+    bool readable;
+    CHECK(ReadableStreamIsReadable(cx, stream, &readable));
+    CHECK(readable);
     RootedValue error(cx, Int32Value(42));
     CHECK(ReadableStreamError(cx, stream, error));
 
     CHECK(GetPromiseState(request) == PromiseState::Rejected);
     RootedValue reason(cx, GetPromiseResult(request));
     CHECK(reason.isInt32());
     CHECK(reason.toInt32() == 42);
 
     // The callbacks are only invoked for external streams.
     CHECK(!streamErroredCBCalled);
 
     return true;
 }
-END_TEST(testReadableStream_ReadableStreamDefaultReaderError)
+END_FIXTURE_TEST(StreamTestFixture,
+                 testReadableStream_ReadableStreamDefaultReaderError)
 
 static JSObject*
 NewExternalSourceStream(JSContext* cx, void* underlyingSource,
                         RequestReadableStreamDataCallback dataRequestCallback,
                         WriteIntoReadRequestBufferCallback writeIntoReadRequestCallback,
                         CancelReadableStreamCallback cancelCallback,
                         ReadableStreamClosedCallback closedCallback,
                         ReadableStreamErroredCallback erroredCallback,
                         ReadableStreamFinalizeCallback finalizeCallback)
 {
     SetReadableStreamCallbacks(cx, dataRequestCallback, writeIntoReadRequestCallback,
-                                             cancelCallback, closedCallback, erroredCallback,
-                                             finalizeCallback);
+                               cancelCallback, closedCallback, erroredCallback,
+                               finalizeCallback);
     RootedObject stream(cx, NewReadableExternalSourceStreamObject(cx, underlyingSource));
     MOZ_ASSERT_IF(stream, IsReadableStream(stream));
     return stream;
 }
 
-BEGIN_TEST(testReadableStream_CreateReadableByteStreamWithExternalSource)
+static JSObject*
+NewExternalSourceStream(JSContext* cx)
+{
+    return NewExternalSourceStream(cx,
+                                   &stubExternalUnderlyingSource,
+                                   &DataRequestCB,
+                                   &WriteIntoRequestBufferCB,
+                                   &CancelStreamCB,
+                                   &StreamClosedCB,
+                                   &StreamErroredCB,
+                                   &FinalizeStreamCB);
+}
+
+BEGIN_FIXTURE_TEST(StreamTestFixture,
+                   testReadableStream_CreateReadableByteStreamWithExternalSource)
 {
     ResetCallbacks();
 
-    RootedObject stream(cx, NewExternalSourceStream(cx, &test_buffer_data, &DataRequestCB,
-                                                    &WriteIntoRequestBufferCB, &CancelStreamCB,
-                                                    &StreamClosedCB, &StreamErroredCB,
-                                                    &FinalizeStreamCB));
+    RootedObject stream(cx, NewExternalSourceStream(cx));
     CHECK(stream);
-    CHECK(ReadableStreamGetMode(stream) == JS::ReadableStreamMode::ExternalSource);
+    ReadableStreamMode mode;
+    CHECK(ReadableStreamGetMode(cx, stream, &mode));
+    CHECK(mode == ReadableStreamMode::ExternalSource);
     void* underlyingSource;
     CHECK(ReadableStreamGetExternalUnderlyingSource(cx, stream, &underlyingSource));
-    CHECK(underlyingSource == &test_buffer_data);
-    CHECK(ReadableStreamIsLocked(stream));
-    ReadableStreamReleaseExternalUnderlyingSource(stream);
+    CHECK(underlyingSource == &stubExternalUnderlyingSource);
+    bool locked;
+    CHECK(ReadableStreamIsLocked(cx, stream, &locked));
+    CHECK(locked);
+    CHECK(ReadableStreamReleaseExternalUnderlyingSource(cx, stream));
 
     return true;
 }
-END_TEST(testReadableStream_CreateReadableByteStreamWithExternalSource)
+END_FIXTURE_TEST(StreamTestFixture,
+                 testReadableStream_CreateReadableByteStreamWithExternalSource)
 
-BEGIN_TEST(testReadableStream_ExternalSourceCancel)
+BEGIN_FIXTURE_TEST(StreamTestFixture,
+                   testReadableStream_ExternalSourceCancel)
 {
     ResetCallbacks();
 
-    RootedObject stream(cx, NewExternalSourceStream(cx, &test_buffer_data, &DataRequestCB,
-                                                    &WriteIntoRequestBufferCB, &CancelStreamCB,
-                                                    &StreamClosedCB, &StreamErroredCB,
-                                                    &FinalizeStreamCB));
+    RootedObject stream(cx, NewExternalSourceStream(cx));
     CHECK(stream);
     RootedValue reason(cx, Int32Value(42));
     CHECK(ReadableStreamCancel(cx, stream, reason));
     CHECK(cancelStreamCBCalled);
     CHECK(cancelStreamReason == reason);
 
     return true;
 }
-END_TEST(testReadableStream_ExternalSourceCancel)
+END_FIXTURE_TEST(StreamTestFixture,
+                 testReadableStream_ExternalSourceCancel)
 
-BEGIN_TEST(testReadableStream_ExternalSourceGetReader)
+BEGIN_FIXTURE_TEST(StreamTestFixture,
+                   testReadableStream_ExternalSourceGetReader)
 {
     ResetCallbacks();
 
-    RootedObject stream(cx, NewExternalSourceStream(cx, &test_buffer_data, &DataRequestCB,
-                                                    &WriteIntoRequestBufferCB, &CancelStreamCB,
-                                                    &StreamClosedCB, &StreamErroredCB,
-                                                    &FinalizeStreamCB));
+    RootedObject stream(cx, NewExternalSourceStream(cx));
     CHECK(stream);
 
     RootedValue streamVal(cx, ObjectValue(*stream));
     CHECK(JS_SetProperty(cx, global, "stream", streamVal));
     RootedValue rval(cx);
     EVAL("stream.getReader()", &rval);
     CHECK(rval.isObject());
     RootedObject reader(cx, &rval.toObject());
     CHECK(IsReadableStreamDefaultReader(reader));
 
     return true;
 }
-END_TEST(testReadableStream_ExternalSourceGetReader)
-
-BEGIN_TEST(testReadableStream_ExternalSourceUpdateAvailableData)
-{
-    ResetCallbacks();
+END_FIXTURE_TEST(StreamTestFixture,
+                 testReadableStream_ExternalSourceGetReader)
 
-    RootedObject stream(cx, NewExternalSourceStream(cx, &test_buffer_data, &DataRequestCB,
-                                                    &WriteIntoRequestBufferCB, &CancelStreamCB,
-                                                    &StreamClosedCB, &StreamErroredCB,
-                                                    &FinalizeStreamCB));
-    CHECK(stream);
-
-    ReadableStreamUpdateDataAvailableFromSource(cx, stream, 1024);
-
-    return true;
-}
-END_TEST(testReadableStream_ExternalSourceUpdateAvailableData)
-
-struct ReadFromExternalSourceFixture : public JSAPITest
+struct ReadFromExternalSourceFixture : public StreamTestFixture
 {
     virtual ~ReadFromExternalSourceFixture() {}
 
     bool readWithoutDataAvailable(const char* evalSrc, const char* evalSrc2,
                                   uint32_t writtenLength)
     {
         ResetCallbacks();
         definePrint();
 
-        RootedObject stream(cx, NewExternalSourceStream(cx, &test_buffer_data, &DataRequestCB,
-                                                        &WriteIntoRequestBufferCB,
-                                                        &CancelStreamCB,
-                                                        &StreamClosedCB, &StreamErroredCB,
-                                                        &FinalizeStreamCB));
+        RootedObject stream(cx, NewExternalSourceStream(cx));
         CHECK(stream);
         js::RunJobs(cx);
         void* underlyingSource;
         CHECK(ReadableStreamGetExternalUnderlyingSource(cx, stream, &underlyingSource));
-        CHECK(underlyingSource == &test_buffer_data);
-        CHECK(ReadableStreamIsLocked(stream));
-        ReadableStreamReleaseExternalUnderlyingSource(stream);
+        CHECK(underlyingSource == &stubExternalUnderlyingSource);
+        bool locked;
+        CHECK(ReadableStreamIsLocked(cx, stream, &locked));
+        CHECK(locked);
+        CHECK(ReadableStreamReleaseExternalUnderlyingSource(cx, stream));
 
         RootedValue streamVal(cx, ObjectValue(*stream));
         CHECK(JS_SetProperty(cx, global, "stream", streamVal));
 
         RootedValue rval(cx);
         EVAL(evalSrc, &rval);
         CHECK(dataRequestCBCalled);
         CHECK(!writeIntoRequestBufferCBCalled);
         CHECK(rval.isObject());
         RootedObject promise(cx, &rval.toObject());
         CHECK(IsPromiseObject(promise));
         CHECK(GetPromiseState(promise) == PromiseState::Pending);
 
-        size_t length = sizeof(test_buffer_data);
+        size_t length = sizeof(testBufferData);
         ReadableStreamUpdateDataAvailableFromSource(cx, stream, length);
 
         CHECK(writeIntoRequestBufferCBCalled);
         CHECK(GetPromiseState(promise) == PromiseState::Fulfilled);
         RootedValue iterVal(cx);
         bool done;
         if (!GetIterResult(cx, promise, &iterVal, &done)) {
             return false;
@@ -552,44 +465,43 @@ struct ReadFromExternalSourceFixture : p
         CHECK(!done);
         RootedObject chunk(cx, &iterVal.toObject());
         CHECK(JS_IsUint8Array(chunk));
 
         {
             JS::AutoCheckCannotGC noGC(cx);
             bool dummy;
             void* buffer = JS_GetArrayBufferViewData(chunk, &dummy, noGC);
-            CHECK(!memcmp(buffer, test_buffer_data, writtenLength));
+            CHECK(!memcmp(buffer, testBufferData, writtenLength));
         }
 
         dataRequestCBCalled = false;
         writeIntoRequestBufferCBCalled = false;
         EVAL(evalSrc2, &rval);
         CHECK(dataRequestCBCalled);
         CHECK(!writeIntoRequestBufferCBCalled);
 
         return true;
     }
 
     bool readWithDataAvailable(const char* evalSrc, uint32_t writtenLength) {
         ResetCallbacks();
         definePrint();
 
-        RootedObject stream(cx, NewExternalSourceStream(cx, &test_buffer_data, &DataRequestCB,
-                                                        &WriteIntoRequestBufferCB, &CancelStreamCB,
-                                                        &StreamClosedCB, &StreamErroredCB,
-                                                        &FinalizeStreamCB));
+        RootedObject stream(cx, NewExternalSourceStream(cx));
         CHECK(stream);
         void* underlyingSource;
         CHECK(ReadableStreamGetExternalUnderlyingSource(cx, stream, &underlyingSource));
-        CHECK(underlyingSource == &test_buffer_data);
-        CHECK(ReadableStreamIsLocked(stream));
-        ReadableStreamReleaseExternalUnderlyingSource(stream);
+        CHECK(underlyingSource == &stubExternalUnderlyingSource);
+        bool locked;
+        CHECK(ReadableStreamIsLocked(cx, stream, &locked));
+        CHECK(locked);
+        CHECK(ReadableStreamReleaseExternalUnderlyingSource(cx, stream));
 
-        size_t length = sizeof(test_buffer_data);
+        size_t length = sizeof(testBufferData);
         ReadableStreamUpdateDataAvailableFromSource(cx, stream, length);
 
         RootedValue streamVal(cx, ObjectValue(*stream));
         CHECK(JS_SetProperty(cx, global, "stream", streamVal));
 
         RootedValue rval(cx);
         EVAL(evalSrc, &rval);
         CHECK(writeIntoRequestBufferCBCalled);
@@ -606,37 +518,37 @@ struct ReadFromExternalSourceFixture : p
         CHECK(!done);
         RootedObject chunk(cx, &iterVal.toObject());
         CHECK(JS_IsUint8Array(chunk));
 
         {
             JS::AutoCheckCannotGC noGC(cx);
             bool dummy;
             void* buffer = JS_GetArrayBufferViewData(chunk, &dummy, noGC);
-            CHECK(!memcmp(buffer, test_buffer_data, writtenLength));
+            CHECK(!memcmp(buffer, testBufferData, writtenLength));
         }
 
         return true;
     }
 };
 
 BEGIN_FIXTURE_TEST(ReadFromExternalSourceFixture,
                    testReadableStream_ExternalSourceReadDefaultWithoutDataAvailable)
 {
-    return readWithoutDataAvailable("r = stream.getReader(); r.read()", "r.read()", sizeof(test_buffer_data));
+    return readWithoutDataAvailable("r = stream.getReader(); r.read()", "r.read()", sizeof(testBufferData));
 }
 END_FIXTURE_TEST(ReadFromExternalSourceFixture,
                  testReadableStream_ExternalSourceReadDefaultWithoutDataAvailable)
 
 BEGIN_FIXTURE_TEST(ReadFromExternalSourceFixture,
                    testReadableStream_ExternalSourceCloseWithPendingRead)
 {
     CHECK(readWithoutDataAvailable("r = stream.getReader(); request0 = r.read(); "
                                    "request1 = r.read(); request0", "r.read()",
-                                   sizeof(test_buffer_data)));
+                                   sizeof(testBufferData)));
 
     RootedValue val(cx);
     CHECK(JS_GetProperty(cx, global, "request1", &val));
     CHECK(val.isObject());
     RootedObject request(cx, &val.toObject());
     CHECK(IsPromiseObject(request));
     CHECK(GetPromiseState(request) == PromiseState::Pending);
 
@@ -657,28 +569,547 @@ BEGIN_FIXTURE_TEST(ReadFromExternalSourc
     return true;
 }
 END_FIXTURE_TEST(ReadFromExternalSourceFixture,
                  testReadableStream_ExternalSourceCloseWithPendingRead)
 
 BEGIN_FIXTURE_TEST(ReadFromExternalSourceFixture,
                    testReadableStream_ExternalSourceReadDefaultWithDataAvailable)
 {
-    return readWithDataAvailable("r = stream.getReader(); r.read()", sizeof(test_buffer_data));
+    return readWithDataAvailable("r = stream.getReader(); r.read()", sizeof(testBufferData));
 }
 END_FIXTURE_TEST(ReadFromExternalSourceFixture,
                  testReadableStream_ExternalSourceReadDefaultWithDataAvailable)
 
-BEGIN_FIXTURE_TEST(ReadFromExternalSourceFixture,
-                   testReadableStream_ExternalSourceReadBYOBWithoutDataAvailable)
+// Cross-global tests:
+BEGIN_FIXTURE_TEST(StreamTestFixture,
+                   testReadableStream_ReadableStreamOtherGlobalDefaultReaderRead)
+{
+    RootedObject stream(cx, NewDefaultStream(cx));
+    CHECK(stream);
+    RootedObject otherGlobal(cx, createGlobal());
+    CHECK(otherGlobal);
+
+    {
+        JSAutoRealm ar(cx, otherGlobal);
+        CHECK(JS_WrapObject(cx, &stream));
+        RootedObject reader(cx, ReadableStreamGetReader(cx, stream,
+                                                        ReadableStreamReaderMode::Default));
+        CHECK(reader);
+
+        RootedObject request(cx, ReadableStreamDefaultReaderRead(cx, reader));
+        CHECK(request);
+        CHECK(IsPromiseObject(request));
+        CHECK(!js::IsWrapper(request));
+        CHECK(GetPromiseState(request) == PromiseState::Pending);
+
+        RootedObject chunk(cx, JS_NewPlainObject(cx));
+        CHECK(chunk);
+        RootedValue chunkVal(cx, ObjectValue(*chunk));
+        CHECK(ReadableStreamEnqueue(cx, stream, chunkVal));
+
+        CHECK(GetReadChunk(cx, request) == chunk);
+    }
+
+    return true;
+}
+END_FIXTURE_TEST(StreamTestFixture,
+                 testReadableStream_ReadableStreamOtherGlobalDefaultReaderRead)
+
+BEGIN_FIXTURE_TEST(StreamTestFixture,
+                   testReadableStream_ReadableStreamGetEmbeddingFlags)
+{
+    RootedObject stream(cx, NewDefaultStream(cx));
+    CHECK(stream);
+    uint8_t flags;
+    CHECK(ReadableStreamGetEmbeddingFlags(cx, stream, &flags));
+
+    RootedObject otherGlobal(cx, createGlobal());
+    CHECK(otherGlobal);
+    {
+        JSAutoRealm ar(cx, otherGlobal);
+        CHECK(JS_WrapObject(cx, &stream));
+        uint8_t flags;
+        CHECK(ReadableStreamGetEmbeddingFlags(cx, stream, &flags));
+    }
+
+    return true;
+}
+END_FIXTURE_TEST(StreamTestFixture,
+                 testReadableStream_ReadableStreamGetEmbeddingFlags)
+
+BEGIN_FIXTURE_TEST(StreamTestFixture,
+                   testReadableStream_ReadableStreamGetExternalUnderlyingSource)
+{
+    ResetCallbacks();
+
+    RootedObject stream(cx, NewExternalSourceStream(cx));
+    CHECK(stream);
+    void* source;
+    CHECK(ReadableStreamGetExternalUnderlyingSource(cx, stream, &source));
+    CHECK(source == &stubExternalUnderlyingSource);
+    CHECK(ReadableStreamReleaseExternalUnderlyingSource(cx, stream));
+
+    RootedObject otherGlobal(cx, createGlobal());
+    CHECK(otherGlobal);
+    {
+        JSAutoRealm ar(cx, otherGlobal);
+        CHECK(JS_WrapObject(cx, &stream));
+        void* source;
+        CHECK(ReadableStreamGetExternalUnderlyingSource(cx, stream, &source));
+        CHECK(source == &stubExternalUnderlyingSource);
+        CHECK(ReadableStreamReleaseExternalUnderlyingSource(cx, stream));
+    }
+
+    return true;
+}
+END_FIXTURE_TEST(StreamTestFixture,
+                 testReadableStream_ReadableStreamGetExternalUnderlyingSource)
+
+BEGIN_FIXTURE_TEST(StreamTestFixture,
+                   testReadableStream_ReadableStreamUpdateDataAvailableFromSource)
+{
+    RootedObject stream(cx, NewExternalSourceStream(cx));
+    CHECK(stream);
+    CHECK(ReadableStreamUpdateDataAvailableFromSource(cx, stream, 0));
+
+    RootedObject otherGlobal(cx, createGlobal());
+    CHECK(otherGlobal);
+    {
+        JSAutoRealm ar(cx, otherGlobal);
+        CHECK(JS_WrapObject(cx, &stream));
+        CHECK(ReadableStreamUpdateDataAvailableFromSource(cx, stream, 1));
+    }
+
+    return true;
+}
+END_FIXTURE_TEST(StreamTestFixture,
+                 testReadableStream_ReadableStreamUpdateDataAvailableFromSource)
+
+BEGIN_FIXTURE_TEST(StreamTestFixture,
+                   testReadableStream_IsReadableStream)
+{
+    RootedObject stream(cx, NewDefaultStream(cx));
+    CHECK(stream);
+    CHECK(IsReadableStream(stream));
+
+    RootedObject otherGlobal(cx, createGlobal());
+    CHECK(otherGlobal);
+    {
+        JSAutoRealm ar(cx, otherGlobal);
+        CHECK(JS_WrapObject(cx, &stream));
+        CHECK(IsReadableStream(stream));
+    }
+
+    return true;
+}
+END_FIXTURE_TEST(StreamTestFixture,
+                 testReadableStream_IsReadableStream)
+
+BEGIN_FIXTURE_TEST(StreamTestFixture,
+                   testReadableStream_ReadableStreamGetMode)
 {
-    return readWithoutDataAvailable("r = stream.getReader({mode: 'byob'}); r.read(new Uint8Array(63))", "r.read(new Uint8Array(10))", 10);
+    RootedObject stream(cx, NewDefaultStream(cx));
+    CHECK(stream);
+    ReadableStreamMode mode;
+    CHECK(ReadableStreamGetMode(cx, stream, &mode));
+    CHECK(mode == ReadableStreamMode::Default);
+
+    RootedObject otherGlobal(cx, createGlobal());
+    CHECK(otherGlobal);
+    {
+        JSAutoRealm ar(cx, otherGlobal);
+        CHECK(JS_WrapObject(cx, &stream));
+        CHECK(ReadableStreamGetMode(cx, stream, &mode));
+        CHECK(mode == ReadableStreamMode::Default);
+    }
+
+    return true;
+}
+END_FIXTURE_TEST(StreamTestFixture,
+                 testReadableStream_ReadableStreamGetMode)
+
+BEGIN_FIXTURE_TEST(StreamTestFixture,
+                   testReadableStream_ReadableStreamIsReadable)
+{
+    RootedObject stream(cx, NewDefaultStream(cx));
+    CHECK(stream);
+    bool result;
+    CHECK(ReadableStreamIsReadable(cx, stream, &result));
+    CHECK(result);
+
+    RootedObject otherGlobal(cx, createGlobal());
+    CHECK(otherGlobal);
+    {
+        JSAutoRealm ar(cx, otherGlobal);
+        CHECK(JS_WrapObject(cx, &stream));
+        CHECK(ReadableStreamIsReadable(cx, stream, &result));
+        CHECK(result);
+    }
+
+    return true;
+}
+END_FIXTURE_TEST(StreamTestFixture,
+                 testReadableStream_ReadableStreamIsReadable)
+
+BEGIN_FIXTURE_TEST(StreamTestFixture,
+                   testReadableStream_ReadableStreamIsLocked)
+{
+    RootedObject stream(cx, NewDefaultStream(cx));
+    CHECK(stream);
+    bool result;
+    CHECK(ReadableStreamIsLocked(cx, stream, &result));
+    CHECK_EQUAL(result, false);
+
+    RootedObject otherGlobal(cx, createGlobal());
+    CHECK(otherGlobal);
+    {
+        JSAutoRealm ar(cx, otherGlobal);
+        CHECK(JS_WrapObject(cx, &stream));
+        CHECK(ReadableStreamIsLocked(cx, stream, &result));
+        CHECK_EQUAL(result, false);
+    }
+
+    return true;
 }
-END_FIXTURE_TEST(ReadFromExternalSourceFixture,
-                 testReadableStream_ExternalSourceReadBYOBWithoutDataAvailable)
+END_FIXTURE_TEST(StreamTestFixture,
+                 testReadableStream_ReadableStreamIsLocked)
+
+BEGIN_FIXTURE_TEST(StreamTestFixture,
+                   testReadableStream_ReadableStreamIsDisturbed)
+{
+    RootedObject stream(cx, NewDefaultStream(cx));
+    CHECK(stream);
+    bool result;
+    CHECK(ReadableStreamIsDisturbed(cx, stream, &result));
+    CHECK_EQUAL(result, false);
+
+    RootedObject otherGlobal(cx, createGlobal());
+    CHECK(otherGlobal);
+    {
+        JSAutoRealm ar(cx, otherGlobal);
+        CHECK(JS_WrapObject(cx, &stream));
+        CHECK(ReadableStreamIsDisturbed(cx, stream, &result));
+        CHECK_EQUAL(result, false);
+    }
+
+    return true;
+}
+END_FIXTURE_TEST(StreamTestFixture,
+                 testReadableStream_ReadableStreamIsDisturbed)
+
+BEGIN_FIXTURE_TEST(StreamTestFixture,
+                   testReadableStream_ReadableStreamCancel)
+{
+    RootedObject stream(cx, NewDefaultStream(cx));
+    CHECK(stream);
+
+
+    RootedValue reason(cx);
+    JSObject* callResult = ReadableStreamCancel(cx, stream, reason);
+    CHECK(callResult);
+
+    RootedObject otherGlobal(cx, createGlobal());
+    CHECK(otherGlobal);
+    {
+        JSAutoRealm ar(cx, otherGlobal);
+        CHECK(JS_WrapObject(cx, &stream));
+        RootedValue reason(cx);
+        JSObject* callResult = ReadableStreamCancel(cx, stream, reason);
+        CHECK(callResult);
+    }
+
+    return true;
+}
+END_FIXTURE_TEST(StreamTestFixture,
+                 testReadableStream_ReadableStreamCancel)
+
+BEGIN_FIXTURE_TEST(StreamTestFixture,
+                   testReadableStream_ReadableStreamGetReader)
+{
+    RootedObject stream(cx, NewDefaultStream(cx));
+    CHECK(stream);
+
+
+    RootedObject reader(cx);
+    reader = ReadableStreamGetReader(cx, stream, ReadableStreamReaderMode::Default);
+    CHECK(reader);
+    CHECK(IsReadableStreamDefaultReader(reader));
+    CHECK(ReadableStreamReaderReleaseLock(cx, reader));
+
+    RootedObject otherGlobal(cx, createGlobal());
+    CHECK(otherGlobal);
+    {
+        JSAutoRealm ar(cx, otherGlobal);
+        CHECK(JS_WrapObject(cx, &stream));
+        JSObject* callResult = ReadableStreamGetReader(cx, stream, ReadableStreamReaderMode::Default);
+        CHECK(callResult);
+    }
 
-BEGIN_FIXTURE_TEST(ReadFromExternalSourceFixture,
-                   testReadableStream_ExternalSourceReadBYOBWithDataAvailable)
+    return true;
+}
+END_FIXTURE_TEST(StreamTestFixture,
+                 testReadableStream_ReadableStreamGetReader)
+
+BEGIN_FIXTURE_TEST(StreamTestFixture,
+                   testReadableStream_ReadableStreamTee_CrossCompartment)
+{
+    RootedObject stream(cx, NewDefaultStream(cx));
+    CHECK(stream);
+
+
+    RootedObject branch1Stream(cx);
+    RootedObject branch2Stream(cx);
+    CHECK(ReadableStreamTee(cx, stream, &branch1Stream, &branch2Stream));
+    CHECK(IsReadableStream(branch1Stream));
+    CHECK(IsReadableStream(branch2Stream));
+    stream = branch1Stream;
+
+    RootedObject otherGlobal(cx, createGlobal());
+    CHECK(otherGlobal);
+    {
+        JSAutoRealm ar(cx, otherGlobal);
+        CHECK(JS_WrapObject(cx, &stream));
+        CHECK(ReadableStreamTee(cx, stream, &branch1Stream, &branch2Stream));
+        CHECK(IsReadableStream(branch1Stream));
+        CHECK(IsReadableStream(branch2Stream));
+    }
+
+    return true;
+}
+END_FIXTURE_TEST(StreamTestFixture,
+                 testReadableStream_ReadableStreamTee_CrossCompartment)
+
+BEGIN_FIXTURE_TEST(StreamTestFixture,
+                   testReadableStream_ReadableStreamGetDesiredSize)
+{
+    RootedObject stream(cx, NewDefaultStream(cx));
+    CHECK(stream);
+    bool hasValue;
+    double value;
+    CHECK(ReadableStreamGetDesiredSize(cx, stream, &hasValue, &value));
+    CHECK_EQUAL(hasValue, true);
+    CHECK_EQUAL(value, 1.0);
+
+    RootedObject otherGlobal(cx, createGlobal());
+    CHECK(otherGlobal);
+    {
+        JSAutoRealm ar(cx, otherGlobal);
+        CHECK(JS_WrapObject(cx, &stream));
+        hasValue = false;
+        value = 0;
+        CHECK(ReadableStreamGetDesiredSize(cx, stream, &hasValue, &value));
+        CHECK_EQUAL(hasValue, true);
+        CHECK_EQUAL(value, 1.0);
+    }
+
+    return true;
+}
+END_FIXTURE_TEST(StreamTestFixture,
+                 testReadableStream_ReadableStreamGetDesiredSize)
+
+BEGIN_FIXTURE_TEST(StreamTestFixture,
+                   testReadableStream_ReadableStreamClose)
+{
+    RootedObject stream(cx, NewDefaultStream(cx));
+    CHECK(stream);
+    CHECK(ReadableStreamClose(cx, stream));
+
+    stream = NewDefaultStream(cx);
+    CHECK(stream);
+    RootedObject otherGlobal(cx, createGlobal());
+    CHECK(otherGlobal);
+    {
+        JSAutoRealm ar(cx, otherGlobal);
+        CHECK(JS_WrapObject(cx, &stream));
+        CHECK(ReadableStreamClose(cx, stream));
+    }
+
+    return true;
+}
+END_FIXTURE_TEST(StreamTestFixture,
+                 testReadableStream_ReadableStreamClose)
+
+BEGIN_FIXTURE_TEST(StreamTestFixture,
+                   testReadableStream_ReadableStreamEnqueue_CrossCompartment)
+{
+    RootedObject stream(cx, NewDefaultStream(cx));
+    CHECK(stream);
+    RootedValue chunk(cx);
+    CHECK(ReadableStreamEnqueue(cx, stream, chunk));
+
+    RootedObject otherGlobal(cx, createGlobal());
+    CHECK(otherGlobal);
+    {
+        JSAutoRealm ar(cx, otherGlobal);
+        CHECK(JS_WrapObject(cx, &stream));
+        RootedValue chunk(cx);
+        CHECK(ReadableStreamEnqueue(cx, stream, chunk));
+    }
+
+    return true;
+}
+END_FIXTURE_TEST(StreamTestFixture,
+                 testReadableStream_ReadableStreamEnqueue_CrossCompartment)
+
+BEGIN_FIXTURE_TEST(StreamTestFixture,
+                   testReadableStream_ReadableStreamError)
+{
+    RootedObject stream(cx, NewDefaultStream(cx));
+    CHECK(stream);
+    RootedValue error(cx);
+    CHECK(ReadableStreamError(cx, stream, error));
+
+    stream = NewDefaultStream(cx);
+    RootedObject otherGlobal(cx, createGlobal());
+    CHECK(otherGlobal);
+    {
+        JSAutoRealm ar(cx, otherGlobal);
+        CHECK(JS_WrapObject(cx, &stream));
+        RootedValue error(cx);
+        CHECK(ReadableStreamError(cx, stream, error));
+    }
+
+    return true;
+}
+END_FIXTURE_TEST(StreamTestFixture,
+                 testReadableStream_ReadableStreamError)
+
+BEGIN_FIXTURE_TEST(StreamTestFixture,
+                   testReadableStream_IsReadableStreamReader)
 {
-    return readWithDataAvailable("r = stream.getReader({mode: 'byob'}); r.read(new Uint8Array(10))", 10);
+    RootedObject stream(cx, NewDefaultStream(cx));
+    CHECK(stream);
+    RootedObject reader(cx, ReadableStreamGetReader(cx, stream, ReadableStreamReaderMode::Default));
+    CHECK(reader);
+    CHECK(IsReadableStreamReader(reader));
+
+    RootedObject otherGlobal(cx, createGlobal());
+    CHECK(otherGlobal);
+    {
+        JSAutoRealm ar(cx, otherGlobal);
+        CHECK(JS_WrapObject(cx, &reader));
+        CHECK(IsReadableStreamReader(reader));
+    }
+
+    return true;
+}
+END_FIXTURE_TEST(StreamTestFixture,
+                 testReadableStream_IsReadableStreamReader)
+
+BEGIN_FIXTURE_TEST(StreamTestFixture,
+                   testReadableStream_IsReadableStreamDefaultReader)
+{
+    RootedObject stream(cx, NewDefaultStream(cx));
+    CHECK(stream);
+    RootedObject reader(cx, ReadableStreamGetReader(cx, stream, ReadableStreamReaderMode::Default));
+    CHECK(IsReadableStreamDefaultReader(reader));
+
+    RootedObject otherGlobal(cx, createGlobal());
+    CHECK(otherGlobal);
+    {
+        JSAutoRealm ar(cx, otherGlobal);
+        CHECK(JS_WrapObject(cx, &reader));
+        CHECK(IsReadableStreamDefaultReader(reader));
+    }
+
+    return true;
+}
+END_FIXTURE_TEST(StreamTestFixture,
+                 testReadableStream_IsReadableStreamDefaultReader)
+
+BEGIN_FIXTURE_TEST(StreamTestFixture,
+                   testReadableStream_ReadableStreamReaderIsClosed)
+{
+    RootedObject stream(cx, NewDefaultStream(cx));
+    CHECK(stream);
+
+    RootedObject reader(cx, ReadableStreamGetReader(cx, stream, ReadableStreamReaderMode::Default));
+    bool result;
+    CHECK(ReadableStreamReaderIsClosed(cx, reader, &result));
+    CHECK_EQUAL(result, false);
+
+    RootedObject otherGlobal(cx, createGlobal());
+    CHECK(otherGlobal);
+    {
+        JSAutoRealm ar(cx, otherGlobal);
+        CHECK(JS_WrapObject(cx, &reader));
+        bool result;
+        CHECK(ReadableStreamReaderIsClosed(cx, reader, &result));
+    }
+
+    return true;
 }
-END_FIXTURE_TEST(ReadFromExternalSourceFixture,
-                 testReadableStream_ExternalSourceReadBYOBWithDataAvailable)
+END_FIXTURE_TEST(StreamTestFixture,
+                 testReadableStream_ReadableStreamReaderIsClosed)
+
+BEGIN_FIXTURE_TEST(StreamTestFixture,
+                   testReadableStream_ReadableStreamReaderCancel)
+{
+    RootedObject stream(cx, NewDefaultStream(cx));
+    CHECK(stream);
+    RootedObject reader(cx, ReadableStreamGetReader(cx, stream, ReadableStreamReaderMode::Default));
+    RootedValue reason(cx);
+    CHECK(ReadableStreamReaderCancel(cx, reader, reason));
+
+    RootedObject otherGlobal(cx, createGlobal());
+    CHECK(otherGlobal);
+    {
+        JSAutoRealm ar(cx, otherGlobal);
+        CHECK(JS_WrapObject(cx, &reader));
+        RootedValue reason(cx);
+        CHECK(ReadableStreamReaderCancel(cx, reader, reason));
+    }
+
+    return true;
+}
+END_FIXTURE_TEST(StreamTestFixture,
+                 testReadableStream_ReadableStreamReaderCancel)
+
+BEGIN_FIXTURE_TEST(StreamTestFixture,
+                   testReadableStream_ReadableStreamReaderReleaseLock)
+{
+    RootedObject stream(cx, NewDefaultStream(cx));
+    CHECK(stream);
+    RootedObject reader(cx, ReadableStreamGetReader(cx, stream, ReadableStreamReaderMode::Default));
+    CHECK(reader);
+    CHECK(ReadableStreamReaderReleaseLock(cx, reader));
+
+    // Repeat the test cross-compartment. This creates a new reader, since
+    // releasing the lock above deactivated the first reader.
+    reader = ReadableStreamGetReader(cx, stream, ReadableStreamReaderMode::Default);
+    CHECK(reader);
+    RootedObject otherGlobal(cx, createGlobal());
+    CHECK(otherGlobal);
+    {
+        JSAutoRealm ar(cx, otherGlobal);
+        CHECK(JS_WrapObject(cx, &reader));
+        CHECK(ReadableStreamReaderReleaseLock(cx, reader));
+    }
+
+    return true;
+}
+END_FIXTURE_TEST(StreamTestFixture,
+                 testReadableStream_ReadableStreamReaderReleaseLock)
+
+BEGIN_FIXTURE_TEST(StreamTestFixture,
+                   testReadableStream_ReadableStreamDefaultReaderRead_CrossCompartment)
+{
+    RootedObject stream(cx, NewDefaultStream(cx));
+    CHECK(stream);
+    RootedObject reader(cx, ReadableStreamGetReader(cx, stream, ReadableStreamReaderMode::Default));
+    JSObject* callResult = ReadableStreamDefaultReaderRead(cx, reader);
+    CHECK(callResult);
+
+    RootedObject otherGlobal(cx, createGlobal());
+    CHECK(otherGlobal);
+    {
+        JSAutoRealm ar(cx, otherGlobal);
+        CHECK(JS_WrapObject(cx, &reader));
+        JSObject* callResult = ReadableStreamDefaultReaderRead(cx, reader);
+        CHECK(callResult);
+    }
+
+    return true;
+}
+END_FIXTURE_TEST(StreamTestFixture,
+                 testReadableStream_ReadableStreamDefaultReaderRead_CrossCompartment)
+
--- a/js/src/jsapi-tests/tests.h
+++ b/js/src/jsapi-tests/tests.h
@@ -3,16 +3,17 @@
  * This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #ifndef jsapi_tests_tests_h
 #define jsapi_tests_tests_h
 
 #include "mozilla/ArrayUtils.h"
+#include "mozilla/Sprintf.h"
 #include "mozilla/TypeTraits.h"
 
 #include <errno.h>
 #include <stdio.h>
 #include <stdlib.h>
 #include <string.h>
 
 #include "gc/GC.h"
@@ -133,16 +134,22 @@ class JSAPITest
     }
 
     JSAPITestString toSource(unsigned long long v) {
         char buf[40];
         sprintf(buf, "%llu", v);
         return JSAPITestString(buf);
     }
 
+    JSAPITestString toSource(double d) {
+        char buf[40];
+        SprintfLiteral(buf, "%17lg", d);
+        return JSAPITestString(buf);
+    }
+
     JSAPITestString toSource(unsigned int v) {
         return toSource((unsigned long)v);
     }
 
     JSAPITestString toSource(int v) {
         return toSource((long)v);
     }
 
--- a/js/src/jsapi.cpp
+++ b/js/src/jsapi.cpp
@@ -4485,52 +4485,28 @@ CallOriginalPromiseThenImpl(JSContext* c
                             CreateDependentPromise createDependent)
 {
     AssertHeapIsIdle();
     CHECK_THREAD(cx);
     cx->check(promiseObj, onResolvedObj_, onRejectedObj_);
 
     MOZ_ASSERT_IF(onResolvedObj_, IsCallable(onResolvedObj_));
     MOZ_ASSERT_IF(onRejectedObj_, IsCallable(onRejectedObj_));
-
-    {
-        mozilla::Maybe<AutoRealm> ar;
-        Rooted<PromiseObject*> promise(cx);
-        RootedObject onResolvedObj(cx, onResolvedObj_);
-        RootedObject onRejectedObj(cx, onRejectedObj_);
-        if (IsWrapper(promiseObj)) {
-            JSObject* unwrappedPromiseObj = CheckedUnwrap(promiseObj);
-            if (!unwrappedPromiseObj) {
-                ReportAccessDenied(cx);
-                return false;
-            }
-            promise = &unwrappedPromiseObj->as<PromiseObject>();
-            ar.emplace(cx, promise);
-            if (!cx->compartment()->wrap(cx, &onResolvedObj) ||
-                !cx->compartment()->wrap(cx, &onRejectedObj))
-            {
-                return false;
-            }
-        } else {
-            promise = promiseObj.as<PromiseObject>();
-        }
-
-        RootedValue onFulfilled(cx, ObjectOrNullValue(onResolvedObj));
-        RootedValue onRejected(cx, ObjectOrNullValue(onRejectedObj));
-        if (!OriginalPromiseThen(cx, promise, onFulfilled, onRejected, resultObj, createDependent)) {
-            return false;
-        }
-    }
-
-    if (resultObj) {
-        if (!cx->compartment()->wrap(cx, resultObj)) {
-            return false;
-        }
-    }
-    return true;
+    RootedObject onResolvedObj(cx, onResolvedObj_);
+    RootedObject onRejectedObj(cx, onRejectedObj_);
+
+    if (IsWrapper(promiseObj) && !CheckedUnwrap(promiseObj)) {
+        ReportAccessDenied(cx);
+        return false;
+    }
+    MOZ_ASSERT(CheckedUnwrap(promiseObj)->is<PromiseObject>());
+
+    RootedValue onFulfilled(cx, ObjectOrNullValue(onResolvedObj));
+    RootedValue onRejected(cx, ObjectOrNullValue(onRejectedObj));
+    return OriginalPromiseThen(cx, promiseObj, onFulfilled, onRejected, resultObj, createDependent);
 }
 
 JS_PUBLIC_API(JSObject*)
 JS::CallOriginalPromiseThen(JSContext* cx, JS::HandleObject promiseObj,
                             JS::HandleObject onResolvedObj, JS::HandleObject onRejectedObj)
 {
     RootedObject resultPromise(cx);
     if (!CallOriginalPromiseThenImpl(cx, promiseObj, onResolvedObj, onRejectedObj, &resultPromise,
@@ -4617,62 +4593,16 @@ JS_PUBLIC_API(JSObject*)
 JS::GetWaitForAllPromise(JSContext* cx, const JS::AutoObjectVector& promises)
 {
     AssertHeapIsIdle();
     CHECK_THREAD(cx);
 
     return js::GetWaitForAllPromise(cx, promises);
 }
 
-JS_PUBLIC_API(JSObject*)
-JS::NewReadableDefaultStreamObject(JSContext* cx,
-                                   JS::HandleObject underlyingSource /* = nullptr */,
-                                   JS::HandleFunction size /* = nullptr */,
-                                   double highWaterMark /* = 1 */,
-                                   JS::HandleObject proto /* = nullptr */)
-{
-    MOZ_ASSERT(!cx->zone()->isAtomsZone());
-    AssertHeapIsIdle();
-    CHECK_THREAD(cx);
-
-    RootedObject source(cx, underlyingSource);
-    if (!source) {
-        source = NewBuiltinClassInstance<PlainObject>(cx);
-        if (!source) {
-            return nullptr;
-        }
-    }
-    RootedValue sourceVal(cx, ObjectValue(*source));
-    RootedValue sizeVal(cx, size ? ObjectValue(*size) : UndefinedValue());
-    RootedValue highWaterMarkVal(cx, NumberValue(highWaterMark));
-    return ReadableStream::createDefaultStream(cx, sourceVal, sizeVal, highWaterMarkVal, proto);
-}
-
-JS_PUBLIC_API(JSObject*)
-JS::NewReadableByteStreamObject(JSContext* cx,
-                                JS::HandleObject underlyingSource /* = nullptr */,
-                                double highWaterMark /* = 1 */,
-                                JS::HandleObject proto /* = nullptr */)
-{
-    MOZ_ASSERT(!cx->zone()->isAtomsZone());
-    AssertHeapIsIdle();
-    CHECK_THREAD(cx);
-
-    RootedObject source(cx, underlyingSource);
-    if (!source) {
-        source = NewBuiltinClassInstance<PlainObject>(cx);
-        if (!source) {
-            return nullptr;
-        }
-    }
-    RootedValue sourceVal(cx, ObjectValue(*source));
-    RootedValue highWaterMarkVal(cx, NumberValue(highWaterMark));
-    return ReadableStream::createByteStream(cx, sourceVal, highWaterMarkVal, proto);
-}
-
 extern JS_PUBLIC_API(void)
 JS::SetReadableStreamCallbacks(JSContext* cx,
                                JS::RequestReadableStreamDataCallback dataRequestCallback,
                                JS::WriteIntoReadRequestBufferCallback writeIntoReadRequestCallback,
                                JS::CancelReadableStreamCallback cancelCallback,
                                JS::ReadableStreamClosedCallback closedCallback,
                                JS::ReadableStreamErroredCallback erroredCallback,
                                JS::ReadableStreamFinalizeCallback finalizeCallback)
@@ -4703,295 +4633,356 @@ JS::SetReadableStreamCallbacks(JSContext
 
 JS_PUBLIC_API(bool)
 JS::HasReadableStreamCallbacks(JSContext* cx)
 {
     return cx->runtime()->readableStreamDataRequestCallback;
 }
 
 JS_PUBLIC_API(JSObject*)
+JS::NewReadableDefaultStreamObject(JSContext* cx,
+                                   JS::HandleObject underlyingSource /* = nullptr */,
+                                   JS::HandleFunction size /* = nullptr */,
+                                   double highWaterMark /* = 1 */,
+                                   JS::HandleObject proto /* = nullptr */)
+{
+    MOZ_ASSERT(!cx->zone()->isAtomsZone());
+    AssertHeapIsIdle();
+    CHECK_THREAD(cx);
+
+    RootedObject source(cx, underlyingSource);
+    if (!source) {
+        source = NewBuiltinClassInstance<PlainObject>(cx);
+        if (!source)
+            return nullptr;
+    }
+    RootedValue sourceVal(cx, ObjectValue(*source));
+    RootedValue sizeVal(cx, size ? ObjectValue(*size) : UndefinedValue());
+    RootedValue highWaterMarkVal(cx, NumberValue(highWaterMark));
+    return ReadableStream::createDefaultStream(cx, sourceVal, sizeVal, highWaterMarkVal, proto);
+}
+
+JS_PUBLIC_API(JSObject*)
 JS::NewReadableExternalSourceStreamObject(JSContext* cx, void* underlyingSource,
                                           uint8_t flags /* = 0 */,
                                           HandleObject proto /* = nullptr */)
 {
     MOZ_ASSERT(!cx->zone()->isAtomsZone());
     AssertHeapIsIdle();
     CHECK_THREAD(cx);
-
+    MOZ_ASSERT((uintptr_t(underlyingSource) & 1) == 0,
+               "external underlying source pointers must be aligned");
 #ifdef DEBUG
     JSRuntime* rt = cx->runtime();
     MOZ_ASSERT(rt->readableStreamDataRequestCallback);
     MOZ_ASSERT(rt->readableStreamWriteIntoReadRequestCallback);
     MOZ_ASSERT(rt->readableStreamCancelCallback);
     MOZ_ASSERT(rt->readableStreamClosedCallback);
     MOZ_ASSERT(rt->readableStreamErroredCallback);
     MOZ_ASSERT(rt->readableStreamFinalizeCallback);
 #endif // DEBUG
 
     return ReadableStream::createExternalSourceStream(cx, underlyingSource, flags, proto);
 }
 
-JS_PUBLIC_API(uint8_t)
-JS::ReadableStreamGetEmbeddingFlags(const JSObject* stream)
-{
-    return stream->as<ReadableStream>().embeddingFlags();
-}
-
 JS_PUBLIC_API(bool)
 JS::IsReadableStream(const JSObject* obj)
 {
-    return obj->is<ReadableStream>();
+    if (IsWrapper(const_cast<JSObject*>(obj)))
+        obj = CheckedUnwrap(const_cast<JSObject*>(obj));
+    return obj && obj->is<ReadableStream>();
 }
 
 JS_PUBLIC_API(bool)
 JS::IsReadableStreamReader(const JSObject* obj)
 {
-    return obj->is<ReadableStreamDefaultReader>() || obj->is<ReadableStreamBYOBReader>();
+    if (IsWrapper(const_cast<JSObject*>(obj)))
+        obj = CheckedUnwrap(const_cast<JSObject*>(obj));
+    return obj && obj->is<ReadableStreamDefaultReader>();
 }
 
 JS_PUBLIC_API(bool)
 JS::IsReadableStreamDefaultReader(const JSObject* obj)
 {
-    return obj->is<ReadableStreamDefaultReader>();
-}
-
-JS_PUBLIC_API(bool)
-JS::IsReadableStreamBYOBReader(const JSObject* obj)
-{
-    return obj->is<ReadableStreamBYOBReader>();
-}
-
-JS_PUBLIC_API(bool)
-JS::ReadableStreamIsReadable(const JSObject* stream)
-{
-    return stream->as<ReadableStream>().readable();
-}
-
-JS_PUBLIC_API(bool)
-JS::ReadableStreamIsLocked(const JSObject* stream)
-{
-    return stream->as<ReadableStream>().locked();
-}
-
-JS_PUBLIC_API(bool)
-JS::ReadableStreamIsDisturbed(const JSObject* stream)
-{
-    return stream->as<ReadableStream>().disturbed();
+    if (IsWrapper(const_cast<JSObject*>(obj)))
+        obj = CheckedUnwrap(const_cast<JSObject*>(obj));
+    return obj && obj->is<ReadableStreamDefaultReader>();
+}
+
+template<class T>
+static MOZ_MUST_USE T*
+ToUnwrapped(JSContext* cx, JSObject* obj)
+{
+    cx->check(obj);
+    if (IsWrapper(obj)) {
+        obj = CheckedUnwrap(obj);
+        if (!obj) {
+            ReportAccessDenied(cx);
+            return nullptr;
+        }
+    }
+    return &obj->as<T>();
+}
+
+JS_PUBLIC_API(bool)
+JS::ReadableStreamIsReadable(JSContext* cx, const JSObject* streamObj, bool* result)
+{
+    ReadableStream* stream = ToUnwrapped<ReadableStream>(cx, const_cast<JSObject*>(streamObj));
+    if (!stream)
+        return false;
+
+    *result = stream->readable();
+    return true;
+}
+
+JS_PUBLIC_API(bool)
+JS::ReadableStreamIsLocked(JSContext* cx, const JSObject* streamObj, bool* result)
+{
+    ReadableStream* stream = ToUnwrapped<ReadableStream>(cx, const_cast<JSObject*>(streamObj));
+    if (!stream)
+        return false;
+
+    *result = stream->locked();
+    return true;
+}
+
+JS_PUBLIC_API(bool)
+JS::ReadableStreamIsDisturbed(JSContext* cx, const JSObject* streamObj, bool* result)
+{
+    ReadableStream* stream = ToUnwrapped<ReadableStream>(cx, const_cast<JSObject*>(streamObj));
+    if (!stream)
+        return false;
+
+    *result = stream->disturbed();
+    return true;
+}
+
+JS_PUBLIC_API(bool)
+JS::ReadableStreamGetEmbeddingFlags(JSContext* cx, const JSObject* streamObj, uint8_t* flags)
+{
+    ReadableStream* stream = ToUnwrapped<ReadableStream>(cx, const_cast<JSObject*>(streamObj));
+    if (!stream)
+        return false;
+
+    *flags = stream->embeddingFlags();
+    return true;
 }
 
 JS_PUBLIC_API(JSObject*)
 JS::ReadableStreamCancel(JSContext* cx, HandleObject streamObj, HandleValue reason)
 {
     AssertHeapIsIdle();
     CHECK_THREAD(cx);
-    cx->check(streamObj);
     cx->check(reason);
 
-    Rooted<ReadableStream*> stream(cx, &streamObj->as<ReadableStream>());
+    Rooted<ReadableStream*> stream(cx, ToUnwrapped<ReadableStream>(cx, streamObj));
+    if (!stream)
+        return nullptr;
+
     return ReadableStream::cancel(cx, stream, reason);
 }
 
-JS_PUBLIC_API(JS::ReadableStreamMode)
-JS::ReadableStreamGetMode(const JSObject* stream)
-{
-    return stream->as<ReadableStream>().mode();
+JS_PUBLIC_API(bool)
+JS::ReadableStreamGetMode(JSContext* cx, const JSObject* streamObj, JS::ReadableStreamMode* mode)
+{
+    ReadableStream* stream = ToUnwrapped<ReadableStream>(cx, const_cast<JSObject*>(streamObj));
+    if (!stream)
+        return false;
+
+    *mode = stream->mode();
+    return true;
 }
 
 JS_PUBLIC_API(JSObject*)
 JS::ReadableStreamGetReader(JSContext* cx, HandleObject streamObj, ReadableStreamReaderMode mode)
 {
     AssertHeapIsIdle();
     CHECK_THREAD(cx);
-    cx->check(streamObj);
-
-    Rooted<ReadableStream*> stream(cx, &streamObj->as<ReadableStream>());
-    return ReadableStream::getReader(cx, stream, mode);
+
+    Rooted<ReadableStream*> stream(cx, ToUnwrapped<ReadableStream>(cx, streamObj));
+    if (!stream)
+        return nullptr;
+
+    JSObject* result = ReadableStream::getReader(cx, stream, mode);
+    MOZ_ASSERT_IF(result, IsObjectInContextCompartment(result, cx));
+    return result;
 }
 
 JS_PUBLIC_API(bool)
 JS::ReadableStreamGetExternalUnderlyingSource(JSContext* cx, HandleObject streamObj, void** source)
 {
     AssertHeapIsIdle();
     CHECK_THREAD(cx);
-    cx->check(streamObj);
-
-    Rooted<ReadableStream*> stream(cx, &streamObj->as<ReadableStream>());
+
+    Rooted<ReadableStream*> stream(cx, ToUnwrapped<ReadableStream>(cx, streamObj));
+    if (!stream)
+        return false;
+
     return ReadableStream::getExternalSource(cx, stream, source);
 }
 
-JS_PUBLIC_API(void)
-JS::ReadableStreamReleaseExternalUnderlyingSource(JSObject* stream)
-{
-    stream->as<ReadableStream>().releaseExternalSource();
+JS_PUBLIC_API(bool)
+JS::ReadableStreamReleaseExternalUnderlyingSource(JSContext* cx, JSObject* streamObj)
+{
+    ReadableStream* stream = ToUnwrapped<ReadableStream>(cx, const_cast<JSObject*>(streamObj));
+    if (!stream)
+        return false;
+
+    stream->releaseExternalSource();
+    return true;
 }
 
 JS_PUBLIC_API(bool)
 JS::ReadableStreamUpdateDataAvailableFromSource(JSContext* cx, JS::HandleObject streamObj,
                                                 uint32_t availableData)
 {
     AssertHeapIsIdle();
     CHECK_THREAD(cx);
-    cx->check(streamObj);
-
-    Rooted<ReadableStream*> stream(cx, &streamObj->as<ReadableStream>());
+
+    Rooted<ReadableStream*> stream(cx, ToUnwrapped<ReadableStream>(cx, streamObj));
+    if (!stream)
+        return false;
+
     return ReadableStream::updateDataAvailableFromSource(cx, stream, availableData);
 }
 
 JS_PUBLIC_API(bool)
 JS::ReadableStreamTee(JSContext* cx, HandleObject streamObj,
                       MutableHandleObject branch1Obj, MutableHandleObject branch2Obj)
 {
     AssertHeapIsIdle();
     CHECK_THREAD(cx);
-    cx->check(streamObj);
-
-    Rooted<ReadableStream*> stream(cx, &streamObj->as<ReadableStream>());
+
+    Rooted<ReadableStream*> stream(cx, ToUnwrapped<ReadableStream>(cx, streamObj));
+    if (!stream)
+        return false;
+
     Rooted<ReadableStream*> branch1Stream(cx);
     Rooted<ReadableStream*> branch2Stream(cx);
 
     if (!ReadableStream::tee(cx, stream, false, &branch1Stream, &branch2Stream)) {
         return false;
     }
 
     branch1Obj.set(branch1Stream);
     branch2Obj.set(branch2Stream);
 
     return true;
 }
 
-JS_PUBLIC_API(void)
-JS::ReadableStreamGetDesiredSize(JSObject* streamObj, bool* hasValue, double* value)
-{
-    streamObj->as<ReadableStream>().desiredSize(hasValue, value);
+JS_PUBLIC_API(bool)
+JS::ReadableStreamGetDesiredSize(JSContext* cx, JSObject* streamObj, bool* hasValue, double* value)
+{
+    ReadableStream* stream = ToUnwrapped<ReadableStream>(cx, streamObj);
+    if (!stream)
+        return false;
+
+    stream->desiredSize(hasValue, value);
+    return true;
 }
 
 JS_PUBLIC_API(bool)
 JS::ReadableStreamClose(JSContext* cx, HandleObject streamObj)
 {
     AssertHeapIsIdle();
     CHECK_THREAD(cx);
-    cx->check(streamObj);
-
-    Rooted<ReadableStream*> stream(cx, &streamObj->as<ReadableStream>());
+
+    Rooted<ReadableStream*> stream(cx, ToUnwrapped<ReadableStream>(cx, streamObj));
+    if (!stream)
+        return false;
+
     return ReadableStream::close(cx, stream);
 }
 
 JS_PUBLIC_API(bool)
 JS::ReadableStreamEnqueue(JSContext* cx, HandleObject streamObj, HandleValue chunk)
 {
     AssertHeapIsIdle();
     CHECK_THREAD(cx);
-    cx->check(streamObj);
     cx->check(chunk);
 
-    Rooted<ReadableStream*> stream(cx, &streamObj->as<ReadableStream>());
+    Rooted<ReadableStream*> stream(cx, ToUnwrapped<ReadableStream>(cx, streamObj));
+    if (!stream)
+        return false;
+
     if (stream->mode() != JS::ReadableStreamMode::Default) {
         JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
                                   JSMSG_READABLESTREAM_NOT_DEFAULT_CONTROLLER,
                                   "JS::ReadableStreamEnqueue");
         return false;
     }
     return ReadableStream::enqueue(cx, stream, chunk);
 }
 
 JS_PUBLIC_API(bool)
-JS::ReadableByteStreamEnqueueBuffer(JSContext* cx, HandleObject streamObj, HandleObject chunkObj)
-{
-    AssertHeapIsIdle();
-    CHECK_THREAD(cx);
-    cx->check(streamObj);
-    cx->check(chunkObj);
-
-    Rooted<ReadableStream*> stream(cx, &streamObj->as<ReadableStream>());
-    if (stream->mode() != JS::ReadableStreamMode::Byte) {
-        JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
-                                  JSMSG_READABLESTREAM_NOT_BYTE_STREAM_CONTROLLER,
-                                  "JS::ReadableByteStreamEnqueueBuffer");
-        return false;
-    }
-
-    Rooted<ArrayBufferObject*> buffer(cx);
-    if (chunkObj->is<ArrayBufferViewObject>()) {
-        bool dummy;
-        buffer = &JS_GetArrayBufferViewBuffer(cx, chunkObj, &dummy)->as<ArrayBufferObject>();
-    } else if (chunkObj->is<ArrayBufferObject>()) {
-        buffer = &chunkObj->as<ArrayBufferObject>();
-    } else {
-        JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
-                                  JSMSG_READABLEBYTESTREAMCONTROLLER_BAD_CHUNK,
-                                  "JS::ReadableByteStreamEnqueueBuffer");
+JS::ReadableStreamError(JSContext* cx, HandleObject streamObj, HandleValue error)
+{
+    AssertHeapIsIdle();
+    CHECK_THREAD(cx);
+    cx->check(error);
+
+    Rooted<ReadableStream*> stream(cx, ToUnwrapped<ReadableStream>(cx, streamObj));
+    if (!stream)
         return false;
-    }
-
-    return ReadableStream::enqueueBuffer(cx, stream, buffer);
-}
-
-JS_PUBLIC_API(bool)
-JS::ReadableStreamError(JSContext* cx, HandleObject streamObj, HandleValue error)
-{
-    AssertHeapIsIdle();
-    CHECK_THREAD(cx);
-    cx->check(streamObj);
-    cx->check(error);
-
-    Rooted<ReadableStream*> stream(cx, &streamObj->as<ReadableStream>());
+
     return js::ReadableStream::error(cx, stream, error);
 }
 
 JS_PUBLIC_API(bool)
-JS::ReadableStreamReaderIsClosed(const JSObject* reader)
-{
-    return js::ReadableStreamReaderIsClosed(reader);
-}
-
-JS_PUBLIC_API(bool)
-JS::ReadableStreamReaderCancel(JSContext* cx, HandleObject reader, HandleValue reason)
-{
-    AssertHeapIsIdle();
-    CHECK_THREAD(cx);
-    cx->check(reader);
+JS::ReadableStreamReaderIsClosed(JSContext* cx, const JSObject* reader, bool* result)
+{
+    reader = ToUnwrapped<NativeObject>(cx, const_cast<JSObject*>(reader));
+    if (!reader)
+        return false;
+
+    *result = js::ReadableStreamReaderIsClosed(reader);
+    return true;
+}
+
+JS_PUBLIC_API(bool)
+JS::ReadableStreamReaderCancel(JSContext* cx, HandleObject readerObj, HandleValue reason)
+{
+    AssertHeapIsIdle();
+    CHECK_THREAD(cx);
     cx->check(reason);
 
+    RootedNativeObject reader(cx, ToUnwrapped<NativeObject>(cx, readerObj));
+    if (!reader)
+        return false;
+
     return js::ReadableStreamReaderCancel(cx, reader, reason);
 }
 
 JS_PUBLIC_API(bool)
-JS::ReadableStreamReaderReleaseLock(JSContext* cx, HandleObject reader)
-{
-    AssertHeapIsIdle();
-    CHECK_THREAD(cx);
-    cx->check(reader);
+JS::ReadableStreamReaderReleaseLock(JSContext* cx, HandleObject readerObj)
+{
+    AssertHeapIsIdle();
+    CHECK_THREAD(cx);
+
+    RootedNativeObject reader(cx, ToUnwrapped<NativeObject>(cx, readerObj));
+    if (!reader)
+        return false;
 
     return js::ReadableStreamReaderReleaseLock(cx, reader);
 }
 
 JS_PUBLIC_API(JSObject*)
 JS::ReadableStreamDefaultReaderRead(JSContext* cx, HandleObject readerObj)
 {
     AssertHeapIsIdle();
     CHECK_THREAD(cx);
-    cx->check(readerObj);
-
-    Rooted<ReadableStreamDefaultReader*> reader(cx, &readerObj->as<ReadableStreamDefaultReader>());
+
+    Rooted<ReadableStreamDefaultReader*> reader(cx);
+    reader = ToUnwrapped<ReadableStreamDefaultReader>(cx, readerObj);
+    if (!reader)
+        return nullptr;
+
     return js::ReadableStreamDefaultReader::read(cx, reader);
 }
 
-JS_PUBLIC_API(JSObject*)
-JS::ReadableStreamBYOBReaderRead(JSContext* cx, HandleObject readerObj, HandleObject viewObj)
-{
-    AssertHeapIsIdle();
-    CHECK_THREAD(cx);
-    cx->check(readerObj);
-    cx->check(viewObj);
-
-    Rooted<ReadableStreamBYOBReader*> reader(cx, &readerObj->as<ReadableStreamBYOBReader>());
-    Rooted<ArrayBufferViewObject*> view(cx, &viewObj->as<ArrayBufferViewObject>());
-    return js::ReadableStreamBYOBReader::read(cx, reader, view);
-}
-
 JS_PUBLIC_API(void)
 JS::InitDispatchToEventLoop(JSContext* cx, JS::DispatchToEventLoopCallback callback, void* closure)
 {
     cx->runtime()->offThreadPromiseState.ref().init(callback, closure);
 }
 
 JS_PUBLIC_API(void)
 JS::ShutdownAsyncTasks(JSContext* cx)
new file mode 100644
--- /dev/null
+++ b/js/src/tests/whatwg/shell.js
@@ -0,0 +1,257 @@
+/* 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(global) {
+    /** Yield every permutation of the elements in some array. */
+    global.Permutations = function* Permutations(items) {
+        if (items.length == 0) {
+            yield [];
+        } else {
+            items = items.slice(0);
+            for (let i = 0; i < items.length; i++) {
+                let swap = items[0];
+                items[0] = items[i];
+                items[i] = swap;
+                for (let e of Permutations(items.slice(1, items.length)))
+                    yield [items[0]].concat(e);
+            }
+        }
+    };
+
+    /** Make an iterator with a return method. */
+    global.makeIterator = function makeIterator(overrides) {
+        var throwMethod;
+        if (overrides && overrides.throw)
+            throwMethod = overrides.throw;
+        var iterator = {
+            throw: throwMethod,
+            next: function(x) {
+                if (overrides && overrides.next)
+                    return overrides.next(x);
+                return { done: false };
+            },
+            return: function(x) {
+                if (overrides && overrides.ret)
+                    return overrides.ret(x);
+                return { done: true };
+            }
+        };
+
+        return function() { return iterator; };
+    };
+})(this);
+
+if (typeof assertThrowsInstanceOf === 'undefined') {
+    var assertThrowsInstanceOf = function assertThrowsInstanceOf(f, ctor, msg) {
+        var fullmsg;
+        try {
+            f();
+        } catch (exc) {
+            if (exc instanceof ctor)
+                return;
+            fullmsg = "Assertion failed: expected exception " + ctor.name + ", got " + exc;
+        }
+        if (fullmsg === undefined)
+            fullmsg = "Assertion failed: expected exception " + ctor.name + ", no exception thrown";
+        if (msg !== undefined)
+            fullmsg += " - " + msg;
+        throw new Error(fullmsg);
+    };
+}
+
+if (typeof assertThrowsValue === 'undefined') {
+    var assertThrowsValue = function assertThrowsValue(f, val, msg) {
+        var fullmsg;
+        try {
+            f();
+        } catch (exc) {
+            if ((exc === val) === (val === val) && (val !== 0 || 1 / exc === 1 / val))
+                return;
+            fullmsg = "Assertion failed: expected exception " + val + ", got " + exc;
+        }
+        if (fullmsg === undefined)
+            fullmsg = "Assertion failed: expected exception " + val + ", no exception thrown";
+        if (msg !== undefined)
+            fullmsg += " - " + msg;
+        throw new Error(fullmsg);
+    };
+}
+
+if (typeof assertDeepEq === 'undefined') {
+    var assertDeepEq = (function(){
+        var call = Function.prototype.call,
+            Array_isArray = Array.isArray,
+            Map_ = Map,
+            Error_ = Error,
+            Symbol_ = Symbol,
+            Map_has = call.bind(Map.prototype.has),
+            Map_get = call.bind(Map.prototype.get),
+            Map_set = call.bind(Map.prototype.set),
+            Object_toString = call.bind(Object.prototype.toString),
+            Function_toString = call.bind(Function.prototype.toString),
+            Object_getPrototypeOf = Object.getPrototypeOf,
+            Object_hasOwnProperty = call.bind(Object.prototype.hasOwnProperty),
+            Object_getOwnPropertyDescriptor = Object.getOwnPropertyDescriptor,
+            Object_isExtensible = Object.isExtensible,
+            Object_getOwnPropertyNames = Object.getOwnPropertyNames,
+            uneval_ = uneval;
+
+        // Return true iff ES6 Type(v) isn't Object.
+        // Note that `typeof document.all === "undefined"`.
+        function isPrimitive(v) {
+            return (v === null ||
+                    v === undefined ||
+                    typeof v === "boolean" ||
+                    typeof v === "number" ||
+                    typeof v === "string" ||
+                    typeof v === "symbol");
+        }
+
+        function assertSameValue(a, b, msg) {
+            try {
+                assertEq(a, b);
+            } catch (exc) {
+                throw Error_(exc.message + (msg ? " " + msg : ""));
+            }
+        }
+
+        function assertSameClass(a, b, msg) {
+            var ac = Object_toString(a), bc = Object_toString(b);
+            assertSameValue(ac, bc, msg);
+            switch (ac) {
+            case "[object Function]":
+                if (typeof isProxy !== "undefined" && !isProxy(a) && !isProxy(b))
+                    assertSameValue(Function_toString(a), Function_toString(b), msg);
+            }
+        }
+
+        function at(prevmsg, segment) {
+            return prevmsg ? prevmsg + segment : "at _" + segment;
+        }
+
+        // Assert that the arguments a and b are thoroughly structurally equivalent.
+        //
+        // For the sake of speed, we cut a corner:
+        //        var x = {}, y = {}, ax = [x];
+        //        assertDeepEq([ax, x], [ax, y]);  // passes (?!)
+        //
+        // Technically this should fail, since the two object graphs are different.
+        // (The graph of [ax, y] contains one more object than the graph of [ax, x].)
+        //
+        // To get technically correct behavior, pass {strictEquivalence: true}.
+        // This is slower because we have to walk the entire graph, and Object.prototype
+        // is big.
+        //
+        return function assertDeepEq(a, b, options) {
+            var strictEquivalence = options ? options.strictEquivalence : false;
+
+            function assertSameProto(a, b, msg) {
+                check(Object_getPrototypeOf(a), Object_getPrototypeOf(b), at(msg, ".__proto__"));
+            }
+
+            function failPropList(na, nb, msg) {
+                throw Error_("got own properties " + uneval_(na) + ", expected " + uneval_(nb) +
+                             (msg ? " " + msg : ""));
+            }
+
+            function assertSameProps(a, b, msg) {
+                var na = Object_getOwnPropertyNames(a),
+                    nb = Object_getOwnPropertyNames(b);
+                if (na.length !== nb.length)
+                    failPropList(na, nb, msg);
+
+                // Ignore differences in whether Array elements are stored densely.
+                if (Array_isArray(a)) {
+                    na.sort();
+                    nb.sort();
+                }
+
+                for (var i = 0; i < na.length; i++) {
+                    var name = na[i];
+                    if (name !== nb[i])
+                        failPropList(na, nb, msg);
+                    var da = Object_getOwnPropertyDescriptor(a, name),
+                        db = Object_getOwnPropertyDescriptor(b, name);
+                    var pmsg = at(msg, /^[_$A-Za-z0-9]+$/.test(name)
+                                       ? /0|[1-9][0-9]*/.test(name) ? "[" + name + "]" : "." + name
+                                       : "[" + uneval_(name) + "]");
+                    assertSameValue(da.configurable, db.configurable, at(pmsg, ".[[Configurable]]"));
+                    assertSameValue(da.enumerable, db.enumerable, at(pmsg, ".[[Enumerable]]"));
+                    if (Object_hasOwnProperty(da, "value")) {
+                        if (!Object_hasOwnProperty(db, "value"))
+                            throw Error_("got data property, expected accessor property" + pmsg);
+                        check(da.value, db.value, pmsg);
+                    } else {
+                        if (Object_hasOwnProperty(db, "value"))
+                            throw Error_("got accessor property, expected data property" + pmsg);
+                        check(da.get, db.get, at(pmsg, ".[[Get]]"));
+                        check(da.set, db.set, at(pmsg, ".[[Set]]"));
+                    }
+                }
+            };
+
+            var ab = new Map_();
+            var bpath = new Map_();
+
+            function check(a, b, path) {
+                if (typeof a === "symbol") {
+                    // Symbols are primitives, but they have identity.
+                    // Symbol("x") !== Symbol("x") but
+                    // assertDeepEq(Symbol("x"), Symbol("x")) should pass.
+                    if (typeof b !== "symbol") {
+                        throw Error_("got " + uneval_(a) + ", expected " + uneval_(b) + " " + path);
+                    } else if (uneval_(a) !== uneval_(b)) {
+                        // We lamely use uneval_ to distinguish well-known symbols
+                        // from user-created symbols. The standard doesn't offer
+                        // a convenient way to do it.
+                        throw Error_("got " + uneval_(a) + ", expected " + uneval_(b) + " " + path);
+                    } else if (Map_has(ab, a)) {
+                        assertSameValue(Map_get(ab, a), b, path);
+                    } else if (Map_has(bpath, b)) {
+                        var bPrevPath = Map_get(bpath, b) || "_";
+                        throw Error_("got distinct symbols " + at(path, "") + " and " +
+                                     at(bPrevPath, "") + ", expected the same symbol both places");
+                    } else {
+                        Map_set(ab, a, b);
+                        Map_set(bpath, b, path);
+                    }
+                } else if (isPrimitive(a)) {
+                    assertSameValue(a, b, path);
+                } else if (isPrimitive(b)) {
+                    throw Error_("got " + Object_toString(a) + ", expected " + uneval_(b) + " " + path);
+                } else if (Map_has(ab, a)) {
+                    assertSameValue(Map_get(ab, a), b, path);
+                } else if (Map_has(bpath, b)) {
+                    var bPrevPath = Map_get(bpath, b) || "_";
+                    throw Error_("got distinct objects " + at(path, "") + " and " + at(bPrevPath, "") +
+                                 ", expected the same object both places");
+                } else {
+                    Map_set(ab, a, b);
+                    Map_set(bpath, b, path);
+                    if (a !== b || strictEquivalence) {
+                        assertSameClass(a, b, path);
+                        assertSameProto(a, b, path);
+                        assertSameProps(a, b, path);
+                        assertSameValue(Object_isExtensible(a),
+                                        Object_isExtensible(b),
+                                        at(path, ".[[Extensible]]"));
+                    }
+                }
+            }
+
+            check(a, b, "");
+        };
+    })();
+}
+
+if (typeof assertWarning === 'undefined') {
+    function assertWarning(func, name) {
+        enableLastWarning();
+        func();
+        var warning = getLastWarning();
+        assertEq(warning !== null, true);
+        assertEq(warning.name, name);
+        disableLastWarning();
+    }
+}
new file mode 100644
--- /dev/null
+++ b/js/src/tests/whatwg/streams/readable-stream-globals.js
@@ -0,0 +1,363 @@
+// |reftest| skip-if(!this.hasOwnProperty("ReadableStream"))
+
+async function test() {
+    if (typeof newGlobal !== 'undefined') {
+        otherGlobal = newGlobal();
+    }
+
+    OtherReadableStream = otherGlobal.ReadableStream;
+
+    ReadableStreamReader = new ReadableStream().getReader().constructor;
+    OtherReadableStreamReader = new otherGlobal.ReadableStream().getReader().constructor;
+
+    let byteStreamsSupported = false;
+    try {
+        let controller;
+        let reader = new ReadableStream({
+            start(c) {
+                ByteStreamController = c.constructor;
+                controller = c;
+            },
+            type: "bytes"
+        }).getReader({ mode: "byob" })
+        ReadableStreamBYOBReader = reader.constructor;
+        reader.read(new Uint8Array(10));
+        BYOBRequest = controller.byobRequest.constructor;
+        reader = new otherGlobal.ReadableStream({
+            start(c) {
+                OtherByteStreamController = c.constructor;
+                controller = c;
+            },
+            type: "bytes"
+        }).getReader({ mode: "byob" });
+        OtherReadableStreamBYOBReader = reader.constructor;
+        reader.read(new Uint8Array(10));
+        OtherBYOBRequest = controller.byobRequest.constructor;
+
+        BYOBRequestGetter = Object.getOwnPropertyDescriptor(ByteStreamController.prototype,
+                                                            "byobRequest").get;
+        OtherBYOBRequestGetter = Object.getOwnPropertyDescriptor(OtherByteStreamController.prototype,
+                                                                 "byobRequest").get;
+
+        byteStreamsSupported = true;
+    } catch (e) {
+    }
+
+    let chunk = { name: "chunk" };
+    let enqueuedError = { name: "enqueuedError" };
+
+    let controller;
+    let stream;
+    let otherStream;
+    let otherController;
+    let reader;
+    let otherReader;
+
+    function getFreshInstances(type, otherType = type) {
+        stream = new ReadableStream({ start(c) { controller = c; }, type });
+
+        otherStream = new OtherReadableStream({ start(c) { otherController = c; }, type: otherType });
+    }
+
+    getFreshInstances();
+
+    Controller = controller.constructor;
+    OtherController = otherController.constructor;
+
+
+    otherReader = OtherReadableStream.prototype.getReader.call(stream);
+    assertEq(otherReader instanceof ReadableStreamReader, false);
+    assertEq(otherReader instanceof OtherReadableStreamReader, true);
+    assertEq(otherController instanceof Controller, false);
+
+    assertEq(stream.locked, true);
+    Object.defineProperty(stream, "locked",
+        Object.getOwnPropertyDescriptor(OtherReadableStream.prototype, "locked"));
+    assertEq(stream.locked, true);
+
+
+    request = otherReader.read();
+    assertEq(request instanceof otherGlobal.Promise, true);
+    controller.close();
+    assertEq(await request instanceof Object, true);
+
+    getFreshInstances();
+    otherReader = new OtherReadableStreamReader(stream);
+
+    getFreshInstances();
+    otherReader = new OtherReadableStreamReader(stream);
+    let cancelSucceeded = false;
+    let cancelPromise = ReadableStreamReader.prototype.cancel.call(otherReader);
+    assertEq(cancelPromise instanceof Promise, true);
+    assertEq(await cancelPromise, undefined);
+
+    getFreshInstances();
+    otherReader = new OtherReadableStreamReader(stream);
+    let closeSucceeded = false;
+    Object.defineProperty(otherReader, "closed",
+        Object.getOwnPropertyDescriptor(ReadableStreamReader.prototype, "closed"));
+    let closedPromise = otherReader.closed;
+    assertEq(closedPromise instanceof otherGlobal.Promise, true);
+    controller.close();
+    assertEq(await closedPromise, undefined);
+
+    getFreshInstances();
+
+    otherReader = OtherReadableStream.prototype.getReader.call(stream);
+    request = otherReader.read();
+    assertEq(request instanceof otherGlobal.Promise, true);
+    otherController.close.call(controller);
+    assertEq(await request instanceof otherGlobal.Object, true);
+
+    getFreshInstances();
+
+    assertEq(controller.desiredSize, 1);
+    Object.defineProperty(controller, "desiredSize",
+        Object.getOwnPropertyDescriptor(OtherController.prototype, "desiredSize"));
+    assertEq(controller.desiredSize, 1);
+
+
+    request = otherReader.read();
+
+    controller.error(enqueuedError);
+
+    expectException(() => controller.close(), TypeError);
+    expectException(() => otherController.close.call(controller), otherGlobal.TypeError);
+
+    otherReader.releaseLock();
+
+    reader = stream.getReader();
+    assertEq(await expectAsyncException(async () => reader.read(), enqueuedError.constructor),
+             enqueuedError);
+
+    otherReader.releaseLock.call(reader);
+    assertEq(reader.closed instanceof otherGlobal.Promise, true);
+
+    // getFreshInstances();
+
+    // reader = stream.getReader();
+    // request = otherReader.read.call(reader);
+    // assertEq(request instanceof otherGlobal.Promise, true);
+    // controller.enqueue(chunk);
+    // assertEq((await request).value, chunk);
+
+    // reader.releaseLock();
+
+    // getFreshInstances();
+
+    // reader = stream.getReader();
+    // request = otherReader.read.call(reader);
+    // otherController.enqueue.call(controller, chunk);
+    // otherController.enqueue.call(controller, new otherGlobal.Uint8Array(10));
+    // controller.enqueue(new otherGlobal.Uint8Array(10));
+    // request = otherReader.read.call(reader);
+
+    getFreshInstances();
+
+    stream = new ReadableStream({ start(c) { controller = c; } }, { size() {return 1} });
+    otherController.enqueue.call(controller, chunk);
+    otherController.enqueue.call(controller, new otherGlobal.Uint8Array(10));
+    controller.enqueue(new otherGlobal.Uint8Array(10));
+
+    getFreshInstances();
+
+    controller.close();
+    expectException(() => controller.enqueue(new otherGlobal.Uint8Array(10)), TypeError);
+    expectException(() => otherController.enqueue.call(controller, chunk), otherGlobal.TypeError);
+    expectException(() => otherController.enqueue.call(controller, new otherGlobal.Uint8Array(10)),
+                    otherGlobal.TypeError);
+
+    getFreshInstances();
+
+    let [branch1, branch2] = otherGlobal.ReadableStream.prototype.tee.call(stream);
+    assertEq(branch1 instanceof otherGlobal.ReadableStream, true);
+    assertEq(branch2 instanceof otherGlobal.ReadableStream, true);
+
+    controller.enqueue(chunk);
+    reader = branch1.getReader();
+    result = await reader.read();
+    reader.releaseLock();
+    let subPromiseCreated = false;
+    let speciesInvoked = false;
+    class SubPromise extends Promise {
+        constructor(executor) {
+            super(executor);
+            subPromiseCreated = true;
+        }
+    }
+    Object.defineProperty(Promise, Symbol.species, {get: function() {
+        speciesInvoked = true;
+        return SubPromise;
+    }
+    });
+
+    otherGlobal.eval(`
+    subPromiseCreated = false;
+    speciesInvoked = false;
+    class OtherSubPromise extends Promise {
+        constructor(executor) {
+            super(executor);
+            subPromiseCreated = true;
+        }
+    }
+    Object.defineProperty(Promise, Symbol.species, {get: function() {
+        speciesInvoked = true;
+        return OtherSubPromise;
+    }
+    });`);
+
+    controller.error(enqueuedError);
+    subPromiseCreated = false;
+    speciesInvoked = false;
+    otherGlobal.subPromiseCreated = false;
+    otherGlobal.speciesInvoked = false;
+    let cancelPromise1 = branch1.cancel({ name: "cancel 1" });
+    assertEq(cancelPromise1 instanceof otherGlobal.Promise, true);
+    assertEq(subPromiseCreated, false);
+    assertEq(speciesInvoked, false);
+    assertEq(otherGlobal.subPromiseCreated, true);
+    assertEq(otherGlobal.speciesInvoked, true);
+    subPromiseCreated = false;
+    speciesInvoked = false;
+    otherGlobal.subPromiseCreated = false;
+    otherGlobal.speciesInvoked = false;
+    let cancelPromise2 = branch2.cancel({ name: "cancel 2" });
+    assertEq(cancelPromise2 instanceof otherGlobal.Promise, true);
+    assertEq(subPromiseCreated, false);
+    assertEq(speciesInvoked, false);
+    assertEq(otherGlobal.subPromiseCreated, true);
+    assertEq(otherGlobal.speciesInvoked, true);
+    await 1;
+
+
+    getFreshInstances();
+
+    [branch1, branch2] = otherGlobal.ReadableStream.prototype.tee.call(stream);
+    assertEq(branch1 instanceof otherGlobal.ReadableStream, true);
+    assertEq(branch2 instanceof otherGlobal.ReadableStream, true);
+
+    controller.enqueue(chunk);
+    reader = branch1.getReader();
+    result = await reader.read();
+    reader.releaseLock();
+
+
+    assertEq(result.value, chunk);
+
+    controller.error(enqueuedError);
+    subPromiseCreated = false;
+    speciesInvoked = false;
+    otherGlobal.subPromiseCreated = false;
+    otherGlobal.speciesInvoked = false;
+    cancelPromise1 = ReadableStream.prototype.cancel.call(branch1, { name: "cancel 1" });
+    assertEq(cancelPromise1 instanceof otherGlobal.Promise, true);
+    assertEq(subPromiseCreated, false);
+    assertEq(speciesInvoked, false);
+    assertEq(otherGlobal.subPromiseCreated, true);
+    assertEq(otherGlobal.speciesInvoked, true);
+    subPromiseCreated = false;
+    speciesInvoked = false;
+    otherGlobal.subPromiseCreated = false;
+    otherGlobal.speciesInvoked = false;
+    cancelPromise2 = ReadableStream.prototype.cancel.call(branch2, { name: "cancel 2" });
+    assertEq(cancelPromise2 instanceof otherGlobal.Promise, true);
+    assertEq(subPromiseCreated, false);
+    assertEq(speciesInvoked, false);
+    assertEq(otherGlobal.subPromiseCreated, true);
+    assertEq(otherGlobal.speciesInvoked, true);
+
+    if (!byteStreamsSupported) {
+        return;
+    }
+
+    if (typeof nukeCCW === 'function') {
+        getFreshInstances("bytes");
+        assertEq(otherController instanceof OtherByteStreamController, true);
+        reader = ReadableStream.prototype.getReader.call(otherStream);
+        otherGlobal.reader = reader;
+        otherGlobal.nukeCCW(otherGlobal.reader);
+        let chunk = new Uint8Array(10);
+        expectException(() => otherController.enqueue(chunk), otherGlobal.TypeError);
+        // otherController.error();
+        expectException(() => reader.read(), TypeError);
+    }
+
+    function testBYOBRequest(controller, view) {
+        const request = new BYOBRequest(controller, view);
+        let storedView = request.view;
+        assertEq(storedView, view);
+        storedView = Object.getOwnPropertyDescriptor(OtherBYOBRequest.prototype, "view").get.call(request);
+        assertEq(storedView, view);
+        request.respond(10);
+        OtherBYOBRequest.prototype.respond.call(request, 10);
+        request.respondWithNewView(new view.constructor(10));
+        OtherBYOBRequest.prototype.respondWithNewView.call(request, new view.constructor(10));
+    }
+
+    expectException(() => new BYOBRequest(), TypeError);
+    getFreshInstances("bytes");
+    expectException(() => new BYOBRequest(controller, new Uint8Array(10)), TypeError);
+    expectException(() => new BYOBRequest(otherController, new Uint8Array(10)), TypeError);
+    expectException(() => new BYOBRequest(otherController, new Uint8Array(10)), TypeError);
+    expectException(() => new BYOBRequest(otherController, new otherGlobal.Uint8Array(10)), TypeError);
+
+    getFreshInstances("bytes");
+
+    reader = stream.getReader({ mode: "byob" });
+    request = OtherReadableStreamBYOBReader.prototype.read.call(reader, new Uint8Array(10));
+    assertEq(request instanceof otherGlobal.Promise, true);
+    controller.enqueue(new Uint8Array([1, 2, 3, 4]));
+    result = await request;
+
+    getFreshInstances("bytes");
+
+    reader = stream.getReader({ mode: "byob" });
+    request = OtherReadableStreamBYOBReader.prototype.read.call(reader, new Uint8Array(10));
+    assertEq(request instanceof otherGlobal.Promise, true);
+    try {
+        let byobRequest = OtherBYOBRequestGetter.call(controller);
+    } catch (e) {
+        print(e, '\n', e.stack);
+    }
+    controller.enqueue(new Uint8Array([1, 2, 3, 4]));
+    result = await request;
+
+    await 1;
+}
+
+function expectException(closure, errorType) {
+    let error;
+    try {
+        closure();
+    } catch (e) {
+        error = e;
+    }
+    assertEq(error !== undefined, true);
+    assertEq(error.constructor, errorType);
+    return error;
+}
+
+async function expectAsyncException(closure, errorType) {
+    let error;
+    try {
+        await closure();
+    } catch (e) {
+        error = e;
+    }
+    assertEq(error !== undefined, true);
+    assertEq(error.constructor, errorType);
+    return error;
+}
+
+async function runTest() {
+    try {
+        await test();
+    } catch (e) {
+        assertEq(false, true, `Unexpected exception ${e}\n${e.stack}`);
+    }
+    console.log("done");
+    if (typeof reportCompare === "function")
+        reportCompare(true, true);
+}
+
+runTest();
new file mode 100644
--- a/js/src/vm/CommonPropertyNames.h
+++ b/js/src/vm/CommonPropertyNames.h
@@ -12,18 +12,16 @@
 #include "js/ProtoKey.h"
 
 #define FOR_EACH_COMMON_PROPERTYNAME(macro) \
     macro(add, add, "add") \
     macro(allowContentIter, allowContentIter, "allowContentIter") \
     macro(anonymous, anonymous, "anonymous") \
     macro(Any, Any, "Any") \
     macro(apply, apply, "apply") \
-    macro(AcquireReadableStreamBYOBReader, AcquireReadableStreamBYOBReader, "AcquireReadableStreamBYOBReader") \
-    macro(AcquireReadableStreamDefaultReader, AcquireReadableStreamDefaultReader, "AcquireReadableStreamDefaultReader") \
     macro(arguments, arguments, "arguments") \
     macro(ArrayBufferSpecies, ArrayBufferSpecies, "ArrayBufferSpecies") \
     macro(ArrayIterator, ArrayIterator, "Array Iterator") \
     macro(ArrayIteratorNext, ArrayIteratorNext, "ArrayIteratorNext") \
     macro(ArraySort, ArraySort, "ArraySort") \
     macro(ArraySpecies, ArraySpecies, "ArraySpecies") \
     macro(ArraySpeciesCreate, ArraySpeciesCreate, "ArraySpeciesCreate") \
     macro(ArrayToLocaleString, ArrayToLocaleString, "ArrayToLocaleString") \
@@ -299,60 +297,16 @@
     macro(private, private_, "private") \
     macro(promise, promise, "promise") \
     macro(propertyIsEnumerable, propertyIsEnumerable, "propertyIsEnumerable") \
     macro(protected, protected_, "protected") \
     macro(proto, proto, "__proto__") \
     macro(prototype, prototype, "prototype") \
     macro(proxy, proxy, "proxy") \
     macro(raw, raw, "raw") \
-    macro(ReadableByteStreamControllerGetDesiredSize, \
-          ReadableByteStreamControllerGetDesiredSize, \
-          "ReadableByteStreamControllerGetDesiredSize") \
-    macro(ReadableByteStreamController_close, \
-          ReadableByteStreamController_close, \
-          "ReadableByteStreamController_close") \
-    macro(ReadableByteStreamController_enqueue, \
-          ReadableByteStreamController_enqueue, \
-          "ReadableByteStreamController_enqueue") \
-    macro(ReadableByteStreamController_error, \
-          ReadableByteStreamController_error, \
-          "ReadableByteStreamController_error") \
-    macro(ReadableStreamBYOBReader_cancel, \
-          ReadableStreamBYOBReader_cancel, \
-          "ReadableStreamBYOBReader_cancel") \
-    macro(ReadableStreamBYOBReader_read, \
-          ReadableStreamBYOBReader_read, \
-          "ReadableStreamBYOBReader_read") \
-    macro(ReadableStreamBYOBReader_releaseLock, \
-          ReadableStreamBYOBReader_releaseLock, \
-          "ReadableStreamBYOBReader_releaseLock") \