Atbar -- Async callback manager for javascript in node and browsers
Atbar is a pair of functions, this._ and this.$ (in coffescript: @_ and @$) that allow for simple
control of execution flow in async javascript programs. The name 'atbar' comes from the coffeescript
version of the first function. Atbar borrows some concepts from creationix/step such as using this
for easy function calling.
Features ...
- Allows use of async functions with callbacks in a linear sequential manner.
- Allows for parallel or serial execution with no extra operators.
- Support for try/catch, if and while flow control.
- A block of atbar code can be called as an async function itself with a callback.
- No compiling required.
- No namespace conflicts (e.g.
_
). Everything is a property of an atbar object hidden inthis
. - Works in any javascript environment, client or server.
- No function signature requirements such as node callback style.
- Does not try to eliminate or hide callback functions. Just makes them easier to use.
- Behaviour is always visible. No hidden parallel execution.
- Built-in tracing provides easy debugging. Find out where execution disappears.
- Clean code - object oriented - no evals.
How to install ...
npm i atbar
is all it takes.
A quick example ...
The following useless example shows serial and parallel execution. First setTimeout A executes. A calls back to a function where B and C execute in parallel. When both B and C have finished then D executes. For those of you who like coffeescript as I do, the coffeescript version follows.
atbar.run(function() {
this._(function() {
setTimeout(this.$(), 1000); // A
});
this._(function() {
setTimeout(this.$(), 2000); // B
setTimeout(this.$(), 1000); // C
});
this._(function() {
setTimeout(this.$(), 1000); // D
});
});
atbar.run ->
@_ ->
setTimeout @$(), 1000 # A
@_ ->
setTimeout @$(), 2000 # B
setTimeout @$(), 1000 # C
@_ ->
setTimeout @$(), 1000 # D
How it works ...
this._
wraps functions to make them easy for callbacks to find. this.$
creates callback
functions that automatically find and call this._
functions. So simple patterns such as
passing control to the next function can be implemented without deep nesting of anonymous functions
or having to name every function called. Patterns such as if
conditionals and while
loops are also made convenient.
Tracing ...
Because all the execution flow of the callbacks goes through method calls of the atbar objects, it was easy to add tracing. You start tracing by adding the 'trace' argument to the first atbar.run call. Only atbar functions are traced so the listing is concise. This is the console output, with tracing turned on, from the example above.
Starting atbar trace (Version 0.1) ...
0.001 0 root(enter) -> 0 () {setTimeout(this.$(), 1000); }
1.005 0 0(next) -> 1 () {setTimeout(this.$(), 2000); setTimeout(this.$(), 1000); }
2.015 1 1(next) -> 2 Callback ignored, waiting for 1 more
3.005 0 1(next) -> 2 () {setTimeout(this.$(), 1000); }
4.015 0 2 -> root Nothing to do, exiting ...
Legend: The columns, from left to right ...
- Time since start in seconds.
- Total number of async functions being waited on.
- Source and destination labels (or
this._
numbers) with the method-used in parens. - Source code of destination or a comment.
Detailed API ...
atbar.run
: run([option], [option]..., wrappedFunction, [callbackFunction]); No return value
atbar
is the usual var name from atbar = require('atbar');
. The run function creates the root
of an atbar object tree. wrappedFunction is kept as an attribute of that root object.
The wrappedFunction is called immediately by atbar.run
. The code in the wrappedFunction contains
both arbitrary javascript code and this._
calls. When the wrappedFunction returns, atbar.run
starts the execution of the first child object of the root atbar object (i.e. its wrapped function).
Child objects are created by this._
calls.
The optional callbackFunction is called when all activity in atbar is finished. It's signature is (err, results). The results come from one or more different types of actions.
Options provide some debug features that are output to console.log. Possible option values are
currently 'trace'
, 'listing'
, and 'debug'
. Trace is shown above. Listing provides a list of
atbar objects at the end of atbar execution. This is mainly used as a legend to understand the
labels shown in the trace. Debug is used to allow internal atbar debug messages to be shown.
this._
: _([destLabel|options], wrappedFunction); No return value
this._
creates an atbar child object in the tree. It also keeps its wrapped function as an
attribute of its object. However it does not call the wrapped function until it is the receiver of
an atbar callback.
destLabel
is a string that can be used to identify the this._
object and its wrapped function.
The destLabel is used by some callbacks to achieve a "go-to" action. In addition to being an
identifier, if it has the value 'catch', then when an exception is thrown this object will be
executed. An exception can either be thrown by the real javascript throw
command or by
this.$('throw')
(see below).
options
: If the first arg is an object instead of a string, then it is an options object. The
{label:destLabel}
option is the same as above. The {nojoin:true}
option turns off all
callback registration and monitoring for just this atbar object. So if multiple callbacks are
started that have this as a destination, the wrapped function will be called once for each callback.
this.$
: $([targetSpec = 'next'], [srcLabel], [treeLevelOfs = 0]); Returns callback function.
This generator creates an atbar callback function. Its output is usually passed directly as an
argument to an async function call, but it can also be called immediately by the form this.$()()
,
effectively creating a synchronous call.
The srcLabel is used much like the destLabel except it identifies the source of the callback instead
of the destination. When multiple callbacks target the same this._
and are "joined", the results
of each callback are kept in a hash with the srcLabel used as the key. The srcLabel is also
available to the wrapped function to find out who referenced it.
The targetSpec is also a string that tells the this.$()
callback generator how to find its
target. It specifies one of several algorithms ...
- 'next' is the default. It just calls the next
this._
as you see in the example above. - 'loop' Calls the same
this._
. This is how thewhile
pattern is implemented. - 'throw' Call the next
this._
with a destLabel of 'catch'. - 'first' Call the first
this._
at the top. This allows looping over multiplethis._
calls. - 'enter' Same as 'first', except it is the first in the current wrapped function.
- 'root' starts the whole atbar.run function over again.
- 'exit' causes atbar.run to finish immediately upon callback.
- Any other value targets the next destLabel with a matching value. This is a "go-to".
The treeLevelOfs is a positive integer that allows you to go up the chain of ancestors before applying a targetSpec algorithm. Zero means no escalation, 1 is the parent, 2 is the grandparent, etc. This emulates a sort of "return" pattern.
Atbar callbacks to wrapped functions ...
Each wrapped function can have formal parameters like (err, response) that receive the results of
the callback. If the function has a parameter named _throw
as in (_throw, response)
, then any
value passed in the _throw
position is automatically thrown as an exception and the function
does not execute. This allows usage of the node, or any other, error parameter convention.
Flow control ...
if
constructs can be inserted using standard javascript. To create an if
just place
if(!condition) this.$()();
as the first line of the wrapped function. The condition must
be met or the function will be skipped by creating and using a callback to the next wrapped
function.
while
constructs are created in a similar manner. In the wrapped function, make all callback
generators look like this.$('loop')
which will direct those callbacks to this same function.
If this is all you did then you would have a while(true)
loop that would require a "break" like
this.$()()
which would take you to the next function. To make a real while
statement just add an
if
pattern, if(!condition) this.$()();
, to the beginning.
I am considering adding some kind of syntax sugar to make these conditional patterns prettier.
Automatic joining ...
Atbar callbacks are registered in atbar objects when created. When two or more callbacks target the
same object (this._
wrapper), then that object holds the registry. As each callback happens,
it's completion is noted in the registry. The first callbacks are ignored except for storing their
results in the registry. When the last callback happens then the wrapped function is executed,
thus joining the callbacks. This always happens so if you want serial execution of functions each
function must be in its own this._
wrapper.
Object tree significance ...
this._
calls can be nested inside wrapped functions. This creates a muti-level atbar object tree for a block-structured pattern of coding. One if
or while
condition can control multiple
functions in a block. You may also just create and use a totally new atbar.run block inside
another but flow control to the outer atbar is limited.
All target finding algorithms work by traversing objects in the tree until an object qualifies for the targetSpec condition. The traversal is the normal depth-first so that when you pass the last child you then start back at the parent. This traversal may fall off of the end of the first level of children. When this happens the atbar function terminates, unless some other callback is waiting, then the callback is just ignored.
Examples used in production ...
Logging function ... (from my underscore.inspector module)
# coffescript version
exports.log = (path, msg) -> atbar.run 'trace', ->
fd = null
@_ -> fs.open path, 'a', @$()
@_ (_throw, fdi) -> fd = fdi; fs.write fd, msg + '\n', null, null, @$()
@_ (_throw) -> fs.close fd
@_ 'catch', (err) -> console.log 'underscore.inspector.log err: ' + err
// javascript version
exports.log = function(path, msg) {
return atbar.run('trace', function() {
var fd;
this._(function() {
fs.open(path, 'a', this.$());
});
this._(function(_throw, fdi) {
fd = fdi;
fs.write(fd, msg + '\n', null, null, this.$());
});
this._(function(_throw) {
fs.close(fd);
});
return this._('catch', function(err) {
console.log('underscore.inspector.log err: ' + err);
});
});
};
A dispatcher (from a scrape utility)
# Only the coffeescript version is shown, for brevity
# Notice the non-nested pattern even though there are five async calls
# Each @_ clearly shows destinations of async callbacks and @$() shows source of callbacks
exports.write = (req, res) -> atbar.run 'trace', ->
_.log 'scrape.log', 'SCRAPE-ALL STARTED\n' # uses logger from above
dirList = seq = 0;
path = 'scrape-all-seq'
@_ -> fs.readFile path, @$() # bump sequence in file
@_ (_throw, txt) -> # missing file throws to next catch
seq = +txt + 1; @$()()
@_ 'catch', -> fs.writeFile path, ''+seq, @$() # file error ignored, uses default
@_ (_throw) -> fs.readdir __dirname, @$() # find certain modules in this dir
@_ (_throw, list) -> # normal javascript is mixed in with atbar wrapped functions
dirList = _.reject list, (item) -> not _.startsWith item, 'page_scrape_'
dirList.sort()
len = dirList.length; ofs = seq % len
if ofs then dirList = dirList[ofs...len].concat dirList[0...ofs]
@$()() # synchronous step to next function
@_ ->
if not (file = dirList.shift()) then @$()(); return # while loop condition (break)
console.log '\n\nSCRAPE-ALL: starting scrape of ' + file + '\n' +
'dirList.length ' + dirList.length + '\n'
module = require file
module.write req, res, @$('loop') # while loop callback
@_ ->
_.log '/opt/node/bb/scrape.log', 'SCRAPE-ALL FINISHED\n'
Also see test functions in tests
folder of repository.
Status of project ...
Version 0.4.0 is the first beta relase. While it is completely coded, parts of it have only been used in tests. This version is in limited production at my place of work. I feel it can be used in any production environment, especially since I am eager to help others use it. Questions left in atbar's github issues section will be responded to quickly.
To-Do
- Simplify passing final callback results
- Revisit exit behavior
if
andwhen
syntax sugar
license ...
atbar usage is controlled by the MIT-LICENSE file included in the repository.