Why does Javascript readline question
method read more than one line when wrapped in a Promise?
The code below is supposed to simply add line numbers to the input. It works as expected if I run it and type the input at the command line. However, if I redirect a file into the process, then it consumes the entire file at once. Why is that?
Expected output:
Next line, please: File line 1
1 File line 1
Next line, please: File line 2
2 File line 2
Next line, please: File line 3
3 File line 3
Next line, please: File line 4
4 File line 4
Next line, please: Input stream closed.
Observed output (when running node testReadline.mjs < the_file.txt
)
Next line, please: File line 1
File line 2
File line 3
File line 4
1 File line 1
Next line, please: Input stream closed.
It appears to be consuming the entire file after the first call to question
, rather than consuming only one line at a time.
(I know there is the readline/promises
package. I'm curious why the code below doesn't behave as expected.)
import * as readline from 'node:readline';
const io = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
io.on('close', () => {console.log("Input stream closed.")});
let questionWrapper = (prompt) => {
return new Promise((resolve, reject) => {
io.question(prompt, (line) => {
resolve(line)
});
});
}
let printLine_await = async () => {
let line_num = 1;
while(true) {
let line = await questionWrapper('Next line, please: ');
console.log(`${line_num}\t${line}`)
line_num++;
}
}
printLine_await(1)
For what it's worth, I get the expected result when using callbacks.
This code
let printLine_callback = (line_num) => {
io.question('Next line, please: ', (line) => {
console.log(`${line_num}\t${line}`)
printLine_callback(line_num + 1)
})
}
Produces this result:
Next line, please: File line 1
1 File line 1
Next line, please: File line 2
2 File line 2
Next line, please: File line 3
3 File line 3
Next line, please: File line 4
4 File line 4
Next line, please: Input stream closed.
It's not clear from the documentation what is supposed to happen if the input ends while question
is waiting; but, the behavior I see makes sense in this case (where the file ends with a newline).
As @Kaiido noticed, if the input stream data
event contains multiple lines together (as will be the case when piping a file into the process, with a stream chunk size much larger then the average line length), the line
events fire all at once. It's just a synchronous loop.
The question
method more or less attaches the callback as a once-only line
event handler. (It will be called instead of firing a line
event, but that's just a detail). The problem with using a promise to wait for the question's answer is that when the promise is fulfilled, it takes an extra tick of the event loop until the code after the await
gets to run, which would attach the next line
handler. But with multiple lines entered at once, those line
events will have fired before the question()
could be called again. You can verify this by adding
io.on('line', line => { console.log("Ignore\t"+line); });
Is there something I can do to get the expected behavior?
When using the question()
method, not really. However, what it does is rather trivial, so you can replace it by your own working version. I would recommend to use the async iterator of readline
which does the necessary buffering and can be consumed using async
/await
syntax. For your demo, the equivalent code would be
import * as readline from 'node:readline';
const io = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
let line_num = 1;
io.setPrompt('Next line, please: ');
io.prompt()
for await (const line of io) {
console.log(`${line_num}\t${line}`)
line_num++;
io.prompt()
}
console.log("Input stream closed.")
More generally, for scripts that are not just a single loop, I'd do
const iterator = io[Symbol.asyncIterator]();
function question(query) {
const old = io.getPrompt();
io.setPrompt(query);
io.prompt();
const {done, value} = await iterator.next();
io.setPrompt(old);
if (done) throw new Error('EOF'); // or something?
return value;
}