diff --git a/lib/internal/blob.js b/lib/internal/blob.js index 167c0521b4573d..d0e47c1a4397a8 100644 --- a/lib/internal/blob.js +++ b/lib/internal/blob.js @@ -321,6 +321,7 @@ class Blob { const reader = this[kHandle].getReader(); return new lazyReadableStream({ + type: 'bytes', start(c) { // There really should only be one read at a time so using an // array here is purely defensive. @@ -340,6 +341,9 @@ class Blob { if (status === 0) { // EOS c.close(); + // This is to signal the end for byob readers + // see https://streams.spec.whatwg.org/#example-rbs-pull + c.byobRequest?.respond(0); const pending = this.pendingPulls.shift(); pending.resolve(); return; @@ -353,13 +357,15 @@ class Blob { pending.reject(error); return; } - if (buffer !== undefined) { + // ReadableByteStreamController.enqueue errors if we submit a 0-length + // buffer. We need to check for that here. + if (buffer !== undefined && buffer.byteLength !== 0) { c.enqueue(new Uint8Array(buffer)); } // We keep reading until we either reach EOS, some error, or we // hit the flow rate of the stream (c.desiredSize). queueMicrotask(() => { - if (c.desiredSize <= 0) { + if (c.desiredSize < 0) { // A manual backpressure check. if (this.pendingPulls.length !== 0) { // A case of waiting pull finished (= not yet canceled) diff --git a/test/parallel/test-blob.js b/test/parallel/test-blob.js index a517bad1ccb42d..e17824d2833682 100644 --- a/test/parallel/test-blob.js +++ b/test/parallel/test-blob.js @@ -331,12 +331,54 @@ assert.throws(() => new Blob({}), { const b = new Blob(Array(10).fill('hello')); const stream = b.stream(); const reader = stream.getReader(); - assert.strictEqual(stream[kState].controller.desiredSize, 1); + assert.strictEqual(stream[kState].controller.desiredSize, 0); const { value, done } = await reader.read(); assert.strictEqual(value.byteLength, 5); assert(!done); setTimeout(() => { - assert.strictEqual(stream[kState].controller.desiredSize, 0); + // The blob stream is now a byte stream hence after the first read, + // it should pull in the next 'hello' which is 5 bytes hence -5. + assert.strictEqual(stream[kState].controller.desiredSize, -5); + }, 0); +})().then(common.mustCall()); + +(async () => { + const blob = new Blob(['hello', 'world']); + const stream = blob.stream(); + const reader = stream.getReader({ mode: 'byob' }); + const decoder = new TextDecoder(); + const chunks = []; + while (true) { + const { value, done } = await reader.read(new Uint8Array(100)); + if (done) break; + chunks.push(decoder.decode(value, { stream: true })); + } + assert.strictEqual(chunks.join(''), 'helloworld'); +})().then(common.mustCall()); + +(async () => { + const b = new Blob(Array(10).fill('hello')); + const stream = b.stream(); + const reader = stream.getReader({ mode: 'byob' }); + assert.strictEqual(stream[kState].controller.desiredSize, 0); + const { value, done } = await reader.read(new Uint8Array(100)); + assert.strictEqual(value.byteLength, 5); + assert(!done); + setTimeout(() => { + assert.strictEqual(stream[kState].controller.desiredSize, -5); + }, 0); +})().then(common.mustCall()); + +(async () => { + const b = new Blob(Array(10).fill('hello')); + const stream = b.stream(); + const reader = stream.getReader({ mode: 'byob' }); + assert.strictEqual(stream[kState].controller.desiredSize, 0); + const { value, done } = await reader.read(new Uint8Array(2)); + assert.strictEqual(value.byteLength, 2); + assert(!done); + setTimeout(() => { + assert.strictEqual(stream[kState].controller.desiredSize, -3); }, 0); })().then(common.mustCall()); diff --git a/test/wpt/status/FileAPI/blob.json b/test/wpt/status/FileAPI/blob.json index 017d931d7abdc4..8ea03bbc019992 100644 --- a/test/wpt/status/FileAPI/blob.json +++ b/test/wpt/status/FileAPI/blob.json @@ -44,12 +44,5 @@ }, "Blob-slice.any.js": { "skip": "Depends on File API" - }, - "Blob-stream.any.js": { - "fail": { - "expected": [ - "Reading Blob.stream() with BYOB reader" - ] - } } }