chains
Javascript execution utility
chains.js is a utility for managing the structure and execution of javascript.
Overview
Chains allows for the managing of complex asychronous or sychronous application execution flow. Chains is a purely functional approach to managing application execution. Chains.js is not a framework, it is a simple utility that allows developers to structure their code in a straight-forward manner, though as mentioned below, sophisticated application orchestration is possible by compositing chains together.
Rather than deeply nested anonymous functions, callbacks, promises, deferreds, state management or other complex solutions to the asychronous execution models, chains allows developers to simply chain functions and define the order of their execution, either implicitly or explicitly.
One of the greatest things about chains is that they are reusable and nestable, meaning that a chain can be assigned to a variable and reused inside of other chains later. This provides a convenient development pattern where larger complex chains can be built from smaller chains.
For example, you could develop a chain that goes out and retrieves posts from a Twitter feed, and name it "twitter_chain". Another chain could be used to retrieve facebook posts and named "facebook_chain". Then, a composite chain could be created called something like "social_chain" which contains the twitter_chain and the facebook_chain subchains. This chain can then be combined with other chains, perhaps an "authenticate_chain" and a "get_app_data" chain into one large "main_chain", which when executed orchestrates all of the complex async loads for the entire app.
Installation
For browsers, just include a reference to chains.js, i.e.
<script src='script/lib/chains.js'><script>
For node
npm install chainsjsor add a dependency to your package.json file - then require it in your module like:
var o_o = require("chainsjs");
Basic Usage
In it's most simple form, a chain is nothing more than a serial, in-order execution of a list of funcitons, either sychronous or asychronous.
A very simple chain might look like:
o_o(someFunction)(anotherFunction)(yetAnotherFunction)();
In the above example, each function will execute in order, one after another. Functions that are "chains enabled" are referred to as "links". There is only one requirement for a function to be a link: at some point, it must call next() on the "this" context.
Links can be inline, or external. For example, in NodeJS it is common to have links defined in an external file, but this is by no means a requirement. Here's a simple example of a basic chain using in-line anonymous functions:
o_o
(
function()
{
//...do something
this.result = "some value";
this.next();
}
)(
function()
{
//...this will get executed next
var ctx = this; //store the this context in the ctx variable so it can be access within the async function
setTimeout(
function()
{
//do someting with the last result, and store it in this context
this.result = ctx.last.result + ", more text";
ctx.next();
}
,100);
}
)(
function()
{
//...this will be executed after the timeout has expired
var val = this.last.result;
console.log(this.last.last.result); //"some value"
console.log(val); // "some value, more text"
this.next();
}
)();
Just like the first example, in the above code each inline function executes in order. There are some interesting parts that we'll examine here.
this.result = "some value"
Using this.last.last can quickly become cumbersome when you want to retrieve values from previous links. Additionally, it ties your code to a specific execution order, which can make refactoring error prone. To overcome this, chains introduces a concept called the "accumulator".
The accumulator object is a chain-local object that carries a context between links. Using the accumulator object allows you to store values or objects that will be easily accessible from any other link in the chain, subchains or parent chains (more on this later).
The above example rewritten to use the accumulator object:
o_o
(
function()
{
//...do something
this.accumulator.result = "some value";
this.next();
}
)(
function()
{
//...this will get executed next
var ctx = this; //store the this context in the ctx variable so it can be access within the async function
setTimeout(
function()
{
//do someting with the last result, and store it in the accumulator
this.accumulator.result = this.accumulator.result + ", more text";
this.accumulator.someObject = {we:"can",also:"store",objects:"here"};
ctx.next();
}
,100);
}
)(
function()
{
//...this will be executed after the timeout has expired
console.log(this.accumulator.result); // "some value, more text"
console.log(this.accumulator.someObject.also); //"store"
this.next();
}
)();
this.next()
Each function calls this.next() in order to progress to the next function in the chain. This can be executed synchronously, or as in the example from the second function, asynchronously (useful for ajax calls, and other asynchronous execution).
this.result = ctx.last.result + ", more text";
Values from previous functions can be accessed, modified, or stored in the current 'this' context for use in future calls
var next = this.next();
setTimeout(
function()
{
next();
}
,100);
In this function we cache the "this" value in a variable so we can refer to it later asynchronously. This is a common technique when using chains with other async API's, like jquery or node. Chains is easy to use with other libraries, and doesn't restrict you to a single "right way" of doing things.
Parallel Execution
Chains' ability to execute simple functions one after another is a powerful technique that simplifies asynchronous development quite a bit. Sometimes though, you don't want to wait for a function to complete before you run the next one, sometimes you want to run several functions in parallel and then take some action. Chains makes this simple too:
o_o([function1,function2,function3])(doThisAtTheEnd)();
Chains will execute an array of functions in parallel, then contiue on as if all of the parallel functions we a single function call. This powerful technique can be extended to support very advanced execution orchestrations. Here's a conceptual example:
o_o
(
authenticateUser
)
(
getUserContextFromDatabase
)
(
[
getUserPosts,
getUserMessages,
getUserNotifications
]
)
(
renderWebPage
)();
Control of Flow
One of the most interesting capabilities of chains is the ability to perform asynchronous flow control and looping. This can be difficult to achieve in other libraries, however chains makes this very simple by allowing you to give links names, and execute them explicitly from within a different link or subchain.
If a funtion is declared using the javascript "function" keyword, there is no need to give the link a name, the function name will be used. If the function is declared anonymously, then giving it a name will let you refer to it later
function authenticateUser() { ... //verify credentials }
function getFacebookPosts() { ... //do stuff }
o_o(
authenticateUser
)(
function()
{
if(!this.accumulator.user)
return this.next("loginError");
next("getFacebookPosts");
}
)(
getFacebookPosts
)(
"loginError",
function()
{
... //notify the user of the problem
}
)();
Chains even allows asynchronous looping!
o_o(
"start",
function()
{
this.accumulator.counter = 10;
this.next();
}
)(
"looper",
function()
{
var thys = this;
if(this.accumulator.counter < 0)
return this.next("finished");
this.accumulator.counter--;
$.ajax(
{
... //settings
complete: function()
{
thys.next("looper");
}
});
}
)(
"finished",
function()
{
//all done!
}
)();
The above example demonstrated a very simple pattern to execute an asyncronous function 10 times, then execute a final function when complete. Of course, we could have modified the accumulator.counter property inside of the looper to control the number of loops, perhaps based on the result of our async call to the server. One great use for a technique like this is implementing a long poll with throttling and dynamic timing based on the response from the server.
Subchains, Parent Chains, and Nesting
Chains can be nested n-levels deep, and will maintain context and connection between all of the links and levels, both with the "this.last.last" method, and via the accumulator object.
var authChain = o_o(
function(
{
this.accumulator.user = {userName: "zaphod", password: "supersecret123"};
}
)(
require("security").authenticateUser
);
o_o
(
authChain
)(
function()
{
console.log(this.accumulator.user.userName); //"zaphod"
}
)();
Chains can be named and aliased like any other function:
var chain1 = o_o
(
function() { ... }
)(
function() { ... }
);
var chain2 = o_o
(
function() { ... }
)(
function() { ... }
);
o_o
(
chain1
)(
chain2
)(
"chain3",
o_o
(
function() { ... }
)(
function() { ... }
)
)(
{
"chain1":"chain2",
"chain2":"chain3"
}
)();
The above are just a few basic examples of what is possible with chains.js. Many more advanced execution patterns are possible. For more information, review the wiki pages and the advanced usage examples, as well as the plugins.
Error Handling
Dealing with errors in asyncronous programming models can be complex. For example, exceptions in NodeJS can be hard to track down and sometimes even ignored completely. To help with this, chains has an error handler that will be called any time an exeption occurs in your code. This will happen automatically, or can be invoked explicitly inside of any link. The error handler "bubbles", meaning an error handler in a parent chain will be invoked if there is an error in a subchain, or a sub-sub-chain, etc.
To implement an error handler, just alias a function with the name "error"
o_o(
subChain1
)(
someFunction
)(
"error",
function()
{
//handle any errors in the subchains or links
}
)();
Tracing and Profilng
Tracing the flow of asynchronous stacks can be difficult, and it can be even more difficult to identify performance problems with async calls. Chains makes this easy by providing a full debug async stack trace with every function call, as well as total time spent in the function.
To enable this, all you need to do is set debug to true:
o_o.debug=true;
Doing this will cause a console log to be output, containing the full flow of your chains and subchains along with the total time spent in each function. In the future, we will implement a full profiling interface that will provide standard profiling information, i.e. call count, time in function, total time, etc...
Plugins
Chains has a simple plugin model that allows any developer to extend the functionality of chains and expand the api. Several plugins are in the planning phases, so check back here for more information.