Bug 1272697 - Part 4: Add JSAPI functions for working with ReadableStream. r=shu
☠☠ backed out by 3d6b96a16105 ☠ ☠
authorTill Schneidereit <till@tillschneidereit.net>
Mon, 29 May 2017 16:26:32 +0200
changeset 420432 2693a863dabdd783e32e7b8ac6164e25c30fed4c
parent 420431 862ba8683d196e7d194ab72d2a1680ff707c47d3
child 420433 1dde9a9f77457df35fee9d19818926df6e552b34
push id7566
push usermtabara@mozilla.com
push dateWed, 02 Aug 2017 08:25:16 +0000
treeherdermozilla-beta@86913f512c3c [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersshu
bugs1272697
milestone56.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 1272697 - Part 4: Add JSAPI functions for working with ReadableStream. r=shu This adds a ton of JSAPI functions for creating and querying the state of ReadableStreams, and support for creating ReadableStream instances whose source is supplied by the embedding. MozReview-Commit-ID: 9uDWOazPaUI
js/public/Stream.h
js/src/builtin/Stream.cpp
js/src/builtin/Stream.h
js/src/js.msg
js/src/jsapi-tests/moz.build
js/src/jsapi-tests/testIntTypesABI.cpp
js/src/jsapi-tests/testReadableStream.cpp
js/src/jsapi-tests/tests.cpp
js/src/jsapi.cpp
js/src/jsapi.h
js/src/moz.build
js/src/vm/CommonPropertyNames.h
js/src/vm/Runtime.cpp
js/src/vm/Runtime.h
new file mode 100644
--- /dev/null
+++ b/js/public/Stream.h
@@ -0,0 +1,522 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*-
+ * vim: set ts=8 sts=4 et sw=4 tw=99:
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/*
+ * JSAPI functions and callbacks related to WHATWG Stream objects.
+ *
+ * Much of the API here mirrors the JS API of ReadableStream and associated
+ * classes, e.g. ReadableStreamDefaultReader, ReadableStreamBYOBReader,
+ * ReadableStreamDefaultController, ReadableByteStreamController, and
+ * ReadableStreamBYOBRequest.
+ *
+ * There are some crucial differences, though: Functionality that's exposed
+ * as methods/accessors on controllers in JS is exposed as functions taking
+ * ReadableStream instances instead. This is because an analysis of how
+ * the API would be used showed that all functions that'd take controllers
+ * would do so by first getting the controller from the stream instance it's
+ * associated with and then call the function taking it. I.e., it would purely
+ * add boilerplate without any gains in ease of use of the API.
+ *
+ * It would probably still make sense to factor the API the same as the JS API
+ * if we had to keep any API stability guarantees: the JS API won't change, so
+ * we could be sure that the C++ API could stay the same, too. Given that we
+ * don't guarantee API stability, this concern isn't too pressing.
+ *
+ * Some functions exposed here deal with ReadableStream instances that have an
+ * embedding-provided underlying source. These instances are largely similar
+ * to byte streams as created using |new ReadableStream({type: "bytes"})|:
+ * They enable users to acquire ReadableStreamBYOBReaders and only vend chunks
+ * that're typed array instances.
+ *
+ * When creating an "external readable stream" using
+ * JS::NewReadableExternalSourceStreamObject, an underlying source and a set
+ * of flags can be passed to be stored on the stream. The underlying source is
+ * treated as an opaque void* pointer by the JS engine: it's purely meant as
+ * a reference to be used by the embedding to identify whatever actual source
+ * it uses to supply data for the stream. Similarly, the flags aren't
+ * interpreted by the JS engine, but are passed to some of the callbacks below
+ * and can be retrieved using JS::ReadableStreamGetEmbeddingFlags.
+ *
+ * External readable streams are optimized to allow the embedding to interact
+ * with them with a minimum of overhead: chunks aren't enqueued as individual
+ * typed array instances; instead, the embedding only updates the amount of
+ * data available using ReadableStreamUpdateDataAvailableFromSource.
+ * When content requests data by reading from a reader,
+ * WriteIntoReadRequestBufferCallback is invoked, asking the embedding to
+ * write data directly into the buffer we're about to hand to content.
+ *
+ * Additionally, ReadableStreamGetExternalUnderlyingSource can be used to
+ * get the void* pointer to the underlying source. This is equivalent to
+ * acquiring a reader for the stream in that it locks the stream until it
+ * is released again using JS::ReadableStreamReleaseExternalUnderlyingSource.
+ *
+ * Embeddings are expected to detect situations where an API exposed to JS
+ * takes a ReadableStream to read from that has an external underlying source.
+ * In those situations, it might be preferable to directly perform data
+ * transfers from the stream's underlying source to whatever sink the
+ * embedding uses, assuming that such direct transfers can be performed
+ * more efficiently.
+ *
+ * An example of such an optimized operation might be a ServiceWorker piping a
+ * fetch Response body to a TextDecoder: instead of writing chunks of data
+ * into JS typed array buffers only to immediately read from them again, the
+ * embedding can presumably directly feed the incoming data to the
+ * TextDecoder's underlying implementation.
+ */
+
+#ifndef js_Stream_h
+#define js_Stream_h
+
+#include "jstypes.h"
+
+#include "js/TypeDecls.h"
+
+namespace JS {
+
+/**
+ * Invoked whenever a reader desires more data from a ReadableStream's
+ * embedding-provided underlying source.
+ *
+ * The given |desiredSize| is the absolute size, not a delta from the previous
+ * desired size.
+ */
+typedef void
+(* RequestReadableStreamDataCallback)(JSContext* cx, HandleObject stream,
+                                      void* underlyingSource, uint8_t flags, size_t desiredSize);
+
+/**
+ * Invoked to cause the embedding to fill the given |buffer| with data from
+ * the given embedding-provided underlying source.
+ *
+ * This can only happen after the embedding has updated the amount of data
+ * available using JS::ReadableStreamUpdateDataAvailableFromSource. If at
+ * least one read request is pending when
+ * JS::ReadableStreamUpdateDataAvailableFromSource is called,
+ * the WriteIntoReadRequestBufferCallback is invoked immediately from under
+ * the call to JS::WriteIntoReadRequestBufferCallback. If not, it is invoked
+ * if and when a new read request is made.
+ *
+ * Note: This callback *must not cause GC*, because that could potentially
+ * invalidate the |buffer| pointer.
+ */
+typedef void
+(* WriteIntoReadRequestBufferCallback)(JSContext* cx, HandleObject stream,
+                                       void* underlyingSource, uint8_t flags, void* buffer,
+                                       size_t length, size_t* bytesWritten);
+
+/**
+ * Invoked in reaction to the ReadableStream being canceled to allow the
+ * embedding to free the underlying source.
+ *
+ * This is equivalent to calling |cancel| on non-external underlying sources
+ * provided to the ReadableStream constructor in JavaScript.
+ *
+ * The given |reason| is the JS::Value that was passed as an argument to
+ * ReadableStream#cancel().
+ *
+ * The returned JS::Value will be used to resolve the Promise returned by
+ * ReadableStream#cancel().
+ */
+typedef Value
+(* CancelReadableStreamCallback)(JSContext* cx, HandleObject stream,
+                                 void* underlyingSource, uint8_t flags, HandleValue reason);
+
+/**
+ * Invoked in reaction to a ReadableStream with an embedding-provided
+ * underlying source being closed.
+ */
+typedef void
+(* ReadableStreamClosedCallback)(JSContext* cx, HandleObject stream, void* underlyingSource,
+                                 uint8_t flags);
+
+/**
+ * Invoked in reaction to a ReadableStream with an embedding-provided
+ * underlying source being errored with the
+ * given reason.
+ */
+typedef void
+(* ReadableStreamErroredCallback)(JSContext* cx, HandleObject stream, void* underlyingSource,
+                                  uint8_t flags, HandleValue reason);
+
+/**
+ * Invoked in reaction to a ReadableStream with an embedding-provided
+ * underlying source being finalized. Only the underlying source is passed
+ * as an argument, while the ReadableStream itself is not to prevent the
+ * embedding from operating on a JSObject that might not be in a valid state
+ * anymore.
+ *
+ * Note: the ReadableStream might be finalized on a background thread. That
+ * means this callback might be invoked from an arbitrary thread, which the
+ * embedding must be able to handle.
+ */
+typedef void
+(* ReadableStreamFinalizeCallback)(void* underlyingSource, uint8_t flags);
+
+/**
+ * Sets runtime-wide callbacks to use for interacting with embedding-provided
+ * hooks for operating on ReadableStream instances.
+ *
+ * See the documentation for the individual callback types for details.
+ */
+extern JS_PUBLIC_API(void)
+SetReadableStreamCallbacks(JSContext* cx,
+                           RequestReadableStreamDataCallback dataRequestCallback,
+                           WriteIntoReadRequestBufferCallback writeIntoReadRequestCallback,
+                           CancelReadableStreamCallback cancelCallback,
+                           ReadableStreamClosedCallback closedCallback,
+                           ReadableStreamErroredCallback erroredCallback,
+                           ReadableStreamFinalizeCallback finalizeCallback);
+
+extern JS_PUBLIC_API(bool)
+HasReadableStreamCallbacks(JSContext* cx);
+
+/**
+ * Returns a new instance of the ReadableStream builtin class in the current
+ * compartment, configured as a default stream.
+ * If a |proto| is passed, that gets set as the instance's [[Prototype]]
+ * instead of the original value of |ReadableStream.prototype|.
+ */
+extern JS_PUBLIC_API(JSObject*)
+NewReadableDefaultStreamObject(JSContext* cx, HandleObject underlyingSource = nullptr,
+                               HandleFunction size = nullptr, double highWaterMark = 1,
+                               HandleObject proto = nullptr);
+
+/**
+ * Returns a new instance of the ReadableStream builtin class in the current
+ * compartment, configured as a byte stream.
+ * If a |proto| is passed, that gets set as the instance's [[Prototype]]
+ * instead of the original value of |ReadableStream.prototype|.
+ */
+extern JS_PUBLIC_API(JSObject*)
+NewReadableByteStreamObject(JSContext* cx, HandleObject underlyingSource = nullptr,
+                            double highWaterMark = 0, HandleObject proto = nullptr);
+
+/**
+ * Returns a new instance of the ReadableStream builtin class in the current
+ * compartment, with the right slot layout. If a |proto| is passed, that gets
+ * set as the instance's [[Prototype]] instead of the original value of
+ * |ReadableStream.prototype|.
+ *
+ * The instance is optimized for operating as a byte stream backed by an
+ * embedding-provided underlying source, using the callbacks set via
+ * |JS::SetReadableStreamCallbacks|.
+ *
+ * The given |flags| will be passed to all applicable callbacks and can be
+ * used to disambiguate between different types of stream sources the
+ * embedding might support.
+ *
+ * Note: the embedding is responsible for ensuring that the pointer to the
+ * underlying source stays valid as long as the stream can be read from.
+ * The underlying source can be freed if the tree is canceled or errored.
+ * It can also be freed if the stream is destroyed. The embedding is notified
+ * of that using ReadableStreamFinalizeCallback.
+ */
+extern JS_PUBLIC_API(JSObject*)
+NewReadableExternalSourceStreamObject(JSContext* cx, void* underlyingSource,
+                                      uint8_t flags = 0, HandleObject proto = nullptr);
+
+/**
+ * Returns the flags that were passed to NewReadableExternalSourceStreamObject
+ * when creating the given stream.
+ *
+ * Asserts that the given stream has an embedding-provided underlying source.
+ */
+extern JS_PUBLIC_API(uint8_t)
+ReadableStreamGetEmbeddingFlags(const JSObject* stream);
+
+/**
+ * Returns the embedding-provided underlying source of the given |stream|.
+ *
+ * Can be used to optimize operations if both the underlying source and the
+ * intended sink are embedding-provided. In that case it might be
+ * preferrable to pipe data directly from source to sink without interacting
+ * with the stream at all.
+ *
+ * Locks the stream until ReadableStreamReleaseExternalUnderlyingSource is
+ * called.
+ *
+ * Throws an exception if the stream is locked, i.e. if a reader has been
+ * acquired for the stream, or if ReadableStreamGetExternalUnderlyingSource
+ * has been used previously without releasing the external source again.
+ *
+ * Throws an exception if the stream isn't readable, i.e if it is errored or
+ * closed. This is different from ReadableStreamGetReader because we don't
+ * have a Promise to resolve/reject, which a reader provides.
+ *
+ * Asserts that the stream has an embedding-provided underlying source.
+ */
+extern JS_PUBLIC_API(bool)
+ReadableStreamGetExternalUnderlyingSource(JSContext* cx, HandleObject stream, void** source);
+
+/**
+ * Releases the embedding-provided underlying source of the given |stream|,
+ * returning the stream into an unlocked state.
+ *
+ * Asserts that the stream was locked through
+ * ReadableStreamGetExternalUnderlyingSource.
+ *
+ * Asserts that the stream has an embedding-provided underlying source.
+ */
+extern JS_PUBLIC_API(void)
+ReadableStreamReleaseExternalUnderlyingSource(JSObject* stream);
+
+/**
+ * Update the amount of data available at the underlying source of the given
+ * |stream|.
+ *
+ * Can only be used for streams with an embedding-provided underlying source.
+ * The JS engine will use the given value to satisfy read requests for the
+ * stream by invoking the JS::WriteIntoReadRequestBuffer callback.
+ */
+extern JS_PUBLIC_API(bool)
+ReadableStreamUpdateDataAvailableFromSource(JSContext* cx, HandleObject stream,
+                                            uint32_t availableData);
+
+/**
+ * Returns true if the given object is an unwrapped ReadableStream object,
+ * false otherwise.
+ */
+extern JS_PUBLIC_API(bool)
+IsReadableStream(const JSObject* obj);
+
+/**
+ * Returns true if the given object is an unwrapped
+ * ReadableStreamDefaultReader or ReadableStreamBYOBReader object,
+ * false otherwise.
+ */
+extern JS_PUBLIC_API(bool)
+IsReadableStreamReader(const JSObject* obj);
+
+/**
+ * Returns true if the given object is an unwrapped
+ * ReadableStreamDefaultReader object, false otherwise.
+ */
+extern JS_PUBLIC_API(bool)
+IsReadableStreamDefaultReader(const JSObject* obj);
+
+/**
+ * Returns true if the given object is an unwrapped
+ * ReadableStreamBYOBReader object, false otherwise.
+ */
+extern JS_PUBLIC_API(bool)
+IsReadableStreamBYOBReader(const JSObject* obj);
+
+enum class ReadableStreamMode {
+    Default,
+    Byte,
+    ExternalSource
+};
+
+/**
+ * Returns the stream's ReadableStreamMode. If the mode is |Byte| or
+ * |ExternalSource|, it's possible to acquire a BYOB reader for more optimized
+ * operations.
+ *
+ * Asserts that |stream| is an unwrapped ReadableStream instance.
+ */
+extern JS_PUBLIC_API(ReadableStreamMode)
+ReadableStreamGetMode(const JSObject* stream);
+
+enum class ReadableStreamReaderMode {
+    Default,
+    BYOB
+};
+
+/**
+ * Returns true if the given ReadableStream is readable, false if not.
+ *
+ * Asserts that |stream| is an unwrapped ReadableStream instance.
+ */
+extern JS_PUBLIC_API(bool)
+ReadableStreamIsReadable(const JSObject* stream);
+
+/**
+ * Returns true if the given ReadableStream is locked, false if not.
+ *
+ * Asserts that |stream| is an unwrapped ReadableStream instance.
+ */
+extern JS_PUBLIC_API(bool)
+ReadableStreamIsLocked(const JSObject* stream);
+
+/**
+ * Returns true if the given ReadableStream is disturbed, false if not.
+ *
+ * Asserts that |stream| is an ReadableStream instance.
+ */
+extern JS_PUBLIC_API(bool)
+ReadableStreamIsDisturbed(const JSObject* stream);
+
+/**
+ * Cancels the given ReadableStream with the given reason and returns a
+ * Promise resolved according to the result.
+ *
+ * Asserts that |stream| is an unwrapped ReadableStream instance.
+ */
+extern JS_PUBLIC_API(JSObject*)
+ReadableStreamCancel(JSContext* cx, HandleObject stream, HandleValue reason);
+
+/**
+ * Creates a reader of the type specified by the mode option and locks the
+ * stream to the new reader.
+ *
+ * Asserts that |stream| is an unwrapped ReadableStream instance.
+ */
+extern JS_PUBLIC_API(JSObject*)
+ReadableStreamGetReader(JSContext* cx, HandleObject stream, ReadableStreamReaderMode mode);
+
+/**
+ * Tees the given ReadableStream and stores the two resulting streams in
+ * outparams. Returns false if the operation fails, e.g. because the stream is
+ * locked.
+ *
+ * Asserts that |stream| is an unwrapped ReadableStream instance.
+ */
+extern JS_PUBLIC_API(bool)
+ReadableStreamTee(JSContext* cx, HandleObject stream,
+                  MutableHandleObject branch1Stream, MutableHandleObject branch2Stream);
+
+/**
+ * Retrieves the desired combined size of additional chunks to fill the given
+ * ReadableStream's queue. Stores the result in |value| and sets |hasValue| to
+ * true on success, returns false on failure.
+ *
+ * If the stream is errored, the call will succeed but no value will be stored
+ * in |value| and |hasValue| will be set to false.
+ *
+ * Note: This is semantically equivalent to the |desiredSize| getter on
+ * the stream controller's prototype in JS. We expose it with the stream
+ * itself as a target for simplicity.
+ *
+ * Asserts that |stream| is an unwrapped ReadableStream instance.
+ */
+extern JS_PUBLIC_API(void)
+ReadableStreamGetDesiredSize(JSObject* stream, bool* hasValue, double* value);
+
+/**
+ * Closes the given ReadableStream.
+ *
+ * Throws a TypeError and returns false if the closing operation fails.
+ *
+ * Note: This is semantically equivalent to the |close| method on
+ * the stream controller's prototype in JS. We expose it with the stream
+ * itself as a target for simplicity.
+ *
+ * Asserts that |stream| is an unwrapped ReadableStream instance.
+ */
+extern JS_PUBLIC_API(bool)
+ReadableStreamClose(JSContext* cx, HandleObject stream);
+
+/**
+ * Returns true if the given ReadableStream reader is locked, false otherwise.
+ *
+ * Asserts that |reader| is an unwrapped ReadableStreamDefaultReader or
+ * ReadableStreamBYOBReader instance.
+ */
+extern JS_PUBLIC_API(bool)
+ReadableStreamReaderIsClosed(const JSObject* reader);
+
+/**
+ * Enqueues the given chunk in the given ReadableStream.
+ *
+ * Throws a TypeError and returns false if the enqueing operation fails.
+ *
+ * Note: This is semantically equivalent to the |enqueue| method on
+ * the stream controller's prototype in JS. We expose it with the stream
+ * itself as a target for simplicity.
+ *
+ * If the ReadableStream has an underlying byte source, the given chunk must
+ * be a typed array or a DataView. Consider using
+ * ReadableByteStreamEnqueueBuffer.
+ *
+ * Asserts that |stream| is an unwrapped ReadableStream instance.
+ */
+extern JS_PUBLIC_API(bool)
+ReadableStreamEnqueue(JSContext* cx, HandleObject stream, HandleValue chunk);
+
+/**
+ * Enqueues the given buffer as a chunk in the given ReadableStream.
+ *
+ * Throws a TypeError and returns false if the enqueing operation fails.
+ *
+ * Note: This is semantically equivalent to the |enqueue| method on
+ * the stream controller's prototype in JS. We expose it with the stream
+ * itself as a target for simplicity. Additionally, the JS version only
+ * takes typed arrays and ArrayBufferView instances as arguments, whereas
+ * this takes an ArrayBuffer, obviating the need to wrap it into a typed
+ * array.
+ *
+ * Asserts that |stream| is an unwrapped ReadableStream instance and |buffer|
+ * an unwrapped ArrayBuffer instance.
+ */
+extern JS_PUBLIC_API(bool)
+ReadableByteStreamEnqueueBuffer(JSContext* cx, HandleObject stream, HandleObject buffer);
+
+/**
+ * Errors the given ReadableStream, causing all future interactions to fail
+ * with the given error value.
+ *
+ * Throws a TypeError and returns false if the erroring operation fails.
+ *
+ * Note: This is semantically equivalent to the |error| method on
+ * the stream controller's prototype in JS. We expose it with the stream
+ * itself as a target for simplicity.
+ *
+ * Asserts that |stream| is an unwrapped ReadableStream instance.
+ */
+extern JS_PUBLIC_API(bool)
+ReadableStreamError(JSContext* cx, HandleObject stream, HandleValue error);
+
+/**
+ * Cancels the given ReadableStream reader's associated stream.
+ *
+ * Throws a TypeError and returns false if the given reader isn't active.
+ *
+ * Asserts that |reader| is an unwrapped ReadableStreamDefaultReader or
+ * ReadableStreamBYOBReader instance.
+ */
+extern JS_PUBLIC_API(bool)
+ReadableStreamReaderCancel(JSContext* cx, HandleObject reader, HandleValue reason);
+
+/**
+ * Cancels the given ReadableStream reader's associated stream.
+ *
+ * Throws a TypeError and returns false if the given reader has pending
+ * read or readInto (for default or byob readers, respectively) requests.
+ *
+ * Asserts that |reader| is an unwrapped ReadableStreamDefaultReader or
+ * ReadableStreamBYOBReader instance.
+ */
+extern JS_PUBLIC_API(bool)
+ReadableStreamReaderReleaseLock(JSContext* cx, HandleObject reader);
+
+/**
+ * Requests a read from the reader's associated ReadableStream and returns the
+ * resulting PromiseObject.
+ *
+ * Returns a Promise that's resolved with the read result once available or
+ * rejected immediately if the stream is errored or the operation failed.
+ *
+ * Asserts that |reader| is an unwrapped ReadableStreamDefaultReader instance.
+ */
+extern JS_PUBLIC_API(JSObject*)
+ReadableStreamDefaultReaderRead(JSContext* cx, HandleObject reader);
+
+/**
+ * Requests a read from the reader's associated ReadableStream into the given
+ * ArrayBufferView and returns the resulting PromiseObject.
+ *
+ * Returns a Promise that's resolved with the read result once available or
+ * rejected immediately if the stream is errored or the operation failed.
+ *
+ * Asserts that |reader| is an unwrapped ReadableStreamDefaultReader and
+ * |view| an unwrapped typed array or DataView instance.
+ */
+extern JS_PUBLIC_API(JSObject*)
+ReadableStreamBYOBReaderRead(JSContext* cx, HandleObject reader, HandleObject view);
+
+} // namespace JS
+
+#endif // js_Realm_h
--- a/js/src/builtin/Stream.cpp
+++ b/js/src/builtin/Stream.cpp
@@ -1,16 +1,18 @@
 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*-
  * vim: set ts=8 sts=4 et sw=4 tw=99:
  * This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include "builtin/Stream.h"
 
+#include "js/Stream.h"
+
 #include "jscntxt.h"
 
 #include "gc/Heap.h"
 #include "vm/SelfHosting.h"
 
 #include "jsobjinlines.h"
 
 #include "vm/NativeObject-inl.h"
@@ -69,48 +71,46 @@ enum ByteControllerSlots {
 
 enum ControllerFlags {
     ControllerFlag_Started        = 1 << 0,
     ControllerFlag_Pulling        = 1 << 1,
     ControllerFlag_PullAgain      = 1 << 2,
     ControllerFlag_CloseRequested = 1 << 3,
     ControllerFlag_TeeBranch      = 1 << 4,
     ControllerFlag_TeeBranch1     = 1 << 5,
-    ControllerFlag_TeeBranch2     = 1 << 6
+    ControllerFlag_TeeBranch2     = 1 << 6,
+    ControllerFlag_ExternalSource = 1 << 7,
+    ControllerFlag_SourceLocked   = 1 << 8,
 };
 
+// Offset at which embedding flags are stored.
+constexpr uint8_t ControllerEmbeddingFlagsOffset = 24;
+
 enum BYOBRequestSlots {
     BYOBRequestSlot_Controller,
     BYOBRequestSlot_View,
     BYOBRequestSlotCount
 };
 
 template<class T>
 MOZ_ALWAYS_INLINE bool
-Is(HandleValue v)
+Is(const HandleValue v)
 {
     return v.isObject() && v.toObject().is<T>();
 }
 
 #ifdef DEBUG
 static bool
 IsReadableStreamController(const JSObject* controller)
 {
     return controller->is<ReadableStreamDefaultController>() ||
            controller->is<ReadableByteStreamController>();
 }
 #endif // DEBUG
 
-static bool
-IsReadableStreamReader(const JSObject* reader)
-{
-    return reader->is<ReadableStreamDefaultReader>() ||
-           reader->is<ReadableStreamBYOBReader>();
-}
-
 static inline uint32_t
 ControllerFlags(const NativeObject* controller)
 {
     MOZ_ASSERT(IsReadableStreamController(controller));
     return controller->getFixedSlot(ControllerSlot_Flags).toInt32();
 }
 
 static inline void
@@ -138,70 +138,106 @@ StreamState(const ReadableStream* stream
 static inline void
 SetStreamState(ReadableStream* stream, uint32_t state)
 {
     MOZ_ASSERT_IF(stream->disturbed(), state & ReadableStream::Disturbed);
     MOZ_ASSERT_IF(stream->closed() || stream->errored(), !(state & ReadableStream::Readable));
     stream->setFixedSlot(StreamSlot_State, Int32Value(state));
 }
 
-inline bool
+bool
 ReadableStream::readable() const
 {
     return StreamState(this) & Readable;
 }
 
-inline bool
+bool
 ReadableStream::closed() const
 {
     return StreamState(this) & Closed;
 }
 
-inline bool
+bool
 ReadableStream::errored() const
 {
     return StreamState(this) & Errored;
 }
 
-inline bool
+bool
 ReadableStream::disturbed() const
 {
     return StreamState(this) & Disturbed;
 }
 
+inline static bool
+ReaderHasStream(const NativeObject* reader)
+{
+    MOZ_ASSERT(JS::IsReadableStreamReader(reader));
+    return !reader->getFixedSlot(ReaderSlot_Stream).isUndefined();
+}
+
+bool
+js::ReadableStreamReaderIsClosed(const JSObject* reader)
+{
+    return !ReaderHasStream(&reader->as<NativeObject>());
+}
+
 inline static MOZ_MUST_USE ReadableStream*
 StreamFromController(const NativeObject* controller)
 {
     MOZ_ASSERT(IsReadableStreamController(controller));
     return &controller->getFixedSlot(ControllerSlot_Stream).toObject().as<ReadableStream>();
 }
 
 inline static MOZ_MUST_USE NativeObject*
-ControllerFromStream(ReadableStream* stream)
+ControllerFromStream(const ReadableStream* stream)
 {
     Value controllerVal = stream->getFixedSlot(StreamSlot_Controller);
     MOZ_ASSERT(IsReadableStreamController(&controllerVal.toObject()));
     return &controllerVal.toObject().as<NativeObject>();
 }
 
+inline static bool
+HasController(const ReadableStream* stream)
+{
+    return !stream->getFixedSlot(StreamSlot_Controller).isUndefined();
+}
+
+JS::ReadableStreamMode
+ReadableStream::mode() const
+{
+    NativeObject* controller = ControllerFromStream(this);
+    if (controller->is<ReadableStreamDefaultController>())
+        return JS::ReadableStreamMode::Default;
+    return controller->as<ReadableByteStreamController>().hasExternalSource()
+           ? JS::ReadableStreamMode::ExternalSource
+           : JS::ReadableStreamMode::Byte;
+}
+
 inline static MOZ_MUST_USE ReadableStream*
 StreamFromReader(const NativeObject* reader)
 {
-    MOZ_ASSERT(IsReadableStreamReader(reader));
+    MOZ_ASSERT(ReaderHasStream(reader));
     return &reader->getFixedSlot(ReaderSlot_Stream).toObject().as<ReadableStream>();
 }
 
 inline static MOZ_MUST_USE NativeObject*
-ReaderFromStream(NativeObject* stream)
+ReaderFromStream(const NativeObject* stream)
 {
     Value readerVal = stream->getFixedSlot(StreamSlot_Reader);
-    MOZ_ASSERT(IsReadableStreamReader(&readerVal.toObject()));
+    MOZ_ASSERT(JS::IsReadableStreamReader(&readerVal.toObject()));
     return &readerVal.toObject().as<NativeObject>();
 }
 
+inline static bool
+HasReader(const ReadableStream* stream)
+{
+    return !stream->getFixedSlot(StreamSlot_Reader).isUndefined();
+}
+
 inline static MOZ_MUST_USE JSFunction*
 NewHandler(JSContext *cx, Native handler, HandleObject target)
 {
     RootedAtom funName(cx, cx->names().empty);
     RootedFunction handlerFun(cx, NewNativeFunction(cx, handler, 0, funName,
                                                     gc::AllocKind::FUNCTION_EXTENDED,
                                                     GenericObject));
     if (!handlerFun)
@@ -571,79 +607,81 @@ class TeeState : public NativeObject
    }
 };
 
 const Class TeeState::class_ = {
     "TeeState",
     JSCLASS_HAS_RESERVED_SLOTS(SlotCount)
 };
 
-#define CLASS_SPEC(cls, nCtorArgs, nSlots, specFlags) \
+#define CLASS_SPEC(cls, nCtorArgs, nSlots, specFlags, classFlags, classOps) \
 const ClassSpec cls::classSpec_ = { \
     GenericCreateConstructor<cls::constructor, nCtorArgs, gc::AllocKind::FUNCTION>, \
     GenericCreatePrototype, \
     nullptr, \
     nullptr, \
     cls##_methods, \
     cls##_properties, \
     nullptr, \
     specFlags \
 }; \
 \
 const Class cls::class_ = { \
     #cls, \
     JSCLASS_HAS_RESERVED_SLOTS(nSlots) | \
-    JSCLASS_HAS_CACHED_PROTO(JSProto_##cls), \
-    JS_NULL_CLASS_OPS, \
+    JSCLASS_HAS_CACHED_PROTO(JSProto_##cls) | \
+    classFlags, \
+    classOps, \
     &cls::classSpec_ \
 }; \
 \
 const Class cls::protoClass_ = { \
     "object", \
     JSCLASS_HAS_CACHED_PROTO(JSProto_##cls), \
     JS_NULL_CLASS_OPS, \
     &cls::classSpec_ \
 };
 
 // Streams spec, 3.2.3., steps 1-4.
 ReadableStream*
-ReadableStream::createStream(JSContext* cx)
+ReadableStream::createStream(JSContext* cx, HandleObject proto /* = nullptr */)
 {
-    Rooted<ReadableStream*> stream(cx, NewBuiltinClassInstance<ReadableStream>(cx));
+    Rooted<ReadableStream*> stream(cx, NewObjectWithClassProto<ReadableStream>(cx, proto));
     if (!stream)
         return nullptr;
 
-    // Step 1 (reordered): Set this.[[state]] to "readable".
+    // Step 1: Set this.[[state]] to "readable".
     // Step 2: Set this.[[reader]] and this.[[storedError]] to undefined (implicit).
     // Step 3: Set this.[[disturbed]] to false (implicit).
     // Step 4: Set this.[[readableStreamController]] to undefined (implicit).
     stream->setFixedSlot(StreamSlot_State, Int32Value(Readable));
 
     return stream;
 }
 
 static MOZ_MUST_USE ReadableStreamDefaultController*
 CreateReadableStreamDefaultController(JSContext* cx, Handle<ReadableStream*> stream,
                                       HandleValue underlyingSource, HandleValue size,
                                       HandleValue highWaterMarkVal);
 
 // Streams spec, 3.2.3., steps 1-4, 8.
 ReadableStream*
 ReadableStream::createDefaultStream(JSContext* cx, HandleValue underlyingSource,
-                                    HandleValue size, HandleValue highWaterMark)
+                                    HandleValue size, HandleValue highWaterMark,
+                                    HandleObject proto /* = nullptr */)
 {
-
+    // Steps 1-4.
     Rooted<ReadableStream*> stream(cx, createStream(cx));
     if (!stream)
         return nullptr;
 
-    // Step b: Set this.[[readableStreamController]] to
-    //         ? Construct(ReadableStreamDefaultController,
-    //                     « this, underlyingSource, size,
-    //                       highWaterMark »).
+    // Step 8.b: Set this.[[readableStreamController]] to
+    //           ? Construct(ReadableStreamDefaultController,
+    //                       « this, underlyingSource, size,
+    //                         highWaterMark »).
     RootedObject controller(cx, CreateReadableStreamDefaultController(cx, stream,
                                                                       underlyingSource,
                                                                       size,
                                                                       highWaterMark));
     if (!controller)
         return nullptr;
 
     stream->setFixedSlot(StreamSlot_Controller, ObjectValue(*controller));
@@ -654,37 +692,60 @@ ReadableStream::createDefaultStream(JSCo
 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)
+                                 HandleValue highWaterMark, HandleObject proto /* = nullptr */)
 {
-
-    Rooted<ReadableStream*> stream(cx, createStream(cx));
+    // Steps 1-4.
+    Rooted<ReadableStream*> stream(cx, createStream(cx, proto));
     if (!stream)
         return nullptr;
 
-    // Step b: Set this.[[readableStreamController]] to
-    //         ? Construct(ReadableByteStreamController, « this, underlyingSource,
-    //                     highWaterMark »).
+    // Step 7.b: Set this.[[readableStreamController]] to
+    //           ? Construct(ReadableByteStreamController,
+    //                       « this, underlyingSource, highWaterMark »).
     RootedObject controller(cx, CreateReadableByteStreamController(cx, stream,
                                                                    underlyingSource,
                                                                    highWaterMark));
     if (!controller)
         return nullptr;
 
     stream->setFixedSlot(StreamSlot_Controller, ObjectValue(*controller));
 
     return stream;
 }
 
+static MOZ_MUST_USE ReadableByteStreamController*
+CreateReadableByteStreamController(JSContext* cx, Handle<ReadableStream*> stream,
+                                   void* underlyingSource);
+
+ReadableStream*
+ReadableStream::createExternalSourceStream(JSContext* cx, void* underlyingSource,
+                                           uint8_t flags, HandleObject proto /* = nullptr */)
+{
+    Rooted<ReadableStream*> stream(cx, createStream(cx, proto));
+    if (!stream)
+        return nullptr;
+
+    RootedNativeObject controller(cx, CreateReadableByteStreamController(cx, stream,
+                                                                         underlyingSource));
+    if (!controller)
+        return nullptr;
+
+    stream->setFixedSlot(StreamSlot_Controller, ObjectValue(*controller));
+    AddControllerFlags(controller, flags << ControllerEmbeddingFlagsOffset);
+
+    return stream;
+}
+
 // Streams spec, 3.2.3.
 bool
 ReadableStream::constructor(JSContext* cx, unsigned argc, Value* vp)
 {
     CallArgs args = CallArgsFromVp(argc, vp);
 
     RootedValue val(cx, args.get(0));
     RootedValue underlyingSource(cx, args.get(0));
@@ -720,67 +781,67 @@ ReadableStream::constructor(JSContext* c
     RootedString type(cx, ToString<CanGC>(cx, typeVal));
     if (!type)
         return false;
 
     int32_t notByteStream;
     if (!CompareStrings(cx, type, cx->names().bytes, &notByteStream))
         return false;
 
-    // Step 7 & 8.a (reordered): If highWaterMark is undefined, let
-    //                           highWaterMark be 1 (or 0 for byte streams).
+    // Step 7.a & 8.a (reordered): If highWaterMark is undefined, let
+    //                             highWaterMark be 1 (or 0 for byte streams).
     if (highWaterMark.isUndefined())
         highWaterMark = Int32Value(notByteStream ? 1 : 0);
 
     Rooted<ReadableStream*> stream(cx);
 
     // Step 7: If typeString is "bytes",
     if (!notByteStream) {
+        // Step 7.b: Set this.[[readableStreamController]] to
+        //           ? Construct(ReadableByteStreamController,
+        //                       « this, underlyingSource, highWaterMark »).
         stream = createByteStream(cx, underlyingSource, highWaterMark);
     } else if (typeVal.isUndefined()) {
         // Step 8: Otherwise, if type is undefined,
+        // Step 8.b: Set this.[[readableStreamController]] to
+        //           ? Construct(ReadableStreamDefaultController,
+        //                       « this, underlyingSource, size, highWaterMark »).
         stream = createDefaultStream(cx, underlyingSource, size, highWaterMark);
     } else {
         // Step 9: Otherwise, throw a RangeError exception.
         JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
                                   JSMSG_READABLESTREAM_UNDERLYINGSOURCE_TYPE_WRONG);
         return false;
     }
     if (!stream)
         return false;
 
     args.rval().setObject(*stream);
     return true;
 }
 
-static MOZ_ALWAYS_INLINE bool
-IsReadableStreamLocked(ReadableStream* stream);
-
 // Streams spec, 3.2.4.1. get locked
 static MOZ_MUST_USE bool
 ReadableStream_locked_impl(JSContext* cx, const CallArgs& args)
 {
     Rooted<ReadableStream*> stream(cx, &args.thisv().toObject().as<ReadableStream>());
 
     // Step 2: Return ! IsReadableStreamLocked(this).
-    args.rval().setBoolean(IsReadableStreamLocked(stream));
+    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);
 }
 
-static MOZ_MUST_USE JSObject*
-ReadableStreamCancel(JSContext* cx, Handle<ReadableStream*> stream, HandleValue reason);
-
 // 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())) {
@@ -788,24 +849,24 @@ ReadableStream_cancel(JSContext* cx, uns
                           nullptr, "cancel", "");
         return ReturnPromiseRejectedWithPendingError(cx, args);
     }
 
     Rooted<ReadableStream*> stream(cx, &args.thisv().toObject().as<ReadableStream>());
 
     // Step 2: If ! IsReadableStreamLocked(this) is true, return a promise
     //         rejected with a TypeError exception.
-    if (IsReadableStreamLocked(stream)) {
+    if (stream->locked()) {
         JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
                                   JSMSG_READABLESTREAM_NOT_LOCKED, "cancel");
         return ReturnPromiseRejectedWithPendingError(cx, args);
     }
 
     // Step 3: Return ! ReadableStreamCancel(this, reason).
-    RootedObject cancelPromise(cx, ReadableStreamCancel(cx, stream, args.get(0)));
+    RootedObject cancelPromise(cx, ReadableStream::cancel(cx, stream, args.get(0)));
     if (!cancelPromise)
         return false;
     args.rval().setObject(*cancelPromise);
     return true;
 }
 
 static MOZ_MUST_USE ReadableStreamDefaultReader*
 CreateReadableStreamDefaultReader(JSContext* cx, Handle<ReadableStream*> stream);
@@ -934,44 +995,48 @@ static const JSFunctionSpec ReadableStre
     JS_FS_END
 };
 
 static const JSPropertySpec ReadableStream_properties[] = {
     JS_PSG("locked", ReadableStream_locked, 0),
     JS_PS_END
 };
 
-CLASS_SPEC(ReadableStream, 0, StreamSlotCount, 0);
+CLASS_SPEC(ReadableStream, 0, StreamSlotCount, 0, 0, JS_NULL_CLASS_OPS);
 
 // Streams spec, 3.3.1. AcquireReadableStreamBYOBReader ( stream )
 // Always inlined.
 
 // Streams spec, 3.3.2. AcquireReadableStreamDefaultReader ( stream )
 // Always inlined.
 
 // Streams spec, 3.3.3. IsReadableStream ( x )
 // Using is<T> instead.
 
 // Streams spec, 3.3.4. IsReadableStreamDisturbed ( stream )
-static MOZ_ALWAYS_INLINE bool
-IsReadableStreamDisturbed(ReadableStream* stream)
-{
-    // Step 1: Assert: ! IsReadableStream(stream) is true (implicit).
-    // Step 2: Return stream.[[disturbed]].
-    return stream->disturbed();
-}
+// Using stream->disturbed() instead.
 
 // Streams spec, 3.3.5. IsReadableStreamLocked ( stream )
-static MOZ_ALWAYS_INLINE bool
-IsReadableStreamLocked(ReadableStream* stream)
+bool
+ReadableStream::locked() const
 {
     // Step 1: Assert: ! IsReadableStream(stream) is true (implicit).
     // Step 2: If stream.[[reader]] is undefined, return false.
     // Step 3: Return true.
-    return !stream->getFixedSlot(StreamSlot_Reader).isUndefined();
+    // Special-casing for streams with external sources. Those can be locked
+    // explicitly via JSAPI, which is indicated by a controller flag.
+    // IsReadableStreamLocked is called from the controller's constructor, at
+    // which point we can't yet call ControllerFromStream(stream), but the
+    // source also can't be locked yet.
+    if (HasController(this) &&
+        (ControllerFlags(ControllerFromStream(this)) & ControllerFlag_SourceLocked))
+    {
+        return true;
+    }
+    return HasReader(this);
 }
 
 static MOZ_MUST_USE bool
 ReadableStreamDefaultControllerClose(JSContext* cx,
                                      Handle<ReadableStreamDefaultController*> controller);
 static MOZ_MUST_USE bool
 ReadableStreamDefaultControllerEnqueue(JSContext* cx,
                                        Handle<ReadableStreamDefaultController*> controller,
@@ -1054,32 +1119,29 @@ TeeReaderReadHandler(JSContext* cx, unsi
             return false;
     }
 
     args.rval().setUndefined();
     return true;
 }
 
 static MOZ_MUST_USE JSObject*
-ReadableStreamDefaultReaderRead(JSContext* cx, HandleNativeObject reader);
-
-static MOZ_MUST_USE JSObject*
 ReadableStreamTee_Pull(JSContext* cx, Handle<TeeState*> teeState,
                        Handle<ReadableStream*> branchStream)
 {
     // Step 1: Let reader be F.[[reader]], branch1 be F.[[branch1]],
     //         branch2 be F.[[branch2]], teeState be F.[[teeState]], and
     //         cloneForBranch2 be F.[[cloneForBranch2]].
 
     // Step 2: Return the result of transforming
     //         ! ReadableStreamDefaultReaderRead(reader) by a fulfillment
     //         handler which takes the argument result and performs the
     //         following steps:
     Rooted<ReadableStreamDefaultReader*> reader(cx, teeState->reader());
-    RootedObject readPromise(cx, ReadableStreamDefaultReaderRead(cx, reader));
+    RootedObject readPromise(cx, ReadableStreamDefaultReader::read(cx, reader));
     if (!readPromise)
         return nullptr;
 
     RootedObject onFulfilled(cx, NewHandler(cx, TeeReaderReadHandler, teeState));
     if (!onFulfilled)
         return nullptr;
 
     return JS::CallOriginalPromiseThen(cx, readPromise, onFulfilled, nullptr);
@@ -1117,17 +1179,17 @@ ReadableStreamTee_Cancel(JSContext* cx, 
         compositeReason->setDenseInitializedLength(2);
         compositeReason->initDenseElement(0, teeState->reason1());
         compositeReason->initDenseElement(1, teeState->reason2());
         RootedValue compositeReasonVal(cx, ObjectValue(*compositeReason));
 
         Rooted<PromiseObject*> promise(cx, teeState->promise());
 
         // Step b: Let cancelResult be ! ReadableStreamCancel(stream, compositeReason).
-        RootedObject cancelResult(cx, ReadableStreamCancel(cx, stream, compositeReasonVal));
+        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;
@@ -1312,35 +1374,35 @@ ReadableStreamAddReadRequest(JSContext* 
   val = ObjectValue(*promise);
   if (!AppendToList(cx, readRequests, val))
       return nullptr;
 
   // Step 6: Return promise.
   return promise;
 }
 
-static MOZ_MUST_USE bool
-ReadableStreamClose(JSContext* cx, Handle<ReadableStream*> stream);
-
 static MOZ_MUST_USE JSObject*
 ReadableStreamControllerCancelSteps(JSContext* cx,
                                     HandleNativeObject controller, HandleValue reason);
 
 // Used for transforming the result of promise fulfillment/rejection.
 static bool
 ReturnUndefined(JSContext* cx, unsigned argc, Value* vp)
 {
     CallArgs args = CallArgsFromVp(argc, vp);
     args.rval().setUndefined();
     return true;
 }
 
+MOZ_MUST_USE bool
+ReadableStreamCloseInternal(JSContext* cx, Handle<ReadableStream*> stream);
+
 // Streams spec, 3.4.3. ReadableStreamCancel ( stream, reason )
-static MOZ_MUST_USE JSObject*
-ReadableStreamCancel(JSContext* cx, Handle<ReadableStream*> stream, HandleValue reason)
+/* static */ MOZ_MUST_USE JSObject*
+ReadableStream::cancel(JSContext* cx, Handle<ReadableStream*> stream, HandleValue reason)
 {
     // Step 1: Set stream.[[disturbed]] to true.
     uint32_t state = StreamState(stream) | ReadableStream::Disturbed;
     SetStreamState(stream, state);
 
     // Step 2: If stream.[[state]] is "closed", return a new promise resolved
     //         with undefined.
     if (stream->closed())
@@ -1349,40 +1411,39 @@ ReadableStreamCancel(JSContext* cx, Hand
     // Step 3: If stream.[[state]] is "errored", return a new promise rejected
     //         with stream.[[storedError]].
     if (stream->errored()) {
         RootedValue storedError(cx, stream->getFixedSlot(StreamSlot_StoredError));
         return PromiseObject::unforgeableReject(cx, storedError);
     }
 
     // Step 4: Perform ! ReadableStreamClose(stream).
-    if (!ReadableStreamClose(cx, stream))
+    if (!ReadableStreamCloseInternal(cx, stream))
         return nullptr;
 
     // Step 5: Let sourceCancelPromise be
     //         ! stream.[[readableStreamController]].[[CancelSteps]](reason).
     RootedNativeObject controller(cx, ControllerFromStream(stream));
     RootedObject sourceCancelPromise(cx);
     sourceCancelPromise = ReadableStreamControllerCancelSteps(cx, controller, reason);
     if (!sourceCancelPromise)
         return nullptr;
 
     // Step 6: Return the result of transforming sourceCancelPromise by a
     //         fulfillment handler that returns undefined.
     RootedAtom funName(cx, cx->names().empty);
-    RootedFunction returnUndefined(cx,
-                                   NewNativeFunction(cx, ReturnUndefined, 0, funName));
+    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 )
-static MOZ_MUST_USE bool
-ReadableStreamClose(JSContext* cx, Handle<ReadableStream*> stream)
+MOZ_MUST_USE bool
+ReadableStreamCloseInternal(JSContext* cx, Handle<ReadableStream*> stream)
 {
   // Step 1: Assert: stream.[[state]] is "readable".
   MOZ_ASSERT(stream->readable());
 
   uint32_t state = StreamState(stream);
   // Step 2: Set stream.[[state]] to "closed".
   SetStreamState(stream, (state & ReadableStream::Disturbed) | ReadableStream::Closed);
 
@@ -1420,22 +1481,33 @@ ReadableStreamClose(JSContext* cx, Handl
           // 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());
-  return ResolvePromise(cx, closedPromise, UndefinedHandleValue);
+  if (!ResolvePromise(cx, closedPromise, UndefinedHandleValue))
+      return false;
+
+  if (stream->mode() == JS::ReadableStreamMode::ExternalSource &&
+      cx->runtime()->readableStreamClosedCallback)
+  {
+      NativeObject* controller = ControllerFromStream(stream);
+      void* source = controller->getFixedSlot(ControllerSlot_UnderlyingSource).toPrivate();
+      cx->runtime()->readableStreamClosedCallback(cx, stream, source, stream->embeddingFlags());
+  }
+
+  return true;
 }
 
 // Streams spec, 3.4.5. ReadableStreamError ( stream, e )
-static MOZ_MUST_USE bool
-ReadableStreamError(JSContext* cx, Handle<ReadableStream*> stream, HandleValue e)
+MOZ_MUST_USE bool
+ReadableStreamErrorInternal(JSContext* cx, Handle<ReadableStream*> stream, HandleValue e)
 {
     // Step 1: Assert: ! IsReadableStream(stream) is true (implicit).
 
     // Step 2: Assert: stream.[[state]] is "readable".
     MOZ_ASSERT(stream->readable());
 
     // Step 3: Set stream.[[state]] to "errored".
     uint32_t state = StreamState(stream);
@@ -1469,17 +1541,29 @@ ReadableStreamError(JSContext* cx, Handl
 
     // 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>());
-    return PromiseObject::reject(cx, closedPromise, e);
+    if (!PromiseObject::reject(cx, closedPromise, e))
+        return false;
+
+    if (stream->mode() == JS::ReadableStreamMode::ExternalSource &&
+        cx->runtime()->readableStreamErroredCallback)
+    {
+        NativeObject* controller = ControllerFromStream(stream);
+        void* source = controller->getFixedSlot(ControllerSlot_UnderlyingSource).toPrivate();
+        cx->runtime()->readableStreamErroredCallback(cx, stream, source,
+                                                     stream->embeddingFlags(), e);
+    }
+
+    return true;
 }
 
 // Streams spec, 3.4.6. ReadableStreamFulfillReadIntoRequest( stream, chunk, done )
 // Streams spec, 3.4.7. ReadableStreamFulfillReadRequest ( stream, chunk, done )
 // These two spec functions are identical in our implementation.
 static MOZ_MUST_USE bool
 ReadableStreamFulfillReadOrReadIntoRequest(JSContext* cx, Handle<ReadableStream*> stream,
                                            HandleValue chunk, bool done)
@@ -1507,26 +1591,23 @@ ReadableStreamFulfillReadOrReadIntoReque
     val = ObjectValue(*iterResult);
     return PromiseObject::resolve(cx, readIntoRequest, val);
 }
 
 // Streams spec, 3.4.8. ReadableStreamGetNumReadIntoRequests ( stream )
 // Streams spec, 3.4.9. ReadableStreamGetNumReadRequests ( stream )
 // (Identical implementation.)
 static uint32_t
-ReadableStreamGetNumReadRequests(NativeObject* stream)
+ReadableStreamGetNumReadRequests(ReadableStream* stream)
 {
-    MOZ_ASSERT(stream->is<ReadableStream>());
-
     // Step 1: Return the number of elements in
     //         stream.[[reader]].[[readRequests]].
-    Value readerVal = stream->getFixedSlot(StreamSlot_Reader);
-    NativeObject* reader = &readerVal.toObject().as<NativeObject>();
-    MOZ_ASSERT(reader->is<ReadableStreamDefaultReader>() ||
-               reader->is<ReadableStreamBYOBReader>());
+    if (!HasReader(stream))
+        return 0;
+    NativeObject* reader = ReaderFromStream(stream);
     Value readRequests = reader->getFixedSlot(ReaderSlot_Requests);
     return readRequests.toObject().as<NativeObject>().getDenseInitializedLength();
 }
 
 // Stream spec 3.4.10. ReadableStreamHasBYOBReader ( stream )
 static MOZ_MUST_USE bool
 ReadableStreamHasBYOBReader(ReadableStream* stream)
 {
@@ -1562,17 +1643,17 @@ CreateReadableStreamDefaultReader(JSCont
 {
     Rooted<ReadableStreamDefaultReader*> reader(cx);
     reader = NewBuiltinClassInstance<ReadableStreamDefaultReader>(cx);
     if (!reader)
         return nullptr;
 
     // Step 2: If ! IsReadableStreamLocked(stream) is true, throw a TypeError
     //         exception.
-    if (IsReadableStreamLocked(stream)) {
+    if (stream->locked()) {
         JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
                                   JSMSG_READABLESTREAM_LOCKED);
         return nullptr;
     }
 
     // Step 3: Perform ! ReadableStreamReaderGenericInitialize(this, stream).
     if (!ReadableStreamReaderGenericInitialize(cx, reader, stream))
         return nullptr;
@@ -1639,17 +1720,17 @@ ReadableStreamDefaultReader_cancel(JSCon
     // Step 1: If ! IsReadableStreamDefaultReader(this) is false, return a promise
     //         rejected with a TypeError exception.
     if (!Is<ReadableStreamDefaultReader>(args.thisv()))
         return RejectNonGenericMethod(cx, args, "ReadableStreamDefaultReader", "cancel");
 
     // Step 2: If this.[[ownerReadableStream]] is undefined, return a promise
     //         rejected with a TypeError exception.
     RootedNativeObject reader(cx, &args.thisv().toObject().as<NativeObject>());
-    if (reader->getFixedSlot(ReaderSlot_Stream).isUndefined()) {
+    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)
@@ -1666,25 +1747,26 @@ ReadableStreamDefaultReader_read(JSConte
 
     // Step 1: If ! IsReadableStreamDefaultReader(this) is false, return a promise
     //         rejected with a TypeError exception.
     if (!Is<ReadableStreamDefaultReader>(args.thisv()))
         return RejectNonGenericMethod(cx, args, "ReadableStreamDefaultReader", "read");
 
     // Step 2: If this.[[ownerReadableStream]] is undefined, return a promise
     //         rejected with a TypeError exception.
-    RootedNativeObject reader(cx, &args.thisv().toObject().as<NativeObject>());
-    if (reader->getFixedSlot(ReaderSlot_Stream).isUndefined()) {
+    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 = ReadableStreamDefaultReaderRead(cx, reader);
+    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);
@@ -1692,17 +1774,17 @@ ReadableStreamReaderGenericRelease(JSCon
 // Streams spec, 3.5.4.4. releaseLock ( )
 static MOZ_MUST_USE bool
 ReadableStreamDefaultReader_releaseLock_impl(JSContext* cx, const CallArgs& args)
 {
     Rooted<ReadableStreamDefaultReader*> reader(cx);
     reader = &args.thisv().toObject().as<ReadableStreamDefaultReader>();
 
     // Step 2: If this.[[ownerReadableStream]] is undefined, return.
-    if (reader->getFixedSlot(ReaderSlot_Stream).isUndefined()) {
+    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>();
@@ -1736,35 +1818,37 @@ static const JSFunctionSpec ReadableStre
     JS_FS_END
 };
 
 static const JSPropertySpec ReadableStreamDefaultReader_properties[] = {
     JS_PSG("closed", ReadableStreamDefaultReader_closed, 0),
     JS_PS_END
 };
 
-CLASS_SPEC(ReadableStreamDefaultReader, 1, ReaderSlotCount, ClassSpec::DontDefineConstructor);
+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);
+                                  JSMSG_READABLESTREAM_NOT_BYTE_STREAM_CONTROLLER,
+                                  "ReadableStream.getReader('byob')");
         return nullptr;
     }
 
     // Step 3: If ! IsReadableStreamLocked(stream) is true, throw a TypeError
     //         exception.
-    if (IsReadableStreamLocked(stream)) {
+    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;
@@ -1830,78 +1914,75 @@ ReadableStreamBYOBReader_cancel(JSContex
     // 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 (reader->getFixedSlot(ReaderSlot_Stream).isUndefined()) {
+    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;
 }
 
-static MOZ_MUST_USE JSObject*
-ReadableStreamBYOBReaderRead(JSContext* cx, HandleNativeObject reader,
-                             Handle<TypedArrayObject*> view);
-
 // 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.
-    RootedNativeObject reader(cx, &args.thisv().toObject().as<NativeObject>());
-    if (reader->getFixedSlot(ReaderSlot_Stream).isUndefined()) {
+    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<TypedArrayObject*> view(cx, &viewVal.toObject().as<TypedArrayObject>());
+    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 (view->length() == 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 = ReadableStreamBYOBReaderRead(cx, reader, 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);
@@ -1909,17 +1990,17 @@ ReadableStreamReaderGenericRelease(JSCon
 // 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 (reader->getFixedSlot(ReaderSlot_Stream).isUndefined()) {
+    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>();
@@ -1952,17 +2033,17 @@ static const JSPropertySpec ReadableStre
 
 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);
+CLASS_SPEC(ReadableStreamBYOBReader, 1, 3, ClassSpec::DontDefineConstructor, 0, JS_NULL_CLASS_OPS);
 
 inline static MOZ_MUST_USE bool
 ReadableStreamControllerCallPullIfNeeded(JSContext* cx, HandleNativeObject controller);
 
 // Streams spec, 3.7.1. IsReadableStreamDefaultReader ( x )
 // Implemented via intrinsic_isInstanceOfBuiltin<ReadableStreamDefaultReader>()
 
 // Streams spec, 3.7.2. IsReadableStreamBYOBReader ( x )
@@ -1973,17 +2054,17 @@ static MOZ_MUST_USE JSObject*
 ReadableStreamReaderGenericCancel(JSContext* cx, HandleNativeObject reader, HandleValue reason)
 {
     // Step 1: Let stream be reader.[[ownerReadableStream]].
     Rooted<ReadableStream*> stream(cx, StreamFromReader(reader));
 
     // Step 2: Assert: stream is not undefined (implicit).
 
     // Step 3: Return ! ReadableStreamCancel(stream, reason).
-    return ReadableStreamCancel(cx, stream, reason);
+    return &ReadableStreamCancel(cx, stream, reason)->as<PromiseObject>();
 }
 
 // Streams spec, 3.7.4. ReadableStreamReaderGenericInitialize ( reader, stream )
 static MOZ_MUST_USE bool
 ReadableStreamReaderGenericInitialize(JSContext* cx, HandleNativeObject reader,
                                       Handle<ReadableStream*> stream)
 {
     // Step 1: Set reader.[[ownerReadableStream]] to stream.
@@ -2062,22 +2143,22 @@ ReadableStreamReaderGenericRelease(JSCon
     reader->setFixedSlot(ReaderSlot_Stream, UndefinedValue());
 
     return true;
 }
 
 static MOZ_MUST_USE JSObject*
 ReadableByteStreamControllerPullInto(JSContext* cx,
                                      Handle<ReadableByteStreamController*> controller,
-                                     HandleNativeObject view);
+                                     Handle<ArrayBufferViewObject*> view);
 
 // Streams spec, 3.7.6. ReadableStreamBYOBReaderRead ( reader, view )
-static MOZ_MUST_USE JSObject*
-ReadableStreamBYOBReaderRead(JSContext* cx, HandleNativeObject reader,
-                             Handle<TypedArrayObject*> view)
+/* static */ MOZ_MUST_USE JSObject*
+ReadableStreamBYOBReader::read(JSContext* cx, Handle<ReadableStreamBYOBReader*> reader,
+                               Handle<ArrayBufferViewObject*> view)
 {
     MOZ_ASSERT(reader->is<ReadableStreamBYOBReader>());
 
     // Step 1: Let stream be reader.[[ownerReadableStream]].
     // Step 2: Assert: stream is not undefined.
     Rooted<ReadableStream*> stream(cx, StreamFromReader(reader));
 
     // Step 3: Set stream.[[disturbed]] to true.
@@ -2095,21 +2176,19 @@ ReadableStreamBYOBReaderRead(JSContext* 
     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 )
-static MOZ_MUST_USE JSObject*
-ReadableStreamDefaultReaderRead(JSContext* cx, HandleNativeObject reader)
+MOZ_MUST_USE JSObject*
+ReadableStreamDefaultReader::read(JSContext* cx, Handle<ReadableStreamDefaultReader*> reader)
 {
-    MOZ_ASSERT(reader->is<ReadableStreamDefaultReader>());
-
     // Step 1: Let stream be reader.[[ownerReadableStream]].
     // Step 2: Assert: stream is not undefined.
     Rooted<ReadableStream*> stream(cx, StreamFromReader(reader));
 
     // Step 3: Set stream.[[disturbed]] to true.
     SetStreamState(stream, StreamState(stream) | ReadableStream::Disturbed);
 
     // Step 4: If stream.[[state]] is "closed", return a new promise resolved with
@@ -2292,22 +2371,23 @@ ReadableStreamDefaultController::constru
                            args.get(0));
         return false;
     }
 
     Rooted<ReadableStream*> stream(cx, &streamVal.toObject().as<ReadableStream>());
 
     // Step 2: If stream.[[readableStreamController]] is not undefined, throw a
     //         TypeError exception.
-    if (!stream->getFixedSlot(StreamSlot_Controller).isUndefined()) {
+    if (HasController(stream)) {
         JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
                                   JSMSG_READABLESTREAM_CONTROLLER_SET);
         return false;
     }
 
+    // Steps 3-11.
     RootedObject controller(cx, CreateReadableStreamDefaultController(cx, stream, args.get(1),
                                                                       args.get(2), args.get(3)));
     if (!controller)
         return false;
 
     args.rval().setObject(*controller);
     return true;
 }
@@ -2355,39 +2435,50 @@ ReadableStreamDefaultController_desiredS
     return CallNonGenericMethod<Is<ReadableStreamDefaultController>,
                                 ReadableStreamController_desiredSize_impl>(cx, args);
 }
 
 static MOZ_MUST_USE bool
 ReadableStreamDefaultControllerClose(JSContext* cx,
                                      Handle<ReadableStreamDefaultController*> controller);
 
-// Streams spec, 3.8.4.2 close()
+// Unified implementation of steps 2-3 of 3.8.4.2 and 3.10.4.3.
 static MOZ_MUST_USE bool
-ReadableStreamDefaultController_close_impl(JSContext* cx, const CallArgs& args)
+VerifyControllerStateForClosing(JSContext* cx, HandleNativeObject controller)
 {
-    Rooted<ReadableStreamDefaultController*> controller(cx);
-    controller = &args.thisv().toObject().as<ReadableStreamDefaultController>();
-
     // Step 2: If this.[[closeRequested]] is true, throw a TypeError exception.
     if (ControllerFlags(controller) & ControllerFlag_CloseRequested) {
         JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
                                   JSMSG_READABLESTREAMCONTROLLER_CLOSED, "close");
         return false;
     }
 
     // Step 3: If this.[[controlledReadableStream]].[[state]] is not "readable",
     //         throw a TypeError exception.
     ReadableStream* stream = StreamFromController(controller);
     if (!stream->readable()) {
         JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
                                   JSMSG_READABLESTREAMCONTROLLER_NOT_READABLE, "close");
         return false;
     }
 
+    return true;
+}
+
+// Streams spec, 3.8.4.2 close()
+static MOZ_MUST_USE bool
+ReadableStreamDefaultController_close_impl(JSContext* cx, const CallArgs& args)
+{
+    Rooted<ReadableStreamDefaultController*> controller(cx);
+    controller = &args.thisv().toObject().as<ReadableStreamDefaultController>();
+
+    // Steps 2-3.
+    if (!VerifyControllerStateForClosing(cx, controller))
+        return false;
+
     // Step 4: Perform ! ReadableStreamDefaultControllerClose(this).
     if (!ReadableStreamDefaultControllerClose(cx, controller))
         return false;
     args.rval().setUndefined();
     return true;
 }
 
 static bool
@@ -2450,22 +2541,20 @@ ReadableStreamDefaultController_enqueue(
 // Streams spec, 3.8.4.4. error ( e )
 static MOZ_MUST_USE bool
 ReadableStreamDefaultController_error_impl(JSContext* cx, const CallArgs& args)
 {
     Rooted<ReadableStreamDefaultController*> controller(cx);
     controller = &args.thisv().toObject().as<ReadableStreamDefaultController>();
 
     // Step 2: Let stream be this.[[controlledReadableStream]].
-    ReadableStream* stream = StreamFromController(controller);
-
     // Step 3: If stream.[[state]] is not "readable", throw a TypeError exception.
-    if (!stream->readable()) {
+    if (!StreamFromController(controller)->readable()) {
         JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
-                                  JSMSG_READABLESTREAMCONTROLLER_NOT_READABLE, "close");
+                                  JSMSG_READABLESTREAMCONTROLLER_NOT_READABLE, "error");
         return false;
     }
 
     // Step 4: Perform ! ReadableStreamDefaultControllerError(this, e).
     if (!ReadableStreamControllerError(cx, controller, args.get(0)))
         return false;
     args.rval().setUndefined();
     return true;
@@ -2489,17 +2578,18 @@ static const JSPropertySpec ReadableStre
 
 static const JSFunctionSpec ReadableStreamDefaultController_methods[] = {
     JS_FN("close",      ReadableStreamDefaultController_close,      0, 0),
     JS_FN("enqueue",    ReadableStreamDefaultController_enqueue,    1, 0),
     JS_FN("error",      ReadableStreamDefaultController_error,      1, 0),
     JS_FS_END
 };
 
-CLASS_SPEC(ReadableStreamDefaultController, 4, 7, ClassSpec::DontDefineConstructor);
+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 )
  */
@@ -2536,16 +2626,25 @@ ReadableStreamControllerCancelSteps(JSCo
 
     if (Is<TeeState>(underlyingSource)) {
         Rooted<TeeState*> teeState(cx, &underlyingSource.toObject().as<TeeState>());
         Rooted<ReadableStreamDefaultController*> defaultController(cx);
         defaultController = &controller->as<ReadableStreamDefaultController>();
         return ReadableStreamTee_Cancel(cx, teeState, defaultController, reason);
     }
 
+    if (ControllerFlags(controller) & ControllerFlag_ExternalSource) {
+        void* source = underlyingSource.toPrivate();
+        Rooted<ReadableStream*> stream(cx, StreamFromController(controller));
+        RootedValue rval(cx);
+        rval = cx->runtime()->readableStreamCancelCallback(cx, stream, source,
+                                                           stream->embeddingFlags(), reason);
+        return PromiseObject::unforgeableResolve(cx, rval);
+    }
+
     return PromiseInvokeOrNoop(cx, underlyingSource, cx->names().cancel, reason);
 }
 
 inline static MOZ_MUST_USE bool
 DequeueValue(JSContext* cx, HandleNativeObject container, MutableHandleValue chunk);
 
 // Streams spec, 3.8.5.2. ReadableStreamDefaultController [[PullSteps]]()
 static JSObject*
@@ -2567,18 +2666,18 @@ ReadableStreamDefaultControllerPullSteps
         RootedValue chunk(cx);
         if (!DequeueValue(cx, controller, &chunk))
             return nullptr;
 
         // Step b: If this.[[closeRequested]] is true and this.[[queue]] is empty,
         //         perform ! ReadableStreamClose(stream).
         bool closeRequested = ControllerFlags(controller) & ControllerFlag_CloseRequested;
         if (closeRequested && queue->getDenseInitializedLength() == 0) {
-          if (!ReadableStreamClose(cx, stream))
-              return nullptr;
+            if (!ReadableStreamCloseInternal(cx, stream))
+                return nullptr;
         }
 
         // Step c: Otherwise, perform ! ReadableStreamDefaultControllerCallPullIfNeeded(this).
         else {
         if (!ReadableStreamControllerCallPullIfNeeded(cx, controller))
             return nullptr;
         }
 
@@ -2645,16 +2744,19 @@ ControllerPullFailedHandler(JSContext* c
 
     args.rval().setUndefined();
     return true;
 }
 
 static bool
 ReadableStreamControllerShouldCallPull(NativeObject* controller);
 
+static MOZ_MUST_USE double
+ReadableStreamControllerGetDesiredSizeUnchecked(NativeObject* controller);
+
 // Streams spec, 3.9.2 ReadableStreamDefaultControllerCallPullIfNeeded ( controller )
 // and
 // Streams spec, 3.12.3. ReadableByteStreamControllerCallPullIfNeeded ( controller )
 inline static MOZ_MUST_USE bool
 ReadableStreamControllerCallPullIfNeeded(JSContext* cx, HandleNativeObject controller)
 {
     // Step 1: Let shouldPull be
     //         ! ReadableByteStreamControllerShouldCallPull(controller).
@@ -2685,16 +2787,23 @@ ReadableStreamControllerCallPullIfNeeded
     RootedValue underlyingSource(cx);
     underlyingSource = controller->getFixedSlot(ControllerSlot_UnderlyingSource);
     RootedValue controllerVal(cx, ObjectValue(*controller));
 
     if (Is<TeeState>(underlyingSource)) {
         Rooted<TeeState*> teeState(cx, &underlyingSource.toObject().as<TeeState>());
         Rooted<ReadableStream*> stream(cx, StreamFromController(controller));
         pullPromise = ReadableStreamTee_Pull(cx, teeState, stream);
+    } else if (ControllerFlags(controller) & ControllerFlag_ExternalSource) {
+        void* source = underlyingSource.toPrivate();
+        Rooted<ReadableStream*> stream(cx, StreamFromController(controller));
+        double desiredSize = ReadableStreamControllerGetDesiredSizeUnchecked(controller);
+        cx->runtime()->readableStreamDataRequestCallback(cx, stream, source,
+                                                         stream->embeddingFlags(), desiredSize);
+        pullPromise = PromiseObject::unforgeableResolve(cx, UndefinedHandleValue);
     } else {
         pullPromise = PromiseInvokeOrNoop(cx, underlyingSource, cx->names().pull, controllerVal);
     }
     if (!pullPromise)
         return false;
 
     RootedObject onPullFulfilled(cx, NewHandler(cx, ControllerPullHandler, controller));
     if (!onPullFulfilled)
@@ -2732,17 +2841,17 @@ ReadableStreamControllerShouldCallPull(N
 
     // Step 4: If controller.[[started]] is false, return false.
     if (!(flags & ControllerFlag_Started))
         return false;
 
     // Step 5: If ! IsReadableStreamLocked(stream) is true and
     //         ! ReadableStreamGetNumReadRequests(stream) > 0, return true.
     // Steps 5-6 of 3.12.24 are equivalent in our implementation.
-    if (IsReadableStreamLocked(stream) && ReadableStreamGetNumReadRequests(stream) > 0)
+    if (stream->locked() && ReadableStreamGetNumReadRequests(stream) > 0)
         return true;
 
     // Step 6: Let desiredSize be ReadableStreamDefaultControllerGetDesiredSize(controller).
     double desiredSize = ReadableStreamControllerGetDesiredSizeUnchecked(controller);
 
     // Step 7: If desiredSize > 0, return true.
     // Step 8: Return false.
     // Steps 7-8 of 3.12.24 are equivalent in our implementation.
@@ -2765,26 +2874,22 @@ ReadableStreamDefaultControllerClose(JSC
 
     // Step 4: Set controller.[[closeRequested]] to true.
     AddControllerFlags(controller, ControllerFlag_CloseRequested);
 
     // Step 5: If controller.[[queue]] is empty, perform ! ReadableStreamClose(stream).
     RootedNativeObject queue(cx);
     queue = &controller->getFixedSlot(QueueContainerSlot_Queue).toObject().as<NativeObject>();
     if (queue->getDenseInitializedLength() == 0)
-        return ReadableStreamClose(cx, stream);
+        return ReadableStreamCloseInternal(cx, stream);
 
     return true;
 }
 
 static MOZ_MUST_USE bool
-ReadableStreamFulfillReadOrReadIntoRequest(JSContext* cx, Handle<ReadableStream*> stream,
-                                           HandleValue chunk, bool done);
-
-static MOZ_MUST_USE bool
 EnqueueValueWithSize(JSContext* cx, HandleNativeObject container, HandleValue value,
                      HandleValue sizeVal);
 
 // Streams spec, 3.9.5. ReadableStreamDefaultControllerEnqueue ( controller, chunk )
 static MOZ_MUST_USE bool
 ReadableStreamDefaultControllerEnqueue(JSContext* cx,
                                        Handle<ReadableStreamDefaultController*> controller,
                                        HandleValue chunk)
@@ -2796,17 +2901,17 @@ ReadableStreamDefaultControllerEnqueue(J
     MOZ_ASSERT(!(ControllerFlags(controller) & ControllerFlag_CloseRequested));
 
     // Step 3: Assert: stream.[[state]] is "readable".
     MOZ_ASSERT(stream->readable());
 
     // Step 4: If ! IsReadableStreamLocked(stream) is true and
     //         ! ReadableStreamGetNumReadRequests(stream) > 0, perform
     //         ! ReadableStreamFulfillReadRequest(stream, chunk, false).
-    if (IsReadableStreamLocked(stream) && ReadableStreamGetNumReadRequests(stream) > 0) {
+    if (stream->locked() && ReadableStreamGetNumReadRequests(stream) > 0) {
         if (!ReadableStreamFulfillReadOrReadIntoRequest(cx, stream, chunk, false))
             return false;
     } else {
         // Step 5: Otherwise,
         // Step a: Let chunkSize be 1.
         RootedValue chunkSize(cx, NumberValue(1));
         bool success = true;
 
@@ -2846,19 +2951,16 @@ ReadableStreamDefaultControllerEnqueue(J
     if (!ReadableStreamControllerCallPullIfNeeded(cx, controller))
         return false;
 
     // Step 7: Return.
     return true;
 }
 
 static MOZ_MUST_USE bool
-ReadableStreamError(JSContext* cx, Handle<ReadableStream*> stream, HandleValue e);
-
-static MOZ_MUST_USE bool
 ReadableByteStreamControllerClearPendingPullIntos(JSContext* cx, HandleNativeObject controller);
 
 // Streams spec, 3.9.6. ReadableStreamDefaultControllerError ( controller, e )
 // and
 // Streams spec, 3.12.10. ReadableByteStreamControllerError ( controller, e )
 static MOZ_MUST_USE bool
 ReadableStreamControllerError(JSContext* cx, HandleNativeObject controller, HandleValue e)
 {
@@ -2879,17 +2981,17 @@ ReadableStreamControllerError(JSContext*
             return false;
     }
 
     // Step 3 (or 4): Perform ! ResetQueue(controller).
     if (!ResetQueue(cx, controller))
         return false;
 
     // Step 4 (or 5): Perform ! ReadableStreamError(stream, e).
-    return ReadableStreamError(cx, stream, e);
+    return ReadableStreamErrorInternal(cx, stream, e);
 }
 
 // Streams spec, 3.9.7. ReadableStreamDefaultControllerErrorIfNeeded ( controller, e ) nothrow
 static MOZ_MUST_USE bool
 ReadableStreamDefaultControllerErrorIfNeeded(JSContext* cx,
                                              Handle<ReadableStreamDefaultController*> controller,
                                              HandleValue e)
 {
@@ -3010,16 +3112,21 @@ CreateReadableByteStreamController(JSCon
         return nullptr;
 
     if (!JS::AddPromiseReactions(cx, startPromise, onStartFulfilled, onStartRejected))
         return nullptr;
 
     return controller;
 }
 
+bool
+ReadableByteStreamController::hasExternalSource() {
+    return ControllerFlags(this) & ControllerFlag_ExternalSource;
+}
+
 // Streams spec, 3.10.3.
 // new ReadableByteStreamController ( stream, underlyingByteSource,
 //                                    highWaterMark )
 bool
 ReadableByteStreamController::constructor(JSContext* cx, unsigned argc, Value* vp)
 {
     CallArgs args = CallArgsFromVp(argc, vp);
 
@@ -3033,31 +3140,96 @@ ReadableByteStreamController::constructo
                            args.get(0));
         return false;
     }
 
     Rooted<ReadableStream*> stream(cx, &streamVal.toObject().as<ReadableStream>());
 
     // Step 2: If stream.[[readableStreamController]] is not undefined, throw a
     //         TypeError exception.
-    if (!stream->getFixedSlot(StreamSlot_Controller).isUndefined()) {
+    if (HasController(stream)) {
         JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
                                   JSMSG_READABLESTREAM_CONTROLLER_SET);
         return false;
     }
 
     RootedObject controller(cx, CreateReadableByteStreamController(cx, stream, args.get(1),
                                                                    args.get(2)));
     if (!controller)
         return false;
 
     args.rval().setObject(*controller);
     return true;
 }
 
+// Version of the ReadableByteStreamConstructor that's specialized for
+// handling external, embedding-provided, underlying sources.
+static MOZ_MUST_USE ReadableByteStreamController*
+CreateReadableByteStreamController(JSContext* cx, Handle<ReadableStream*> stream,
+                                   void* underlyingSource)
+{
+    Rooted<ReadableByteStreamController*> controller(cx);
+    controller = NewBuiltinClassInstance<ReadableByteStreamController>(cx);
+    if (!controller)
+        return nullptr;
+
+    // Step 3: Set this.[[controlledReadableStream]] to stream.
+    controller->setFixedSlot(ControllerSlot_Stream, ObjectValue(*stream));
+
+    // Step 4: Set this.[[underlyingByteSource]] to underlyingByteSource.
+    controller->setFixedSlot(ControllerSlot_UnderlyingSource, PrivateValue(underlyingSource));
+
+    // Step 5: Set this.[[pullAgain]], and this.[[pulling]] to false.
+    controller->setFixedSlot(ControllerSlot_Flags, Int32Value(ControllerFlag_ExternalSource));
+
+    // Step 6: Perform ! ReadableByteStreamControllerClearPendingPullIntos(this).
+    // Omitted.
+
+    // Step 7: Perform ! ResetQueue(this).
+    controller->setFixedSlot(QueueContainerSlot_TotalSize, Int32Value(0));
+
+    // Step 8: Set this.[[started]] and this.[[closeRequested]] to false.
+    // Step 9: Set this.[[strategyHWM]] to
+    //         ? ValidateAndNormalizeHighWaterMark(highWaterMark).
+    controller->setFixedSlot(ControllerSlot_StrategyHWM, Int32Value(0));
+
+    // Step 10: Let autoAllocateChunkSize be
+    //          ? GetV(underlyingByteSource, "autoAllocateChunkSize").
+    // Step 11: If autoAllocateChunkSize is not undefined,
+    // Step 12: Set this.[[autoAllocateChunkSize]] to autoAllocateChunkSize.
+    // Omitted.
+
+    // Step 13: Set this.[[pendingPullIntos]] to a new empty List.
+    if (!SetNewList(cx, controller, ByteControllerSlot_PendingPullIntos))
+        return nullptr;
+
+    // Step 14: Let controller be this (implicit).
+    // Step 15: Let startResult be
+    //          ? InvokeOrNoop(underlyingSource, "start", « this »).
+    // Omitted.
+
+    // Step 16: Let startPromise be a promise resolved with startResult:
+    RootedObject startPromise(cx, PromiseObject::unforgeableResolve(cx, UndefinedHandleValue));
+    if (!startPromise)
+        return nullptr;
+
+    RootedObject onStartFulfilled(cx, NewHandler(cx, ControllerStartHandler, controller));
+    if (!onStartFulfilled)
+        return nullptr;
+
+    RootedObject onStartRejected(cx, NewHandler(cx, ControllerStartFailedHandler, controller));
+    if (!onStartRejected)
+        return nullptr;
+
+    if (!JS::AddPromiseReactions(cx, startPromise, onStartFulfilled, onStartRejected))
+        return nullptr;
+
+    return controller;
+}
+
 static MOZ_MUST_USE ReadableStreamBYOBRequest*
 CreateReadableStreamBYOBRequest(JSContext* cx, Handle<ReadableByteStreamController*> controller,
                                 HandleObject view);
 
 // Streams spec, 3.10.4.1. get byobRequest
 static MOZ_MUST_USE bool
 ReadableByteStreamController_byobRequest_impl(JSContext* cx, const CallArgs& args)
 {
@@ -3130,30 +3302,19 @@ ReadableByteStreamControllerClose(JSCont
 
 // 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>();
 
-    // 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");
+    // Steps 2-3.
+    if (!VerifyControllerStateForClosing(cx, controller))
         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, "close");
-        return false;
-    }
 
     // Step 4: Perform ? ReadableByteStreamControllerClose(this).
     if (!ReadableByteStreamControllerClose(cx, controller))
         return false;
     args.rval().setUndefined();
     return true;
 }
 
@@ -3195,17 +3356,18 @@ ReadableByteStreamController_enqueue_imp
         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);
+                                  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();
@@ -3263,17 +3425,52 @@ static const JSPropertySpec ReadableByte
 
 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
 };
 
-CLASS_SPEC(ReadableByteStreamController, 3, 9, ClassSpec::DontDefineConstructor);
+static void
+ReadableByteStreamControllerFinalize(FreeOp* fop, JSObject* obj)
+{
+    ReadableByteStreamController& controller = obj->as<ReadableByteStreamController>();
+
+    if (controller.getFixedSlot(ControllerSlot_Flags).isUndefined())
+        return;
+
+    uint32_t flags = ControllerFlags(&controller);
+    if (!(flags & ControllerFlag_ExternalSource))
+        return;
+
+    uint8_t embeddingFlags = flags >> ControllerEmbeddingFlagsOffset;
+
+    void* underlyingSource = controller.getFixedSlot(ControllerSlot_UnderlyingSource).toPrivate();
+    obj->runtimeFromAnyThread()->readableStreamFinalizeCallback(underlyingSource, embeddingFlags);
+}
+
+static const ClassOps ReadableByteStreamControllerClassOps = {
+    nullptr,        /* addProperty */
+    nullptr,        /* delProperty */
+    nullptr,        /* getProperty */
+    nullptr,        /* setProperty */
+    nullptr,        /* enumerate */
+    nullptr,        /* newEnumerate */
+    nullptr,        /* resolve */
+    nullptr,        /* mayResolve */
+    ReadableByteStreamControllerFinalize,
+    nullptr,        /* call        */
+    nullptr,        /* hasInstance */
+    nullptr,        /* construct   */
+    nullptr,        /* trace   */
+};
+
+CLASS_SPEC(ReadableByteStreamController, 3, 9, ClassSpec::DontDefineConstructor,
+           JSCLASS_BACKGROUND_FINALIZE, &ReadableByteStreamControllerClassOps);
 
 // Streams spec, 3.10.5.1. [[PullSteps]] ()
 // Unified with 3.8.5.1 above.
 
 static MOZ_MUST_USE bool
 ReadableByteStreamControllerHandleQueueDrain(JSContext* cx, HandleNativeObject controller);
 
 // Streams spec, 3.10.5.2. [[PullSteps]] ()
@@ -3288,42 +3485,71 @@ ReadableByteStreamControllerPullSteps(JS
 
     RootedValue val(cx);
     // Step 3: If this.[[queueTotalSize]] > 0,
     double queueTotalSize = controller->getFixedSlot(QueueContainerSlot_TotalSize).toNumber();
     if (queueTotalSize > 0) {
         // Step 3.a: MOZ_ASSERT: ! ReadableStreamGetNumReadRequests(_stream_) is 0.
         MOZ_ASSERT(ReadableStreamGetNumReadRequests(stream) == 0);
 
-        // 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);
-
-        // Step 3.d: Set this.[[queueTotalSize]] to this.[[queueTotalSize]] − entry.[[byteLength]].
-        uint32_t byteLength = entry->byteLength();
-        queueTotalSize = queueTotalSize - byteLength;
-        controller->setFixedSlot(QueueContainerSlot_TotalSize, NumberValue(queueTotalSize));
+        RootedObject view(cx);
+
+        if (stream->mode() == JS::ReadableStreamMode::ExternalSource) {
+            val = controller->getFixedSlot(ControllerSlot_UnderlyingSource);
+            void* underlyingSource = val.toPrivate();
+
+            view = JS_NewUint8Array(cx, queueTotalSize);
+            if (!view)
+                return nullptr;
+
+            size_t bytesWritten;
+            {
+                JS::AutoSuppressGCAnalysis noGC(cx);
+                bool dummy;
+                void* buffer = JS_GetArrayBufferViewData(view, &dummy, noGC);
+                auto cb = cx->runtime()->readableStreamWriteIntoReadRequestCallback;
+                MOZ_ASSERT(cb);
+                // TODO: use bytesWritten to correctly update the request's state.
+                cb(cx, stream, underlyingSource, stream->embeddingFlags(), buffer,
+                   queueTotalSize, &bytesWritten);
+            }
+
+            queueTotalSize = queueTotalSize - bytesWritten;
+        } else {
+            // Step 3.b: Let entry be the first element of this.[[queue]].
+            // Step 3.c: Remove entry from this.[[queue]], shifting all other elements
+            //           downward (so that the second becomes the first, and so on).
+            val = controller->getFixedSlot(QueueContainerSlot_Queue);
+            RootedNativeObject queue(cx, &val.toObject().as<NativeObject>());
+            Rooted<ByteStreamChunk*> entry(cx, ShiftFromList<ByteStreamChunk>(cx, queue));
+            MOZ_ASSERT(entry);
+
+            queueTotalSize = queueTotalSize - entry->byteLength();
+
+            // Step 3.f: Let view be ! Construct(%Uint8Array%, « entry.[[buffer]],
+            //                                   entry.[[byteOffset]], entry.[[byteLength]] »).
+            // (reordered)
+            RootedObject buffer(cx, entry->buffer());
+
+            uint32_t byteOffset = entry->byteOffset();
+            view = JS_NewUint8ArrayWithBuffer(cx, buffer, byteOffset, entry->byteLength());
+            if (!view)
+                return nullptr;
+        }
+
+        // Step 3.d: Set this.[[queueTotalSize]] to
+        //           this.[[queueTotalSize]] − entry.[[byteLength]].
+        // (reordered)
+        controller->setFixedSlot(QueueContainerSlot_TotalSize, Int32Value(queueTotalSize));
 
         // Step 3.e: Perform ! ReadableByteStreamControllerHandleQueueDrain(this).
+        // (reordered)
         if (!ReadableByteStreamControllerHandleQueueDrain(cx, controller))
             return nullptr;
 
-        // Step 3.f: Let view be ! Construct(%Uint8Array%, « entry.[[buffer]],
-        //                                   entry.[[byteOffset]], entry.[[byteLength]] »).
-        RootedObject buffer(cx, entry->buffer());
-
-        uint32_t byteOffset = entry->byteOffset();
-        RootedObject view(cx, JS_NewUint8ArrayWithBuffer(cx, buffer, byteOffset, byteLength));
-        if (!view)
-            return nullptr;
-
         // Step 3.g: Return a promise resolved with ! CreateIterResultObject(view, false).
         val.setObject(*view);
         RootedObject iterResult(cx, CreateIterResultObject(cx, val, false));
         if (!iterResult)
             return nullptr;
         val.setObject(*iterResult);
 
         return PromiseObject::unforgeableResolve(cx, val);
@@ -3546,17 +3772,18 @@ ReadableStreamBYOBRequest_respondWithNew
         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);
+                                  JSMSG_READABLEBYTESTREAMCONTROLLER_BAD_CHUNK,
+                                  "ReadableStreamBYOBRequest#respondWithNewView");
         return false;
     }
 
     // Step 5: Return ?
     // ReadableByteStreamControllerRespondWithNewView(this.[[associatedReadableByteStreamController]],
     //                                                view).
     Rooted<ReadableByteStreamController*> controller(cx);
     controller = &controllerVal.toObject().as<ReadableByteStreamController>();
@@ -3585,17 +3812,18 @@ static const JSPropertySpec ReadableStre
 };
 
 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);
+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 )
@@ -3663,17 +3891,17 @@ ReadableByteStreamControllerClose(JSCont
                 return false;
 
             // Step iii: Throw e.
             return false;
         }
     }
 
     // Step 6: Perform ! ReadableStreamClose(stream).
-    return ReadableStreamClose(cx, 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
@@ -3780,27 +4008,45 @@ ReadableByteStreamControllerEnqueue(JSCo
     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: Let buffer be chunk.[[ViewedArrayBuffer]].
-    bool dummy;
-    RootedObject buffer(cx, JS_GetArrayBufferViewBuffer(cx, chunk, &dummy));
-    if (!buffer)
-        return false;
-
-    // Step 5: Let byteOffset be chunk.[[ByteOffset]].
-    uint32_t byteOffset = JS_GetArrayBufferViewByteOffset(chunk);
-
-    // Step 6: Let byteLength be chunk.[[ByteLength]].
-    uint32_t byteLength = JS_GetArrayBufferViewByteLength(chunk);
+    // 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)) {
@@ -3852,17 +4098,17 @@ ReadableByteStreamControllerEnqueue(JSCo
         }
 
         // Step ii: Perform ! ReadableByteStreamControllerProcessPullIntoDescriptorsUsingQueue(controller).
         if (!ReadableByteStreamControllerProcessPullIntoDescriptorsUsingQueue(cx, controller))
             return false;
     } else {
         // Step b: Otherwise,
         // Step i: Assert: ! IsReadableStreamLocked(stream) is false.
-        MOZ_ASSERT(!IsReadableStreamLocked(stream));
+        MOZ_ASSERT(!stream->locked());
 
         // Step ii: Perform
         //          ! ReadableByteStreamControllerEnqueueChunkToQueue(controller,
         //                                                            transferredBuffer,
         //                                                            byteOffset,
         //                                                            byteLength).
         if (!ReadableByteStreamControllerEnqueueChunkToQueue(cx, controller, transferredBuffer,
                                                             byteOffset, byteLength))
@@ -3977,16 +4223,49 @@ ReadableByteStreamControllerFillPullInto
         // Step a: Set totalBytesToCopyRemaining to maxAlignedBytes −
         //         pullIntoDescriptor.[[bytesFilled]].
         totalBytesToCopyRemaining = maxAlignedBytes - bytesFilled;
 
         // Step b: Let ready be true.
         *ready = true;
     }
 
+    if (ControllerFlags(controller) & ControllerFlag_ExternalSource) {
+        // TODO: it probably makes sense to eagerly drain the underlying source.
+        // We have a buffer lying around anyway, whereas the source might be
+        // able to free or reuse buffers once their content is copied into
+        // our buffer.
+        if (!ready)
+            return true;
+
+        Value val = controller->getFixedSlot(ControllerSlot_UnderlyingSource);
+        void* underlyingSource = val.toPrivate();
+
+        RootedArrayBufferObject targetBuffer(cx, pullIntoDescriptor->buffer());
+        Rooted<ReadableStream*> stream(cx, StreamFromController(controller));
+
+        size_t bytesWritten;
+        {
+            JS::AutoSuppressGCAnalysis noGC(cx);
+            bool dummy;
+            uint8_t* buffer = JS_GetArrayBufferData(targetBuffer, &dummy, noGC);
+            buffer += bytesFilled;
+            auto cb = cx->runtime()->readableStreamWriteIntoReadRequestCallback;
+            MOZ_ASSERT(cb);
+            cb(cx, stream, underlyingSource, stream->embeddingFlags(), buffer,
+               totalBytesToCopyRemaining, &bytesWritten);
+            pullIntoDescriptor->setBytesFilled(bytesFilled + bytesWritten);
+        }
+
+        queueTotalSize -= bytesWritten;
+        controller->setFixedSlot(QueueContainerSlot_TotalSize, Int32Value(queueTotalSize));
+
+        return true;
+    }
+
     // Step 9: Let queue be controller.[[queue]].
     RootedValue val(cx, controller->getFixedSlot(QueueContainerSlot_Queue));
     RootedNativeObject queue(cx, &val.toObject().as<NativeObject>());
 
     // Step 10: Repeat the following steps while totalBytesToCopyRemaining > 0,
     Rooted<ByteStreamChunk*> headOfQueue(cx);
     while (totalBytesToCopyRemaining > 0) {
         MOZ_ASSERT(queue->getDenseInitializedLength() != 0);
@@ -4080,17 +4359,17 @@ ReadableByteStreamControllerHandleQueueD
     MOZ_ASSERT(stream->readable());
 
     // Step 2: If controller.[[queueTotalSize]] is 0 and
     //         controller.[[closeRequested]] is true,
     double totalSize = controller->getFixedSlot(QueueContainerSlot_TotalSize).toNumber();
     bool closeRequested = ControllerFlags(controller) & ControllerFlag_CloseRequested;
     if (totalSize == 0 && closeRequested) {
       // Step a: Perform ! ReadableStreamClose(controller.[[controlledReadableStream]]).
-      return ReadableStreamClose(cx, stream);
+      return ReadableStreamCloseInternal(cx, stream);
     }
 
     // Step 3: Otherwise,
     // Step a: Perform ! ReadableByteStreamControllerCallPullIfNeeded(controller).
     return ReadableStreamControllerCallPullIfNeeded(cx, controller);
 }
 
 // Streams spec 3.12.15. ReadableByteStreamControllerInvalidateBYOBRequest ( controller )
@@ -4170,20 +4449,19 @@ ReadableByteStreamControllerProcessPullI
 
     return true;
 }
 
 // Streams spec, 3.12.17. ReadableByteStreamControllerPullInto ( controller, view )
 static MOZ_MUST_USE JSObject*
 ReadableByteStreamControllerPullInto(JSContext* cx,
                                      Handle<ReadableByteStreamController*> controller,
-                                     HandleNativeObject view)
+                                     Handle<ArrayBufferViewObject*> view)
 {
     MOZ_ASSERT(controller->is<ReadableByteStreamController>());
-    MOZ_ASSERT(JS_IsArrayBufferViewObject(view));
 
     // 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);
@@ -4667,17 +4945,17 @@ static const JSPropertySpec ByteLengthQu
     JS_PS_END
 };
 
 static const JSFunctionSpec ByteLengthQueuingStrategy_methods[] = {
     JS_FN("size", ByteLengthQueuingStrategy_size, 1, 0),
     JS_FS_END
 };
 
-CLASS_SPEC(ByteLengthQueuingStrategy, 1, 0, 0);
+CLASS_SPEC(ByteLengthQueuingStrategy, 1, 0, 0, 0, JS_NULL_CLASS_OPS);
 
 // Streams spec, 6.2.2. new CountQueuingStrategy({ highWaterMark })
 bool
 js::CountQueuingStrategy::constructor(JSContext* cx, unsigned argc, Value* vp)
 {
     CallArgs args = CallArgsFromVp(argc, vp);
 
     Rooted<CountQueuingStrategy*> strategy(cx, NewBuiltinClassInstance<CountQueuingStrategy>(cx));
@@ -4714,17 +4992,17 @@ static const JSPropertySpec CountQueuing
     JS_PS_END
 };
 
 static const JSFunctionSpec CountQueuingStrategy_methods[] = {
     JS_FN("size", CountQueuingStrategy_size, 0, 0),
     JS_FS_END
 };
 
-CLASS_SPEC(CountQueuingStrategy, 1, 0, 0);
+CLASS_SPEC(CountQueuingStrategy, 1, 0, 0, 0, JS_NULL_CLASS_OPS);
 
 #undef CLASS_SPEC
 
 // Streams spec, 6.3.1. DequeueValue ( container ) nothrow
 inline static MOZ_MUST_USE bool
 DequeueValue(JSContext* cx, HandleNativeObject container, MutableHandleValue chunk)
 {
     // Step 1: Assert: container has [[queue]] and [[queueTotalSize]] internal
@@ -4953,8 +5231,263 @@ ValidateAndNormalizeQueuingStrategy(JSCo
 
     // Step 2: Let highWaterMark be ? ValidateAndNormalizeHighWaterMark(highWaterMark).
     if (!ValidateAndNormalizeHighWaterMark(cx, highWaterMarkVal, highWaterMark))
         return false;
 
     // Step 3: Return Record {[[size]]: size, [[highWaterMark]]: highWaterMark}.
     return true;
 }
+
+MOZ_MUST_USE bool
+js::ReadableStreamReaderCancel(JSContext* cx, HandleObject readerObj, HandleValue reason)
+{
+    MOZ_ASSERT(IsReadableStreamReader(readerObj));
+    RootedNativeObject reader(cx, &readerObj->as<NativeObject>());
+    MOZ_ASSERT(StreamFromReader(reader));
+    return ReadableStreamReaderGenericCancel(cx, reader, reason);
+}
+
+MOZ_MUST_USE bool
+js::ReadableStreamReaderReleaseLock(JSContext* cx, HandleObject readerObj)
+{
+    MOZ_ASSERT(IsReadableStreamReader(readerObj));
+    RootedNativeObject reader(cx, &readerObj->as<NativeObject>());
+    MOZ_ASSERT(ReadableStreamGetNumReadRequests(StreamFromReader(reader)) == 0);
+    return ReadableStreamReaderGenericRelease(cx, reader);
+}
+
+MOZ_MUST_USE bool
+ReadableStream::enqueue(JSContext* cx, Handle<ReadableStream*> stream, HandleValue chunk)
+{
+    Rooted<ReadableStreamDefaultController*> controller(cx);
+    controller = &ControllerFromStream(stream)->as<ReadableStreamDefaultController>();
+
+    MOZ_ASSERT(!(ControllerFlags(controller) & ControllerFlag_CloseRequested));
+    MOZ_ASSERT(stream->readable());
+
+    return ReadableStreamDefaultControllerEnqueue(cx, controller, chunk);
+}
+
+MOZ_MUST_USE bool
+ReadableStream::enqueueBuffer(JSContext* cx, Handle<ReadableStream*> stream,
+                              Handle<ArrayBufferObject*> chunk)
+{
+    Rooted<ReadableByteStreamController*> controller(cx);
+    controller = &ControllerFromStream(stream)->as<ReadableByteStreamController>();
+
+    MOZ_ASSERT(!(ControllerFlags(controller) & ControllerFlag_CloseRequested));
+    MOZ_ASSERT(stream->readable());
+
+    return ReadableByteStreamControllerEnqueue(cx, controller, chunk);
+}
+
+void
+ReadableStream::desiredSize(bool* hasSize, double* size) const
+{
+    if (errored()) {
+        *hasSize = false;
+        return;
+    }
+
+    *hasSize = true;
+
+    if (closed()) {
+        *size = 0;
+        return;
+    }
+
+    NativeObject* controller = ControllerFromStream(this);
+    *size = ReadableStreamControllerGetDesiredSizeUnchecked(controller);
+}
+
+/*static */ bool
+ReadableStream::getExternalSource(JSContext* cx, Handle<ReadableStream*> stream, void** source)
+{
+    MOZ_ASSERT(stream->mode() == JS::ReadableStreamMode::ExternalSource);
+    if (stream->locked()) {
+        JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_READABLESTREAM_LOCKED);
+        return false;
+    }
+    if (!stream->readable()) {
+        JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
+                                  JSMSG_READABLESTREAMCONTROLLER_NOT_READABLE,
+                                  "ReadableStreamGetExternalUnderlyingSource");
+        return false;
+    }
+
+    auto controller = &ControllerFromStream(stream)->as<ReadableByteStreamController>();
+    AddControllerFlags(controller, ControllerFlag_SourceLocked);
+    *source = controller->getFixedSlot(ControllerSlot_UnderlyingSource).toPrivate();
+    return true;
+}
+
+void
+ReadableStream::releaseExternalSource()
+{
+    MOZ_ASSERT(mode() == JS::ReadableStreamMode::ExternalSource);
+    MOZ_ASSERT(locked());
+    auto controller = ControllerFromStream(this);
+    MOZ_ASSERT(ControllerFlags(controller) & ControllerFlag_SourceLocked);
+    RemoveControllerFlags(controller, ControllerFlag_SourceLocked);
+}
+
+uint8_t
+ReadableStream::embeddingFlags() const
+{
+    uint8_t flags = ControllerFlags(ControllerFromStream(this)) >> ControllerEmbeddingFlagsOffset;
+    MOZ_ASSERT_IF(flags, mode() == JS::ReadableStreamMode::ExternalSource);
+    return flags;
+}
+
+// Streams spec, 3.10.4.4. steps 1-3
+// and
+// Streams spec, 3.12.8. steps 8-9
+//
+// Adapted to handling updates signaled by the embedding for streams with
+// external underlying sources.
+//
+// The remaining steps of those two functions perform checks and asserts that
+// don't apply to streams with external underlying sources.
+MOZ_MUST_USE bool
+ReadableStream::updateDataAvailableFromSource(JSContext* cx, Handle<ReadableStream*> stream,
+                                              uint32_t availableData)
+{
+    Rooted<ReadableByteStreamController*> controller(cx);
+    controller = &ControllerFromStream(stream)->as<ReadableByteStreamController>();
+
+    // Step 2: If this.[[closeRequested]] is true, throw a TypeError exception.
+    if (ControllerFlags(controller) & ControllerFlag_CloseRequested) {
+        JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
+                                  JSMSG_READABLESTREAMCONTROLLER_CLOSED, "enqueue");
+        return false;
+    }
+
+    // Step 3: If this.[[controlledReadableStream]].[[state]] is not "readable",
+    //         throw a TypeError exception.
+    if (!StreamFromController(controller)->readable()) {
+        JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
+                                  JSMSG_READABLESTREAMCONTROLLER_NOT_READABLE, "enqueue");
+        return false;
+    }
+
+    RemoveControllerFlags(controller, ControllerFlag_Pulling | ControllerFlag_PullAgain);
+
+#if DEBUG
+    uint32_t oldAvailableData = controller->getFixedSlot(QueueContainerSlot_TotalSize).toInt32();
+#endif // DEBUG
+    controller->setFixedSlot(QueueContainerSlot_TotalSize, Int32Value(availableData));
+
+    // Step 8.a: If ! ReadableStreamGetNumReadRequests(stream) is 0,
+    // Reordered because for externally-sourced streams it applies regardless
+    // of reader type.
+    if (ReadableStreamGetNumReadRequests(stream) == 0)
+        return true;
+
+    // Step 8: If ! ReadableStreamHasDefaultReader(stream) is true
+    if (ReadableStreamHasDefaultReader(stream)) {
+        // Step b: Otherwise,
+        // Step i: Assert: controller.[[queue]] is empty.
+        MOZ_ASSERT(oldAvailableData == 0);
+
+        // Step ii: Let transferredView be
+        //          ! Construct(%Uint8Array%, transferredBuffer, byteOffset, byteLength).
+        JSObject* viewObj = JS_NewUint8Array(cx, availableData);
+        Rooted<ArrayBufferViewObject*> transferredView(cx, &viewObj->as<ArrayBufferViewObject>());
+        if (!transferredView)
+            return false;
+
+        Value val = controller->getFixedSlot(ControllerSlot_UnderlyingSource);
+        void* underlyingSource = val.toPrivate();
+
+        size_t bytesWritten;
+        {
+            JS::AutoSuppressGCAnalysis noGC(cx);
+            bool dummy;
+            void* buffer = JS_GetArrayBufferViewData(transferredView, &dummy, noGC);
+            auto cb = cx->runtime()->readableStreamWriteIntoReadRequestCallback;
+            MOZ_ASSERT(cb);
+            // TODO: use bytesWritten to correctly update the request's state.
+            cb(cx, stream, underlyingSource, stream->embeddingFlags(), buffer,
+               availableData, &bytesWritten);
+        }
+
+        // Step iii: Perform ! ReadableStreamFulfillReadRequest(stream, transferredView, false).
+        RootedValue chunk(cx, ObjectValue(*transferredView));
+        if (!ReadableStreamFulfillReadOrReadIntoRequest(cx, stream, chunk, false))
+            return false;
+
+        controller->setFixedSlot(QueueContainerSlot_TotalSize,
+                                 Int32Value(availableData - bytesWritten));
+    } else if (ReadableStreamHasBYOBReader(stream)) {
+        // Step 9: Otherwise,
+        // Step a: If ! ReadableStreamHasBYOBReader(stream) is true,
+        // Step i: Perform
+        // (Not needed for external underlying sources.)
+
+        // Step ii: Perform ! ReadableByteStreamControllerProcessPullIntoDescriptorsUsingQueue(controller).
+        if (!ReadableByteStreamControllerProcessPullIntoDescriptorsUsingQueue(cx, controller))
+            return false;
+    } else {
+        // Step b: Otherwise,
+        // Step i: Assert: ! IsReadableStreamLocked(stream) is false.
+        MOZ_ASSERT(!stream->locked());
+
+        // Step ii: Perform
+        //          ! ReadableByteStreamControllerEnqueueChunkToQueue(controller,
+        //                                                            transferredBuffer,
+        //                                                            byteOffset,
+        //                                                            byteLength).
+        // (Not needed for external underlying sources.)
+    }
+
+    return true;
+}
+
+MOZ_MUST_USE bool
+ReadableStream::close(JSContext* cx, Handle<ReadableStream*> stream)
+{
+    RootedNativeObject controllerObj(cx, ControllerFromStream(stream));
+    if (!VerifyControllerStateForClosing(cx, controllerObj))
+        return false;
+
+    if (controllerObj->is<ReadableStreamDefaultController>()) {
+        Rooted<ReadableStreamDefaultController*> controller(cx);
+        controller = &controllerObj->as<ReadableStreamDefaultController>();
+        return ReadableStreamDefaultControllerClose(cx, controller);
+    }
+
+    Rooted<ReadableByteStreamController*> controller(cx);
+    controller = &controllerObj->as<ReadableByteStreamController>();
+    return ReadableByteStreamControllerClose(cx, controller);
+}
+
+MOZ_MUST_USE bool
+ReadableStream::error(JSContext* cx, Handle<ReadableStream*> stream, HandleValue reason)
+{
+    // Step 3: If stream.[[state]] is not "readable", throw a TypeError exception.
+    if (!stream->readable()) {
+        JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
+                                  JSMSG_READABLESTREAMCONTROLLER_NOT_READABLE, "error");
+        return false;
+    }
+
+    // Step 4: Perform ! ReadableStreamDefaultControllerError(this, e).
+    RootedNativeObject controller(cx, ControllerFromStream(stream));
+    return ReadableStreamControllerError(cx, controller, reason);
+}
+
+MOZ_MUST_USE bool
+ReadableStream::tee(JSContext* cx, Handle<ReadableStream*> stream, bool cloneForBranch2,
+                    MutableHandle<ReadableStream*> branch1Stream,
+                    MutableHandle<ReadableStream*> branch2Stream)
+{
+    return ReadableStreamTee(cx, stream, false, branch1Stream, branch2Stream);
+}
+
+MOZ_MUST_USE NativeObject*
+ReadableStream::getReader(JSContext* cx, Handle<ReadableStream*> stream,
+                          JS::ReadableStreamReaderMode mode)
+{
+    if (mode == JS::ReadableStreamReaderMode::Default)
+        return CreateReadableStreamDefaultReader(cx, stream);
+    return CreateReadableStreamBYOBReader(cx, stream);
+}
--- a/js/src/builtin/Stream.h
+++ b/js/src/builtin/Stream.h
@@ -2,86 +2,138 @@
  * vim: set ts=8 sts=4 et sw=4 tw=99:
  * This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #ifndef builtin_Stream_h
 #define builtin_Stream_h
 
+#include "builtin/Promise.h"
 #include "vm/NativeObject.h"
 
+
 namespace js {
 
 class AutoSetNewObjectMetadata;
 
 class ReadableStream : public NativeObject
 {
   public:
     static ReadableStream* createDefaultStream(JSContext* cx, HandleValue underlyingSource,
-                                               HandleValue size, HandleValue highWaterMark);
+                                               HandleValue size, HandleValue highWaterMark,
+                                               HandleObject proto = nullptr);
     static ReadableStream* createByteStream(JSContext* cx, HandleValue underlyingSource,
-                                            HandleValue highWaterMark);
+                                            HandleValue highWaterMark,
+                                            HandleObject proto = nullptr);
+    static ReadableStream* createExternalSourceStream(JSContext* cx, void* underlyingSource,
+                                                      uint8_t flags, HandleObject proto = nullptr);
+
+    bool readable() const;
+    bool closed() const;
+    bool errored() const;
+    bool disturbed() const;
+
+    bool locked() const;
+
+    void desiredSize(bool* hasSize, double* size) const;
+
+    JS::ReadableStreamMode mode() const;
 
-    inline bool readable() const;
-    inline bool closed() const;
-    inline bool errored() const;
-    inline bool disturbed() const;
+    static MOZ_MUST_USE bool close(JSContext* cx, Handle<ReadableStream*> stream);
+    static MOZ_MUST_USE JSObject* cancel(JSContext* cx, Handle<ReadableStream*> stream,
+                                         HandleValue reason);
+    static MOZ_MUST_USE bool error(JSContext* cx, Handle<ReadableStream*> stream,
+                                   HandleValue error);
+
+    static MOZ_MUST_USE NativeObject* getReader(JSContext* cx, Handle<ReadableStream*> stream,
+                                                JS::ReadableStreamReaderMode mode);
+
+    static MOZ_MUST_USE bool tee(JSContext* cx,
+                                 Handle<ReadableStream*> stream, bool cloneForBranch2,
+                                 MutableHandle<ReadableStream*> branch1Stream,
+                                 MutableHandle<ReadableStream*> branch2Stream);
+
+    static MOZ_MUST_USE bool enqueue(JSContext* cx, Handle<ReadableStream*> stream,
+                                     HandleValue chunk);
+    static MOZ_MUST_USE bool enqueueBuffer(JSContext* cx, Handle<ReadableStream*> stream,
+                                           Handle<ArrayBufferObject*> chunk);
+    static MOZ_MUST_USE bool getExternalSource(JSContext* cx, Handle<ReadableStream*> stream,
+                                               void** source);
+    void releaseExternalSource();
+    uint8_t embeddingFlags() const;
+    static MOZ_MUST_USE bool updateDataAvailableFromSource(JSContext* cx,
+                                                           Handle<ReadableStream*> stream,
+                                                           uint32_t availableData);
 
     enum State {
          Readable  = 1 << 0,
          Closed    = 1 << 1,
          Errored   = 1 << 2,
          Disturbed = 1 << 3
     };
 
   private:
-    static ReadableStream* createStream(JSContext* cx);
+    static MOZ_MUST_USE ReadableStream* createStream(JSContext* cx, HandleObject proto = nullptr);
 
   public:
     static bool constructor(JSContext* cx, unsigned argc, Value* vp);
     static const ClassSpec classSpec_;
     static const Class class_;
     static const ClassSpec protoClassSpec_;
     static const Class protoClass_;
 };
 
 class ReadableStreamDefaultReader : public NativeObject
 {
   public:
+    static MOZ_MUST_USE JSObject* read(JSContext* cx, Handle<ReadableStreamDefaultReader*> reader);
+
     static bool constructor(JSContext* cx, unsigned argc, Value* vp);
     static const ClassSpec classSpec_;
     static const Class class_;
     static const ClassSpec protoClassSpec_;
     static const Class protoClass_;
 };
 
 class ReadableStreamBYOBReader : public NativeObject
 {
   public:
+    static MOZ_MUST_USE JSObject* read(JSContext* cx, Handle<ReadableStreamBYOBReader*> reader,
+                                       Handle<ArrayBufferViewObject*> view);
+
     static bool constructor(JSContext* cx, unsigned argc, Value* vp);
     static const ClassSpec classSpec_;
     static const Class class_;
     static const ClassSpec protoClassSpec_;
     static const Class protoClass_;
 };
 
+bool ReadableStreamReaderIsClosed(const JSObject* reader);
+
+MOZ_MUST_USE bool ReadableStreamReaderCancel(JSContext* cx, HandleObject reader,
+                                             HandleValue reason);
+
+MOZ_MUST_USE bool ReadableStreamReaderReleaseLock(JSContext* cx, HandleObject reader);
+
 class ReadableStreamDefaultController : public NativeObject
 {
   public:
     static bool constructor(JSContext* cx, unsigned argc, Value* vp);
     static const ClassSpec classSpec_;
     static const Class class_;
     static const ClassSpec protoClassSpec_;
     static const Class protoClass_;
 };
 
 class ReadableByteStreamController : public NativeObject
 {
   public:
+    bool hasExternalSource();
+
     static bool constructor(JSContext* cx, unsigned argc, Value* vp);
     static const ClassSpec classSpec_;
     static const Class class_;
     static const ClassSpec protoClassSpec_;
     static const Class protoClass_;
 };
 
 class ReadableStreamBYOBRequest : public NativeObject
--- a/js/src/js.msg
+++ b/js/src/js.msg
@@ -606,27 +606,28 @@ MSG_DEF(JSMSG_GET_ASYNC_ITER_RETURNED_PR
 
 // ReadableStream
 MSG_DEF(JSMSG_READABLESTREAM_UNDERLYINGSOURCE_TYPE_WRONG,0, JSEXN_RANGEERR,"'underlyingSource.type' must be \"bytes\" or undefined.")
 MSG_DEF(JSMSG_READABLESTREAM_INVALID_READER_MODE,        0, JSEXN_RANGEERR,"'mode' must be \"byob\" or undefined.")
 MSG_DEF(JSMSG_NUMBER_MUST_BE_FINITE_NON_NEGATIVE, 1, JSEXN_RANGEERR, "'{0}' must be a finite, non-negative number.")
 MSG_DEF(JSMSG_READABLEBYTESTREAMCONTROLLER_INVALID_BYTESWRITTEN, 0, JSEXN_RANGEERR, "'bytesWritten' exceeds remaining length.")
 MSG_DEF(JSMSG_READABLEBYTESTREAMCONTROLLER_INVALID_VIEW_SIZE, 0, JSEXN_RANGEERR, "view size does not match requested data.")
 MSG_DEF(JSMSG_READABLEBYTESTREAMCONTROLLER_INVALID_VIEW_OFFSET, 0, JSEXN_RANGEERR, "view offset does not match requested position.")
-MSG_DEF(JSMSG_READABLESTREAM_NOT_LOCKED,                 1, JSEXN_TYPEERR, "The ReadableStream method '{0}' may only be called on a locked stream.")
+MSG_DEF(JSMSG_READABLESTREAM_NOT_LOCKED,                 1, JSEXN_TYPEERR, "'{0}' may only be called on a locked stream.")
 MSG_DEF(JSMSG_READABLESTREAM_LOCKED,                     0, JSEXN_TYPEERR, "A Reader may only be created for an unlocked ReadableStream.")
-MSG_DEF(JSMSG_READABLESTREAM_NOT_BYTE_STREAM_CONTROLLER, 0, JSEXN_TYPEERR, "ReadableStream.getReader('byob') requires a ReadableByteStreamController.")
+MSG_DEF(JSMSG_READABLESTREAM_NOT_BYTE_STREAM_CONTROLLER, 1, JSEXN_TYPEERR, "{0} requires a ReadableByteStreamController.")
+MSG_DEF(JSMSG_READABLESTREAM_NOT_DEFAULT_CONTROLLER,     1, JSEXN_TYPEERR, "{0} requires a ReadableStreamDefaultController.")
 MSG_DEF(JSMSG_READABLESTREAM_CONTROLLER_SET,             0, JSEXN_TYPEERR, "The ReadableStream already has a controller defined.")
 MSG_DEF(JSMSG_READABLESTREAMREADER_NOT_OWNED,            1, JSEXN_TYPEERR, "The ReadableStream reader method '{0}' may only be called on a reader owned by a stream.")
 MSG_DEF(JSMSG_READABLESTREAMREADER_NOT_EMPTY,            1, JSEXN_TYPEERR, "The ReadableStream reader method '{0}' may not be called on a reader with read requests.")
 MSG_DEF(JSMSG_READABLESTREAMBYOBREADER_READ_EMPTY_VIEW,  0, JSEXN_TYPEERR, "ReadableStreamBYOBReader.read() was passed an empty TypedArrayBuffer view.")
 MSG_DEF(JSMSG_READABLESTREAMREADER_RELEASED,             0, JSEXN_TYPEERR, "The ReadableStream reader was released.")
-MSG_DEF(JSMSG_READABLESTREAMCONTROLLER_CLOSED,           1, JSEXN_TYPEERR, "The ReadableStream controller method '{0}' called on a stream already closing.")
-MSG_DEF(JSMSG_READABLESTREAMCONTROLLER_NOT_READABLE,     1, JSEXN_TYPEERR, "The ReadableStream controller method '{0}' may only be called on a stream in the 'readable' state.")
+MSG_DEF(JSMSG_READABLESTREAMCONTROLLER_CLOSED,           1, JSEXN_TYPEERR, "'{0}' called on a stream already closing.")
+MSG_DEF(JSMSG_READABLESTREAMCONTROLLER_NOT_READABLE,     1, JSEXN_TYPEERR, "'{0}' may only be called on a stream in the 'readable' state.")
 MSG_DEF(JSMSG_READABLEBYTESTREAMCONTROLLER_BAD_CHUNKSIZE,0, JSEXN_RANGEERR, "ReadableByteStreamController requires a positive integer or undefined for 'autoAllocateChunkSize'.")
-MSG_DEF(JSMSG_READABLEBYTESTREAMCONTROLLER_BAD_CHUNK,    0, JSEXN_TYPEERR, "ReadableByteStreamController passed a bad chunk.")
+MSG_DEF(JSMSG_READABLEBYTESTREAMCONTROLLER_BAD_CHUNK,    1, JSEXN_TYPEERR, "{0} passed a bad chunk.")
 MSG_DEF(JSMSG_READABLEBYTESTREAMCONTROLLER_CLOSE_PENDING_PULL, 0, JSEXN_TYPEERR, "The ReadableByteStreamController cannot be closed while the buffer is being filled.")
 MSG_DEF(JSMSG_READABLESTREAMBYOBREQUEST_NO_CONTROLLER,   1, JSEXN_TYPEERR, "ReadableStreamBYOBRequest method '{0}' called on a request with no controller.")
 MSG_DEF(JSMSG_READABLESTREAMBYOBREQUEST_RESPOND_CLOSED,  0, JSEXN_TYPEERR, "ReadableStreamBYOBRequest method 'respond' called with non-zero number of bytes with a closed controller.")
 MSG_DEF(JSMSG_READABLESTREAM_METHOD_NOT_IMPLEMENTED,     1, JSEXN_TYPEERR, "ReadableStream method {0} not yet implemented")
 
 // Other Stream-related
 MSG_DEF(JSMSG_STREAM_INVALID_HIGHWATERMARK,             0, JSEXN_RANGEERR, "'highWaterMark' must be a non-negative, non-NaN number.")
--- a/js/src/jsapi-tests/moz.build
+++ b/js/src/jsapi-tests/moz.build
@@ -120,16 +120,21 @@ if CONFIG['ENABLE_ION']:
         'testJitMacroAssembler.cpp',
         'testJitMoveEmitterCycles-mips32.cpp',
         'testJitMoveEmitterCycles.cpp',
         'testJitRangeAnalysis.cpp',
         'testJitRegisterSet.cpp',
         'testJitRValueAlloc.cpp',
     ]
 
+if CONFIG['ENABLE_STREAMS']:
+    UNIFIED_SOURCES += [
+        'testReadableStream.cpp',
+    ]
+
 DEFINES['EXPORT_JS_API'] = True
 
 LOCAL_INCLUDES += [
     '!..',
     '..',
 ]
 
 if CONFIG['ENABLE_INTL_API'] and CONFIG['MOZ_ICU_DATA_ARCHIVE']:
--- a/js/src/jsapi-tests/testIntTypesABI.cpp
+++ b/js/src/jsapi-tests/testIntTypesABI.cpp
@@ -25,16 +25,17 @@
 #include "js/Id.h"
 /* LegacyIntTypes.h is deliberately exempted from this requirement */
 #include "js/MemoryMetrics.h"
 #include "js/ProfilingStack.h"
 #include "js/RefCounted.h"
 #include "js/RequiredDefines.h"
 #include "js/RootingAPI.h"
 #include "js/SliceBudget.h"
+#include "js/Stream.h"
 #include "js/StructuredClone.h"
 #include "js/TracingAPI.h"
 #include "js/TrackedOptimizationInfo.h"
 #include "js/TypeDecls.h"
 #include "js/UbiNode.h"
 #include "js/Utility.h"
 #include "js/Value.h"
 #include "js/Vector.h"
new file mode 100644
--- /dev/null
+++ b/js/src/jsapi-tests/testReadableStream.cpp
@@ -0,0 +1,678 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*-
+ * vim: set ts=8 sts=4 et sw=4 tw=99:
+ */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "jsapi.h"
+
+#include "jsapi-tests/tests.h"
+
+using namespace JS;
+
+char test_buffer_data[] = "1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
+
+static JSObject*
+NewDefaultStream(JSContext* cx, HandleObject source = nullptr, HandleFunction size = nullptr,
+                 double highWaterMark = 1, HandleObject proto = nullptr)
+{
+    RootedObject stream(cx, NewReadableDefaultStreamObject(cx, source, size, highWaterMark,
+                                                           proto));
+    MOZ_ASSERT_IF(stream, IsReadableStream(stream));
+    return stream;
+}
+
+static JSObject*
+NewByteStream(JSContext* cx, double highWaterMark = 0, HandleObject proto = nullptr)
+{
+    RootedObject source(cx, JS_NewPlainObject(cx));
+    MOZ_ASSERT(source);
+
+    RootedObject stream(cx, NewReadableByteStreamObject(cx, source, highWaterMark, proto));
+    MOZ_ASSERT_IF(stream, IsReadableStream(stream));
+    return stream;
+}
+
+static bool dataRequestCBCalled = false;
+static void
+DataRequestCB(JSContext* cx, HandleObject stream, void* underlyingSource, uint8_t flags,
+              size_t desiredSize)
+{
+    MOZ_ASSERT(!dataRequestCBCalled, "Invalid test setup");
+    dataRequestCBCalled = true;
+}
+
+static bool writeIntoRequestBufferCBCalled = false;
+static void
+WriteIntoRequestBufferCB(JSContext* cx, HandleObject stream, void* underlyingSource, uint8_t flags,
+                         void* buffer, size_t length, size_t* bytesWritten)
+{
+    MOZ_ASSERT(!writeIntoRequestBufferCBCalled, "Invalid test setup");
+    MOZ_ASSERT(length <= sizeof(test_buffer_data));
+    memcpy(buffer, test_buffer_data, length);
+    writeIntoRequestBufferCBCalled = true;
+    *bytesWritten = length;
+}
+
+static bool cancelStreamCBCalled = false;
+static Value cancelStreamReason;
+static Value
+CancelStreamCB(JSContext* cx, HandleObject stream, void* underlyingSource, uint8_t flags,
+               HandleValue reason)
+{
+    MOZ_ASSERT(!cancelStreamCBCalled, "Invalid test setup");
+    cancelStreamCBCalled = true;
+    cancelStreamReason = reason;
+    return reason;
+}
+
+static bool streamClosedCBCalled = false;
+static Value streamClosedReason;
+static void
+StreamClosedCB(JSContext* cx, HandleObject stream, void* underlyingSource, uint8_t flags)
+{
+    MOZ_ASSERT(!streamClosedCBCalled, "Invalid test setup");
+    streamClosedCBCalled = true;
+}
+
+static bool streamErroredCBCalled = false;
+static Value streamErroredReason;
+static void
+StreamErroredCB(JSContext* cx, HandleObject stream, void* underlyingSource, uint8_t flags,
+                HandleValue reason)
+{
+    MOZ_ASSERT(!streamErroredCBCalled, "Invalid test setup");
+    streamErroredCBCalled = true;
+    streamErroredReason = reason;
+}
+
+static bool finalizeStreamCBCalled = false;
+static void* finalizedStreamUnderlyingSource;
+static void
+FinalizeStreamCB(void* underlyingSource, uint8_t flags)
+{
+    MOZ_ASSERT(!finalizeStreamCBCalled, "Invalid test setup");
+    finalizeStreamCBCalled = true;
+    finalizedStreamUnderlyingSource = underlyingSource;
+}
+
+static void
+ResetCallbacks()
+{
+    dataRequestCBCalled = false;
+    writeIntoRequestBufferCBCalled = false;
+    cancelStreamReason = UndefinedValue();
+    cancelStreamCBCalled = false;
+    streamClosedCBCalled = false;
+    streamErroredCBCalled = false;
+    finalizeStreamCBCalled = false;
+}
+
+static bool
+GetIterResult(JSContext* cx, HandleObject promise, MutableHandleValue value, bool* done)
+{
+    RootedObject iterResult(cx, &GetPromiseResult(promise).toObject());
+
+    bool found;
+    if (!JS_HasProperty(cx, iterResult, "value", &found))
+        return false;
+    MOZ_ASSERT(found);
+    if (!JS_HasProperty(cx, iterResult, "done", &found))
+        return false;
+    MOZ_ASSERT(found);
+
+    RootedValue doneVal(cx);
+    if (!JS_GetProperty(cx, iterResult, "value", value))
+        return false;
+    if (!JS_GetProperty(cx, iterResult, "done", &doneVal))
+        return false;
+
+    *done = doneVal.toBoolean();
+    MOZ_ASSERT_IF(*done, value.isUndefined());
+
+    return true;
+}
+
+static JSObject*
+GetReadChunk(JSContext* cx, HandleObject readRequest)
+{
+    MOZ_ASSERT(GetPromiseState(readRequest) == PromiseState::Fulfilled);
+    RootedValue resultVal(cx, GetPromiseResult(readRequest));
+    MOZ_ASSERT(resultVal.isObject());
+    RootedObject result(cx, &resultVal.toObject());
+    RootedValue chunkVal(cx);
+    JS_GetProperty(cx, result, "value", &chunkVal);
+    return &chunkVal.toObject();
+}
+
+BEGIN_TEST(testReadableStream_NewReadableStream)
+{
+    RootedObject stream(cx, NewDefaultStream(cx));
+    CHECK(stream);
+    CHECK(ReadableStreamGetMode(stream) == ReadableStreamMode::Default);
+    return true;
+}
+END_TEST(testReadableStream_NewReadableStream)
+
+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)
+{
+    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));
+
+    return true;
+}
+END_TEST(testReadableStream_ReadableStreamGetReaderDefault)
+
+BEGIN_TEST(testReadableStream_ReadableStreamGetReaderBYOB)
+{
+    RootedObject stream(cx, NewByteStream(cx));
+    CHECK(stream);
+
+    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)
+{
+    RootedObject stream(cx, NewDefaultStream(cx));
+    CHECK(stream);
+
+    RootedObject leftStream(cx);
+    RootedObject rightStream(cx);
+    CHECK(ReadableStreamTee(cx, stream, &leftStream, &rightStream));
+    CHECK(ReadableStreamIsLocked(stream));
+    CHECK(leftStream);
+    CHECK(IsReadableStream(leftStream));
+    CHECK(rightStream);
+    CHECK(IsReadableStream(rightStream));
+
+    return true;
+}
+END_TEST(testReadableStream_ReadableStreamTee)
+
+BEGIN_TEST(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);
+
+    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)
+{
+    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);
+
+    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_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)
+
+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)
+{
+    SetReadableStreamCallbacks(cx, &DataRequestCB, &WriteIntoRequestBufferCB,
+                                             &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(ReadableStreamClose(cx, stream));
+
+    bool done;
+    RootedValue value(cx);
+    CHECK(GetPromiseState(request) == PromiseState::Fulfilled);
+    CHECK(GetIterResult(cx, request, &value, &done));
+    CHECK(value.isUndefined());
+    CHECK(done);
+
+    // The callbacks are only invoked for external streams.
+    CHECK(!streamClosedCBCalled);
+
+    return true;
+}
+END_TEST(testReadableStream_ReadableStreamDefaultReaderClose)
+
+BEGIN_TEST(testReadableStream_ReadableStreamDefaultReaderError)
+{
+    ResetCallbacks();
+    SetReadableStreamCallbacks(cx, &DataRequestCB, &WriteIntoRequestBufferCB,
+                                             &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));
+    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)
+
+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);
+    RootedObject stream(cx, NewReadableExternalSourceStreamObject(cx, underlyingSource));
+    MOZ_ASSERT_IF(stream, IsReadableStream(stream));
+    return stream;
+}
+
+BEGIN_TEST(testReadableStream_CreateReadableByteStreamWithExternalSource)
+{
+    ResetCallbacks();
+
+    RootedObject stream(cx, NewExternalSourceStream(cx, &test_buffer_data, &DataRequestCB,
+                                                    &WriteIntoRequestBufferCB, &CancelStreamCB,
+                                                    &StreamClosedCB, &StreamErroredCB,
+                                                    &FinalizeStreamCB));
+    CHECK(stream);
+    CHECK(ReadableStreamGetMode(stream) == JS::ReadableStreamMode::ExternalSource);
+    void* underlyingSource;
+    CHECK(ReadableStreamGetExternalUnderlyingSource(cx, stream, &underlyingSource));
+    CHECK(underlyingSource == &test_buffer_data);
+    CHECK(ReadableStreamIsLocked(stream));
+    ReadableStreamReleaseExternalUnderlyingSource(stream);
+
+    return true;
+}
+END_TEST(testReadableStream_CreateReadableByteStreamWithExternalSource)
+
+BEGIN_TEST(testReadableStream_ExternalSourceCancel)
+{
+    ResetCallbacks();
+
+    RootedObject stream(cx, NewExternalSourceStream(cx, &test_buffer_data, &DataRequestCB,
+                                                    &WriteIntoRequestBufferCB, &CancelStreamCB,
+                                                    &StreamClosedCB, &StreamErroredCB,
+                                                    &FinalizeStreamCB));
+    CHECK(stream);
+    RootedValue reason(cx, Int32Value(42));
+    CHECK(ReadableStreamCancel(cx, stream, reason));
+    CHECK(cancelStreamCBCalled);
+    CHECK(cancelStreamReason == reason);
+
+    return true;
+}
+END_TEST(testReadableStream_ExternalSourceCancel)
+
+BEGIN_TEST(testReadableStream_ExternalSourceGetReader)
+{
+    ResetCallbacks();
+
+    RootedObject stream(cx, NewExternalSourceStream(cx, &test_buffer_data, &DataRequestCB,
+                                                    &WriteIntoRequestBufferCB, &CancelStreamCB,
+                                                    &StreamClosedCB, &StreamErroredCB,
+                                                    &FinalizeStreamCB));
+    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();
+
+    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
+{
+    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));
+        CHECK(stream);
+        js::RunJobs(cx);
+        void* underlyingSource;
+        CHECK(ReadableStreamGetExternalUnderlyingSource(cx, stream, &underlyingSource));
+        CHECK(underlyingSource == &test_buffer_data);
+        CHECK(ReadableStreamIsLocked(stream));
+        ReadableStreamReleaseExternalUnderlyingSource(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);
+        ReadableStreamUpdateDataAvailableFromSource(cx, stream, length);
+
+        CHECK(writeIntoRequestBufferCBCalled);
+        CHECK(GetPromiseState(promise) == PromiseState::Fulfilled);
+        RootedValue iterVal(cx);
+        bool done;
+        if (!GetIterResult(cx, promise, &iterVal, &done))
+            return false;
+
+        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));
+        }
+
+        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));
+        CHECK(stream);
+        void* underlyingSource;
+        CHECK(ReadableStreamGetExternalUnderlyingSource(cx, stream, &underlyingSource));
+        CHECK(underlyingSource == &test_buffer_data);
+        CHECK(ReadableStreamIsLocked(stream));
+        ReadableStreamReleaseExternalUnderlyingSource(stream);
+
+        size_t length = sizeof(test_buffer_data);
+        ReadableStreamUpdateDataAvailableFromSource(cx, stream, length);
+
+        RootedValue streamVal(cx, ObjectValue(*stream));
+        CHECK(JS_SetProperty(cx, global, "stream", streamVal));
+
+        RootedValue rval(cx);
+        EVAL(evalSrc, &rval);
+        CHECK(writeIntoRequestBufferCBCalled);
+        CHECK(rval.isObject());
+        RootedObject promise(cx, &rval.toObject());
+        CHECK(IsPromiseObject(promise));
+        CHECK(GetPromiseState(promise) == PromiseState::Fulfilled);
+        RootedValue iterVal(cx);
+        bool done;
+        if (!GetIterResult(cx, promise, &iterVal, &done))
+            return false;
+
+        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));
+        }
+
+        return true;
+    }
+};
+
+BEGIN_FIXTURE_TEST(ReadFromExternalSourceFixture,
+                   testReadableStream_ExternalSourceReadDefaultWithoutDataAvailable)
+{
+    return readWithoutDataAvailable("r = stream.getReader(); r.read()", "r.read()", sizeof(test_buffer_data));
+}
+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)));
+
+    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);
+
+    CHECK(JS_GetProperty(cx, global, "stream", &val));
+    RootedObject stream(cx, &val.toObject());
+    ReadableStreamClose(cx, stream);
+
+    val = GetPromiseResult(request);
+    MOZ_ASSERT(val.isObject());
+    RootedObject result(cx, &val.toObject());
+
+    JS_GetProperty(cx, result, "done", &val);
+    CHECK(val.isBoolean());
+    CHECK(val.toBoolean() == true);
+
+    JS_GetProperty(cx, result, "value", &val);
+    CHECK(val.isUndefined());
+    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));
+}
+END_FIXTURE_TEST(ReadFromExternalSourceFixture,
+                 testReadableStream_ExternalSourceReadDefaultWithDataAvailable)
+
+BEGIN_FIXTURE_TEST(ReadFromExternalSourceFixture,
+                   testReadableStream_ExternalSourceReadBYOBWithoutDataAvailable)
+{
+    return readWithoutDataAvailable("r = stream.getReader({mode: 'byob'}); r.read(new Uint8Array(63))", "r.read(new Uint8Array(10))", 10);
+}
+END_FIXTURE_TEST(ReadFromExternalSourceFixture,
+                 testReadableStream_ExternalSourceReadBYOBWithoutDataAvailable)
+
+BEGIN_FIXTURE_TEST(ReadFromExternalSourceFixture,
+                   testReadableStream_ExternalSourceReadBYOBWithDataAvailable)
+{
+    return readWithDataAvailable("r = stream.getReader({mode: 'byob'}); r.read(new Uint8Array(10))", 10);
+}
+END_FIXTURE_TEST(ReadFromExternalSourceFixture,
+                 testReadableStream_ExternalSourceReadBYOBWithDataAvailable)
--- a/js/src/jsapi-tests/tests.cpp
+++ b/js/src/jsapi-tests/tests.cpp
@@ -78,16 +78,20 @@ bool JSAPITest::definePrint()
     return JS_DefineFunction(cx, global, "print", (JSNative) print, 0, 0);
 }
 
 JSObject* JSAPITest::createGlobal(JSPrincipals* principals)
 {
     /* Create the global object. */
     JS::RootedObject newGlobal(cx);
     JS::CompartmentOptions options;
+#ifdef ENABLE_STREAMS
+    options.creationOptions().setStreamsEnabled(true);
+#endif
+    printf("enabled\n");
     options.behaviors().setVersion(JSVERSION_LATEST);
     newGlobal = JS_NewGlobalObject(cx, getGlobalClass(), principals, JS::FireOnNewGlobalHook,
                                    options);
     if (!newGlobal)
         return nullptr;
 
     JSAutoCompartment ac(cx, newGlobal);
 
--- a/js/src/jsapi.cpp
+++ b/js/src/jsapi.cpp
@@ -44,16 +44,17 @@
 #include "jswrapper.h"
 
 #include "builtin/AtomicsObject.h"
 #include "builtin/Eval.h"
 #include "builtin/Intl.h"
 #include "builtin/MapObject.h"
 #include "builtin/Promise.h"
 #include "builtin/RegExp.h"
+#include "builtin/Stream.h"
 #include "builtin/SymbolObject.h"
 #ifdef ENABLE_SIMD
 # include "builtin/SIMD.h"
 #endif
 #ifdef ENABLE_BINARYDATA
 # include "builtin/TypedObject.h"
 #endif
 #include "frontend/BytecodeCompiler.h"
@@ -5206,17 +5207,16 @@ CallOriginalPromiseThenImpl(JSContext* c
             return false;
     }
 
     if (resultObj) {
         if (!cx->compartment()->wrap(cx, resultObj))
             return false;
     }
     return true;
-
 }
 
 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, true))
@@ -5248,16 +5248,378 @@ JS_PUBLIC_API(JSObject*)
 JS::GetWaitForAllPromise(JSContext* cx, const JS::AutoObjectVector& promises)
 {
     AssertHeapIsIdle();
     CHECK_REQUEST(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->runtime()->isAtomsCompartment(cx->compartment()));
+    AssertHeapIsIdle();
+    CHECK_REQUEST(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->runtime()->isAtomsCompartment(cx->compartment()));
+    AssertHeapIsIdle();
+    CHECK_REQUEST(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)
+{
+    MOZ_ASSERT(dataRequestCallback);
+    MOZ_ASSERT(writeIntoReadRequestCallback);
+    MOZ_ASSERT(cancelCallback);
+    MOZ_ASSERT(closedCallback);
+    MOZ_ASSERT(erroredCallback);
+    MOZ_ASSERT(finalizeCallback);
+
+    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);
+
+    rt->readableStreamDataRequestCallback = dataRequestCallback;
+    rt->readableStreamWriteIntoReadRequestCallback = writeIntoReadRequestCallback;
+    rt->readableStreamCancelCallback = cancelCallback;
+    rt->readableStreamClosedCallback = closedCallback;
+    rt->readableStreamErroredCallback = erroredCallback;
+    rt->readableStreamFinalizeCallback = finalizeCallback;
+}
+
+JS_PUBLIC_API(bool)
+JS::HasReadableStreamCallbacks(JSContext* cx)
+{
+    return cx->runtime()->readableStreamDataRequestCallback;
+}
+
+JS_PUBLIC_API(JSObject*)
+JS::NewReadableExternalSourceStreamObject(JSContext* cx, void* underlyingSource,
+                                          uint8_t flags /* = 0 */,
+                                          HandleObject proto /* = nullptr */)
+{
+    MOZ_ASSERT(!cx->runtime()->isAtomsCompartment(cx->compartment()));
+    AssertHeapIsIdle();
+    CHECK_REQUEST(cx);
+
+#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>();
+}
+
+JS_PUBLIC_API(bool)
+JS::IsReadableStreamReader(const JSObject* obj)
+{
+    return obj->is<ReadableStreamDefaultReader>() || obj->is<ReadableStreamBYOBReader>();
+}
+
+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();
+}
+
+JS_PUBLIC_API(JSObject*)
+JS::ReadableStreamCancel(JSContext* cx, HandleObject streamObj, HandleValue reason)
+{
+    AssertHeapIsIdle();
+    CHECK_REQUEST(cx);
+    assertSameCompartment(cx, streamObj);
+    assertSameCompartment(cx, reason);
+
+    Rooted<ReadableStream*> stream(cx, &streamObj->as<ReadableStream>());
+    return ReadableStream::cancel(cx, stream, reason);
+}
+
+JS_PUBLIC_API(JS::ReadableStreamMode)
+JS::ReadableStreamGetMode(const JSObject* stream)
+{
+    return stream->as<ReadableStream>().mode();
+}
+
+JS_PUBLIC_API(JSObject*)
+JS::ReadableStreamGetReader(JSContext* cx, HandleObject streamObj, ReadableStreamReaderMode mode)
+{
+    AssertHeapIsIdle();
+    CHECK_REQUEST(cx);
+    assertSameCompartment(cx, streamObj);
+
+    Rooted<ReadableStream*> stream(cx, &streamObj->as<ReadableStream>());
+    return ReadableStream::getReader(cx, stream, mode);
+}
+
+JS_PUBLIC_API(bool)
+JS::ReadableStreamGetExternalUnderlyingSource(JSContext* cx, HandleObject streamObj, void** source)
+{
+    AssertHeapIsIdle();
+    CHECK_REQUEST(cx);
+    assertSameCompartment(cx, streamObj);
+
+    Rooted<ReadableStream*> stream(cx, &streamObj->as<ReadableStream>());
+    return ReadableStream::getExternalSource(cx, stream, source);
+}
+
+JS_PUBLIC_API(void)
+JS::ReadableStreamReleaseExternalUnderlyingSource(JSObject* stream)
+{
+    stream->as<ReadableStream>().releaseExternalSource();
+}
+
+JS_PUBLIC_API(bool)
+JS::ReadableStreamUpdateDataAvailableFromSource(JSContext* cx, JS::HandleObject streamObj,
+                                                uint32_t availableData)
+{
+    AssertHeapIsIdle();
+    CHECK_REQUEST(cx);
+    assertSameCompartment(cx, streamObj);
+
+    Rooted<ReadableStream*> stream(cx, &streamObj->as<ReadableStream>());
+    return ReadableStream::updateDataAvailableFromSource(cx, stream, availableData);
+}
+
+JS_PUBLIC_API(bool)
+JS::ReadableStreamTee(JSContext* cx, HandleObject streamObj,
+                      MutableHandleObject branch1Obj, MutableHandleObject branch2Obj)
+{
+    AssertHeapIsIdle();
+    CHECK_REQUEST(cx);
+    assertSameCompartment(cx, streamObj);
+
+    Rooted<ReadableStream*> stream(cx, &streamObj->as<ReadableStream>());
+    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::ReadableStreamClose(JSContext* cx, HandleObject streamObj)
+{
+    AssertHeapIsIdle();
+    CHECK_REQUEST(cx);
+    assertSameCompartment(cx, streamObj);
+
+    Rooted<ReadableStream*> stream(cx, &streamObj->as<ReadableStream>());
+    return ReadableStream::close(cx, stream);
+}
+
+JS_PUBLIC_API(bool)
+JS::ReadableStreamEnqueue(JSContext* cx, HandleObject streamObj, HandleValue chunk)
+{
+    AssertHeapIsIdle();
+    CHECK_REQUEST(cx);
+    assertSameCompartment(cx, streamObj);
+    assertSameCompartment(cx, chunk);
+
+    Rooted<ReadableStream*> stream(cx, &streamObj->as<ReadableStream>());
+    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_REQUEST(cx);
+    assertSameCompartment(cx, streamObj);
+    assertSameCompartment(cx, 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");
+        return false;
+    }
+
+    return ReadableStream::enqueueBuffer(cx, stream, buffer);
+}
+
+JS_PUBLIC_API(bool)
+JS::ReadableStreamError(JSContext* cx, HandleObject streamObj, HandleValue error)
+{
+    AssertHeapIsIdle();
+    CHECK_REQUEST(cx);
+    assertSameCompartment(cx, streamObj);
+    assertSameCompartment(cx, 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_REQUEST(cx);
+    assertSameCompartment(cx, reader);
+    assertSameCompartment(cx, reason);
+
+    return js::ReadableStreamReaderCancel(cx, reader, reason);
+}
+
+JS_PUBLIC_API(bool)
+JS::ReadableStreamReaderReleaseLock(JSContext* cx, HandleObject reader)
+{
+    AssertHeapIsIdle();
+    CHECK_REQUEST(cx);
+    assertSameCompartment(cx, reader);
+
+    return js::ReadableStreamReaderReleaseLock(cx, reader);
+}
+
+JS_PUBLIC_API(JSObject*)
+JS::ReadableStreamDefaultReaderRead(JSContext* cx, HandleObject readerObj)
+{
+    AssertHeapIsIdle();
+    CHECK_REQUEST(cx);
+    assertSameCompartment(cx, readerObj);
+
+    Rooted<ReadableStreamDefaultReader*> reader(cx, &readerObj->as<ReadableStreamDefaultReader>());
+    return js::ReadableStreamDefaultReader::read(cx, reader);
+}
+
+JS_PUBLIC_API(JSObject*)
+JS::ReadableStreamBYOBReaderRead(JSContext* cx, HandleObject readerObj, HandleObject viewObj)
+{
+    AssertHeapIsIdle();
+    CHECK_REQUEST(cx);
+    assertSameCompartment(cx, readerObj);
+    assertSameCompartment(cx, 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::SetAsyncTaskCallbacks(JSContext* cx, JS::StartAsyncTaskCallback start,
                           JS::FinishAsyncTaskCallback finish)
 {
     cx->runtime()->startAsyncTaskCallback = start;
     cx->runtime()->finishAsyncTaskCallback = finish;
 }
 
--- a/js/src/jsapi.h
+++ b/js/src/jsapi.h
@@ -31,16 +31,17 @@
 #include "js/Class.h"
 #include "js/GCVector.h"
 #include "js/HashTable.h"
 #include "js/Id.h"
 #include "js/Principals.h"
 #include "js/Realm.h"
 #include "js/RefCounted.h"
 #include "js/RootingAPI.h"
+#include "js/Stream.h"
 #include "js/TracingAPI.h"
 #include "js/UniquePtr.h"
 #include "js/Utility.h"
 #include "js/Value.h"
 #include "js/Vector.h"
 
 /************************************************************************/
 
--- a/js/src/moz.build
+++ b/js/src/moz.build
@@ -120,16 +120,17 @@ EXPORTS.js += [
     '../public/ProfilingStack.h',
     '../public/Proxy.h',
     '../public/Realm.h',
     '../public/RefCounted.h',
     '../public/RequiredDefines.h',
     '../public/Result.h',
     '../public/RootingAPI.h',
     '../public/SliceBudget.h',
+    '../public/Stream.h',
     '../public/StructuredClone.h',
     '../public/SweepingAPI.h',
     '../public/TraceKind.h',
     '../public/TracingAPI.h',
     '../public/TrackedOptimizationInfo.h',
     '../public/TypeDecls.h',
     '../public/UbiNode.h',
     '../public/UbiNodeBreadthFirst.h',
--- a/js/src/vm/CommonPropertyNames.h
+++ b/js/src/vm/CommonPropertyNames.h
@@ -12,16 +12,18 @@
 #include "jsprototypes.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") \
@@ -292,16 +294,60 @@
     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") \
+    macro(ReadableStream_cancel, ReadableStream_cancel, "ReadableStream_cancel") \
+    macro(ReadableStreamDefaultControllerGetDesiredSize, \
+          ReadableStreamDefaultControllerGetDesiredSize, \
+          "ReadableStreamDefaultControllerGetDesiredSize") \
+    macro(ReadableStreamDefaultController_close, \
+          ReadableStreamDefaultController_close, \
+          "ReadableStreamDefaultController_close") \
+    macro(ReadableStreamDefaultController_enqueue, \
+          ReadableStreamDefaultController_enqueue, \
+          "ReadableStreamDefaultController_enqueue") \
+    macro(ReadableStreamDefaultController_error, \
+          ReadableStreamDefaultController_error, \
+          "ReadableStreamDefaultController_error") \
+    macro(ReadableStreamDefaultReader_cancel, \
+          ReadableStreamDefaultReader_cancel, \
+          "ReadableStreamDefaultReader_cancel") \
+    macro(ReadableStreamDefaultReader_read, \
+          ReadableStreamDefaultReader_read, \
+          "ReadableStreamDefaultReader_read") \
+    macro(ReadableStreamDefaultReader_releaseLock, \
+          ReadableStreamDefaultReader_releaseLock, \
+          "ReadableStreamDefaultReader_releaseLock") \
+    macro(ReadableStreamTee, ReadableStreamTee, "ReadableStreamTee") \
     macro(reason, reason, "reason") \
     macro(RegExpBuiltinExec, RegExpBuiltinExec, "RegExpBuiltinExec") \
     macro(RegExpFlagsGetter, RegExpFlagsGetter, "RegExpFlagsGetter") \
     macro(RegExpMatcher, RegExpMatcher, "RegExpMatcher") \
     macro(RegExpSearcher, RegExpSearcher, "RegExpSearcher") \
     macro(RegExpTester, RegExpTester, "RegExpTester") \
     macro(RegExp_prototype_Exec, RegExp_prototype_Exec, "RegExp_prototype_Exec") \
     macro(Reify, Reify, "Reify") \
--- a/js/src/vm/Runtime.cpp
+++ b/js/src/vm/Runtime.cpp
@@ -103,16 +103,22 @@ JSRuntime::JSRuntime(JSRuntime* parentRu
     beginSingleThreadedExecutionCallback(nullptr),
     endSingleThreadedExecutionCallback(nullptr),
     profilerSampleBufferGen_(0),
     profilerSampleBufferLapCount_(1),
     telemetryCallback(nullptr),
     startAsyncTaskCallback(nullptr),
     finishAsyncTaskCallback(nullptr),
     promiseTasksToDestroy(mutexid::PromiseTaskPtrVector),
+    readableStreamDataRequestCallback(nullptr),
+    readableStreamWriteIntoReadRequestCallback(nullptr),
+    readableStreamCancelCallback(nullptr),
+    readableStreamClosedCallback(nullptr),
+    readableStreamErroredCallback(nullptr),
+    readableStreamFinalizeCallback(nullptr),
     hadOutOfMemory(false),
     allowRelazificationForTesting(false),
     destroyCompartmentCallback(nullptr),
     sizeOfIncludingThisCompartmentCallback(nullptr),
     compartmentNameCallback(nullptr),
     externalStringSizeofCallback(nullptr),
     securityCallbacks(&NullSecurityCallbacks),
     DOMcallbacks(nullptr),
--- a/js/src/vm/Runtime.h
+++ b/js/src/vm/Runtime.h
@@ -471,16 +471,23 @@ struct JSRuntime : public js::MallocProv
     js::ExclusiveData<js::PromiseTaskPtrVector> promiseTasksToDestroy;
 
     JSObject* getIncumbentGlobal(JSContext* cx);
     bool enqueuePromiseJob(JSContext* cx, js::HandleFunction job, js::HandleObject promise,
                            js::HandleObject incumbentGlobal);
     void addUnhandledRejectedPromise(JSContext* cx, js::HandleObject promise);
     void removeUnhandledRejectedPromise(JSContext* cx, js::HandleObject promise);
 
+    js::UnprotectedData<JS::RequestReadableStreamDataCallback> readableStreamDataRequestCallback;
+    js::UnprotectedData<JS::WriteIntoReadRequestBufferCallback> readableStreamWriteIntoReadRequestCallback;
+    js::UnprotectedData<JS::CancelReadableStreamCallback> readableStreamCancelCallback;
+    js::UnprotectedData<JS::ReadableStreamClosedCallback> readableStreamClosedCallback;
+    js::UnprotectedData<JS::ReadableStreamErroredCallback> readableStreamErroredCallback;
+    js::UnprotectedData<JS::ReadableStreamFinalizeCallback> readableStreamFinalizeCallback;
+
     /* Had an out-of-memory error which did not populate an exception. */
     mozilla::Atomic<bool> hadOutOfMemory;
 
     /*
      * Allow relazifying functions in compartments that are active. This is
      * only used by the relazifyFunctions() testing function.
      */
     js::ActiveThreadData<bool> allowRelazificationForTesting;