-
Notifications
You must be signed in to change notification settings - Fork 161
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
What types does ReadableByteStream's reader's read(x) accept and return? #295
Comments
Here's today's snapshot of thin BYOB byte stream reader I've been prototyping.
Some notes
|
I understand all of your analysis here. I prefer Uint8Array and Int8Array to the others (including Uint8Clamped). But yeah, it would be nice if we could also eliminate the implication of sign and byte-wise processing. It looks beautiful if we had a new concrete type on which only stuffs defined on the ArrayBufferView interface are exposed. I like it, but it doesn't add any practical benefit. I feel that we shouldn't add more type just for purity. If no one has strong opinion, I'd go with Uint8Array to finish the bikeshed :) As I think we want to allow passing the read data directly to someone expecting ArrayBufferView, returning a tuple of (buffer, offset, size) is not an option. |
Is this the right issue to discuss how EOF signal should be returned from reader.read(view)? If we choose to return only a view, we need to represent EOF using any of the value on the ArrayBufferView. Are you fine with the idea that byteLength === 0 is EOF? Do we want to give the EOF meaning to a view with byteLength === 0 more generally? I mean ByteStreamWriter should handle a view with byteLength === 0 as close? If so, piping will be a little simplified. For normal users, it's not useful, I think. |
Wait! I was re-reading my thread on es-discuss about this question and I realized @jorendorff had a great solution that I forgot to list in the OP: just make the return type be the same type of view as gets passed in to the read(view) method! I guess in that solution we disallow
We should be able to avoid making this assumption (even though it will always be true, I think) by using the |
I'd just keep it consistent with other web APIs such as XHR (accepting ArrayBufferView or ArrayBuffer) unless we find any strong benefit in using the structural array buffer view. |
Yeah, I am OK with that. (Although maybe we only allow ArrayBufferView if we go with the type-of-input-determines-type-of-output idea.) |
Is the benefit of that approach that we explicitly pass the underlying buffer than passing the view implying that the whole buffer behind it is subject to detaching? Hmm, I understand. Unlike others such as XHR, the BYOD stream temporarily takes the ownership of the provided buffer. If we really set a high value on that point, we should rather stop accepting ArrayBufferViews directly, I guess. And, ... that's just inconvenient? Maybe we could establish another view point for ArrayBufferView type arguments:
in addition to the view point we already have, that is:
OK |
Makes sense. |
Yes, I definitely agree with this. |
OK. I think the conclusion is that it should only accept an ArrayBufferView (not an ArrayBuffer, and not a structural ArrayBufferView, but DataView is allowed) and the promise should fulfill with the same type as was passed in. Agreed? |
OK. If we passed Float32Array, Float32Array is returned? |
Yep! |
Sorry. The consensus about the return type for non-BYOB byte stream is Uint8Array? IIRC, we haven't made conclusion for it explicitly. |
I think that is good. I was tempted to say ArrayBuffer... but Uint8Array might allow more flexibility and will probably be more uniform. |
Oh, yeah. We don't have to use a view, but maybe having it consistent with BYOB (use ArrayBufferView for both) is beneficial in some points? (Sorry, I just forgot that we could also just use the ArrayBuffer and said Uint8Array :) ) |
+1 |
Yeah, I think the consistency is good. Plus I could imagine a hypothetical scenario where the stream allocates one large ArrayBuffer then hands out smaller Uint8Array views into it as it fills up (all with the same backing buffer). Probably will never happen given multi-threading means lots of transfers anyway, but imagining it helped convince me. |
The pattern across most APIs so far has been to accept either ArrayBufferView/ArrayBuffer as input (now called BufferSource) and output ArrayBuffer (e.g. fetch(), XMLHttpRequest, and WebSocket). The exception is the Encoding Standard (which outputs Uint8Array), but we're not necessarily happy with that. I'm missing the reason why we want to output a view in this thread. Please forgive my reopening if I missed it. |
A view gives the tuple (buffer, bytesRead, offsetIntoBuffer) which is necessary information anyway if you're reusing buffers. (ReadableByteStreams definitely are used for reusing buffers in the BYOB reader case; for the naive reader case they can't right now, but e.g. #324 would give a mechanism for doing so). In balance an array buffer view seems better than a custom |
In prototyping, I found that:
complicates the library much.
|
I also considered returning (fulfill the returned promise with) a tuple of the view and the number of filled bytes. But, ... if the consumer wants to fill only part (not aligned to the boundaries of elements) of a TypedArray with non-1 BYTES_PER_ELEMENT, they can still choose to extract the underlying buffer and pass wrapped with a Uint8Array to the BYOB interface. So, maybe, it makes more sense to give a functionality of aligned filling (i.e. returns only when at least one element is fully filled, and with no partially filled elements) to invocation of read(view) with a TypedArray with non-1 BYTES_PER_ELEMENT. |
I see. This is a tough one. Hmm.
Hmm. At first I thought as-is would be reasonable. But I am not sure underlying sources should have to care about this---it would probably be inconvenient for them, and they would always just unwrap it into an ArrayBuffer and/or a Uint8Array or DataView. Maybe underlying sources only get Uint8Arrays? See below.
Agreed. Here are three major possibilities:
What do you think of this analysis? It sounds like #295 (comment) is leaning more toward 2 or 3. Both seem kind of nice, although I am worried about how they are brittle in the face of EOF. I also imagine that they add a lot more complication than 1 does. Maybe 1 is good enough? It is readable byte stream after all, not readable float64 stream. |
(2) sounds good but it disables ability to issue multiple pullInto requests. E.g. suppose we got pullInto0 of 8 byte and pullInto1 of 8 byte. If we controller.respond(2), then the stream wants to re-issue pullInto to continue filling the first view, but the underlying byte source already received pullInto1. So, we need to refrain from issuing multiple pullInto to the underlying byte source. |
Maybe, multiple pullInto() is not something we want to support by the default controller library. I'll make only one pullInto issued at a time. |
Oh, yeah, I'd always been kind of assuming only a single pullInto at a time instead of allowing multiple concurrent. When thinking about read(2) and friends it seems like multiple pullIntos could be bad, but please correct me if I'm wrong. |
Thanks for the comment. I'm updating https://github.com/whatwg/streams/tree/bytestream to adopt that. I did bunch of refactoring, so maybe it's easier to read now (3f7fd74 is the latest green branch https://github.com/whatwg/streams/blob/3f7fd74816be1fc529acf45bc567931265978965/reference-implementation/lib/readable-byte-stream.js https://github.com/whatwg/streams/blob/3f7fd74816be1fc529acf45bc567931265978965/reference-implementation/test/readable-byte-stream.js). Not yet ready for full review, but FYI. |
Reference implementation in https://github.com/whatwg/streams/tree/bytestream is now ready for review. Current implementation is kinda combination of (2) and (3) of domenic's idea. When getByobReader() and read(view) is called, underlyingByteSource.pullInto() is called. When getReader() and read() is called, underlyingByteSource.pull() is called. As far as the stream is readable and controller.close() has not yet been called, the underlyingByteSource can call controller.enqueue(). If there's pending pullInto call, the underlyingByteSource can call controller.respond(). If byobReader.read(new Float64Array(2)) is called when:
When there is a pending read request of new Float64Array(2) with 0 byte filled:
When there is a pending read request of new Float64Array(2) with 1 byte filled:
When to call pull or pullInto again:
|
Awesome! I'll do a PR so I can do some line comments.
What is the motivation for having pull() at all, instead of just using pullInto()? I imagine there is one and I just forgot it. Let's be sure to write it down, either in FAQ.md or in the spec or something?
These sound pretty great. So to be clear, pullInto() always gets a Uint8Array? I like that.
This one is a big strange. I am not sure I understand the reasoning. Maybe instead we should just give the buffer back as a zero-length view? |
Create #363 for reminder.
Yes. buffer, offset and length.
Hmm, which point? We require the argument of |
I guess I kind of was thinking that close() should automatically do respond(0) for you. But actually that is perhaps not a good idea. Nevermind! |
Yeah, we could. But it's possible if the underlying byte source wants to issue |
OK, I think we got this tied off; closing again. |
- For now, most of the spec side texts are removed. To be added by a separate patch - Added some abstract operations on typed arrays to be used by the readable byte stream algorithms - Added ReadableByteStreamByobReader - The underlying byte source instance is now owned by the controller - The interface exposed to the underlying byte source has been revised to have less constraints - Handles typed arrays with multibyte elements as discussed in #295
Per #253 the "bring your own buffer" reader for ReadableByteStream will generally be of the form
The reason for an ArrayBufferView for the argument is that it allows authors to supply the (buffer, offset, maxLength) trio all at once. The reason for the ArrayBufferView for the return type is that it gives back (buffer, bytesRead), plus the offset if one was supplied, all at once.
For the input, there are a few possibilities:
Uint8Array
andDataView
are the big candidatesDataView
), with nominal typing{ buffer, byteOffset, byteLength }
structure; all array buffer views match this, but it would also allow user-supplied structures if they only have a buffer handy (and we could even default byteOffset = 0 and byteLength = buffer.byteLength).I think the most lenient option here is probably best: allow structural array buffer views or array buffers directly. Alternately we could just allow structural array buffer views. The only difference is that the former allows
.read(ab)
whereas the latter requires.read({ buffer: ab })
.For the return type, I think our choices are:
Uint8Array
orDataView
. HereDataView
feels a bit more general---less like "picking favorites". (Why notInt8Array
? Why notUint8ClampedArray
?) But it is also less useful, I think---it doesn't have the nice array methods likemap
/filter
/etc., or the typed array methods likeset
, and it exposes you to endianness concerns (I think)...I think
Uint8Array
is best here.The text was updated successfully, but these errors were encountered: