Skip to content
winterland edited this page Mar 7, 2016 · 43 revisions

Core functions

function Action(go)

This is the constructor of Action which wrap an sync or async action inside, the go should be a function that wait for a cb, aka. a continuation, inside this continuation you should call cb with a value or an Error sync or async one time, these values will be passed to downstream when Action are fired, sync or async.

var Action = require('action-js');
var fs = require('fs');

var readFileAction = new Action(
    function(cb){
        return fs.readFile("data.txt", function(err, data){
            if (err) {
                cb(err);
            }else{
                cb(data);
            }
        });
    }
);

Action._go(cb)

_go is the async action wrapped inside the Action, fire an Action immediately by passing a cb to it, it will not delayed to next tick, cb will receive the value produced by Action, sync or async one time, no matter it's a Error or not.

If the continuation has a return value, it will be returned immediately, please check Return value of go.

readFileAction._go(function(data){
    if (data instanceof Error){
        console.log('Oh, no...');
    }else{
        console.log(data);
    }
})

Action.prototype.go(cb :: a -> b)

Fire an Action with given cb, cb will receive the value produced by Action only if it's not an Error, otherwise the Error will be thrown, leave cb empty to ignore the value(but an Error will still be throw), the Error is thrown asynchronously, so you can't use normal catch to catch it, put a guard before go instead, see below.

If the continuation has a return value, it will be returned immediately, please check Return value of go.

Action.prototype._next(cb)

Return an new Action with given cb added to the chain, cb will receive the value produced by original Action, no matter it's an Error or not, the return value of cb will be pass to downstream, you can return an Action inside cb to insert a new async action into the chain.

var processAction = readFileAction
._next(function(data){
    if (data instanceof Error){
        return 'error handled';
    }else{
        return processA(data);
    }
})
._next(function(data){
    return new Action(function(cb){
        asyncProcessB(data, cb)  
    })
})

processAction.go(function(data){
    console.log(data);    
})

Action.prototype.next(cb :: a -> b)

Return an new Action with given cb added to the chain, cb will receive the value produced by original Action only if it's not an Error, the return value of cb will be pass to downstream, you can return an Action inside the cb to insert a new async action.

Action.prototype.guard([prefix :: String,] cb :: (e :: Error) -> b)

Return an new Action with given cb added to the chain, cb will receive the value produced by original Action only if it's an Error, and the return value of cb will be pass to downstream, you can return an Action inside the cb to insert a new async action.

If you pass an prefix String before cb, then Error caught by guard will be filtered by following condition:

error.message.indexOf(prefix) === 0

So you can use use prefix to guard certain kind of Error, other Errors will flow to downstream.

Safety

Action.safe(err :: Error | Default , fn :: a -> b)

Return a function that will run fn and return its result if no Error occured, otherwise return err.

var FuncMayReturnErr = Action.safe( (new Error 'Error: custom error'), FuncMayThrowErr);
FuncMayReturnErr(params);
// If FuncMayThrowErr(params) throw error, we return Error 'Error: custom error'

You can also use a default value as first parameter for failure instead an Error instance, this default value will be pass to downstream and processed by following next. This function is recommended over try..catch because it can minimize V8 try..catch overhead.

Action.safeRaw(fn :: a -> b)

Return a function that will run fn and return its result if no Error occured, otherwise return the error.

Helpers to compose Action

Action.wrap(data :: a)

Wrap a value in an Action.

Action.wrap('OK');
// it's equivalent to
new Action(function(cb){
    return cb('OK');
});

Action.freeze(action :: Action)

Fire action immediately(not next tick), memorize the resolved value, and return a new Action that will always give the same value without run the action again.

// The file will be read now, and saved in dataFrozen
var dataFrozen = Action.freeze(readFileAction.next(process))

dataFrozen.go(function(data){
    console.log(data);
})

dataFrozen.go(function(data){
    console.log(data);
})
// two console.log should always be the same

Action.chain(monadicActions :: [a -> Action])

Given an array of functions which product Action, run them in order, use the result of former Action to produce later Action, pass the value of the last Action to downstream, or pass Error if occurred.

The return value is not a new Action, but a function which product Action, pass an argument to it as the init value of the chain.

var accAction = function(x){
    return new Action(function(cb){
        cb(x+1);
    });
}
Action.chain([accAction, accAction, accAction])(0)
.go(function(x){
    console.log(x);
    // should be 3
})

Action.repeat(n :: Int, action :: Action, stopAtError = false :: Boolean)

Return an Action which will repeat action n times, pass the last action's value to downstream, if stopAtError is true, any Error will stop repeating and the error will be passed, repeat forever if n = -1.

Action.delay(n :: Int, action :: Action)

Return an Action delay the action n milliseconds.

Action.repeat(-1,
    Action.delay(1000, readFileAction)
    .next(function(data){
        console.log(data);
    })
)
.go()
// now the file will be read every 1 second forever.

Action.retry(n :: Int, action :: Action)

Return an Action which will run action, if failed, retry action n times at most, the action will be fired n + 1 times at most, if all failed, an Error 'RETRY_ERROR: Retry limit reached' will be passed on, retry forever if n == -1.

Action.parallel(actions :: [Action], stopAtError = false :: Boolean)

Return an Action which will run action in actions array in parallel and collect results in an Array, if stopAtError is true, stop at Error and pass the error, otherwise save the error in the result array.

Action.parallel([ActionA, ActionB])
.go(function(datas){
    console.log(datas[0]); // will be an Error if ActionA return an Error
    console.log(datas[1]); // will be an Error if ActionB return an Error
})

Action.parallel([ActionA, ActionB], true)
.next(function(datas){
    ...
    // if ActionA or ActionB return Error, this will be skipped
})
.guard(function(e){
    ...
})
.go()

Action.join(action1 :: Action, action2 :: Action, cb :: (a, b) -> c, stopAtError = false)

This is a lightweight version of parallel for running two Actions together, the cb will receive two results from action1 and action2, if you don't set stopAtError to true, the results may contain Errors, so guard them if you must.

Action.parallel(ActionA, ActionB, function(data0, data1){
    console.log(datas0); // will be an Error if ActionA return an Error
    console.log(datas1); // will be an Error if ActionB return an Error
})
.go()

Action.race(actions :: [Action], stopAtError = false :: Boolean)

Run action in actions array in parallel and find the first result, if stopAtError is true, stop at Error and pass the error, otherwise continue to look for the first none Error value.

Action.sequence(actions :: [Action], stopAtError = false :: Boolean)

Run action in actions array in sequence and collect results in an Array, if stopAtError is true, stop at Error and pass the error, otherwise save the error in the result array.

Action.throttle(actions :: [Action], limit :: Int, stopAtError = false :: Boolean)

Run action in actions array with maximum running number of limit, if stopAtError is true, stop at Error and pass the error, otherwise save the error in the result array. This function can be used to control degree of parallelism, and sequence/parallel are implemented by it.

Action.co(genFn :: GeneratorFunction)

Use generator function to chain action in a more imperative way, inside the generator function, you can wait an Action's value using yield:

var readFileAction = makeNodeAction(fs.readFile)

var actionFoo = Action.co(function*(paths){
    var content = '';
    var i = 0;
    while(content.length < 1000)
        content = yield readFileAction(paths[i++], {encoding: 'utf8'});

    if (content !== ''){
        console.log(content);
    }
});

actionFoo(['./foo.txt', './bar.txt' ... ]).go();
// print the first file content >= 1000

Since Action's semantic match perfectly with await, the underline implementation is much shorter:

  spawn = function(gen, action, cb) {
    return action._go(function(v) {
      var done, nextAction, ref;
      if (v instanceof Error) {
        gen["throw"](v);
      }
      ref = gen.next(v), nextAction = ref.value, done = ref.done;
      if (done) {
        return cb(v);
      } else {
        return spawn(gen, nextAction, cb);
      }
    });
  };

  Action.co = function(genFn) {
    return function() {
      var gen = genFn.apply(this, arguments);
      return new Action(function(cb) {
        return spawn(gen, gen.next().value, cb);
      });
    };
  };

Action.co is just a fancy way to compose Actions, but Error handling will be somehow different, you must use try-catch to wrap everything throw/yield Error inside generator function, if you want to send value to downstream, wrap it using Action.wrap and yield the action, so if you want to use guard like any other Action, do it like this:

var actionFoo = Action.co(function*(){
    try{
        ...
        // something may throw Error
        // or some Action may pass Error
    } catch (err) {
        // you can deal with the error here
        // and return something useful using yield
        // or just wrap the Error and pass to downstream
        yield Action.wrap(err); 
    }
});

actionFoo()
.guard(function(err){
    // handle err here
})
.next(...)
.go()

Since generator can reuse its executing context, it's faster than splitting them into several functions and connect them with next, so use co if you don't need too much modularity, see Benchmark.

Signal

Action.signal :: Action

This's a very special Action, when a signal fired, instead of passing value to downstream, it directly return the callback chain, so you can manually fire it with a value, let's call the return value of signal's go a pump.

testSignal = Action.signal
.guard(function(err){
    console.log('fail: ' + err.message);
})
.next(function(data){
    console.log('ok: ' + data);
})

testPump = testSignal.go();

// now you can manually fire the `testSignal` anytime you want
testPump('hey');
// ok: hey
testPump(new Error('you'));
// fail: you

Action.fuseSignal(signals :: [Action], fireAtError :: Boolean = false)

This function fuse an array of actions, after each of the signal is pumped, the whole Action will be continue.

var pumps = Action.fuseSignal([ logSignal, logSignal ])
.next(function(data){
    console.log(data[0], data[1]);    
})
.go()

setTimeout(pumps[0], 100, 'foo'); 
setTimeout(pumps[1], 1000, 'bar');

// after 100ms, 'foo' will be logged.
// after 1000ms, 'bar' will be logged.
// after 'bar' was logged, true, true will also be logged.

Node.js helper

Action.makeNodeAction(nodeAPI :: NodeStyleFn)

Convert a node style(last argument is a callback with type like (err, data) -> undefined) into a function return Action.

var readFileAction = Action.makeNodeAction(fs.readFile)
readFileAction('data.txt', {encoding: 'utf8'})
.go(function(data){
    console.log(data); 
});