oven-sh / bun

Incredibly fast JavaScript runtime, bundler, test runner, and package manager – all in one
https://bun.sh
Other
74.18k stars 2.77k forks source link

Calling `next` on `console`'s asyncIterable causes its stream to become locked #7541

Open robobun opened 11 months ago

robobun commented 11 months ago

I need to read the first line from stdin, perform some logic, and then iterate over the remaining lines of stdin. The suggested way from the docs (https://bun.sh/docs/api/console) to iterate over stdin lines is using its asyncIterator. But there's no suggested method to get a single line.

I attempted to call the asyncIterator method directly:

let firstLine = (await console[Symbol.asyncIterator]().next()).value;
// perform logic on firstLine
for await (const line of console) {
  // perform logic on remaining lines
}

Defining firstLine this way works. However it will fail on const line of console with the error ReadableStream is locked It seems that manually retrieving an item from console's asyncIterator causes its stream to become locked, and I'm unsure how to unlock it.

I tried using the prompt function with an empty string as the message. But it seems prompt will still print a single space character to stdout if called with an empty string as the message. So it's not ideal.

The best working solution I have right now is to write a for loop that I break out of after one iteration.

for await (const line of console) {
  // perform logic on first line
  break;
}
for await (const line of console) {
  // perform logic on remaining lines
}

I'd appreciate any suggestions on how to unlock the ReadableStream, or how to more elegantly handle this use case.

Originally reported on Discord: Callingnextonconsole's asyncIterable causes its stream to become locked

paperdave commented 11 months ago

ConsoleObject.ts line 2 defines the async iterator. i touched this code last and probably made some mistakes. i feel like this is "semi" a good first issue but it requires some knowledge on async iterators and readable streams.

for anyone working on this, note that you have to rebuild bun to see changes in this file apply.

guest271314 commented 11 months ago

You can do something like this.

const decoder = new TextDecoder();
process.stdin.on("readable", () => {
  let input = process.stdin.read();
  const lines = decoder.decode(input).split(/\\n|\\r\\n/);
  const firstLine = lines.shift();
  console.log({ firstLine });
  console.log({ lines });
});

I wouldn't mix Node.js Streams implementation with WHATWG Streams. Two different implementations.

Keep in mind ECMA-262 does not define STDIO. JavaScript engines and runtimes implement STDIO differently. From the tests I've done no two JavaScript runtimes implement STDIO the same.

guest271314 commented 11 months ago

I'd appreciate any suggestions on how to unlock the ReadableStream, or how to more elegantly handle this use case.

For a WHATWG Streams version you can pipe the ReadableStream through a TextDecoderStream() then pipe to a WritableStream() and split on newline character, something like

new Blob(["a\nb\nc\n".repeat(100)])
  .stream()
  .pipeThrough(new TextDecoderStream())
  .pipeTo(new WritableStream({
    start: () => {
      this.firstLineRead = false;
      this.lines = [];
    },
    write: (value) => {
      const data = value.split(/\n|\r\n/);
      if (this.firstLineRead === false) {
        const firstLine = data.shift();
        this.firstLineRead = true;
        console.log({
          firstLine
        });
      }
      this.lines.push(...data)
    },
    close: () => {
      console.log({
        lines: this.lines
      });
      console.log("Stream closed");
    }
  }));
mirismaili commented 7 months ago

From my question in stackoverflow:

const stdinIterator = process.stdin.iterator()

console.write('What is your name?\n> ')
const userName = (await stdinIterator.next()).value.toString().trimEnd()
console.log('Hello,', userName)

console.write(`What would you like, ${userName}?\n> `)
const answer = (await stdinIterator.next()).value.toString().trimEnd()
console.log('Do something with answer:', answer)

It works. But then the process is not terminated automatically (I need to press Ctrl+C manually).