Skip to content

Instantly share code, notes, and snippets.

@codeimpossible
Created February 2, 2017 07:27
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save codeimpossible/a7ef122851b7b1e8358cd79ac66a1c66 to your computer and use it in GitHub Desktop.
Save codeimpossible/a7ef122851b7b1e8358cd79ac66a1c66 to your computer and use it in GitHub Desktop.
Looping inside promises

Promise Loops

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.

  1. 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.
  2. there is potentially a circular dependency between whatever determines if we exit and the loop. There needs to be some abstraction or indirection there.
  3. 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.

'use strict';
const readline = require('readline');
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
// helper method, "promisifies" the readline api
const question = (text) => {
return new Promise(resolve => {
rl.question(text + ' ', (answer) => {
resolve(answer);
});
});
};
// a promise compatible loop method
// executes a block of javascript until
// an exit condition is met
let loop = () => {
return new Promise(resolveLoop => {
let LoopProcessor = function(next) {
this.next = (result) => {
if (result === 'done') {
resolveLoop(); // resolve the loop promise
} else {
return next(); // otherwise call the next iteration of the loop
}
};
};
// main loop code, this will execute every iteration
let getInputFromUser = () => {
let loopProcessor = new LoopProcessor(getInputFromUser);
question('Enter any text or "done" to exit: ').then(loopProcessor.next);
};
// kick off the first iteration
getInputFromUser();
});
};
// main entry point, start the loop
Promise.resolve()
.then(loop)
.then(() => console.log('exiting!'))
.then(() => process.exit());
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment