pax_global_header00006660000000000000000000000064116771106110014513gustar00rootroot0000000000000052 comment=f8f100671351d8abbe47331e0d6fbd059757385e creationix-step-f8f1006/000077500000000000000000000000001167711061100151655ustar00rootroot00000000000000creationix-step-f8f1006/README.markdown000066400000000000000000000057561167711061100177030ustar00rootroot00000000000000# Step A simple control-flow library for node.JS that makes parallel execution, serial execution, and error handling painless. ## How to install Simply copy or link the lib/step.js file into your `$HOME/.node_libraries` folder. ## How to use The step library exports a single function I call `Step`. It accepts any number of functions as arguments and runs them in serial order using the passed in `this` context as the callback to the next step. Step( function readSelf() { fs.readFile(__filename, this); }, function capitalize(err, text) { if (err) throw err; return text.toUpperCase(); }, function showIt(err, newText) { if (err) throw err; console.log(newText); } ); Notice that we pass in `this` as the callback to `fs.readFile`. When the file read completes, step will send the result as the arguments to the next function in the chain. Then in the `capitalize` function we're doing synchronous work so we can simple return the new value and Step will route it as if we called the callback. The first parameter is reserved for errors since this is the node standard. Also any exceptions thrown are caught and passed as the first argument to the next function. As long as you don't nest callback functions inline your main functions this prevents there from ever being any uncaught exceptions. This is very important for long running node.JS servers since a single uncaught exception can bring the whole server down. Also there is support for parallel actions: Step( // Loads two files in parallel function loadStuff() { fs.readFile(__filename, this.parallel()); fs.readFile("/etc/passwd", this.parallel()); }, // Show the result when done function showStuff(err, code, users) { if (err) throw err; console.log(code); console.log(users); } ) Here we pass `this.parallel()` instead of `this` as the callback. It internally keeps track of the number of callbacks issued and preserves their order then giving the result to the next step after all have finished. If there is an error in any of the parallel actions, it will be passed as the first argument to the next step. Also you can use group with a dynamic number of common tasks. Step( function readDir() { fs.readdir(__dirname, this); }, function readFiles(err, results) { if (err) throw err; // Create a new group var group = this.group(); results.forEach(function (filename) { if (/\.js$/.test(filename)) { fs.readFile(__dirname + "/" + filename, 'utf8', group()); } }); }, function showAll(err , files) { if (err) throw err; console.dir(files); } ); *Note* that we both call `this.group()` and `group()`. The first reserves a slot in the parameters of the next step, then calling `group()` generates the individual callbacks and increments the internal counter. creationix-step-f8f1006/lib/000077500000000000000000000000001167711061100157335ustar00rootroot00000000000000creationix-step-f8f1006/lib/step.js000077500000000000000000000110351167711061100172470ustar00rootroot00000000000000/* Copyright (c) 2011 Tim Caswell MIT License Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ // Inspired by http://github.com/willconant/flow-js, but reimplemented and // modified to fit my taste and the node.JS error handling system. function Step() { var steps = Array.prototype.slice.call(arguments), pending, counter, results, lock; // Define the main callback that's given as `this` to the steps. function next() { counter = pending = 0; // Check if there are no steps left if (steps.length === 0) { // Throw uncaught errors if (arguments[0]) { throw arguments[0]; } return; } // Get the next step to execute var fn = steps.shift(); results = []; // Run the step in a try..catch block so exceptions don't get out of hand. try { lock = true; var result = fn.apply(next, arguments); } catch (e) { // Pass any exceptions on through the next callback next(e); } if (counter > 0 && pending == 0) { // If parallel() was called, and all parallel branches executed // synchronously, go on to the next step immediately. next.apply(null, results); } else if (result !== undefined) { // If a synchronous return is used, pass it to the callback next(undefined, result); } lock = false; } // Add a special callback generator `this.parallel()` that groups stuff. next.parallel = function () { var index = 1 + counter++; pending++; return function () { pending--; // Compress the error from any result to the first argument if (arguments[0]) { results[0] = arguments[0]; } // Send the other results as arguments results[index] = arguments[1]; if (!lock && pending === 0) { // When all parallel branches done, call the callback next.apply(null, results); } }; }; // Generates a callback generator for grouped results next.group = function () { var localCallback = next.parallel(); var counter = 0; var pending = 0; var result = []; var error = undefined; function check() { if (pending === 0) { // When group is done, call the callback localCallback(error, result); } } process.nextTick(check); // Ensures that check is called at least once // Generates a callback for the group return function () { var index = counter++; pending++; return function () { pending--; // Compress the error from any result to the first argument if (arguments[0]) { error = arguments[0]; } // Send the other results as arguments result[index] = arguments[1]; if (!lock) { check(); } }; }; }; // Start the engine an pass nothing to the first step. next(); } // Tack on leading and tailing steps for input and output and return // the whole thing as a function. Basically turns step calls into function // factories. Step.fn = function StepFn() { var steps = Array.prototype.slice.call(arguments); return function () { var args = Array.prototype.slice.call(arguments); // Insert a first step that primes the data stream var toRun = [function () { this.apply(null, args); }].concat(steps); // If the last arg is a function add it as a last step if (typeof args[args.length-1] === 'function') { toRun.push(args.pop()); } Step.apply(null, toRun); } } // Hook into commonJS module systems if (typeof module !== 'undefined' && "exports" in module) { module.exports = Step; } creationix-step-f8f1006/package.json000066400000000000000000000005701167711061100174550ustar00rootroot00000000000000{ "name": "step", "version": "0.0.5", "description": "A simple control-flow library for node.JS that makes parallel execution, serial execution, and error handling painless.", "engine": [ "node >=0.2.0" ], "author": "Tim Caswell ", "repository": { "type" : "git", "url" : "http://github.com/creationix/step.git" }, "main": "lib/step" } creationix-step-f8f1006/test/000077500000000000000000000000001167711061100161445ustar00rootroot00000000000000creationix-step-f8f1006/test/callbackTest.js000066400000000000000000000011551167711061100211000ustar00rootroot00000000000000require('./helper'); var selfText = fs.readFileSync(__filename, 'utf8'); // This example tests passing async results and sync results to the next layer expect('one'); expect('two'); expect('three'); Step( function readSelf() { fulfill("one"); fs.readFile(__filename, 'utf8', this); }, function capitalize(err, text) { fulfill("two"); if (err) throw err; assert.equal(selfText, text, "Text Loaded"); return text.toUpperCase(); }, function showIt(err, newText) { fulfill("three"); if (err) throw err; assert.equal(selfText.toUpperCase(), newText, "Text Uppercased"); } ); creationix-step-f8f1006/test/errorTest.js000066400000000000000000000010241167711061100204700ustar00rootroot00000000000000require('./helper'); var exception = new Error('Catch me!'); expect('one'); expect('timeout'); expect('two'); expect('three'); Step( function () { fulfill('one'); var callback = this; setTimeout(function () { fulfill('timeout'); callback(exception); }, 0); }, function (err) { fulfill('two'); assert.equal(exception, err, "error should passed through"); throw exception; }, function (err) { fulfill('three'); assert.equal(exception, err, "error should be caught"); } ); creationix-step-f8f1006/test/fnTest.js000066400000000000000000000006551167711061100177530ustar00rootroot00000000000000require('./helper'); var myfn = Step.fn( function (name) { fs.readFile(name, 'utf8', this); }, function capitalize(err, text) { if (err) throw err; return text.toUpperCase(); } ); var selfText = fs.readFileSync(__filename, 'utf8'); expect('result'); myfn(__filename, function (err, result) { fulfill('result'); if (err) throw err; assert.equal(selfText.toUpperCase(), result, "It should work"); }); creationix-step-f8f1006/test/groupTest.js000066400000000000000000000062471167711061100205070ustar00rootroot00000000000000require('./helper'); var dirListing = fs.readdirSync(__dirname), dirResults = dirListing.map(function (filename) { return fs.readFileSync(__dirname + "/" + filename, 'utf8'); }); expect('one'); expect('two'); expect('three'); Step( function readDir() { fulfill('one'); fs.readdir(__dirname, this); }, function readFiles(err, results) { fulfill('two'); if (err) throw err; // Create a new group assert.deepEqual(dirListing, results); var group = this.group(); results.forEach(function (filename) { if (/\.js$/.test(filename)) { fs.readFile(__dirname + "/" + filename, 'utf8', group()); } }); }, function showAll(err , files) { fulfill('three'); if (err) throw err; assert.deepEqual(dirResults, files); } ); expect('four'); expect('five'); // When the group is empty, it should fire with an empty array Step( function start() { var group = this.group(); fulfill('four'); }, function readFiles(err, results) { if (err) throw err; fulfill('five'); assert.deepEqual(results, []); } ); // Test lock functionality with N sized groups expect("test3: 1"); expect("test3: 1,2,3"); expect("test3: 2"); Step( function() { return 1; }, function makeGroup(err, num) { if(err) throw err; fulfill("test3: " + num); var group = this.group(); setTimeout((function(callback) { return function() { callback(null, 1); } })(group()), 100); group()(null, 2); setTimeout((function(callback) { return function() { callback(null, 3); } })(group()), 0); }, function groupResults(err, results) { if(err) throw err; fulfill("test3: " + results); return 2 }, function terminate(err, num) { if(err) throw err; fulfill("test3: " + num); } ); // Test lock functionality with zero sized groups expect("test4: 1"); expect("test4: empty array"); expect("test4: group of zero terminated"); expect("test4: 2"); Step( function() { return 1; }, function makeGroup(err, num) { if(err) throw err; fulfill("test4: " + num); this.group(); }, function groupResults(err, results) { if(err) throw err; if(results.length === 0) { fulfill("test4: empty array"); } fulfill('test4: group of zero terminated'); return 2 }, function terminate(err, num) { if(err) throw err; fulfill("test4: " + num); } ); // Test lock functionality with groups which return immediately expect("test5: 1,2"); expect("test5 t1: 666"); expect("test5 t2: 333"); setTimeout(function() { Step( function parallelCalls() { var group = this.group(); var p1 = group(), p2 = group(); p1(null, 1); p2(null, 2); }, function parallelResults(err, results) { if(err) throw err; fulfill("test5: " + results); return 666; }, function terminate1(err, num) { if(err) throw err; fulfill("test5 t1: " + num); var next = this; setTimeout(function() { next(null, 333); }, 50); }, function terminate2(err, num) { if(err) throw err; fulfill("test5 t2: " + num); this(); } ); }, 1000); creationix-step-f8f1006/test/helper.js000066400000000000000000000010251167711061100177570ustar00rootroot00000000000000global.assert = require('assert'); global.fs = require('fs'); global.Step = require('../lib/step'); // A mini expectations module to ensure expected callback fire at all. var expectations = {}; global.expect = function expect(message) { expectations[message] = new Error("Missing expectation: " + message); } global.fulfill = function fulfill(message) { delete expectations[message]; } process.addListener('exit', function () { Object.keys(expectations).forEach(function (message) { throw expectations[message]; }); }); creationix-step-f8f1006/test/parallelTest.js000066400000000000000000000053341167711061100211430ustar00rootroot00000000000000require('./helper'); var selfText = fs.readFileSync(__filename, 'utf8'), etcText = fs.readFileSync('/etc/passwd', 'utf8'); expect('one'); expect('two'); Step( // Loads two files in parallel function loadStuff() { fulfill('one'); fs.readFile(__filename, this.parallel()); fs.readFile("/etc/passwd", this.parallel()); }, // Show the result when done function showStuff(err, code, users) { fulfill('two'); if (err) throw err; assert.equal(selfText, code, "Code should come first"); assert.equal(etcText, users, "Users should come second"); } ); // Test lock functionality with N parallel calls expect("test2: 1"); expect("test2: 1,2,3"); expect("test2: 2"); Step( function() { return 1; }, function makeParallelCalls(err, num) { if(err) throw err; fulfill("test2: " + num); setTimeout((function(callback) { return function() { callback(null, 1); } })(this.parallel()), 100); this.parallel()(null, 2); setTimeout((function(callback) { return function() { callback(null, 3); } })(this.parallel()), 0); }, function parallelResults(err, one, two, three) { if(err) throw err; fulfill("test2: " + [one, two, three]); return 2 }, function terminate(err, num) { if(err) throw err; fulfill("test2: " + num); } ) // Test lock functionality with parallel calls with delay expect("test3: 1,2"); expect("test3 t1: 666"); expect("test3 t2: 333"); Step( function parallelCalls() { var p1 = this.parallel(), p2 = this.parallel(); process.nextTick(function() { p1(null, 1); }); process.nextTick(function() { p2(null, 2); }); }, function parallelResults(err, one, two) { if(err) throw err; fulfill("test3: " + [one, two]); return 666; }, function terminate1(err, num) { if(err) throw err; fulfill("test3 t1: " + num); var next = this; setTimeout(function() { next(null, 333); }, 50); }, function terminate2(err, num) { if(err) throw err; fulfill("test3 t2: " + num); this(); } ); // Test lock functionality with parallel calls which return immediately expect("test4: 1,2"); expect("test4 t1: 666"); expect("test4 t2: 333"); Step( function parallelCalls() { var p1 = this.parallel(), p2 = this.parallel(); p1(null, 1); p2(null, 2); }, function parallelResults(err, one, two) { if(err) throw err; fulfill("test4: " + [one, two]); return 666; }, function terminate1(err, num) { if(err) throw err; fulfill("test4 t1: " + num); var next = this; setTimeout(function() { next(null, 333); }, 50); }, function terminate2(err, num) { if(err) throw err; fulfill("test4 t2: " + num); this(); } );