Search code examples
javascriptnode.jsasynchronousio

How to override previous rl.qu/estion with new one temporarily?


Note: I apologize for the "qu/estion" however stackoverflow blocked me from putting "question" in my title.

How do you temporarily override a previously asked rl.question with a new one that resolves and the old one can come back? Here is an example:

const readline = require('readline')
const rl = readline.createInterface({
  input: process.stdin,
  output: process.stdout
})

function log(message) {
  readline.cursorTo(process.stdout, 0)
  console.log(message)

  rl.prompt(true)
}

function ask() {
  rl.question('> ', async function(answer) {
    log(answer)
    if (answer === 'testfunc') {
      await test()
    }
    ask()
  })
}

async function test() {
  await setTimeout(() => {
    log('Attempting')
    rl.question('can you see this? > ', function(answer) {
      log('This answer: ' + answer)
    })
  }, 3000)
}

ask()

The log + ask() function allows for an in-process command line of sorts, letting you to always have the > at the bottom of the output and be able to always have access to commands.

I have a function, test(), that is async, and it wants to ask its own questions. I want the question within this async function to temporarily override the "permanent" one until an answer is given then the "permanent" one can have priority back.

Currently, the behavior is kind of interesting. At first glance, it seems like the second question has no effect, however if you type a few letters before the 3 seconds of wait time happens, you'll the see the cursor moved back to the beginning of the line, however it has no other real effect.

Edit: I do want the main rl.question available while the async function is running, except for when an rl.question is asked within the async function.

Edit 2: Expected behavior

1) you can write anything in for the main ask() question and it will just repeat.

2) If you write testfunc, it will go to the main ask() question, and allow you to type in it, until the 3 seconds on testfunc are done.

3) After this, testfunc's rl.question will trigger and override the main question.

4) Once testfunc's question is answered, the async function could continue if needed, and the main ask() question would still be available.


Solution

  • I believe you mention the core readline package of Node. It seems to take a callback so if you go the promise (async - await way) you need to define the event listener callback multiple times.

    So first thing first you may start with promisifying the rl.question() function. One way of doing that could be.

    var rlQuestion = q => new Promise(v => rl.question(q, a => (rl.close() , v(a))));
    

    only then you may do like

    var answer = await rlQuestion('What do you think of Node.js? ');
    

    OK... Here we carry on with some further detail. The function following the await command should return a promise. So refractoring your your codebin code into something like below, i believe you should now be able to see better how it works.

    const readline = require("readline"),
          rl       = readline.createInterface({ input : process.stdin,
                                                output: process.stdout
                                                }),
          rlq      = q => new Promise(v => rl.question(q, a => v(a))),
          log      = (...m) => {readline.cursorTo(process.stdout,0);
                                console.log(...m);
                                rl.prompt(true);};
    async function test(){
      return new Promise(v => setTimeout(async function(){
                                                 log("Attempting");
                                                 let answer = await rlq("Can you see this? >");
                                                 log("Answer: " + answer);
                                                 v(answer);
                                               }, 3000));
    }
    
    async function ask(){
      let answer = await rlq("> ");
      answer = await (answer === "testfunc" ? test()
                                            : Promise.resolve(answer));
      log("ender ", answer);
      /^q$|^quit$/i.test(answer) ? log("bye..!"): ask(); // the "test" function here is RegExp.test..!}
    }
    ask();
    

    Edit as per your comment;

    As I understand the async function (which is currently simulated to take 3 seconds) might last indefinitelly longer and during that period you want your prompt back right. Then obviously you are dealing with two separate asynchronous timelines (one for the normal console input and the second is the async process which may take an unknown amount of time) where both target the same entry point.

    This is problematic since when test() function finalizes and invokes rlq (promisified rl.question) task the ask() function's rlq task is already at the head of the microtask queue (event loop of promises) and the test() function prompt won't appear. You have to get rid of the ask() question rlq task.

    Enter rl.close(). So first we close the readline interface and then re-create it within the test function. By the way since we do reassignments to rl it can no longer be defined as const but var.

    See if the following works for you.

    var readline = require("readline"),
        rl       = readline.createInterface({ input : process.stdin,
                                              output: process.stdout
                                            }),
        rlq      = q => new Promise(v => rl.question(q, a => v(a))),
        log      = (...m) => {readline.cursorTo(process.stdout,0);
                              console.log(...m);
                              rl.prompt(true);},
        test     = _ => setTimeout(async function(){
                                           log("Attempting");
                                           rl.close();
                                           rl = readline.createInterface({ input : process.stdin,
                                                                           output: process.stdout
                                                                         });
                                           var answer = await rlq("Can you see this? >");
                                           log(`Here i do something with ${answer} inside the test function`);
                                           ask();
                                         }, 3000);
    
    async function ask(){
      let answer = await rlq("> ");
      answer === "testfunc" ? test()
                            : log(`Here i do something with ${answer}`);
      /^q$|^quit$/i.test(answer) ? (log("bye..!"), rl.close())
                                 : ask(); // the "test" function here is RegExp.test..!}
    }
    ask();