I was building a CLI for GitHub code search which was using promises pretty heavily as the control flow of the application. It worked well enough in the beginning because the network library was promise based. However I wanted to add a feature where you could query the JSON returned by the GitHub API using json-query. This required that the user enter a REPL-type interaction, where they can enter a command and see the output, rinse and repeat until they have the data they want.
The problem is that all of the I/O in node is non-blocking. This means that all the libraries i looked at to capture user input from the command line were all either prommise based, or used node style method(data, callback)
invocations. This ruled out my first option: using a while
loop inside a promise to repeat the REPL until the user entered 'done'. If I did this I'd end up firing hundreds or thousands of calls to readline
(the library I ended up using) to gather user input. No bueno.
The problem seemed solvable via promises, so I started hacking out a rough layout of what I needed:
main_wrapper_fn
determine_if_we_exit(promise_result)
if _some_condition
resolve_outter_promise
else
loop
loop
do_something().then(determine_if_we_exit)
A few things became clear after I roughed out this psuedo code.
- I'd need for the
main_wrapper_fn
to return a promise, so that it can be resolved and the loop can terminate and hand control back to the promise flow. - there is potentially a circular dependency between whatever determines if we exit and the loop. There needs to be some abstraction or indirection there.
- My psuedo code looks a lot like ruby
From the final javascript code you can see how I solved both of these issues. The loop
method does return a Promise
object, that was simple enough. I created a very small class LoopProcessor
that takes a pointer to the next iteration of the loop but is within the same scope as the outter promise so it can terminate the promise by calling resolveLoop
. Then in getInputFromUser
(which is now our main loop code) we instantiate the LoopProcessor
, setup the next iteration call and have the next
method get called after our user enters their information.
This is a pretty neat pattern, I've already used it to wrap calls to axios to download all the pages in GitHub search results automatically without needing any loops.