From 4a3ecbfc9b3bf1ae7ac73f59c50936a61e17a69d Mon Sep 17 00:00:00 2001
From: Mattias Buelens <649348+MattiasBuelens@users.noreply.github.com>
Date: Thu, 4 Jan 2024 12:37:44 +0100
Subject: [PATCH] stream: implement `min` option for
 `ReadableStreamBYOBReader.read`

PR-URL: https://github.com/nodejs/node/pull/50888
Backport-PR-URL: https://github.com/nodejs/node/pull/54044
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com>
Reviewed-By: Debadree Chatterjee <debadree333@gmail.com>
Reviewed-By: Rafael Gonzaga <rafael.nunu@hotmail.com>
---
 doc/api/webstreams.md                         |  15 +-
 lib/internal/encoding.js                      |   8 +-
 lib/internal/validators.js                    |   7 +
 lib/internal/webstreams/readablestream.js     | 107 ++-
 lib/internal/webstreams/transformstream.js    |  11 +-
 lib/internal/webstreams/writablestream.js     |  10 +-
 test/fixtures/wpt/README.md                   |   2 +-
 .../wpt/streams/piping/general.any.js         |   7 +-
 .../non-transferable-buffers.any.js           |  12 +
 .../readable-byte-streams/read-min.any.js     | 774 ++++++++++++++++++
 .../wpt/streams/resources/test-utils.js       |  47 --
 test/fixtures/wpt/versions.json               |   2 +-
 test/parallel/test-whatwg-readablestream.js   |  28 +-
 test/parallel/test-whatwg-transformstream.js  |  34 +
 test/parallel/test-whatwg-writablestream.js   |  18 +-
 15 files changed, 968 insertions(+), 114 deletions(-)
 create mode 100644 test/fixtures/wpt/streams/readable-byte-streams/read-min.any.js

diff --git a/doc/api/webstreams.md b/doc/api/webstreams.md
index 034d1c5579389f..e3b1f45f935759 100644
--- a/doc/api/webstreams.md
+++ b/doc/api/webstreams.md
@@ -488,7 +488,7 @@ added: v16.5.0
 -->
 
 * Returns: A promise fulfilled with an object:
-  * `value` {ArrayBuffer}
+  * `value` {any}
   * `done` {boolean}
 
 Requests the next chunk of data from the underlying {ReadableStream}
@@ -613,15 +613,24 @@ added: v16.5.0
   {ReadableStream} is closed or rejected if the stream errors or the reader's
   lock is released before the stream finishes closing.
 
-#### `readableStreamBYOBReader.read(view)`
+#### `readableStreamBYOBReader.read(view[, options])`
 
 <!-- YAML
 added: v16.5.0
+changes:
+  - version: REPLACEME
+    pr-url: https://github.com/nodejs/node/pull/54044
+    description: Added `min` option.
 -->
 
 * `view` {Buffer|TypedArray|DataView}
+* `options` {Object}
+  * `min` {number} When set, the returned promise will only be
+    fulfilled as soon as `min` number of elements are available.
+    When not set, the promise fulfills when at least one element
+    is available.
 * Returns: A promise fulfilled with an object:
-  * `value` {ArrayBuffer}
+  * `value` {TypedArray|DataView}
   * `done` {boolean}
 
 Requests the next chunk of data from the underlying {ReadableStream}
diff --git a/lib/internal/encoding.js b/lib/internal/encoding.js
index 6ed89b3f9b15a4..252eaa75fac22b 100644
--- a/lib/internal/encoding.js
+++ b/lib/internal/encoding.js
@@ -47,9 +47,7 @@ const {
 const {
   validateString,
   validateObject,
-  kValidateObjectAllowNullable,
-  kValidateObjectAllowArray,
-  kValidateObjectAllowFunction,
+  kValidateObjectAllowObjectsAndNull,
 } = require('internal/validators');
 const binding = internalBinding('encoding_binding');
 const {
@@ -393,10 +391,6 @@ const TextDecoder =
     makeTextDecoderICU() :
     makeTextDecoderJS();
 
-const kValidateObjectAllowObjectsAndNull = kValidateObjectAllowNullable |
-  kValidateObjectAllowArray |
-  kValidateObjectAllowFunction;
-
 function makeTextDecoderICU() {
   const {
     decode: _decode,
diff --git a/lib/internal/validators.js b/lib/internal/validators.js
index 2862a8f10c7bed..68ba56cf6a5b20 100644
--- a/lib/internal/validators.js
+++ b/lib/internal/validators.js
@@ -222,6 +222,11 @@ const kValidateObjectNone = 0;
 const kValidateObjectAllowNullable = 1 << 0;
 const kValidateObjectAllowArray = 1 << 1;
 const kValidateObjectAllowFunction = 1 << 2;
+const kValidateObjectAllowObjects = kValidateObjectAllowArray |
+  kValidateObjectAllowFunction;
+const kValidateObjectAllowObjectsAndNull = kValidateObjectAllowNullable |
+  kValidateObjectAllowArray |
+  kValidateObjectAllowFunction;
 
 /**
  * @callback validateObject
@@ -583,6 +588,8 @@ module.exports = {
   kValidateObjectAllowNullable,
   kValidateObjectAllowArray,
   kValidateObjectAllowFunction,
+  kValidateObjectAllowObjects,
+  kValidateObjectAllowObjectsAndNull,
   validateOneOf,
   validatePlainFunction,
   validatePort,
diff --git a/lib/internal/webstreams/readablestream.js b/lib/internal/webstreams/readablestream.js
index 28b97208922ed4..f4afe696546d5a 100644
--- a/lib/internal/webstreams/readablestream.js
+++ b/lib/internal/webstreams/readablestream.js
@@ -23,6 +23,7 @@ const {
   SymbolAsyncIterator,
   SymbolDispose,
   SymbolToStringTag,
+  TypedArrayPrototypeGetLength,
   Uint8Array,
 } = primordials;
 
@@ -34,6 +35,7 @@ const {
     ERR_INVALID_ARG_TYPE,
     ERR_INVALID_STATE,
     ERR_INVALID_THIS,
+    ERR_OUT_OF_RANGE,
   },
 } = require('internal/errors');
 
@@ -59,8 +61,8 @@ const {
   validateAbortSignal,
   validateBuffer,
   validateObject,
-  kValidateObjectAllowNullable,
-  kValidateObjectAllowFunction,
+  kValidateObjectAllowObjects,
+  kValidateObjectAllowObjectsAndNull,
 } = require('internal/validators');
 
 const {
@@ -247,9 +249,9 @@ class ReadableStream {
    * @param {UnderlyingSource} [source]
    * @param {QueuingStrategy} [strategy]
    */
-  constructor(source = {}, strategy = kEmptyObject) {
-    if (source === null)
-      throw new ERR_INVALID_ARG_VALUE('source', 'Object', source);
+  constructor(source = kEmptyObject, strategy = kEmptyObject) {
+    validateObject(source, 'source', kValidateObjectAllowObjects);
+    validateObject(strategy, 'strategy', kValidateObjectAllowObjectsAndNull);
     this[kState] = createReadableStreamState();
 
     this[kIsClosedPromise] = createDeferredPromise();
@@ -335,7 +337,7 @@ class ReadableStream {
   getReader(options = kEmptyObject) {
     if (!isReadableStream(this))
       throw new ERR_INVALID_THIS('ReadableStream');
-    validateObject(options, 'options', kValidateObjectAllowNullable | kValidateObjectAllowFunction);
+    validateObject(options, 'options', kValidateObjectAllowObjectsAndNull);
     const mode = options?.mode;
 
     if (mode === undefined)
@@ -373,6 +375,7 @@ class ReadableStream {
 
     // The web platform tests require that these be handled one at a
     // time and in a specific order. options can be null or undefined.
+    validateObject(options, 'options', kValidateObjectAllowObjectsAndNull);
     const preventAbort = options?.preventAbort;
     const preventCancel = options?.preventCancel;
     const preventClose = options?.preventClose;
@@ -415,6 +418,7 @@ class ReadableStream {
           destination);
       }
 
+      validateObject(options, 'options', kValidateObjectAllowObjectsAndNull);
       const preventAbort = options?.preventAbort;
       const preventCancel = options?.preventCancel;
       const preventClose = options?.preventClose;
@@ -459,10 +463,8 @@ class ReadableStream {
   values(options = kEmptyObject) {
     if (!isReadableStream(this))
       throw new ERR_INVALID_THIS('ReadableStream');
-    validateObject(options, 'options');
-    const {
-      preventCancel = false,
-    } = options;
+    validateObject(options, 'options', kValidateObjectAllowObjectsAndNull);
+    const preventCancel = !!(options?.preventCancel);
 
     // eslint-disable-next-line no-use-before-define
     const reader = new ReadableStreamDefaultReader(this);
@@ -926,47 +928,62 @@ class ReadableStreamBYOBReader {
 
   /**
    * @param {ArrayBufferView} view
+   * @param {{
+   *   min? : number
+   * }} [options]
    * @returns {Promise<{
-   *   view : ArrayBufferView,
+   *   value : ArrayBufferView,
    *   done : boolean,
    * }>}
    */
-  read(view) {
+  async read(view, options = kEmptyObject) {
     if (!isReadableStreamBYOBReader(this))
-      return PromiseReject(new ERR_INVALID_THIS('ReadableStreamBYOBReader'));
+      throw new ERR_INVALID_THIS('ReadableStreamBYOBReader');
     if (!isArrayBufferView(view)) {
-      return PromiseReject(
-        new ERR_INVALID_ARG_TYPE(
-          'view',
-          [
-            'Buffer',
-            'TypedArray',
-            'DataView',
-          ],
-          view));
+      throw new ERR_INVALID_ARG_TYPE(
+        'view',
+        [
+          'Buffer',
+          'TypedArray',
+          'DataView',
+        ],
+        view,
+      );
     }
+    validateObject(options, 'options', kValidateObjectAllowObjectsAndNull);
 
     const viewByteLength = ArrayBufferViewGetByteLength(view);
     const viewBuffer = ArrayBufferViewGetBuffer(view);
     const viewBufferByteLength = ArrayBufferPrototypeGetByteLength(viewBuffer);
 
     if (viewByteLength === 0 || viewBufferByteLength === 0) {
-      return PromiseReject(
-        new ERR_INVALID_STATE.TypeError(
-          'View or Viewed ArrayBuffer is zero-length or detached',
-        ),
-      );
+      throw new ERR_INVALID_STATE.TypeError(
+        'View or Viewed ArrayBuffer is zero-length or detached');
     }
 
     // Supposed to assert here that the view's buffer is not
     // detached, but there's no API available to use to check that.
+
+    const min = options?.min ?? 1;
+    if (typeof min !== 'number')
+      throw new ERR_INVALID_ARG_TYPE('options.min', 'number', min);
+    if (!NumberIsInteger(min))
+      throw new ERR_INVALID_ARG_VALUE('options.min', min, 'must be an integer');
+    if (min <= 0)
+      throw new ERR_INVALID_ARG_VALUE('options.min', min, 'must be greater than 0');
+    if (!isDataView(view)) {
+      if (min > TypedArrayPrototypeGetLength(view)) {
+        throw new ERR_OUT_OF_RANGE('options.min', '<= view.length', min);
+      }
+    } else if (min > viewByteLength) {
+      throw new ERR_OUT_OF_RANGE('options.min', '<= view.byteLength', min);
+    }
+
     if (this[kState].stream === undefined) {
-      return PromiseReject(
-        new ERR_INVALID_STATE.TypeError(
-          'The reader is not attached to a stream'));
+      throw new ERR_INVALID_STATE.TypeError('The reader is not attached to a stream');
     }
     const readIntoRequest = new ReadIntoRequest();
-    readableStreamBYOBReaderRead(this, view, readIntoRequest);
+    readableStreamBYOBReaderRead(this, view, min, readIntoRequest);
     return readIntoRequest.promise;
   }
 
@@ -1880,7 +1897,7 @@ function readableByteStreamTee(stream) {
         reading = false;
       },
     };
-    readableStreamBYOBReaderRead(reader, view, readIntoRequest);
+    readableStreamBYOBReaderRead(reader, view, 1, readIntoRequest);
   }
 
   function pull1Algorithm() {
@@ -2207,7 +2224,7 @@ function readableStreamReaderGenericRelease(reader) {
   reader[kState].stream = undefined;
 }
 
-function readableStreamBYOBReaderRead(reader, view, readIntoRequest) {
+function readableStreamBYOBReaderRead(reader, view, min, readIntoRequest) {
   const {
     stream,
   } = reader[kState];
@@ -2220,6 +2237,7 @@ function readableStreamBYOBReaderRead(reader, view, readIntoRequest) {
   readableByteStreamControllerPullInto(
     stream[kState].controller,
     view,
+    min,
     readIntoRequest);
 }
 
@@ -2492,7 +2510,7 @@ function readableByteStreamControllerClose(controller) {
 
   if (pendingPullIntos.length) {
     const firstPendingPullInto = pendingPullIntos[0];
-    if (firstPendingPullInto.bytesFilled > 0) {
+    if (firstPendingPullInto.bytesFilled % firstPendingPullInto.elementSize !== 0) {
       const error = new ERR_INVALID_STATE.TypeError('Partial read');
       readableByteStreamControllerError(controller, error);
       throw error;
@@ -2509,7 +2527,7 @@ function readableByteStreamControllerCommitPullIntoDescriptor(stream, desc) {
 
   let done = false;
   if (stream[kState].state === 'closed') {
-    desc.bytesFilled = 0;
+    assert(desc.bytesFilled % desc.elementSize === 0);
     done = true;
   }
 
@@ -2598,6 +2616,7 @@ function readableByteStreamControllerHandleQueueDrain(controller) {
 function readableByteStreamControllerPullInto(
   controller,
   view,
+  min,
   readIntoRequest) {
   const {
     closeRequested,
@@ -2610,6 +2629,11 @@ function readableByteStreamControllerPullInto(
     elementSize = view.constructor.BYTES_PER_ELEMENT;
     ctor = view.constructor;
   }
+
+  const minimumFill = min * elementSize;
+  assert(minimumFill >= elementSize && minimumFill <= view.byteLength);
+  assert(minimumFill % elementSize === 0);
+
   const buffer = ArrayBufferViewGetBuffer(view);
   const byteOffset = ArrayBufferViewGetByteOffset(view);
   const byteLength = ArrayBufferViewGetByteLength(view);
@@ -2628,6 +2652,7 @@ function readableByteStreamControllerPullInto(
     byteOffset,
     byteLength,
     bytesFilled: 0,
+    minimumFill,
     elementSize,
     ctor,
     type: 'byob',
@@ -2715,7 +2740,7 @@ function readableByteStreamControllerRespond(controller, bytesWritten) {
 }
 
 function readableByteStreamControllerRespondInClosedState(controller, desc) {
-  assert(!desc.bytesFilled);
+  assert(desc.bytesFilled % desc.elementSize === 0);
   if (desc.type === 'none') {
     readableByteStreamControllerShiftPendingPullInto(controller);
   }
@@ -2892,9 +2917,9 @@ function readableByteStreamControllerFillPullIntoDescriptorFromQueue(
     byteLength,
     byteOffset,
     bytesFilled,
+    minimumFill,
     elementSize,
   } = desc;
-  const currentAlignedBytes = bytesFilled - (bytesFilled % elementSize);
   const maxBytesToCopy = MathMin(
     controller[kState].queueTotalSize,
     byteLength - bytesFilled);
@@ -2902,7 +2927,8 @@ function readableByteStreamControllerFillPullIntoDescriptorFromQueue(
   const maxAlignedBytes = maxBytesFilled - (maxBytesFilled % elementSize);
   let totalBytesToCopyRemaining = maxBytesToCopy;
   let ready = false;
-  if (maxAlignedBytes > currentAlignedBytes) {
+  assert(bytesFilled < minimumFill);
+  if (maxAlignedBytes >= minimumFill) {
     totalBytesToCopyRemaining = maxAlignedBytes - bytesFilled;
     ready = true;
   }
@@ -2945,7 +2971,7 @@ function readableByteStreamControllerFillPullIntoDescriptorFromQueue(
   if (!ready) {
     assert(!controller[kState].queueTotalSize);
     assert(desc.bytesFilled > 0);
-    assert(desc.bytesFilled < elementSize);
+    assert(desc.bytesFilled < minimumFill);
   }
   return ready;
 }
@@ -3001,7 +3027,7 @@ function readableByteStreamControllerRespondInReadableState(
     return;
   }
 
-  if (desc.bytesFilled < desc.elementSize)
+  if (desc.bytesFilled < desc.minimumFill)
     return;
 
   readableByteStreamControllerShiftPendingPullInto(controller);
@@ -3186,6 +3212,7 @@ function readableByteStreamControllerPullSteps(controller, readRequest) {
           byteOffset: 0,
           byteLength: autoAllocateChunkSize,
           bytesFilled: 0,
+          minimumFill: 1,
           elementSize: 1,
           ctor: Uint8Array,
           type: 'default',
diff --git a/lib/internal/webstreams/transformstream.js b/lib/internal/webstreams/transformstream.js
index 486faf884741aa..b6157cb5e67ee6 100644
--- a/lib/internal/webstreams/transformstream.js
+++ b/lib/internal/webstreams/transformstream.js
@@ -29,6 +29,12 @@ const {
   kEnumerableProperty,
 } = require('internal/util');
 
+const {
+  validateObject,
+  kValidateObjectAllowObjects,
+  kValidateObjectAllowObjectsAndNull,
+} = require('internal/validators');
+
 const {
   kDeserialize,
   kTransfer,
@@ -119,9 +125,12 @@ class TransformStream {
    * @param {QueuingStrategy} [readableStrategy]
    */
   constructor(
-    transformer = null,
+    transformer = kEmptyObject,
     writableStrategy = kEmptyObject,
     readableStrategy = kEmptyObject) {
+    validateObject(transformer, 'transformer', kValidateObjectAllowObjects);
+    validateObject(writableStrategy, 'writableStrategy', kValidateObjectAllowObjectsAndNull);
+    validateObject(readableStrategy, 'readableStrategy', kValidateObjectAllowObjectsAndNull);
     const readableType = transformer?.readableType;
     const writableType = transformer?.writableType;
     const start = transformer?.start;
diff --git a/lib/internal/webstreams/writablestream.js b/lib/internal/webstreams/writablestream.js
index 82129a8586da3e..75a9453ad490e9 100644
--- a/lib/internal/webstreams/writablestream.js
+++ b/lib/internal/webstreams/writablestream.js
@@ -38,6 +38,12 @@ const {
   SideEffectFreeRegExpPrototypeSymbolReplace,
 } = require('internal/util');
 
+const {
+  validateObject,
+  kValidateObjectAllowObjects,
+  kValidateObjectAllowObjectsAndNull,
+} = require('internal/validators');
+
 const {
   MessageChannel,
 } = require('internal/worker/io');
@@ -155,7 +161,9 @@ class WritableStream {
    * @param {UnderlyingSink} [sink]
    * @param {QueuingStrategy} [strategy]
    */
-  constructor(sink = null, strategy = kEmptyObject) {
+  constructor(sink = kEmptyObject, strategy = kEmptyObject) {
+    validateObject(sink, 'sink', kValidateObjectAllowObjects);
+    validateObject(strategy, 'strategy', kValidateObjectAllowObjectsAndNull);
     const type = sink?.type;
     if (type !== undefined)
       throw new ERR_INVALID_ARG_VALUE.RangeError('type', type);
diff --git a/test/fixtures/wpt/README.md b/test/fixtures/wpt/README.md
index 125a313fff0a23..e1fd3c3c0dd5c7 100644
--- a/test/fixtures/wpt/README.md
+++ b/test/fixtures/wpt/README.md
@@ -26,7 +26,7 @@ Last update:
 - performance-timeline: https://github.com/web-platform-tests/wpt/tree/17ebc3aea0/performance-timeline
 - resource-timing: https://github.com/web-platform-tests/wpt/tree/22d38586d0/resource-timing
 - resources: https://github.com/web-platform-tests/wpt/tree/919874f84f/resources
-- streams: https://github.com/web-platform-tests/wpt/tree/a8872d92b1/streams
+- streams: https://github.com/web-platform-tests/wpt/tree/3df6d94318/streams
 - url: https://github.com/web-platform-tests/wpt/tree/c2d7e70b52/url
 - user-timing: https://github.com/web-platform-tests/wpt/tree/5ae85bf826/user-timing
 - wasm/jsapi: https://github.com/web-platform-tests/wpt/tree/cde25e7e3c/wasm/jsapi
diff --git a/test/fixtures/wpt/streams/piping/general.any.js b/test/fixtures/wpt/streams/piping/general.any.js
index 09e01536325cca..f051d8102c2bed 100644
--- a/test/fixtures/wpt/streams/piping/general.any.js
+++ b/test/fixtures/wpt/streams/piping/general.any.js
@@ -1,5 +1,4 @@
 // META: global=window,worker,shadowrealm
-// META: script=../resources/test-utils.js
 // META: script=../resources/recording-streams.js
 'use strict';
 
@@ -39,7 +38,8 @@ promise_test(t => {
   const fakeRS = Object.create(ReadableStream.prototype);
   const ws = new WritableStream();
 
-  return methodRejects(t, ReadableStream.prototype, 'pipeTo', fakeRS, [ws]);
+  return promise_rejects_js(t, TypeError, ReadableStream.prototype.pipeTo.apply(fakeRS, [ws]),
+    'pipeTo should reject with a TypeError');
 
 }, 'pipeTo must check the brand of its ReadableStream this value');
 
@@ -48,7 +48,8 @@ promise_test(t => {
   const rs = new ReadableStream();
   const fakeWS = Object.create(WritableStream.prototype);
 
-  return methodRejects(t, ReadableStream.prototype, 'pipeTo', rs, [fakeWS]);
+  return promise_rejects_js(t, TypeError, ReadableStream.prototype.pipeTo.apply(rs, [fakeWS]),
+    'pipeTo should reject with a TypeError');
 
 }, 'pipeTo must check the brand of its WritableStream argument');
 
diff --git a/test/fixtures/wpt/streams/readable-byte-streams/non-transferable-buffers.any.js b/test/fixtures/wpt/streams/readable-byte-streams/non-transferable-buffers.any.js
index 47d7b2e653e5ec..4bddaef5d647df 100644
--- a/test/fixtures/wpt/streams/readable-byte-streams/non-transferable-buffers.any.js
+++ b/test/fixtures/wpt/streams/readable-byte-streams/non-transferable-buffers.any.js
@@ -13,6 +13,18 @@ promise_test(async t => {
   await promise_rejects_js(t, TypeError, reader.read(view));
 }, 'ReadableStream with byte source: read() with a non-transferable buffer');
 
+promise_test(async t => {
+  const rs = new ReadableStream({
+    pull: t.unreached_func('pull() should not be called'),
+    type: 'bytes'
+  });
+
+  const reader = rs.getReader({ mode: 'byob' });
+  const memory = new WebAssembly.Memory({ initial: 1 });
+  const view = new Uint8Array(memory.buffer, 0, 1);
+  await promise_rejects_js(t, TypeError, reader.read(view, { min: 1 }));
+}, 'ReadableStream with byte source: fill() with a non-transferable buffer');
+
 test(t => {
   let controller;
   const rs = new ReadableStream({
diff --git a/test/fixtures/wpt/streams/readable-byte-streams/read-min.any.js b/test/fixtures/wpt/streams/readable-byte-streams/read-min.any.js
new file mode 100644
index 00000000000000..4010e3750ce2a8
--- /dev/null
+++ b/test/fixtures/wpt/streams/readable-byte-streams/read-min.any.js
@@ -0,0 +1,774 @@
+// META: global=window,worker,shadowrealm
+// META: script=../resources/rs-utils.js
+// META: script=../resources/test-utils.js
+'use strict';
+
+// View buffers are detached after pull() returns, so record the information at the time that pull() was called.
+function extractViewInfo(view) {
+  return {
+    constructor: view.constructor,
+    bufferByteLength: view.buffer.byteLength,
+    byteOffset: view.byteOffset,
+    byteLength: view.byteLength
+  };
+}
+
+promise_test(async t => {
+  const rs = new ReadableStream({
+    type: 'bytes',
+    pull: t.unreached_func('pull() should not be called'),
+  });
+  const reader = rs.getReader({ mode: 'byob' });
+  await promise_rejects_js(t, TypeError, reader.read(new Uint8Array(1), { min: 0 }));
+}, 'ReadableStream with byte source: read({ min }) rejects if min is 0');
+
+promise_test(async t => {
+  const rs = new ReadableStream({
+    type: 'bytes',
+    pull: t.unreached_func('pull() should not be called'),
+  });
+  const reader = rs.getReader({ mode: 'byob' });
+  await promise_rejects_js(t, TypeError, reader.read(new Uint8Array(1), { min: -1 }));
+}, 'ReadableStream with byte source: read({ min }) rejects if min is negative');
+
+promise_test(async t => {
+  const rs = new ReadableStream({
+    type: 'bytes',
+    pull: t.unreached_func('pull() should not be called'),
+  });
+  const reader = rs.getReader({ mode: 'byob' });
+  await promise_rejects_js(t, RangeError, reader.read(new Uint8Array(1), { min: 2 }));
+}, 'ReadableStream with byte source: read({ min }) rejects if min is larger than view\'s length (Uint8Array)');
+
+promise_test(async t => {
+  const rs = new ReadableStream({
+    type: 'bytes',
+    pull: t.unreached_func('pull() should not be called'),
+  });
+  const reader = rs.getReader({ mode: 'byob' });
+  await promise_rejects_js(t, RangeError, reader.read(new Uint16Array(1), { min: 2 }));
+}, 'ReadableStream with byte source: read({ min }) rejects if min is larger than view\'s length (Uint16Array)');
+
+promise_test(async t => {
+  const rs = new ReadableStream({
+    type: 'bytes',
+    pull: t.unreached_func('pull() should not be called'),
+  });
+  const reader = rs.getReader({ mode: 'byob' });
+  await promise_rejects_js(t, RangeError, reader.read(new DataView(new ArrayBuffer(1)), { min: 2 }));
+}, 'ReadableStream with byte source: read({ min }) rejects if min is larger than view\'s length (DataView)');
+
+promise_test(async t => {
+  let pullCount = 0;
+  const byobRequests = [];
+  const rs = new ReadableStream({
+    type: 'bytes',
+    pull: t.step_func((c) => {
+      const byobRequest = c.byobRequest;
+      const view = byobRequest.view;
+      byobRequests[pullCount] = {
+        nonNull: byobRequest !== null,
+        viewNonNull: view !== null,
+        viewInfo: extractViewInfo(view)
+      };
+      if (pullCount === 0) {
+        view[0] = 0x01;
+        view[1] = 0x02;
+        byobRequest.respond(2);
+      } else if (pullCount === 1) {
+        view[0] = 0x03;
+        byobRequest.respond(1);
+      } else if (pullCount === 2) {
+        view[0] = 0x04;
+        byobRequest.respond(1);
+      }
+      ++pullCount;
+    })
+  });
+  const reader = rs.getReader({ mode: 'byob' });
+  const read1 = reader.read(new Uint8Array(3), { min: 3 });
+  const read2 = reader.read(new Uint8Array(1));
+
+  const result1 = await read1;
+  assert_false(result1.done, 'first result should not be done');
+  assert_typed_array_equals(result1.value, new Uint8Array([0x01, 0x02, 0x03]), 'first result value');
+
+  const result2 = await read2;
+  assert_false(result2.done, 'second result should not be done');
+  assert_typed_array_equals(result2.value, new Uint8Array([0x04]), 'second result value');
+
+  assert_equals(pullCount, 3, 'pull() must have been called 3 times');
+
+  {
+    const byobRequest = byobRequests[0];
+    assert_true(byobRequest.nonNull, 'first byobRequest must not be null');
+    assert_true(byobRequest.viewNonNull, 'first byobRequest.view must not be null');
+    const viewInfo = byobRequest.viewInfo;
+    assert_equals(viewInfo.constructor, Uint8Array, 'first view.constructor should be Uint8Array');
+    assert_equals(viewInfo.bufferByteLength, 3, 'first view.buffer.byteLength should be 3');
+    assert_equals(viewInfo.byteOffset, 0, 'first view.byteOffset should be 0');
+    assert_equals(viewInfo.byteLength, 3, 'first view.byteLength should be 3');
+  }
+
+  {
+    const byobRequest = byobRequests[1];
+    assert_true(byobRequest.nonNull, 'second byobRequest must not be null');
+    assert_true(byobRequest.viewNonNull, 'second byobRequest.view must not be null');
+    const viewInfo = byobRequest.viewInfo;
+    assert_equals(viewInfo.constructor, Uint8Array, 'second view.constructor should be Uint8Array');
+    assert_equals(viewInfo.bufferByteLength, 3, 'second view.buffer.byteLength should be 3');
+    assert_equals(viewInfo.byteOffset, 2, 'second view.byteOffset should be 2');
+    assert_equals(viewInfo.byteLength, 1, 'second view.byteLength should be 1');
+  }
+
+  {
+    const byobRequest = byobRequests[2];
+    assert_true(byobRequest.nonNull, 'third byobRequest must not be null');
+    assert_true(byobRequest.viewNonNull, 'third byobRequest.view must not be null');
+    const viewInfo = byobRequest.viewInfo;
+    assert_equals(viewInfo.constructor, Uint8Array, 'third view.constructor should be Uint8Array');
+    assert_equals(viewInfo.bufferByteLength, 1, 'third view.buffer.byteLength should be 1');
+    assert_equals(viewInfo.byteOffset, 0, 'third view.byteOffset should be 0');
+    assert_equals(viewInfo.byteLength, 1, 'third view.byteLength should be 1');
+  }
+
+}, 'ReadableStream with byte source: read({ min }), then read()');
+
+promise_test(async t => {
+  let pullCount = 0;
+  const byobRequests = [];
+  const rs = new ReadableStream({
+    type: 'bytes',
+    pull: t.step_func((c) => {
+      const byobRequest = c.byobRequest;
+      const view = byobRequest.view;
+      byobRequests[pullCount] = {
+        nonNull: byobRequest !== null,
+        viewNonNull: view !== null,
+        viewInfo: extractViewInfo(view)
+      };
+      if (pullCount === 0) {
+        view[0] = 0x01;
+        view[1] = 0x02;
+        byobRequest.respond(2);
+      } else if (pullCount === 1) {
+        view[0] = 0x03;
+        byobRequest.respond(1);
+      }
+      ++pullCount;
+    })
+  });
+  const reader = rs.getReader({ mode: 'byob' });
+
+  const result = await reader.read(new DataView(new ArrayBuffer(3)), { min: 3 });
+  assert_false(result.done, 'result should not be done');
+  assert_equals(result.value.constructor, DataView, 'result.value must be a DataView');
+  assert_equals(result.value.byteOffset, 0, 'result.value.byteOffset');
+  assert_equals(result.value.byteLength, 3, 'result.value.byteLength');
+  assert_equals(result.value.buffer.byteLength, 3, 'result.value.buffer.byteLength');
+  assert_array_equals([...new Uint8Array(result.value.buffer)], [0x01, 0x02, 0x03], `result.value.buffer contents`);
+
+  assert_equals(pullCount, 2, 'pull() must have been called 2 times');
+
+  {
+    const byobRequest = byobRequests[0];
+    assert_true(byobRequest.nonNull, 'first byobRequest must not be null');
+    assert_true(byobRequest.viewNonNull, 'first byobRequest.view must not be null');
+    const viewInfo = byobRequest.viewInfo;
+    assert_equals(viewInfo.constructor, Uint8Array, 'first view.constructor should be Uint8Array');
+    assert_equals(viewInfo.bufferByteLength, 3, 'first view.buffer.byteLength should be 3');
+    assert_equals(viewInfo.byteOffset, 0, 'first view.byteOffset should be 0');
+    assert_equals(viewInfo.byteLength, 3, 'first view.byteLength should be 3');
+  }
+
+  {
+    const byobRequest = byobRequests[1];
+    assert_true(byobRequest.nonNull, 'second byobRequest must not be null');
+    assert_true(byobRequest.viewNonNull, 'second byobRequest.view must not be null');
+    const viewInfo = byobRequest.viewInfo;
+    assert_equals(viewInfo.constructor, Uint8Array, 'second view.constructor should be Uint8Array');
+    assert_equals(viewInfo.bufferByteLength, 3, 'second view.buffer.byteLength should be 3');
+    assert_equals(viewInfo.byteOffset, 2, 'second view.byteOffset should be 2');
+    assert_equals(viewInfo.byteLength, 1, 'second view.byteLength should be 1');
+  }
+
+}, 'ReadableStream with byte source: read({ min }) with a DataView');
+
+promise_test(async t => {
+  let pullCount = 0;
+  const byobRequests = [];
+  const rs = new ReadableStream({
+    type: 'bytes',
+    start: t.step_func((c) => {
+      c.enqueue(new Uint8Array([0x01]));
+    }),
+    pull: t.step_func((c) => {
+      const byobRequest = c.byobRequest;
+      const view = byobRequest.view;
+      byobRequests[pullCount] = {
+        nonNull: byobRequest !== null,
+        viewNonNull: view !== null,
+        viewInfo: extractViewInfo(view)
+      };
+      if (pullCount === 0) {
+        view[0] = 0x02;
+        view[1] = 0x03;
+        byobRequest.respond(2);
+      }
+      ++pullCount;
+    })
+  });
+  const reader = rs.getReader({ mode: 'byob' });
+
+  const result = await reader.read(new Uint8Array(3), { min: 3 });
+  assert_false(result.done, 'first result should not be done');
+  assert_typed_array_equals(result.value, new Uint8Array([0x01, 0x02, 0x03]), 'first result value');
+
+  assert_equals(pullCount, 1, 'pull() must have only been called once');
+
+  const byobRequest = byobRequests[0];
+  assert_true(byobRequest.nonNull, 'first byobRequest must not be null');
+  assert_true(byobRequest.viewNonNull, 'first byobRequest.view must not be null');
+  const viewInfo = byobRequest.viewInfo;
+  assert_equals(viewInfo.constructor, Uint8Array, 'first view.constructor should be Uint8Array');
+  assert_equals(viewInfo.bufferByteLength, 3, 'first view.buffer.byteLength should be 3');
+  assert_equals(viewInfo.byteOffset, 1, 'first view.byteOffset should be 1');
+  assert_equals(viewInfo.byteLength, 2, 'first view.byteLength should be 2');
+
+}, 'ReadableStream with byte source: enqueue(), then read({ min })');
+
+promise_test(async t => {
+  let pullCount = 0;
+  const byobRequests = [];
+  const rs = new ReadableStream({
+    type: 'bytes',
+    pull: t.step_func((c) => {
+      const byobRequest = c.byobRequest;
+      const view = byobRequest.view;
+      byobRequests[pullCount] = {
+        nonNull: byobRequest !== null,
+        viewNonNull: view !== null,
+        viewInfo: extractViewInfo(view)
+      };
+      if (pullCount === 0) {
+        c.enqueue(new Uint8Array([0x01, 0x02]));
+      } else if (pullCount === 1) {
+        c.enqueue(new Uint8Array([0x03]));
+      }
+      ++pullCount;
+    })
+  });
+  const reader = rs.getReader({ mode: 'byob' });
+
+  const result = await reader.read(new Uint8Array(3), { min: 3 });
+  assert_false(result.done, 'first result should not be done');
+  assert_typed_array_equals(result.value, new Uint8Array([0x01, 0x02, 0x03]), 'first result value');
+
+  assert_equals(pullCount, 2, 'pull() must have been called 2 times');
+
+  {
+    const byobRequest = byobRequests[0];
+    assert_true(byobRequest.nonNull, 'first byobRequest must not be null');
+    assert_true(byobRequest.viewNonNull, 'first byobRequest.view must not be null');
+    const viewInfo = byobRequest.viewInfo;
+    assert_equals(viewInfo.constructor, Uint8Array, 'first view.constructor should be Uint8Array');
+    assert_equals(viewInfo.bufferByteLength, 3, 'first view.buffer.byteLength should be 3');
+    assert_equals(viewInfo.byteOffset, 0, 'first view.byteOffset should be 0');
+    assert_equals(viewInfo.byteLength, 3, 'first view.byteLength should be 3');
+  }
+
+  {
+    const byobRequest = byobRequests[1];
+    assert_true(byobRequest.nonNull, 'second byobRequest must not be null');
+    assert_true(byobRequest.viewNonNull, 'second byobRequest.view must not be null');
+    const viewInfo = byobRequest.viewInfo;
+    assert_equals(viewInfo.constructor, Uint8Array, 'second view.constructor should be Uint8Array');
+    assert_equals(viewInfo.bufferByteLength, 3, 'second view.buffer.byteLength should be 3');
+    assert_equals(viewInfo.byteOffset, 2, 'second view.byteOffset should be 2');
+    assert_equals(viewInfo.byteLength, 1, 'second view.byteLength should be 1');
+  }
+
+}, 'ReadableStream with byte source: read({ min: 3 }) on a 3-byte Uint8Array, then multiple enqueue() up to 3 bytes');
+
+promise_test(async t => {
+  let pullCount = 0;
+  const byobRequests = [];
+  const rs = new ReadableStream({
+    type: 'bytes',
+    pull: t.step_func((c) => {
+      const byobRequest = c.byobRequest;
+      const view = byobRequest.view;
+      byobRequests[pullCount] = {
+        nonNull: byobRequest !== null,
+        viewNonNull: view !== null,
+        viewInfo: extractViewInfo(view)
+      };
+      if (pullCount === 0) {
+        c.enqueue(new Uint8Array([0x01, 0x02]));
+      } else if (pullCount === 1) {
+        c.enqueue(new Uint8Array([0x03]));
+      }
+      ++pullCount;
+    })
+  });
+  const reader = rs.getReader({ mode: 'byob' });
+
+  const result = await reader.read(new Uint8Array(5), { min: 3 });
+  assert_false(result.done, 'first result should not be done');
+  assert_typed_array_equals(result.value, new Uint8Array([0x01, 0x02, 0x03, 0, 0]).subarray(0, 3), 'first result value');
+
+  assert_equals(pullCount, 2, 'pull() must have been called 2 times');
+
+  {
+    const byobRequest = byobRequests[0];
+    assert_true(byobRequest.nonNull, 'first byobRequest must not be null');
+    assert_true(byobRequest.viewNonNull, 'first byobRequest.view must not be null');
+    const viewInfo = byobRequest.viewInfo;
+    assert_equals(viewInfo.constructor, Uint8Array, 'first view.constructor should be Uint8Array');
+    assert_equals(viewInfo.bufferByteLength, 5, 'first view.buffer.byteLength should be 5');
+    assert_equals(viewInfo.byteOffset, 0, 'first view.byteOffset should be 0');
+    assert_equals(viewInfo.byteLength, 5, 'first view.byteLength should be 5');
+  }
+
+  {
+    const byobRequest = byobRequests[1];
+    assert_true(byobRequest.nonNull, 'second byobRequest must not be null');
+    assert_true(byobRequest.viewNonNull, 'second byobRequest.view must not be null');
+    const viewInfo = byobRequest.viewInfo;
+    assert_equals(viewInfo.constructor, Uint8Array, 'second view.constructor should be Uint8Array');
+    assert_equals(viewInfo.bufferByteLength, 5, 'second view.buffer.byteLength should be 5');
+    assert_equals(viewInfo.byteOffset, 2, 'second view.byteOffset should be 2');
+    assert_equals(viewInfo.byteLength, 3, 'second view.byteLength should be 3');
+  }
+
+}, 'ReadableStream with byte source: read({ min: 3 }) on a 5-byte Uint8Array, then multiple enqueue() up to 3 bytes');
+
+promise_test(async t => {
+  let pullCount = 0;
+  const byobRequests = [];
+  const rs = new ReadableStream({
+    type: 'bytes',
+    pull: t.step_func((c) => {
+      const byobRequest = c.byobRequest;
+      const view = byobRequest.view;
+      byobRequests[pullCount] = {
+        nonNull: byobRequest !== null,
+        viewNonNull: view !== null,
+        viewInfo: extractViewInfo(view)
+      };
+      if (pullCount === 0) {
+        c.enqueue(new Uint8Array([0x01, 0x02]));
+      } else if (pullCount === 1) {
+        c.enqueue(new Uint8Array([0x03, 0x04]));
+      }
+      ++pullCount;
+    })
+  });
+  const reader = rs.getReader({ mode: 'byob' });
+
+  const result = await reader.read(new Uint8Array(5), { min: 3 });
+  assert_false(result.done, 'first result should not be done');
+  assert_typed_array_equals(result.value, new Uint8Array([0x01, 0x02, 0x03, 0x04, 0]).subarray(0, 4), 'first result value');
+
+  assert_equals(pullCount, 2, 'pull() must have been called 2 times');
+
+  {
+    const byobRequest = byobRequests[0];
+    assert_true(byobRequest.nonNull, 'first byobRequest must not be null');
+    assert_true(byobRequest.viewNonNull, 'first byobRequest.view must not be null');
+    const viewInfo = byobRequest.viewInfo;
+    assert_equals(viewInfo.constructor, Uint8Array, 'first view.constructor should be Uint8Array');
+    assert_equals(viewInfo.bufferByteLength, 5, 'first view.buffer.byteLength should be 5');
+    assert_equals(viewInfo.byteOffset, 0, 'first view.byteOffset should be 0');
+    assert_equals(viewInfo.byteLength, 5, 'first view.byteLength should be 5');
+  }
+
+  {
+    const byobRequest = byobRequests[1];
+    assert_true(byobRequest.nonNull, 'second byobRequest must not be null');
+    assert_true(byobRequest.viewNonNull, 'second byobRequest.view must not be null');
+    const viewInfo = byobRequest.viewInfo;
+    assert_equals(viewInfo.constructor, Uint8Array, 'second view.constructor should be Uint8Array');
+    assert_equals(viewInfo.bufferByteLength, 5, 'second view.buffer.byteLength should be 5');
+    assert_equals(viewInfo.byteOffset, 2, 'second view.byteOffset should be 2');
+    assert_equals(viewInfo.byteLength, 3, 'second view.byteLength should be 3');
+  }
+
+}, 'ReadableStream with byte source: read({ min: 3 }) on a 5-byte Uint8Array, then multiple enqueue() up to 4 bytes');
+
+promise_test(async t => {
+  const stream = new ReadableStream({
+    start(c) {
+      const view = new Uint8Array(16);
+      view[0] = 0x01;
+      view[8] = 0x02;
+      c.enqueue(view);
+    },
+    pull: t.unreached_func('pull() should not be called'),
+    type: 'bytes'
+  });
+
+  const byobReader = stream.getReader({ mode: 'byob' });
+  const result1 = await byobReader.read(new Uint8Array(8), { min: 8 });
+  assert_false(result1.done, 'result1.done');
+
+  const view1 = result1.value;
+  assert_equals(view1.constructor, Uint8Array, 'result1.value.constructor');
+  assert_equals(view1.buffer.byteLength, 8, 'result1.value.buffer.byteLength');
+  assert_equals(view1.byteOffset, 0, 'result1.value.byteOffset');
+  assert_equals(view1.byteLength, 8, 'result1.value.byteLength');
+  assert_equals(view1[0], 0x01, 'result1.value[0]');
+
+  byobReader.releaseLock();
+
+  const reader = stream.getReader();
+  const result2 = await reader.read();
+  assert_false(result2.done, 'result2.done');
+
+  const view2 = result2.value;
+  assert_equals(view2.constructor, Uint8Array, 'result2.value.constructor');
+  assert_equals(view2.buffer.byteLength, 16, 'result2.value.buffer.byteLength');
+  assert_equals(view2.byteOffset, 8, 'result2.value.byteOffset');
+  assert_equals(view2.byteLength, 8, 'result2.value.byteLength');
+  assert_equals(view2[0], 0x02, 'result2.value[0]');
+}, 'ReadableStream with byte source: enqueue(), read({ min }) partially, then read()');
+
+promise_test(async () => {
+  let pullCount = 0;
+  const byobRequestDefined = [];
+  let byobRequestViewDefined;
+
+  const stream = new ReadableStream({
+    async pull(c) {
+      byobRequestDefined.push(c.byobRequest !== null);
+      const initialByobRequest = c.byobRequest;
+
+      const transferredView = await transferArrayBufferView(c.byobRequest.view);
+      transferredView[0] = 0x01;
+      c.byobRequest.respondWithNewView(transferredView);
+
+      byobRequestDefined.push(c.byobRequest !== null);
+      byobRequestViewDefined = initialByobRequest.view !== null;
+
+      ++pullCount;
+    },
+    type: 'bytes'
+  });
+
+  const reader = stream.getReader({ mode: 'byob' });
+  const result = await reader.read(new Uint8Array(1), { min: 1 });
+  assert_false(result.done, 'result.done');
+  assert_equals(result.value.byteLength, 1, 'result.value.byteLength');
+  assert_equals(result.value[0], 0x01, 'result.value[0]');
+  assert_equals(pullCount, 1, 'pull() should be called only once');
+  assert_true(byobRequestDefined[0], 'byobRequest must not be null before respondWithNewView()');
+  assert_false(byobRequestDefined[1], 'byobRequest must be null after respondWithNewView()');
+  assert_false(byobRequestViewDefined, 'view of initial byobRequest must be null after respondWithNewView()');
+}, 'ReadableStream with byte source: read({ min }), then respondWithNewView() with a transferred ArrayBuffer');
+
+promise_test(async t => {
+  const stream = new ReadableStream({
+    start(c) {
+      c.close();
+    },
+    pull: t.unreached_func('pull() should not be called'),
+    type: 'bytes'
+  });
+
+  const reader = stream.getReader({ mode: 'byob' });
+
+  const result = await reader.read(new Uint8Array([0x01]), { min: 1 });
+  assert_true(result.done, 'result.done');
+  assert_typed_array_equals(result.value, new Uint8Array([0x01]).subarray(0, 0), 'result.value');
+
+  await reader.closed;
+}, 'ReadableStream with byte source: read({ min }) on a closed stream');
+
+promise_test(async t => {
+  let pullCount = 0;
+  const rs = new ReadableStream({
+    type: 'bytes',
+    pull: t.step_func((c) => {
+      if (pullCount === 0) {
+        c.byobRequest.view[0] = 0x01;
+        c.byobRequest.respond(1);
+      } else if (pullCount === 1) {
+        c.close();
+        c.byobRequest.respond(0);
+      }
+      ++pullCount;
+    })
+  });
+  const reader = rs.getReader({ mode: 'byob' });
+
+  const result = await reader.read(new Uint8Array(3), { min: 3 });
+  assert_true(result.done, 'result.done');
+  assert_typed_array_equals(result.value, new Uint8Array([0x01, 0, 0]).subarray(0, 1), 'result.value');
+
+  assert_equals(pullCount, 2, 'pull() must have been called 2 times');
+
+  await reader.closed;
+}, 'ReadableStream with byte source: read({ min }) when closed before view is filled');
+
+promise_test(async t => {
+  let pullCount = 0;
+  const rs = new ReadableStream({
+    type: 'bytes',
+    pull: t.step_func((c) => {
+      if (pullCount === 0) {
+        c.byobRequest.view[0] = 0x01;
+        c.byobRequest.view[1] = 0x02;
+        c.byobRequest.respond(2);
+      } else if (pullCount === 1) {
+        c.byobRequest.view[0] = 0x03;
+        c.byobRequest.respond(1);
+        c.close();
+      }
+      ++pullCount;
+    })
+  });
+  const reader = rs.getReader({ mode: 'byob' });
+
+  const result = await reader.read(new Uint8Array(3), { min: 3 });
+  assert_false(result.done, 'result.done');
+  assert_typed_array_equals(result.value, new Uint8Array([0x01, 0x02, 0x03]), 'result.value');
+
+  assert_equals(pullCount, 2, 'pull() must have been called 2 times');
+
+  await reader.closed;
+}, 'ReadableStream with byte source: read({ min }) when closed immediately after view is filled');
+
+promise_test(async t => {
+  const error1 = new Error('error1');
+  const stream = new ReadableStream({
+    start(c) {
+      c.error(error1);
+    },
+    pull: t.unreached_func('pull() should not be called'),
+    type: 'bytes'
+  });
+
+  const reader = stream.getReader({ mode: 'byob' });
+  const read = reader.read(new Uint8Array(1), { min: 1 });
+
+  await Promise.all([
+    promise_rejects_exactly(t, error1, read, 'read() must fail'),
+    promise_rejects_exactly(t, error1, reader.closed, 'closed must fail')
+  ]);
+}, 'ReadableStream with byte source: read({ min }) on an errored stream');
+
+promise_test(async t => {
+  const error1 = new Error('error1');
+  let controller;
+  const stream = new ReadableStream({
+    start(c) {
+      controller = c;
+    },
+    type: 'bytes'
+  });
+
+  const reader = stream.getReader({ mode: 'byob' });
+  const read = reader.read(new Uint8Array(1), { min: 1 });
+
+  controller.error(error1);
+
+  await Promise.all([
+    promise_rejects_exactly(t, error1, read, 'read() must fail'),
+    promise_rejects_exactly(t, error1, reader.closed, 'closed must fail')
+  ]);
+}, 'ReadableStream with byte source: read({ min }), then error()');
+
+promise_test(t => {
+  let cancelCount = 0;
+  let reason;
+
+  const passedReason = new TypeError('foo');
+
+  const stream = new ReadableStream({
+    pull: t.unreached_func('pull() should not be called'),
+    cancel(r) {
+      if (cancelCount === 0) {
+        reason = r;
+      }
+
+      ++cancelCount;
+
+      return 'bar';
+    },
+    type: 'bytes'
+  });
+
+  const reader = stream.getReader({ mode: 'byob' });
+
+  const readPromise = reader.read(new Uint8Array(1), { min: 1 }).then(result => {
+    assert_true(result.done, 'result.done');
+    assert_equals(result.value, undefined, 'result.value');
+  });
+
+  const cancelPromise = reader.cancel(passedReason).then(result => {
+    assert_equals(result, undefined, 'cancel() return value should be fulfilled with undefined');
+    assert_equals(cancelCount, 1, 'cancel() should be called only once');
+    assert_equals(reason, passedReason, 'reason should equal the passed reason');
+  });
+
+  return Promise.all([readPromise, cancelPromise]);
+}, 'ReadableStream with byte source: getReader(), read({ min }), then cancel()');
+
+promise_test(async t => {
+  let pullCount = 0;
+  let byobRequest;
+  const viewInfos = [];
+  const rs = new ReadableStream({
+    type: 'bytes',
+    pull: t.step_func((c) => {
+      byobRequest = c.byobRequest;
+
+      viewInfos.push(extractViewInfo(c.byobRequest.view));
+      c.byobRequest.view[0] = 0x01;
+      c.byobRequest.respond(1);
+      viewInfos.push(extractViewInfo(c.byobRequest.view));
+
+      ++pullCount;
+    })
+  });
+
+  await Promise.resolve();
+  assert_equals(pullCount, 0, 'pull() must not have been called yet');
+
+  const reader = rs.getReader({ mode: 'byob' });
+  const read = reader.read(new Uint8Array(3), { min: 3 });
+  assert_equals(pullCount, 1, 'pull() must have been called once');
+  assert_not_equals(byobRequest, null, 'byobRequest should not be null');
+  assert_equals(viewInfos[0].byteLength, 3, 'byteLength before respond() should be 3');
+  assert_equals(viewInfos[1].byteLength, 2, 'byteLength after respond() should be 2');
+
+  reader.cancel().catch(t.unreached_func('cancel() should not reject'));
+
+  const result = await read;
+  assert_true(result.done, 'result.done');
+  assert_equals(result.value, undefined, 'result.value');
+
+  assert_equals(pullCount, 1, 'pull() must only be called once');
+
+  await reader.closed;
+}, 'ReadableStream with byte source: cancel() with partially filled pending read({ min }) request');
+
+promise_test(async () => {
+  let pullCalled = false;
+
+  const stream = new ReadableStream({
+    start(c) {
+      const view = new Uint8Array(16);
+      view[7] = 0x01;
+      view[15] = 0x02;
+      c.enqueue(view);
+    },
+    pull() {
+      pullCalled = true;
+    },
+    type: 'bytes'
+  });
+
+  const reader = stream.getReader({ mode: 'byob' });
+
+  const result1 = await reader.read(new Uint8Array(8), { min: 8 });
+  assert_false(result1.done, 'result1.done');
+
+  const view1 = result1.value;
+  assert_equals(view1.byteOffset, 0, 'result1.value.byteOffset');
+  assert_equals(view1.byteLength, 8, 'result1.value.byteLength');
+  assert_equals(view1[7], 0x01, 'result1.value[7]');
+
+  const result2 = await reader.read(new Uint8Array(8), { min: 8 });
+  assert_false(pullCalled, 'pull() must not have been called');
+  assert_false(result2.done, 'result2.done');
+
+  const view2 = result2.value;
+  assert_equals(view2.byteOffset, 0, 'result2.value.byteOffset');
+  assert_equals(view2.byteLength, 8, 'result2.value.byteLength');
+  assert_equals(view2[7], 0x02, 'result2.value[7]');
+}, 'ReadableStream with byte source: enqueue(), then read({ min }) with smaller views');
+
+promise_test(async t => {
+  const stream = new ReadableStream({
+    start(c) {
+      c.enqueue(new Uint8Array([0xaa, 0xbb, 0xcc]));
+      c.close();
+    },
+    pull: t.unreached_func('pull() should not be called'),
+    type: 'bytes'
+  });
+
+  const reader = stream.getReader({ mode: 'byob' });
+
+  await promise_rejects_js(t, TypeError, reader.read(new Uint16Array(2), { min: 2 }), 'read() must fail');
+  await promise_rejects_js(t, TypeError, reader.closed, 'reader.closed should reject');
+}, 'ReadableStream with byte source: 3 byte enqueue(), then close(), then read({ min }) with 2-element Uint16Array must fail');
+
+promise_test(async t => {
+  let controller;
+  const stream = new ReadableStream({
+    start(c) {
+      controller = c;
+    },
+    pull: t.unreached_func('pull() should not be called'),
+    type: 'bytes'
+  });
+
+  const reader = stream.getReader({ mode: 'byob' });
+  const readPromise = reader.read(new Uint16Array(2), { min: 2 });
+
+  controller.enqueue(new Uint8Array([0xaa, 0xbb, 0xcc]));
+  assert_throws_js(TypeError, () => controller.close(), 'controller.close() must throw');
+
+  await promise_rejects_js(t, TypeError, readPromise, 'read() must fail');
+  await promise_rejects_js(t, TypeError, reader.closed, 'reader.closed must reject');
+}, 'ReadableStream with byte source: read({ min }) with 2-element Uint16Array, then 3 byte enqueue(), then close() must fail');
+
+promise_test(async t => {
+  let pullCount = 0;
+  let controller;
+  const rs = new ReadableStream({
+    type: 'bytes',
+    start: t.step_func((c) => {
+      controller = c;
+    }),
+    pull: t.step_func((c) => {
+      ++pullCount;
+    })
+  });
+
+  const [reader1, reader2] = rs.tee().map(branch => branch.getReader({ mode: 'byob' }));
+
+  await Promise.resolve();
+  assert_equals(pullCount, 0, 'pull() must not have been called yet');
+
+  const read1 = reader1.read(new Uint8Array(3), { min: 3 });
+  const read2 = reader2.read(new Uint8Array(1));
+
+  assert_equals(pullCount, 1, 'pull() must have been called once');
+  const byobRequest1 = controller.byobRequest;
+  assert_equals(byobRequest1.view.byteLength, 3, 'first byobRequest.view.byteLength should be 3');
+  byobRequest1.view[0] = 0x01;
+  byobRequest1.respond(1);
+
+  const result2 = await read2;
+  assert_false(result2.done, 'branch2 first read() should not be done');
+  assert_typed_array_equals(result2.value, new Uint8Array([0x01]), 'branch2 first read() value');
+
+  assert_equals(pullCount, 2, 'pull() must have been called 2 times');
+  const byobRequest2 = controller.byobRequest;
+  assert_equals(byobRequest2.view.byteLength, 2, 'second byobRequest.view.byteLength should be 2');
+  byobRequest2.view[0] = 0x02;
+  byobRequest2.view[1] = 0x03;
+  byobRequest2.respond(2);
+
+  const result1 = await read1;
+  assert_false(result1.done, 'branch1 read() should not be done');
+  assert_typed_array_equals(result1.value, new Uint8Array([0x01, 0x02, 0x03]), 'branch1 read() value');
+
+  const result3 = await reader2.read(new Uint8Array(2));
+  assert_equals(pullCount, 2, 'pull() must only be called 2 times');
+  assert_false(result3.done, 'branch2 second read() should not be done');
+  assert_typed_array_equals(result3.value, new Uint8Array([0x02, 0x03]), 'branch2 second read() value');
+}, 'ReadableStream with byte source: tee() with read({ min }) from branch1 and read() from branch2');
diff --git a/test/fixtures/wpt/streams/resources/test-utils.js b/test/fixtures/wpt/streams/resources/test-utils.js
index 5ff8fc8cec939a..a38f78027bf0e9 100644
--- a/test/fixtures/wpt/streams/resources/test-utils.js
+++ b/test/fixtures/wpt/streams/resources/test-utils.js
@@ -1,52 +1,5 @@
 'use strict';
 
-self.getterRejects = (t, obj, getterName, target) => {
-  const getter = Object.getOwnPropertyDescriptor(obj, getterName).get;
-
-  return promise_rejects_js(t, TypeError, getter.call(target), getterName + ' should reject with a TypeError');
-};
-
-self.getterRejectsForAll = (t, obj, getterName, targets) => {
-  return Promise.all(targets.map(target => self.getterRejects(t, obj, getterName, target)));
-};
-
-self.methodRejects = (t, obj, methodName, target, args) => {
-  const method = obj[methodName];
-
-  return promise_rejects_js(t, TypeError, method.apply(target, args),
-                         methodName + ' should reject with a TypeError');
-};
-
-self.methodRejectsForAll = (t, obj, methodName, targets, args) => {
-  return Promise.all(targets.map(target => self.methodRejects(t, obj, methodName, target, args)));
-};
-
-self.getterThrows = (obj, getterName, target) => {
-  const getter = Object.getOwnPropertyDescriptor(obj, getterName).get;
-
-  assert_throws_js(TypeError, () => getter.call(target), getterName + ' should throw a TypeError');
-};
-
-self.getterThrowsForAll = (obj, getterName, targets) => {
-  targets.forEach(target => self.getterThrows(obj, getterName, target));
-};
-
-self.methodThrows = (obj, methodName, target, args) => {
-  const method = obj[methodName];
-  assert_equals(typeof method, 'function', methodName + ' should exist');
-
-  assert_throws_js(TypeError, () => method.apply(target, args), methodName + ' should throw a TypeError');
-};
-
-self.methodThrowsForAll = (obj, methodName, targets, args) => {
-  targets.forEach(target => self.methodThrows(obj, methodName, target, args));
-};
-
-self.constructorThrowsForAll = (constructor, firstArgs) => {
-  firstArgs.forEach(firstArg => assert_throws_js(TypeError, () => new constructor(firstArg),
-                                                 'constructor should throw a TypeError'));
-};
-
 self.delay = ms => new Promise(resolve => step_timeout(resolve, ms));
 
 // For tests which verify that the implementation doesn't do something it shouldn't, it's better not to use a
diff --git a/test/fixtures/wpt/versions.json b/test/fixtures/wpt/versions.json
index a1f0c3c607fcd1..7d9faed976b67e 100644
--- a/test/fixtures/wpt/versions.json
+++ b/test/fixtures/wpt/versions.json
@@ -64,7 +64,7 @@
     "path": "resources"
   },
   "streams": {
-    "commit": "a8872d92b147fc87200eb0c14fe7a4a9e7cd4f73",
+    "commit": "3df6d94318b225845a0c8e4c7718484f41c9b8ce",
     "path": "streams"
   },
   "url": {
diff --git a/test/parallel/test-whatwg-readablestream.js b/test/parallel/test-whatwg-readablestream.js
index db48facddab906..122500a3cfe0d5 100644
--- a/test/parallel/test-whatwg-readablestream.js
+++ b/test/parallel/test-whatwg-readablestream.js
@@ -181,17 +181,29 @@ const {
 }
 
 {
-  // These are silly but they should all work per spec
-  new ReadableStream(1);
-  new ReadableStream('hello');
-  new ReadableStream(false);
+  new ReadableStream({});
   new ReadableStream([]);
-  new ReadableStream(1, 1);
-  new ReadableStream(1, 'hello');
-  new ReadableStream(1, false);
-  new ReadableStream(1, []);
+  new ReadableStream({}, null);
+  new ReadableStream({}, {});
+  new ReadableStream({}, []);
 }
 
+['a', false, 1, null].forEach((source) => {
+  assert.throws(() => {
+    new ReadableStream(source);
+  }, {
+    code: 'ERR_INVALID_ARG_TYPE',
+  });
+});
+
+['a', false, 1].forEach((strategy) => {
+  assert.throws(() => {
+    new ReadableStream({}, strategy);
+  }, {
+    code: 'ERR_INVALID_ARG_TYPE',
+  });
+});
+
 ['a', {}, false].forEach((size) => {
   assert.throws(() => {
     new ReadableStream({}, { size });
diff --git a/test/parallel/test-whatwg-transformstream.js b/test/parallel/test-whatwg-transformstream.js
index 3276b4dd54a4ec..2ec2c21c66819f 100644
--- a/test/parallel/test-whatwg-transformstream.js
+++ b/test/parallel/test-whatwg-transformstream.js
@@ -30,6 +30,40 @@ assert.throws(() => new TransformStream({ writableType: 1 }), {
   code: 'ERR_INVALID_ARG_VALUE',
 });
 
+{
+  new TransformStream({});
+  new TransformStream([]);
+  new TransformStream({}, null);
+  new TransformStream({}, {});
+  new TransformStream({}, []);
+  new TransformStream({}, {}, null);
+  new TransformStream({}, {}, {});
+  new TransformStream({}, {}, []);
+}
+
+['a', false, 1, null].forEach((transform) => {
+  assert.throws(() => {
+    new TransformStream(transform);
+  }, {
+    code: 'ERR_INVALID_ARG_TYPE',
+  });
+});
+
+['a', false, 1].forEach((writableStrategy) => {
+  assert.throws(() => {
+    new TransformStream({}, writableStrategy);
+  }, {
+    code: 'ERR_INVALID_ARG_TYPE',
+  });
+});
+
+['a', false, 1].forEach((readableStrategy) => {
+  assert.throws(() => {
+    new TransformStream({}, {}, readableStrategy);
+  }, {
+    code: 'ERR_INVALID_ARG_TYPE',
+  });
+});
 
 {
   const stream = new TransformStream();
diff --git a/test/parallel/test-whatwg-writablestream.js b/test/parallel/test-whatwg-writablestream.js
index 7d1d686358c0e1..9db61a6faa475a 100644
--- a/test/parallel/test-whatwg-writablestream.js
+++ b/test/parallel/test-whatwg-writablestream.js
@@ -60,6 +60,18 @@ class Sink {
   assert.strictEqual(typeof stream.getWriter, 'function');
 }
 
+['a', false, 1, null].forEach((sink) => {
+  assert.throws(() => new WritableStream(sink), {
+    code: 'ERR_INVALID_ARG_TYPE',
+  });
+});
+
+['a', false, 1].forEach((strategy) => {
+  assert.throws(() => new WritableStream({}, strategy), {
+    code: 'ERR_INVALID_ARG_TYPE',
+  });
+});
+
 [1, false, ''].forEach((type) => {
   assert.throws(() => new WritableStream({ type }), {
     code: 'ERR_INVALID_ARG_VALUE',
@@ -79,9 +91,11 @@ class Sink {
 });
 
 {
-  new WritableStream({}, 1);
-  new WritableStream({}, 'a');
+  new WritableStream({});
+  new WritableStream([]);
   new WritableStream({}, null);
+  new WritableStream({}, {});
+  new WritableStream({}, []);
 }
 
 {