uncategorized

Generators and Promises

With the release of Node .12 exposing many ES 6 Harmony I decided to try to grok generators, in particular using them for flow control with promises.

I found a pretty good starting point here.

I created a serial promise runner a while back. The basic idea is to have the ability to run a sequrence of promise based tasks and then return the results much like Promise.settle (bluebird) or Promise.allSettled (q) does except instead of running the tasks in parallel, run them in sequence, bailing on the first error like Promise.all.

The solution worked fine but there was little more “bookkeeping” than I liked so I wanted to see if things were cleaner using generators.

Indeed it is much simpler!

Here is the resulting code:

1
serialRunner.prototype.runTasksGenerator = function (aTasks) {
	'use strict'
	var self = this;
	return new Promise(function (resolve, reject) {
		function *gen(aTasks) {
			for (let task of aTasks) {
				task.args.push(self.results);
				let res = yield task.funct.apply(null, task.args);
				self.results[task.name] = {success: true, results: res};
			}
		}

		Promise.coroutine(gen)(self.getTasks())
			.then(function (results) {
				resolve(self.results);
			})
			.catch(function (err) {
				resolve({results: self.results, error: err});
			})
	})
}

We pass an array of tasks in and return a promise. The promise is resolved when the sequence is finished or there is an error. Each task is an object with three members:

  1. the name of the task
  2. The function to call which returns a promise
  3. An array of arguments the function expects as input.

Why not pass in the promise you might ask. You cannot do that if you want to run sequentially. As soon as you create the promise it is on the event loop and starts running, essentially running all tasks in parallel.

With the generator (*gen) we iterate over the array and yield to the promise, resuming only after the promise is resolved, capturing the results as we go. (We actually pass the results received in prior tasks to subsequent tasks in case they are necessary).

But wait, you say, I don’t see any calls to next() to resume the generator. That is all abstracted away in the promise (bluebird) library’s coroutine. coroutine takes a generator function as input and returns a function. When that function is invoked it returns a promise which is resolved when the generator has finished iterating.

The individual task is invoked within gen*

1
task.args.push(self.results);
let res = yield task.funct.apply(null, task.args);
self.results[task.name] = {success: true, results: res};

Notice that we push the current results onto the array of arguments the function expects and then just yield to that function, calling apply on the function. When “funct”, which is a promise, resolves we captures the results for that task and hang them on the overall results hash.

When the generator is done, the coroutine promise resolves and we can then resolve the promise returned when we call runTasksGenerator.

I suspect there is probably a function buried in the promise libaries or in an npm module to do this so I probably reinvented the wheel. That really doesn’t bother me though since I now understand a bit more about generators and writing 20 lines of code to do that was well worth it.

Share