Sunday 6 January 2013

Pausable Async Loop

I already did a write up about async loops in JavaScript a good while ago. These days, while working on a toy project of mine, it occurred to me that it would be useful to be able to pause-continue asynchronous loops. I wrote a first implementation that didn't seem too natural to use, so I decided to write a new non pausable-continuable function to run async functions in a loop, and based on that write the pausable-continuable version.

So, the thing is that we have an asynchronous function, that once finished invokes a callback, and we want to run it n times (each new iteration will be launched when the previous async operation is done, so it will be invoked through the mentioned callback). We'll leverage for this (as so many other times) the power of closures and the arguments pseudo array.

If we don't need the pause-continue functionality, we can write a function like this:

//runs an async function n times
runInLoop = function(fn, times /*, parameters to fn, the last one is a callback, and is compulsory (but can be null)*/){
 var timesDone = 0;
 
 //remove from arguments the 2 first and the last parameters, and add a new callback
 //the new callback calls the old callback and calls the next iteration (_run)
 var _arguments = Array.prototype.slice.call(arguments, 0);
 _arguments.splice(0, 2);
 var initialCallback = _arguments.pop();
 _arguments.push(function(){
  if (typeof initialCallback == "function"){
   initialCallback();
  }
  run();
 });
  
 var run = function(){
  if (timesDone < times){
   debugPrint("iteration: " + timesDone);
   fn.apply(null, _arguments);
   timesDone++;
  }
 };
 run();
};

that will be used like this

var asyncPrint = function(txt1, txt2, onDone){
 console.log("print start: " + txt1 + " - " + txt2);
 setTimeout(function(){
  console.log("print end: " + txt1 + " - " + txt2);
  onDone();
 }, 400);
};

//callback to be invoked once asyncPrint is done
var asyncPrintOnDone = function(){
 console.log("asyncPrintOnDone");
};

runInLoop(asyncPrint, 20, "hi there", "guys", asyncPrintOnDone);

Notice that if we don't want to invoke a callback (asyncPrintOnDone) after the function is done, we need to pass a null parameter (we can't simply skip it), as our runInLoop function needs to distinguish the normal parameters from the (optional) callback. We wrap the invocation to this normal callback and the invocation to the next iteration into a new function. This new function is the one that we'll end up being passed as callback to our function when it's complete.

Now, we can improve this by adding a pause-continue functionality. For this, we'll have a factory function, that returns a new function that can be invoked (just use ()), paused and continued. This returned function is intended to be invoked (started) only once, so further invocations will have no effect (but we can pause-continue it to our will).
The function looks like this:

//creates a "pausable loop" that runs an async function "fn" for x times
runInPausableLoop = function(fn, times /*, parameters to fn, the last one is a callback, and is compulsory (but can be null(*/){
 var paused = false;
 var timesDone = 0;
 
 //remove from arguments the 2 first and the last parameters, and add a new callback
 //the new callback calls the old callback and calls the next iteration (run)
 var _arguments = Array.prototype.slice.call(arguments, 0);
 _arguments.splice(0, 2);
 var initialCallback = _arguments.pop();
 _arguments.push(function(){
  if (typeof initialCallback == "function"){
   initialCallback();
  }
  run();
 });
 var self; //I don't think this _self will be of much use, but well, in case we wanted to use a this
 //the loop should be launched only once, so use this started flag so that on second invocation it does nothing
 var started = false;
 var me = function(){
  if (!started){
   self = this;
   started = true;
   run();
  }
  return me; //to allow chaining
 };
 var run = function(){
  if (!paused && timesDone < times){
   debugPrint("iteration: " + timesDone);
   fn.apply(self, _arguments);
   timesDone++;
  }
 };
 
 //"public functions"
 me.pause = function(){
  debugPrint(fn.name + " paused");
  paused = true;
 };
 me.continue = function(){
  debugPrint(fn.name + " continued");
  paused = false;
  run();
 };
 
 return me;
};

I think the more natural (and elegant) use case is a one liner where the function is created and immediately invoked (that's why the invocation returns the function itself, so that we get a reference that will later on use to pause and stop). Anyway, for cases where you want to store the reference to the returned function for invoking later on, I've also contemplated that you could want to invoke it via myObj.myPausableAsyncLoop... and want it to properly use myObj as "this", so you'll see the "self" pattern in the code.

Let's see it in action:

//async function that we want to run n times in a pausable loop
//the last parameter is a callback to be run after each iteration
var asyncPrint = function(txt1, txt2, onDone){
 console.log("print start: " + txt1 + " - " + txt2);
 setTimeout(function(){
  console.log("print end: " + txt1 + " - " + txt2);
  onDone();
 }, 400);
};

var asyncPrintOnDone = function(){
 console.log("asyncPrintOnDone");
};

var pausablePrintLoop = runInPausableLoop(asyncPrint, 20, "hi there", "guys", asyncPrintOnDone)();

//let's try the pause-continue functionality
setTimeout(function(){
 console.log("pausing");
 pausablePrintLoop.pause();
}, 1000);

setTimeout(function(){
 console.log("continuing");
 pausablePrintLoop.continue();
}, 4000);

Note that on both cases I've tried to do the looped invocation of the async function as similar as possible to a simple invocation:

asyncPrint("hi there", "guys", asyncPrintOnDone);

runInLoop(asyncPrint, 20, "hi there", "guys", asyncPrintOnDone);

var pausablePrintLoop = runInPausableLoop(asyncPrint, 20, "hi there", "guys", asyncPrintOnDone)();

I've uploaded this to GitHub along with a couple of samples (samples to run under node so far, but it should work smoothly on any modern browser)

Update, 2012/01/31

I've done quite a few additions to this code and got it updated in GitHub:

  • We can repeat the Loop by calling its "repeat" method.
  • I've added the possibility of invoking another callback (onLoopDoneCallback) function when the loop is done (this is mainly useful for the repeat case)
  • Now I also contemplate the possibility of this callback or the onIterationDone callback been asynchronous. I identify this by annotating the function with an "isAsync" property
  • The asynchronous function to be run inside the loop could expect as first parameter the iteration index. We let the loop know about this by annotating the function with an "isLoopAware" property.

No comments:

Post a Comment