#Several concerns about a json-streaming example
35 messages ยท Page 1 of 1 (latest)
Dyno got a bit aggressive
I tried to use something more explicit than the default CountQueuingStrategy when instantiating the ReadableStream (typically just a simple
new CountQueuingStrategy({ highWaterMark: 1 }), but I got errors of the kind:
(index):71 Uncaught (in promise) RangeError: Failed to construct 'ReadableStream': Cannot create byte stream with size() defined on the strategythis puzzles me a bit because there is no mention of this behavior in MDN and I couldn't find one in the specification pages associated to those API. in particular, this one default should be the same as the spec mentions, but still raises the error. I don't get it
It also deleted the first post with the actual issue but it's not in the log. Probably a bug of Dyno and forum posts ๐ค
maybe I could post it back ?
Go ahead
hello, I have a small concern about some example code I'm doing. concretely, I have a synchronous iterator of bytes, basically made up as follow:
const stream = (function* chunks(json) {
const jsonString = JSON.stringify(json);
const jsonBytes = new TextEncoder().encode(jsonString);
let index = 0;
while(index < jsonBytes.length) {
const i = index;
const j = Math.min(jsonBytes.length, index + 10);
const slice = jsonBytes.slice(i, j);
index = j;
yield slice;
}
})({ "Hello": [2023, 2024 ]});
my goal is to recreate the data based on the stream above. in order to do that (and since JSON.parse cann't accept stream), I'm using the Response API (and related):
const jsonBack = await new Response(new ReadableStream({
type: "bytes",
start: function(controller) {
for(const chunk of stream) {
controller.enqueue(chunk);
}
controller.close();
}
})).json();
I've followed pages on MDN (https://developer.mozilla.org/en-US/docs/Web/API/Response, https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream/ReadableStream, https://developer.mozilla.org/en-US/docs/Web/API/ReadableByteStreamController)
however, I have several questions:
In general for JSON streaming however, one would use JSONL/NDJSON
^this was the first question
second question is:
since I cannot control the number of chunks that will be stacked before back-pressuring (apparently the default is higherMark=1 but anyway), I made the choice to slice the small buffers as they are emitted. I did right, because modifying this to subarray causes unexpected end of JSON error (not clear why because in my case, I don't mutate the subarray, so that's a bit weird to me. in practice, later, the "big" array will be mutable so I'm all fine with slicing it. I just don't get the reason of error (so changing slice to subarray yields:
VM318:1 Uncaught (in promise) SyntaxError: Unexpected end of JSON input
third question is:
if I use the following data, I got no error due to UTF-8 sequence being truncated:
{"Hello!๐": [2023, 2024 ]} // The emoji happens at index 9 and takes more than 1 UTF-8 => split on 2 chunks
when I use subarray I get an error related to the UTF-8 sequence terminating too soon; when I use slice , I don't get that behavior. maybe it relates with the previous question about slice vs subarray. I took the time to review the spec of both methods but they don't seem different (except slice copies the buffer if I got it right, while subarray only makes a view of it): they don't seem to differ in the way they handle the start and end parameter
is it safe to split UTF-8 sequences like this while streaming? (I would assume that might really well happen in case of a chunked-http rquest but I prefer to ask)
2nd and 3rd are the same EDITed
my bad
and then very last question:
Is the usage of start function legit here? The example on MDN used one of the same kind (using a timer but essentially the same idea). I cannot use pull in my case, so the current behavior of start is kind of crucial. is it safe to directly enqueue in the controller when the stream starts, without relying on pull at all?
(sorry for the long post)
I wanted to find my way through built-ins api's, for the entertainment
(2nd question) From what I understand, you can't parse with .subarray because it checks the updated arraybuffer, which changes on each chunk received
While with a copy you don't mind this update
(3rd question) It's not safe to consider the last byte of a chunk as valid UTF-8, you would need to check if it's part of a surrogate
so just so I understand:
if I pass to the controller a bytes.subarray(i, j), the controller can modify the original bytes (because it's a subarray, not a slice). but for the next iteration, I don't modify the same subarray, right? it's another subarray of bytes, on an independent segment
yes the third question, since it only occurs using subarray and not slice, might be just the same kind of behavior as for the second question, more or less with the same error message I assume.
the code with slice works fine even with the emoji, so I guess the underlying collecting engine works correctly and is robust to those cases
no I really don't get it. what ever I do to compare both versions, the arrays passed to the controller look exactly he same (except ofc one is a lice and the other is a subarray)
const stream = (function* chunks(json) {
const jsonString = JSON.stringify(json);
const jsonBytes = new TextEncoder().encode(jsonString);
let index = 0;
while(index < jsonBytes.length) {
const i = index;
const j = Math.min(jsonBytes.length, index + 10);
const slice = jsonBytes.subarray(i, j); // This makes it crash. slice does not
index = j;
yield slice;
}
})({"Hello!": [2023, 2024 ]});
const jsonBack = await new Response(new ReadableStream({
type: "bytes",
start: function(controller) {
for(const chunk of stream) {
controller.enqueue(chunk);
}
controller.close();
}
})).json();
ok, for the CountQueuingStrategy issue, this is this part of the spec:
https://streams.spec.whatwg.org/#rs-prototype
in "bytes" mode, I cannot configure it, apparently
The situation on Firefox game me a hint:
When using .subarray instead of .slice, the error is:
SyntaxError: JSON.parse: unexpected end of data at line 1 column 11 of the JSON data
which is one byte after the chunk size. so I digged a bit deeper about how the BytesStreamController was handling .enqueue:
https://streams.spec.whatwg.org/#readable-byte-stream-controller-enqueue
I feel like something will be wrong at step 3. not too sure what they mean by viewedArrayBuffer, because my chunks never have that property
but if the implementations inspect the .buffer property of the typed array, then it starts to make a bit of sense, because in the .subarray case and in the .slice case, they differ. maybe there is something wrong in the way TransferArrayBuffer is then created or something
more generally, maybe the mistake I made is that I shouldn't be passing a TypedArray to .enqueue, but a correctly shaped ArrayBuffer instead
TypeError: Failed to execute 'enqueue' on 'ReadableByteStreamController': parameter 1 is not of type 'ArrayBufferView'
so passing an array buffer directly won't work, I need to pass a view always. I think there is something off in the way the algorithm is written then, because as a functionality, it just feels so bad the behavior depends on whether or not the chunk is a subarray or a slice
all that just doesn't make any sense. wtf is going on when .subarray
=======
ok, I've found the issue
@half robin if ever you're interested, the reason (I believe) is subtly explained here:
doing .enqueue actually detaches the buffer (confirmed by debugging the detached property before and after the call)
I believe that's why in the subarray case, the buffer gets detached after chunk 1 is enqueued, and then nothing is appened in the queue anymore
(although as per the spec, a TypeError should have been raised... I never got it. not sure why)
I never got the issue because when a buffer is detached, its length goes down to 0. therefore, my loop abruptly terminates and no chunk is emitted anymore, creating an early closure of the stream
Nice to know. One fewer reason to use .subarray and go with copies ๐
basically, I needed that functionality because I'm creating a wasm module that outputs JSON. it does it by writing in the memory buffer that represents the memory; this one has fixed size. so it basically prints until the buffer is full, and then flush the buffer
flushing the buffer is a JS external method that triggers the machinery I've sketched, more or less (just phrased differently to become callback friendly). so the output json can get streamed as it is flushed by the wasm module
(I didn't want to rely on a third-party, if there was a straightforward way to do it with built-in's api. I'm quite satisfied with the current solution, as it's really short in the end. just took me a bunch of time to really understand what was going on, but I guess that's not lost knowledge)
You probably know more about buffers now that the majority of this server