Repository navigation
feat: add Blob/Body textStream() - #35616
Conversation
Implements the `textStream()` convenience method that returns a `ReadableStream<string>` produced by piping the binary body/blob stream through a new `TextDecoderStream` (always UTF-8, ignoring any Content-Type/charset), per whatwg/fetch#1862 and w3c/FileAPI#221. - `Blob.prototype.textStream()` (inherited by `File`) in ext/web/09_file.js, using a lazy `TextDecoderStream` wrapper that mirrors the existing lazy `ReadableStream` wrapper so the streams polyfill is not loaded eagerly at module-body time. - `textStream()` on the fetch `Body` mixin (Request/Response) in ext/fetch/22_body.js. Throws `TypeError` when the body is already consumed; a null body yields an empty, already-closed stream that reads to "". - Type declarations in lib.deno_web.d.ts and lib.deno_fetch.d.ts. - Unit tests in tests/unit/blob_test.ts and tests/unit/body_test.ts covering UTF-8 (multi-byte) decoding, Response/Request bodies, the consumed-body TypeError, and the null-body empty-stream case. Closes denoland#34792
|
It looks like we need to update the wpt expectations (#35803) |
The Blob-textStream WPT tests and the textStream() idlharness subtests were
listed as expected failures. Now that textStream() is implemented they pass,
which the WPT runner reports as a failure ("expected test failures that
passed"). Flip the two Blob-textStream files to passing and drop the
textStream() entries from the idlharness expectedFailures lists.
|
Pushed 89fb367 to fix the failing
|
bartlomieju
left a comment
There was a problem hiding this comment.
Checked the implementation against the merged spec algorithms — the unusable-first check, the null-body branch and the always-UTF-8 decoder all line up, and inner.stream materializing static bodies means pipeThrough is safe on that path. One small deviation inline.
Unrelated to the code: both proposals have merged (whatwg/fetch#1862 and w3c/FileAPI#221), so the "in-progress standards proposals" framing in the PR description is out of date — worth fixing since that text becomes the squashed commit message.
| let stream; | ||
| if (inner !== null) { | ||
| stream = inner.stream; | ||
| } else { | ||
| // A null body yields an empty, already-closed stream so that reading | ||
| // it to completion produces "". | ||
| stream = new ReadableStream(); | ||
| readableStreamClose(stream); | ||
| } | ||
| return stream.pipeThrough(new TextDecoderStream()); |
There was a problem hiding this comment.
Small deviation from the spec here. The null-body branch in the merged algorithm creates an empty stream, closes it, and returns it — it doesn't fall through to the decoder:
- If this's body is null:
- Let emptyStream be a new ReadableStream in this's relevant realm.
- Set up emptyStream.
- Close emptyStream.
- Return emptyStream.
The piping is only steps 3-6, on the non-null path. It isn't observably different — a closed stream piped through TextDecoderStream also yields zero chunks, since the transform's flush guards with if (final) — but returning it directly matches the spec exactly and avoids constructing a TextDecoderStream (and therefore a TransformStream, which is what actually pulls in the streams polyfill) for a body that has nothing to decode.
| let stream; | |
| if (inner !== null) { | |
| stream = inner.stream; | |
| } else { | |
| // A null body yields an empty, already-closed stream so that reading | |
| // it to completion produces "". | |
| stream = new ReadableStream(); | |
| readableStreamClose(stream); | |
| } | |
| return stream.pipeThrough(new TextDecoderStream()); | |
| if (inner === null) { | |
| // A null body yields an empty, already-closed stream. Per the spec | |
| // this is returned as-is — no decoder is set up for it. | |
| const emptyStream = new ReadableStream(); | |
| readableStreamClose(emptyStream); | |
| return emptyStream; | |
| } | |
| return inner.stream.pipeThrough(new TextDecoderStream()); |
Per the merged textStream() algorithm, the null-body branch returns the empty closed stream as-is rather than piping it through a decoder. Not observably different, but it matches the spec and avoids constructing a TextDecoderStream (and hence a TransformStream, which pulls in the streams polyfill) for a body with nothing to decode.
|
Applied the suggestion in 9d68dbf — the null-body branch now returns the closed empty stream directly instead of falling through to the decoder, matching steps 2.1-2.4 of the merged algorithm and skipping the One adjustment to the suggested snippet: the Also rewrote the PR description to reflect that both proposals have merged — it now links the merged algorithms directly rather than describing them as in-progress.
|
Closes #34792 --------- Co-authored-by: Nathan Whitaker <nathan@deno.com> Co-authored-by: Bartek Iwańczuk <biwanczuk@gmail.com>
Implements the
textStream()convenience method onBlob/Fileand on the fetchBodymixin (Request/Response), per the standard algorithms:Body.textStream()(Add a textStream() method to the Body mixin whatwg/fetch#1862)Blob.textStream()(AddtextStream()method toBlobinterface w3c/FileAPI#221)textStream()returns aReadableStream<string>obtained by piping the binary body/blob stream through a freshTextDecoderStream. It always decodes as UTF-8 (ignoring anyContent-Type/charset), making it a convenient shorthand forobj.stream().pipeThrough(new TextDecoderStream()).Changes
ext/fetch/22_body.js— addtextStream()to themixinBodymixin. ThrowsTypeErrorif the body is unusable (already consumed/locked) and returns an empty, already-closed stream when the body is null.ext/web/09_file.js— addBlob.prototype.textStream()(inherited byFile).TextDecoderStreamis loaded lazily, mirroring the existing lazyReadableStreamwrapper, to preserve the deferred-load of the streams polyfill at startup.cli/tsc/dts/lib.deno_fetch.d.ts/cli/tsc/dts/lib.deno_web.d.ts— type declarations.tests/unit/blob_test.ts,tests/unit/body_test.ts— unit tests covering UTF-8 (multi-byte) decoding, consumed-bodyTypeError, and the null-body case.Closes #34792