Ctrl is (yet another) a flow control library for Javascript.
What's wrong with TameJS or Flow or Step? Well, I'm a Rubyist and CoffeeScript fan. TameJS doesn't play well with CoffeeScript because of the extra compilation step. Plus the code that TameJS compiles down to is pretty heinous.
I didn't like how results are passed in either Flow or Step. Plus, both Flow and Step
make use of this
, which is no good for me since I want to use them from
within objects and I want this
to refer to my object.
Ctrl is not in npm
yet, so you have to install manually.
cd node_modules
git clone git://github.com/cjbottaro/ctrl.git
Then in a Javascript file.
var Ctrl = require("ctrl");
All examples are written in Javascript, if you want the CoffeeScript version of this README (which I highly recommend), then click here.
I make use of two contrived async functions to demonstrate how Ctrl works.
oneArgTimeout(n, callback);
twoArgTimeout(n, message, callback);
oneArgTimeout
calls callback
after n
seconds. It passes n
to
the callback.
twoArgTimeout
does the same thing, but passes both n
and message
to the callback.
Example:
twoArgTimeout(5, "I slept", function(n, message) {
console.log(message + "for " + n + "seconds");
});
Outputs:
"I slept for 5 seconds"
Consider this code that is trying to execute each call to
oneArgTimeout
serially.
oneArgTimeout(1, function(n) {
console.log("slept for " + n);
oneArgTimeout(2, function(n) {
console.log("slept for " + n);
oneArgTimeout(3, function(n) {
console.log("slept for " + n);
});
});
});
Here's how we would "un-nest" it with Ctrl.
Ctrl.run(
function(ctrl) {
oneArgTimeout(1, ctrl.collect());
},
function(ctrl) {
console.log("slept for " + ctrl.result);
oneArgTimeout(2, ctrl.collect());
},
function(ctrl) {
console.log("slept for " + ctrl.result);
oneArgTimeout(3, ctrl.collect());
},
function(ctrl) {
console.log("slept for " + ctrl.result);
});
Consider the following code that is trying to execute both calls to oneArgTimeout
in parallel, collect the results, and then call weAreDone
with the results after both of them are finished.
var finished_count = 0
var results = []
callback = function(result) {
finished_count = finished_count + 1
results.push(result)
if (finished_count == 2)
weAreDone(results);
};
oneArgTimeout(1, callback);
oneArgTimeout(1, callback);
Now with Ctrl.
Ctrl.run(
function(ctrl) {
oneArgTimeout(1, ctrl.collect());
oneArgTimeout(1, ctrl.collect());
},
function(ctrl) {
weAreDone(ctrl.results);
}
);
Oh man, that was sweet.
A little bit of terminology first... we're going to call each function passed to Ctrl a step.
Each step can designate how to collect results from callbacks, as well as access results from the previous step.
If you call collect
only once in a step, then you can access the
results with result
(notice it's singular) from the next step.
Ctrl.run(
function(ctrl) {
oneArgTimeout(1.2, ctrl.collect());
},
function(ctrl) {
console.log(ctrl.result)
}
);
That outputs 1.2
, but what if the callback is invoked with multiple
arguments?
Ctrl.run(
function(ctrl) {
twoArgTimeout(1.2, "hi", ctrl.collect());
},
function(ctrl) {
console.log(ctrl.result);
}
);
That outputs [ 1.2, 'hi' ]
, i.e. ctrl.result
is an array.
If collect
is called multiple times, then results
(notice it's
plural) holds the results corresponding to each call of collect
.
Ctrl.run(
function(ctrl) {
twoArgTimeout(2, "hi", ctrl.collect());
twoArgTimeout(1, "bye", ctrl.collect());
},
function(ctrl) {
console.log(ctrl.results);
}
);
That outputs [ [ 2, 'hi' ], [ 1, 'bye' ] ]
.
Notice the order of the results correspond to the order that collect
is called, not the
order in which the callbacks are executed.
collect
can be called with arguments which will result in
named_results
being a hash (or I guess object in JS) where the keys
correspond to the arguments.
Ctrl.run(
function(ctrl) {
twoArgTimeout(1, "hi", ctrl.collect("result1"));
twoArgTimeout(2, "bye", ctrl.collect("result2"));
},
function(ctrl) {
console.log(ctrl.named_results["result1"]);
console.log(ctrl.named_results["result2"]);
}
);
Results in the output:
[ 1, "hi" ]
[ 2, "bye" ]
Or you can unpack arguments into discrete keys.
Ctrl.run(
function(ctrl) {
twoArgTimeout(1, "hi", ctrl.collect("time1", "message1"));
twoArgTimeout(2, "bye", ctrl.collect("time2", "message2"));
},
function(ctrl) {
console.log(ctrl.named_results["time1"]);
console.log(ctrl.named_results["message1"]);
console.log(ctrl.named_results["time2"]);
console.log(ctrl.named_results["message2"]);
}
);
Which results in:
1
hi
2
bye
What happens if a step results in an error and we want to stop execution
of any remaining steps. That's what the stop
method is for.
Ctrl.run(
function(ctrl) {
redis.get(key, ctrl.collect());
},
function(ctrl) {
error = ctrl.result[0];
value = ctrl.result[1];
if (error) {
console.log("oops, error with redis: " + error);
ctrl.stop();
}
else
redis.get(value, ctrl.collect());
},
function(ctrl) {
error = ctrl.result[0];
value = ctrl.result[1];
console.log("final value is " + value);
}
);
If there is an error, then the 3rd step will never be executed.
You don't have to pass the Ctrl object to each step. You can just use the power of closures instead.
ctrl = new Ctrl;
ctrl.run(
function() {
oneArgTimeout(1, ctrl.collect());
},
function() {
console.log(ctrl.result);
}
);
Anytime a one element array would be returned in the results, just the element will be returned instead.
TameJS extends the Javascript langauge, thus requiring you to run a preprocessor (compiler) on your TameJS code. This makes it not play so nice with CoffeeScript (which also requires a step to compile into JS).
Also, TameJS offers a more natural way to program in that it doesn't require all your steps be wrapped in a function call. It's more akin to spawning threads and calling join on them.
Step and Flow are both less flexible with how async return values are passed between the steps (as noted on the TameJS page).
This may sound silly, but I didn't like the way my code looked when using Step or Flow. Each step took different arguments and thus my indentation wasn't pretty.
Also, both Step and Flow do some funky stuff with bindings which changes
this
in the scope of a step. I didn't like that because, I use "classes"
a lot. I want this
to refer to the object
I'm in so I can use "instances variables" and "instance methods". I
know I can work around this with Flow and Step by using closures, but it
didn't make for pretty code.
Sorry for the "quotes"... I am new to Javascript and I feel like I'm not using the proper terminology.
jasmine-node --color --coffee spec
This assumes you have jasmine-node
and coffee-script
installed
globally and that you are in the root dir of the Ctrl project.
Like I said, I'm new to Javascript and Node, but I've been immersed in the Ruby world for quite some time now. Please tweet at me to let me know if I'm doing something wrong, or some Ruby concepts don't carry over or I'm just not getting Javascript... :) Name is @cjbottaro