Bernstein: Promises Orchestrated

A few days ago I finished bernstein and released it to the world. I was asked several questions about its use and what problem it solves. So I thought I’d whip up a quick article about what it is and what you might use it for.

First, if you haven’t already checked out the documentation, you should. Bernstein is really pretty simple to use and I think the advantages it provides are pretty compelling.

Second, a little bit of backstory is in order. Why did I write this thing? Bernstein was the result of another library that I am working on (Jayne) needing to be able to execute an arbitrary series of functions sequentially on a given set of data. After refactoring Jayne a bit I realized that the sequential execution part should be stand alone. Thus bernstein was born.

Say you need to execute a series of functions sequentially over the same data set. Rather than chaining all the promises like this:


somePromiseReturningFunction.then(function(data){
    data.num++;
    return data;
}).then(function(data){
    data.num++;
    return data;
}).then(function(data){
    data.num++;
    return data;
}).then(function(data){
    data.num++;
    return data;
}).then(function(data){
    data.num++;
    return data;
}).then(function(result){
    console.log(result);
});

or nesting callbacks, bernstein lets you create an array of functions to be executed in order.

This is really powerful because it allows you to generate stacks of functions (both synchronous and asynchronous) that can be used to transform data over and over again. All without you needing to hard code their execution patterns. Making it easy to change them in the future and simplifying your life as a developer.

Our example becomes:


var funcArray = [function(data, orig, next){
        data.num++;
        next(data);
    }
    , function(data, orig, next){
        data.num++;
        next(data);
    }
    , function(data, orig, next){
        data.num++;
        next(data);
    }
    , function(data, orig, next){
        data.num++;
        next(data);
    }
    , function(data, orig){
        return new Promise(function (resolve, reject) {
                var dataMod = data;
                
                dataMod.num++;
                setTimeout(function () {
                        resolve(dataMod);
                }, 200);
        });
    }]
    
    , addLots = bernstein.create(funcArray);
    
addLots({num: 1}).then(function (result) {
    console.log(result);
});

addLots({num: 11}).then(function (result) {
    console.log(result);
});

addLots({num: 42}).then(function (result) {
    console.log(result);
});

A few things to note about this example. It uses both the promise interface and the callback interface in its array of functions to execute. Authoring in either style will work equally well. Just make sure you resolve your promise or call the next function with your data. Otherwise the world will end.

The promise style lets you execute asynchronous operations in the sequence that you need them to execute in. Once the returned promise resolves its data is passed to the next function.

One last thing of note about the example is that you can call the same stack of functions on multiple data sets.

Both of those examples are interesting, but not particularly useful. So what would you actually use bernstein for in real life?

Let’s say you have a user signup service, that needs to do a few things:

  1. Crop user uploaded images (thumbnail, medium, large, round)
  2. Add metadata to the user object
  3. Get a random quote for the user’s bio until they are allowed to add one later on
  4. Save the user object
  5. Return the saved information to the user’s browser for rendering

So, with a generous helping of pseudo code, the process could look like this.


var userCreationStack = bernstein.create([
    function (data, orig, next){
        var imageProm = imageServiceWrapper.sliceAndDice(data.image);
        
        imageProm.then(function (imageData) {
            data.images = imageData;
            
            next(data);
        });
    }
    
    , function (data, orig, next) {
        next(data.quote = getRandomQuote());   
    }
    
    , function (data, orig) {
        data.type = "free";
        data.createdData = new Date();
        data.interactions = 0;
        
        return new Promise(function (resolve, reject) {
            var userProm = userService.save(data);
            
            userProm.then(function (userObj) {
                resolve(userObj);
            });
        }); 
    }
]);

//inside some handler for a route...
var user = userCreationStack(req.body);
user.then(function (data) {
    response.send(data); 
});

Only do something like this if your steps must be run synchronously. That’s really when you reach for bernstein. If you can use a more asynchronous approach then do it. Embrace that asynchronicity!

Bernstein is for the use cases where you really really need synchronous execution of functions on the same data and you don’t want to worry about hard coding the promise/callback stack or want to use mixed promise/callback style functions.

In closing I give you Leonard Bernstein

(The ever-awesome Blake Johnson provided editing on this bad boy)

Published: 17 Jul 2015 | Tags: js , promises , es6 , es2015 , open source , code