Skip to content

Commit

Permalink
Add reflect that wraps a function with a always passing callback and …
Browse files Browse the repository at this point in the history
…a object with error or value property set.

This is one way to solve issue caolan#942.
  • Loading branch information
jtwebman committed Feb 18, 2016
1 parent 01205e0 commit 83ded82
Show file tree
Hide file tree
Showing 3 changed files with 277 additions and 5 deletions.
89 changes: 85 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,8 @@ Some functions are also available in the following forms:
* [`log`](#log)
* [`dir`](#dir)
* [`noConflict`](#noConflict)
* [`reflect`](#reflect)
* [`reflectAll`](#reflectAll)
## Collections
Expand Down Expand Up @@ -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
Expand All @@ -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
});
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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.
---------------------------------------
<a name="reflect"></a>
### 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'
});
```

---------------------------------------

<a name="reflectAll"></a>
### 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'
});
```
34 changes: 33 additions & 1 deletion lib/async.js
Original file line number Diff line number Diff line change
Expand Up @@ -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]);
});
Expand Down Expand Up @@ -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;
Expand Down
159 changes: 159 additions & 0 deletions test/test-async.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'");
Expand All @@ -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();},
Expand Down Expand Up @@ -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);
Expand All @@ -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();},
Expand Down Expand Up @@ -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){
Expand Down

0 comments on commit 83ded82

Please sign in to comment.