+25
-18
@@ -63,3 +63,3 @@ /** | ||
| function _loop(cb) { | ||
| if (test()) func(function(err, ret) { cb(err, false); }); | ||
| if (test()) func(function(err) { cb(err, false); }); | ||
| else cb(null, true); | ||
@@ -79,3 +79,3 @@ }, | ||
| if (!Array.isArray(dataItems)) { | ||
| if (!dataItems || typeof dataItems.length !== 'number') { | ||
| return callback(new Error("expected a data array, but got " + typeof data)); | ||
@@ -108,3 +108,4 @@ } | ||
| var lastResult; | ||
| var lastResult, lastResult2; | ||
| var callReturned; | ||
| applyVisitor( | ||
@@ -115,8 +116,13 @@ functionList, | ||
| // it will become the next func`s lastResult | ||
| // forwards the first 2 results to the next function in the chain | ||
| callReturned = false; | ||
| func( | ||
| function(err, ret) { | ||
| function(err, ret, ret2) { | ||
| callReturned = true; | ||
| lastResult = ret; | ||
| cb(err, ret); | ||
| lastResult2 = ret2; | ||
| cb(err, ret, ret2); | ||
| }, | ||
| lastResult | ||
| lastResult, | ||
| lastResult2 | ||
| ); | ||
@@ -126,6 +132,5 @@ }, | ||
| // return the very last result computed, that applyVisitor returns | ||
| return callback ? callback(err, lastResult) : null; | ||
| return callback ? (callReturned ? callback(err, lastResult, lastResult2) : callback(err)) : null; | ||
| } | ||
| ); | ||
| // node v0.11.13 runs this 30% slower than v0.10.25 | ||
| } | ||
@@ -135,5 +140,7 @@ | ||
| * Combine all items in the data array using func. | ||
| * runningTotal is the initial combined value; a call to combinerFunc is given | ||
| * the running total and item and the return value is the new runningTotal. | ||
| * This is an async variant of Array.prototype.reduce(). | ||
| */ | ||
| // TODO: testme | ||
| function reduce( data, combinerFunc, runningTotal, callback ) { | ||
| function reduce( data, runningTotal, combinerFunc, callback ) { | ||
| 'use strict'; | ||
@@ -144,7 +151,7 @@ | ||
| function visitor(item, cb) { | ||
| combinerFunc(item, function(err, ret) { | ||
| combinerFunc(runningTotal, item, function(err, ret) { | ||
| // the running total is the combined results of item and the | ||
| // previous subtotal, as computed by func(). | ||
| runningTotal = ret; | ||
| return err ? cb(err) : cb(null, runningTotal); | ||
| if (!err || ret !== undefined) runningTotal = ret; | ||
| return cb(err, runningTotal); | ||
| }); | ||
@@ -164,3 +171,2 @@ }, | ||
| */ | ||
| // TODO: testme | ||
| function map( data, mapperFunc, callback ) { | ||
@@ -175,3 +181,3 @@ // TODO: might also want to map properties of an object, not just arrays | ||
| mapperFunc(item, function(err, mappedItem) { | ||
| mappedResults.push(mappedItem); | ||
| if (!err || mappedItem !== undefined) mappedResults.push(mappedItem); | ||
| cb(err); | ||
@@ -188,4 +194,5 @@ }); | ||
| * Return the list of items that were picked by the filter function. | ||
| * The filter function should return truthy for values to include, | ||
| * and falsy for values to omit from the results. | ||
| */ | ||
| // TODO: testme | ||
| function filter( data, filterFunc, callback ) { | ||
@@ -196,4 +203,4 @@ var pickedItems = []; | ||
| function visitor(item, cb) { | ||
| filterFunc(item, function(err, ret) { | ||
| if (ret && !err) pickedItems.push(item); | ||
| filterFunc(item, function(err, yesno) { | ||
| if (yesno) pickedItems.push(item); | ||
| cb(err); | ||
@@ -200,0 +207,0 @@ }); |
+12
-2
| { | ||
| "name": "aflow", | ||
| "version": "0.2.3", | ||
| "version": "0.9.0", | ||
| "description": "async flow control for calls with callbacks", | ||
@@ -24,5 +24,14 @@ "license": "Apache-2.0", | ||
| "quick", | ||
| "async", | ||
| "fast", | ||
| "recursion-safe", | ||
| "flow", | ||
| "control", | ||
| "async" | ||
| "repeatUntil", | ||
| "repeatWhile", | ||
| "applyVisitor", | ||
| "iterate", | ||
| "reduce", | ||
| "map", | ||
| "filter" | ||
| ], | ||
@@ -32,3 +41,4 @@ "dependencies": { | ||
| "devDependencies": { | ||
| "nodeunit": "0.9.0" | ||
| } | ||
| } |
+142
-40
@@ -6,14 +6,20 @@ aflow | ||
| ## Install | ||
| All functions run sequentially and stop immediately on error. | ||
| ## Installation | ||
| npm install aflow | ||
| npm test aflow | ||
| or `git clone git://github.com/andrasq/node-aflow` | ||
| ## Calls | ||
| ## Functions | ||
| ### repeatUntil( function, callback ) | ||
| ### repeatUntil( repeatedFunc(cb), done(err, stoppedWithValue) ) | ||
| keep calling function until it returns a truthy value, then return that value | ||
| via callback. | ||
| Keep calling the repeated function until it returns a truthy value, then | ||
| return that value via the callback. Stops looping on error. This call is | ||
| recursion-safe, it periodically yields to the event loop with setImmediate(). | ||
| The repeated call must call its callback, the loop will wait indefinitely and | ||
| will not time out. | ||
@@ -23,53 +29,149 @@ var aflow = require('aflow'); | ||
| aflow.repeatUntil( | ||
| function(cb) { | ||
| function repeated(cb) { | ||
| count += 1; | ||
| var stopNow = count >= 10; | ||
| cb(null, stopNow); | ||
| if (count < 10) cb(null, false); | ||
| else cb(null, count); | ||
| }, | ||
| function(err) { | ||
| console.log("count =", count); | ||
| function whenDone(err, stoppingCount) { | ||
| console.log("stoppingCount = %d, count = %d", stoppingCount, count); | ||
| // => stoppingCount = 10, count = 10 | ||
| } | ||
| ) | ||
| // => count = 10 | ||
| ); | ||
| ### repeatWhile( loopCondition, function, callback ) | ||
| ### repeatWhile( loopTest(), repeatedFunc(cb), done(err) ) | ||
| keep calling function as long as loopCondition holds. | ||
| Keep calling the repeated function as long as the loopTest() function returns | ||
| truthy. loopTest() does not take arguments or a callback. Stops looping on | ||
| error. Any value returned by the repeated function is ignored. | ||
| var aflow = require('aflow'); | ||
| var count = 0; | ||
| aflow.repeatWhile( | ||
| function loopTest() { | ||
| return count < 10; | ||
| }, | ||
| function repeated(cb) { | ||
| count += 1; | ||
| cb(); | ||
| }, | ||
| function whenDone(err) { | ||
| console.log("final count =", count); | ||
| // => final count = 10 | ||
| } | ||
| ); | ||
| ### applyVisitor( items, visitorFunc(item, cb), done(err) ) | ||
| Invoke the visitor function on each data item in turn. Stops on error and | ||
| returns the error object. Does not return a value. | ||
| var aflow = require('aflow'); | ||
| var count = 0; | ||
| aflow.repeatWhile( | ||
| function() { | ||
| return count < 10; | ||
| }, | ||
| function(cb) { | ||
| count += 1; | ||
| var visitedItems = []; | ||
| aflow.applyVisitor( | ||
| [1,2,3,4], | ||
| function visitor(item, cb) { | ||
| visitedItems.push(2 * item); | ||
| cb(); | ||
| }, | ||
| function(err) { | ||
| console.log("count =", count); | ||
| function whenDone(err) { | ||
| // visitedItems = [2,4,6,8]; | ||
| } | ||
| ) | ||
| // => count = 10 | ||
| ); | ||
| ### iterate( functionList, callback ) | ||
| ### iterate( functionList, done(err, ret1, ret2) ) | ||
| call each function in turn. Each function is passed two arguments, a | ||
| callback and the returned value from the previous function. An error | ||
| will interrupt the flow. Callback will be called with the returned | ||
| value from the last function, or the error. | ||
| Call each function on the list. The functions are passed three arguments, a | ||
| callback and the first two returned values from the previous function called. | ||
| An error will interrupt the flow. The when done callback will be called with | ||
| the first two returned values from the last function. On error, the callback | ||
| will be called with the error and the first two results returned by the call | ||
| that produced the error (if the call returned results). | ||
| var aflow = require('aflow'); | ||
| var args = []; | ||
| aflow.iterate([ | ||
| function(cb, last) { console.log("first, last", last); cb(null, 1); }, | ||
| function(cb, last) { console.log("second, last", last); cb(null, 2); }, | ||
| function(cb, last) { console.log("third, last", last); cb(null, 3); } | ||
| function(cb, arg1, arg2) { args.push(arg1); args.push(arg2); cb(null, 1, 11); }, | ||
| function(cb, arg1, arg2) { args.push(arg1); args.push(arg2); cb(null, 2, 22,); }, | ||
| function(cb, arg1, arg2) { args.push(arg1); args.push(arg2); cb(null, 3, 33, 333); }, | ||
| ], | ||
| function(err, last) { | ||
| console.log("done, last", last); | ||
| function(err, arg1, arg2, arg3) { | ||
| args.push(arg1); | ||
| args.push(arg2); | ||
| args.push(arg3); | ||
| // args = [undefined, undefined, 1, 11, 2, 22, 3, 33, undefined] | ||
| } | ||
| ); | ||
| // => first, last undefined | ||
| // => second, last 1 | ||
| // => third, last 2 | ||
| // => done, last 3 | ||
| ### reduce( items, subtotal, combineFunc(subtot, item, cb), done(err, total) ) | ||
| Combine all data items with the subtotal using the given combine function. | ||
| The result returned from the last combine function becomes the subtotal passed | ||
| to the next next combine function. Stops processing on error and returns the | ||
| subtotal so far. If a combiner function returs a defined subtotal along with | ||
| an error object, that subtotal will be the final subtotal returned. | ||
| var aflow = require('aflow'); | ||
| aflow.reduce( | ||
| ['a', 'b', 'c', 'd'], | ||
| ':', | ||
| function concatenate(subtotal, item, cb) { | ||
| cb(null, subtotal + item); | ||
| }, | ||
| function whenDone(err, total) { | ||
| // total = ":abcd" | ||
| } | ||
| ); | ||
| ### map( items, transformFunc(item, cb), done(err, transformedItems) ) | ||
| Apply the transformation to each data item, and return the list of transformed | ||
| items. The transfor function is provided the item and a callback, and should | ||
| call its callback with any error and the transformed item. Stops and returns | ||
| the partial results so far on error. If the transform returns an error object | ||
| along with a defined result, it will be included in the partial results. | ||
| var aflow = require('aflow'); | ||
| aflow.map( | ||
| [1,2,3,4,5], | ||
| function double(item, cb) { | ||
| cb(null, 2 * item); | ||
| }, | ||
| function whenDone(err, transformedItems) { | ||
| // transformedItems = [2,4,6,8,10] | ||
| } | ||
| ); | ||
| ### filter( items, selectFunc(item, cb), done(err, selectedItems) ) | ||
| Evaluate each data item with the select function and return the list of items | ||
| that were selected. The select function is provided the item and a callback, | ||
| and should call its callback with any error and a truthy value to select that | ||
| item. If the select function returns an error object along with a truthy | ||
| selection flag, the item generating the error will be included in the partial | ||
| results. | ||
| var aflow = require('aflow'); | ||
| aflow.filter( | ||
| [1,2,3,4,5], | ||
| function oddItems(item, cb) { | ||
| if (item % 2) cb(null, true); | ||
| else cb(null, false); | ||
| }, | ||
| function whenDone(err, selectedItems) { | ||
| // selectedItems = [1,3,5] | ||
| } | ||
| ); | ||
| ## ChangeLog | ||
| 0.9.0 | ||
| - document all calls | ||
| - better unit test coverage | ||
| - allow applyVisitor() to iterate any object with a numeric length, eg `arguments` or a `Buffer` | ||
| ## Todo | ||
| - benchmark the speed of each of the primitives |
+265
-1
@@ -15,2 +15,7 @@ 'use strict'; | ||
| 'should parse package.json': function(t) { | ||
| require('../package.json'); | ||
| t.done(); | ||
| }, | ||
| 'repeatUntil': { | ||
@@ -24,3 +29,3 @@ 'should trap errors and return them in callback': function(t) { | ||
| function(err, last) { | ||
| t.ok(err); | ||
| t.ok(err instanceof Error); | ||
| t.equal(err.message, "die"); | ||
@@ -62,2 +67,261 @@ t.done(); | ||
| }, | ||
| 'repeatWhile': { | ||
| 'should test loop condition at top': function(t) { | ||
| var ncalls = 0; | ||
| t.expect(1); | ||
| qflow.repeatWhile( | ||
| function() { return false; }, | ||
| function(next) { ncalls += 1; next(); }, | ||
| function(err) { | ||
| t.equal(ncalls, 0); | ||
| t.done(); | ||
| } | ||
| ); | ||
| }, | ||
| 'should loop n times': function(t) { | ||
| var ncalls = 0; | ||
| t.expect(1); | ||
| qflow.repeatWhile( | ||
| function() { return ncalls < 1234; }, | ||
| function(next) { ncalls += 1; next(); }, | ||
| function(err) { | ||
| t.equal(ncalls, 1234); | ||
| t.done(); | ||
| } | ||
| ); | ||
| }, | ||
| 'should stop loop on error': function(t) { | ||
| var ncalls = 0; | ||
| t.expect(3); | ||
| qflow.repeatWhile( | ||
| function() { return ncalls < 1234; }, | ||
| function(next) { ncalls += 1; if (ncalls < 111) next(); else throw new Error("stop at 111"); }, | ||
| function(err) { | ||
| t.equal(ncalls, 111); | ||
| t.ok(err instanceof Error); | ||
| t.equal(err.message, "stop at 111"); | ||
| t.done(); | ||
| } | ||
| ); | ||
| }, | ||
| }, | ||
| 'applyVisitor': { | ||
| 'should call visitor with each item': function(t) { | ||
| var visited = []; | ||
| qflow.applyVisitor( | ||
| [1,2,3,4], | ||
| function(item, cb) { | ||
| visited.push(item); | ||
| cb(); | ||
| }, | ||
| function(err) { | ||
| t.deepEqual(visited, [1,2,3,4]); | ||
| t.done(); | ||
| } | ||
| ); | ||
| }, | ||
| 'should return error and stop looping on error': function(t) { | ||
| var visited = []; | ||
| t.expect(3); | ||
| qflow.applyVisitor( | ||
| [1,2,3,4], | ||
| function(item, cb) { | ||
| visited.push(item); | ||
| if (item == 3) throw new Error("stop at 3"); | ||
| cb(); | ||
| }, | ||
| function(err) { | ||
| t.deepEqual(visited, [1,2,3]); | ||
| t.ok(err instanceof Error); | ||
| t.equal(err.message, "stop at 3"); | ||
| t.done(); | ||
| } | ||
| ); | ||
| }, | ||
| }, | ||
| 'iterate': { | ||
| 'should call each function': function(t) { | ||
| var items = []; | ||
| t.expect(1); | ||
| qflow.iterate([ | ||
| function(cb) { items.push(1); cb() }, | ||
| function(cb) { items.push(2); cb() }, | ||
| function(cb) { items.push(3); cb() }, | ||
| ], | ||
| function(err) { | ||
| t.deepEqual(items, [1,2,3]); | ||
| t.done(); | ||
| } | ||
| ); | ||
| }, | ||
| 'should pass previous result to each next function and return the last result': function(t) { | ||
| var args = []; | ||
| t.expect(3); | ||
| qflow.iterate([ | ||
| function(cb, arg, arg2) { args.push(arg); args.push(arg2); cb(null, 1, 11); }, | ||
| function(cb, arg, arg2) { args.push(arg); args.push(arg2); cb(null, 2, 22); }, | ||
| function(cb, arg, arg2) { args.push(arg); args.push(arg2); cb(null, 3, 33); }, | ||
| ], | ||
| function(err, arg, arg2) { | ||
| t.deepEqual(args, [undefined, undefined, 1, 11, 2, 22]); | ||
| t.equal(arg, 3); | ||
| t.equal(arg2, 33); | ||
| t.done(); | ||
| } | ||
| ); | ||
| }, | ||
| 'should stop on error': function(t) { | ||
| var items = []; | ||
| t.expect(3); | ||
| qflow.iterate([ | ||
| function(cb) { items.push(1); cb(); }, | ||
| function(cb) { items.push(2); throw new Error("stop at 2"); cb() }, | ||
| function(cb) { items.push(3); cb() }, | ||
| ], | ||
| function(err) { | ||
| t.deepEqual(items, [1,2]); | ||
| t.ok(err instanceof Error); | ||
| t.equal(err.message, "stop at 2"); | ||
| t.done(); | ||
| } | ||
| ); | ||
| }, | ||
| }, | ||
| 'reduce': { | ||
| 'should combine items': function(t) { | ||
| var items = [1,2,3,4,5]; | ||
| t.expect(1); | ||
| qflow.reduce( | ||
| items, | ||
| '', | ||
| function(total, item, cb) { | ||
| total += '.' + item; | ||
| cb(null, total); | ||
| }, | ||
| function(err, final) { | ||
| t.equal(final, '.1.2.3.4.5'); | ||
| t.done(); | ||
| } | ||
| ); | ||
| }, | ||
| 'returns errors along with partial results': function(t) { | ||
| var items = [1,2,3,4,5]; | ||
| t.expect(3); | ||
| qflow.reduce( | ||
| items, | ||
| '', | ||
| function(total, item, cb) { | ||
| if (item === 3) throw new Error("err at 3"); | ||
| total += '.' + item; | ||
| cb(null, total); | ||
| }, | ||
| function(err, final) { | ||
| t.ok(err instanceof Error); | ||
| t.equal(err.message, "err at 3"); | ||
| t.equal(final, ".1.2"); | ||
| t.done(); | ||
| } | ||
| ); | ||
| }, | ||
| }, | ||
| 'map': { | ||
| 'should apply function to all items': function(t) { | ||
| var items = [1,2,3,4]; | ||
| t.expect(1); | ||
| qflow.map( | ||
| items, | ||
| function(item, cb) { cb(null, item + item); }, | ||
| function(err, mapped) { | ||
| t.deepEqual(mapped, [2,4,6,8]); | ||
| t.done(); | ||
| } | ||
| ); | ||
| }, | ||
| 'should return error and partial results': function(t) { | ||
| var items = [1,2,3,4]; | ||
| t.expect(3); | ||
| qflow.map( | ||
| items, | ||
| function(item, cb) { if (item === 3) throw new Error("three"); else cb(null, item + item); }, | ||
| function(err, mapped) { | ||
| t.deepEqual(mapped, [2,4]); | ||
| t.ok(err instanceof Error); | ||
| t.equal(err.message, "three"); | ||
| t.done(); | ||
| } | ||
| ); | ||
| }, | ||
| 'should include a defined item that came with error': function(t) { | ||
| var items = [1,2,3,4]; | ||
| t.expect(1); | ||
| qflow.map( | ||
| items, | ||
| function(item, cb) { cb((item === 3 ? new Error("three") : null), item); }, | ||
| function(err, mapped) { | ||
| t.equal(mapped[2], 3); | ||
| t.done(); | ||
| } | ||
| ); | ||
| }, | ||
| }, | ||
| 'filter': { | ||
| 'should return array of items selected by filter function': function(t) { | ||
| var items = [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20]; | ||
| t.expect(1); | ||
| qflow.filter( | ||
| items, | ||
| function(item, cb) { | ||
| // odd multiples of 3 | ||
| var yesInclude = (item % 2) && (item % 3 == 0) | ||
| cb(null, yesInclude); | ||
| }, | ||
| function(err, found) { | ||
| t.deepEqual(found, [3, 9, 15]); | ||
| t.done(); | ||
| } | ||
| ); | ||
| }, | ||
| 'should return any errors': function(t) { | ||
| var items = [1,2,3,4]; | ||
| t.expect(3); | ||
| qflow.filter( | ||
| items, | ||
| function(item, cb) { if (item == 3) throw new Error("err on 3"); cb(null, true); }, | ||
| function(err, found) { | ||
| t.ok(err instanceof Error); | ||
| t.equal(err.message, "err on 3"); | ||
| t.deepEqual(found, [1,2]); | ||
| t.done(); | ||
| } | ||
| ); | ||
| }, | ||
| 'should include a defined item that came with error': function(t) { | ||
| var items = [1,2,3,4]; | ||
| t.expect(1); | ||
| qflow.filter( | ||
| items, | ||
| function(item, cb) { cb((item === 3 ? new Error("three") : null), item); }, | ||
| function(err, picked) { | ||
| t.equal(picked[2], 3); | ||
| t.done(); | ||
| } | ||
| ); | ||
| }, | ||
| }, | ||
| }; |
23945
127.77%487
109.01%176
137.84%1
Infinity%