Action.js

A sane way to write async code

View the Project on GitHub winterland1989/Action.js

Action.js, a fast, small, full feature async library

What is Action

Interested? Action is a fast and clean alternative to both Promise(and Observable if you'd like to), it's intended to solve what Promise can't solve:

Besides all the benifits, Action run at blazing fast speed with simpler and smaller code. A simple example:

var Action = require('action-js');
new Action(function(cb){
    readFile('fileA', function(err, data){
        if (err){
            cb(err);
        }else{
            cb(data);
        }
    });
})
.next(function(data){
    // sync process
    return processData(data);
})
.next(function(data){
    // async process
    return new Action(function(cb){
        processDataAsync(data, cb);
    })
})
.next(function(data){
    // This process will be skip if previous steps pass an Error
    return anotherProcess(data);
})
.guard(function(e){
    // This process will be skip if there's no Errors
    return processError(e);
});
.go(function(data){console.log data});

It looks like Promise with some differences:

You can also use makeNodeAction to replace promisify in other promise library, let's get into the core since now you must have a lot of questions.

How does Action works

Action's core is an extremly simple javascript class(to mimic haskell's newtype):

var Action = function(go) {
    this._go = go;
}

We construct an Action by passing a function which consume a callback(aka. contination), take fs.readFile as an example:

// suppose this readFile never fail
// we will talk error handleing later
var readFileAction = new Action(function(cb){
    fs.readFile('data.txt', function(err, data){
        cb(data);
    })
});

Now if we provide a callback to readFileAction._go:

readFileAction._go(function(err, data){
    console.log(data);
})

The callback chain will be fired, it's equivalent to following code:

readFile("data.txt", function(data){
    console.log(data);
});

With one difference, Action seperate contination creation(wrap readFile in new Action) and application(supply a callback to _go), that's the core idea of Action, an Action always wrap a contination inside.

Now we can add a method to compose another callback with this contination:

Action.prototype._next = function(cb) {
    var self = this;
    return new Action(function(_cb) {
        return self._go(function(data) {
            var _data = cb(data); // this cb is what we pass to next
            return _cb(_data); // this _cb has not been passed yet!
        });
    });
};

Let's break down _next a little here:

With our _next, we can chain multiply callbacks, note how data flows between them:

readFileAction
._next(function(data){
    return data.length;
})
._next(function(data){
    // data here is the length we obtain last step
    console.log(data);
    return length > 0
})
._go(function(data){
    // data here is a Boolean
    if(data){
        ...
    }
})

Each _next return a new Action, if we give the final Action a callback with _go, the whole callback chain will be fired sequential.

Nice, we just use a very simple class, one very simple functions, the callbacks are written in a much more readable way now, but we have a key problem to be solved yet: what if we want to nest async Action inside an Action? Turn out with a little modification to our _next function, we can handle that:

Action.prototype._next = function(cb) {
    var self = this;
    return new Action(function(_cb) {
        return self._go(function(data) {
            var _data = cb(data);
            if (_data instanceof Action) {
                return _data._go(_cb);
            } else {
                return _cb(_data);
            }
        });
    });
};

This's the core of composable contination, We use instanceof Action to check if cb returns an Action or not, if an Action is returned, we fire it with _cb, the callback which our new Action will going to receive:

readFileAction
._next(function(data){
    var newFile = parse(data);
    return new Action(function(cb){
        readFile(newFile, cb);
    });
})
._go(function(data){
    // data here is the newFile's content
    console.log(data)
})

Now we have solved the callback hell problem! Well, actually just 50% of it. Before we proceed another 50%, one important thing to keep in mind: an Action is not a Promise, it will not happen if you don't pass a callback to _go, and it can be fired multiple times, it's just a reference to a wrapped contination:

readFileAction
._next(processOne)
._go(console.log)

// after we do other things, or inside another request handler
...

// processTwo may receive different data since the file may change!
readFileAction
._next(processTwo)
._go(console.log)

I'll present Action.freeze in Difference from Promise to give you Promise behavior when you need it, now let's attack another 50% of the callback hell issue.

Error handling

One biggest issue with Promise is that error handleing is somewhat magic and complex:

What we can do to make it simpler? It's a complex problem, we start solving it by simplify it: Action.js use Error type as a special type to pass error information to the downstream, what does this mean?

Action.prototype.next = function(cb) {
    var self = this;
    return new Action(function(_cb) {
        return self._go(function(data) {
            if (data instanceof Error) {
                return _cb(data); // we directly skip cb here
            } else {
                var _data = cb(data);
                if (_data instanceof Action) {
                    return _data._go(_cb);
                } else {
                    return _cb(_data);
                }
            }
        });
    });
};

Let me present the final version of our next function, comparing to _next we write before:

next ensure the cb it received, will never receive an Error, we just skip cb and pass Error downstream, symmetrically, we define a function which only deal with Error, and let normal values pass:

Action.prototype.guard = function(cb) {
    var self = this;
    return new Action(function(_cb) {
        return self._go(function(data) {
           if (data instanceof Error) {
            var _data = cb(data);
                if (_data instanceof Action) {
                    return _data._go(_cb);
                } else {
                    return _cb(_data);
                }
            } else {
                return _cb(data);
            }
        });
    });
};

This time, the cb that guard received are prepared for Error values, so we flip the logic, you can also return an Action if your need some async code to deal with the Error. Actually guard can receive an extra parameter before cb to filter what kind of Error cb can deal with, check out source code/api doc.

Following code demonstrate how to use our next and guard:

new Action(function(cb){
    readFile('fileA', function(err, data){
        if (err){
            // see how to pass an Error to downstream
            // not reject, not throw, just pass it on, let it go
            cb(err);
        }else{
            cb(data);
        }
    });
})
.next(function(data){
    // sync process
    return processData(data);
})
.next(function(data){
    // async process
    return new Action(function(cb){
        processDataAsync(data, cb);
    })
})
.next(
    try{
        return someProcessMayWentWrong(data);
    }catch(e){
        // same as above, we return the error to pass it on
        return e;
    }
}))
.next(function(data){
    // This process will be skip if previous steps pass an Error
    return anotherProcess(data);
})
.guard('ENOENT', function(e){
    // This process will be called only when Error's message begin with 'ENOENT'
    return processENOENT(e);
});
.guard(function(e){
    // This process will be skip if there's no Errors
    return processError(e);
})
._go(console.log);

The final result will be produced by anotherProcess if someProcessMayWentWrong didn't go wrong and readFile didn't fail, otherwise it will be produced by processError.

You can place guard in the middle of the chain, all Errors before it will be handled by it, and the value it produced, sync or async, will be passed down to the rest of the chain.

What if we don't supply a guard? Since we have to supply a callback to _go, we can check if the final result is an Error or not like this:

apiReturnAction('...')._go(function(data){
    if (data instanceof Error){
        //handle error here
        ...
    } else {
        // process data here
        ...
    }
});

Yeah, it does work, and often you want it work in this way, but:

So here let me present the final version of go:

Action.prototype.go = function(cb) {
    return this._go(function(data) {
        if (data instanceof Error) {
            throw data;
        } else if (cb != null) {
            return cb(data);
        }
    });
};

Now user can omit the callback, and if user don't guard Errors, we will yell at them when Error occurs!

new Action(function(cb){
    readFile('fileA', function(err, data){
        if (err){
            // suppose we got an Error here
            cb(err);
        }else{
            cb(data);
        }
    });
})
.go() // The Error will be thrown!

Finally, to ease error management, and attack the v8 optimization problems. We recommand using Action.safe:

// this small function minimize v8 try-catch overhead
// and make attaching custom Error easy
Action.safe = function(err, fn) {
    return function(data) {
        try {
            return fn(data);
        } catch (_error) {
            return err;
        }
    };
};

Use safe wrap your someProcessMayWentWrong like this:

var safe = Action.safe;
new Action(function(cb){
    ...
})
.next(
    safe(new Error("PROCESS_ERROR_XXX: process xxx failed when xxx")
        , someProcessMayWentWrong)
)
.next(...)
.guard(function(e){
    if (e.message.indexOf('ENOENT') === 0){
        ...
    }
    if (e.message.indexOf('PROCESS_ERROR_XXX') === 0 ){
        ...
    }
})
.go()

That's all core functions of Action, but it's much more powerful than first look! make sure you read:

FAQ

When to use this library?

With Promise added to ES6 and ES7 async/await proposal, one must ask, why another library to do the same things again?

Because Action is not Promise, It's a faster, simpler and full feature alternative comes with more flexible semantics. Actually Action have a very elegant Action.co implementation to work with generators, nevertheless, use this library if you:

If you have a FP background, you must find all i have done is porting the Cont monad from Haskell, and i believe you have divided your program into many composable functions already, just connect them with next.

The semantics of Action also fit varieties situations like animation and interactive UI, it's far more suitable than Promise in these situations.

What makes Action fast?

Check out Benchmark, even use bluebird's benchmark suit, which heavily depend on library's promisify implementation, Action can match bluebird's performance.

Generally speaking, Action simply does less work:

Why following code doesn't work?

var fileA = readFileAction
.go(processOne)

// Error, fileA is not an Action anymore
fileA
.next(processTwo)
.go()

Well, read Difference from Promise and Return value of go to get a detailed answer, tl,dr... here is the short answer:

// readFile now and return a Action, this function won't block
var fileA = Action.freeze(readFileAction.next(processOne))

// now fileA will always have the same content
// and file will never be read again.
fileA
.next(processTwo)
.go()

// processTwo will receive the same content
fileA
.next(processTwo)
.go()

If you want have a Promise behavior(fire and memorize), use Action.freeze, go won't return a new Action, instead go return a cancel handler if underline action can be cancelled.

How can i send an Error to downstream's next

No, you can't, however, you can receive Error from upstream use _next, _go or guard. or you can wrap the Error in an Array like [e], it's a very rare situation one want to process a Error value like normal values.

The choice of using Error to skip next and hit guard is not arbitrary, instead of creating an ActionError class, use Error unify type with system runtime, and providing callstack information. And you can now break your program by throwing an Error if you really want to.

Changelog

V4.2.2 Seperate ajax related stuff into seperate package, ajax-action.

V4.2.0 prototype.guard now accpet an extra parameter(before cb) to guard Error based on their message (prefix).

V4.1.1 Small throttle optimization.

V4.1.0 Add throttle, now parallel and sequence are implemented by throttle.

v3.1.0

  1. Add stopAtError flag to Action.join.

  2. Change ajaxHelpers.buildParam behavior when input contain arrays, now foo: [1,2,3] will output foo=1&foo=2&foo=3.

v3.0.0 Seperate ajax related stuff into ajaxHelper.js.

v2.4.2 Make prototype.go default to id function if no callback is provided.

v2.4.1 Fix a Action.fuseSignal bug.

v2.4.0 Add Action.signal and Action.fuseSignal to ease async UI management.

v2.3.0 Now when you construct an Action, the this variable inside the contination will be the Action instance.

v2.2.0 Action.join, Action.parallel, Action.race now return an Array of cancel handler when the composed Action fired, you can now cancel them with ease.

v2.1.1 Fix a bug of Action.parallel, add test.

v2.1.0 Change Action.co into more async-await style, you can use try-catch to catch Errors now.

v2.0.0 Update doc, Remove gapRetry, since it's just a retry compose delay.

v1.4.1 Run bluebird benchmark, add some optimization.

v1.4.0 Add Action.co, fix Action.join typos, test cover 100% agian.

v1.3.0 Add Action.join, optimized internal

v1.2.4 Improve makeNodeAction

v1.2.3 Fix responseType related.

v1.2.2 Auto add header based on data type.

v1.2.1 Clear some error types.

v1.2.0 add param, jsonp and ajax for front-end usage.

License

The MIT License (MIT)

Copyright (c) 2016 Winterland

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.