Promises: So We Rewire it!

We’ve been doing asynchronous code all wrong

The more I learn, the more I realize that old ways of doing things just aren’t capable enough.  For instance, JavaScript developers are taught to write asynchronous code using callback functions:

function asyncDriver(callback) {
    controller.goDoSomething(var1, var2, function whenDone(result) {
        controller.doSomethingElse(var3, function whenThatIsDone(result) {
            controller.etc(var4, callback);
        });
    });
}

This code is ugly, and hard to think about.  Wouldn’t it be nice to write:

function asyncDriver(callback) {
    var p = Task.spawn(function() {
        yield controller.goDoSomething(var1, var2);
        yield controller.doSomethingElse(var3);
        let result = controller.etc(var4);
        throw new Task.Result(result);
    });
    p.then(callback, reportError);
}

Why, yes, it would!  This is what Promises and Task.jsm bring to us.  It makes really messy code easy to read again.  The yield statements here force the function to pause in the middle of its operation, under the assumption that each value yielded is a Promise object.  When the promise “resolves”, the function continues with the promise’s resolved value.  A sequence of nested asynchronous functions becomes one function which looks synchronous (but is really still asynchronous).

This was what I used to write a JSONStore module for addons to use as a replacement for preferences – a way to store data and settings, and get them back when you need them.  There are some bugs, and this is just one of many ideas being floated for addon settings.  Discussion on the new module is ongoing in bug 952304 – it’s not part of Mozilla yet and won’t be for some time.  Use at your own risk.

But that’s not the whole point of this post.  I’m going a step further.

Promises, meet transactions

Promises are great when dealing with asynchronous operations… but if they fail and you want to roll back what has already happened, what do you do?  Being able to undo an operation is pretty important in a few environments, especially editing multiple files at once.

Now I like Mozilla’s native transaction manager API.  It works well for what it was designed for.  But it wasn’t designed for asynchronous operations.  Nor can I use the transaction manager in a chrome worker thread, because I can’t access XPCOM from chrome workers.  (There’s good reason for that, but it’s really unfortunate in this case.)

The transaction manager API has a couple other flaws:

  • Transaction objects don’t have any method for getting a human-readable description of what actually happened in the transaction.
  • If a transaction listener vetoes something, there’s no way for other transaction listeners which had already approved an operation to find out it was vetoed.
  • The transaction manager has limited ways of indicating its current state when things go wrong, not just in performing an operation, but in rolling the operation back.
  • If you’re dealing with multiple kinds of editing (DOM operations, source code changes), there’s really no good way to coordinate those.

As Tim Allen would say, “No power.  So I rewired it!”

I don’t have answers for these issues yet.  I think I’ll have to implement my own transaction manager API to improve upon all of the above.  But a design like that isn’t necessarily easy… I’d love to have help, especially if you think you might need something like this yourself.

One thing’s for sure:  when this new API and implementation is ready, it’ll have a lot of tests to go with it.  So, lend me your thoughts in the comments, please!