denoland / deno

A modern runtime for JavaScript and TypeScript.
https://deno.com
MIT License
96.42k stars 5.33k forks source link

ReadableStream.pipeTo doesn't always work with Deno streams. #25050

Closed sigmaxipi closed 2 months ago

sigmaxipi commented 2 months ago

Version: Deno 1.45.5 on Linux and OSX

The basic use case of an echo from stdin -> stdout works:

const stringStream = ReadableStream.from(Deno.stdin.readable);
await stringStream.pipeTo(Deno.stdout.writable);

Similarly, echoing to console.log works (though it outputs an unwanted encoding as expected)

const consoleStream = new WritableStream({ write(chunk) { console.log(chunk); } });
const stringStream = ReadableStream.from(Deno.stdin.readable);
await stringStream.pipeTo(consoleStream);

I can also pipeTo from an arbitrary iterable as expected:

const consoleStream = new WritableStream({ write(chunk) { console.log(chunk); } });
const stringStream = ReadableStream.from([1,2,3]);
await stringStream.pipeTo(consoleStream);

However, I can't pipe from an arbitrary iterable to a Deno stream:

const stringStream = ReadableStream.from([1,2,3]);
await stringStream.pipeTo(Deno.stdout.writable);

fails with

error: Uncaught (in promise) TypeError: expected typed ArrayBufferView
    at Object.write (ext:deno_web/06_streams.js:1146:20)
    at Module.invokeCallbackFunction (ext:deno_webidl/00_webidl.js:981:16)
    at WritableStreamDefaultController.writeAlgorithm (ext:deno_web/06_streams.js:3914:14)
    at writableStreamDefaultControllerProcessWrite (ext:deno_web/06_streams.js:4484:55)
    at writableStreamDefaultControllerAdvanceQueueIfNeeded (ext:deno_web/06_streams.js:4387:5)
    at writableStreamDefaultControllerWrite (ext:deno_web/06_streams.js:4531:3)
    at writableStreamDefaultWriterWrite (ext:deno_web/06_streams.js:4678:3)
    at Object.chunkSteps (ext:deno_web/06_streams.js:2742:17)
    at readableStreamFulfillReadRequest (ext:deno_web/06_streams.js:2584:17)
    at readableStreamDefaultControllerEnqueue (ext:deno_web/06_streams.js:1755:5)

The only thing I've been able to use .pipeTo(Deno.stdout.writable) with is Deno streams like a file or Deno.stdin. This appears to go against the spec for ReadableStream.from which takes in an arbitrary iterable.

BlackAsLight commented 2 months ago

However, I can't pipe from an arbitrary iterable to a Deno stream:

const stringStream = ReadableStream.from([1,2,3]);
await stringStream.pipeTo(Deno.stdout.writable);

fails with

error: Uncaught (in promise) TypeError: expected typed ArrayBufferView
    at Object.write (ext:deno_web/06_streams.js:1146:20)
    at Module.invokeCallbackFunction (ext:deno_webidl/00_webidl.js:981:16)
    at WritableStreamDefaultController.writeAlgorithm (ext:deno_web/06_streams.js:3914:14)
    at writableStreamDefaultControllerProcessWrite (ext:deno_web/06_streams.js:4484:55)
    at writableStreamDefaultControllerAdvanceQueueIfNeeded (ext:deno_web/06_streams.js:4387:5)
    at writableStreamDefaultControllerWrite (ext:deno_web/06_streams.js:4531:3)
    at writableStreamDefaultWriterWrite (ext:deno_web/06_streams.js:4678:3)
    at Object.chunkSteps (ext:deno_web/06_streams.js:2742:17)
    at readableStreamFulfillReadRequest (ext:deno_web/06_streams.js:2584:17)
    at readableStreamDefaultControllerEnqueue (ext:deno_web/06_streams.js:1755:5)

This code looks to be working as expected. Deno.stout.writable expects the chunks to be of type Uint8Array, but you're providing it chunks of type number.

sigmaxipi commented 2 months ago

It seems unexpected since I thought the purpose of the stream API was to allow combining of arbitrary streams. Should there be an implicit conversion from the iterable emitted from ReadableStream.from to the Deno.stdout.writable WritableStream<Uint8Array> stream? Or should this conversion be done via an explicit TransformStream before the .writable?

sigmaxipi commented 2 months ago

Also, it's possible that I'm misunderstanding the Stream API, but I didn't find anything about the nature of the data types involved other than it needs to be an iterable.

sigmaxipi commented 2 months ago

Looking at the Stream API docs and Deno source code some more, it appears that I misinterpreted the API. What I should have done was something like await ReadableStream.from([new Uint8Array(new TextEncoder().encode("TEXT").buffer)]).pipeTo(Deno.stdout.writable);

BlackAsLight commented 2 months ago

It seems unexpected since I thought the purpose of the stream API was to allow combining of arbitrary streams.

The purpose of streams is to allow working with large amounts of data in a linear fashion while maintaining a small memory footprint. If you had a large CSV file for example, instead of loading it all into memory, assuming JavaScript would allow an object that big, you could consume it row by row as if the information was on a conveyor belt.

All streams expect information to be in a certain form. Just like a function with signature (a: number, b: number) => number, it would make no sense to pass it two arrays.

Looking at the Stream API docs and Deno source code some more, it appears that I misinterpreted the API. What I should have done was something like await ReadableStream.from([new Uint8Array(new TextEncoder().encode("TEXT").buffer)]).pipeTo(Deno.stdout.writable);

new TextEncoder().encode() returns a Uint8Array So you could have just done

await ReadableStream.from([new TextEncoder().encode('TEXT')])
    .pipeTo(Deno.stdout.writable, { preventClose: true })

or alternatively you could pipe it through TextEncoderStream

await ReadableStream.from(['TEXT'])
    .pipeThroug(new TextEncoderStream())
    .pipeTo(Deno.stdout.writable, { preventClose: true })