From 83ded8229360e06fdaa39d1aa1fb288c2d0938f1 Mon Sep 17 00:00:00 2001 From: JT Turner Date: Mon, 25 Jan 2016 18:13:09 -0800 Subject: [PATCH] Add reflect that wraps a function with a always passing callback and a object with error or value property set. This is one way to solve issue #942. --- README.md | 89 +++++++++++++++++++++++-- lib/async.js | 34 +++++++++- test/test-async.js | 159 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 277 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index d731b82b1..e87aa0180 100644 --- a/README.md +++ b/README.md @@ -229,6 +229,8 @@ Some functions are also available in the following forms: * [`log`](#log) * [`dir`](#dir) * [`noConflict`](#noConflict) +* [`reflect`](#reflect) +* [`reflectAll`](#reflectAll) ## Collections @@ -1446,7 +1448,7 @@ __Arguments__ * `opts` - Can be either an object with `times` and `interval` or a number. * `times` - The number of attempts to make before giving up. The default is `5`. * `interval` - The time to wait between retries, in milliseconds. The default is `0`. - * If `opts` is a number, the number specifies the number of times to retry, with the default interval of `0`. + * If `opts` is a number, the number specifies the number of times to retry, with the default interval of `0`. * `task(callback, results)` - A function which receives two arguments: (1) a `callback(err, result)` which must be called when finished, passing `err` (which can be `null`) and the `result` of the function's execution, and (2) a `results` object, containing the results of @@ -1464,14 +1466,14 @@ async.retry(3, apiMethod, function(err, result) { ``` ```js -// try calling apiMethod 3 times, waiting 200 ms between each retry +// try calling apiMethod 3 times, waiting 200 ms between each retry async.retry({times: 3, interval: 200}, apiMethod, function(err, result) { // do something with the result }); ``` ```js -// try calling apiMethod the default 5 times no delay between each retry +// try calling apiMethod the default 5 times no delay between each retry async.retry(apiMethod, function(err, result) { // do something with the result }); @@ -1792,7 +1794,7 @@ async.waterfall([ return db.model.create(contents); }), function (model, next) { - // `model` is the instantiated model object. + // `model` is the instantiated model object. // If there was an error, this function would be skipped. } ], callback) @@ -1875,3 +1877,82 @@ node> async.dir(hello, 'world'); Changes the value of `async` back to its original value, returning a reference to the `async` object. + +--------------------------------------- + + +### reflect(function) + +Wraps the function in another function that always returns data even when it errors. +The object returns ether has a property of error or value. + +__Arguments__ + +* `function` - The function you want to wrap + +__Example__ + +```js +async.parallel([ + async.reflect(function(callback){ + // do some stuff ... + callback(null, 'one'); + }), + async.reflect(function(callback){ + // do some more stuff but error ... + callback('bad stuff happened'); + }), + async.reflect(function(callback){ + // do some more stuff ... + callback(null, 'two'); + }) +], +// optional callback +function(err, results){ + // values + // results[0].value = 'one' + // results[1].error = 'bad stuff happened' + // results[2].value = 'two' +}); +``` + +--------------------------------------- + + +### reflectAll() + +A helper function that wraps an array of functions with reflect. + +__Arguments__ + +* `tasks` - The array of functions to wrap in reflect. + +__Example__ + +```javascript +let tasks = [ + function(callback){ + setTimeout(function(){ + callback(null, 'one'); + }, 200); + }, + function(callback){ + // do some more stuff but error ... + callback(new Error('bad stuff happened')); + } + function(callback){ + setTimeout(function(){ + callback(null, 'two'); + }, 100); + } +]; + +async.parallel(async.reflectAll(tasks), +// optional callback +function(err, results){ + // values + // results[0].value = 'one' + // results[1].error = Error('bad stuff happened') + // results[2].value = 'two' +}); +``` diff --git a/lib/async.js b/lib/async.js index dbc56825f..3bbc7e82d 100644 --- a/lib/async.js +++ b/lib/async.js @@ -1090,7 +1090,7 @@ var memoized = _restParam(function memoized(args) { var callback = args.pop(); var key = hasher.apply(null, args); - if (has.call(memo, key)) { + if (has.call(memo, key)) { async.setImmediate(function () { callback.apply(null, memo[key]); }); @@ -1247,6 +1247,38 @@ }); }; + async.reflect = function(fn) { + return function reflectOn() { + var args = Array.prototype.slice.call(arguments); + var reflectCallback = args.pop(); + + args.push(function callback(err) { + if (err) { + reflectCallback(null, { + error: err + }); + } else { + var cbArgs = Array.prototype.slice.call(arguments, 1); + var value = null; + if (cbArgs.length === 1) { + value = cbArgs[0]; + } else if (cbArgs.length > 1) { + value = cbArgs; + } + reflectCallback(null, { + value: value + }); + } + }); + + return fn.apply(this, args); + }; + }; + + async.reflectAll = function(tasks) { + return tasks.map(async.reflect); + }; + // Node.js if (typeof module === 'object' && module.exports) { module.exports = async; diff --git a/test/test-async.js b/test/test-async.js index e6fe8effd..e3fa9d9a4 100755 --- a/test/test-async.js +++ b/test/test-async.js @@ -906,6 +906,40 @@ exports['parallel'] = function(test){ }); }; +exports['parallel with reflect'] = function(test){ + var call_order = []; + async.parallel([ + async.reflect(function(callback){ + setTimeout(function(){ + call_order.push(1); + callback(null, 1); + }, 50); + }), + async.reflect(function(callback){ + setTimeout(function(){ + call_order.push(2); + callback(null, 2); + }, 100); + }), + async.reflect(function(callback){ + setTimeout(function(){ + call_order.push(3); + callback(null, 3,3); + }, 25); + }) + ], + function(err, results){ + test.ok(err === null, err + " passed instead of 'null'"); + test.same(call_order, [3,1,2]); + test.same(results, [ + { value: 1 }, + { value: 2 }, + { value: [3,3] } + ]); + test.done(); + }); +}; + exports['parallel empty array'] = function(test){ async.parallel([], function(err, results){ test.ok(err === null, err + " passed instead of 'null'"); @@ -929,6 +963,29 @@ exports['parallel error'] = function(test){ setTimeout(test.done, 100); }; +exports['parallel error with reflect'] = function(test){ + async.parallel([ + async.reflect(function(callback){ + callback('error', 1); + }), + async.reflect(function(callback){ + callback('error2', 2); + }), + async.reflect(function(callback){ + callback(null, 2); + }) + ], + function(err, results){ + test.ok(err === null, err + " passed instead of 'null'"); + test.same(results, [ + { error: 'error' }, + { error: 'error2' }, + { value: 2 } + ]); + test.done(); + }); +}; + exports['parallel no callback'] = function(test){ async.parallel([ function(callback){callback();}, @@ -1155,6 +1212,40 @@ exports['series'] = { }); }, + 'with reflect': function(test){ + var call_order = []; + async.series([ + async.reflect(function(callback){ + setTimeout(function(){ + call_order.push(1); + callback(null, 1); + }, 25); + }), + async.reflect(function(callback){ + setTimeout(function(){ + call_order.push(2); + callback(null, 2); + }, 50); + }), + async.reflect(function(callback){ + setTimeout(function(){ + call_order.push(3); + callback(null, 3,3); + }, 15); + }) + ], + function(err, results){ + test.ok(err === null, err + " passed instead of 'null'"); + test.deepEqual(results, [ + { value: 1 }, + { value: 2 }, + { value: [3,3] } + ]); + test.same(call_order, [1,2,3]); + test.done(); + }); +}, + 'empty array': function(test){ async.series([], function(err, results){ test.equals(err, null); @@ -1180,6 +1271,30 @@ exports['series'] = { setTimeout(test.done, 100); }, + 'error with reflect': function(test){ + test.expect(2); + async.series([ + async.reflect(function(callback){ + callback('error', 1); + }), + async.reflect(function(callback){ + callback('error2', 2); + }), + async.reflect(function(callback){ + callback(null, 1); + }) + ], + function(err, results){ + test.ok(err === null, err + " passed instead of 'null'"); + test.deepEqual(results, [ + { error: 'error' }, + { error: 'error2' }, + { value: 1 } + ]); + test.done(); + }); +}, + 'no callback': function(test){ async.series([ function(callback){callback();}, @@ -1841,6 +1956,50 @@ exports['map'] = { }); }, + 'with reflect': function(test){ + var call_order = []; + async.map([1,3,2], async.reflect(function(item, cb) { + setTimeout(function(){ + call_order.push(item); + cb(null, item*2); + }, item*25); + }), function(err, results){ + test.ok(err === null, err + " passed instead of 'null'"); + test.same(call_order, [1,2,3]); + test.same(results, [ + { value: 2 }, + { value: 6 }, + { value: 4 } + ]); + test.done(); + }); +}, + + 'error with reflect': function(test){ + var call_order = []; + async.map([-1,1,3,2], async.reflect(function(item, cb) { + setTimeout(function(){ + call_order.push(item); + if (item < 0) { + cb('number less then zero'); + } else { + cb(null, item*2); + } + + }, item*25); + }), function(err, results){ + test.ok(err === null, err + " passed instead of 'null'"); + test.same(call_order, [-1,1,2,3]); + test.same(results, [ + { error: 'number less then zero' }, + { value: 2 }, + { value: 6 }, + { value: 4 } + ]); + test.done(); + }); +}, + 'map original untouched': function(test){ var a = [1,2,3]; async.map(a, function(x, callback){