atbar

0.4.2 • Public • Published

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 in this.
  • 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 ...

  1. Time since start in seconds.
  2. Total number of async functions being waited on.
  3. Source and destination labels (or this._ numbers) with the method-used in parens.
  4. 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 the while 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 multiple this._ 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 and when syntax sugar

license ...

atbar usage is controlled by the MIT-LICENSE file included in the repository.

Readme

Keywords

none

Package Sidebar

Install

npm i atbar

Weekly Downloads

18

Version

0.4.2

License

none

Last publish

Collaborators

  • mark-hahn