From 9ab353af0086e504432349b939db95e527469f4e 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 Reviewed-By: Matteo Collina Reviewed-By: Benjamin Gruenbaum Reviewed-By: Debadree Chatterjee --- 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 a90575c8fe4aca..37612e70715069 100644 --- a/doc/api/webstreams.md +++ b/doc/api/webstreams.md @@ -492,7 +492,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} @@ -617,15 +617,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])` * `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 6abf4d9a3cd7d3..35d18a9d1fc4a4 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 35656216680875..62dfd8a288ab4c 100644 --- a/lib/internal/webstreams/readablestream.js +++ b/lib/internal/webstreams/readablestream.js @@ -22,6 +22,7 @@ const { SymbolAsyncIterator, SymbolDispose, SymbolToStringTag, + TypedArrayPrototypeGetLength, Uint8Array, } = primordials; @@ -33,6 +34,7 @@ const { ERR_INVALID_ARG_TYPE, ERR_INVALID_STATE, ERR_INVALID_THIS, + ERR_OUT_OF_RANGE, }, } = require('internal/errors'); @@ -58,8 +60,8 @@ const { validateAbortSignal, validateBuffer, validateObject, - kValidateObjectAllowNullable, - kValidateObjectAllowFunction, + kValidateObjectAllowObjects, + kValidateObjectAllowObjectsAndNull, } = require('internal/validators'); const { @@ -246,10 +248,10 @@ class ReadableStream { * @param {UnderlyingSource} [source] * @param {QueuingStrategy} [strategy] */ - constructor(source = {}, strategy = kEmptyObject) { + constructor(source = kEmptyObject, strategy = kEmptyObject) { markTransferMode(this, false, true); - if (source === null) - throw new ERR_INVALID_ARG_VALUE('source', 'Object', source); + validateObject(source, 'source', kValidateObjectAllowObjects); + validateObject(strategy, 'strategy', kValidateObjectAllowObjectsAndNull); this[kState] = createReadableStreamState(); this[kIsClosedPromise] = createDeferredPromise(); @@ -332,7 +334,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) @@ -370,6 +372,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; @@ -412,6 +415,7 @@ class ReadableStream { destination); } + validateObject(options, 'options', kValidateObjectAllowObjectsAndNull); const preventAbort = options?.preventAbort; const preventCancel = options?.preventCancel; const preventClose = options?.preventClose; @@ -456,10 +460,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); @@ -929,47 +931,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; } @@ -1883,7 +1900,7 @@ function readableByteStreamTee(stream) { reading = false; }, }; - readableStreamBYOBReaderRead(reader, view, readIntoRequest); + readableStreamBYOBReaderRead(reader, view, 1, readIntoRequest); } function pull1Algorithm() { @@ -2210,7 +2227,7 @@ function readableStreamReaderGenericRelease(reader) { reader[kState].stream = undefined; } -function readableStreamBYOBReaderRead(reader, view, readIntoRequest) { +function readableStreamBYOBReaderRead(reader, view, min, readIntoRequest) { const { stream, } = reader[kState]; @@ -2223,6 +2240,7 @@ function readableStreamBYOBReaderRead(reader, view, readIntoRequest) { readableByteStreamControllerPullInto( stream[kState].controller, view, + min, readIntoRequest); } @@ -2495,7 +2513,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; @@ -2512,7 +2530,7 @@ function readableByteStreamControllerCommitPullIntoDescriptor(stream, desc) { let done = false; if (stream[kState].state === 'closed') { - desc.bytesFilled = 0; + assert(desc.bytesFilled % desc.elementSize === 0); done = true; } @@ -2601,6 +2619,7 @@ function readableByteStreamControllerHandleQueueDrain(controller) { function readableByteStreamControllerPullInto( controller, view, + min, readIntoRequest) { const { closeRequested, @@ -2613,6 +2632,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); @@ -2631,6 +2655,7 @@ function readableByteStreamControllerPullInto( byteOffset, byteLength, bytesFilled: 0, + minimumFill, elementSize, ctor, type: 'byob', @@ -2718,7 +2743,7 @@ function readableByteStreamControllerRespond(controller, bytesWritten) { } function readableByteStreamControllerRespondInClosedState(controller, desc) { - assert(!desc.bytesFilled); + assert(desc.bytesFilled % desc.elementSize === 0); if (desc.type === 'none') { readableByteStreamControllerShiftPendingPullInto(controller); } @@ -2895,9 +2920,9 @@ function readableByteStreamControllerFillPullIntoDescriptorFromQueue( byteLength, byteOffset, bytesFilled, + minimumFill, elementSize, } = desc; - const currentAlignedBytes = bytesFilled - (bytesFilled % elementSize); const maxBytesToCopy = MathMin( controller[kState].queueTotalSize, byteLength - bytesFilled); @@ -2905,7 +2930,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; } @@ -2948,7 +2974,7 @@ function readableByteStreamControllerFillPullIntoDescriptorFromQueue( if (!ready) { assert(!controller[kState].queueTotalSize); assert(desc.bytesFilled > 0); - assert(desc.bytesFilled < elementSize); + assert(desc.bytesFilled < minimumFill); } return ready; } @@ -3004,7 +3030,7 @@ function readableByteStreamControllerRespondInReadableState( return; } - if (desc.bytesFilled < desc.elementSize) + if (desc.bytesFilled < desc.minimumFill) return; readableByteStreamControllerShiftPendingPullInto(controller); @@ -3189,6 +3215,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 b418fd2cb5347c..5bcb757dd5f85a 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,10 +125,13 @@ class TransformStream { * @param {QueuingStrategy} [readableStrategy] */ constructor( - transformer = null, + transformer = kEmptyObject, writableStrategy = kEmptyObject, readableStrategy = kEmptyObject) { markTransferMode(this, false, true); + 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 954bc6c2cc93d8..eea99f321d50c6 100644 --- a/lib/internal/webstreams/writablestream.js +++ b/lib/internal/webstreams/writablestream.js @@ -37,6 +37,12 @@ const { SideEffectFreeRegExpPrototypeSymbolReplace, } = require('internal/util'); +const { + validateObject, + kValidateObjectAllowObjects, + kValidateObjectAllowObjectsAndNull, +} = require('internal/validators'); + const { MessageChannel, } = require('internal/worker/io'); @@ -154,8 +160,10 @@ class WritableStream { * @param {UnderlyingSink} [sink] * @param {QueuingStrategy} [strategy] */ - constructor(sink = null, strategy = kEmptyObject) { + constructor(sink = kEmptyObject, strategy = kEmptyObject) { markTransferMode(this, false, true); + 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 6d635668f81a1c..33af47703674bb 100644 --- a/test/fixtures/wpt/README.md +++ b/test/fixtures/wpt/README.md @@ -27,7 +27,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/1e140d63ec/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 c9ad58bfce7d86..0e04684b381f2f 100644 --- a/test/fixtures/wpt/versions.json +++ b/test/fixtures/wpt/versions.json @@ -68,7 +68,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({}, []); } {