Can someone explain the rational reading from a Web API (WHATWG) ReadableStream, using the BYOB (Bring Your Own Buffer) reader (ReadableStreamBYOBReader
), why you cannot reuse your own buffer you brought.
Question: What is the point Bringing Your Own Buffer, if the Buffer you brought cannot be reused? What is the remaining advantage of bringing your own buffer? Why not just give the maximum read size?
If I may compare it to a supermarket, where you are encouraged to bring your own shopping bag. The first time you enter the supermarket with your own bag, everything works as expected, your fill up your bag with groceries, you empty it at home, wife happy with you, great. Next time you go shopping, you try again with your own bag. But this time the supermarket does now allow you to fill your bag as it has already been used before. Good luck with explaining this one at home.
The following code demonstrate multiple reads using the same buffer, and as expected fails, after the first attempt. Does not work in Safari, as Safari lacks BYOB support in the first place.
const url = 'https://raw.githubusercontent.com/Borewit/test-audio/958e057/Various%20Artists%20-%202009%20-%20netBloc%20Vol%2024_%20tiuqottigeloot%20%5BMP3-V2%5D/01%20-%20Diablo%20Swing%20Orchestra%20-%20Heroines.mp3';
async function byobReader(reader) {
const myBuffer = new Uint8Array(256);
let result;
let i=0;
do {
console.log(`Read iteration ${++i}...`);
result = await reader.read(myBuffer);
console.log(`Read read=${result.value.length} bytes, done=${result.done}`);
} while(!result.done);
}
async function run() {
console.log('fetching...');
const response = await fetch(url);
if (response.ok) {
console.log('HTTP response succussful');
const stream = response.body;
try {
const reader = stream.getReader({mode:'byob'});
console.log('BYOB Reader supported');
await byobReader(reader);
} catch(error) {
console.error(`Failed to initialize BYOB-Reader=${error.message}`);
}
} else {
console.error(`Failed with HTTP-status=${response.status}`);
}
}
run().catch(error => {
console.error(`Error: ${error.message}`);
});
If you browser support BYOB, the output will be:
fetching...
HTTP response succussful
BYOB Reader supported
Read iteration 1...
Read read=256 bytes, done=false
Read iteration 2...
Failed to initialize BYOB-Reader=Failed to execute 'read' on 'ReadableStreamBYOBReader': This readable stream reader cannot be used to read as the view has byte length equal to 0
The only clear advantage I can see compared to the ReadableStreamDefaultReader
, is that you can set a constraint on the buffer size.
It's because the ArrayBuffer
that your Uint8Array
points to has been transferred so that whatever will write from the reader can do so without you meddling with it at the same time.
To reuse it, you need to grab the ArrayBuffer
again from the read()
's callback and create a new view from it. This ArrayBuffer
will use the same memory allocated for your original view, so no new memory is wasted.
const url = 'https://raw.githubusercontent.com/Borewit/test-audio/958e057/Various%20Artists%20-%202009%20-%20netBloc%20Vol%2024_%20tiuqottigeloot%20%5BMP3-V2%5D/01%20-%20Diablo%20Swing%20Orchestra%20-%20Heroines.mp3';
async function byobReader(reader) {
// Made the buffer bigger so that the example ends earlier
let myBuffer = new Uint8Array(1024*1024);
let result;
let i=0;
do {
console.log(`Read iteration ${++i}...`);
result = await reader.read(myBuffer);
console.log(`Read read=${result.value.length} bytes, done=${result.done}`);
// Here no new memory is allocated,
// we use the same memory space as our original myBuffer.buffer.
myBuffer = new Uint8Array(result.value.buffer);
} while(!result.done);
}
async function run() {
console.log('fetching...');
const response = await fetch(url);
if (response.ok) {
console.log('HTTP response succussful');
const stream = response.body;
try {
const reader = stream.getReader({mode:'byob'});
console.log('BYOB Reader supported');
await byobReader(reader);
} catch(error) {
console.error(`Failed to initialize BYOB-Reader=${error.message}`);
}
} else {
console.error(`Failed with HTTP-status=${response.status}`);
}
}
run().catch(error => {
console.error(`Error: ${error.message}`);
});
Or, as put by the MDN article:
When a chunk of data is supplied, the value property will contain a new view. This will be a view over the same buffer/backing memory (and of the same type) as the original view passed to the read() method, now populated with the new chunk of data. Note that once the promise fulfills, the original view passed to the method will be detached and no longer usable. The promise will fulfill with a value: undefined if the stream has been cancelled. In this case the backing memory region of view is discarded and not returned to the caller (all previously read data in the view's buffer is lost).
(emphasizes mine)
If you're versed into reading-specs, the entry point is here. We then go to ReadableStreamBYOBReaderRead which leads to ReadableByteStreamControllerPullInto
which does call TransferArrayBuffer
.