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){